Skip to main content
Connect a Google Cloud project and every BigQuery table in it becomes queryable from the lake, joinable against your Iceberg tables in a single SQL statement. Import a table into Iceberg when you want a copy you can run large jobs against, or keep in sync with the source. Connections are managed in Settings → Lake.

Setup

  1. Open Settings → Lake and find BigQuery connections.
  2. Give the connection a name - lowercase letters, numbers, and underscores, e.g. my_bq. This becomes the catalog prefix in SQL.
  3. Paste your service account JSON. The GCP project ID is filled in from the key.
The service account needs roles/bigquery.dataViewer, or narrower table-level permissions. The key is stored encrypted and is never returned to the browser after saving.

Querying

Tables are reachable as connection_name.dataset.table:
A BigQuery table runs on Bloom or DuckDB. The query router prefers Bloom for reads and uses DuckDB for writes or when Bloom isn’t available - leave engine on auto. Polars and Spark can’t query connections.

Importing into Iceberg

For anything larger than an ad hoc read, import the table. In the lake tree, hover a BigQuery table and choose Import into the lake. The import runs as a Spark job that reads one consistent snapshot of the table through the BigQuery Storage Read API, split into streams that executors read in parallel, and lands the data as an Iceberg table in your lake, where every engine can reach it.
  • The source is dataset.table in the connection’s project, or project.dataset.table for another project the service account can read, such as a public dataset.
  • Logical views can’t be imported; import the tables they select from instead.
  • Your BigQuery project is billed for the bytes read.
The destination defaults to oleander.default.<table> - the sanitized source table name, with no connection prefix. OVERWRITE replaces it, APPEND adds to it.
For large tables, prefer more executors (4-8) over bigger machines. Each executor core reads one Storage Read API stream.
An import does not return rows. It submits a job and returns a run_id you can monitor like any other run, and once it completes you can sample the destination with a normal query. Importing requires Describe on the connection and Execute on Spark.

Table syncing

A one-off import gives you a snapshot. Put the same import on a schedule and the Iceberg copy tracks the source instead. Schedules run hourly or daily, and are managed the same way as Postgres syncs: under Scheduled syncs in Settings → Lake, and under Transfers in the lake’s left panel.
Scheduled syncs are incremental where the table allows it. A table with a timestamp cursor column (such as updated_at) and a declared primary key (PRIMARY KEY (...) NOT ENFORCED) merges rows changed since the last run. A table without both is copied in full each run, and the reason is written to the run’s plan log. Rows deleted in BigQuery are not removed from the Iceberg copy.
Every sync run is a normal oleander run: it appears in run history, emits lineage with the BigQuery table as the input and the Iceberg table as the output, and carries cost like any other job.

Cost attribution

BigQuery spend is pulled into the context graph alongside your own runs. Use bigquery_cost_get from an agent, or the cost views in observability, to see cost per table broken down by producing pipeline, query type, and full-scan detection.

From an agent

An agent connected over MCP reaches the same tables through query_run. It discovers them with catalogs_list and queries connection.dataset.table directly - no BigQuery-specific tool needed for live reads. Two tools cover imports: