Skip to Content
We are live but in Staging 🎉
TablesAPI ReferenceExecute (SQL)Overview

Execute — API Reference

Package: dodil.k3.tables.v1 · Service: TableService

The canonical SQL surface. Send a DuckDB SQL string; K3’s planner parses it, picks the right dispatch strategy (keyed/non-keyed write, eventual/strong/federated read, DDL, CTAS), and runs it. Every shape Data RPCs cover is reachable here, plus everything they don’t (JOINs, CTEs, window functions, CTAS, ALTER TABLE).

RPCHTTP
ExecutePOST /:bucket/tables/_execute
MaterializePOST /:bucket/tables/:source_table/materialize

Request / response

rpc Execute(ExecuteRequest) returns (ExecuteResponse); message ExecuteRequest { string bucket = 1; string sql = 2; // DuckDB-flavored SQL Freshness freshness = 3; // Read-freshness override — ignored for write/DDL } // Response is a oneof — the variant matches the SQL kind. message ExecuteResponse { oneof result { ExecuteQueryResult query = 1; ExecuteWriteResult write = 2; ExecuteDdlResult ddl = 3; } } message ExecuteQueryResult { repeated string columns = 1; repeated string rows = 2; // JSON-encoded rows int64 row_count = 3; ServedBy served_by = 4; // WAREHOUSE | MERGED | OLTP QueryStrategy strategy = 5; string strategy_reason = 7; // e.g. "table is not partitioned" repeated string warnings = 6; } message ExecuteWriteResult { int64 rows_written = 1; WriteStrategy strategy = 2; bool noop = 3; // true when keys list resolved to zero rows bool pending_drain = 4; // true when write went through the write log string target_table = 5; repeated string warnings = 6; } message ExecuteDdlResult { string op = 1; // "create_table" | "create_table_as_select" | "alter_table" | "drop_table" string target_table = 2; optional int64 version = 3; // present for CREATE/ALTER; absent for DROP }

Generic Execute invocation via grpcurl:

grpcurl \ -H "Authorization: Bearer $DODIL_TOKEN" \ -d '{ "bucket": "kb-prod", "sql": "SELECT 1" }' \ $K3_GRPC \ dodil.k3.tables.v1.TableService/Execute

SQL constraints: one statement per call. No BEGIN/COMMIT/ROLLBACK. The planner refuses both — see Refusals.

For the dialect itself (types, JSON ops, statement shapes), see SQL Compatibility.

Sub-pages — by SQL shape

  • DDLCREATE TABLE (with PRIMARY KEY + PARTITIONED BY), CTAS (type inference), ALTER TABLE ADD COLUMN, DROP TABLE. Start here — define the table before you query or write to it.
  • SELECT — read strategies (UNARY_WAREHOUSE, UNARY_MERGED_STRONG, FEDERATED_AGGREGATE / SCAN / TO_SINGLE), JSON column reads, joins / CTEs / windows.
  • INSERT — keyed (KEYED_INSERT_SINGLE / BULK / FROM_SELECT) and non-keyed (NON_KEYED_INSERT, NON_KEYED_INSERT_FROM_SELECT).
  • UPDATE — keyed (KEYED_UPDATE, KEYED_RANGE, KEYED_FROM_SUBQUERY) and non-keyed (with WAL-bypass warning and the two safe patterns).
  • DELETE — keyed (KEYED_DELETE, KEYED_RANGE_DELETE, KEYED_DELETE_FROM_SUBQUERY) and non-keyed.
  • MERGEMERGE_ROWS / MERGE_QUERY / MERGE_TABLE, WHEN MATCHED THEN DELETE, pre/post-drain semantics.
  • Materialize — structured SELECT INTO-style RPC; same dispatcher as CTAS with separate inputs for source / SQL / target / partitioning / mode.

Freshness — read-side request control

For SELECT plans, you control freshness via ExecuteRequest.freshness; the planner picks the strategy based on table size + partitioning + the query shape.

Freshness requestTypical strategyserved_byWhat it does
EVENTUAL (default)UNARY_WAREHOUSE or FEDERATED_*WAREHOUSEDelta-only — OLAP-optimized, may miss recent un-drained writes
STRONGUNARY_MERGED_STRONGMERGEDWrite log + Delta merged on read — read-your-writes
OLTP_ONLY(single-pod write-log scan)OLTPWrite log only — niche; for point lookups during heavy backlog

QueryStrategy — read-side observability

Returned on ExecuteQueryResult.strategy. Full per-strategy walkthrough on SELECT.

StrategyWhen the planner picks it
UNARY_WAREHOUSEDefault analytical read — Delta-only, no fan-out
UNARY_MERGED_STRONGfreshness=STRONG — single-pod merge of write log + Delta
FEDERATED_AGGREGATELarge partitioned table + aggregate SELECT — fan-out + reduce
FEDERATED_SCANLarge partitioned table + non-aggregate SELECT — fan-out + concat
FEDERATED_TO_SINGLEFederated plan downgraded — strategy_reason says why

WriteStrategy — write-side observability

Returned on ExecuteWriteResult.strategy. The planner classifies the SQL into one of 16 variants based on: (1) does the table have merge_keys, (2) does the predicate match all merge_keys, (3) is the SQL shape INSERT / UPDATE / DELETE / MERGE.

StrategySQL shapePage
KEYED_INSERT_SINGLE / _BULK / _FROM_SELECTINSERT INTO pk_table VALUES (...) / multi-VALUES / INSERT ... SELECTINSERT
NON_KEYED_INSERT / _FROM_SELECTINSERT INTO no_pk_table ...INSERT
KEYED_UPDATE / KEYED_RANGE / KEYED_FROM_SUBQUERYUPDATE with PK predicate / range / subqueryUPDATE
NON_KEYED_UPDATE ⚠️UPDATE whose predicate doesn’t match merge_keysUPDATE
KEYED_DELETE / KEYED_RANGE_DELETE / KEYED_DELETE_FROM_SUBQUERYDELETE with PK predicate / range / subqueryDELETE
NON_KEYED_DELETE ⚠️DELETE whose predicate doesn’t match merge_keysDELETE
MERGE_ROWS / MERGE_QUERY / MERGE_TABLEMERGE INTO ... USING ... (source = VALUES list, subquery, or table)MERGE

Keyed strategies route through the write log → compactor MERGE on next drain. Safe for concurrent writes; visible immediately under freshness=STRONG.

Non-keyed strategies ⚠️ go directly to Delta, bypassing the write log. If pending log entries for the same rows haven’t compacted yet, the next drain may overwrite the Delta-side change. See UPDATE → Non-keyed safe patterns.

ServedBy

Returned on ExecuteQueryResult.served_by to make it obvious which engine answered.

ValueMeaning
SERVED_BY_WAREHOUSEDelta-only read
SERVED_BY_MERGEDWrite log + Delta merged at query time
SERVED_BY_OLTPWrite log only

What you can put in an Execute SQL string

SQL shapeStrategy familyRoutes through
SELECT (no freshness override)UNARY_WAREHOUSE / FEDERATED_*Delta
SELECT ... freshness=STRONGUNARY_MERGED_STRONGWrite log + Delta
INSERT INTO pk_table ...KEYED_INSERT_*Write log
INSERT INTO no_pk_table ...NON_KEYED_INSERT*Delta
UPDATE with PK predicateKEYED_UPDATE / _RANGE / _FROM_SUBQUERYWrite log
UPDATE without PK predicateNON_KEYED_UPDATE ⚠️Delta-direct
DELETE with PK predicateKEYED_DELETE / _RANGE_DELETE / _DELETE_FROM_SUBQUERYWrite log
DELETE without PK predicateNON_KEYED_DELETE ⚠️Delta-direct
MERGE INTO ... USING <values|select|table>MERGE_ROWS / MERGE_QUERY / MERGE_TABLEWrite log
CREATE TABLE [AS SELECT]DDLDelta + sidecar
ALTER TABLE ADD COLUMNDDLDelta + sidecar
DROP TABLEDDLDelta + sidecar

Refusals

The following are valid DuckDB SQL but K3 refuses them:

RefusedWhyWorkaround
BEGIN / COMMIT / ROLLBACKNo transaction coordinatorFor multi-statement ACID, use Postgres. Individual Execute calls are atomic against Delta.
Multi-statement bodies (stmt1; stmt2;)One SQL per callChain on the caller side.
ALTER TABLE DROP COLUMNNot implementedRecreate via CTAS without the column.
ALTER TABLE RENAME COLUMNNot implementedRecreate via CTAS with the new name.

When the response says noop

ExecuteWriteResult.noop = true means the planner picked a strategy but the keys list resolved to zero rows — no dispatch happened. Common cases:

  • KEYED_RANGE / KEYED_RANGE_DELETE whose range matched no existing PKs
  • KEYED_FROM_SUBQUERY where the subquery returned empty

Read it as a clean “no rows affected” — same outcome as a SELECT with zero matches.

See also