PostgreSQL compatibility
See which PostgreSQL forms Minnow supports, changes, extends, or excludes.
Minnow implements PostgreSQL-style SQL for one embedded browser database. Familiar behavior
includes double-quoted identifiers, $1 parameters, RETURNING, ILIKE, ON CONFLICT, and
PostgreSQL NULL ordering.
The list below classifies each documented SQL form:
- PostgreSQL compatible — the syntax and result follow PostgreSQL for the example shown.
- PostgreSQL difference — Minnow supports the form with a documented result or type difference.
- Minnow extension — Minnow adds syntax or behavior for its embedded use case.
- Not applicable — the PostgreSQL form belongs to a server concept Minnow does not have.
- Unsupported — Minnow rejects the form with the listed error.
Every supported example runs through both query execution paths. Compatible reads and writes are also compared with PGlite. Every excluded example is checked for the documented error, so this page stays aligned with the engine.
This is a form-by-form compatibility guide, not a claim that Minnow contains the complete PostgreSQL grammar. These supported features have narrower limits:
- grouped correlated scalar select items may reference grouping keys, but cannot sit inside an outer aggregate.
- a correlated scalar's inner block supports aggregate expressions or an ordinary single row,
with any number of qualified outer-column references plus per-probe ordering and limits, but
not its own
GROUP BYorHAVING. LATERALsupports equality and range-correlated projections and joins. An equality-correlated lateral query may also group, aggregate, and take a per-rowORDER BY … LIMIT; a range-correlated one cannot group or limit.JSON_TABLEsupports constant documents with$and$[*]row paths.CREATE SEQUENCEsupportsNEXTVALand session-localCURRVAL, without sequence options.- enums support declaration, validation, and declaration-order comparison, without
ALTER TYPE. - exact
NUMERIC, JSON/JSONB, UUID, arrays,DATE,TIME, intervals, and enums keep their full values internally and return strings at the JavaScript boundary.
Minnow deliberately excludes three PostgreSQL server or storage-model features:
UPDATEandDELETEon tables without row identity;- the
SERIALIZABLEisolation level — every transaction reads one snapshot, which satisfies the weaker levels, soREAD COMMITTEDandREPEATABLE READare accepted and ignored; - roles and
GRANTprivileges.
The excluded-forms list below also records the narrower forms the engine rejects — RIGHT JOIN
and FULL JOIN beyond their supported sole-join forms, temporal interval RANGE offsets, DISTINCT
window aggregates, windows in WHERE/GROUP BY/HAVING, and ON DELETE SET DEFAULT — each with the
exact error it raises.
Supported SQL
306 checked forms · 271 PostgreSQL compatible · 28 different · 7 extensions
select.projectionPostgreSQL compatibleSELECT region, amount FROM rowsselect.aliasPostgreSQL compatibleSELECT amount AS total FROM rowsselect.allPostgreSQL compatibleSELECT ALL region, amount FROM rowsDetails
SELECT ALL names the default: every row, duplicates kept. ALL is also accepted inside an aggregate, as in COUNT(ALL amount).
select.distinct-onPostgreSQL compatibleSELECT DISTINCT ON (region) region, amount FROM rows ORDER BY region, amount DESCDetails
PostgreSQL's DISTINCT ON (expressions) keeps the first row of each group in ORDER BY order. It lowers to ROW_NUMBER() OVER (PARTITION BY expressions ORDER BY …) = 1 over the block, so ORDER BY may name output aliases and columns the select list omits, and LIMIT and OFFSET apply to the kept rows.
select.locking-clausePostgreSQL compatibleSELECT region, amount FROM rows FOR UPDATEDetails
FOR UPDATE, FOR NO KEY UPDATE, FOR SHARE, and FOR KEY SHARE, with OF, NOWAIT, or SKIP LOCKED, are accepted and ignored: row locks coordinate concurrent sessions, and a single-session engine has none to coordinate.
select.table-commandPostgreSQL compatibleTABLE rowsDetails
TABLE name is the standard's spelling of SELECT * FROM name.
literal.string-spellingsPostgreSQL compatibleSELECT E'tab\\there' AS escaped, $$dollar 'quoted'$$ AS plain, 5. AS five, .5 AS half FROM rowsDetails
E'…' strings take C-style backslash escapes (\\n, \\t, \\xHH, \\uHHHH, octal), $tag$…$tag$ strings are taken verbatim, and 5. and .5 spell 5.0 and 0.5, as in PostgreSQL.
predicate.like-operatorsPostgreSQL compatibleSELECT region FROM rows WHERE region ~~ 'w%' OR region !~~* 'E%'Details
~~, !~~, ~~*, and !~~* are PostgreSQL's operator spellings of LIKE, NOT LIKE, ILIKE, and NOT ILIKE.
select.label-without-asPostgreSQL compatibleSELECT amount total, region "area" FROM rowsDetails
A column label needs no AS, as in PostgreSQL and SQLite. The label is any identifier that no clause or operator keyword can claim; a quoted identifier is always a label.
select.wildcardPostgreSQL compatibleSELECT * FROM rowsselect.distinctPostgreSQL compatibleSELECT DISTINCT region FROM rowsDetails
Plain projections and SELECT DISTINCT * are supported. DISTINCT combined with grouping, HAVING, or aggregate/window expressions is rejected.
select.scalar-subqueryPostgreSQL compatibleSELECT (SELECT MAX(amount) FROM rows) AS peak FROM rows LIMIT 1expression.arithmeticPostgreSQL differenceSELECT amount * 2 + 1 AS scaled FROM rowsDetails
PostgreSQL: Division by zero returns NULL instead of raising PostgreSQL's error. Integer division itself follows PostgreSQL: two integer operands truncate toward zero.
expression.roundPostgreSQL differenceSELECT ROUND(amount / 3, 2) AS thirds FROM rowsDetails
Over a double, precision truncates to an integer and clamps to 0..30, and halfway values round away from zero, matching SQLite. Over an exact NUMERIC the result is PostgreSQL's numeric ROUND: exact, half away from zero, a negative digit count rounds left of the decimal point, and a literal digit count is the result's display scale.
PostgreSQL: Minnow accepts ROUND(double precision, digits); PostgreSQL requires numeric for the two-argument form.
literal.stringPostgreSQL compatibleSELECT region FROM rows WHERE region = 'west'literal.numberPostgreSQL compatibleSELECT region FROM rows WHERE amount >= 10literal.booleanPostgreSQL compatibleSELECT active, amount FROM rows WHERE active = TRUEliteral.null-comparisonPostgreSQL compatibleSELECT region FROM rows WHERE region != NULLliteral.datePostgreSQL compatibleSELECT region FROM rows WHERE joined >= DATE '2026-01-01'literal.timestampPostgreSQL compatibleSELECT region FROM rows WHERE joined >= TIMESTAMP '2026-01-01 00:00:00'Details
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.numberedPostgreSQL compatibleSELECT region, amount FROM rows WHERE amount >= $1 ORDER BY amount
-- bound: [6]Details
Values bind by 1-based number and may repeat; the compiled plan is cached on the SQL text and re-bound per execution.
parameter.positionalMinnow extensionSELECT region, amount FROM rows WHERE amount >= ? AND active = ? ORDER BY amount
-- bound: [6,true]Details
Each ? takes the next value in order. A statement uses either ? or $n placeholders, never both; PostgreSQL itself has no ? form.
PostgreSQL: PostgreSQL uses $n parameters. Minnow also accepts ? as adapter-friendly shorthand.
join.inner-equiPostgreSQL compatibleSELECT r.region, d.label FROM rows r JOIN dims d ON d.region = r.regionjoin.left-equiPostgreSQL compatibleSELECT r.region, d.label FROM rows r LEFT JOIN dims d ON d.region = r.regionwhere.andPostgreSQL compatibleSELECT region FROM rows WHERE amount > 5 AND region = 'west'where.in-listPostgreSQL compatibleSELECT region FROM rows WHERE region IN ('west', 'east')where.not-in-listPostgreSQL compatibleSELECT region FROM rows WHERE region NOT IN ('north')where.in-subqueryPostgreSQL compatibleSELECT region FROM rows WHERE region IN (SELECT region FROM dims)where.scalar-subqueryPostgreSQL compatibleSELECT region FROM rows WHERE amount > (SELECT AVG(amount) FROM rows)group-byPostgreSQL compatibleSELECT region, COUNT(*) AS count FROM rows GROUP BY regiongroup-by.rollupPostgreSQL compatibleSELECT region, SUM(amount) AS total FROM rows GROUP BY ROLLUP(region)Details
ROLLUP/CUBE/GROUPING SETS desugar into a UNION ALL of grouped blocks. GROUPING() distinguishes rolled-up columns from data NULLs. SQLite itself has none of these.
group-by.grouping-setsPostgreSQL compatibleSELECT region, active, COUNT(*) AS c FROM rows GROUP BY GROUPING SETS ((region), (active), ())havingPostgreSQL compatibleSELECT region, COUNT(*) AS count FROM rows GROUP BY region HAVING COUNT(*) > 1aggregate.countPostgreSQL compatibleSELECT COUNT(*) AS count FROM rowsaggregate.sumPostgreSQL compatibleSELECT SUM(amount) AS total FROM rowsaggregate.avgPostgreSQL compatibleSELECT AVG(amount) AS mean FROM rowsaggregate.min-maxPostgreSQL compatibleSELECT MIN(amount) AS low, MAX(amount) AS high FROM rowsorder-by.multi-columnPostgreSQL compatibleSELECT region, amount FROM rows ORDER BY region, amount DESCorder-by.wildcard-referencePostgreSQL compatibleSELECT * FROM rows ORDER BY amountorder-by.qualified-wildcard-referencePostgreSQL compatibleSELECT rows.* FROM rows ORDER BY rows.amount DESCDetails
Both the wildcard and the ordering reference may use a table name or alias, quoted or unquoted. The conformance corpus crosses those spellings and compares them with SQLite and PostgreSQL.
limitPostgreSQL compatibleSELECT amount FROM rows ORDER BY amount LIMIT 2cte.non-recursivePostgreSQL compatibleWITH west AS (SELECT amount FROM rows WHERE region = 'west') SELECT COUNT(*) AS count FROM westcte.materializedPostgreSQL compatibleWITH w AS MATERIALIZED (SELECT region, amount FROM rows) SELECT region FROM wDetails
[NOT] MATERIALIZED is PostgreSQL's planner hint on a CTE; the block is planned the same way either way.
cte.chainedPostgreSQL compatibleWITH a AS (SELECT amount FROM rows), b AS (SELECT amount FROM a WHERE amount > 5) SELECT COUNT(*) AS count FROM bcte.column-listPostgreSQL compatibleWITH totals(place, total) AS (SELECT region, SUM(amount) FROM rows GROUP BY region) SELECT place, total FROM totalsDetails
A 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-tablePostgreSQL compatibleSELECT d.total FROM (SELECT region, SUM(amount) AS total FROM rows GROUP BY region) d ORDER BY d.totalunion.distinctPostgreSQL compatibleSELECT region FROM rows UNION SELECT region FROM dims ORDER BY regionunion.allPostgreSQL compatibleSELECT region FROM rows UNION ALL SELECT region FROM dimswindow.row-numberPostgreSQL compatibleSELECT region, ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount) AS rn FROM rowswindow.rankPostgreSQL compatibleSELECT region, RANK() OVER (ORDER BY amount) AS r FROM rowswindow.dense-rankPostgreSQL compatibleSELECT region, DENSE_RANK() OVER (ORDER BY amount) AS dr FROM rowsmutation.insert-valuesPostgreSQL compatibleINSERT INTO keyed (name, score) VALUES ('a', 1), ('b', 2)Details
Through execute(); query() stays read-only.
mutation.insert-values-implicit-columnsPostgreSQL compatibleINSERT INTO keyed VALUES ('z', 2, NULL)Details
When the column list is omitted, VALUES must supply every table column in declaration order.
mutation.insert-default-valuesPostgreSQL compatibleINSERT INTO defaulted_insert DEFAULT VALUESDetails
DEFAULT VALUES inserts one row using catalog defaults. DEFAULT is also accepted in an individual VALUES slot.
mutation.insert-runtime-valuesPostgreSQL compatibleINSERT INTO runtime_values VALUES (1, CURRENT_TIMESTAMP, RANDOM(), GEN_RANDOM_UUID()) RETURNING idDetails
Statement-time, random, UUID, and sequence calls in VALUES are evaluated at execution rather than frozen in the compiled-statement cache. Every CURRENT_* call in one statement shares one clock.
mutation.update-keyedPostgreSQL compatibleUPDATE keyed SET score = score + 1 WHERE score > 0Details
Requires a unique-key table. A standalone statement reads and publishes inside one retryable write scope; an explicit transaction stages it with the transaction's other statements.
mutation.delete-keyedPostgreSQL compatibleDELETE FROM keyed WHERE score < 0Details
Requires a unique-key table.
mutation.update-fromPostgreSQL compatibleUPDATE keyed SET score = r.amount FROM (SELECT 'x' AS name, 5 AS amount) r WHERE r.name = keyed.nameDetails
UPDATE … FROM joins extra sources — tables, views, or derived tables — to the target as PostgreSQL does; the assignments and predicates may read them. A target row matched by several source rows is updated once, from its first match.
mutation.delete-usingPostgreSQL compatibleDELETE FROM keyed USING (SELECT 'y' AS name) gone WHERE gone.name = keyed.nameDetails
DELETE … USING joins extra sources to the target as PostgreSQL does; a target row matched by several source rows is deleted once.
mutation.truncatePostgreSQL differenceTRUNCATE TABLE keyedDetails
TRUNCATE [TABLE] [ONLY] name [RESTART IDENTITY | CONTINUE IDENTITY] [RESTRICT] removes every row of one table, the same as an unfiltered DELETE, so like DELETE it needs a table with a unique key. Several tables in one statement and CASCADE are refused; truncate each table separately.
PostgreSQL: TRUNCATE reports the number of rows it removed, where PostgreSQL's command tag carries no count. The resulting table state is identical.
mutation.returningPostgreSQL compatibleDELETE FROM keyed WHERE name = 'x' RETURNING keyed.name, keyed.scoreDetails
RETURNING works on INSERT, UPDATE, and DELETE; inserts echo written values, updates return post-update values, and deletes return the rows as read. Columns and target.* may be target-qualified.
mutation.returning-expressionPostgreSQL compatibleUPDATE keyed SET score = score + 1 WHERE name = 'x' RETURNING name, score * 2 AS doubled, UPPER(name) AS labelDetails
Any scalar expression may appear in RETURNING, with SELECT semantics over the affected row: the post-image for INSERT and UPDATE, the removed row for DELETE. Aggregates are refused.
mutation.upsertPostgreSQL compatibleINSERT INTO keyed (name, score) VALUES ('x', 9) ON CONFLICT (name) DO UPDATE SET score = EXCLUDED.scoreDetails
PostgreSQL-compatible ON CONFLICT form. The conflict target is the unique key and assigned values may read the target row or EXCLUDED proposal.
PostgreSQL: ON CONFLICT ... DO UPDATE follows PostgreSQL syntax.
mutation.insert-do-nothingPostgreSQL compatibleINSERT INTO keyed (name, score) VALUES ('x', 9), ('z', 1) ON CONFLICT (name) DO NOTHINGDetails
Rows whose key already exists at the statement's snapshot are skipped. Each proposed row is considered in order, so later duplicates of a key retained earlier in the same statement are skipped too.
PostgreSQL: ON CONFLICT ... DO NOTHING follows PostgreSQL syntax.
mutation.upsert-replaceMinnow extensionINSERT INTO keyed (name, score, bonus) VALUES ('x', 50, 9) ON CONFLICT (name) DO REPLACEDetails
Minnow's concise whole-row upsert spelling replaces every non-key column and also has exact semantics for a key-only table.
PostgreSQL: DO REPLACE is Minnow shorthand for replacing every non-key value from EXCLUDED.
mutation.upsert-partialPostgreSQL compatibleINSERT INTO keyed (name, score, bonus) VALUES ('x', 50, 9) ON CONFLICT (name) DO UPDATE SET score = EXCLUDED.scoreDetails
Assigning a subset of target columns changes only those columns; unassigned columns keep their stored values. Assignment targets need not appear in the INSERT column list. Mixed update/insert batches publish atomically or roll back together.
PostgreSQL: A partial SET list with EXCLUDED follows PostgreSQL syntax.
where.orPostgreSQL compatibleSELECT region FROM rows WHERE amount > 5 OR region = 'west'where.likePostgreSQL compatibleSELECT region FROM rows WHERE region LIKE 'w%'Details
% matches any run and _ matches one Unicode codepoint.
predicate.is-distinct-fromPostgreSQL compatibleSELECT region FROM rows WHERE region IS DISTINCT FROM 'west'Details
Null-safe: NULL is not distinct from NULL.
predicate.boolean-testPostgreSQL compatibleSELECT region FROM rows WHERE active IS TRUE OR active IS UNKNOWNDetails
IS [NOT] TRUE/FALSE/UNKNOWN never return UNKNOWN; they desugar to null-safe comparisons.
predicate.like-escapePostgreSQL compatibleSELECT region FROM rows WHERE region LIKE 'we!%st' ESCAPE '!' OR region LIKE 'we%'Details
ESCAPE makes the next pattern character literal, wildcards included.
predicate.quantifiedPostgreSQL compatibleSELECT region FROM rows WHERE amount > ALL (SELECT amount FROM dims)Details
ANY/SOME/ALL use full three-valued logic, including when a correlated form is nested below OR, NOT, CASE, or a select expression. SQLite itself has no quantified comparisons.
predicate.ilikePostgreSQL compatibleSELECT region FROM rows WHERE region ILIKE 'WE%'Details
Case-insensitive LIKE, a PostgreSQL extension; SQLite's LIKE is case-insensitive by default instead.
PostgreSQL: ILIKE follows PostgreSQL syntax and case-insensitive matching semantics.
predicate.matchMinnow extensionSELECT region FROM rows WHERE MATCH(region) AGAINST 'west'Details
PostgreSQL: MATCH is Minnow's index-transparent full-text predicate rather than a PostgreSQL operator.
predicate.match-starMinnow extensionSELECT region FROM rows WHERE MATCH(*) AGAINST 'wes*'Details
PostgreSQL: MATCH(*) searches every text column and is a Minnow full-text extension.
predicate.match-parameterMinnow extensionSELECT region FROM rows WHERE MATCH(region) AGAINST $1 ORDER BY BM25(region) AGAINST $1 DESC
-- bound: ["west"]Details
Search text binds like any other value, so search-as-you-type can reuse one planned statement.
PostgreSQL: Parameterized MATCH uses Minnow's index-transparent full-text predicate.
function.bm25Minnow extensionSELECT region, BM25(region) AGAINST 'west' AS score FROM rows WHERE MATCH(region) AGAINST 'west' ORDER BY score DESCDetails
PostgreSQL: BM25 exposes Minnow's full-text score; PostgreSQL uses its own text-search types and ranking functions.
function.regexp-substringPostgreSQL compatibleSELECT SUBSTRING(region FROM '(e.)') AS part, SUBSTRING(region FROM 2 FOR 2) AS middle, TO_HEX(255) AS hex, QUOTE_LITERAL(region) AS quoted, QUOTE_IDENT('Mixed Name') AS ident FROM rowsDetails
SUBSTRING(text FROM 'pattern') is PostgreSQL's POSIX-regex form: the first match, or its first parenthesized group; a number after FROM is the ordinary start position. TO_HEX renders an integer in hexadecimal; QUOTE_LITERAL and QUOTE_IDENT quote a value or a name for use in SQL text. Matching is leftmost-longest and bounded; the regex operator profile lists unsupported syntax.
order-by.expressionPostgreSQL compatibleSELECT region FROM rows WHERE amount > 0 ORDER BY amount * 2 DESC, regionwhere.betweenPostgreSQL compatibleSELECT region FROM rows WHERE amount BETWEEN 1 AND 5where.between-symmetricPostgreSQL compatibleSELECT region FROM rows WHERE amount BETWEEN SYMMETRIC 5 AND 1Details
SYMMETRIC accepts the bounds in either order.
where.is-nullPostgreSQL compatibleSELECT region FROM rows WHERE region IS NULLwhere.is-not-nullPostgreSQL compatibleSELECT amount FROM rows WHERE region IS NOT NULLwhere.existsPostgreSQL compatibleSELECT region FROM rows WHERE EXISTS (SELECT 1 FROM dims)Details
Both uncorrelated and correlated EXISTS are supported. Correlated forms are decorrelated into joins rather than executed once per outer row.
expression.casePostgreSQL compatibleSELECT CASE WHEN amount > 5 THEN 'big' ELSE 'small' END AS size FROM rowssubquery.correlatedPostgreSQL compatibleSELECT region FROM rows r WHERE amount > (SELECT AVG(amount) FROM rows q WHERE q.region = r.region)Details
Correlated subqueries decorrelate into derived-table joins at compile time; both executors run plain joins.
subquery.correlated-existsPostgreSQL compatibleSELECT amount FROM rows r WHERE EXISTS (SELECT region FROM dims d WHERE d.region = r.region)Details
Top-level EXISTS and NOT EXISTS lower to semi-joins and anti-joins. Nested boolean forms use hidden per-probe match flags.
subquery.correlated-selectPostgreSQL compatibleSELECT r.region, (SELECT MAX(q.amount + r.amount) FROM rows q WHERE q.region = r.region) AS regional FROM rows rDetails
Correlated scalar aggregates and single-row projections decorrelate in the select list. Every qualified outer column used by the inner predicates or projection becomes part of the distinct probe tuple. In a grouped query, outer references must be GROUP BY columns and the scalar cannot sit inside an outer aggregate.
subquery.correlated-select-limitPostgreSQL compatibleSELECT r.region, (SELECT q.amount FROM rows q WHERE q.region = r.region ORDER BY q.amount DESC LIMIT 1) AS peak FROM rows rDetails
ORDER BY, LIMIT, and OFFSET apply independently to each distinct outer probe. Zero rows yield NULL and more than one unbounded row raises a scalar-cardinality error.
subquery.correlated-json-aggregatePostgreSQL differenceSELECT r.region, (SELECT JSON_ARRAYAGG(JSON_OBJECT('amount' VALUE q.amount) ORDER BY q.amount) FROM rows q WHERE q.region = r.region) AS amounts FROM rows rDetails
JSON aggregate expressions use the same set-at-a-time decorrelation as numeric aggregates and preserve their JSON result domain.
PostgreSQL: Both engines accept the correlated JSON aggregate and agree on its JSON value, but Minnow returns JSON text while PostgreSQL returns a native JSON value.
subquery.correlated-select-groupedPostgreSQL compatibleSELECT r.region, COUNT(*) AS c, (SELECT AVG(q.amount) FROM rows q WHERE q.region = r.region) AS regional FROM rows r GROUP BY r.regionDetails
The decorrelated value is functionally determined by the outer grouping keys and is carried as an internal group key.
subquery.correlated-scalar-non-equiPostgreSQL compatibleSELECT r.amount, (SELECT COUNT(*) FROM rows q WHERE q.amount < r.amount) AS lower_count FROM rows rDetails
Non-equality scalar aggregates group inner rows once per distinct outer probe tuple, then join the result back by equality.
subquery.correlated-exists-expressionPostgreSQL compatibleSELECT r.amount FROM rows r WHERE r.amount > 100 OR EXISTS (SELECT d.region FROM dims d WHERE d.region = r.region)Details
Correlated EXISTS and NOT EXISTS remain set-at-a-time below OR, NOT, or CASE and across deeper correlated EXISTS, IN, NOT IN, or scalar blocks. Generated aliases are unique across the complete plan tree.
subquery.correlated-non-equiPostgreSQL compatibleSELECT r.amount FROM rows r WHERE EXISTS (SELECT q.amount FROM rows q WHERE q.amount < r.amount)Details
Non-equality EXISTS and NOT EXISTS correlations lower to semi-joins and anti-joins. Scalar aggregates use distinct outer probes.
subquery.correlated-in-non-equiPostgreSQL compatibleSELECT r.amount FROM rows r WHERE r.region IN (SELECT q.region FROM rows q WHERE q.amount < r.amount)Details
A range-correlated IN predicate lowers to a semi-join carrying both its correlation and membership comparisons.
subquery.correlated-not-inPostgreSQL compatibleSELECT region FROM rows r WHERE region NOT IN (SELECT d.region FROM dims d WHERE d.region = r.region)Details
Correlated NOT IN preserves empty-set and NULL semantics rather than treating it as a simple anti-join.
subquery.correlated-membership-expressionPostgreSQL compatibleSELECT r.amount FROM rows r WHERE r.amount = 3 OR r.region NOT IN (SELECT q.region FROM rows q WHERE q.amount < r.amount)Details
Correlated IN and NOT IN retain true, false, and unknown results below OR, NOT, CASE, and in select expressions.
subquery.correlated-not-in-non-equiPostgreSQL compatibleSELECT r.amount FROM rows r WHERE 'north' NOT IN (SELECT q.region FROM rows q WHERE q.amount < r.amount)Details
Range correlation uses an anti-join for exact matches plus per-probe total and non-NULL counts.
subquery.correlated-quantifiedPostgreSQL compatibleSELECT r.amount FROM rows r WHERE r.amount = 3 OR r.amount > ALL (SELECT q.amount FROM rows q WHERE q.region = r.region)Details
Top-level WHERE uses semi/anti joins. Nested expressions group true, false, unknown, and empty-set counts per distinct outer probe tuple.
cte.recursivePostgreSQL compatibleWITH RECURSIVE n AS (SELECT MIN(amount) AS v FROM rows UNION ALL SELECT v + 1 FROM n WHERE v < 6) SELECT v FROM nDetails
Linear 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-ctePostgreSQL compatibleWITH totals AS (SELECT MAX(score) AS top FROM keyed) DELETE FROM keyed WHERE score >= (SELECT top FROM totals) RETURNING nameDetails
WITH precedes INSERT/UPDATE/DELETE; the CTEs are visible to the statement's queries and subqueries.
set.intersectPostgreSQL compatibleSELECT region FROM rows INTERSECT SELECT region FROM dimsDetails
INTERSECT binds tighter than UNION and EXCEPT, matching PostgreSQL.
set.exceptPostgreSQL compatibleSELECT region FROM rows EXCEPT SELECT region FROM dimsset.intersect-allPostgreSQL compatibleSELECT region FROM rows INTERSECT ALL SELECT region FROM dimsDetails
Bag semantics; SQLite itself has no INTERSECT ALL.
set.except-allPostgreSQL compatibleSELECT region FROM rows EXCEPT ALL SELECT region FROM dimsDetails
Bag semantics; SQLite itself has no EXCEPT ALL.
aggregate.count-distinctPostgreSQL compatibleSELECT COUNT(DISTINCT region) AS regions FROM rowsaggregate.filterPostgreSQL compatibleSELECT region, COUNT(*) FILTER (WHERE amount > 5) AS big FROM rows GROUP BY regionDetails
Desugars into a CASE inside COUNT/SUM/AVG/MIN/MAX and preserves DISTINCT. JSON_ARRAYAGG does not support FILTER.
window.aggregate-overPostgreSQL compatibleSELECT SUM(amount) OVER (PARTITION BY region) AS total FROM rowsDetails
Without explicit framing, the default is the whole partition when unordered and a peer-aware running frame when ordered. ROWS, RANGE, GROUPS, and exclusions are tracked separately below.
window.in-expressionPostgreSQL compatibleSELECT amount, amount - LAG(amount) OVER (ORDER BY amount, region) AS change, 100.0 * amount / SUM(amount) OVER () AS pct FROM rowsDetails
A window is an expression: the arithmetic around it is evaluated after the window has run, over the column it produced.
window.over-groupedPostgreSQL compatibleSELECT 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(*) > 0Details
Windows run after GROUP BY and HAVING, matching PostgreSQL, so they rank groups and their OVER clause reads the group's aggregates.
window.value-functionsPostgreSQL compatibleSELECT 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 rowsDetails
FIRST_VALUE/LAST_VALUE respect the frame; matching PostgreSQL, the default frame ends at the current peer group.
window.ntilePostgreSQL compatibleSELECT amount, NTILE(2) OVER (ORDER BY amount) AS half FROM rowswindow.distributionPostgreSQL compatibleSELECT amount, PERCENT_RANK() OVER (ORDER BY amount) AS pr, CUME_DIST() OVER (ORDER BY amount) AS cd FROM rowswindow.framePostgreSQL compatibleSELECT amount, SUM(amount) OVER (ORDER BY amount, joined ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS windowed FROM rowsDetails
ROWS frames use row distances; RANGE frames use peers or numeric ordering-value distances; GROUPS frames use peer-group distances. Exclusions are supported.
window.frame-range-offsetPostgreSQL compatibleSELECT amount, SUM(amount) OVER (ORDER BY amount RANGE BETWEEN 1 PRECEDING AND CURRENT ROW) AS windowed FROM rowsDetails
Numeric RANGE offsets measure ordering-value distance and require one numeric ORDER BY expression. Temporal interval offsets are not supported.
window.outside-selectPostgreSQL compatibleSELECT amount FROM rows ORDER BY ROW_NUMBER() OVER (ORDER BY amount)Details
Window functions work in SELECT and ORDER BY. WHERE, GROUP BY, and HAVING cannot contain windows.
join.rightPostgreSQL compatibleSELECT r.region FROM rows r RIGHT JOIN dims d ON d.region = r.regionDetails
Desugars to the mirrored LEFT JOIN; supported as the sole join of a block, and not beside SELECT *.
join.non-equiPostgreSQL compatibleSELECT r.region FROM rows r JOIN dims d ON d.amount > r.amountDetails
Executes as a nested-loop join (probe x build); equalities keep the hash path.
select.distinct-wildcardPostgreSQL compatibleSELECT DISTINCT * FROM rowsDetails
Expands bare or qualified wildcards to exactly their selected source columns before DISTINCT grouping is planned.
limit.offsetPostgreSQL compatibleSELECT amount FROM rows LIMIT 5 OFFSET 2Details
OFFSET is accepted directly after LIMIT.
select.no-fromPostgreSQL compatibleSELECT 1 + 1 AS two, UPPER('minnow') AS nameselect.valuesPostgreSQL compatibleSELECT v.column1 AS n, v.column2 AS tag FROM (VALUES (1, 'one'), (2, 'two')) vDetails
VALUES works standalone, as a set-operation member, and as a derived table with AS alias(col, ...) renaming; columns default to column1..columnN.
limit.parameterPostgreSQL compatibleSELECT region, amount FROM rows ORDER BY region NULLS LAST, amount LIMIT $1 OFFSET $2
-- bound: [2,1]Details
LIMIT and OFFSET take placeholders; the plan re-binds per execution like any parameter.
limit.fetch-firstPostgreSQL compatibleSELECT amount FROM rows ORDER BY amount OFFSET 1 ROWS FETCH FIRST 2 ROWS ONLYDetails
PostgreSQL's FETCH clause is accepted as a spelling of LIMIT; SQLite itself only speaks LIMIT.
offset.standalonePostgreSQL compatibleSELECT amount FROM rows ORDER BY amount OFFSET 2Details
OFFSET no longer requires LIMIT. SQLite itself needs LIMIT -1 OFFSET n.
ddl.create-tablePostgreSQL compatibleCREATE TABLE made (id INTEGER PRIMARY KEY, label TEXT NOT NULL, at TIMESTAMP)Details
Common PostgreSQL type names map onto Minnow's four stored value kinds (widths parse and are ignored); one PRIMARY KEY or UNIQUE column becomes the row-addressing key. ALTER TABLE ADD/DROP COLUMN and DROP TABLE are tracked separately.
ddl.type-spellingsPostgreSQL compatibleCREATE TABLE spelled (id int4 PRIMARY KEY, name character varying(10), note character varying, at timestamp with time zone, seen timestamp without time zone, big int8, small int2, f float8, r float4, flag bool)Details
PostgreSQL's internal type names (int2, int4, int8, float4, float8, bool) and its multi-word spellings (character varying, timestamp with time zone, timestamp without time zone, time with time zone) map onto the same storage as INTEGER, BIGINT, DOUBLE PRECISION, BOOLEAN, VARCHAR, and TIMESTAMPTZ. pg_dump and migration tools emit these spellings.
ddl.temporary-tablePostgreSQL compatibleCREATE TEMP TABLE scratch (id INTEGER PRIMARY KEY, note TEXT)Details
TEMP, TEMPORARY, UNLOGGED, GLOBAL, and LOCAL are accepted and ignored: every table lives in the one database with one durability, so the modifiers document intent and change nothing.
ddl.named-column-constraintPostgreSQL compatibleCREATE TABLE guarded (id INTEGER PRIMARY KEY, n INTEGER CONSTRAINT guarded_n_positive CHECK (n > 0))Details
CONSTRAINT name before a column's CHECK or REFERENCES names that constraint, as it does at table level; the name of a column's PRIMARY KEY, UNIQUE, or NOT NULL is informational.
trigger.create-afterPostgreSQL differenceCREATE TRIGGER keyed_audit AFTER INSERT ON keyed BEGIN INSERT INTO rows (region, amount) VALUES (NEW.name, NEW.score); ENDDetails
AFTER and BEFORE row triggers on INSERT/UPDATE/DELETE execute atomically with NEW/OLD references. Bodies support parameter-free INSERT ... VALUES into keyless tables and UPDATE/DELETE against keyed tables; ON CONFLICT and RETURNING are rejected. One cascade level is allowed.
PostgreSQL: Minnow uses an embedded BEGIN ... END trigger body; PostgreSQL triggers call a separately declared function.
trigger.create-beforePostgreSQL differenceCREATE TRIGGER keyed_before BEFORE INSERT ON keyed BEGIN INSERT INTO rows (region, amount) VALUES (NEW.name, NEW.score); ENDDetails
BEFORE 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.
PostgreSQL: Minnow uses an embedded BEGIN ... END trigger body; PostgreSQL triggers call a separately declared function.
trigger.body-update-deletePostgreSQL differenceCREATE TRIGGER keyed_counts AFTER INSERT ON keyed BEGIN UPDATE stats SET total = total + NEW.score WHERE region = NEW.name; ENDDetails
UPDATE and DELETE trigger bodies run against keyed tables, reading current state each firing. One body statement touching the same target row for two triggering rows in one firing is rejected; separate body statements may each touch that row.
PostgreSQL: Minnow uses an embedded BEGIN ... END trigger body; PostgreSQL triggers call a separately declared function.
trigger.dropPostgreSQL differenceDROP TRIGGER droppable_auditDetails
PostgreSQL: PostgreSQL requires DROP TRIGGER name ON table; Minnow's trigger names are database-wide.
where.parenthesizedPostgreSQL compatibleSELECT region FROM rows WHERE (amount > 5 AND region = 'west')where.notPostgreSQL compatibleSELECT region FROM rows WHERE NOT active = TRUEexpression.concatPostgreSQL compatibleSELECT region || '-' || label AS tag FROM dimsDetails
|| concatenates strings and propagates NULL; non-string operands are a type error. One operand must be ordinary text, matching PostgreSQL's operator resolution: array and JSONB || are structural concatenation (see array.concat and json.concat), and two non-text domain operands have no || operator. A domain value concatenated with text renders in its Minnow text form.
expression.integer-divisionPostgreSQL compatibleSELECT 7 / 2 AS quotient, -7 / 2 AS negative, 7.0 / 2 AS exact, CAST(amount AS INTEGER) / 2 AS half FROM rowsDetails
Division follows PostgreSQL's typing. Two integer operands — INTEGER, BIGINT, or SMALLINT columns, integer constants, COUNT, integer CASTs, and integer arithmetic or aggregates over them — divide as integers, truncating toward zero: 7 / 2 is 3 and -7 / 2 is -3. A double, NUMERIC, or decimal constant operand makes the quotient fractional: 7.0 / 2 is 3.5. A bound parameter takes the type of its integer partner, so id / $1 truncates when $1 is bound to an integer.
expression.untyped-arithmeticPostgreSQL compatibleSELECT '5' + 1 AS six, 2 * '3' AS six_again FROM rowsDetails
An untyped string constant beside a number in + - * / % is read as a number, as PostgreSQL types the unknown literal by its partner. Text that is not a number is still an error.
expression.moduloPostgreSQL differenceSELECT amount % 3 AS remainder FROM rowsDetails
Division and remainder by zero are NULL, matching SQLite.
PostgreSQL: Minnow permits % on double precision values; PostgreSQL has no % operator for double precision.
expression.castPostgreSQL compatibleSELECT CAST(amount AS INTEGER) AS whole, CAST(amount AS TEXT) AS label FROM rowsDetails
Integer casts round exact NUMERIC ties away from zero and floating-point ties to even. Integer text accepts signed decimal digits. Postfix :: and CAST use the same rules.
identifier.quotedPostgreSQL compatibleSELECT "region", "rows"."amount" FROM "rows" WHERE "amount" > 5Details
Double-quoted identifiers are never keywords and keep their exact spelling.
order-by.nullsPostgreSQL compatibleSELECT region, amount FROM rows ORDER BY region NULLS LAST, amountDetails
Without NULLS FIRST/LAST the default follows PostgreSQL: NULLs last ascending, first descending.
expression.coalescePostgreSQL compatibleSELECT COALESCE(region, 'unknown') AS region_label FROM rowsDetails
Arguments evaluate left to right; the first non-NULL value wins. Arguments are not type-checked: mixed-type arguments are accepted and return the first non-NULL value as-is, where PostgreSQL requires a common type.
expression.date-truncPostgreSQL compatibleSELECT DATE_TRUNC('month', joined) AS joined_month FROM rowsDetails
Units: year, quarter, month, week (Monday start), day, hour, minute, second. Truncation is in UTC; the engine has no session time zone.
PostgreSQL: DATE_TRUNC follows PostgreSQL syntax; Minnow evaluates datetimes in UTC because it has no session time zone.
expression.date-addPostgreSQL compatibleSELECT joined + INTERVAL '1 month' AS next_month, joined - INTERVAL '2 days 3 hours' AS earlier FROM rows WHERE joined IS NOT NULLDetails
INTERVAL 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-corePostgreSQL compatibleSELECT UPPER(label) AS u, LOWER(label) AS l, LENGTH(label) AS n, SUBSTR(label, 2, 3) AS mid, TRIM(label) AS t FROM dimsDetails
SUBSTRING is accepted as a spelling of SUBSTR; LENGTH and SUBSTR count characters, not UTF-16 units.
function.absPostgreSQL compatibleSELECT ABS(amount - 5) AS distance FROM rowsfunction.numeric-corePostgreSQL differenceSELECT 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 rowsDetails
GREATEST/LEAST ignore NULL arguments, matching PostgreSQL.
PostgreSQL: Minnow's numeric functions accept integer, exact, and double-precision arguments interchangeably, where PostgreSQL's distinct integer, numeric, and double-precision overloads do not.
function.string-extendedPostgreSQL differenceSELECT 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 NULLDetails
PostgreSQL: The bundled form includes INSTR; PostgreSQL has no INSTR and spells the same (string, substring) lookup STRPOS, with the arguments in the same order.
function.extractPostgreSQL compatibleSELECT EXTRACT(year FROM joined) AS y, EXTRACT(dow FROM joined) AS d FROM rows WHERE joined IS NOT NULLDetails
Fields: year, quarter, month, week (ISO), day, hour, minute, second, epoch, dow — all in UTC. SQLite spells this strftime.
aggregate.distinct-argumentPostgreSQL compatibleSELECT region, COUNT(DISTINCT amount) AS amounts, COUNT(DISTINCT active) AS states, SUM(amount) AS total FROM rows GROUP BY regionDetails
COUNT/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-keyPostgreSQL compatibleSELECT r.region FROM rows r JOIN dims d ON d.region = r.region AND d.amount = r.amountDetails
Multi-key conditions execute as a nested-loop join; single equalities keep the hash path.
join.crossPostgreSQL compatibleSELECT r.region AS region, d.label AS label FROM rows r CROSS JOIN dims djoin.fullPostgreSQL compatibleSELECT r.amount AS amount, d.label AS label FROM rows r FULL JOIN dims d ON d.region = r.regionDetails
Desugars into a union of two left joins, so it must be the sole join, with an equality ON, named output columns rather than SELECT *, and no grouping, DISTINCT, or window functions yet.
join.full-groupedPostgreSQL compatibleSELECT r.region AS region, COUNT(*) AS matched FROM rows r FULL JOIN dims d ON d.region = r.region GROUP BY r.regionDetails
The sole FULL JOIN composes with grouping, DISTINCT, wildcards, and window functions by applying these operations after both unmatched sides are included.
order-by.ordinalPostgreSQL compatibleSELECT region, amount FROM rows ORDER BY 2 DESCDetails
Ordinals resolve after schema-bound wildcard expansion when needed; out-of-range ordinals are an error.
window.lag-leadPostgreSQL compatibleSELECT amount, LAG(amount) OVER (ORDER BY amount) AS previous, LEAD(amount, 1, -1) OVER (ORDER BY amount) AS next FROM rowsDetails
LAG/LEAD take a constant offset (default 1) and default value (default NULL), and require ORDER BY inside OVER.
mutation.insert-selectPostgreSQL compatibleINSERT INTO keyed (name, score) SELECT name || '2' AS name, score + 1 AS score FROM keyedDetails
The SELECT runs at one snapshot and materializes before the batch write. Bare and qualified wildcard select lists are supported and their expanded width is checked against the target columns.
mutation.mergePostgreSQL compatibleMERGE 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)Details
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. Matching PostgreSQL, two source rows cannot act on one target row. MATCHED BY SOURCE, MATCHED BY TARGET, and RETURNING are not supported.
transaction.beginPostgreSQL compatibleBEGINDetails
Holds 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.endPostgreSQL compatibleENDDetails
END and END TRANSACTION commit, as COMMIT does.
transaction.abortPostgreSQL compatibleABORTDetails
ABORT rolls back, as ROLLBACK does.
transaction.session-settingsPostgreSQL compatibleSET search_path TO publicDetails
SET [SESSION | LOCAL] name TO value, SET TRANSACTION …, and RESET name are accepted and ignored: an embedded single-session engine has no search path, timeouts, encodings, or isolation levels to configure, and drivers and migration tools issue these on every connection. SET TIME ZONE accepts only UTC, because every datetime is an instant in UTC; another zone is refused rather than silently ignored.
transaction.show-settingPostgreSQL compatibleSHOW server_versionDetails
SHOW returns the engine's fixed answer for the settings drivers and tools read on connection — search_path, server_version, timezone, transaction_isolation, client_encoding, and the like — as one row. An unknown setting is an error, as in PostgreSQL.
transaction.commitPostgreSQL compatibleCOMMITtransaction.rollbackPostgreSQL compatibleROLLBACKfunction.char-lengthPostgreSQL compatibleSELECT CHAR_LENGTH(region) AS n FROM rows WHERE region IS NOT NULLfunction.octet-lengthPostgreSQL compatibleSELECT OCTET_LENGTH(region) AS n FROM rows WHERE region IS NOT NULLDetails
Counts the UTF-8 encoding's bytes.
function.substring-from-forPostgreSQL compatibleSELECT SUBSTRING(region FROM 1 FOR 2) AS part FROM rows WHERE region IS NOT NULLDetails
The position window is intersected with the string, so a start below 1 shortens the result instead of shifting it.
function.trim-specificationPostgreSQL compatibleSELECT TRIM(LEADING 'w' FROM region) AS trimmed FROM rows WHERE region IS NOT NULLfunction.trim-multi-characterPostgreSQL differenceSELECT TRIM(BOTH 'we' FROM region) AS trimmed FROM rows WHERE region IS NOT NULLDetails
Minnow removes a multi-character trim string as a repeated unit; PostgreSQL reads the argument as a set of characters instead.
PostgreSQL: Minnow removes the trim string as a repeated unit; PostgreSQL treats its characters as a set.
function.positionPostgreSQL compatibleSELECT POSITION('es' IN region) AS at FROM rows WHERE region IS NOT NULLfunction.padPostgreSQL compatibleSELECT LPAD(region, 6, '-') AS padded FROM rows WHERE region IS NOT NULLfunction.overlayPostgreSQL compatibleSELECT OVERLAY(region PLACING 'X' FROM 1 FOR 1) AS masked FROM rows WHERE region IS NOT NULLselect.qualified-wildcardPostgreSQL compatibleSELECT rows.* FROM rowsDetails
Output names follow the rule a bare * uses: the column's own name from one source, alias-qualified from several. Expansion happens before shape-dependent planning, so qualified wildcards compose with DISTINCT, windows, set operations, CTE/derived column lists, ORDER BY expressions and ordinals, and INSERT SELECT.
select.wildcard-window-compositionPostgreSQL compatibleSELECT rows.*, ROW_NUMBER() OVER (ORDER BY amount) AS position FROM rows ORDER BY amountDetails
Wildcard columns are schema-bound before window lowering. The generated conformance corpus also crosses wildcards with DISTINCT, set operations, column lists, and ORDER BY expressions and ordinals.
from.column-alias-listPostgreSQL compatibleSELECT y.a AS a FROM rows AS y(a, b, c, d)from.parenthesized-joinPostgreSQL compatibleSELECT COUNT(*) AS pairs FROM (rows r CROSS JOIN dims d)Details
A parenthesized join group is accepted as the first source or as the operand of a comma, CROSS JOIN, or INNER JOIN, whose ON condition then filters the product. A group cannot take an alias, and LEFT, RIGHT, FULL, NATURAL, or USING joins onto a group are rejected: the flat join chain cannot treat the group as one side.
aggregate.all-quantifierPostgreSQL compatibleSELECT SUM(ALL amount) AS total FROM rowsderived-table.set-operationPostgreSQL compatibleSELECT s.amount AS amount FROM (SELECT amount FROM rows UNION SELECT amount FROM dims) scomment.simplePostgreSQL compatibleSELECT amount FROM rows -- a commentcomment.bracketedPostgreSQL compatibleSELECT /* a comment */ amount FROM rowsjoin.commaPostgreSQL compatibleSELECT rows.amount AS amount FROM rows, dims WHERE dims.region = rows.regionjoin.usingPostgreSQL compatibleSELECT rows.amount AS amount FROM rows JOIN dims USING (region)Details
Unlike PostgreSQL, joined columns stay one per side: `SELECT *` returns both and an unqualified reference to a join column is ambiguous. Qualify it, or name the side you want.
join.naturalPostgreSQL compatibleSELECT rows.amount AS amount FROM rows NATURAL JOIN dimsDetails
The 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.
join.full-compound-onPostgreSQL compatibleSELECT r.region FROM rows r FULL JOIN dims d ON d.region = r.region AND d.label <> ''Details
A sole FULL JOIN accepts compound ON predicates and preserves unmatched rows from both sides.
datetime.current-datePostgreSQL compatibleSELECT CURRENT_DATE > DATE '2000-01-01' AS elapsedDetails
Resolved once per execution, so every row of a statement sees one instant; results that read the clock never memoize.
datetime.current-timestampPostgreSQL compatibleSELECT CURRENT_TIMESTAMP > TIMESTAMP '2000-01-01 00:00:00' AS elapseddatetime.localtimePostgreSQL compatibleSELECT LOCALTIME IS NOT NULL AS tickingDetails
LOCALTIME reads as an 'HH:MM:SS' string of the current UTC time, like SQLite's CURRENT_TIME — Minnow has no session time zone, where PostgreSQL renders the session-local wall clock.
predicate.row-comparisonPostgreSQL compatibleSELECT amount FROM rows WHERE (region, amount) = ('west', 10)predicate.row-inPostgreSQL compatibleSELECT amount FROM rows WHERE (region, amount) IN (('west', 10), ('east', 3))predicate.row-nullPostgreSQL compatibleSELECT amount FROM rows WHERE (region, region) IS NOT NULLliteral.radixPostgreSQL compatibleSELECT 0x0A AS tenliteral.digit-separatorPostgreSQL compatibleSELECT 1_000 AS thousandliteral.exact-decimalPostgreSQL compatibleSELECT 1.000000000000000000000000 / 3 = CAST('0.333333333333333333333333' AS NUMERIC) AS exact_thirdsDetails
Decimal constants keep their exact digits, as PostgreSQL's NUMERIC typing does. Arithmetic among constants is exact — 0.1 + 0.2 is 0.3 — and a quotient's scale follows the operands' written scales. A result that reads back identically from a JavaScript number is returned as a number; one the number boundary would visibly round is returned as an exact decimal string.
literal.big-integerPostgreSQL compatibleSELECT 10000000000000000001 = CAST('10000000000000000001' AS NUMERIC) AS sameDetails
An integer constant beyond 2^53 stays exact instead of rounding or failing. A value Float64 cannot hold is returned as a decimal string, and writing one to an INTEGER or BIGINT column is rejected rather than silently rounded.
literal.scientificPostgreSQL differenceSELECT 2.5e-1 AS quarterDetails
Scientific notation is a numeric constant, as in PostgreSQL. PostgreSQL renders every such constant as NUMERIC text; Minnow returns a number when the value reads back identically from one, and renders a constant that stays exact fully expanded, as PostgreSQL renders it.
PostgreSQL: PostgreSQL types every scientific-notation constant NUMERIC and renders it as text. Minnow evaluates it exactly but returns a number whenever the value reads back identically from one, keeping ordinary constants number-typed at the JavaScript boundary; a constant that stays exact renders fully expanded, as PostgreSQL renders it.
limit.with-tiesPostgreSQL compatibleSELECT region FROM rows WHERE region IS NOT NULL ORDER BY region DESC FETCH FIRST 1 ROWS WITH TIESDetails
The limit cannot be pushed into a scan, so these plans run unlimited and the ordered result is trimmed.
cte.in-subqueryPostgreSQL compatibleSELECT s.amount AS amount FROM (WITH inner_cte AS (SELECT amount FROM rows) SELECT amount FROM inner_cte) swindow.nth-valuePostgreSQL compatibleSELECT NTH_VALUE(amount, 2) OVER (ORDER BY amount) AS second FROM rowswindow.namedPostgreSQL compatibleSELECT SUM(amount) OVER w AS running FROM rows WINDOW w AS (ORDER BY amount)window.frame-groupsPostgreSQL compatibleSELECT COUNT(*) OVER (ORDER BY amount GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW) AS peers FROM rowswindow.frame-excludePostgreSQL compatibleSELECT COUNT(*) OVER (ORDER BY amount RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING EXCLUDE CURRENT ROW) AS others FROM rowsaggregate.groupingPostgreSQL compatibleSELECT GROUPING(region) AS aggregated FROM rows GROUP BY ROLLUP(region)Details
A bitmask over the arguments, most significant first.
aggregate.any-valuePostgreSQL compatibleSELECT ANY_VALUE(amount) AS sample FROM rowsDetails
Which row of the group answers is implementation-dependent; this engine returns the minimum.
PostgreSQL: PostgreSQL may choose any non-null group member; Minnow documents the minimum. This cross-engine profile checks acceptance rather than row equality, while the native browser behavior probe asserts Minnow's minimum.
aggregate.variancePostgreSQL compatibleSELECT VAR_POP(amount) AS spread FROM rowsDetails
Built 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.stddevPostgreSQL compatibleSELECT STDDEV_POP(amount) AS spread FROM rowsaggregate.booleanPostgreSQL compatibleSELECT EVERY(amount > 1) AS all_positive FROM rowsjson.agg-spellingsPostgreSQL compatibleSELECT region, JSON_AGG(amount ORDER BY amount) AS amounts, JSONB_AGG(amount ORDER BY amount DESC) AS amounts_desc FROM rows GROUP BY regionDetails
json_agg and jsonb_agg are PostgreSQL's spellings of JSON_ARRAYAGG, with the same ORDER BY inside the call. jsonb and json produce the same document text.
json.build-objectPostgreSQL compatibleSELECT JSON_BUILD_OBJECT('region', region, 'amount', amount) AS doc, JSONB_BUILD_ARRAY(amount, region) AS list FROM rowsDetails
json_build_object, jsonb_build_object, json_build_array, and jsonb_build_array are PostgreSQL's spellings of JSON_OBJECT and JSON_ARRAY.
json.to-jsonPostgreSQL compatibleSELECT TO_JSON(amount) AS amount_doc, TO_JSONB(region) AS region_doc FROM rowsDetails
to_json and to_jsonb render one SQL value as a JSON document: a number as itself, text as a JSON string, a boolean as true or false, NULL as the document null, and a JSON value as itself.
json.row-referencePostgreSQL compatibleSELECT r.region, JSON_AGG(r ORDER BY r.amount) AS rows_doc, JSON_AGG(ROW_TO_JSON(r) ORDER BY r.amount) AS row_docs FROM (SELECT region, amount FROM rows) r GROUP BY r.regionDetails
A table alias used as a value is the row as a JSON object with the source's columns in order, as PostgreSQL treats it — the shape json_agg(agg) and to_json(obj) take in Kysely's jsonArrayFrom and jsonObjectFrom. Datetime members render as ISO 8601 with a Z suffix.
json.valuePostgreSQL compatibleSELECT JSON_VALUE('{"a": 1}', '$.a') AS aDetails
Accepts JSON text and stored JSON/JSONB values. Paths support $, member steps, and array subscripts.
json.queryPostgreSQL differenceSELECT JSON_QUERY('{"a": [1, 2]}', '$.a') AS aDetails
PostgreSQL: The JSON value agrees, but Minnow returns compact JSON text while PostgreSQL's text rendering includes spaces.
json.existsPostgreSQL compatibleSELECT JSON_EXISTS('{"a": 1}', '$.a') AS presentjson.is-jsonPostgreSQL compatibleSELECT '{"a": 1}' IS JSON OBJECT AS shapedjson.objectPostgreSQL differenceSELECT JSON_OBJECT('a' VALUE 1, 'detail' VALUE JSON_OBJECT('name' VALUE 'Acme')) AS documentDetails
Defaults to NULL ON NULL and WITHOUT UNIQUE KEYS. NULL keys are rejected. JSON-producing arguments embed as documents rather than escaped strings; ordinary text remains a string. The constructor returns JSON text; cast or store it as JSON/JSONB for domain validation and JSONB canonicalization.
PostgreSQL: The JSON value agrees, but Minnow returns compact JSON text while PostgreSQL's text rendering includes spaces.
json.arrayPostgreSQL differenceSELECT JSON_ARRAY(1, NULL, 2) AS documentDetails
Defaults to NULL ON NULL, unlike PostgreSQL's ABSENT ON NULL default for JSON_ARRAY. The explicit NULL/ABSENT clause is not supported. The constructor returns JSON text; cast or store it as JSON/JSONB for domain validation and JSONB canonicalization.
PostgreSQL: Minnow defaults JSON_ARRAY to NULL ON NULL, so a NULL argument becomes a JSON null where PostgreSQL's ABSENT ON NULL default drops it; Minnow also returns compact JSON text while PostgreSQL's rendering includes spaces.
json.arrowPostgreSQL differenceSELECT CAST('{"a": {"b": [5, 6]}}' AS JSON) -> 'a' -> 'b' -> 1 AS elementDetails
PostgreSQL's -> member/element access, chainable and usable anywhere an expression is. A text key selects an object member; an integer key selects an array element. The result is a JSON value, so a selected string keeps its quotes and a selected JSON null stays the document null. Behaviour follows the json type: a document of the wrong shape for the key selects NULL, without jsonb's scalar-as-one-element-array reading. A document that is not JSON is an error, matching the operators' typed-json requirement.
PostgreSQL: The JSON value agrees, but Minnow returns compact JSON text at the JavaScript boundary while PostgreSQL clients commonly decode the json result natively. Minnow's parameters carry JavaScript types, so an integer parameter key selects an array element directly where an untyped PostgreSQL placeholder resolves to the text-key operator and needs an explicit ::int cast. Minnow's JSON values also compare as their canonical text, so -> results are valid comparison operands where PostgreSQL has no json = json operator.
json.arrow-textPostgreSQL compatibleSELECT CAST('{"items": ["x", "y"]}' AS JSON) -> 'items' ->> 0 AS first_itemDetails
->> returns text: strings unquoted, other scalars as their JSON rendering, objects and arrays serialized as compact JSON text, and a selected JSON null as SQL NULL.
json.arrow-indexPostgreSQL compatibleSELECT CAST('["x", "y", "z"]' AS JSON) ->> -1 AS last_itemDetails
Negative element positions count from the end of the array; positions out of range in either direction select NULL.
json.arrow-untypedMinnow extensionSELECT '{"a": {"b": ["x", "y"]}}' -> 'a' -> 'b' ->> 1 AS second_itemDetails
Like Minnow's SQL/JSON functions, the arrows accept ordinary JSON text and stored JSON/JSONB values directly; PostgreSQL requires a json-typed document to resolve the operator.
PostgreSQL: PostgreSQL requires a json-typed document for -> and ->>; Minnow also accepts ordinary JSON text directly, as its SQL/JSON functions do.
ddl.create-table-if-not-existsPostgreSQL compatibleCREATE TABLE IF NOT EXISTS made (a INTEGER)ddl.create-table-defaultPostgreSQL compatibleCREATE TABLE defaulted (id INTEGER PRIMARY KEY, tier TEXT DEFAULT 'basic')Details
DEFAULT accepts a variable-free scalar expression. Omission or SQL DEFAULT invokes it; explicit NULL follows the column's independent nullability.
ddl.generated-columnPostgreSQL compatibleCREATE TABLE generated_value (base INTEGER, doubled INTEGER GENERATED ALWAYS AS (base * 2) STORED)Details
Stored generated columns recompute on every write and may be indexed, but cannot be caller-assigned or used as Minnow's row-addressing primary/unique key. Expressions are immutable and row-local.
ddl.create-table-key-clausePostgreSQL compatibleCREATE TABLE keyed_clause (a INTEGER, b TEXT, PRIMARY KEY (a))ddl.alter-table-add-columnPostgreSQL compatibleALTER TABLE rows ADD COLUMN note TEXTDetails
Existing rows have no value for the new column, so it is always nullable.
ddl.create-table-as-selectPostgreSQL compatibleCREATE TABLE copied AS SELECT region FROM rowstype.exact-numericPostgreSQL differenceSELECT CAST(1.25 AS DECIMAL(12, 2)) AS amountDetails
Exact through storage, arithmetic, comparison, aggregates, windows, and ROUND, TRUNC, ABS, FLOOR, CEIL, MOD, and SIGN. Addition, subtraction, remainder, and multiplication carry PostgreSQL's display scale (the larger operand scale, or their sum for a product); division selects its result scale the way PostgreSQL does: at least sixteen significant digits, never fewer fractional digits than either operand, rounded half away from zero. A plain-number fallback beside NUMERIC values, as in COALESCE(amount, 0), renders at its own scale.
PostgreSQL: Minnow preserves exact decimals at the JavaScript boundary as strings, as PGlite's default decoder also does. A declared scale renders at exactly that scale as PostgreSQL does; a bare NUMERIC column, a derived arithmetic result, and a value cast or concatenated to text render canonically, without the trailing fractional zeros PostgreSQL preserves. Division and AVG select their result scale the way PostgreSQL does, so quotient digits agree — including AVG over a column whose declared scale exceeds the selection. The canonical encoding does drop a stored value's display scale, so an explicit arithmetic quotient (such as SUM(v) / COUNT(v)) over a column declared with more than about twenty fractional digits can carry fewer digits than PostgreSQL, which floors the selection at the operand's display scale. An arithmetic or comparison expression mixing a float column with a constant Float64 cannot represent stays exact, where PostgreSQL casts the constant to float8 and rounds it before evaluating.
type.json-jsonbPostgreSQL differenceSELECT CAST('{"a":1}' AS JSONB) AS documentDetails
JSON validates and preserves key order; JSONB canonicalizes object keys. Results use JSON text.
PostgreSQL: Minnow returns canonical JSON text at the JavaScript boundary while PostgreSQL clients commonly decode JSONB to a native object.
type.uuidPostgreSQL compatibleSELECT CAST('550e8400-e29b-41d4-a716-446655440000' AS UUID) AS idDetails
Validated and normalized to canonical lowercase text.
type.intervalPostgreSQL differenceSELECT joined + INTERVAL '1 day' AS next_day FROM rowsDetails
Preserves separate month, day, and microsecond fields, and a date or datetime accepts + and - INTERVAL. Interval-valued arithmetic is not supported (see type.interval-arithmetic).
PostgreSQL: Minnow returns a canonical months/days/microseconds string; PostgreSQL clients use their own interval representation and formatting.
ddl.enumPostgreSQL compatibleCREATE TYPE mood AS ENUM ('sad', 'ok')Details
Enum columns validate members and compare in declaration order. ALTER TYPE is not supported.
ddl.secondary-indexPostgreSQL compatibleCREATE INDEX keyed_score_idx ON keyed(score)Details
PostgreSQL-compatible CREATE INDEX syntax over durable scalar and composite indexes. Leftmost equality and IN prefixes plus the next range column prune candidates; every predicate is rechecked.
PostgreSQL: CREATE INDEX follows PostgreSQL syntax.
ddl.drop-secondary-indexPostgreSQL compatibleDROP INDEX keyed_score_idxDetails
Index names are catalog-global. IF EXISTS is also supported.
PostgreSQL: DROP INDEX follows PostgreSQL syntax.
ddl.composite-secondary-indexPostgreSQL compatibleCREATE INDEX keyed_score_bonus_idx ON keyed(score, bonus)Details
Composite keys use a prefix-free, type-preserving tuple encoding with ASC or DESC per component. Planning follows the SQL leftmost-prefix rule.
PostgreSQL: Composite CREATE INDEX follows PostgreSQL syntax.
ddl.unique-secondary-indexPostgreSQL compatibleCREATE UNIQUE INDEX keyed_bonus_idx ON keyed(bonus)Details
UNIQUE membership is enforced atomically across inserts, updates, deletes, upserts, triggers, write scopes, snapshots, and concurrent tabs. Matching PostgreSQL, any NULL component does not conflict.
PostgreSQL: CREATE UNIQUE INDEX follows PostgreSQL syntax.
ddl.multiple-unique-constraintsPostgreSQL compatibleCREATE TABLE multi_unique (id INTEGER PRIMARY KEY, email TEXT UNIQUE)Details
Each table-level or column-level UNIQUE constraint is enforced independently.
ddl.composite-primary-keyPostgreSQL compatibleCREATE TABLE composite_key (shop INTEGER, receipt INTEGER, PRIMARY KEY (shop, receipt))Details
Composite keys use a hidden scalar row locator; primary-key components are immutable.
ddl.composite-foreign-keyPostgreSQL compatibleCREATE TABLE composite_child (shop INTEGER, receipt INTEGER, FOREIGN KEY (shop, receipt) REFERENCES composite_key(shop, receipt))ddl.alter-table-drop-columnPostgreSQL compatibleALTER TABLE keyed DROP COLUMN bonusDetails
A metadata-only drop refuses unique keys, checks, foreign keys, triggers, views, and the last column. Stored column blocks remain until compaction rewrites their segments; persisted full-text data for the column is removed atomically with the catalog update. RESTRICT is the default and CASCADE is rejected.
mutation.upsert-expressionPostgreSQL differenceINSERT INTO keyed (name, score) VALUES ('x', 2) ON CONFLICT (name) DO UPDATE SET score = score + EXCLUDED.scoreDetails
Assignments may read the stored target row, the proposed EXCLUDED row, parameters, constants, CASE, and scalar functions. All conflicting updates and fresh inserts execute in one write scope; one statement cannot affect the same existing key twice. Aggregates, windows, subqueries, and conflict-key reassignment are rejected.
PostgreSQL: Minnow resolves an unqualified target column in DO UPDATE; PostgreSQL treats score beside EXCLUDED.score as ambiguous unless the target is qualified.
mutation.upsert-update-wherePostgreSQL compatibleINSERT INTO keyed (name, score) VALUES ('x', 2) ON CONFLICT (name) DO UPDATE SET score = EXCLUDED.score WHERE EXCLUDED.score > keyed.scoreDetails
A false conflict predicate leaves the existing row unchanged.
transaction.savepointPostgreSQL compatibleSAVEPOINT line_itemDetails
SAVEPOINT, ROLLBACK TO, and RELEASE operate on staged transaction state.
from.lateralPostgreSQL compatibleSELECT x.amount FROM rows r, LATERAL (SELECT amount FROM dims WHERE dims.region = r.region) xDetails
Equality and range-correlated derived sources become set-at-a-time joins. Grouping, aggregates, and per-row ORDER BY ... LIMIT are supported for equality correlations; a range correlation keeps the plain join.
from.lateral-aggregatePostgreSQL compatibleSELECT r.amount, x.n, x.best FROM rows r JOIN LATERAL (SELECT COUNT(*) AS n, MAX(d.label) AS best FROM dims d WHERE d.region = r.region) x ON TRUE ORDER BY r.amountDetails
A global aggregate yields one row per outer row even with no matching inner rows: COUNT reads 0, other aggregates NULL, as PostgreSQL returns. GROUP BY and HAVING inside the lateral query gain the correlation key.
from.lateral-limitPostgreSQL compatibleSELECT r.amount, x.label FROM rows r LEFT JOIN LATERAL (SELECT d.label FROM dims d WHERE d.region = r.region ORDER BY d.label DESC LIMIT 1) x ON TRUE ORDER BY r.amountDetails
ORDER BY ... LIMIT/OFFSET inside an equality-correlated lateral query ranks rows per outer row (ROW_NUMBER partitioned by the key, RANK for WITH TIES) instead of running the query once per row.
from.lateral-non-equiPostgreSQL compatibleSELECT x.amount FROM rows r, LATERAL (SELECT q.amount FROM rows q WHERE q.amount < r.amount) xDetails
Non-equality correlation lowers to an ordinary general join rather than executing the derived source once per outer row.
aggregate.string-aggPostgreSQL compatibleSELECT STRING_AGG(region, ',') AS regions FROM rowsDetails
DISTINCT, aggregate-local ORDER BY, and bounded spill execution are supported.
json.tablePostgreSQL compatibleSELECT j.a FROM JSON_TABLE('{"a":1}', '$' COLUMNS (a INTEGER PATH '$.a')) AS jDetails
Constant documents support $ and $[*] row paths. A document read from row data is refused (see json.table-correlated).
predicate.similar-toPostgreSQL compatibleSELECT amount FROM rows WHERE region SIMILAR TO 'w%'Details
Whole-string SQL wildcard and regular-expression semantics.
collation.explicitPostgreSQL compatibleSELECT region FROM rows ORDER BY region COLLATE "C"Details
C, POSIX, and host Intl locale names are accepted. Collated ordering does not use a plain string index.
aggregate.jsonPostgreSQL differenceSELECT JSON_ARRAYAGG(JSON_OBJECT('region' VALUE region)) AS regions FROM rowsDetails
Supports DISTINCT and aggregate-local ORDER BY, embeds JSON-producing inputs as documents, includes SQL NULL as JSON null, and returns NULL for empty input. FILTER, window use, and explicit NULL/ABSENT clauses are not supported; input order is unspecified without ORDER BY.
PostgreSQL: Minnow returns JSON text; PostgreSQL returns a native JSON value, and member order is unspecified without aggregate-local ORDER BY.
aggregate.array-aggPostgreSQL differenceSELECT region, array_agg(amount ORDER BY amount) AS amounts FROM rows GROUP BY region ORDER BY regionDetails
ARRAY_AGG supports DISTINCT and aggregate-local ORDER BY, includes NULL elements, and returns NULL for empty input. Arrays cross the JavaScript boundary as canonical JSON text. FILTER and window use are not supported.
PostgreSQL: Minnow returns arrays as canonical JSON text at the JavaScript boundary; PostgreSQL clients return native arrays. ARRAY_AGG supports DISTINCT and ordering, but not FILTER or window use.
type.arrayPostgreSQL differenceSELECT ARRAY[1, 2] AS pairDetails
Constructors and array columns use canonical JSON text at the JavaScript boundary. One-based scalar subscripts and ARRAY_AGG are supported. Array operators such as concatenation and ANY/ALL remain unsupported.
PostgreSQL: Minnow returns arrays as canonical JSON text at the JavaScript boundary while PostgreSQL clients commonly return native arrays.
type.array-subscriptPostgreSQL compatibleSELECT (ARRAY[1, 2, 3])[1] AS first_elementDetails
One-based scalar subscripts return NULL for an out-of-range position. Slices and multidimensional access are not supported.
type.timePostgreSQL compatibleSELECT TIME '12:00:00' AS atDetails
A time of day without a time zone, returned as canonical text.
type.datePostgreSQL differenceSELECT CAST('2026-08-26' AS DATE) AS dayDetails
A calendar date without a time zone, returned as canonical YYYY-MM-DD text.
PostgreSQL: Minnow returns zoneless DATE values as canonical YYYY-MM-DD text while PostgreSQL clients commonly materialize them as midnight Date objects.
ddl.sequencePostgreSQL compatibleCREATE SEQUENCE order_idsDetails
NEXTVAL and session-local CURRVAL are supported. Sequence options and ALTER SEQUENCE are not.
ddl.drop-tablePostgreSQL compatibleDROP TABLE doomedDetails
Takes 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-viewPostgreSQL compatibleCREATE VIEW west AS SELECT region, amount FROM rows WHERE region = 'west'Details
The catalog stores the query text and inferred schema, so reads expand a view anywhere a table can be read, including inside a write scope. A view is never a write target. CREATE OR REPLACE redefines one; dependent views follow it, and a definition that would create a cycle is rejected when the view is defined. CREATE VIEW column-name lists are not supported.
ddl.drop-viewPostgreSQL compatibleDROP VIEW doomed_viewddl.check-constraintPostgreSQL compatibleCREATE TABLE checked (a INTEGER NOT NULL CHECK (a > 0), CONSTRAINT small CHECK (a < 100))Details
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.
expression.cast-postfixPostgreSQL compatibleSELECT amount::INTEGER AS whole, -amount::INTEGER * 2 AS scaled FROM rowsDetails
PostgreSQL's postfix cast spelling, the same conversion as CAST(x AS type). It binds tighter than every binary and unary operator, so -amount::INTEGER negates the cast value.
expression.concat-typedPostgreSQL compatibleSELECT 'order-' || amount || '/' || active AS tag FROM rowsDetails
PostgreSQL's text || anynonarray: one operand is text and a number, boolean, or timestamp on the other side renders as text. Two non-text operands (1 || 2) have no || operator, in PostgreSQL or here.
where.datetime-textPostgreSQL compatibleSELECT region FROM rows WHERE joined >= '2026-01-01' AND joined < '2026-02-01 00:00:00'Details
A string constant beside a datetime column reads as a timestamp, a zoneless spelling in UTC, as PostgreSQL types an untyped literal by its context. Catalog-backed plans coerce before execution so zone-map pruning and the keyed point read still apply; both executors read the same way at comparison time. Text that is not a timestamp stays a type error.
where.number-textPostgreSQL compatibleSELECT region FROM rows WHERE amount = '10' OR amount IN ('3', '6')Details
A numeric string beside a number column reads as a number, including in IN lists and bound parameters. A text column compared with a number literal is still rejected, as it is in PostgreSQL.
where.boolean-textPostgreSQL compatibleSELECT region FROM rows WHERE active = 't' AND active <> 'false'Details
PostgreSQL's boolean input spellings t, true, 1, f, false, and 0 read as booleans beside a boolean column.
function.string-postgresPostgreSQL compatibleSELECT CONCAT(region, '-', amount) AS tag, CONCAT_WS('/', region, NULL, 'x') AS joined, LEFT(region, 2) AS l, RIGHT(region, 2) AS r, REVERSE(region) AS rev, REPEAT('ab', 2) AS rep, INITCAP('hello world') AS cap, SPLIT_PART('a-b-c', '-', 2) AS part, STRPOS(region, 'st') AS at, STARTS_WITH(region, 'we') AS starts, TRANSLATE(region, 'we', 'WE') AS tr, ASCII(region) AS code, CHR(65) AS letter, BTRIM('xxhixx', 'x') AS trimmed FROM rows WHERE region IS NOT NULLDetails
PostgreSQL's everyday string functions. CONCAT and CONCAT_WS skip NULL arguments and render numbers, booleans, and timestamps as text; LEFT and RIGHT take negative counts; SPLIT_PART counts from the end for a negative field; BTRIM removes any character of its set, as PostgreSQL does.
function.md5-formatPostgreSQL compatibleSELECT MD5(region) AS digest, FORMAT('%s has %s items (%I, %L)', region, amount, 'a b', 'it''s') AS message FROM rows WHERE region IS NOT NULLDetails
MD5 hashes the UTF-8 bytes to 32 lowercase hex digits. FORMAT supports %s, %I (quoted identifier), %L (quoted literal), %% and positional %n$s; other conversion letters are rejected. Positional arguments advance the next implicit argument position. Missing arguments, malformed directives, and width/alignment directives throw.
predicate.regexPostgreSQL compatibleSELECT region FROM rows WHERE region ~ '^w' AND region !~* 'EAST$' AND region ~* 'W.ST'Details
PostgreSQL's ~, ~*, !~, and !~* use a bounded interpreter with leftmost-longest matching. Grouping, alternation, greedy repetition, anchors, and ASCII POSIX character classes are supported; locale-dependent non-ASCII class membership differs. Pattern backreferences, lookaround, inline flags, and non-greedy quantifiers are refused. Pattern matching is subject to the SQL work and state limits. Operator precedence matches PostgreSQL.
function.regexp-replacePostgreSQL compatibleSELECT REGEXP_REPLACE(region, 'e+', 'E') AS first, REGEXP_REPLACE(region, '[aeiou]', '_', 'g') AS all_vowels, REGEXP_REPLACE('abc', '(a)(b)', '\2\1') AS swapped FROM rows WHERE region IS NOT NULLDetails
Flags g (every match), i (case-insensitive), and n (newline-sensitive); \1 back-references and \& in the replacement follow PostgreSQL. Pattern syntax and work limits follow the regex operator profile; c overrides case-insensitive matching when it appears after i.
expression.power-operatorPostgreSQL compatibleSELECT 2 ^ 10 AS kib, 2 ^ 3 ^ 2 AS left_assoc, -2 ^ 2 AS negated, 2 * 3 ^ 2 AS mixedDetails
PostgreSQL's ^ is exponentiation, binding above * and / and associating to the left: 2 ^ 3 ^ 2 is 64.
function.math-extendedPostgreSQL compatibleSELECT EXP(1) AS e, LN(amount) AS ln, LOG(amount) AS log10, LOG(2, 8) AS log2, LOG10(1000) AS thousand, SIGN(amount - 5) AS sign, TRUNC(CAST(amount AS NUMERIC) / 3, 2) AS trunc, PI() AS pi, CBRT(27) AS cbrt, DIV(CAST(amount AS NUMERIC), 3) AS quotient, WIDTH_BUCKET(amount, 0, 10, 5) AS bucket, DEGREES(PI()) AS half_turn, ROUND(SIN(RADIANS(90))) AS sine FROM rowsDetails
LOG(x) is base 10 and LOG(b, x) an explicit base, as in PostgreSQL. LN, LOG, and LOG10 reject non-positive input; DIV truncates toward zero; the trigonometric family (SIN, COS, TAN, ASIN, ACOS, ATAN, ATAN2, DEGREES, RADIANS) is included.
function.to-char-datetimePostgreSQL compatibleSELECT TO_CHAR(joined, 'YYYY-MM-DD HH24:MI:SS') AS iso, TO_CHAR(joined, 'FMDay, DD FMMonth YYYY') AS spoken, TO_CHAR(joined, 'HH12:MI AM') AS clock, TO_CHAR(joined, 'IW DDD Q') AS calendar FROM rows WHERE joined IS NOT NULLDetails
The datetime template fields YYYY, YY, MM, DD, DDD, D, Q, IW, J, HH24, HH12, HH, MI, SS, MS, US, AM/PM, Month/Mon/Day/Dy in every case, TZ (always UTC), FM to drop padding, and double-quoted literal text. Every datetime is an instant in UTC.
function.to-char-numericPostgreSQL compatibleSELECT TO_CHAR(amount, '999.99') AS padded, TO_CHAR(amount, 'FM999.00') AS trimmed, TO_CHAR(-amount, '9999.9') AS negative, TO_CHAR(amount, '00009') AS zeros, TO_CHAR(amount * 1000, '9,999,999.99') AS grouped, TO_CHAR(amount, 'S999.99') AS signed FROM rowsDetails
The numeric template elements 9, 0, the decimal point, group separators, FM, S, and MI, with PostgreSQL's padding and sign placement. Other elements (EEEE, RN, V, PL, L, TH) are rejected rather than rendered wrongly.
function.to-date-timestampPostgreSQL differenceSELECT TO_DATE('02/01/2026', 'DD/MM/YYYY') AS day, TO_TIMESTAMP('2026-01-02 03:04 PM', 'YYYY-MM-DD HH12:MI AM') AS at, TO_TIMESTAMP(1767322800) AS epochDetails
Reads text against the same template fields TO_CHAR writes; a one-argument TO_TIMESTAMP converts seconds since the epoch. TO_DATE returns a DATE value, rendered as YYYY-MM-DD at the JavaScript boundary.
PostgreSQL: TO_DATE returns a zoneless DATE rendered as YYYY-MM-DD text at the JavaScript boundary, where PostgreSQL clients commonly materialize a midnight Date; TO_TIMESTAMP values agree.
function.make-datePostgreSQL differenceSELECT MAKE_DATE(2026, 1, 2) AS day, MAKE_TIMESTAMP(2026, 1, 2, 3, 4, 5.5) AS atDetails
Fields that do not form a real date or timestamp are an error.
PostgreSQL: MAKE_DATE returns a zoneless DATE rendered as YYYY-MM-DD text at the JavaScript boundary, where PostgreSQL clients commonly materialize a midnight Date; MAKE_TIMESTAMP values agree.
function.agePostgreSQL differenceSELECT AGE(TIMESTAMP '2026-03-15 12:00:00', joined) AS since, AGE(joined) AS so_far FROM rows WHERE joined IS NOT NULLDetails
The calendar difference PostgreSQL's AGE reports (years and months, then days borrowed from the earlier date's month, then time); AGE(x) measures from the statement's CURRENT_DATE. The result is Minnow's canonical interval text.
PostgreSQL: AGE computes PostgreSQL's calendar difference but returns Minnow's canonical months/days/usecs interval text rather than PostgreSQL's '1 mon 14 days 12:00:00' rendering.
where.calendar-equalityPostgreSQL compatibleSELECT amount FROM rows WHERE DATE_TRUNC('month', joined) = TIMESTAMP '2026-01-01 00:00:00' OR EXTRACT(YEAR FROM joined) = 2026 ORDER BY amountDetails
DATE_TRUNC('unit', col) = ts and EXTRACT(YEAR FROM col) = n are planned as ranges on the column (col >= start AND col < start + 1 unit), so they skip blocks by value range and run on the raw datetime kernel; an unaligned timestamp is a constant false.
function.date-partPostgreSQL compatibleSELECT DATE_PART('year', joined) AS y, EXTRACT(DOY FROM joined) AS doy, EXTRACT(ISODOW FROM joined) AS isodow, EXTRACT(ISOYEAR FROM joined) AS isoyear, EXTRACT(DECADE FROM joined) AS decade, EXTRACT(MILLISECONDS FROM joined) AS ms, EXTRACT(YEAR FROM DATE '2026-03-04') AS from_date FROM rows WHERE joined IS NOT NULLDetails
DATE_PART('field', value) is EXTRACT(field FROM value). The fields year, quarter, month, week, day, hour, minute, second, epoch, dow, doy, isodow, isoyear, decade, century, millennium, milliseconds, and microseconds, over timestamps and DATE values, in UTC.
function.unnestPostgreSQL compatibleSELECT x FROM unnest(ARRAY[3, 1, 2]) AS xDetails
UNNEST over an ARRAY constructor produces typed rows and supports WITH ORDINALITY and output column aliases. Table-correlated array inputs and arbitrary array expressions are not supported.
group-by.ordinalPostgreSQL compatibleSELECT UPPER(region) AS place, COUNT(*) AS c FROM rows GROUP BY 1 ORDER BY placeDetails
An integer GROUP BY item is a select-list ordinal, resolved to that select expression before grouping, as PostgreSQL resolves it. An ordinal naming an aggregate or window column is rejected.
group-by.aliasPostgreSQL compatibleSELECT COALESCE(region, 'none') AS place, COUNT(*) AS c FROM rows GROUP BY place ORDER BY placeDetails
A bare GROUP BY name that is an output alias stands for the aliased expression. A name that is both an alias and a source column keeps its source-column meaning, which is also what PostgreSQL does.
group-by.qualified-spellingPostgreSQL compatibleSELECT region, SUM(r.amount) AS total FROM rows r GROUP BY r.region ORDER BY regionDetails
A grouped column may be spelled bare in the select list and qualified in GROUP BY, or the reverse, and both spellings may appear in one expression. With several sources a bare name is the column of the one source that has it, resolved from the schemas at execution.
group-by.expression-over-keyPostgreSQL compatibleSELECT FLOOR(amount / 5) * 5 AS bucket, COUNT(*) AS c FROM rows GROUP BY FLOOR(amount / 5) ORDER BY bucketDetails
A selected expression whose column references all sit inside grouping expressions is grouped, PostgreSQL's rule. COALESCE(region, 'all') over GROUP BY ROLLUP (region) labels the total row the same way.
order-by.qualified-selected-columnPostgreSQL compatibleSELECT region, amount FROM rows AS r ORDER BY r.amount DESCDetails
A sort key qualified by one of the block's own source aliases resolves to the same column selected unqualified. The qualifier must name a real source, and exactly one selected column must match.
limit.zeroPostgreSQL compatibleSELECT region FROM rows ORDER BY amount LIMIT 0Details
LIMIT 0 returns no rows and still reports the result's column shape, the way clients read a query's columns without fetching data.
limit.allPostgreSQL compatibleSELECT amount FROM rows ORDER BY amount LIMIT ALL OFFSET 1Details
LIMIT ALL is PostgreSQL's spelling of no limit, alone or before OFFSET, on a block or a set operation. SQLite has no LIMIT ALL.
set.trailing-orderPostgreSQL compatibleSELECT amount AS n FROM rows WHERE amount > 5 UNION SELECT amount FROM dims UNION ALL SELECT 100 ORDER BY n DESC LIMIT 4Details
A trailing ORDER BY, LIMIT, or OFFSET applies to the whole set operation and names the first member's output columns, including an alias only that member declares and columns an aggregate member does not group by.
select.values-orderedPostgreSQL compatibleVALUES (2, 'two'), (1, 'one') ORDER BY 1 LIMIT 1Details
A VALUES list is a set operation of one-row selects, so it takes the same trailing ORDER BY, LIMIT, and OFFSET, with ordinals naming its column1, column2, ... outputs.
datetime.nowPostgreSQL compatibleSELECT NOW() > TIMESTAMP '2000-01-01 00:00:00' AS elapsedDetails
PostgreSQL's now() is the statement's clock reading, the same instant CURRENT_TIMESTAMP names, resolved once per statement in every row and in mutation defaults.
mutation.insert-do-nothing-any-keyPostgreSQL compatibleINSERT INTO keyed (name, score) VALUES ('x', 9), ('z', 1) ON CONFLICT DO NOTHINGDetails
A DO NOTHING without a conflict target skips rows that collide on the table's unique key, the spelling Kysely's onConflict((oc) => oc.doNothing()) emits. DO UPDATE still names its target.
select.wildcard-with-expressionsPostgreSQL compatibleSELECT *, amount * 2 AS doubled, region IS NULL AS unplaced FROM rows ORDER BY doubled DESCDetails
A bare or qualified wildcard may stand beside other select items, before or after them; each expands from the input schema at binding, as PostgreSQL expands it.
select.distinct-groupedPostgreSQL compatibleSELECT DISTINCT active, COUNT(*) AS c FROM rows GROUP BY region, active ORDER BY active, cDetails
DISTINCT over a grouped, aggregated, HAVING-filtered, or windowed block takes the distinct rows of that block's output, which runs inside a derived table; ORDER BY and LIMIT apply to the distinct rows.
mutation.update-aliasPostgreSQL compatibleUPDATE keyed AS k SET score = k.score + 1 WHERE k.name = 'x' RETURNING k.name, k.scoreDetails
PostgreSQL's mutation alias, with or without AS; assignments, predicates, and RETURNING may qualify by it. DELETE FROM keyed AS k WHERE k.score < 0 takes the same alias.
mutation.delete-aliasPostgreSQL compatibleDELETE FROM keyed AS k WHERE k.score < 0 RETURNING k.nameDetails
The alias resolves in the predicates and RETURNING exactly as a table alias does in SELECT.
mutation.update-subquery-assignmentPostgreSQL compatibleUPDATE keyed SET bonus = (SELECT MAX(score) FROM keyed) + (SELECT COUNT(*) FROM keyed k WHERE k.score > keyed.score) WHERE name = 'x'Details
A scalar subquery in SET, correlated or not, reads the pre-update rows through the ordinary query pipeline: an uncorrelated subquery resolves once at the statement's snapshot, a correlated one is decorrelated like a SELECT item. Aggregates and window functions outside a subquery stay rejected.
mutation.insert-subquery-valuePostgreSQL compatibleINSERT INTO keyed (name, score) VALUES ('q', (SELECT MAX(score) + 1 FROM keyed))Details
A scalar subquery in VALUES is evaluated when the statement runs, at its snapshot, like the statement clock and sequence calls.
mutation.insert-select-on-conflictPostgreSQL compatibleINSERT INTO keyed (name, score) SELECT name, score + 100 FROM keyed WHERE score > 0 ON CONFLICT (name) DO UPDATE SET score = EXCLUDED.scoreDetails
ON CONFLICT applies to the rows a query source produces exactly as to a VALUES list, DO NOTHING (with or without a target) and DO UPDATE alike.
mutation.insert-select-compoundPostgreSQL compatibleINSERT INTO keyed (name, score) WITH top AS (SELECT name FROM keyed WHERE score > 0) SELECT name || '2', 2 FROM top UNION ALL SELECT 'u', 3Details
Any query expression feeds INSERT: a WITH, a set operation, or a parenthesized member; the produced column count is checked against the column list when the rows materialize.
ddl.serialPostgreSQL compatibleCREATE TABLE ticketed (id SERIAL PRIMARY KEY, label TEXT)Details
SERIAL, BIGSERIAL, and SMALLSERIAL are integer columns fed by the table's auto-increment counter, the same default the schema DSL's autoIncrement() declares; the column is NOT NULL, and an explicit value is accepted without advancing the counter, as a PostgreSQL sequence behaves. The counter belongs to the table's unique key, so a serial column must be the primary key.
ddl.identityPostgreSQL compatibleCREATE TABLE identified (id INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, label TEXT)Details
GENERATED ALWAYS AS IDENTITY and GENERATED BY DEFAULT AS IDENTITY read as the auto-increment default; sequence options in parentheses are accepted and ignored, and the counter starts at 1. SQLite's INTEGER PRIMARY KEY AUTOINCREMENT spelling is accepted the same way.
ddl.alter-table-add-column-defaultPostgreSQL compatibleALTER TABLE filled ADD COLUMN tier TEXT NOT NULL DEFAULT 'basic'Details
A constant DEFAULT fills the rows already stored, as PostgreSQL does, which is what allows the added column to be NOT NULL; an expression default such as NOW() fills only the rows written afterwards, so a NOT NULL column with one is still refused.
ddl.foreign-keyPostgreSQL compatibleCREATE TABLE children (id INTEGER PRIMARY KEY, parent INTEGER REFERENCES parents(id) ON DELETE CASCADE)Details
Scalar and composite references target the parent's matching primary-key columns. Writes validate against their transaction; NULL references are satisfied. ON DELETE supports RESTRICT, CASCADE, and SET NULL atomically. Primary keys are immutable, so ON UPDATE has no action.
predicate.regex-longest-classesPostgreSQL compatibleSELECT SUBSTRING('abc' FROM 'a|ab') AS longest, 'A19!' ~ '^[[:alpha:]][[:digit:]]+[[:punct:]]$' AS classesDetails
Leftmost-longest alternation and ASCII POSIX character classes follow the bounded regex profile.
Excluded forms
64 excluded forms, each rejected with the recorded error
aggregate.ordered-setUnsupportedSELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY amount) AS median FROM rowsError: Unsupported function: percentile_cont
Details
Ordered-set aggregates (percentile_cont, percentile_disc, mode) and corr are not supported; a window over ROW_NUMBER() and COUNT() OVER () computes a median.
array.concatUnsupportedSELECT ARRAY[1, 2] || ARRAY[3] AS combinedError: PostgreSQL array concatenation (||) is not supported
Details
PostgreSQL's array || array and array || element are structural concatenation. Minnow refuses || whenever either operand is an array rather than inventing a text concatenation PostgreSQL does not have.
catalog.information-schemaUnsupportedSELECT column_name FROM information_schema.columns WHERE table_name = 'rows'Error: Expected eof, found .
Details
information_schema, pg_catalog, and sqlite_master are not supported; the catalog is read through the API (listTables, describe).
ddl.add-column-if-not-existsUnsupportedALTER TABLE keyed ADD COLUMN IF NOT EXISTS note TEXTError: Expected eof, found EXISTS
Details
ADD COLUMN IF NOT EXISTS is not supported; check the catalog first or let the duplicate-column error stand.
ddl.add-constraintUnsupportedALTER TABLE keyed ADD CONSTRAINT keyed_score_positive CHECK (score > -10)Error: Expected eof, found CHECK
Details
ADD CONSTRAINT and DROP CONSTRAINT are not supported; constraints are declared in CREATE TABLE.
ddl.alter-columnUnsupportedALTER TABLE keyed ALTER COLUMN score SET DEFAULT 7Error: Expected ADD, found ALTER
Details
ALTER COLUMN forms are not supported; see ddl.alter-table-rename.
ddl.alter-table-renameUnsupportedALTER TABLE keyed RENAME COLUMN score TO pointsError: Expected ADD, found RENAME
Details
ALTER TABLE RENAME COLUMN, RENAME TO, ALTER COLUMN TYPE / SET DEFAULT / SET NOT NULL / DROP NOT NULL, ADD CONSTRAINT, and DROP CONSTRAINT are not supported as SQL. ALTER TABLE ADD COLUMN and DROP COLUMN are. The schema DSL's migrate() renames columns and widens nullability through the catalog.
ddl.alter-typeUnsupportedALTER TYPE mood ADD VALUE 'meh'Error: Expected TABLE, found TYPE
Details
ALTER TYPE and DROP TYPE are not supported; CREATE TYPE … AS ENUM is, and the schema DSL widens enum values through migrate().
ddl.byteaUnsupportedCREATE TABLE blobs (id INTEGER PRIMARY KEY, body BYTEA)Error: Unsupported column type: BYTEA
Details
BYTEA columns and bytea literals, casts, and functions (encode, decode, sha256) are not supported; store binary data as base64 or hex TEXT.
ddl.create-table-likeUnsupportedCREATE TABLE copied (LIKE keyed)Error: Unsupported column type: keyed
Details
CREATE TABLE … (LIKE other) is not supported; spell the columns out, or CREATE TABLE AS SELECT for a data copy.
ddl.deferrable-constraintsUnsupportedCREATE TABLE deferred (id INTEGER PRIMARY KEY, parent TEXT REFERENCES keyed (name) DEFERRABLE INITIALLY DEFERRED)Error: Expected )
Details
DEFERRABLE constraints are not supported; every constraint is checked at the statement.
ddl.domainUnsupportedCREATE DOMAIN positive_int AS INTEGER CHECK (VALUE > 0)Error: Expected TABLE, found DOMAIN
Details
CREATE DOMAIN is not supported; put the CHECK on the column.
ddl.drop-table-multipleUnsupportedDROP TABLE IF EXISTS keyed, rowsError: Expected eof, found ,
Details
DROP TABLE takes one table per statement.
ddl.exclude-constraintUnsupportedCREATE TABLE slots (id INTEGER PRIMARY KEY, n INTEGER, EXCLUDE USING gist (n WITH =))Error: Expected )
Details
EXCLUDE constraints are not supported.
ddl.expression-indexUnsupportedCREATE INDEX keyed_lower ON keyed (LOWER(name))Error: Expected )
Details
Expression indexes are not supported; index a stored generated column instead.
ddl.foreign-key-on-updateUnsupportedCREATE TABLE child (id INTEGER PRIMARY KEY, parent TEXT REFERENCES keyed (name) ON UPDATE CASCADE)Error: ON UPDATE CASCADE has nothing to act on: a unique key cannot change
Details
ON UPDATE actions are not supported because primary-key values cannot be updated; ON DELETE CASCADE, SET NULL, RESTRICT, and NO ACTION are.
ddl.foreign-key-set-defaultUnsupportedCREATE TABLE default_children (id INTEGER PRIMARY KEY, parent INTEGER REFERENCES default_parents(id) ON DELETE SET DEFAULT)Error: SET DEFAULT is not supported; use SET NULL or CASCADE
Details
SET DEFAULT would rewrite orphaned references to the column's default value at delete time; Minnow implements RESTRICT, CASCADE, and SET NULL. The statement is rejected at parse, before touching the catalog.
ddl.functionUnsupportedCREATE FUNCTION one() RETURNS integer AS $$ SELECT 1 $$ LANGUAGE sqlError: Expected TABLE, found FUNCTION
Details
CREATE FUNCTION and CREATE TRIGGER … EXECUTE FUNCTION are not supported; triggers take an inline BEGIN … END body of INSERT, UPDATE, and DELETE statements.
ddl.materialized-viewUnsupportedCREATE MATERIALIZED VIEW scores AS SELECT name, score FROM keyedError: Expected TABLE, found MATERIALIZED
Details
Materialized views are not supported; CREATE TABLE AS SELECT stores a snapshot.
ddl.partial-indexUnsupportedCREATE INDEX keyed_positive ON keyed (score) WHERE score > 0Error: Expected eof, found WHERE
Details
Partial indexes (WHERE), expression indexes, INCLUDE columns, USING method, and CONCURRENTLY are not supported; an index is over one or more plain columns.
ddl.schemaUnsupportedCREATE SCHEMA appError: Expected TABLE, found SCHEMA
Details
Schemas are not supported: there is one namespace, and a schema-qualified name (public.users) is refused.
ddl.schema-qualified-nameUnsupportedSELECT region FROM public.rowsError: Expected eof, found .
Details
Schema-qualified names are not supported; there is one namespace.
ddl.sequence-functionsUnsupportedSELECT setval('numbering', 10) FROM rowsError: Unsupported function: setval
Details
setval, currval, and ALTER SEQUENCE are not supported; nextval and CREATE SEQUENCE are.
ddl.serial-non-keyUnsupportedCREATE TABLE ticketed (id INTEGER PRIMARY KEY, seq SERIAL)Error: Auto-increment requires the unique key column: seq
Details
SERIAL and GENERATED … AS IDENTITY are supported on the primary key only; a sequence-fed non-key column is refused.
ddl.view-column-listUnsupportedCREATE VIEW scored (person, points) AS SELECT name, score FROM keyedError: CREATE VIEW takes a name and AS <query>; column lists are not supported
Details
A column list on CREATE VIEW and WITH CHECK OPTION are not supported; alias the columns in the view's SELECT.
expression.at-time-zoneUnsupportedSELECT CURRENT_TIMESTAMP AT TIME ZONE 'UTC' AS local FROM rowsError: Expected eof, found AT
Details
AT TIME ZONE and timezone() are not supported: every datetime is an instant in UTC, and rendering in another zone belongs to the application.
expression.bitwise-operatorsUnsupportedSELECT 1 & 3 AS both, 1 | 2 AS either, 1 # 3 AS differ, 1 << 2 AS shifted FROM rowsError: Unsupported SQL character: &
Details
The bitwise operators &, |, #, ~, <<, and >> and the prefix operators |/ and @ are not supported.
expression.date-minus-dateUnsupportedSELECT DATE '2026-01-10' - DATE '2026-01-05' AS days FROM rowsError: Arithmetic and numeric aggregates require numbers
Details
date - date and timestamp - timestamp are not supported; see expression.interval-values.
expression.interval-valuesUnsupportedSELECT INTERVAL '1 day' + INTERVAL '2 hours' AS total FROM rowsError: Date arithmetic requires a date or datetime value
Details
Interval-valued arithmetic — interval + interval, timestamp - timestamp, date - date, date + integer, justify_days — is not supported. A date or datetime plus or minus an INTERVAL is; subtract two EXTRACT(EPOCH …) readings for a duration in seconds.
expression.row-constructor-valueUnsupportedSELECT ROW(1, 2) AS pair FROM rowsError: Unsupported function: ROW
Details
A row constructor as a value is not supported; row comparisons ((a, b) = (1, 2), (a, b) IN (…)) are.
function.generate-seriesUnsupportedSELECT n FROM generate_series(1, 3) AS nError: Expected identifier, found 1
Details
generate_series is not supported; a recursive CTE produces a series.
function.regexp-arraysUnsupportedSELECT regexp_match(region, '(e.)') AS groups FROM rowsError: Unsupported function: regexp_match
Details
regexp_match, regexp_matches, regexp_split_to_array, and regexp_split_to_table return arrays or sets, which are not supported; SUBSTRING(text FROM 'pattern'), REGEXP_REPLACE, and the ~ operators cover single matches.
function.server-introspectionUnsupportedSELECT version() AS server FROM rowsError: Unsupported function: version
Details
version(), current_user, session_user, current_schema, pg_typeof, and setseed describe a server this engine is not; SHOW server_version answers the version question.
join.right-multiUnsupportedSELECT r.region FROM rows r JOIN dims d ON d.region = r.region RIGHT JOIN dims e ON e.region = r.regionError: RIGHT JOIN is only supported as the sole join
Details
RIGHT JOIN desugars by swapping the two sides of a LEFT JOIN, which needs the block to hold exactly one join. Rewrite the block so the preserved side is on the left of a LEFT JOIN.
join.right-wildcardUnsupportedSELECT * FROM rows r RIGHT JOIN dims d ON d.region = r.regionError: RIGHT JOIN cannot be combined with SELECT *
Details
The desugaring swaps the two sources, which would reorder a wildcard's output columns. Name the output columns explicitly, or write the mirrored LEFT JOIN.
json.concatUnsupportedSELECT CAST('{"a":1}' AS JSONB) || CAST('{"b":2}' AS JSONB) AS mergedError: PostgreSQL JSONB concatenation (||) is not supported
Details
PostgreSQL's jsonb || jsonb merges documents structurally. Minnow refuses || whenever either operand is JSONB rather than inventing a text concatenation PostgreSQL does not have.
json.containment-operatorsUnsupportedSELECT region FROM rows WHERE ('{"theme":"dark"}'::jsonb) @> '{"theme":"dark"}'Error: Unsupported SQL character: @
Details
The @>, <@, ?, ?|, and ?& operators are not supported; compare members with ->> or test presence with JSON_EXISTS.
json.inspection-functionsUnsupportedSELECT jsonb_typeof('{"a":1}'::jsonb) AS kind FROM rowsError: Unsupported function: jsonb_typeof
Details
jsonb_typeof and jsonb_array_length are not supported; JSON_EXISTS, JSON_VALUE, and JSON_QUERY answer most of the same questions.
json.mutation-functionsUnsupportedSELECT jsonb_set('{"a":1}'::jsonb, '{a}', '2') AS doc FROM rowsError: Unsupported function: jsonb_set
Details
jsonb_set, jsonb_insert, jsonb_strip_nulls, and json_extract_path_text are not supported; rebuild the document with JSON_OBJECT / json_build_object and read members with -> and ->>.
json.object-aggUnsupportedSELECT json_object_agg(region, amount) AS doc FROM rowsError: Unsupported function: json_object_agg
Details
json_object_agg / jsonb_object_agg are not supported; json_agg of json_build_object pairs is the usual substitute.
json.path-operatorsUnsupportedSELECT ('{"a":{"b":[1,2]}}'::jsonb) #>> '{a,b,0}' AS leaf FROM rowsError: Unsupported SQL character: #
Details
The #> and #>> path operators are not supported; chain -> and ->> or use JSON_VALUE with a path.
json.table-correlatedUnsupportedSELECT jt.v FROM (SELECT '[1,2]' AS document) AS d, JSON_TABLE(d.document, '$[*]' COLUMNS (v INTEGER PATH '$')) AS jtError: JSON_TABLE currently requires a constant document
Details
PostgreSQL evaluates JSON_TABLE laterally against each source row's document. Minnow expands only constant documents, so a column-valued document is refused at compile time.
literal.bit-stringUnsupportedSELECT B'101' AS bits FROM rowsError: Expected eof, found 101
Details
Bit-string literals and the BIT types are not supported.
mutation.data-modifying-cteUnsupportedWITH w AS (INSERT INTO keyed (name, score) VALUES ('w', 0) RETURNING name) SELECT name FROM wError: Expected SELECT, found INSERT
Details
A data-modifying statement inside WITH is not supported; run the write, then the read.
mutation.update-keylessUnsupportedUPDATE rows SET amount = 1Error: UPDATE requires a table with a unique key
Details
Deliberate: mutation segments address rows by unique key, so tables without one cannot be updated or deleted through any API.
mutation.update-row-valueUnsupportedUPDATE keyed SET (score, bonus) = (1, 2) WHERE name = 'x'Error: Expected identifier, found (
Details
The row-value assignment SET (a, b) = (…) is not supported; assign each column.
mutation.update-set-defaultUnsupportedUPDATE keyed SET bonus = DEFAULT WHERE name = 'x'Error: Ambiguous or missing column: DEFAULT
Details
SET column = DEFAULT and the row-value form SET (a, b) = (…) are not supported; assign the value explicitly.
mutation.upsert-non-key-uniqueUnsupportedINSERT INTO keyed (name, score) VALUES ('z', 3) ON CONFLICT (score) DO NOTHINGError: ON CONFLICT targets the table's primary or unique key columns: name
Details
ON CONFLICT targets the table's primary or row-addressing unique key; a secondary UNIQUE column, a constraint name (ON CONSTRAINT), or a partial-index predicate cannot be the target.
predicate.regex-backreferencesUnsupportedSELECT 'aa' ~ '(a)\1'Error: pattern backreferences are unsupported
Details
Pattern backreferences are refused explicitly; numbered backreferences remain supported in REGEXP_REPLACE replacement text.
predicate.regex-lookaroundUnsupportedSELECT 'ab' ~ '(?=a)a'Error: lookaround and inline flags are unsupported
Details
Lookaround and inline regex flags are refused. Use the supported function flags or rewrite the predicate.
predicate.regex-nongreedyUnsupportedSELECT 'aaa' ~ 'a+?'Error: repeated or non-greedy quantifiers are unsupported
Details
Non-greedy regex quantifiers are refused. Supported matching chooses the leftmost, longest result.
privileges.grantNot applicableGRANT SELECT ON rows TO readerError: Expected SELECT, found GRANT
Details
An embedded, single-user database in the page has no principals to grant to.
PostgreSQL: An embedded database has no server roles, owners, or GRANT boundary.
select.tablesampleUnsupportedSELECT region FROM rows TABLESAMPLE SYSTEM (50)Error: Expected eof, found SYSTEM
Details
TABLESAMPLE is not supported; ORDER BY RANDOM() LIMIT n samples rows.
statement.explainUnsupportedEXPLAIN SELECT region FROM rowsError: Expected SELECT, found EXPLAIN
Details
EXPLAIN is not supported as SQL; the explain() API renders the plan for a query.
statement.multipleUnsupportedSELECT 1 AS a; SELECT 2 AS bError: Run one SELECT statement at a time
Details
One statement per execute() call; a script is split at its semicolons by the caller.
statement.server-commandsUnsupportedVACUUMError: Expected SELECT, found VACUUM
Details
VACUUM, ANALYZE, COMMENT ON, LISTEN, NOTIFY, and DISCARD are server commands with no meaning here; maintenance runs automatically and is driven from the API.
subquery.array-constructorUnsupportedSELECT ARRAY(SELECT amount FROM rows) AS amounts FROM rowsError: Expected )
Details
ARRAY(subquery) is not supported; a scalar subquery with json_agg builds the same list as a JSON document.
transaction.ddl-insideUnsupportedCREATE TABLE inside (id INTEGER PRIMARY KEY)Error: CREATE TABLE is not allowed inside a transaction
Details
Schema statements are refused inside BEGIN … COMMIT: the catalog commits outside the scope, so a rollback could not take them back. Run DDL outside a transaction; migration tools that wrap migrations in one need that step split out.
transaction.isolation-levelNot applicableSET TRANSACTION ISOLATION LEVEL SERIALIZABLEError: SET TRANSACTION ISOLATION LEVEL SERIALIZABLE is not supported: the engine has one isolation level
Details
Every transaction reads one snapshot and commits atomically, which satisfies READ UNCOMMITTED, READ COMMITTED, and REPEATABLE READ, so those levels are accepted in SET TRANSACTION and BEGIN. SERIALIZABLE promises more than that and is refused rather than silently downgraded.
PostgreSQL: Every transaction reads one snapshot and commits atomically, which satisfies READ UNCOMMITTED, READ COMMITTED, and REPEATABLE READ, so those levels are accepted and ignored. SERIALIZABLE promises more than one snapshot can, and is refused rather than silently downgraded.
transaction.lock-tableUnsupportedLOCK TABLE keyed IN SHARE MODEError: Expected SELECT, found LOCK
Details
LOCK TABLE is not supported; a single-session engine has no other session to lock against.
type.array-anyUnsupportedSELECT 2 = ANY(ARRAY[1, 2, 3]) AS foundError: ANY/ALL take a subquery
Details
PostgreSQL's ANY, ALL, and SOME accept an array operand as well as a subquery. Minnow implements only the subquery form, so the array variant is refused at compile time.
type.array-any-parameterUnsupportedSELECT region FROM rows WHERE amount = ANY(ARRAY[1, 2])Error: ANY/ALL take a subquery
Details
= ANY(array) is not supported; use IN (…) with a list or a subquery.
type.interval-arithmeticUnsupportedSELECT INTERVAL '1 day' + INTERVAL '2 hours' AS totalError: Date arithmetic requires a date or datetime value
Details
PostgreSQL adds intervals into a combined interval and subtracts timestamps into an interval. Minnow's interval arithmetic only shifts a date or datetime by + or - INTERVAL, so an interval-valued result is refused.
window.distinct-aggregateUnsupportedSELECT SUM(DISTINCT amount) OVER (PARTITION BY region) AS total FROM rowsError: DISTINCT window aggregates are not supported
Details
An aggregate used as a window function cannot take DISTINCT; PostgreSQL rejects this form too. DISTINCT aggregates work in grouped aggregation, so aggregate in a grouped block and window over that.