Query language
A typed SQL subset
with explicit semantics.
The query language is a limited native subset. It includes a parser, nominal type checking, three-valued logic, and WAL-protected DML. It is not a complete SQL dialect.
| Area | Supported now | Not supported |
|---|---|---|
| SELECT | Qualified / unqualified columns, wildcard projection, LIMIT | Arbitrary expression projection, output aliases, DISTINCT |
| FROM / JOIN | AS and shorthand aliases, chained INNER JOIN … ON, self joins | Outer joins, USING, join reordering, hash / merge join |
| Predicates | AND / OR / NOT, comparisons, IS NULL, parentheses | IN / BETWEEN / LIKE, subqueries |
| DML | Single-row INSERT with an explicit column list, UPDATE, DELETE, optional WHERE | Defaults, RETURNING, UPSERT, multi-table writes |
| ORDER BY | Multi source-column keys, ASC / DESC, NULLS FIRST / LAST | Aliases, ordinals, arbitrary sort expressions |
| Aggregates | COUNT(*) / COUNT / SUM / MIN / MAX, source-column GROUP BY | HAVING, DISTINCT aggregates, grouping expressions, ROLLUP |
Nominal types
Schema columns keep both a physical representation and an optional nominal semantic type. HIR requires nominal compatibility in comparisons, so contextual NULL typing cannot make UserId = TeamId legal. Self joins distinguish two occurrences of the same TableId with query-local RelationBindingId values.
physical: UINT64 semantic: UserId UserId ≠ TeamId even when both are u64
NULL is a database value
Database NULL is an explicit ScalarValue::Null. Rust Option remains reserved for absent clauses or metadata. Comparisons with NULL yield UNKNOWN; IS NULL / IS NOT NULL are the explicit tests. AND / OR / NOT use SQL three-valued logic. WHERE and JOIN ON keep only TRUE; FALSE and UNKNOWN are rejected.
Bool(true) → TRUE Bool(false) → FALSE NULL → UNKNOWN NULL = NULL → UNKNOWN NULL IS NULL → TRUE
JOIN
An alias hides the underlying table name. Qualified columns resolve through the exposed relation name; unqualified columns are accepted only when exactly one visible relation provides the name. Each ON can see the complete left subtree and its current right relation, but not later joins. Nested-loop execution preserves duplicates in deterministic left-major, right-minor order.
SELECT e.name, m.name
FROM employees e
JOIN employees m ON e.manager_id = m.id
WHERE e.active IS NOT NULL
ORDER BY e.name ASC NULLS LAST
LIMIT 20DML
Typed DML uses the same compiler, transaction, full-page WAL, rollback, and recovery path as heap writes. Database::execute returns query rows or an explicit AffectedRows(u64); query rejects mutating statements. Omitted nullable INSERT columns become NULL; omitted non-nullable columns are rejected. UPDATE evaluates every right-hand side against the original row, so SET a = b, b = a swaps.
INSERT INTO users (id, name) VALUES (1, 'Ada');
UPDATE users SET name = 'Ada Lovelace' WHERE id = 1;
DELETE FROM users WHERE name IS NULL;Sort and aggregates
The ordinary plan is Scan/Join → Filter → Sort → Project → Limit. The aggregate plan is Scan/Join → Filter → Aggregate → Limit. Keys resolve against the complete FROM / JOIN scope before projection, so a query may sort by a column it does not return.
COUNT(*) counts rows; COUNT(column) ignores NULL. Numeric SUM uses checked arithmetic and strips nominal meaning. MIN / MAX preserve the input SemanticType. NULLs at a grouping key share one group, unlike expression NULL = NULL, which remains UNKNOWN. Grouped queries currently reject ORDER BY.
SELECT team_id, COUNT(*), SUM(score), MAX(score)
FROM scores
GROUP BY team_idIndexes and ANALYZE
create_index registers a non-unique single-column index after a transactional backfill. Subsequent heap and SQL DML maintains registered indexes. Eligible equality and IS NULL predicates can select a point IndexScan; analyzed two-sided Int64/UInt64 bounds can select a range IndexScan. ANALYZE is explicit and is not maintained by DML. SQL index DDL is not available.
create_index(table, column)
→ transactional backfill
→ register in IndexCatalog
→ DML maintains the index
→ ANALYZE writes a cost snapshot
→ planner may choose IndexScanMulti-table writes are still unsupported
The core composes unchanged one-table heap files with create_tables / open_tables. JOIN did not change the page, WAL, recovery, or transaction format. Cross-table write transactions remain unsupported.