Skip to content

Replicate MySQL to DuckDB with CDC ​

This walkthrough takes you from a running MySQL database to a DuckDB file kept in sync through Change Data Capture: an initial snapshot, then inserts, updates, and deletes picked up by re-running ingestr. It assumes you already have a MySQL server you can connect to and ingestr installed.

For the full reference on the connector's options, see MySQL → Change data capture.

1. Prepare the source ​

MySQL CDC reads the binary log, so it must be enabled in ROW format with full row images. Check the current settings:

sql
SELECT @@log_bin, @@binlog_format, @@binlog_row_image;

You want @@log_bin = 1, @@binlog_format = ROW, and @@binlog_row_image = FULL. On MySQL 8.0 these are the defaults; if not, set them in your server config (log_bin, binlog_format=ROW, binlog_row_image=FULL) and restart.

The connecting user needs read access plus replication privileges. A dedicated user is straightforward:

sql
CREATE USER 'ingestr_cdc'@'%' IDENTIFIED BY 'cdcpass';
GRANT SELECT, RELOAD, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'ingestr_cdc'@'%';
FLUSH PRIVILEGES;

SELECT and RELOAD cover the consistent initial snapshot; REPLICATION SLAVE and REPLICATION CLIENT cover streaming the binary log.

We'll replicate a small customers table. Create one to follow along (or point the commands at a table of your own — it just needs a primary key):

sql
CREATE TABLE customers (
    id    INT PRIMARY KEY,
    name  VARCHAR(100) NOT NULL,
    email VARCHAR(255)
);
INSERT INTO customers (id, name, email) VALUES
    (1, 'Alice', 'alice@example.com'),
    (2, 'Bob',   'bob@example.com'),
    (3, 'Carol', 'carol@example.com');

2. Run the initial load ​

Run ingestr with the mysql+cdc:// scheme to take the initial snapshot into DuckDB. Pin a unique server_id so the connector has a stable replication identity:

bash
ingestr ingest \
  --source-uri "mysql+cdc://ingestr_cdc:cdcpass@localhost:3306/shop?server_id=1001" \
  --source-table "customers" \
  --dest-uri "duckdb:///warehouse.duckdb" \
  --dest-table "shop.customers"

The run snapshots the table, records its binary-log position, and exits. Inspect the result:

bash
duckdb warehouse.duckdb "SELECT id, name, email, _cdc_deleted FROM shop.customers ORDER BY id;"
plaintext
┌───────┬─────────┬───────────────────┬──────────────┐
│  id   │  name   │       email       │ _cdc_deleted │
├───────┼─────────┼───────────────────┼──────────────┤
│     1 │ Alice   │ alice@example.com │ false        │
│     2 │ Bob     │ bob@example.com   │ false        │
│     3 │ Carol   │ carol@example.com │ false        │
└───────┴─────────┴───────────────────┴──────────────┘

Alongside your columns, ingestr adds the CDC metadata columns _cdc_lsn, _cdc_deleted, and _cdc_synced_at.

3. Capture ongoing changes ​

Change the source:

sql
INSERT INTO customers (id, name, email) VALUES (4, 'Dave', 'dave@example.com');
UPDATE customers SET email = 'alice@newmail.com' WHERE id = 1;
DELETE FROM customers WHERE id = 2;

Now run exactly the same command again. Instead of re-snapshotting, ingestr resumes from the binary-log position stored in the destination's maximum _cdc_lsn and applies only what changed:

bash
ingestr ingest \
  --source-uri "mysql+cdc://ingestr_cdc:cdcpass@localhost:3306/shop?server_id=1001" \
  --source-table "customers" \
  --dest-uri "duckdb:///warehouse.duckdb" \
  --dest-table "shop.customers"
bash
duckdb warehouse.duckdb "SELECT id, name, email, _cdc_deleted FROM shop.customers ORDER BY id;"
plaintext
┌───────┬─────────┬───────────────────┬──────────────┐
│  id   │  name   │       email       │ _cdc_deleted │
├───────┼─────────┼───────────────────┼──────────────┤
│     1 │ Alice   │ alice@newmail.com │ false        │
│     2 │ Bob     │ bob@example.com   │ true         │
│     3 │ Carol   │ carol@example.com │ false        │
│     4 │ Dave    │ dave@example.com  │ false        │
└───────┴─────────┴───────────────────┴──────────────┘

The insert and update are applied, and the delete is a soft delete: id = 2 stays in DuckDB with _cdc_deleted = true and its last known values intact. Filter it out at query time with WHERE _cdc_deleted = false. Schedule this same command (for example with cron) to keep the destination continuously up to date; each run picks up where the previous one stopped.

4. Start over if needed ​

If the saved position is no longer available in MySQL's binary logs (for example the logs were purged), the run fails rather than taking a partial snapshot. To discard the destination state and rebuild from a fresh snapshot, add --full-refresh:

bash
ingestr ingest \
  --source-uri "mysql+cdc://ingestr_cdc:cdcpass@localhost:3306/shop?server_id=1001" \
  --source-table "customers" \
  --dest-uri "duckdb:///warehouse.duckdb" \
  --dest-table "shop.customers" \
  --full-refresh

See also ​