faucet-sink-bigquery
Google BigQuery streaming insert sink for the faucet-stream ecosystem.
Writes JSON records to a BigQuery table using the tabledata.insertAll streaming API. Records are automatically split into configurable batch sizes to stay within BigQuery API limits. The BigQuery client is authenticated once at construction and reused across all writes.
write_batch accepts whatever slice the pipeline hands it. When batch_size > 0 and the slice is larger than batch_size, the sink re-chunks internally and issues one insertAll call per chunk; when batch_size = 0, the entire slice is sent in a single request — see Streaming and batching for the tradeoffs.
Installation
[]
= "1.0"
= { = "1", = ["full"] }
Or via the umbrella crate:
= { = "1.0", = ["sink-bigquery"] }
Quick Start
use ;
use Sink;
use json;
async
Configuration
| Field | Type | Default | Description |
|---|---|---|---|
project_id |
String |
(required) | GCP project ID |
dataset_id |
String |
(required) | BigQuery dataset ID |
table_id |
String |
(required) | BigQuery table ID |
credentials |
BigQueryCredentials |
(required) | Authentication credentials (see below) |
batch_size |
usize |
1000 |
Maximum rows per insertAll request. See Streaming and batching below |
insert_id_field |
Option<String> |
None |
Record field whose value is sent as the per-row streaming insertId. See Retry de-duplication below |
Retry de-duplication
BigQuery streaming inserts are at-least-once — a transport retry can
insert the same row twice. Setting insert_id_field to a stable per-row
key (e.g. a primary key or event id) makes the sink send that value as the
row's insertId, which BigQuery uses for best-effort de-duplication over a
short window. When insert_id_field is unset, or a given row lacks the
field, that row is inserted without an insertId (no dedup for it).
Streaming and batching
The BigQuery sink re-chunks each incoming StreamPage to keep individual
tabledata.insertAll calls under BigQuery's limits.
batch_size > 0(default1000) — the sink slices the incoming slice intobatch_size-row chunks and issues oneinsertAllHTTP call per chunk. Recommended value is500: that's the documented sweet spot for BigQuery streaming inserts (small enough to stay well under the ~10MB request limit even for wide rows, large enough to amortise per-call overhead). Bump it higher when rows are narrow; drop it when rows are wide enough to push individual chunks past 10MB.batch_size = 0— the "no batching" sentinel. The entire upstreamStreamPageis forwarded in a singleinsertAllcall. Use this when the source already emits page sizes tuned for BigQuery — for example a Postgres source configured withbatch_size: 500. Larger pages risk HTTP-413 from BigQuery's ~10MB body limit.
batch_size is purely a chunk-size knob — BigQuery's per-row error
reporting and the sink's "first row that failed" error message are
unchanged. Per-call retry is delegated to gcp_bigquery_client's built-in
retry middleware.
Authentication (BigQueryCredentials)
| Variant | Description |
|---|---|
ServiceAccountKeyPath(String) |
Path to a service account JSON key file on disk |
ServiceAccountKey(String) |
Inline service account JSON key content as a string |
ApplicationDefault |
Use application default credentials (workload identity, gcloud auth application-default login, etc.) |
The ServiceAccountKey variant is useful in environments where the key is injected as an environment variable rather than stored on disk (e.g. Kubernetes secrets, CI/CD).
The Debug implementation masks the inline key content with *** to prevent accidental credential leakage in logs.
Builder Methods
use ;
let config = new
.with_batch_size;
Config Loading
use ;
use BigQuerySinkConfig;
// From a JSON file
let config: BigQuerySinkConfig = load_json?;
// From an .env file with a prefix
let config: BigQuerySinkConfig = load_env_file?;
Example JSON config
Using application default credentials:
Example .env file
BIGQUERY_PROJECT_ID=my-gcp-project
BIGQUERY_DATASET_ID=analytics
BIGQUERY_TABLE_ID=events
BIGQUERY_CREDENTIALS='{"type":"service_account_key_path","config":{"path":"/etc/secrets/bigquery-sa.json"}}'
BIGQUERY_BATCH_SIZE=1000
Config Schema Introspection
use Sink;
let sink = new.await?;
let schema = sink.config_schema;
println!;
Pipeline Usage
use Pipeline;
use ;
use ;
let source_config = new;
let source = new;
let sink_config = new;
let sink = new.await?;
let result = new.run.await?;
println!;
Examples
Streaming inserts with a service account key file
let config = new
.with_batch_size;
let sink = new.await?;
let written = sink.write_batch.await?;
Using inline service account JSON
let sa_json = var?;
let config = new;
let sink = new.await?;
Using application default credentials (local development)
let config = new
.with_batch_size;
let sink = new.await?;
How It Works
- The BigQuery client is created and authenticated in
BigQuerySink::new(). This validates credentials eagerly so failures surface immediately. write_batch()slices the input intobatch_size-row chunks (or forwards the whole slice whenbatch_size = 0) and sends each chunk as a separateinsertAllrequest.- Per-row errors in the BigQuery response are detected and reported. If any rows fail, the entire batch returns an error with details about the first failure.
- The client is reused across all
write_batch()calls -- no re-authentication per request.
Exactly-once delivery
BigQuerySink implements Sink::supports_idempotent_writes (returns true) and the two companion hooks:
write_batch_idempotent(records, scope, token)— writes the page and records thetokenforscopein one BigQuery multi-statement transaction: a typedINSERT INTO <target> SELECT … FROM UNNEST(JSON_QUERY_ARRAY(@payload))(the page ships as a single bound JSON parameter; each column is cast per the target table's schema, fetched once viatables.get) followed by aMERGEinto the_faucet_commit_tokenwatermark table in the target dataset. Either both commit or neither does. Success is verified authoritatively via the job'serrorResult, so a runtime failure (a badCAST, aNULLinto aREQUIREDcolumn) aborts the page rather than advancing the bookmark over data that never landed.last_committed_token(scope)— reads the watermark row forscopeso the pipeline can skip already-committed pages on resume.
On crash/resume the pipeline reads last_committed_token and skips any page whose token is already committed, so the target table never sees duplicates. This is append-with-idempotency, distinct from the default streaming insertAll path and from key-based upsert (write_mode), which BigQuery does not support.
Requirements & limits. The target table must already exist with a defined schema (the sink reads it to build the typed INSERT — a schemaless table is rejected with a clear error). Because each page commits as one atomic transaction (one token per page, no batch_size re-chunking on this path), the page must serialize within BigQuery's ~10 MB jobs.query request limit — keep the CDC source's per-page size modest. A scalar column whose JSON value is an object/array is coerced to NULL (or, for a REQUIRED column, fails the INSERT). This path uses BigQuery DML, which has concurrency limits; it is intended for the opt-in exactly-once mode, not high-rate streaming (use the default streaming insertAll path for that).
To use exactly-once delivery, set delivery: exactly_once in your pipeline config and pair this sink with one of the CDC sources (postgres-cdc, mysql-cdc, mongodb-cdc) plus a state: block. A DLQ is not permitted in exactly-once mode. All four requirements are validated at config-load time (faucet validate) before any run starts.
pipeline:
source:
type: mongodb-cdc
config:
connection_uri: "mongodb://localhost:27017/?replicaSet=rs0"
scope:
type: collection
database: app
collection: orders
start_from:
type: now
sink:
type: bigquery
config:
project_id: my-gcp-project
dataset_id: analytics
table_id: orders
auth:
type: application_default
state:
type: file
config:
path: ./state
delivery: exactly_once
See the Exactly-once delivery cookbook for full rationale and the supported source/sink set.
Dead-letter queue support
This sink overrides Sink::write_batch_partial to surface per-row
failures from BigQuery tabledata.insertAll's insertErrors response.
Configure a DLQ at the pipeline level (see cli/README.md — dlq:)
and only the rows BigQuery actually rejected will be routed there —
already-committed rows stay in the main sink with no duplicates.
Lineage dataset URI
bigquery://<project_id>.<dataset_id>.<table_id> — e.g. bigquery://my-project.warehouse.orders.
License
Licensed under MIT or Apache-2.0.