SQL Dispatcher¶
The SQL dispatcher is the heart of RockLake's translation layer. It sits between the raw SQL strings that arrive over the PostgreSQL wire protocol and the structured catalog operations that the executor performs against the key-value store. Its job is deceptively simple: take a SQL string, determine which specific catalog operation it represents, extract its parameters, and hand everything off to the executor in a strongly-typed form. No ambiguity, no interpretation, no creative SQL execution — just pattern recognition against a finite set of known statement shapes.
The dispatcher is intentionally bounded. It recognizes approximately 50 statement patterns and rejects everything else. This is not a limitation born of laziness — it is a deliberate architectural choice that provides security guarantees (no SQL injection, no information disclosure), predictability (the complete behavior of RockLake is enumerable), and testability (every supported pattern can be exhaustively tested against recorded wire sessions).
This page explains how the classification pipeline works, catalogs every recognized statement kind, describes the pattern matching algorithm, and discusses the security and extensibility properties of this design.
The Classification Pipeline¶
graph LR
SQL["Raw SQL String<br/>(from PG wire)"] --> PARSE["sqlparser-rs<br/>(PostgreSQL dialect)"]
PARSE --> AST["AST Node<br/>(structured tree)"]
AST --> CLASS["Pattern Matcher<br/>(~50 patterns)"]
CLASS --> SK["StatementKind<br/>(typed enum)"]
SK --> EXEC["Executor<br/>(catalog operations)"] The pipeline has four distinct stages, each with clear inputs and outputs:
Stage 1: Parsing¶
The raw SQL string is parsed using sqlparser-rs configured with the PostgreSQL dialect. This produces a structured Abstract Syntax Tree (AST) that represents the syntactic structure of the SQL without any semantic interpretation. Parse errors at this stage (malformed SQL, unrecognized syntax) are returned immediately as SqlDispatchError::ParseError with the original error message, which is then encoded as a PostgreSQL ErrorResponse wire message with SQLSTATE 42601 (syntax error).
The parser is intentionally lenient about SQL features it does not need to understand deeply. It parses SELECT, INSERT, UPDATE, DELETE, BEGIN, COMMIT, ROLLBACK, SET, SHOW, CREATE, ALTER, and DROP at the syntactic level. The semantic filtering (determining whether the specific statement is one RockLake supports) happens in the next stage.
Stage 2: Classification¶
The AST is matched against a catalog of known patterns. Each pattern is a structural predicate: "Is this a SELECT from the table named ducklake_schema?" or "Is this an INSERT into ducklake_data_file with these specific columns?" The classification checks the shape of the AST — which tables are referenced, what columns appear in the SELECT list or INSERT column list, what the WHERE clause looks like — and maps it to a specific StatementKind variant.
If no pattern matches, the statement is classified as StatementKind::Unsupported, and the executor returns an error. There is no fallback execution path, no "try to run it anyway" behavior, and no pass-through to an underlying database. If RockLake does not explicitly recognize a statement pattern, it does not execute it.
Stage 3: Parameter Extraction¶
Once the pattern is identified, relevant values are extracted from the AST and converted to their expected types:
- Integer literals become
u64ori64 - String literals become
String - Boolean literals become
bool - NULL values become
Option::None - Parameter placeholders (
$1,$2) are resolved against the provided parameter values
The extracted parameters become the fields of the StatementKind variant. For example, SelectColumns { table_id: 42, snapshot_id: Some(100) } contains everything the executor needs to perform the operation without re-examining the original SQL.
Stage 4: Dispatch to Executor¶
The fully-classified, parameterized StatementKind is handed to the executor, which performs the corresponding catalog operation. The executor never sees raw SQL — it only sees typed enum variants with validated parameters. This separation ensures that the executor cannot accidentally misinterpret SQL and that all SQL interpretation is concentrated in one auditable location (the dispatcher).
Complete Statement Catalog¶
The following tables document every recognized statement kind, organized by category. This is the complete, bounded set — there are no hidden statement types or undocumented behaviors.
Session and Connection Management¶
| Statement Kind | SQL Pattern | Purpose |
|---|---|---|
SelectVersion | SELECT version() | Returns RockLake's version string (mimics PostgreSQL) |
SelectCurrentSchema | SELECT current_schema | Returns "public" (DuckDB compatibility) |
SelectCurrentDatabase | SELECT current_database() | Returns "ducklake" |
SelectPgType | SELECT * FROM pg_type WHERE ... | OID resolution for DuckDB type mapping |
ShowVariable(name) | SHOW timezone, SHOW client_encoding | Returns session variable values |
SetVariable(name, value) | SET timezone = 'UTC' | Sets session variables (minimal set) |
SelectOne | SELECT 1 | Connection health check |
Transaction Control¶
| Statement Kind | SQL Pattern | Purpose |
|---|---|---|
Begin | BEGIN or START TRANSACTION | Opens a new transaction |
Commit | COMMIT or END | Commits and flushes buffered operations |
Rollback | ROLLBACK or ABORT | Discards buffered operations |
Catalog Reads — Schemas and Tables¶
| Statement Kind | SQL Pattern | Purpose |
|---|---|---|
SelectMaxSnapshot | SELECT MAX(snapshot_id) FROM ducklake_snapshot | Current snapshot ID |
SelectSchemas | SELECT * FROM ducklake_schema WHERE ... | List schemas (MVCC-filtered) |
SelectTables | SELECT * FROM ducklake_table WHERE schema_id = $1 | List tables in a schema |
SelectColumns | SELECT * FROM ducklake_column WHERE table_id = $1 | List columns for a table |
SelectViews | SELECT * FROM ducklake_view WHERE schema_id = $1 | List views in a schema |
SelectMacros | SELECT * FROM ducklake_macro WHERE ... | List macros |
SelectMacroImpls | SELECT * FROM ducklake_macro_impl WHERE ... | Macro implementations |
SelectMacroParams | SELECT * FROM ducklake_macro_parameters WHERE ... | Macro parameters |
Catalog Reads — Files and Statistics¶
| Statement Kind | SQL Pattern | Purpose |
|---|---|---|
SelectDataFiles | SELECT * FROM ducklake_data_file WHERE table_id = $1 | List data files for query planning |
SelectDeleteFiles | SELECT * FROM ducklake_delete_file WHERE ... | List delete files (for merge-on-read) |
SelectFileColumnStats | SELECT * FROM ducklake_file_column_stats WHERE ... | Column statistics for predicate pushdown |
SelectFileVariantStats | SELECT * FROM ducklake_file_variant_stats WHERE ... | Variant type statistics |
SelectTableStats | SELECT * FROM ducklake_table_stats WHERE ... | Aggregate table statistics |
SelectInlinedData | SELECT * FROM ducklake_inlined_data_tables WHERE ... | Small table data stored in catalog |
SelectMetadata | SELECT * FROM ducklake_metadata WHERE ... | Key-value metadata entries |
SelectSnapshots | SELECT * FROM ducklake_snapshot WHERE ... | Snapshot history |
SelectSnapshotChanges | SELECT * FROM ducklake_snapshot_changes WHERE ... | Per-snapshot change log |
Catalog Writes — Entity Creation¶
| Statement Kind | SQL Pattern | Purpose |
|---|---|---|
InsertSchema | INSERT INTO ducklake_schema (...) VALUES (...) | Create a new schema |
InsertTable | INSERT INTO ducklake_table (...) VALUES (...) | Create a new table |
InsertColumn | INSERT INTO ducklake_column (...) VALUES (...) | Add a column to a table |
InsertView | INSERT INTO ducklake_view (...) VALUES (...) | Create a view |
InsertMacro | INSERT INTO ducklake_macro (...) VALUES (...) | Create a macro |
InsertDataFile | INSERT INTO ducklake_data_file (...) VALUES (...) | Register a data file |
InsertDeleteFile | INSERT INTO ducklake_delete_file (...) VALUES (...) | Register a delete file |
InsertSnapshot | INSERT INTO ducklake_snapshot (...) VALUES (...) | Record a new snapshot |
InsertSnapshotChanges | INSERT INTO ducklake_snapshot_changes (...) VALUES (...) | Record snapshot changes |
InsertFileColumnStats | INSERT INTO ducklake_file_column_stats (...) VALUES (...) | Record column statistics |
InsertMetadata | INSERT INTO ducklake_metadata (...) VALUES (...) | Store metadata key-value |
Catalog Writes — Updates¶
| Statement Kind | SQL Pattern | Purpose |
|---|---|---|
UpdateEndSnapshot | UPDATE ducklake_X SET end_snapshot = $1 WHERE ... | Supersede a versioned entity |
UpdateTableStats | UPDATE ducklake_table_stats SET ... WHERE ... | Update aggregate statistics |
UpdateMetadata | UPDATE ducklake_metadata SET ... WHERE ... | Update metadata value |
DeleteMetadata | DELETE FROM ducklake_metadata WHERE ... | Remove metadata entry |
DeleteScheduledFiles | DELETE FROM ducklake_files_scheduled_for_deletion WHERE ... | Clear file deletion schedule |
How Pattern Matching Works¶
The classifier uses a multi-step structural matching approach. Let's walk through two examples to make this concrete.
Example: Matching a SELECT Query¶
When the dispatcher receives:
SELECT * FROM ducklake_column WHERE table_id = 42 AND begin_snapshot <= 100
AND (end_snapshot IS NULL OR end_snapshot > 100)
The matching process:
- Statement type check: Is this a
SELECT? Yes. - FROM clause check: Is the table referenced
ducklake_column? Yes. - WHERE clause decomposition: Extract the conjuncts:
table_id = 42→ numeric equality filter on known columnbegin_snapshot <= 100→ MVCC lower boundend_snapshot IS NULL OR end_snapshot > 100→ MVCC upper bound
- Pattern match: This matches the
SelectColumnspattern withtable_id = 42, snapshot_id = 100. - Classification: Return
StatementKind::SelectColumns { table_id: 42, snapshot_id: Some(100) }.
Example: Matching an INSERT Statement¶
When the dispatcher receives:
INSERT INTO ducklake_data_file (table_id, data_file_id, path, format, row_count, file_size, snapshot_id)
VALUES (5, 1001, 's3://bucket/file.parquet', 'parquet', 50000, 4194304, 42)
The matching process:
- Statement type check: Is this an
INSERT? Yes. - Target table check: Is the target
ducklake_data_file? Yes. - Column list check: Do the columns match the expected set for this table? Yes.
- Value extraction: Extract each value and convert to the appropriate type.
- Classification: Return
StatementKind::InsertDataFile { table_id: 5, data_file_id: 1001, path: "s3://...", format: "parquet", row_count: 50000, file_size: 4194304, snapshot_id: 42 }.
Extended Query Mode and Parameter Binding¶
DuckDB uses both Simple Query mode (SQL with embedded literals) and Extended Query mode (prepared statements with parameter placeholders). The dispatcher handles both transparently:
Simple Query mode: Literal values are extracted directly from the AST. The SQL WHERE table_id = 42 produces an AST with a literal integer node containing 42.
Extended Query mode: Parameter placeholders ($1, $2, ...) appear in the AST. The dispatcher records their positions, and actual values are provided separately in the ParamValues struct during the Bind step of the extended query protocol.
The ParamValues struct provides typed accessors:
get_u64(index)— Parse a parameter as unsigned 64-bit integerget_i64(index)— Parse a parameter as signed 64-bit integerget_string(index)— Get a parameter as stringget_bool(index)— Parse a parameter as booleanget_optional_string(index)— Get a parameter that may be NULL
Type mismatches (e.g., a non-numeric string where a u64 is expected) are reported as SqlDispatchError::TypeMismatch with the parameter index and expected type.
Security Properties¶
The bounded dispatcher provides strong security guarantees that would be difficult to achieve with a general-purpose SQL engine:
No SQL Injection¶
Because the dispatcher matches on AST structure (not string patterns), there is no way to inject additional SQL through crafted input. A malicious value in a WHERE clause is just a literal value node in the AST — it cannot "escape" into additional statements or clauses. The parser handles quoting and escaping; the classifier only sees structured nodes.
No Information Disclosure¶
Unsupported queries are rejected before any catalog data is accessed. An attacker cannot probe the catalog through creative SQL — SELECT * FROM pg_stat_activity, SELECT * FROM information_schema.tables, or any other introspective query returns "unsupported statement" without leaking any information about the catalog's contents.
No Dynamic SQL¶
There is no EXECUTE, no PREPARE (in the sense of server-side prepared statements with arbitrary SQL), and no function call that could generate dynamic SQL. The statement surface is fixed at compile time.
Auditable Surface¶
The complete set of supported statements is documented on this page. An auditor can verify that no dangerous operations are possible by reviewing approximately 50 match arms in the classifier. This is orders of magnitude simpler than auditing a full PostgreSQL SQL engine with its thousands of code paths.
Extending the Dispatcher¶
When a new version of DuckDB's ducklake extension introduces new SQL patterns (for a new catalog feature or protocol change), the dispatcher must be updated. The process is:
- Capture: Record wire sessions between DuckDB and the new feature using the wire corpus tooling
- Analyze: Identify the new SQL patterns in the recorded sessions
- Add variants: Add new variants to the
StatementKindenum - Add matchers: Add new match arms in the classifier
- Add handlers: Add new execution handlers in the executor
- Test: Run the new patterns through the dispatcher and verify correct classification
- Regression test: Ensure existing patterns still classify correctly
This explicit, manual process ensures that every new SQL pattern RockLake supports is a deliberate decision with test coverage. There is no "auto-discovery" of new SQL patterns — each one is reviewed, understood, and intentionally supported.
Further Reading¶
- PG-Wire Protocol — How SQL strings arrive from the network
- Transaction Model — How BEGIN/COMMIT/ROLLBACK interact with the executor
- Design Decisions: Bounded SQL — The philosophy behind the bounded approach
- Internals: Wire Corpus — The test infrastructure for recording and replaying SQL patterns