Copy a Postgres Table into MotherDuck With the DuckDB ADBC Extension
I want a simple Flight that copies a table from a Postgres database into MotherDuck through the DuckDB ADBC extension and an ADBC driver installed with dbc. Help me adapt the "Copy a Postgres Table into MotherDuck With the DuckDB ADBC Extension" recipe to my own data and use case, using it as a guide: https://motherduck.com/docs/cookbook/flight-adbc-ingest
A single-file demo Flight that copies one Postgres table into MotherDuck. The
interesting part is the transport: instead of the DuckDB postgres extension,
it uses the adbc community extension
with the standard PostgreSQL ADBC driver. Swap the driver name and connection
URI and the same pattern reads Snowflake, MySQL, SQLite, or anything else with
an ADBC driver.
How it works
flight.py runs three steps:
- Install the PostgreSQL ADBC driver with
dbc, Columnar's ADBC driver manager.dbcis a pip package fromrequirements.txt, so the driver install is one subprocess call. The code then pointsADBC_DRIVER_PATHat the virtualenv directory wheredbcplaces drivers. - Write an ADBC connection profile, a small TOML file holding the source connection URI. The password comes from a MotherDuck Flights secret and is percent-encoded into the URI.
- Connect to MotherDuck, load the
adbcextension, and copy the table with one statement:CREATE OR REPLACE TABLE <target> AS SELECT * FROM read_adbc('profile://postgres_source', 'SELECT * FROM <schema>.<table>'). With noSOURCE_TABLEconfigured, it first asks the source'sinformation_schemafor the first user table.
read_adbc() sends the query text to the source database, which returns the
result as Arrow data. The copy is one statement with no intermediate files.
Questions to answer
- Source connection: host, port, database, user, and which password? Enter the connection as a MotherDuck Flights secret.
- Which
schema.tableto copy, or rely on discovery of the first user table? - Which
TARGET_DATABASEshould receive the copy? - Is a full refresh of one table enough? This is a demo. For a config-driven,
multi-table Postgres mirror with retries and an audit log, use the
flight-postgres-ingesttemplate instead.
Caveats
- Demo scope. One table, full refresh, no retries, no audit log.
- For Postgres specifically, the
postgresextension is the direct route. ADBC earns its place when the source has an ADBC driver but no DuckDB extension, or when you want one connection pattern across several systems. - Community extension.
adbcis built by Columnar and published in the DuckDB community extension repository for DuckDB 1.5.4 and later. - The extension also supports
ATTACH ... (TYPE adbc). This Flight usesread_adbc()because it hands the query text to the source, so filters run there instead of after transfer.
What you'll adjust
| Knob | Default | Purpose |
|---|---|---|
SOURCE_TABLE | (first user table) | schema.table to copy; a bare table name means public.<table>. |
TARGET_DATABASE | adbc_demo | MotherDuck database for the copy (created if absent). The table lands in its main schema. |
| secret | (required) | Postgres connection. TYPE flights secret (any name) with params PGHOST, PGUSER, PGPASSWORD, and optionally PGPORT (default 5432) and PGDATABASE (default postgres). A legacy secret named pg with HOST/USER/PASSWORD/... params (injected as pg_HOST, ...) works too. |
The secret's params are the libpq env vars themselves: a flights secret injects each param under its bare name, so the secret name is yours to choose.
Run it
You need a MotherDuck account and token plus a reachable Postgres source.
export MOTHERDUCK_TOKEN=your_token_here
# Postgres connection (same names the Flights secret injects):
export PGHOST=your-postgres-host
export PGUSER=your_user
export PGPASSWORD=your_password
export PGDATABASE=your_database
# optional: export SOURCE_TABLE=public.orders TARGET_DATABASE=adbc_demo
uv run --with-requirements requirements.txt flight.py
The run prints one line, for example
Copied public.orders -> "adbc_demo".main."orders" (51001 rows).
Deploy as a Flight
First store the connection as a Flights secret named pg (UI:
Settings > Secrets, type
Flights, params PGHOST, PGUSER, PGPASSWORD, and optionally PGPORT
and PGDATABASE). Then create the Flight with the MD_CREATE_FLIGHT SQL
function, passing:
name: a Flight name, for exampleadbc_ingestsource_code:flight.pyrequirements_txt:requirements.txtflight_secret_names:['pg'](or whatever the secret is named)config:SOURCE_TABLEandTARGET_DATABASEif the defaults don't fit; the connection stays in the secret, never config
Create it without a schedule, run it once with MD_RUN_FLIGHT(flight_id := ...)
(the id is returned by MD_CREATE_FLIGHT and listed by MD_FLIGHTS()), and
check the target table. Add a cron schedule later if the copy should repeat.
Security
- Connection in a secret, never in config or SQL. The password reaches the driver percent-encoded inside the profile URI. The profile file is written with owner-only permissions (0600) in the run's container.
- Quoted identifiers. Schema, table, and database names are quoted before
they land in SQL, and query text sent through
read_adbc()is single-quote-escaped.
Learn more
- DuckDB
adbcextension: community extension page and source. dbcdriver manager: docs.columnar.tech/dbc.- Flight mechanics (create, run, schedule, secrets): MCP
get_flight_guide. - Files:
flight.py(the Flight source),requirements.txt(duckdbpinned to a MotherDuck-supported version, plusdbc).