BigQuery
Load jobs straight from the worker. Native `MERGE`. No bucket to provision.
Every destination receives the same layout; see the tables Ingest creates.
Setup
Ingest needs to create datasets and tables in your project, run load jobs into them, and read back the
small _ingest_* state tables it keeps beside your data. Nothing else: no billing access, no other
Google API.
The one thing people get wrong
Two roles are needed, not one. roles/bigquery.dataEditor grants bigquery.datasets.create but not
bigquery.jobs.create; roles/bigquery.jobUser grants the job permission and no data access. Granting
only Data Editor produces a destination that connects, creates a dataset, and then fails every load,
which reads like a broken pipeline rather than a missing grant.
Pick BigQuery in the deploy wizard and press Sign in with Google: consent opens in a small popup, you approve BigQuery access only, and Ingest stores the refresh token in your own secret namespace; it never sees your password. After you approve, Ingest lists the projects your account can see and runs the connection check itself; there is no project id to hunt down and no test button to remember.
Whoever signs in must already hold the two roles below: signing in grants Ingest the account's access, it does not add any.
On Google Workspace, check this first. If your administrator runs a session-control (reauthentication) policy, Google expires this grant on their schedule even after you approve it: the destination works, then stops, and the connection test says the authorisation is no longer valid. On such an organisation use Option B: a service-account key is not subject to that policy.
Then test it
Press Test connection. A worker connects with your credential, creates a scratch dataset named
test_ingest_conn_… and a table in it, runs a real load job, reads the row count back, round-trips
pipeline state, and drops the lot. Writing is the point: a credential that can create a dataset but not
run a load job (Data Editor without Job User, exactly the trap above) fails here instead of on your
first real run.
Staging
needs_staging: none. BigQuery load jobs accept a local file through the client library, and the load
is atomic: either every record lands or none does. So there is no object-storage hop: no GCS bucket to
create, and no cross-account grant for one. Of every warehouse here, BigQuery is the one that needs a
single credential and nothing else.
Redshift is the contrast worth knowing: its COPY cannot read a local file, which is why it asks for
an S3 staging prefix and an IAM role. That requirement is Redshift's, not a house style, and it does not
apply here.
Writes
- append: one load job per part, preceded by a sweep of this load's own
_ingest_load_id, so a package that half-landed converges on retry instead of double-counting. - replace: staged, then a single
CREATE OR REPLACE TABLE … AS SELECT. One atomic commit, because a reader querying mid-swap must never see a partial table. - merge: staged, NULL keys refused before anything touches the target, then a native
MERGEwhoseUSINGclause carries the dedup (latestdedup_sortwins). A merge source with duplicate keys is an error in BigQuery rather than a coin flip, so the dedup cannot be a separate pass.
There are no client-side transactions across a load job and its DML, so the adapter declares
supports_transactions: false and documents idempotent recovery instead of claiming a rollback that
would only cover half the operation.
Conformance
Every adapter passes the same suite before it ships: create a table with every canonical type, append
100k rows, atomic replace, merge (latest dedup_sort wins; child tables delete-then-insert by root
id), schema evolution, NULL-key rejection, package replay without duplicates, state and schema
round-trip, row-count read-back, identifier edge cases, double-underscored table names (__quarantine, child tables), and rollback on a
mid-load failure.