Search docs.molo17.com
    Configuration guide Connectors

    Set up a ClickHouse target for snapshots and CDC

    ClickHouse does not update or delete rows in place. Gluesync works with that design: on a ReplacingMergeTree table, every insert, update and delete becomes a new row with a higher version, and a delete is a row marked as deleted. This guide creates a ClickHouse user with only the grants Gluesync needs, creates the table that fits this model, copies a PostgreSQL table with a snapshot and keeps it in sync with CDC, shows how an update and a delete look in the table, and lists the errors you are most likely to meet on a new setup.

    In this guide

    You will create a database and a user with least grants, connect the ClickHouse target, create a ReplacingMergeTree(_version, _is_deleted) table and map a source table to it, run a snapshot, and follow inserts, updates and deletes with CDC. You will read the same table with and without FINAL, check it in Query Studio, tune the batch size and the writers, and see what changes when you write into a table that is not a ReplacingMergeTree. A table at the end lists the errors that were reproduced for this guide.

    Quick information

    Goal

    Replicate a PostgreSQL table to ClickHouse with a snapshot, then keep it in sync with change data capture (CDC), and read the current rows

    Audience

    Data engineers and database administrators who own a ClickHouse server

    Level

    Intermediate

    Time required

    About 40 minutes

    Area

    ClickHouse → SQL; Control Plane → Pipelines → ClickHouse target → Base settings and Advanced settings; entity → Edit settings

    Applies to

    Gluesync 2.3

    Before you begin

    Make sure that:

    1. You have a ClickHouse server that Core Hub can reach over HTTP on port 8123, or over HTTPS, and you can run SQL on it as an administrator. The Control Plane shows 24.X as the minimum supported version. This guide was tested with ClickHouse 26.8. ClickHouse is a target only: you need another database as the source;

    2. You have a source that Core Hub can read with CDC. This guide uses PostgreSQL 16 with wal_level = logical, set up as in Set up a PostgreSQL WAL source;

    3. You can sign in to the Control Plane with a role that can create pipelines;

    4. You have a source table with a primary key. This guide uses a PostgreSQL table public.orders with 200,000 rows. Its columns are id bigint (primary key), customer varchar(50), amount numeric(12,2), status varchar(20), created_at timestamptz, ship_date date, paid boolean, tags text[] and note text.

    The statements and the messages quoted in this guide were run and captured against ClickHouse 26.8 and the Control Plane that ships with Gluesync 2.3.

    Guide steps

    Create the database and the user, and grant what the target needs

    SQL

    Gluesync does not need an administrator. Connect to ClickHouse as an administrator and run the statements below. Replace the password.

    1. Create the database and the user:

      CREATE DATABASE IF NOT EXISTS analytics;
      CREATE USER gluesync IDENTIFIED BY '<a strong password>';
    2. Give the user the rights on the target database. SELECT lets Gluesync read the metadata of the tables, INSERT is the write path, TRUNCATE is used by the snapshot option Empty the table first, and CREATE TABLE lets Gluesync run the statement of the Create table dialog:

      GRANT SELECT, INSERT, TRUNCATE, CREATE TABLE ON analytics.* TO gluesync;
    3. Give the user the three read rights that schema discovery uses. Without them the object browser cannot list the target:

      GRANT SELECT ON information_schema.schemata TO gluesync;
      GRANT SELECT ON information_schema.tables TO gluesync;
      GRANT SELECT ON system.parts TO gluesync;
    4. Check the result:

      SHOW GRANTS FOR gluesync;
    Expected result

    The statement returns the grants on analytics.*, on the two information_schema views and on system.parts. Gluesync did not create any helper database: after the whole guide, SHOW DATABASES lists only your own databases and the system ones.

    ALTER DELETE is not in the list on purpose. A table that is a ReplacingMergeTree with _version and _is_deleted receives a delete as a new row, so it needs only INSERT. Grant ALTER DELETE only if you write into a table of another engine. Step 8 shows why.

    Create the pipeline and the ClickHouse target connection

    UI

    1. In the Control Plane, select Create your first pipeline, or the + button of the pipeline list if you already have pipelines. The Agents setup wizard opens;

    2. In 1. Setup source, choose the PostgreSQL CDC agent tile and fill Base settings: Host name / Address, Port, Database name, Username and Password of your source database. Select Test and continue, then Next;

    3. In 2. Setup target(s), choose the ClickHouse agent tile and fill Base settings:

      • Host name / Address: your ClickHouse server;

      • Port: 8123, the HTTP port;

      • Database name: analytics;

      • Username and Password: the user of step 1;

    4. Select Test and continue. The target appears with the user and the address. Select Next, enter a Pipeline name and select Finish.

    Setup target page of the ClickHouse agent with host
    Figure 1. The ClickHouse target connection. Under Advanced connection properties, the Key list offers the connection options, including HTTP compression and ClickHouse async inserts.

    For TLS, turn on Enable TLS and, for a certificate that Core Hub does not trust, Trust server certificate. The port is then the HTTPS port of your server. The ClickHouse target reference lists every option and the REST API setup.

    Expected result

    The pipeline appears in the HEALTHY group of the pipeline list, with the PostgreSQL and the ClickHouse icons. The connection test passes even when the user cannot yet read the metadata: the missing information_schema grants show up in step 4, when you list the objects.

    Create the target table

    SQL

    A target table that Gluesync can write with versions and tombstones is a ReplacingMergeTree whose last two engine arguments are _version and _is_deleted, ordered by the key of the source table. The two columns are owned by the connector: Gluesync writes _version as epoch nanoseconds on every row and _is_deleted as 1 on a delete.

    The Create table dialog of the entity step proposes such a table. Its statement lists every column as Nullable(…​), adds the two technical columns and a PRIMARY KEY on the source key:

    `_version` UInt64 DEFAULT toUnixTimestamp64Nano(now64(9)),
    `_is_deleted` UInt8 DEFAULT 0,
    PRIMARY KEY (`id`)

    On a default ClickHouse server, running that statement fails, because the key column is proposed as Nullable(Int64) and a MergeTree sorting key cannot be nullable. The notification Create Target Table shows:

    Create target table execution error: Code: 44. DB::Exception: Sorting key contains nullable columns, but merge tree setting `allow_nullable_key` is disabled. (ILLEGAL_COLUMN)

    Create the table yourself instead. This also lets you choose the sorting key, the partitioning and the types:

    CREATE TABLE analytics.orders
    (
        id Int64,
        customer Nullable(String),
        amount Nullable(Decimal(12, 2)),
        status LowCardinality(Nullable(String)),
        created_at Nullable(DateTime64(6, 'UTC')),
        ship_date Nullable(Date32),
        paid Nullable(Bool),
        tags Array(Nullable(String)),
        note Nullable(String),
        `_version` UInt64 DEFAULT toUnixTimestamp64Nano(now64(9)),
        `_is_deleted` UInt8 DEFAULT 0
    )
    ENGINE = ReplacingMergeTree(`_version`, `_is_deleted`)
    ORDER BY id;
    Expected result

    The table exists in analytics. The key column id is not nullable, and the ORDER BY lists the same column or columns as the key of the source table.

    Every entity needs a key. A source table without a primary key is rejected in the next step with the errors No key on the source and No key on the target, unless you choose the key columns with Check keys.

    Add the entity and map the table

    UI

    1. In the pipeline, select Entities. The Add entities page opens with the Object browser of the source. Expand the schema public and select the table orders. The Field mapping dialog opens;

    2. Under Target, select the Target Schema analytics. The list shows only the databases that the Gluesync user can see. Then select the Target Table orders from the list of existing tables. The dialog draws one arrow for each source column;

    3. The technical columns _version and _is_deleted appear in the target with no arrow, and the dialog shows the error Field function is required or define a UDF. Clear the check box at the start of both rows. The connector fills them, so they must not be mapped;

    4. Select Confirm, then Confirm and continue.

    Field mapping dialog with the source table orders mapped to analytics.orders
    Figure 2. The mapping of the existing table. The two technical columns are cleared, so the dialog shows no errors.
    Expected result

    The entity appears in the pipeline on Hold as public.orders. The target types follow the source: bigint is Int64, varchar(n) is String, numeric(12,2) is Decimal(12, 2), timestamptz is DateTime64(6, 'UTC'), date is Date32, boolean is Bool and text[] is an array.

    If you let Gluesync create a table for a source column of type text, the Length of the String column shows 0 with the error Missing required field. Clear the field with Ctrl+A and Delete, then press the down arrow once. The value becomes -1, which means no limit, and the error goes away.

    Run the snapshot

    UI + SQL

    1. On Hold, select the start options button, the one next to the play button. The Snapshot (copy-only) dialog opens;

    2. Keep Keep and merge under Data already in the target. It writes every row again as a new version of its key. Empty the table first runs TRUNCATE TABLE before the copy, needs the TRUNCATE grant, and asks you to confirm with I understand that rows in the target will be deleted;

    3. Select Start snapshot.

    The dialog has a Speed section. Under Custom, the Write method of this target is Standard with the label This target has no bulk load, and you can set the number of Writers during the copy. Step 7 shows what the number changes.

    Check the target when the snapshot ends:

    SELECT count() FROM analytics.orders;
    SELECT count() AS n, uniqExact(id) AS keys FROM analytics.orders FINAL;
    Expected result

    Both counts are 200000, the number of rows of the source. The entity goes back to Hold, and the notification Snapshot finished for public.orders appears.

    Capture live changes and see how updates and deletes land

    SQL

    1. Select the play button of the entity. The Sync changes (CDC) dialog opens. Keep Where it stopped, which continues from the last checkpoint, and select Start entity. The entity becomes Active;

    2. On the source, insert, update and delete one row each:

      INSERT INTO orders VALUES (300001, 'cdc-customer', 12.34, 'NEW', now(), current_date, true, ARRAY['x','y'], 'inserted');
      UPDATE orders SET status = 'SHIPPED', amount = 999.99 WHERE id = 5;
      DELETE FROM orders WHERE id = 6;
    3. After a few seconds, read the three keys twice, as stored and as current:

      SELECT id, status, amount, _is_deleted FROM analytics.orders WHERE id IN (5, 6, 300001) ORDER BY id, _version;
      SELECT id, status, amount FROM analytics.orders FINAL WHERE id IN (5, 6, 300001) ORDER BY id;
      SELECT count() FROM analytics.orders FINAL;
    Expected result

    The first query returns the physical rows. Row 5 has two versions: the old amount and 999.99. Row 6 has the original row and a second row with only the key, NULL in every other column and _is_deleted set to 1. That row is the tombstone of the delete. Row 300001 has one version.

    The second query returns the current rows: row 5 with 999.99, row 300001, and no row 6. The count is 200000: the 200,000 rows, plus the insert, minus the delete.

    In short:

    • an insert or an update adds a row with a higher _version and _is_deleted = 0;

    • a delete adds a row with the key, a higher _version and _is_deleted = 1;

    • FINAL returns the last version of each key and hides the keys whose last version is a tombstone;

    • the old versions stay in the table until ClickHouse merges the parts, and the tombstones stay until a merge that cleans them. A plain SELECT therefore counts rows that FINAL does not.

    The Control Plane reads the table in the way that FINAL does, so a count in Query Studio matches the current rows. Step 6 shows it.

    Check the table in Query Studio

    UI

    1. In the left bar, select Query Studio. At the top right, open the SQL-capable Agent list and choose the ClickHouse target;

    2. Enter a query and select Run:

      SELECT count() AS n, uniqExact(id) AS keys FROM analytics.orders
    Query Studio on the ClickHouse target with a count query and the result n equal to keys
    Figure 3. A count in Query Studio on the ClickHouse target. The result is the number of current rows, not the number of stored versions.
    Expected result

    n and keys are equal. In the test, after several snapshots, a plain count() in a ClickHouse client returned about 2 million physical rows, and the query in Query Studio returned 1199999, the number of current keys.

    To compare the target with the source key by key, use Find and fix row differences between source and target.

    Tune the writes

    UI

    This target has no bulk loader. Each flush is a multi-row INSERT …​ VALUES, and the connector writes a transaction in its own order. You can tune three things.

    1. Batch size. Pause the entity, open the three-dot menu of the entity, select Edit settings, then the Entity tab. Bulk insert batch size (10000 by default) is the number of rows in one INSERT. Delete mutation batch size (1000 by default) is used only for a table written in append mode, see step 8;

    2. Writers. In the Snapshot (copy-only) dialog, under Speed and Custom, set Writers during the copy (1 to 10). The entity tab Fine tuning has a matching setting, Snapshot writing concurrency, with the same default of 2 and the same range;

    3. Connection options. In the Advanced connection properties of the target, add Enable HTTP compression (requests and responses) (on by default) or Enable ClickHouse async inserts with Async insert flush interval (ms). The option Use RowBinary for prepared INSERT batches is a driver beta for ClickHouse 25.x and older: leave it off.

    Settings dialog of the entity with the Entity tab open
    Figure 4. The entity settings of a ClickHouse entity: bulk insert batch size and delete mutation batch size.
    Expected result

    Measured on one laptop with Core Hub, PostgreSQL and ClickHouse in containers, a snapshot of 1.2 million rows of this table with Empty the table first took 16.7 seconds with 2 writers, 18.5 seconds with 4 writers and parallel reading, and 21.3 seconds with 8 writers. More writers did not make this copy faster, because the source read and Core Hub were the limit, so change one setting at a time and measure on your own system. ClickHouse 26.8 has async_insert on by default, so even the default inserts appear in its query log as AsyncInsertFlush.

    Write into a table that is not a ReplacingMergeTree

    SQL + UI

    You can map an entity to a table of another engine, for example a plain MergeTree that you already have. Gluesync then writes in append mode. This is what happens, reproduced with a second source table public.customers and this target table:

    CREATE TABLE analytics.customers_append (id Int32, name Nullable(String), email Nullable(String))
    ENGINE = MergeTree ORDER BY id;

    After the first write, the notification center shows the warning ClickHouse table analytics.customers_append is written in append mode. It explains that every update is added as a new row and every delete runs as a DELETE mutation, and how to recreate the table as a ReplacingMergeTree.

    Update, delete and insert one row each on the source, then read the table:

    SELECT * FROM analytics.customers_append WHERE id IN (1, 2, 2001) ORDER BY id;
    Expected result

    Row 1 appears twice: the old name and the new one. Row 2 is gone, because the delete ran as DELETE FROM analytics.customers_append WHERE id IN (2). Row 2001 appears once. A query without FINAL shows the duplicate of row 1, and FINAL does not remove it, because the table has no versions to collapse.

    The delete needs the ALTER DELETE grant. Without it the entity goes to Error. The Access Denied notification shows Code: 497. DB::Exception: gluesync: Not enough privileges. To execute this query, it’s necessary to have the grant ALTER DELETE ON analytics.customers_append. (ACCESS_DENIED). Run GRANT ALTER DELETE ON analytics.* TO gluesync;, and the entity writes again on its next retry. In the same test, a ReplacingMergeTree entity of the same pipeline kept receiving updates and deletes without that grant.

    Fix the errors you are most likely to meet

    SQL + UI

    Each error below was reproduced by removing one grant or changing one setting.

    1. Remove one of the three discovery grants of step 1 and open Add entities. The object browser stays empty and the notification Schemas Fetch shows Code: 497. DB::Exception: gluesync: Not enough privileges. To execute this query, it’s necessary to have the grant SELECT ON information_schema.schemata. (ACCESS_DENIED). Without the grant on information_schema.tables, the message names system.parts, because the view reads that table. Fix: run the three GRANT SELECT statements;

    2. Remove TRUNCATE, and start a snapshot with Empty the table first. The entity goes to Error, retries every few seconds, and the notification Access Denied shows …​ it’s necessary to have the grant TRUNCATE ON analytics.orders. (ACCESS_DENIED). Fix: GRANT TRUNCATE ON analytics.* TO gluesync;. The next retry runs the snapshot;

    3. Save the connection with a wrong password. The page shows Failed to save: The connection was saved, but agent <id> of pipeline <id> did not accept the new credentials: Code: 516. DB::Exception: gluesync: Authentication failed: password is incorrect, or there is no user with such name. (AUTHENTICATION_FAILED). A wrong port or an unknown host gives …​ did not accept the new credentials: Query request failed (attempt: 4, duration: 6ms, queryId: <id>), and a database that does not exist gives Code: 81. DB::Exception: Database nodb does not exist. (UNKNOWN_DATABASE). The wrong values are saved, and the pipeline shows On error with Error: Unknown error. Please check agent clickhouse (<id>) logs in order to find more details. Fix: save the correct values with Test and continue, and the pipeline becomes healthy again;

    4. Create a table from the Create table dialog and get Sorting key contains nullable columns: create the table yourself, as in step 3.

    Pipeline page with the entity public.customers in Error and an Access Denied notification that names the missing ALTER DELETE grant
    Figure 5. A missing grant. The entity of the append-mode table is in Error, and the notification names the grant that ClickHouse asks for.
    Expected result

    After each fix, the entity or the pipeline works again without a restart of Core Hub.

    Data types

    The default types that Gluesync proposes for a PostgreSQL source, as seen in the Field mapping dialog and in the table that you read back:

    Source type ClickHouse type Note

    bigint, integer, smallint

    Int64, Int32, Int16

    Values written unchanged

    numeric(p,s)

    Decimal(p, s)

    Kept exactly, for example 0.01 and 999.99

    varchar(n), text

    String

    text has no length limit. Set the Length to -1 if the dialog asks for one

    boolean

    Bool

    true and false

    timestamptz

    DateTime64(6, 'UTC')

    Microseconds and the UTC instant are kept

    date

    Date32

    The date

    text[]

    Array(Nullable(String))

    A NULL array is stored as an empty array, because ClickHouse cannot make an array nullable

    The ClickHouse target reference lists the alternatives for each Gluesync type, such as Float32, FixedString, UUID and Time64.

    Bulk and throughput settings

    Setting Default When to change it

    Bulk insert batch size (entity)

    10000

    Raise it for wide tables with small rows, lower it when a single INSERT is too large for the server

    Delete mutation batch size (entity)

    1000

    Only for tables written in append mode: the number of keys in one DELETE

    Writers during the copy (snapshot)

    2

    Try it when the source and Core Hub are not the limit. In the test it did not help

    Enable HTTP compression (requests and responses)

    On

    Turn it off only on a very fast local network

    Enable ClickHouse async inserts

    Off

    Turn it on to let the server group small inserts. The setting Async insert flush interval (ms) is 1000 by default

    Disable truncate on target table before snapshot inserts

    Off

    An Advanced settings property of the target. Turn it on when the snapshot must never truncate the table

    Read Tune replication throughput for the settings that apply to every target. TLS for the HTTP connection is in Enable HTTPS and TLS to databases.

    Verify the result

    You have set up the ClickHouse target if:

    • The user has SELECT, INSERT, TRUNCATE and CREATE TABLE on the target database, and SELECT on information_schema.schemata, information_schema.tables and system.parts;

    • The target table is a ReplacingMergeTree(_version, _is_deleted) ordered by the key, with a key column that is not nullable;

    • The field mapping has no error, and _version and _is_deleted are not mapped;

    • The count with FINAL equals the source count after the snapshot;

    • A CDC insert, update and delete reach the table as new versions and a tombstone, and FINAL shows only the current rows;

    • You know which tables, if any, are written in append mode, and the user has ALTER DELETE on them.

    Troubleshooting

    Problem Possible cause What to do

    Schemas Fetch fails with Not enough privileges …​ SELECT ON information_schema.schemata or …​ SELECT ON system.parts

    The user cannot read the metadata views that the object browser uses

    GRANT SELECT ON information_schema.schemata TO <user>; and the same for information_schema.tables and system.parts

    Create Target Table fails with Sorting key contains nullable columns, but merge tree setting allow_nullable_key is disabled

    The Create table dialog proposes every column, key included, as Nullable

    Create the table yourself with a non-nullable key, as in step 3, and select it in Target Table

    The Field mapping dialog shows Field function is required or define a UDF for _version and _is_deleted

    The two technical columns of the target table are not mapped

    Clear the check box of both rows

    The Length of a String column shows 0 with Missing required field

    The source column is text and has no length

    Clear the field and press the down arrow once to get -1

    No key on the source and No key on the target

    The source table has no primary key

    Choose the key columns with Check keys, or add a primary key to the source

    Access Denied with …​ grant TRUNCATE ON <table>

    The snapshot option Empty the table first needs TRUNCATE

    GRANT TRUNCATE ON <database>.* TO <user>;

    Access Denied with …​ grant ALTER DELETE ON <table>

    The table is written in append mode, and a delete is a DELETE mutation

    GRANT ALTER DELETE ON <database>.* TO <user>;, or recreate the table as a ReplacingMergeTree(_version, _is_deleted)

    Warning …​ is written in append mode

    The target table is not a ReplacingMergeTree with _version and _is_deleted as its last two arguments

    Recreate the table with the two columns and the engine of step 3, then run a new snapshot

    Failed to save with Authentication failed: password is incorrect, or there is no user with such name

    Wrong user or password. The values are saved anyway

    Correct them and select Test and continue

    Failed to save with Query request failed (attempt: …​)

    Wrong host or port, or a server that is not reachable from Core Hub

    Check the HTTP port (8123 by default, or the HTTPS port with Enable TLS) and the network

    Failed to save with Database <name> does not exist (UNKNOWN_DATABASE)

    Wrong Database name

    Create the database or correct the name

    A SELECT count() is larger than the source

    ReplacingMergeTree keeps old versions and tombstones until a merge

    Read the table with FINAL, or use Query Studio

    Contact MOLO17 Support if you need assistance with a ClickHouse target.