ROW format and streams
each changed row into the sink. A raw pipeline maps each table row to one
sink document keyed by the table’s primary key. Projection pipelines can embed
related tables through the same join specification used by PostgreSQL.
MariaDB is supported through the same binlog protocol — set
VS_CDC_SOURCE=mariadb (an alias) or mysql.
VentStream writes to a target. OpenSearch and Elasticsearch are the
supported targets today — they share the
_bulk API, so the same engine
handles both. Set the endpoint with VS_OS_ENDPOINT.Source requirements
binlog_format=ROW(withbinlog_row_image=FULL, the default). Statement or mixed formats don’t carry per-row images.- A
server_idunique across the replication topology — the agent registers as a replica. Set it withVS_MYSQL_SERVER_ID. - The login user needs
REPLICATION SLAVEandREPLICATION CLIENTplusSELECTon the watched tables.
How it works
- Trigger, then re-read. A binlog event is only a trigger. The source
re-
SELECTs the current row by primary key and serializes that, so the sink always reflects the latest committed state — the binlog value images are never decoded directly. This keeps column types correct and avoids JSON/blob internals. - Bootstrap. On cold start the source reads
SHOW MASTER STATUSto pin the current binlog position, keyset-paginates every in-scope table by primary key (WHERE (pk) > (?) ORDER BY pk LIMIT n), emits each row as an insert, then tails the binlog from the pinned position. A row mutated during the scan is re-emitted by the tail and de-duplicated by the deterministic doc id at the (idempotent) sink. - Resume. The binlog position (
file:pos) is persisted to a file inVS_MYSQL_STATE_DIR(a PVC in Kubernetes), flushed everyVS_MYSQL_POS_FLUSH_MS. The source places an internal acknowledgement barrier after all output derived from a binlog position. The dispatcher consumes that barrier and confirms it only after the ordered sink batch prefix is durable; the barrier is never written as a customer document. MySQL sink batches are serialized so two changes to the same document cannot land out of order across parallel bulk requests. Batch size and recomposition concurrency still provide throughput without weakening document ordering. A crash before confirmation replays from the previous persisted position, so recovery is at-least-once and deterministic document IDs make duplicates idempotent. A restart resumes from the confirmed position with no re-bootstrap. If the saved position has been purged from the server (or is gone after a failover to a different primary), the source detects it (ER_MASTER_FATAL_ERROR_READING_BINLOG) and fails closed. It does not silently wipe the cursor and re-bootstrap, because a fresh snapshot cannot emit tombstones for rows deleted before that snapshot. Reconcile the destination or perform an explicit drain/reset, then bootstrap from the new position. - Deletes become tombstones — the deterministic doc id targets the exact row to remove.
The doc-id mapping
The sink document id is the canonical, namespaced form{database}.{table}:[<pk>] — e.g. shop.orders:["1"] for a single-column
key and shop.order_items:["1","2"] for a composite key, matching the Postgres
source’s shape. JSON-typed columns are parsed into nested JSON; DATETIME
renders as ISO-8601, and DECIMAL as its exact decimal string (precision
preserved).
Run the agent
VS_MYSQL_TABLES is optional — omit it to watch every table in the
database. The index template renders one index per table (orders,
order_items, …).
TLS
Use strict TLS for a remote database. Amazon RDS needs no downloaded CA file:ca_file instead. Strict mode validates both the certificate chain and
hostname for binlog, snapshot, join-fetch, and SQL recomposition connections.
See Database TLS and trust for the complete decision
guide.
Key environment variables
Full list in the engine env reference.
Denormalization / joins
PointVS_JOINS_YAML at a join spec and the MySQL source feeds the same join
engine the Postgres source uses — embedding related tables into one
denormalized document. A change to any table (primary or related) recomposes
the affected parent docs.
- A row arriving before its related rows is backfilled on demand: the source
re-
SELECTs the missing rows (backfill: sync_on_miss), so order-of-arrival doesn’t matter. - Related tables are consumed into the join (they don’t get their own index); only the primary’s denormalized doc is written.
- All primary + related tables are watched/snapshotted automatically, even if
VS_MYSQL_TABLESfilters. - Memory-mode joins require
VS_JOINS_STATE_DIRon durable storage. The engine refuses to start without it so binlog progress cannot move beyond join state that would be lost on restart.
cardinality, embed_as, on_missing,
composite keys) are shared with the Postgres connector.
Memory modes: in-memory vs SQL
The join can run in two modes (samejoins: spec):
In SQL mode the binlog source skips its snapshot (the denormalizer does the
SQL-join bootstrap) and only tails for live changes.
JSON_ARRAYAGG doesn’t
guarantee element order, so sort_by is applied to each doc’s array after the
query. 1:many child deletes are resolved to their parent via a sink
reverse-lookup (on by default; VS_MYSQL_SINK_REVERSE_LOOKUP=false to disable),
including composite child PKs — each PK column is matched and AND-ed, and
any over-match is harmless since the parent is recomposed from current DB truth.
Current limitations
- Joins are raw, not recursive. One level of embedding per related entry (no nested joins-of-joins yet). Cross-source joins are not supported — all tables in a spec come from the one MySQL database.
- Primary key required. Doc ids and tombstones key on the primary key; a table without one is logged with a warning and skipped at bootstrap, and its binlog events are skipped in the tail.
ROWbinlog format required — statement/mixed formats carry no per-row images. The source tails positions, not GTIDs: position resume is robust on a single server, but a failover to a different primary is recovered through the controlled reset and bootstrap path rather than GTID repositioning.- Schema cache is not invalidated on DDL. Per-table PK/column metadata is
cached on first use. An
ALTER TABLEthat changes the primary key or columns mid-stream is not picked up until the agent restarts — restart after such a migration. - Binlog-bounded resume. A resume reaches back only as far as the binlog is
retained (
binlog_expire_logs_seconds). If the saved position has been purged, the source fails closed and requires explicit reconciliation or drain/reset before bootstrap (see How it works). - MySQL 8.4+ compatible. Uses
SHOW BINARY LOG STATUSwhere available, falling back to the deprecatedSHOW MASTER STATUSon older MySQL / MariaDB.