Skip to main content
Nightly Feature
This feature is currently in private preview. Contact support@firebolt.io to request early access.
Firebolt connects to your MongoDB deployment and keeps a CDC table continuously in sync with a collection: every insert, update, replace, and delete arrives within seconds, with nothing to deploy in between. Under the hood, the connection is a native MongoDB change stream. Documents do not need a fixed schema to become queryable: the table holds each document whole, as JSON, keyed by its _id.

Prepare the source deployment

On the MongoDB side you need:
  1. A replica set or sharded cluster. Change streams read the oplog; a standalone mongod has none. A sharded collection must be sharded on _id, the only key MongoDB keeps unique across shards.
  2. MongoDB 6.0 or later, with document pre- and post-images enabled on the collection. Firebolt reads exact point-in-time images rather than re-fetching documents, so updates are never observed out of order or half-applied:
  3. A role with find and changeStream privileges on the collection, authenticated with SCRAM.
  4. Oplog and pre-image retention sized so the feed’s history outlives any planned ingestion pause. If the stored position ages out of the oplog, ingestion cannot resume from it and the table must be re-bootstrapped.
  5. Network reachability from the Firebolt engine to the cluster (VPC peering, PrivateLink, or an Atlas access list).

Create the location

Credentials go in USER and PASSWORD, never in the URL, which is shown as written in information_schema.locations. TLS is on for every host other than localhost unless the URL sets tls=false. See CREATE LOCATION (MongoDB).

Create the stream

A stream binds to one collection, named as <database>.<collection>:
CREATE STREAM connects to the deployment, checks the prerequisites above, and records the current end of the change stream as the stream’s starting position. A stream takes no column list: every MongoDB stream has the same columns, described below.

Read raw change events

Every change row has two columns and a set of pseudo-columns (reference those double-quoted): The first consuming read starts at the position CREATE STREAM recorded; it does not replay older history. A consuming read has to run inside a writing statement, such as INSERT INTO <table> SELECT ... FROM READ_STREAM(STREAM orders_changes, consume => true). To load the collection as it is now, use READ_SNAPSHOT:
It returns the collection’s documents with the stream’s _id and doc columns, and moves the stream’s position to where the change feed was just before the scan. The scan reads ranges of the collection’s storage order on several cursors at once and has no time limit. It is not a point in time: each document is returned as it was at some moment during the scan, and a document deleted and inserted again during the scan can be returned twice. Every document that changed during the scan has events in the change feed, so applying the feed past the end of the scan, each event replacing every row with its _id, brings the table to the collection’s state at that time. Consuming reads advance the stream’s stored position in the same Firebolt transaction that writes the data, so ingestion is exactly-once with no deduplication step.

Create the CDC table and start ingestion

