faucet-source-bigquery
Google BigQuery query source for the faucet-stream ecosystem. Runs a SQL statement against BigQuery's REST API (jobs.query + jobs.getQueryResults), decodes each row into a typed serde_json::Value, and streams the result set back page-by-page so memory stays bounded no matter how large the query result is.
Reach for it when you want to pull analytics tables, aggregates, or ad-hoc query results out of BigQuery and land them in any faucet-stream sink — a file, a database, a warehouse, a queue — with one declarative config and no glue code.
Feature highlights
- Three credential modes — Application Default Credentials, a service-account key file, or inline service-account JSON. The shared
BigQueryCredentialsenum is re-exported fromfaucet-common-bigqueryso it matches the BigQuery sink byte-for-byte. - Server-side pagination — pages through arbitrarily large result sets via
pageToken; rows are re-framed into pages ofbatch_sizefor streaming, so peak memory isO(batch_size). - Async-job aware — when BigQuery returns
jobComplete=false(the statement ran paststatement_timeout), the source pollsjobs.getQueryResultsuntil the job finishes, bounded bypoll_timeoutso a job that never completes fails cleanly instead of hanging forever. - Type-aware row decoding —
INTEGER/INT64→ JSON number,FLOAT/FLOAT64→ number,BOOLEAN/BOOL→ bool,NUMERIC/BIGNUMERIC→ string (full precision preserved),RECORD/STRUCT→ nested object,REPEATED→ array,JSON→ parsed value, everything else → string. - Positional bind parameters —
paramsfrom config plus any${parent.path}matrix-context values are sent as BigQueryPOSITIONALparameters, typed from the JSON value (INT64/FLOAT64/BOOL/STRING) so a numeric or boolean bind compares correctly against a typed column. - Standard or legacy SQL —
use_legacy_sqltoggle; defaults to Standard SQL. - Client built once — the authenticated BigQuery client is constructed in
new()and reused for every request.
Installation
# As a library:
# In the CLI (opt-in connector feature):
Quick start
# pipeline.yaml — faucet run pipeline.yaml
version: 1
pipeline:
source:
type: bigquery
config:
project_id: my-project
auth:
type: service_account_key_path
config:
path: /etc/secrets/bigquery-sa.json
query: |
SELECT user_id, event_name, occurred_at
FROM `my-project.analytics.events`
WHERE occurred_at >= ?
params:
- "2026-01-01"
sink:
type: jsonl
config:
path: ./events.jsonl
Configuration reference
| Field | Type | Default | Description |
|---|---|---|---|
project_id |
string | — (required) | GCP project ID the query is billed to and run against. |
auth |
BigQueryCredentials |
— (required) | Authentication — see Authentication. |
query |
string | — (required) | SQL to execute. May contain positional ? markers (bound from params + matrix context) and ${field.path} placeholders resolved against the parent-record context at runtime. |
use_legacy_sql |
bool | false |
Use BigQuery's legacy SQL dialect. Leave false (Standard SQL) unless the query uses legacy [project:dataset.table] references. |
location |
string | (unset) | Location override for non-US jobs ("EU", "asia-east1", …). When unset, BigQuery uses the queried tables' default location. |
max_results_per_page |
int | 1000 |
Rows per jobs.getQueryResults page. Smaller = more HTTP round-trips, less memory; larger = fewer requests, more memory. |
params |
array | [] |
Positional bind parameters, sent in declaration order before any context-derived values. Typed from each JSON value. |
statement_timeout |
int (seconds) | 60 |
Per-statement server-side timeout (timeoutMs on jobs.query). If the query isn't done within this window, BigQuery returns jobComplete=false and the source begins polling. |
poll_timeout |
int (seconds) | 300 |
Max wall-clock time spent polling jobs.getQueryResults for a job still reporting jobComplete=false, before failing with FaucetError::Source. 0 disables the cap (poll forever). Only the completion wait is bounded; pageToken paging afterward is unaffected. |
batch_size |
int | 1000 |
Records per emitted StreamPage. 0 = no batching: the entire result set is emitted in a single page (good for small lookup tables or sinks that prefer one large request). |
Authentication
auth uses the shared BigQueryCredentials enum (the project-wide { type, config } shape):
type |
config |
Use when |
|---|---|---|
application_default |
(none) | Running on GCP (workload identity / metadata server) or after gcloud auth application-default login. |
service_account_key_path |
{ path: <file> } |
You have a service-account key file on disk. |
service_account_key |
{ json: <string> } |
You want to inject the key JSON inline, typically via ${env:VAR} / ${secret:…} indirection. |
# Application Default Credentials (workload identity, gcloud)
auth:
type: application_default
# Service-account key file
auth:
type: service_account_key_path
config:
path: /etc/secrets/bigquery-sa.json
# Inline service-account JSON via env indirection
auth:
type: service_account_key
config:
json: ${env:GCP_SERVICE_ACCOUNT_JSON}
Examples
Incremental pull with a typed bind parameter
source:
type: bigquery
config:
project_id: my-project
auth:
query: |
SELECT id, name, score
FROM `my-project.app.users`
WHERE score > ?
params:
- 0.75 # bound as FLOAT64, compares correctly against a numeric column
Large export with no re-chunking
source:
type: bigquery
config:
project_id: my-project
auth:
type: service_account_key_path
config:
query: SELECT * FROM `my-project.warehouse.daily_snapshot`
location: EU
max_results_per_page: 10000
batch_size: 0 # emit the whole result set as one page
Long-running aggregate that may exceed the statement timeout
source:
type: bigquery
config:
project_id: my-project
auth:
query: |
SELECT country, COUNT(*) AS n
FROM `my-project.analytics.events`
GROUP BY country
statement_timeout: 30 # return fast; if not done, poll…
poll_timeout: 600 # …for up to 10 minutes before failing
Streaming & batching
The source overrides Source::stream_pages: it runs the job, then walks jobs.getQueryResults by pageToken, buffering decoded rows and yielding a StreamPage every time the buffer reaches batch_size. Memory is bounded at one page. With batch_size: 0 the entire result set is buffered and emitted in a single page. max_results_per_page controls the HTTP page size independently of the emitted page size.
This is a one-shot query source — it has no incremental bookmark / resume support (each run re-executes the query). For incremental loads, encode the watermark in the query (e.g. WHERE occurred_at >= ?) and drive it from a matrix context or ${now.*} token.
Config loading & schema
Load from YAML/JSON or environment. Inspect the full JSON Schema with:
Library usage
use Source;
use ;
# async
How it works
new()resolvesBigQueryCredentialsand builds an authenticated client once.jobs.querysubmits the SQL withstatement_timeoutastimeoutMsand any positional parameters.- If
jobComplete=false, the source pollsjobs.getQueryResultsuntil the job finishes orpoll_timeoutelapses. - Result pages are walked via
pageToken; each cell is decoded by its schema type into a typed JSON value. - Rows are re-framed into
batch_sizepages and streamed to the pipeline.
Lineage dataset URI
bigquery://<project_id>?query=<sql> — e.g. bigquery://my-project?query=SELECT id FROM events.
Feature flags
This crate has no optional features of its own; enable it in the CLI/umbrella via the source-bigquery feature.
Troubleshooting / FAQ
| Symptom | Likely cause & fix |
|---|---|
Auth error / 401 / 403 |
Credentials invalid or missing scope. Confirm the service account has BigQuery Data Viewer + BigQuery Job User on the project, or that ADC is initialized (gcloud auth application-default login). |
403 ... billing |
The project_id has no billing enabled, or the SA lacks bigquery.jobs.create. Use a project with billing and the Job User role. |
| Job fails with a location error | The queried dataset isn't in the default (US) location. Set location to the dataset's region (e.g. EU). |
FaucetError::Source: poll timeout |
The job ran longer than poll_timeout. Raise poll_timeout (or set 0 to wait indefinitely), and/or raise statement_timeout. |
A NUMERIC column arrives as a string |
Intentional — NUMERIC/BIGNUMERIC are decoded as strings to preserve full precision. Cast in a downstream transform if you need a JSON number. |
| Bind parameter compared as text unexpectedly | Each params entry is typed from its JSON value. Pass 0.75 (number) not "0.75" (string) to bind a FLOAT64. |
| Legacy table reference fails to parse | Set use_legacy_sql: true for [project:dataset.table] syntax. |
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.