Feature matrix
Every SQL form the engine supports, and every one it deliberately rejects.
This page is generated from sql-feature-matrix.json, which is a test fixture rather than a
description. On every test run, each supported example below is executed through both of the
engine's executors, and each rejected example is checked to still fail with the recorded error. A
page built from it cannot drift from what the engine does.
Every entry is keyed to the feature identifier ISO/IEC 9075:2023 (SQL:2023) gives it in Annex F,
so what the engine claims can be checked against the standard rather than against its own
vocabulary. Forms the standard does not define — MATCH and BM25 relevance, ILIKE,
DATE_TRUNC — are marked as extensions instead. Supported forms are also diffed against SQLite
and PostgreSQL on every run, and where the three genuinely disagree the difference is recorded
with the reason rather than hidden: multi-character TRIM removes the whole string here, as the
standard says, while PostgreSQL removes any of its characters.
Deliberate omissions
Some of what is missing is missing on purpose. The reasons fall into three groups.
Because the browser is the runtime
BEGIN … COMMIT works, but with a bound on it. A transaction a statement string can open is one
a lost reference, a closed tab, or a thrown error could leave open forever, holding storage a
background sweep cannot reclaim — so one left untouched rolls itself back, and schema changes are
refused inside it, because the catalog commits outside the scope and a rollback could not take
them back. db.write(async (tx) => { … }) remains the form with no bound to hit: it ends when its
callback does, however it ends.
There is one isolation level, so SET TRANSACTION ISOLATION LEVEL has nothing to set: every read
runs against a single version and every write scope commits atomically.
There are no array, UUID, or INTERVAL column types. Each would need an encoding, a comparison
order, and a set of operators that the columnar format would carry forever, for a gain an
application can get today by storing text and parsing it. There is no JSON column type either,
for the same reason — but the SQL/JSON functions read documents out of ordinary text columns, the
way SQLite does: JSON_VALUE, JSON_QUERY, JSON_EXISTS, JSON_OBJECT, JSON_ARRAY, and the
IS JSON predicate. JSON_TABLE is missing because it produces rows rather than a value, which
the executors have no operator for.
FOREIGN KEY is rejected at CREATE TABLE rather than accepted and ignored: referential actions
would have to run on every write path of every table, and the engine has no cross-table write hook
to hang them on. A constraint that never runs is worse than one the engine declines to promise, so
it fails by name — and a BEFORE trigger can raise instead. CHECK is enforced, on every path
that writes a row.
Because of how mutations work
UPDATE and DELETE need the table to have a unique key. Mutation segments address rows by that
key; a table without one can only be appended to. This is not a SQL-layer restriction — it holds
for every write path.
Not yet, rather than never
Correlated NOT IN and correlated subqueries joined on a non-equality predicate are rejected
because the decorrelation rewrite cannot preserve their semantics, and running them row-by-row
would turn a query that looks ordinary into one that takes minutes. NOT EXISTS expresses the
first correctly today.
Supported
176 forms, each executed through both executors on every test run, and diffed against SQLite and PostgreSQL wherever the three agree on what the answer should be.
select.projectionSQL:2023 E051SELECT region, amount FROM rowsselect.aliasSQL:2023 E051-05SELECT amount AS total FROM rowsselect.wildcardSQL:2023 E051SELECT * FROM rowsselect.distinctSQL:2023 E051-01SELECT DISTINCT region FROM rowsselect.scalar-subquerySQL:2023 F471SELECT (SELECT MAX(amount) FROM rows) AS peak FROM rows LIMIT 1expression.arithmeticSQL:2023 E011-04SELECT amount * 2 + 1 AS scaled FROM rowsexpression.roundSQL:2023 T441SELECT ROUND(amount / 3, 2) AS thirds FROM rowsPrecision truncates to an integer and clamps to 0..30; halfway values round away from zero, matching SQLite.
literal.stringSQL:2023 E021-03SELECT region FROM rows WHERE region = 'west'literal.numberSQL:2023 E011SELECT region FROM rows WHERE amount >= 10literal.booleanSQL:2023 T031SELECT active, amount FROM rows WHERE active = TRUEliteral.null-comparisonSQL:2023 E131SELECT region FROM rows WHERE region != NULLliteral.dateSQL:2023 F051-01SELECT region FROM rows WHERE joined >= DATE '2026-01-01'literal.timestampSQL:2023 F051-03SELECT region FROM rows WHERE joined >= TIMESTAMP '2026-01-01 00:00:00'TIMESTAMP 'y-m-d h:m:s' with the time optional. A literal without a zone is UTC, as every datetime in a Minnow database is.
parameter.numberedSQL:2023 E182SELECT region, amount FROM rows WHERE amount >= $1 ORDER BY amount
-- bound: [6]Values bind by 1-based number and may repeat; the compiled plan is cached on the SQL text and re-bound per execution.
parameter.positionalSQL:2023 E182SELECT region, amount FROM rows WHERE amount >= ? AND active = ? ORDER BY amount
-- bound: [6,true]Each ? takes the next value in order. A statement uses either ? or $n placeholders, never both; PostgreSQL itself has no ? form.
join.inner-equiSQL:2023 F041-01SELECT r.region, d.label FROM rows r JOIN dims d ON d.region = r.regionjoin.left-equiSQL:2023 F041-03SELECT r.region, d.label FROM rows r LEFT JOIN dims d ON d.region = r.regionwhere.andSQL:2023 E061-14SELECT region FROM rows WHERE amount > 5 AND region = 'west'where.in-listSQL:2023 E061-03SELECT region FROM rows WHERE region IN ('west', 'east')where.not-in-listSQL:2023 E061-03SELECT region FROM rows WHERE region NOT IN ('north')where.in-subquerySQL:2023 E061-11SELECT region FROM rows WHERE region IN (SELECT region FROM dims)where.scalar-subquerySQL:2023 E061-09SELECT region FROM rows WHERE amount > (SELECT AVG(amount) FROM rows)group-bySQL:2023 E051-02SELECT region, COUNT(*) AS count FROM rows GROUP BY regiongroup-by.rollupSQL:2023 T431SELECT region, SUM(amount) AS total FROM rows GROUP BY ROLLUP(region)ROLLUP/CUBE/GROUPING SETS desugar into a UNION ALL of grouped blocks. The GROUPING() marker is deliberately unsupported, so rollup NULLs and data NULLs are indistinguishable. SQLite itself has none of these.
group-by.grouping-setsSQL:2023 T431SELECT region, active, COUNT(*) AS c FROM rows GROUP BY GROUPING SETS ((region), (active), ())havingSQL:2023 E051-06SELECT region, COUNT(*) AS count FROM rows GROUP BY region HAVING COUNT(*) > 1aggregate.countSQL:2023 E091-02SELECT COUNT(*) AS count FROM rowsaggregate.sumSQL:2023 E091-05SELECT SUM(amount) AS total FROM rowsaggregate.avgSQL:2023 E091-01SELECT AVG(amount) AS mean FROM rowsaggregate.min-maxSQL:2023 E091-03SELECT MIN(amount) AS low, MAX(amount) AS high FROM rowsorder-by.multi-columnSQL:2023 E121SELECT region, amount FROM rows ORDER BY region, amount DESCorder-by.wildcard-referenceSQL:2023 E121SELECT * FROM rows ORDER BY amountlimitSQL:2023 F856SELECT amount FROM rows ORDER BY amount LIMIT 2cte.non-recursiveSQL:2023 T121WITH west AS (SELECT amount FROM rows WHERE region = 'west') SELECT COUNT(*) AS count FROM westcte.chainedSQL:2023 T121WITH a AS (SELECT amount FROM rows), b AS (SELECT amount FROM a WHERE amount > 5) SELECT COUNT(*) AS count FROM bcte.column-listSQL:2023 T121WITH totals(place, total) AS (SELECT region, SUM(amount) FROM rows GROUP BY region) SELECT place, total FROM totalsA CTE names its own output columns. A recursive CTE takes the names before its step member, which refers to the working set by them.
derived-tableSQL:2023 F591SELECT d.total FROM (SELECT region, SUM(amount) AS total FROM rows GROUP BY region) d ORDER BY d.totalunion.distinctSQL:2023 E071-01SELECT region FROM rows UNION SELECT region FROM dims ORDER BY regionunion.allSQL:2023 E071-02SELECT region FROM rows UNION ALL SELECT region FROM dimswindow.row-numberSQL:2023 T611SELECT region, ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount) AS rn FROM rowswindow.rankSQL:2023 T611SELECT region, RANK() OVER (ORDER BY amount) AS r FROM rowswindow.dense-rankSQL:2023 T611SELECT region, DENSE_RANK() OVER (ORDER BY amount) AS dr FROM rowsmutation.insert-valuesSQL:2023 E101-01INSERT INTO keyed (name, score) VALUES ('a', 1), ('b', 2)Through execute(); query() stays read-only.
mutation.update-keyedSQL:2023 E101-03UPDATE keyed SET score = score + 1 WHERE score > 0Requires a unique-key table; read-then-mutate, not serializable.
mutation.delete-keyedSQL:2023 E101-04DELETE FROM keyed WHERE score < 0Requires a unique-key table.
mutation.returningSQL:2023 T495DELETE FROM keyed WHERE name = 'x' RETURNING name, scoreRETURNING works on INSERT, UPDATE, and DELETE; inserts echo written values, updates return post-update values, deletes the rows as read.
mutation.upsertSQL:2023 F312INSERT INTO keyed (name, score) VALUES ('x', 9) ON CONFLICT (name) DO UPDATE SET score = EXCLUDED.scoreWhole-row upsert: DO UPDATE must set every inserted column from EXCLUDED, and the conflict target is the unique key.
mutation.insert-do-nothingSQL:2023 F312INSERT INTO keyed (name, score) VALUES ('x', 9), ('z', 1) ON CONFLICT (name) DO NOTHINGRows whose key already exists at the statement's snapshot are skipped.
mutation.upsert-partialSQL:2023 F312INSERT INTO keyed (name, score, bonus) VALUES ('x', 50, 9) ON CONFLICT (name) DO UPDATE SET score = EXCLUDED.scoreAssigning a subset of inserted columns merges only those into existing rows; unassigned columns keep their stored values. Mixed update/insert batches publish atomically or roll back together.
where.orSQL:2023 E061-14SELECT region FROM rows WHERE amount > 5 OR region = 'west'where.likeSQL:2023 E061-04SELECT region FROM rows WHERE region LIKE 'w%'% matches any run and _ matches one Unicode codepoint.
predicate.is-distinct-fromSQL:2023 T151SELECT region FROM rows WHERE region IS DISTINCT FROM 'west'Null-safe: NULL is not distinct from NULL.
predicate.boolean-testSQL:2023 T031SELECT region FROM rows WHERE active IS TRUE OR active IS UNKNOWNIS [NOT] TRUE/FALSE/UNKNOWN never return UNKNOWN; they desugar to null-safe comparisons.
predicate.like-escapeSQL:2023 E061-05SELECT region FROM rows WHERE region LIKE 'we!%st' ESCAPE '!' OR region LIKE 'we%'ESCAPE makes the next pattern character literal, wildcards included.
predicate.quantifiedSQL:2023 E061-07SELECT region FROM rows WHERE amount > ALL (SELECT amount FROM dims)ANY/SOME/ALL with full three-valued logic; correlated forms are rejected. SQLite itself has no quantified comparisons.
predicate.ilikeMinnow extensionSELECT region FROM rows WHERE region ILIKE 'WE%'Case-insensitive LIKE, a PostgreSQL extension; SQLite's LIKE is case-insensitive by default instead.
predicate.matchMinnow extensionSELECT region FROM rows WHERE MATCH(region) AGAINST 'west'predicate.match-starMinnow extensionSELECT region FROM rows WHERE MATCH(*) AGAINST 'wes*'function.bm25Minnow extensionSELECT region, BM25(region) AGAINST 'west' AS score FROM rows WHERE MATCH(region) AGAINST 'west' ORDER BY score DESCorder-by.expressionSQL:2023 E121SELECT region FROM rows WHERE amount > 0 ORDER BY amount * 2 DESC, regionwhere.betweenSQL:2023 E061-02SELECT region FROM rows WHERE amount BETWEEN 1 AND 5where.between-symmetricSQL:2023 T461SELECT region FROM rows WHERE amount BETWEEN SYMMETRIC 5 AND 1SYMMETRIC accepts the bounds in either order.
where.is-nullSQL:2023 E061-06SELECT region FROM rows WHERE region IS NULLwhere.is-not-nullSQL:2023 E061-06SELECT amount FROM rows WHERE region IS NOT NULLwhere.existsSQL:2023 E061-08SELECT region FROM rows WHERE EXISTS (SELECT 1 FROM dims)Uncorrelated EXISTS only; correlated references still fail as unknown aliases.
expression.caseSQL:2023 F261-02SELECT CASE WHEN amount > 5 THEN 'big' ELSE 'small' END AS size FROM rowssubquery.correlatedSQL:2023 E061-13SELECT region FROM rows r WHERE amount > (SELECT AVG(amount) FROM rows q WHERE q.region = r.region)Equality-correlated subqueries decorrelate into derived-table joins at compile time; both executors run plain joins.
subquery.correlated-existsSQL:2023 E061-13SELECT amount FROM rows r WHERE EXISTS (SELECT region FROM dims d WHERE d.region = r.region)EXISTS joins the subquery's distinct correlation keys; NOT EXISTS becomes a left join checked with IS NULL.
subquery.correlated-selectSQL:2023 E061-13SELECT r.region, (SELECT AVG(q.amount) FROM rows q WHERE q.region = r.region) AS regional FROM rows rCorrelated scalar aggregates decorrelate in the select list too, outside grouped queries.
cte.recursiveSQL:2023 T131WITH RECURSIVE n AS (SELECT MIN(amount) AS v FROM rows UNION ALL SELECT v + 1 FROM n WHERE v < 6) SELECT v FROM nLinear delta recursion with UNION or UNION ALL, capped at 10,000 iterations and 1,000,000 rows. Plain WITH still rejects self-references.
mutation.with-cteSQL:2023 T121WITH totals AS (SELECT MAX(score) AS top FROM keyed) DELETE FROM keyed WHERE score >= (SELECT top FROM totals) RETURNING nameWITH precedes INSERT/UPDATE/DELETE; the CTEs are visible to the statement's queries and subqueries.
set.intersectSQL:2023 F302-01SELECT region FROM rows INTERSECT SELECT region FROM dimsINTERSECT binds tighter than UNION and EXCEPT, per the SQL standard.
set.exceptSQL:2023 E071-03SELECT region FROM rows EXCEPT SELECT region FROM dimsset.intersect-allSQL:2023 F302-02SELECT region FROM rows INTERSECT ALL SELECT region FROM dimsBag semantics; SQLite itself has no INTERSECT ALL.
set.except-allSQL:2023 F304SELECT region FROM rows EXCEPT ALL SELECT region FROM dimsBag semantics; SQLite itself has no EXCEPT ALL.
aggregate.count-distinctSQL:2023 E091-07SELECT COUNT(DISTINCT region) AS regions FROM rowsaggregate.filterSQL:2023 T612SELECT region, COUNT(*) FILTER (WHERE amount > 5) AS big FROM rows GROUP BY regionDesugars into a CASE inside the aggregate, so it works with every aggregate and DISTINCT.
window.aggregate-overSQL:2023 T611SELECT SUM(amount) OVER (PARTITION BY region) AS total FROM rowsDefault frame only: whole partition without OVER ordering, running with peers when ordered.
window.in-expressionSQL:2023 T611SELECT amount, amount - LAG(amount) OVER (ORDER BY amount, region) AS change, 100.0 * amount / SUM(amount) OVER () AS pct FROM rowsA window is an expression: the arithmetic around it is evaluated after the window has run, over the column it produced.
window.over-groupedSQL:2023 T611SELECT region, SUM(amount) AS total, ROW_NUMBER() OVER (ORDER BY SUM(amount) DESC, region) AS rank, SUM(SUM(amount)) OVER () AS everything FROM rows GROUP BY region HAVING COUNT(*) > 0Windows run after GROUP BY and HAVING, as the standard orders them, so they rank groups and their OVER clause reads the group's aggregates.
window.value-functionsSQL:2023 T617SELECT amount, FIRST_VALUE(amount) OVER (PARTITION BY region ORDER BY amount) AS lowest, LAST_VALUE(amount) OVER (PARTITION BY region ORDER BY amount ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS highest FROM rowsFIRST_VALUE/LAST_VALUE respect the frame; the default frame ends at the current peer group, as the standard specifies.
window.ntileSQL:2023 T614SELECT amount, NTILE(2) OVER (ORDER BY amount) AS half FROM rowswindow.distributionSQL:2023 T612SELECT amount, PERCENT_RANK() OVER (ORDER BY amount) AS pr, CUME_DIST() OVER (ORDER BY amount) AS cd FROM rowswindow.frameSQL:2023 T612SELECT amount, SUM(amount) OVER (ORDER BY amount, joined ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS windowed FROM rowsROWS frames take row-distance bounds; RANGE frames take UNBOUNDED and CURRENT ROW bounds, where CURRENT ROW spans the ordering peer group.
join.rightSQL:2023 F041-04SELECT r.region FROM rows r RIGHT JOIN dims d ON d.region = r.regionDesugars to the mirrored LEFT JOIN; supported as the sole join of a block.
join.non-equiSQL:2023 F041-08SELECT r.region FROM rows r JOIN dims d ON d.amount > r.amountExecutes as a nested-loop join (probe x build); equalities keep the hash path.
select.distinct-wildcardSQL:2023 E051-01SELECT DISTINCT * FROM rowsExpands to DISTINCT over every wildcard output column once input schemas are known.
limit.offsetSQL:2023 F856SELECT amount FROM rows LIMIT 5 OFFSET 2OFFSET is accepted directly after LIMIT.
select.no-fromSQL:2023 E051SELECT 1 + 1 AS two, UPPER('minnow') AS nameselect.valuesSQL:2023 F641SELECT v.column1 AS n, v.column2 AS tag FROM (VALUES (1, 'one'), (2, 'two')) vVALUES works standalone, as a set-operation member, and as a derived table with AS alias(col, ...) renaming; columns default to column1..columnN.
limit.parameterSQL:2023 F865SELECT region, amount FROM rows ORDER BY region NULLS LAST, amount LIMIT $1 OFFSET $2
-- bound: [2,1]LIMIT and OFFSET take placeholders; the plan re-binds per execution like any parameter.
limit.fetch-firstSQL:2023 F856SELECT amount FROM rows ORDER BY amount OFFSET 1 ROWS FETCH FIRST 2 ROWS ONLYThe standard fetch clause is a spelling of LIMIT; SQLite itself only speaks LIMIT.
offset.standaloneSQL:2023 F856SELECT amount FROM rows ORDER BY amount OFFSET 2OFFSET no longer requires LIMIT. SQLite itself needs LIMIT -1 OFFSET n.
ddl.create-tableSQL:2023 F031-01CREATE TABLE made (id INTEGER PRIMARY KEY, label TEXT NOT NULL, at TIMESTAMP)Standard type names map onto the four logical types (widths parse and are ignored); one PRIMARY KEY or UNIQUE column becomes the unique key. Dropping or altering tables stays a programmatic concern.
trigger.create-afterSQL:2023 T211CREATE TRIGGER keyed_audit AFTER INSERT ON keyed BEGIN INSERT INTO rows (region, amount) VALUES (NEW.name, NEW.score); ENDAFTER and BEFORE row triggers on INSERT/UPDATE/DELETE, executed atomically inside the triggering commit with NEW/OLD references. Bodies: INSERT ... VALUES into keyless tables; UPDATE/DELETE against keyed tables. One cascade level is allowed; deeper chains error at write time.
trigger.create-beforeSQL:2023 T211CREATE TRIGGER keyed_before BEFORE INSERT ON keyed BEGIN INSERT INTO rows (region, amount) VALUES (NEW.name, NEW.score); ENDBEFORE bodies stage ahead of the primary write but publish in the same atomic commit, so timing is a portability feature: atomicity and visibility are identical to AFTER.
trigger.body-update-deleteSQL:2023 T211CREATE TRIGGER keyed_counts AFTER INSERT ON keyed BEGIN UPDATE stats SET total = total + NEW.score WHERE region = NEW.name; ENDUPDATE and DELETE trigger bodies run against keyed tables, reading current state each firing. Touching the same target row twice in one firing is rejected.
trigger.dropSQL:2023 T211DROP TRIGGER droppable_auditwhere.parenthesizedSQL:2023 E061-14SELECT region FROM rows WHERE (amount > 5 AND region = 'west')where.notSQL:2023 E061-14SELECT region FROM rows WHERE NOT active = TRUEexpression.concatSQL:2023 E021-07SELECT region || '-' || label AS tag FROM dims|| concatenates strings and propagates NULL; non-string operands are a type error.
expression.moduloSQL:2023 T441SELECT amount % 3 AS remainder FROM rowsDivision and remainder by zero are NULL, matching SQLite.
expression.castSQL:2023 F201SELECT CAST(amount AS INTEGER) AS whole, CAST(amount AS TEXT) AS label FROM rowsStandard type names map to the four logical types; integer targets truncate toward zero, and non-numeric strings fail rather than becoming 0.
identifier.quotedSQL:2023 E031-01SELECT "region", "rows"."amount" FROM "rows" WHERE "amount" > 5Double-quoted identifiers are never keywords and keep their exact spelling.
order-by.nullsSQL:2023 T611SELECT region, amount FROM rows ORDER BY region NULLS LAST, amountWithout NULLS FIRST/LAST the default matches SQLite: NULLs first ascending, last descending.
expression.coalesceSQL:2023 F261-04SELECT COALESCE(region, 'unknown') AS region_label FROM rowsArguments evaluate left to right; the first non-NULL value wins. All non-NULL arguments must share one type.
expression.date-truncMinnow extensionSELECT DATE_TRUNC('month', joined) AS joined_month FROM rowsUnits: year, quarter, month, week (Monday start), day, hour, minute, second. Truncation is in UTC; the engine has no session time zone.
expression.date-addSQL:2023 F052SELECT joined + INTERVAL '1 month' AS next_month, joined - INTERVAL '2 days 3 hours' AS earlier FROM rows WHERE joined IS NOT NULLINTERVAL added to or subtracted from a datetime. Months are calendar arithmetic, so 31 January plus a month clamps to the end of February.
function.string-coreSQL:2023 E021-08SELECT UPPER(label) AS u, LOWER(label) AS l, LENGTH(label) AS n, SUBSTR(label, 2, 3) AS mid, TRIM(label) AS t FROM dimsSUBSTRING is accepted as a spelling of SUBSTR; LENGTH and SUBSTR count characters, not UTF-16 units.
function.absSQL:2023 T441SELECT ABS(amount - 5) AS distance FROM rowsfunction.numeric-coreSQL:2023 T441SELECT NULLIF(amount, 3) AS n, GREATEST(amount, 5) AS g, LEAST(amount, 5) AS l, FLOOR(amount) AS f, CEILING(amount) AS c, MOD(amount, 4) AS m, POWER(2, 3) AS p, SQRT(16) AS s FROM rowsGREATEST/LEAST ignore NULL arguments, matching PostgreSQL.
function.string-extendedSQL:2023 E021-06SELECT REPLACE(region, 'we', 'be') AS r, LTRIM(' x') AS lt, RTRIM('x ') AS rt, INSTR(region, 'st') AS i FROM rows WHERE region IS NOT NULLfunction.extractSQL:2023 F052SELECT EXTRACT(year FROM joined) AS y, EXTRACT(dow FROM joined) AS d FROM rows WHERE joined IS NOT NULLFields: year, quarter, month, week (ISO), day, hour, minute, second, epoch, dow — all in UTC. SQLite spells this strftime.
aggregate.distinct-argumentSQL:2023 E091-07SELECT region, COUNT(DISTINCT amount) AS amounts, COUNT(DISTINCT active) AS states, SUM(amount) AS total FROM rows GROUP BY regionCOUNT/SUM/AVG/MIN/MAX accept DISTINCT. Each one keeps its own set of values, so several can appear in one select, beside ordinary aggregates, inside expressions, and in HAVING.
join.multi-keySQL:2023 F041-01SELECT r.region FROM rows r JOIN dims d ON d.region = r.region AND d.amount = r.amountMulti-key conditions execute as a nested-loop join; single equalities keep the hash path.
join.crossSQL:2023 F401-04SELECT r.region AS region, d.label AS label FROM rows r CROSS JOIN dims djoin.fullSQL:2023 F401-02SELECT r.amount AS amount, d.label AS label FROM rows r FULL JOIN dims d ON d.region = r.regionDesugars into a union of two left joins, so it must be the sole join, with an equality ON and no grouping or DISTINCT yet.
order-by.ordinalSQL:2023 E121SELECT region, amount FROM rows ORDER BY 2 DESCOrdinals resolve to the select list at compile time; out-of-range ordinals are an error.
window.lag-leadSQL:2023 T615SELECT amount, LAG(amount) OVER (ORDER BY amount) AS previous, LEAD(amount, 1, -1) OVER (ORDER BY amount) AS next FROM rowsLAG/LEAD take a constant offset (default 1) and default value (default NULL), and require ORDER BY inside OVER.
mutation.insert-selectSQL:2023 E101-01INSERT INTO keyed (name, score) SELECT name || '2' AS name, score + 1 AS score FROM keyedThe SELECT runs at one snapshot and materializes before the batch write.
mutation.mergeSQL:2023 F312MERGE INTO keyed k USING (SELECT 'z' AS name, 9 AS score) s ON k.name = s.name WHEN MATCHED THEN UPDATE SET score = s.score WHEN NOT MATCHED THEN INSERT (name, score) VALUES (s.name, s.score)One pass over the source decides each row's branch, and the branches apply as batched writes inside a single write scope — atomic, and firing the same triggers the equivalent INSERT, UPDATE, and DELETE would. The match must equate the target's unique key with a source value, which is how rows are addressed. Two source rows matching one target row is a cardinality violation, as the standard says, rather than a last-one-wins race. MATCHED BY SOURCE, MATCHED BY TARGET, and RETURNING are not supported.
transaction.beginSQL:2023 E151-01BEGINHolds the same scope `write()` opens between statements instead of around a callback: writes stage into it, reads see what it staged, and COMMIT publishes them together. Schema changes are refused inside one, because the catalog commits outside the scope and a rollback could not take them back. A transaction left untouched past the idle bound rolls itself back, so an abandoned BEGIN cannot hold storage forever.
transaction.commitSQL:2023 E151-01COMMITtransaction.rollbackSQL:2023 E151-02ROLLBACKfunction.char-lengthSQL:2023 E021-04SELECT CHAR_LENGTH(region) AS n FROM rows WHERE region IS NOT NULLfunction.octet-lengthSQL:2023 E021-05SELECT OCTET_LENGTH(region) AS n FROM rows WHERE region IS NOT NULLCounts the UTF-8 encoding's bytes.
function.substring-from-forSQL:2023 E021-06SELECT SUBSTRING(region FROM 1 FOR 2) AS part FROM rows WHERE region IS NOT NULLThe position window is intersected with the string, so a start below 1 shortens the result instead of shifting it.
function.trim-specificationSQL:2023 E021-09SELECT TRIM(LEADING 'w' FROM region) AS trimmed FROM rows WHERE region IS NOT NULLfunction.trim-multi-characterSQL:2023 T056SELECT TRIM(BOTH 'we' FROM region) AS trimmed FROM rows WHERE region IS NOT NULLThe trim string is removed as a whole repeated unit, per the standard; PostgreSQL reads a multi-character argument as a set of characters instead.
function.positionSQL:2023 E021-11SELECT POSITION('es' IN region) AS at FROM rows WHERE region IS NOT NULLfunction.padSQL:2023 T055SELECT LPAD(region, 6, '-') AS padded FROM rows WHERE region IS NOT NULLfunction.overlaySQL:2023 T042SELECT OVERLAY(region PLACING 'X' FROM 1 FOR 1) AS masked FROM rows WHERE region IS NOT NULLselect.qualified-wildcardSQL:2023 E051-07SELECT rows.* FROM rowsOutput names follow the rule a bare * uses: the column's own name from one source, alias-qualified from several.
from.column-alias-listSQL:2023 E051-09SELECT y.a AS a FROM rows AS y(a, b, c, d)aggregate.all-quantifierSQL:2023 E091-06SELECT SUM(ALL amount) AS total FROM rowsderived-table.set-operationSQL:2023 E071-06SELECT s.amount AS amount FROM (SELECT amount FROM rows UNION SELECT amount FROM dims) scomment.simpleSQL:2023 E161SELECT amount FROM rows -- a commentcomment.bracketedSQL:2023 T351SELECT /* a comment */ amount FROM rowsjoin.commaSQL:2023 F041-07SELECT rows.amount AS amount FROM rows, dims WHERE dims.region = rows.regionjoin.usingSQL:2023 F401-04SELECT rows.amount AS amount FROM rows JOIN dims USING (region)The joined columns are not merged the way the standard describes: they stay one per side, so `SELECT *` returns both and an unqualified reference to a join column is ambiguous. Qualify it, or name the side you want.
join.naturalSQL:2023 F401-01SELECT rows.amount AS amount FROM rows NATURAL JOIN dimsThe shared columns are compared but not merged, so an unqualified reference to one is ambiguous — qualify it. NATURAL RIGHT JOIN is rejected, because the right-join mirror rewrites the sources the shared-column search reads.
datetime.current-dateSQL:2023 F051-06SELECT CURRENT_DATE > DATE '2000-01-01' AS elapsedResolved once per execution, so every row of a statement sees one instant; results that read the clock never memoize.
datetime.current-timestampSQL:2023 F051-08SELECT CURRENT_TIMESTAMP > TIMESTAMP '2000-01-01 00:00:00' AS elapseddatetime.localtimeSQL:2023 F051-07SELECT LOCALTIME IS NOT NULL AS tickingThe engine has no TIME type, so LOCALTIME reads as an 'HH:MM:SS' string, like SQLite's CURRENT_TIME.
predicate.row-comparisonSQL:2023 F641SELECT amount FROM rows WHERE (region, amount) = ('west', 10)predicate.row-inSQL:2023 F641SELECT amount FROM rows WHERE (region, amount) IN (('west', 10), ('east', 3))predicate.row-nullSQL:2023 F641SELECT amount FROM rows WHERE (region, region) IS NOT NULLliteral.radixSQL:2023 T661SELECT 0x0A AS tenliteral.digit-separatorSQL:2023 T662SELECT 1_000 AS thousandlimit.with-tiesSQL:2023 F866SELECT region FROM rows WHERE region IS NOT NULL ORDER BY region DESC FETCH FIRST 1 ROWS WITH TIESThe limit cannot be pushed into a scan, so these plans run unlimited and the ordered result is trimmed.
cte.in-subquerySQL:2023 T122SELECT s.amount AS amount FROM (WITH inner_cte AS (SELECT amount FROM rows) SELECT amount FROM inner_cte) swindow.nth-valueSQL:2023 T618SELECT NTH_VALUE(amount, 2) OVER (ORDER BY amount) AS second FROM rowswindow.namedSQL:2023 T620SELECT SUM(amount) OVER w AS running FROM rows WINDOW w AS (ORDER BY amount)window.frame-groupsSQL:2023 T612SELECT COUNT(*) OVER (ORDER BY amount GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW) AS peers FROM rowswindow.frame-excludeSQL:2023 T612SELECT COUNT(*) OVER (ORDER BY amount RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING EXCLUDE CURRENT ROW) AS others FROM rowsaggregate.groupingSQL:2023 T433SELECT GROUPING(region) AS aggregated FROM rows GROUP BY ROLLUP(region)A bitmask over the arguments, most significant first.
aggregate.any-valueSQL:2023 T626SELECT ANY_VALUE(amount) AS sample FROM rowsWhich row of the group answers is implementation-dependent; this engine returns the minimum.
aggregate.varianceSQL:2023 T621SELECT VAR_POP(amount) AS spread FROM rowsBuilt from COUNT and SUM rather than a dedicated accumulator: the variance is E(x2) - E(x)2. Bare VARIANCE and STDDEV are the sample forms, as in PostgreSQL.
aggregate.stddevSQL:2023 T621SELECT STDDEV_POP(amount) AS spread FROM rowsaggregate.booleanSQL:2023 T631SELECT EVERY(amount > 1) AS all_positive FROM rowsjson.valueSQL:2023 T822SELECT JSON_VALUE('{"a": 1}', '$.a') AS aJSON documents are text in ordinary string columns, as in SQLite; the path subset is $, member steps, and array subscripts.
json.querySQL:2023 T823SELECT JSON_QUERY('{"a": [1, 2]}', '$.a') AS ajson.existsSQL:2023 T821SELECT JSON_EXISTS('{"a": 1}', '$.a') AS presentjson.is-jsonSQL:2023 T825SELECT '{"a": 1}' IS JSON OBJECT AS shapedjson.objectSQL:2023 T811SELECT JSON_OBJECT('a' VALUE 1) AS documentjson.arraySQL:2023 T812SELECT JSON_ARRAY(1, 2) AS documentddl.create-table-if-not-existsSQL:2023 F031-01CREATE TABLE IF NOT EXISTS made (a INTEGER)ddl.create-table-defaultSQL:2023 E141-07CREATE TABLE defaulted (id INTEGER PRIMARY KEY, tier TEXT DEFAULT 'basic')A column with a DEFAULT is NOT NULL unless declared otherwise: the engine fills absent values from the default, so NULL and the default cannot both claim the slot.
ddl.create-table-key-clauseSQL:2023 E141-08CREATE TABLE keyed_clause (a INTEGER, b TEXT, PRIMARY KEY (a))ddl.alter-table-add-columnSQL:2023 F031-04ALTER TABLE rows ADD COLUMN note TEXTExisting rows have no value for the new column, so it is always nullable.
ddl.create-table-as-selectSQL:2023 T172CREATE TABLE copied AS SELECT region FROM rowsddl.drop-tableSQL:2023 F031-13DROP TABLE doomedTakes the table's rows, catalog record, full-text index, and triggers. The blocks are retired through the commit rather than deleted, so a reader pinned to an older version keeps resolving them and the lease-aware collector reclaims them later. Refused while a view reads the table or another table's trigger writes to it — both would be left pointing at something that is not there. DROP TABLE CASCADE is refused too: nothing cascades, because there are no dependent objects to reach.
ddl.create-viewSQL:2023 F031-02CREATE VIEW west AS SELECT region, amount FROM rows WHERE region = 'west'The catalog stores the query text and the schema inferred from it, so a view answers the same questions a table does and reads expand it into the query it stands for — anywhere a read runs, including inside a write scope. A view is never a write target. CREATE OR REPLACE redefines one; a view stacked on it follows the new definition, and a cycle two redefinitions close is caught on the next read rather than recursed.
ddl.drop-viewSQL:2023 F031-16DROP VIEW doomed_viewddl.check-constraintSQL:2023 E141-06CREATE TABLE checked (a INTEGER NOT NULL CHECK (a > 0), CONSTRAINT small CHECK (a < 100))A row condition over the table's own columns, evaluated by the writer on every path that writes a row — insert, upsert, and update, which is checked against its post-image. A constraint fails only when it evaluates to false, so SQL's unknown passes: NULL satisfies CHECK (a > 0) unless the column is also NOT NULL.
ddl.foreign-keySQL:2023 E141-04CREATE TABLE children (id INTEGER PRIMARY KEY, parent INTEGER REFERENCES parents(id) ON DELETE CASCADE)Single-column references to the parent's unique key, which is the column the engine can probe for existence and the one its keyed writes address rows by. Every write of a referencing column checks the parent exists, reading through the writing transaction so a child inserted beside its parent in one scope sees it; a NULL reference names no parent and is satisfied. ON DELETE takes RESTRICT (the default), CASCADE, and SET NULL, applied inside the deleting transaction. ON UPDATE has nothing to act on, because a unique key cannot change.
Rejected
14 forms, each checked on every test run to still fail with the error below.
subquery.correlated-non-equiSQL:2023 E061-13SELECT region FROM rows r WHERE EXISTS (SELECT region FROM dims d WHERE d.amount > r.amount)Rejected with: support only equality conditions
Correlation must be a plain equality between one inner and one outer qualified column.
subquery.correlated-not-inSQL:2023 E061-13SELECT region FROM rows r WHERE region NOT IN (SELECT d.region FROM dims d WHERE d.region = r.region)Rejected with: use NOT EXISTS
Correlated NOT IN has NULL semantics that the join rewrite cannot preserve; NOT EXISTS expresses the intent.
mutation.update-keylessSQL:2023 E101-03UPDATE rows SET amount = 1Rejected with: UPDATE requires a table with a unique key
Deliberate: mutation segments address rows by unique key, so tables without one cannot be updated or deleted through any API.
transaction.isolation-levelSQL:2023 E152-01SET TRANSACTION ISOLATION LEVEL SERIALIZABLERejected with: Expected SELECT, found SET
The engine has one isolation level. Every read runs against a single version and every write scope commits atomically, so there is no weaker mode to relax into and no stronger one to ask for.
privileges.grantSQL:2023 E081GRANT SELECT ON rows TO readerRejected with: Expected SELECT, found GRANT
An embedded, single-user database in the page has no principals to grant to.
from.lateralSQL:2023 T491SELECT x.amount FROM rows, LATERAL (SELECT amount FROM dims WHERE dims.region = rows.region) xRejected with: LATERAL sources are not supported
A lateral source re-executes per row of its left side, which the executors have no operator for.
aggregate.listaggSQL:2023 T625SELECT LISTAGG(region, ',') AS regions FROM rowsRejected with: Unsupported function: LISTAGG
Needs an ordered string accumulator in the vectorized group state, which the fixed count/sum/value accumulators cannot hold.
json.tableSQL:2023 T824SELECT j.a FROM rows, JSON_TABLE(rows.region, '$' COLUMNS (a INTEGER PATH '$.a')) AS jRejected with: JSON_TABLE is not supported
A row-producing operator; the same gap as LATERAL.
predicate.similar-toSQL:2023 T141SELECT amount FROM rows WHERE region SIMILAR TO 'w%'Rejected with: Expected eof, found SIMILAR
LIKE and the regular-expression-free MATCH cover the same ground for the data sizes this engine targets.
collation.explicitSQL:2023 F690SELECT region FROM rows ORDER BY region COLLATE "en"Rejected with: Expected eof, found COLLATE
String comparison is one documented collation; a per-query collation would change index order too.
aggregate.jsonSQL:2023 T826SELECT JSON_ARRAYAGG(region) AS regions FROM rowsRejected with: Unsupported function: JSON_ARRAYAGG
Aggregating into a document needs the same ordered accumulator LISTAGG does; JSON_ARRAY builds one from scalars today.
type.arraySQL:2023 S091SELECT ARRAY[1, 2] AS pairRejected with: Unsupported SQL character: [
Array and multiset types would need an encoding and a comparison order the columnar format would carry forever; a JSON document in a text column covers the same shape.
type.timeSQL:2023 F051-02SELECT TIME '12:00:00' AS atRejected with: Expected eof
The engine has four logical types and stores every datetime as an instant in UTC; a bare time of day has no instant. LOCALTIME reads as an 'HH:MM:SS' string instead.
ddl.sequenceSQL:2023 T176CREATE SEQUENCE order_idsRejected with: Expected TABLE, found SEQUENCE
A sequence is a second kind of catalog object with its own durability and contention story; the key column's autoincrement default covers the common use.