Setup
- Open Settings → Lake and choose Add Postgres connection.
- Paste a connection string to fill the fields automatically, or enter them by hand:
The dialog generates the SQL for a read-only user if you want to create one - copy it and run it against your database before saving.
Querying live
Tables are reachable asconnection_name.schema.table:
A Postgres table selects DuckDB. DuckDB is the only engine that attaches external connections, so the query router routes any query naming a connection table here before it considers input size - leave
engine on auto. Asking for Bloom, Polars, or Spark on one of these queries returns an engine capability error rather than rerouting silently.Importing into Iceberg
For anything larger than an ad hoc read, import the table. The import runs as a Spark JDBC read and lands the data as an Iceberg table in your lake, where every engine can reach it. The import plans itself from Postgres catalog metadata:- Partition count follows table size.
- Partition ranges come from the integer primary key, balanced by histogram, or from physical
ctidchunks when there is no suitable key. - Every partition reads from one exported MVCC snapshot, so the copy is consistent even against a live, changing database.
oleander.default.<connection>_<table>. OVERWRITE replaces it, APPEND adds to it.
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.
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.Scheduled syncs are incremental. After the first full import, each run picks up new and changed rows only rather than recopying the table, so a sync over a large table costs a fraction of the initial import.
connection.source_table → destination_table pair, listed under Scheduled syncs in Settings → Lake with its cadence and next run time, where it can be paused or removed.
Every sync run is a normal oleander run: it appears in run history, emits lineage with the Postgres table as the input and the Iceberg table as the output, and carries cost like any other job. A sync that starts failing or silently stops moving rows shows up in observability the same way a pipeline regression does.
Pick a cadence against how fresh the lake copy needs to be, not how often the source changes - the incremental read is cheap, but each run is still a Spark job against your database.
From an agent
Two tools cover Postgres over MCP:
For ad hoc reads of live data an agent uses
query_run against connection.schema.table instead.