Command line
pondra is one binary. On its own it opens a SQL shell on a lake. pondra serve runs a node on a
lake, or serves a folder of lakes as databases, pondra sql and pondra run run SQL, pondra workspace keeps the lake’s files in a folder of yours, pondra plan and pondra deploy make a
database match a project of files, and pondra catalog prints what the lake’s catalog holds.
| Command | Does |
|---|---|
pondra [LAKE] |
a SQL shell on a lake (./lake if you name none) |
pondra serve [PATH] |
run a node on a lake, or serve every lake in a folder as a database |
pondra sql --dir LAKE "SQL" |
run one statement against the lake, with no server |
pondra run FILE LAKE |
run a SQL file, its $name parameters taking the values you pass |
pondra workspace pull|push DIR LAKE |
the lake’s files in a folder of yours, for git and your editor |
pondra init, plan, deploy, test, export |
a project of files, made true in a database (environments) |
pondra login URL, branch, diff |
a server’s token kept; a database branched from another; what a branch changes |
pondra catalog --dir LAKE PREFIX |
print catalog entries |
pondra --version, pondra --help |
the version, and help for any command |
A lake is a folder or an object-store prefix (s3://bucket/prefix). Credentials and the endpoint
come from the AWS_* environment variables: see environment.
The shell
Section titled “The shell”pondra opens a shell on ./lake, and pondra some/folder or pondra s3://bucket/prefix opens
another. The shell runs a node on the lake, in the background, so views, tasks and windows keep
running while it is open and other nodes can join. When it ends, the lake is left as any node
leaves it. Because this is your own node, its SQL may read files on your machine
(SELECT * FROM 'jan.csv'). Other lakes in the current folder are attached as databases.
Statements end with ;. A line can be one of:
| Line | Does |
|---|---|
.tables |
the tables and views of every lake, with their kind |
.databases |
this lake and the ones attached to it |
.quit, .exit, \q |
leave |
Answers print as a table. On a terminal the shell also shows how long each statement took. The shell reads standard input, so it takes a script too:
printf 'CREATE TABLE orders (id BIGINT, amount DOUBLE);\nINSERT INTO orders VALUES (1, 7.5), (2, 4.5);\nSELECT sum(amount) AS total FROM orders;\n' | pondra shell-lake{"table":"orders","publish":[]}{"rows":2}+-------+| total |+-------+| 12.0 |+-------+A statement that fails prints Error: … on standard error and the shell goes on.
pondra serve
Section titled “pondra serve”pondra serve [PATH] runs a node and stays up until Ctrl-C or SIGTERM. PATH is a lake (a local
folder, or s3://bucket/prefix), or a folder whose subfolders hold lakes. It is the current folder
if you give none. Nodes started on the same lake form a cluster: the first leads (ingest, tiering,
commits), and the rest follow (SQL, task shards, writes forwarded to the leader) and take over if
the leader dies. A node that is stopped hands over at once, and every write it acknowledged is
already durable.
pondra serve lakepondra serve lake --addr 127.0.0.1:8081pondra serve dataThe second line starts a second node on the same lake. It follows the first. The third serves the
lakes in data as databases: see A folder of lakes.
What PATH holds
Section titled “What PATH holds”pondra serve looks at PATH and does the first of these that fits:
PATH |
pondra serve |
|---|---|
holds a lake (it has a catalog/ folder) |
serves that lake |
| has subfolders that hold lakes | serves each as a database named after its folder |
| is new or empty | makes a new lake there, and serves it |
Two guards keep a mistyped path from making a lake where you did not mean to:
pondra servewith noPATHnever makes anything. If the current folder is not a lake and holds no lakes, it stops and says so:pondra serve lakemakes one at./lake.- A folder that holds other things (files, other folders) but no lakes is refused. Serve a lake in
it (
pondra serve folder/lake), or make one there anyway with--lake folder.
For scripts and services (systemd, Docker), name what you mean, and nothing is guessed:
pondra serve --lake /srv/lakepondra serve --lakes /srv/data--lake serves exactly this lake, and makes it if it is missing (--dir is the same, for older scripts). --lakes serves exactly this
folder of lakes. Use these two in anything that starts a node unattended.
Where and how
Section titled “Where and how”| Flag | Default | Meaning |
|---|---|---|
PATH |
the current folder | the lake (a folder, or s3://bucket/prefix, its credentials and endpoint from the AWS_* variables), or a folder of lakes |
--lake LAKE |
none | exactly this lake, made if it is missing, and nothing guessed. For scripts and services (--dir is the same) |
--lakes FOLDER |
none | exactly this folder of lakes, each a database. For scripts and services. --lake on a folder of lakes, or --lakes on a lake, is refused and says which you meant |
--default NAME |
lake, else the only one |
serving a folder of lakes: the database a client that names none gets (HTTP without /db/, Postgres without a database) |
--addr ADDR |
127.0.0.1:8080 |
the address the HTTP API and the console listen on, and other nodes reach this one at |
--advertise ADDR |
--addr |
the address other nodes reach this one at, when it isn’t --addr. A database’s node, behind the process that serves its folder, is host:port/db/name |
--reader |
off | a read-only node: SQL only, never leads. Any number can run beside the writer |
--python PYTHON |
none | run Python functions and procedures (LANGUAGE python) with this Python, which has the pondra package and pyarrow. auto takes the first one found. Routines run on warm workers beside the node, gone after a minute idle |
--stop-with-stdin |
off | stop, as on Ctrl-C, when standard input closes: when the program that started the node ends, however it ends |
| Flag | Default | Meaning |
|---|---|---|
--pg ADDR |
off | also speak the Postgres protocol here, such as 0.0.0.0:5432: Postgres port |
--kafka ADDR |
off | also speak the Kafka protocol here, such as 0.0.0.0:9092: Kafka protocol |
--kafka-advertise HOST:PORT |
the host of --addr, the port of --kafka |
where Kafka clients are told to connect to this node |
--flight ADDR |
off | also speak Arrow Flight and Flight SQL here, such as 0.0.0.0:8815: Arrow Flight |
The HTTP API, /mcp and the Iceberg REST catalog are on --addr.
Access tokens
Section titled “Access tokens”| Flag | Environment | Meaning |
|---|---|---|
--read-token TOKEN |
PONDRA_READ_TOKEN |
reading needs any token |
--write-token TOKEN |
PONDRA_WRITE_TOKEN |
writing rows needs this one or the admin token |
--admin-token TOKEN |
PONDRA_ADMIN_TOKEN |
creating tables, views, tasks and functions needs this one |
Give every node the same tokens. With none set there are no checks. A flag beats its environment variable. See HTTP for what each role may do. Prefer the environment: a flag shows in the process list.
| Flag | Environment | Meaning |
|---|---|---|
--tls-cert FILE |
PONDRA_TLS_CERT |
a PEM certificate: HTTP, Postgres, Kafka and Flight take TLS on their own ports, and plain connections only from this machine |
--tls-key FILE |
PONDRA_TLS_KEY |
its key |
--tls-ca FILE |
PONDRA_TLS_CA |
the authority that signs the nodes’ certificates: mutual TLS between nodes |
See security. Serving a folder of lakes, the server takes TLS, and each database’s node, on this machine only, doesn’t.
Writes and the log
Section titled “Writes and the log”| Flag | Default | Meaning |
|---|---|---|
--ack durable|replicated |
durable |
when a write is acknowledged. durable: once it is in the bucket (a millisecond on local disk, hundreds on S3 or R2). replicated: once --replicas nodes hold it, which takes milliseconds on any storage. Give every node the same setting |
--replicas N |
2 |
with --ack replicated: how many nodes hold a write before it is acknowledged (the leader is one). Until enough followers are up, writes are acknowledged once durable |
--fsync |
off | with --ack replicated: followers flush each copy to disk before acknowledging it, so an acknowledged write survives a power loss of a follower too. It costs a disk flush per change |
--flush-ms N |
0 |
the least number of milliseconds between a node’s flushes. 0 flushes as soon as the last flush is committed, and what queued up meanwhile goes together: the lowest latency |
--backlog N |
10000000 |
rows allowed to wait in the log for tiering before commits pause. Lower it for less memory, raise it to absorb longer bursts |
Tiering and retention
Section titled “Tiering and retention”| Flag | Default | Meaning |
|---|---|---|
--tier-secs SECS |
10 |
tiering starts as soon as rows commit, at most once per this many seconds (fractions are fine). It decides how soon new rows are Parquet, and a Delta or Iceberg version other engines can read. Pondra’s own reads see every commit at once. 0 tiers only on POST /tier. A table with a million rows waiting is tiered within a second anyway |
--task-ms N |
1000 |
streaming tasks run as soon as new rows commit, and at least this often (in milliseconds) |
--retain-secs N |
60 |
how long to keep consumed log segments and replaced files before deleting them |
--changelog-secs N |
0 |
keep the log this many seconds as a change feed: /watch/{table}?after=N and Kafka consumers can replay every change since then. 0: only --retain-secs |
--publish FORMATS |
none | open formats new tables are also published in, for engines that do not know Pondra: delta, iceberg or delta,iceberg. A table can set its own with publish |
Memory and cache
Section titled “Memory and cache”| Flag | Default | Meaning |
|---|---|---|
--memory-gb GB |
a third of the machine’s | memory for queries (also PONDRA_MEMORY_GB). A sort, join or aggregation that needs more spills to the temp directory |
--cache-dir DIR |
<temp dir>/pondra-cache |
for a lake on object storage: where the node keeps its local SSD copy of recent objects |
--cache-gb GB |
20, or a quarter of the free disk if that is less |
the size of that SSD tier. 0 turns it off |
Other lakes
Section titled “Other lakes”| Flag | Meaning |
|---|---|
--attach NAME=DIR |
read another lake as name.table, and write to it through its own leader. Repeat it for more. Several clusters, each leading its own lake, can share one bucket this way |
--attach-found FOLDER |
for as long as the node runs, attach the lakes in this folder (its subfolders that hold one) under their folder names. Nothing is saved in the catalog. The shell passes its own folder |
pondra serve s3://my-bucket/lake --addr 10.0.0.5:8080 \ --pg 0.0.0.0:5432 --kafka 0.0.0.0:9092 --kafka-advertise 10.0.0.5:9092 --flight 0.0.0.0:8815 \ --ack replicated --attach sales=s3://my-bucket/salesA folder of lakes
Section titled “A folder of lakes”pondra serve data serves every lake in the folder as a database, behind one HTTP port and one
Postgres port. Postgres clients pick a database by name, HTTP clients add /db/{name} to the URL,
and the console at / lists them all. Each database runs as a node of its own, started when first
used and stopped when idle (PONDRA_DATABASE_IDLE_SECS, 600). See
Many databases.
pondra serve data --addr 0.0.0.0:8080 --pg 0.0.0.0:5432Every other option of pondra serve applies to each database’s node: --pg, the tokens
(--read-token, --write-token, --admin-token), the TLS files (to the server’s own doors),
--python, --reader (every database
read-only), --tier-secs, --memory-gb, --cache-dir, --cache-gb, --ack, --replicas,
--fsync, --publish, --flush-ms, --task-ms, --retain-secs, --changelog-secs and
--backlog. Each database is a process of its own, so a value such as --memory-gb is for each
database’s node, not for all of them together.
--flight, --kafka (and --kafka-advertise), --attach, --advertise and --attach-found are
refused, and the message names the flag. A Flight or Kafka port speaks for one lake today. Routing
them to many databases (Kafka by topic, such as sales.orders, and Flight by the database the client
names) is planned for the next round. Serve such a lake on its own:
pondra serve data/sales --flight 0.0.0.0:8815pondra sql
Section titled “pondra sql”pondra sql --dir LAKE "SQL" runs one statement against the lake, with no server. A query reads
the lake (and files on this machine: SELECT * FROM 'x.parquet') and prints its rows as a table.
A write (CREATE, INSERT, UPDATE, DELETE) runs here too: the leader records it over HTTP,
or through the bucket if this machine cannot reach the leader, or this process does if nobody
leads. A write prints its outcome as JSON.
| Option | Default | Meaning |
|---|---|---|
--dir DIR (or --lake) |
required | the lake |
--attach NAME=DIR |
none | another lake to read as name.table. Repeat it for more |
pondra sql --dir shell-lake "SELECT id, amount * 2 AS double_amount FROM orders ORDER BY id"pondra sql --dir shell-lake "INSERT INTO orders VALUES (3, 1.0)"+----+---------------+| id | double_amount |+----+---------------+| 1 | 15.0 || 2 | 9.0 |+----+---------------+{"committed":true}pondra run
Section titled “pondra run”pondra run FILE LAKE runs a SQL file: its statements in order, and $name in them takes the
value of --name on the command line. With a lake, it starts a node for the file, and stops it
when the file is done. With --url it runs the file on a node that is already running.
Numbers and true or false are passed as such, and anything else as text. The last statement’s
rows print as a table.
| Option | Default | Meaning |
|---|---|---|
FILE |
required | the SQL file |
LAKE |
lake |
the lake, unless --url |
--url URL |
none | run on the node at this URL |
--token TOKEN |
PONDRA_TOKEN |
a token for that node |
--NAME VALUE |
none | the file’s parameters, after LAKE |
--help |
this, and with FILE the file’s parameters: each DECLARE PARAMETER’s type, default and comment |
printf 'SELECT count(*) AS orders, $day AS day FROM orders;\n' > report.sqlpondra run report.sql shell-lake --day 2026-09-27+--------+------------+| orders | day |+--------+------------+| 3 | 2026-09-27 |+--------+------------+A file that declares its parameters (DECLARE PARAMETER $day DATE = current_date - 1;, with what each
means in the comment above it: see Variables) says so with
--help, and a value given replaces the default, cast to its type:
printf -- '-- The day to count\nDECLARE PARAMETER $day DATE = current_date - 1;\nSELECT count(*) AS orders, $day AS day FROM orders;\n' > daily.sqlpondra run daily.sql --helppondra run daily.sql shell-lake --day 2026-09-27Usage: pondra run daily.sql [LAKE] [--day DATE]
Parameters of daily.sql: --day DATE The day to count · default: current_date - 1
Options: --url URL (a node already running), --token TOKEN (also PONDRA_TOKEN)+--------+------------+| orders | day |+--------+------------+| 3 | 2026-09-27 |+--------+------------+pondra workspace
Section titled “pondra workspace”pondra workspace pull DIR LAKE writes the lake’s files into DIR; pondra workspace push DIR LAKE sends back what changed there since. A file changed on both sides is listed and left as it
is, and the command exits 1. See In git.
| Option | Default | Meaning |
|---|---|---|
DIR |
required | the folder (a pull makes it) |
LAKE |
lake |
the lake, unless --url |
--url URL |
none | sync with the node at this URL |
--token TOKEN |
PONDRA_TOKEN |
a token for that node: a writer’s for a push |
mkdir -p ws/etl && printf 'SELECT count(*) AS orders FROM orders;\n' > ws/etl/count.sqlpondra workspace push ws shell-lakepondra workspace pull ws shell-lakepushed 1, deleted 0 in the lakepulled 0, removed 0 herepondra plan, deploy and the project’s commands
Section titled “pondra plan, deploy and the project’s commands”A project is a folder with pondra.toml, objects/, migrations/ and tests/
(deploy a project). Each command reads it from
--project (this folder by default) and talks to an environment’s node.
| Command | Does |
|---|---|
pondra init [DIR] |
start a project: pondra.toml, objects/, migrations/, tests/ (--name) |
pondra plan |
what a deploy would change, and what it refuses (exit 1 if anything is refused) |
pondra deploy [--test] [--prune] |
the plan, then each step; --test runs tests/ after it, --prune drops what the project no longer declares (exit 1 if it fails) |
pondra test |
run tests/: any row a query returns is a failure (exit 1 if one fails) |
pondra export [DIR] |
the environment’s objects as a project’s files (pondra.toml kept if DIR has one) |
pondra login URL [--token T | --user U] |
keep a token for a server in ~/.pondra (PONDRA_HOME); asked for when not given |
pondra branch [NAME] [--from prod] [--replace] [--drop] |
CREATE DATABASE NAME CLONE prod on prod’s node (NAME: the git branch’s); --drop lets it go |
pondra diff [--against prod] |
the objects that differ and the rows of each table both have, as Markdown |
Where the environment is:
| Option | Default | Meaning |
|---|---|---|
--env NAME, -e |
PONDRA_ENV, else the git branch’s database, else pondra.toml’s only environment |
the environment: [env.NAME] in pondra.toml, or a database on the project’s server |
--url URL |
[env.NAME] url, else [project] server + /db/NAME |
the node to talk to |
--lake LAKE |
[env.NAME] lake |
start a node for this lake instead |
--token TOKEN |
PONDRA_TOKEN, else pondra login’s |
a token for it: an admin’s to deploy, a reader’s to plan |
--project DIR |
. |
the project’s folder |
A database name is letters, digits and _: pr-123 and feature/x become pr_123 and
feature_x. A deploy sends [env.NAME]’s values ([env.dev]‘s for a database pondra.toml doesn’t
name), the git commit, and [secrets]’ values from this machine’s environment.
pondra catalog
Section titled “pondra catalog”pondra catalog --dir LAKE PREFIX prints the catalog’s entries whose keys start with PREFIX: t/
for tables, s/ for log segments, p/ for producers. It is for looking inside a lake, and what
it prints is the lake’s own format: see the lake format.
pondra catalog --dir shell-lake t/ | cut -c1-40t/orders {"columns":[["id","Int64"],["am