A CDC table over a MongoDB stream has the stream’s two columns, keyed by _id. Attach the stream and resume ingestion:
The worker loads the collection with READ_SNAPSHOT, then applies the change feed continuously in the order the source committed it. A multi-document transaction becomes visible all at once. Until the feed has been applied past the end of the load’s scan, documents in the table can be at different points in time, or appear twice, so the table returns no rows to queries: while its stream is attached, it is either empty or exact, never partial. phase in information_schema.cdc_ingests reports backfilling until then and streaming afterwards. The feed passes that point with the next entry in the source’s oplog: on a collection with no writes after the load, that is the periodic no-op a replica set writes every 10 seconds by default, so a deployment that disables writePeriodicNoops keeps such a table empty until its next write. ATTACH of a stream whose load has not caught up also makes the table return no rows, until the worker started by RESUME finds that it has. If the ingest stops for good before the feed has caught up, the table keeps returning no rows until its stream is detached. DETACH STREAM returns the rows the table holds whatever their state, so a table detached before its feed caught up can show the mixed state described above. Semantics and lifecycle commands are covered in CDC tables. The load cuts the collection into ranges of its storage order, the cheapest order for the server to read, and reads them with as many cursors as the table’s initial_load_parallelism names, else execution.cdc.load_source_cursors (default 8, at most 64), each a connection to the source. A load that fails is retried with half as many cursors, down to one. A cursor that runs out of work takes over half of what another has left, so storage left empty by deleted documents does not leave cursors idle. A sharded cluster, or a server before MongoDB 7.0, is read by one cursor. Storage positions differ between the members of a replica set, so all cursors read from one member, the primary unless execution.cdc.load_secondary_reads picks a secondary (the primary, if no secondary can be reached when the load starts); if that member becomes unavailable, the load fails and starts over. On a sharded cluster, the setting reads from each shard’s secondaries where it can. Documents are decoded on up to eight of the Engine’s threads per cursor, at most its max_threads, and the server replies the load holds count toward the load’s memory. The oplog must still hold the load’s start when the load ends, or the stream fails and must be recreated: set oplogMinRetentionHours on the deployment to cover the load. After the load, the worker reads the change feed ahead in the background, also while it writes what it has read, and applies it in commit order. One change stream delivers a few tens of thousands of changes a second, so a busy collection’s feed is cut into consecutive slices of the oplog, read on several at once and applied in order: the table sees exactly what a single change stream reports. A stream reads on one change stream while it keeps up; once that one falls behind, the rest of the feed goes to another. execution.cdc.feed_source_cursors (default 4, at most 64) bounds how many, and a stream that has fallen behind catches up on all of them. The rows read ahead wait in memory, counted toward the Engine’s memory but not toward any query’s, up to execution.cdc.feed_source_buffer_bytes_per_cursor (default 32MiB) for each of a stream’s cursors, so 128MiB per stream at the defaults. Between appends, the Engine keeps each stream’s reader so that the next append goes on where the last one stopped, with what was read ahead meanwhile. A kept reader holds a connection pool of its own to the source, including the driver’s monitoring connections to each replica set member, one Engine thread per cursor plus one, its open change streams, and its read-ahead. A reader no append takes for two minutes is closed, about two and a half minutes after its last read at most, and an Engine keeps at most 64 readers, closing the one kept longest when one more is kept. Size the cursors and their buffer with the number of MongoDB streams the Engine ingests in mind.

Query document fields

Read any field, including nested ones, through doc. A field is JSON; JSON_VALUE turns a scalar into text, which you can cast:
Documents hold relaxed Extended JSON: an int or double is a JSON number, while a type JSON has no number for, such as decimal128 or date, is an object like {"$numberDecimal": "1.5"}. Fields that appear in documents after the stream was created need no DDL: the table always holds the whole document.

Operations

  • Delivery and recovery. The resume token commits atomically with the data it produced. After a crash or Engine restart, run RESUME again; the new worker continues from the last committed token.
  • Feed expiry. If ingestion is paused long enough for the stored token to age out of the oplog (or the needed post-images to expire), the worker’s reads fail with a history-lost error, shown in last_error of information_schema.cdc_ingests, until the retry budget runs out and the attachment stops in state = 'error'. Nothing on the source is harmed; recreate the stream and the CDC table, which loads the collection again. A stream no CDC table is attached to can instead be reloaded with READ_SNAPSHOT, which moves its position to just before the scan.
  • Collection lifecycle events. A collection drop, rename, or stream invalidation ends the feed. Like any persistent failure, the attachment stops in state = 'error' once the retry budget runs out. Recreate the stream and table against the new collection.
  • DROP STREAM removes only Firebolt metadata. MongoDB change streams hold no server-side resource comparable to a replication slot, so there is nothing to clean up on the source. An Engine still reading the dropped stream ahead closes its change streams when the kept reader expires, two to two and a half minutes after the last read.

Limitations

  • One collection per stream.
  • A stream’s columns are fixed: _id and doc.
  • A sharded collection is supported only when it is sharded on _id. CREATE STREAM checks this; resharding the collection later is not detected.
  • A document the engine cannot store stops the table’s ingestion at that change: information_schema.cdc_ingests reports it in last_error; once the retry budget runs out, the attachment stops in state = 'error'. To recover, detach the stream and attach a new one; the next RESUME reloads the collection.
  • ALTER STREAM is not supported; schema changes are drop-and-recreate.
  • The table’s columns are the stream’s: _id and doc. Fields are read through doc.