Quickstart
Five minutes from here you’ll have a real table, a few rows in it, and a working SQL query. Engine setup isn’t a step — every bucket gets an auto-enabled table engine on CreateBucket, so you go straight to CreateTable.
Prerequisites
dodilCLI installed anddodil logindone — CLI Basics- A bucket — we’ll use
kb-prod:dodil k3 bucket create kb-prod -d "Tables quickstart"
1. Create a table
We’ll model a small events table with a composite primary key and one JSON column for arbitrary payload:
dodil k3 table create events --bucket kb-prod \
--columns 'id:long:PK,user_id:string:PK,occurred_at:timestamp,event_type:string,payload:json' \
--partition-columns event_typeWhat that creates:
| Column | Type | Notes |
|---|---|---|
id | long | Primary key (composite, alongside user_id) |
user_id | string | Primary key (composite) |
occurred_at | timestamp | Microseconds-since-epoch UTC |
event_type | string | Partition column — rows physically grouped by this |
payload | json | Arbitrary JSON — queryable with DuckDB JSON operators |
Column types:
string·int·long·short·float·double·boolean·date·timestamp·binary·json. Full reference in SQL Compatibility.
Verify the table is ACTIVE:
dodil k3 table describe events --bucket kb-prod -o json2. Insert rows
Bulk insert via the structured shortcut:
dodil k3 table insert events --bucket kb-prod \
--rows '[
{"id": 1, "user_id": "u-101", "occurred_at": 1716840000000000, "event_type": "click", "payload": {"page": "/pricing"}},
{"id": 2, "user_id": "u-101", "occurred_at": 1716840060000000, "event_type": "click", "payload": {"page": "/signup"}},
{"id": 3, "user_id": "u-102", "occurred_at": 1716840120000000, "event_type": "purchase", "payload": {"sku": "A-12", "amount": 49.99}}
]'The response carries rows_written — counts of rows that landed in the table’s write log (the compactor will fold them into Delta on the next drain). For PK tables, writes go through the log and then into Delta; for no-PK tables, writes append directly to Delta.
Same
INSERTvia raw SQL throughExecute:dodil k3 table query --bucket kb-prod --sql " INSERT INTO events (id, user_id, occurred_at, event_type, payload) VALUES (1, 'u-101', TIMESTAMP '2026-05-27 10:00:00', 'click', '{\"page\":\"/pricing\"}'), (2, 'u-101', TIMESTAMP '2026-05-27 10:01:00', 'click', '{\"page\":\"/signup\"}'), (3, 'u-102', TIMESTAMP '2026-05-27 10:02:00', 'purchase', '{\"sku\":\"A-12\",\"amount\":49.99}') "
3. Query — eventual freshness (default)
dodil k3 table query --bucket kb-prod --sql "
SELECT event_type, COUNT(*) AS n FROM events GROUP BY event_type ORDER BY n DESC
"This is the default — Freshness=EVENTUAL — scans Delta only. Fast, OLAP-optimized. The downside: rows you just inserted may not be visible until the next compaction folds them into Delta.
4. Query — strong freshness (read-your-writes)
When you need to see writes immediately:
dodil k3 table query --bucket kb-prod --sql "
SELECT * FROM events WHERE user_id = 'u-101' ORDER BY occurred_at
" --freshness strongSTRONG merges the write log with Delta on the read path. Pays a small cost vs EVENTUAL; use it sparingly on hot paths, freely for “did my write land?” checks.
Three freshness modes total:
EVENTUAL(default, Delta-only),STRONG(write log + Delta),OLTP_ONLY(write log only — niche, mostly for point-lookups during heavy backlog). See SQL Compatibility.
5. Query a JSON column
JSON columns are first-class — DuckDB’s native JSON operators work directly:
dodil k3 table query --bucket kb-prod --sql "
SELECT user_id, payload->>'sku' AS sku, (payload->>'amount')::double AS amount
FROM events
WHERE event_type = 'purchase'
"->> extracts a JSON field as text; cast to a typed value as needed. Other DuckDB JSON ops also work — ->, json_extract, json_each, etc.
6. Update + merge (the PK paths)
Update a single row via PK:
dodil k3 table update events --bucket kb-prod \
--predicate "id = 1 AND user_id = 'u-101'" \
--updates '{"event_type": "click_pricing"}'Upsert a batch via MERGE:
dodil k3 table merge events --bucket kb-prod \
--match-columns id,user_id \
--rows '[
{"id": 1, "user_id": "u-101", "occurred_at": 1716840000000000, "event_type": "click_pricing", "payload": {"page": "/pricing", "variant": "B"}},
{"id": 4, "user_id": "u-103", "occurred_at": 1716840180000000, "event_type": "signup", "payload": {"plan": "pro"}}
]'MERGE upserts — existing rows (by match_columns) are updated, new rows are inserted. The CLI shape mirrors the API.
For the full SQL shapes that drive these (
UPDATE ... WHERE pk = XvsWHERE non_pk = X,MERGE INTO ... USING ... WHEN MATCHED ...), see SQL Compatibility.
7. Maintenance (optional)
After heavy writes, two quick housekeeping commands:
# Force the write log to drain into Delta now (otherwise it drains automatically)
dodil k3 table compact events --bucket kb-prod
# Compact small Delta files into larger ones (improves analytical query speed)
dodil k3 table optimize events --bucket kb-prodYou usually don’t need to run these manually — K3 maintains the log → Delta drain in the background. They’re for after large bulk writes / nightly batches.
What you just did
| Step | Operation | Surface |
|---|---|---|
| 1 | CreateTable (manual mode) | Tables · admin |
| 2 | Insert | Data · structured shortcut |
| 3 | Query (eventual) | Data · structured shortcut |
| 4 | Query (strong) | Data — with read-your-writes |
| 5 | Query (JSON ops) | Data — DuckDB-native JSON |
| 6 | Update + Merge | Data |
| 7 | Compact + Optimize | Maintenance |
Cleanup
dodil k3 table delete events --bucket kb-prod
dodil k3 bucket delete kb-prod # only if you created it for this quickstartNext steps
- Core Concepts — every type signature (Engine, Table, Column, strategies, freshness, history)
- SQL Compatibility — DuckDB dialect, full statement-shape reference, JSON ops
- API Reference → Execute — the planner-driven SQL surface — every write strategy + read strategy with examples
- Recipes → Pipeline-bound table — auto-extract rows from uploaded documents (no manual
INSERTs) - Recipes → Time-travel + Restore — Delta Lake’s signature feature