SQL

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 BY or HAVING.
  • LATERAL supports equality and range-correlated projections and joins. An equality-correlated lateral query may also group, aggregate, and take a per-row ORDER BY … LIMIT; a range-correlated one cannot group or limit.
  • JSON_TABLE supports constant documents with $ and $[*] row paths.
  • CREATE SEQUENCE supports NEXTVAL and session-local CURRVAL, 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:

  • UPDATE and DELETE on tables without row identity;
  • the SERIALIZABLE isolation level — every transaction reads one snapshot, which satisfies the weaker levels, so READ COMMITTED and REPEATABLE READ are accepted and ignored;
  • roles and GRANT privileges.

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 compatible
SELECT region, amount FROM rows
select.aliasPostgreSQL compatible
SELECT amount AS total FROM rows
select.allPostgreSQL compatible
SELECT ALL region, amount FROM rows
Details

SELECT ALL names the default: every row, duplicates kept. ALL is also accepted inside an aggregate, as in COUNT(ALL amount).

select.distinct-onPostgreSQL compatible
SELECT DISTINCT ON (region) region, amount FROM rows ORDER BY region, amount DESC
Details

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 compatible
SELECT region, amount FROM rows FOR UPDATE
Details

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 compatible
TABLE rows
Details

TABLE name is the standard's spelling of SELECT * FROM name.

literal.string-spellingsPostgreSQL compatible
SELECT E'tab\\there' AS escaped, $$dollar 'quoted'$$ AS plain, 5. AS five, .5 AS half FROM rows
Details

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 compatible
SELECT 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 compatible
SELECT amount total, region "area" FROM rows
Details

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 compatible
SELECT * FROM rows
select.distinctPostgreSQL compatible
SELECT DISTINCT region FROM rows
Details

Plain projections and SELECT DISTINCT * are supported. DISTINCT combined with grouping, HAVING, or aggregate/window expressions is rejected.

select.scalar-subqueryPostgreSQL compatible
SELECT (SELECT MAX(amount) FROM rows) AS peak FROM rows LIMIT 1
expression.arithmeticPostgreSQL difference
SELECT amount * 2 + 1 AS scaled FROM rows
Details

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 difference
SELECT ROUND(amount / 3, 2) AS thirds FROM rows
Details

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 compatible
SELECT region FROM rows WHERE region = 'west'
literal.numberPostgreSQL compatible
SELECT region FROM rows WHERE amount >= 10
literal.booleanPostgreSQL compatible
SELECT active, amount FROM rows WHERE active = TRUE
literal.null-comparisonPostgreSQL compatible
SELECT region FROM rows WHERE region != NULL
literal.datePostgreSQL compatible
SELECT region FROM rows WHERE joined >= DATE '2026-01-01'
literal.timestampPostgreSQL compatible
SELECT 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 compatible
SELECT 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 extension
SELECT 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 compatible
SELECT r.region, d.label FROM rows r JOIN dims d ON d.region = r.region
join.left-equiPostgreSQL compatible
SELECT r.region, d.label FROM rows r LEFT JOIN dims d ON d.region = r.region
where.andPostgreSQL compatible
SELECT region FROM rows WHERE amount > 5 AND region = 'west'
where.in-listPostgreSQL compatible
SELECT region FROM rows WHERE region IN ('west', 'east')
where.not-in-listPostgreSQL compatible
SELECT region FROM rows WHERE region NOT IN ('north')
where.in-subqueryPostgreSQL compatible
SELECT region FROM rows WHERE region IN (SELECT region FROM dims)
where.scalar-subqueryPostgreSQL compatible
SELECT region FROM rows WHERE amount > (SELECT AVG(amount) FROM rows)
group-byPostgreSQL compatible
SELECT region, COUNT(*) AS count FROM rows GROUP BY region
group-by.rollupPostgreSQL compatible
SELECT 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 compatible
SELECT region, active, COUNT(*) AS c FROM rows GROUP BY GROUPING SETS ((region), (active), ())
havingPostgreSQL compatible
SELECT region, COUNT(*) AS count FROM rows GROUP BY region HAVING COUNT(*) > 1
aggregate.countPostgreSQL compatible
SELECT COUNT(*) AS count FROM rows
aggregate.sumPostgreSQL compatible
SELECT SUM(amount) AS total FROM rows
aggregate.avgPostgreSQL compatible
SELECT AVG(amount) AS mean FROM rows
aggregate.min-maxPostgreSQL compatible
SELECT MIN(amount) AS low, MAX(amount) AS high FROM rows
order-by.multi-columnPostgreSQL compatible
SELECT region, amount FROM rows ORDER BY region, amount DESC
order-by.wildcard-referencePostgreSQL compatible
SELECT * FROM rows ORDER BY amount
order-by.qualified-wildcard-referencePostgreSQL compatible
SELECT rows.* FROM rows ORDER BY rows.amount DESC
Details

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 compatible
SELECT amount FROM rows ORDER BY amount LIMIT 2
cte.non-recursivePostgreSQL compatible
WITH west AS (SELECT amount FROM rows WHERE region = 'west') SELECT COUNT(*) AS count FROM west
cte.materializedPostgreSQL compatible
WITH w AS MATERIALIZED (SELECT region, amount FROM rows) SELECT region FROM w
Details

[NOT] MATERIALIZED is PostgreSQL's planner hint on a CTE; the block is planned the same way either way.

cte.chainedPostgreSQL compatible
WITH a AS (SELECT amount FROM rows), b AS (SELECT amount FROM a WHERE amount > 5) SELECT COUNT(*) AS count FROM b
cte.column-listPostgreSQL compatible
WITH totals(place, total) AS (SELECT region, SUM(amount) FROM rows GROUP BY region) SELECT place, total FROM totals
Details

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 compatible
SELECT d.total FROM (SELECT region, SUM(amount) AS total FROM rows GROUP BY region) d ORDER BY d.total
union.distinctPostgreSQL compatible
SELECT region FROM rows UNION SELECT region FROM dims ORDER BY region
union.allPostgreSQL compatible
SELECT region FROM rows UNION ALL SELECT region FROM dims
window.row-numberPostgreSQL compatible
SELECT region, ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount) AS rn FROM rows
window.rankPostgreSQL compatible
SELECT region, RANK() OVER (ORDER BY amount) AS r FROM rows
window.dense-rankPostgreSQL compatible
SELECT region, DENSE_RANK() OVER (ORDER BY amount) AS dr FROM rows
mutation.insert-valuesPostgreSQL compatible
INSERT INTO keyed (name, score) VALUES ('a', 1), ('b', 2)
Details

Through execute(); query() stays read-only.

mutation.insert-values-implicit-columnsPostgreSQL compatible
INSERT 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 compatible
INSERT INTO defaulted_insert DEFAULT VALUES
Details

DEFAULT VALUES inserts one row using catalog defaults. DEFAULT is also accepted in an individual VALUES slot.

mutation.insert-runtime-valuesPostgreSQL compatible
INSERT INTO runtime_values VALUES (1, CURRENT_TIMESTAMP, RANDOM(), GEN_RANDOM_UUID()) RETURNING id
Details

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 compatible
UPDATE keyed SET score = score + 1 WHERE score > 0
Details

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 compatible
DELETE FROM keyed WHERE score < 0
Details

Requires a unique-key table.

mutation.update-fromPostgreSQL compatible
UPDATE keyed SET score = r.amount FROM (SELECT 'x' AS name, 5 AS amount) r WHERE r.name = keyed.name
Details

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 compatible
DELETE FROM keyed USING (SELECT 'y' AS name) gone WHERE gone.name = keyed.name
Details

DELETE … USING joins extra sources to the target as PostgreSQL does; a target row matched by several source rows is deleted once.

mutation.truncatePostgreSQL difference
TRUNCATE TABLE keyed
Details

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 compatible
DELETE FROM keyed WHERE name = 'x' RETURNING keyed.name, keyed.score
Details

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 compatible
UPDATE keyed SET score = score + 1 WHERE name = 'x' RETURNING name, score * 2 AS doubled, UPPER(name) AS label
Details

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 compatible
INSERT INTO keyed (name, score) VALUES ('x', 9) ON CONFLICT (name) DO UPDATE SET score = EXCLUDED.score
Details

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 compatible
INSERT INTO keyed (name, score) VALUES ('x', 9), ('z', 1) ON CONFLICT (name) DO NOTHING
Details

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 extension
INSERT INTO keyed (name, score, bonus) VALUES ('x', 50, 9) ON CONFLICT (name) DO REPLACE
Details

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 compatible
INSERT INTO keyed (name, score, bonus) VALUES ('x', 50, 9) ON CONFLICT (name) DO UPDATE SET score = EXCLUDED.score
Details

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 compatible
SELECT region FROM rows WHERE amount > 5 OR region = 'west'
where.likePostgreSQL compatible
SELECT region FROM rows WHERE region LIKE 'w%'
Details

% matches any run and _ matches one Unicode codepoint.

predicate.is-distinct-fromPostgreSQL compatible
SELECT region FROM rows WHERE region IS DISTINCT FROM 'west'
Details

Null-safe: NULL is not distinct from NULL.

predicate.boolean-testPostgreSQL compatible
SELECT region FROM rows WHERE active IS TRUE OR active IS UNKNOWN
Details

IS [NOT] TRUE/FALSE/UNKNOWN never return UNKNOWN; they desugar to null-safe comparisons.

predicate.like-escapePostgreSQL compatible
SELECT 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 compatible
SELECT 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 compatible
SELECT 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 extension
SELECT 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 extension
SELECT region FROM rows WHERE MATCH(*) AGAINST 'wes*'
Details

PostgreSQL: MATCH(*) searches every text column and is a Minnow full-text extension.

predicate.match-parameterMinnow extension
SELECT 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 extension
SELECT region, BM25(region) AGAINST 'west' AS score FROM rows WHERE MATCH(region) AGAINST 'west' ORDER BY score DESC
Details

PostgreSQL: BM25 exposes Minnow's full-text score; PostgreSQL uses its own text-search types and ranking functions.

function.regexp-substringPostgreSQL compatible
SELECT 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 rows
Details

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 compatible
SELECT region FROM rows WHERE amount > 0 ORDER BY amount * 2 DESC, region
where.betweenPostgreSQL compatible
SELECT region FROM rows WHERE amount BETWEEN 1 AND 5
where.between-symmetricPostgreSQL compatible
SELECT region FROM rows WHERE amount BETWEEN SYMMETRIC 5 AND 1
Details

SYMMETRIC accepts the bounds in either order.

where.is-nullPostgreSQL compatible
SELECT region FROM rows WHERE region IS NULL
where.is-not-nullPostgreSQL compatible
SELECT amount FROM rows WHERE region IS NOT NULL
where.existsPostgreSQL compatible
SELECT 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 compatible
SELECT CASE WHEN amount > 5 THEN 'big' ELSE 'small' END AS size FROM rows
subquery.correlatedPostgreSQL compatible
SELECT 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 compatible
SELECT 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 compatible
SELECT r.region, (SELECT MAX(q.amount + r.amount) FROM rows q WHERE q.region = r.region) AS regional FROM rows r
Details

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 compatible
SELECT r.region, (SELECT q.amount FROM rows q WHERE q.region = r.region ORDER BY q.amount DESC LIMIT 1) AS peak FROM rows r
Details

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 difference
SELECT 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 r
Details

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 compatible
SELECT 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.region
Details

The decorrelated value is functionally determined by the outer grouping keys and is carried as an internal group key.

subquery.correlated-scalar-non-equiPostgreSQL compatible
SELECT r.amount, (SELECT COUNT(*) FROM rows q WHERE q.amount < r.amount) AS lower_count FROM rows r
Details

Non-equality scalar aggregates group inner rows once per distinct outer probe tuple, then join the result back by equality.

subquery.correlated-exists-expressionPostgreSQL compatible
SELECT 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 compatible
SELECT 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 compatible
SELECT 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 compatible
SELECT 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 compatible
SELECT 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 compatible
SELECT 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 compatible
SELECT 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 compatible
WITH RECURSIVE n AS (SELECT MIN(amount) AS v FROM rows UNION ALL SELECT v + 1 FROM n WHERE v < 6) SELECT v FROM n
Details

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 compatible
WITH totals AS (SELECT MAX(score) AS top FROM keyed) DELETE FROM keyed WHERE score >= (SELECT top FROM totals) RETURNING name
Details

WITH precedes INSERT/UPDATE/DELETE; the CTEs are visible to the statement's queries and subqueries.

set.intersectPostgreSQL compatible
SELECT region FROM rows INTERSECT SELECT region FROM dims
Details

INTERSECT binds tighter than UNION and EXCEPT, matching PostgreSQL.

set.exceptPostgreSQL compatible
SELECT region FROM rows EXCEPT SELECT region FROM dims
set.intersect-allPostgreSQL compatible
SELECT region FROM rows INTERSECT ALL SELECT region FROM dims
Details

Bag semantics; SQLite itself has no INTERSECT ALL.

set.except-allPostgreSQL compatible
SELECT region FROM rows EXCEPT ALL SELECT region FROM dims
Details

Bag semantics; SQLite itself has no EXCEPT ALL.

aggregate.count-distinctPostgreSQL compatible
SELECT COUNT(DISTINCT region) AS regions FROM rows
aggregate.filterPostgreSQL compatible
SELECT region, COUNT(*) FILTER (WHERE amount > 5) AS big FROM rows GROUP BY region
Details

Desugars into a CASE inside COUNT/SUM/AVG/MIN/MAX and preserves DISTINCT. JSON_ARRAYAGG does not support FILTER.

window.aggregate-overPostgreSQL compatible
SELECT SUM(amount) OVER (PARTITION BY region) AS total FROM rows
Details

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 compatible
SELECT amount, amount - LAG(amount) OVER (ORDER BY amount, region) AS change, 100.0 * amount / SUM(amount) OVER () AS pct FROM rows
Details

A window is an expression: the arithmetic around it is evaluated after the window has run, over the column it produced.

window.over-groupedPostgreSQL compatible
SELECT 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(*) > 0
Details

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 compatible
SELECT 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 rows
Details

FIRST_VALUE/LAST_VALUE respect the frame; matching PostgreSQL, the default frame ends at the current peer group.

window.ntilePostgreSQL compatible
SELECT amount, NTILE(2) OVER (ORDER BY amount) AS half FROM rows
window.distributionPostgreSQL compatible
SELECT amount, PERCENT_RANK() OVER (ORDER BY amount) AS pr, CUME_DIST() OVER (ORDER BY amount) AS cd FROM rows
window.framePostgreSQL compatible
SELECT amount, SUM(amount) OVER (ORDER BY amount, joined ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS windowed FROM rows
Details

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 compatible
SELECT amount, SUM(amount) OVER (ORDER BY amount RANGE BETWEEN 1 PRECEDING AND CURRENT ROW) AS windowed FROM rows
Details

Numeric RANGE offsets measure ordering-value distance and require one numeric ORDER BY expression. Temporal interval offsets are not supported.

window.outside-selectPostgreSQL compatible
SELECT 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 compatible
SELECT r.region FROM rows r RIGHT JOIN dims d ON d.region = r.region
Details

Desugars to the mirrored LEFT JOIN; supported as the sole join of a block, and not beside SELECT *.

join.non-equiPostgreSQL compatible
SELECT r.region FROM rows r JOIN dims d ON d.amount > r.amount
Details

Executes as a nested-loop join (probe x build); equalities keep the hash path.

select.distinct-wildcardPostgreSQL compatible
SELECT DISTINCT * FROM rows
Details

Expands bare or qualified wildcards to exactly their selected source columns before DISTINCT grouping is planned.

limit.offsetPostgreSQL compatible
SELECT amount FROM rows LIMIT 5 OFFSET 2
Details

OFFSET is accepted directly after LIMIT.

select.no-fromPostgreSQL compatible
SELECT 1 + 1 AS two, UPPER('minnow') AS name
select.valuesPostgreSQL compatible
SELECT v.column1 AS n, v.column2 AS tag FROM (VALUES (1, 'one'), (2, 'two')) v
Details

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 compatible
SELECT 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 compatible
SELECT amount FROM rows ORDER BY amount OFFSET 1 ROWS FETCH FIRST 2 ROWS ONLY
Details

PostgreSQL's FETCH clause is accepted as a spelling of LIMIT; SQLite itself only speaks LIMIT.

offset.standalonePostgreSQL compatible
SELECT amount FROM rows ORDER BY amount OFFSET 2
Details

OFFSET no longer requires LIMIT. SQLite itself needs LIMIT -1 OFFSET n.

ddl.create-tablePostgreSQL compatible
CREATE 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 compatible
CREATE 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 compatible
CREATE 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 compatible
CREATE 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 difference
CREATE TRIGGER keyed_audit AFTER INSERT ON keyed BEGIN INSERT INTO rows (region, amount) VALUES (NEW.name, NEW.score); END
Details

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 difference
CREATE TRIGGER keyed_before BEFORE INSERT ON keyed BEGIN INSERT INTO rows (region, amount) VALUES (NEW.name, NEW.score); END
Details

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 difference
CREATE TRIGGER keyed_counts AFTER INSERT ON keyed BEGIN UPDATE stats SET total = total + NEW.score WHERE region = NEW.name; END
Details

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 difference
DROP TRIGGER droppable_audit
Details

PostgreSQL: PostgreSQL requires DROP TRIGGER name ON table; Minnow's trigger names are database-wide.

where.parenthesizedPostgreSQL compatible
SELECT region FROM rows WHERE (amount > 5 AND region = 'west')
where.notPostgreSQL compatible
SELECT region FROM rows WHERE NOT active = TRUE
expression.concatPostgreSQL compatible
SELECT region || '-' || label AS tag FROM dims
Details

|| 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 compatible
SELECT 7 / 2 AS quotient, -7 / 2 AS negative, 7.0 / 2 AS exact, CAST(amount AS INTEGER) / 2 AS half FROM rows
Details

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 compatible
SELECT '5' + 1 AS six, 2 * '3' AS six_again FROM rows
Details

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 difference
SELECT amount % 3 AS remainder FROM rows
Details

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 compatible
SELECT CAST(amount AS INTEGER) AS whole, CAST(amount AS TEXT) AS label FROM rows
Details

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 compatible
SELECT "region", "rows"."amount" FROM "rows" WHERE "amount" > 5
Details

Double-quoted identifiers are never keywords and keep their exact spelling.

order-by.nullsPostgreSQL compatible
SELECT region, amount FROM rows ORDER BY region NULLS LAST, amount
Details

Without NULLS FIRST/LAST the default follows PostgreSQL: NULLs last ascending, first descending.

expression.coalescePostgreSQL compatible
SELECT COALESCE(region, 'unknown') AS region_label FROM rows
Details

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 compatible
SELECT DATE_TRUNC('month', joined) AS joined_month FROM rows
Details

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 compatible
SELECT joined + INTERVAL '1 month' AS next_month, joined - INTERVAL '2 days 3 hours' AS earlier FROM rows WHERE joined IS NOT NULL
Details

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 compatible
SELECT UPPER(label) AS u, LOWER(label) AS l, LENGTH(label) AS n, SUBSTR(label, 2, 3) AS mid, TRIM(label) AS t FROM dims
Details

SUBSTRING is accepted as a spelling of SUBSTR; LENGTH and SUBSTR count characters, not UTF-16 units.

function.absPostgreSQL compatible
SELECT ABS(amount - 5) AS distance FROM rows
function.numeric-corePostgreSQL difference
SELECT 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 rows
Details

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 difference
SELECT 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 NULL
Details

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 compatible
SELECT EXTRACT(year FROM joined) AS y, EXTRACT(dow FROM joined) AS d FROM rows WHERE joined IS NOT NULL
Details

Fields: year, quarter, month, week (ISO), day, hour, minute, second, epoch, dow — all in UTC. SQLite spells this strftime.

aggregate.distinct-argumentPostgreSQL compatible
SELECT region, COUNT(DISTINCT amount) AS amounts, COUNT(DISTINCT active) AS states, SUM(amount) AS total FROM rows GROUP BY region
Details

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 compatible
SELECT r.region FROM rows r JOIN dims d ON d.region = r.region AND d.amount = r.amount
Details

Multi-key conditions execute as a nested-loop join; single equalities keep the hash path.

join.crossPostgreSQL compatible
SELECT r.region AS region, d.label AS label FROM rows r CROSS JOIN dims d
join.fullPostgreSQL compatible
SELECT r.amount AS amount, d.label AS label FROM rows r FULL JOIN dims d ON d.region = r.region
Details

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 compatible
SELECT r.region AS region, COUNT(*) AS matched FROM rows r FULL JOIN dims d ON d.region = r.region GROUP BY r.region
Details

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 compatible
SELECT region, amount FROM rows ORDER BY 2 DESC
Details

Ordinals resolve after schema-bound wildcard expansion when needed; out-of-range ordinals are an error.

window.lag-leadPostgreSQL compatible
SELECT amount, LAG(amount) OVER (ORDER BY amount) AS previous, LEAD(amount, 1, -1) OVER (ORDER BY amount) AS next FROM rows
Details

LAG/LEAD take a constant offset (default 1) and default value (default NULL), and require ORDER BY inside OVER.

mutation.insert-selectPostgreSQL compatible
INSERT INTO keyed (name, score) SELECT name || '2' AS name, score + 1 AS score FROM keyed
Details

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 compatible
MERGE 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 compatible
BEGIN
Details

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 compatible
END
Details

END and END TRANSACTION commit, as COMMIT does.

transaction.abortPostgreSQL compatible
ABORT
Details

ABORT rolls back, as ROLLBACK does.

transaction.session-settingsPostgreSQL compatible
SET search_path TO public
Details

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 compatible
SHOW server_version
Details

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 compatible
COMMIT
transaction.rollbackPostgreSQL compatible
ROLLBACK
function.char-lengthPostgreSQL compatible
SELECT CHAR_LENGTH(region) AS n FROM rows WHERE region IS NOT NULL
function.octet-lengthPostgreSQL compatible
SELECT OCTET_LENGTH(region) AS n FROM rows WHERE region IS NOT NULL
Details

Counts the UTF-8 encoding's bytes.

function.substring-from-forPostgreSQL compatible
SELECT SUBSTRING(region FROM 1 FOR 2) AS part FROM rows WHERE region IS NOT NULL
Details

The position window is intersected with the string, so a start below 1 shortens the result instead of shifting it.

function.trim-specificationPostgreSQL compatible
SELECT TRIM(LEADING 'w' FROM region) AS trimmed FROM rows WHERE region IS NOT NULL
function.trim-multi-characterPostgreSQL difference
SELECT TRIM(BOTH 'we' FROM region) AS trimmed FROM rows WHERE region IS NOT NULL
Details

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 compatible
SELECT POSITION('es' IN region) AS at FROM rows WHERE region IS NOT NULL
function.padPostgreSQL compatible
SELECT LPAD(region, 6, '-') AS padded FROM rows WHERE region IS NOT NULL
function.overlayPostgreSQL compatible
SELECT OVERLAY(region PLACING 'X' FROM 1 FOR 1) AS masked FROM rows WHERE region IS NOT NULL
select.qualified-wildcardPostgreSQL compatible
SELECT rows.* FROM rows
Details

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 compatible
SELECT rows.*, ROW_NUMBER() OVER (ORDER BY amount) AS position FROM rows ORDER BY amount
Details

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 compatible
SELECT y.a AS a FROM rows AS y(a, b, c, d)
from.parenthesized-joinPostgreSQL compatible
SELECT 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 compatible
SELECT SUM(ALL amount) AS total FROM rows
derived-table.set-operationPostgreSQL compatible
SELECT s.amount AS amount FROM (SELECT amount FROM rows UNION SELECT amount FROM dims) s
comment.simplePostgreSQL compatible
SELECT amount FROM rows -- a comment
comment.bracketedPostgreSQL compatible
SELECT /* a comment */ amount FROM rows
join.commaPostgreSQL compatible
SELECT rows.amount AS amount FROM rows, dims WHERE dims.region = rows.region
join.usingPostgreSQL compatible
SELECT 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 compatible
SELECT rows.amount AS amount FROM rows NATURAL JOIN dims
Details

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 compatible
SELECT 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 compatible
SELECT CURRENT_DATE > DATE '2000-01-01' AS elapsed
Details

Resolved once per execution, so every row of a statement sees one instant; results that read the clock never memoize.

datetime.current-timestampPostgreSQL compatible
SELECT CURRENT_TIMESTAMP > TIMESTAMP '2000-01-01 00:00:00' AS elapsed
datetime.localtimePostgreSQL compatible
SELECT LOCALTIME IS NOT NULL AS ticking
Details

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 compatible
SELECT amount FROM rows WHERE (region, amount) = ('west', 10)
predicate.row-inPostgreSQL compatible
SELECT amount FROM rows WHERE (region, amount) IN (('west', 10), ('east', 3))
predicate.row-nullPostgreSQL compatible
SELECT amount FROM rows WHERE (region, region) IS NOT NULL
literal.radixPostgreSQL compatible
SELECT 0x0A AS ten
literal.digit-separatorPostgreSQL compatible
SELECT 1_000 AS thousand
literal.exact-decimalPostgreSQL compatible
SELECT 1.000000000000000000000000 / 3 = CAST('0.333333333333333333333333' AS NUMERIC) AS exact_thirds
Details

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 compatible
SELECT 10000000000000000001 = CAST('10000000000000000001' AS NUMERIC) AS same
Details

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 difference
SELECT 2.5e-1 AS quarter
Details

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 compatible
SELECT region FROM rows WHERE region IS NOT NULL ORDER BY region DESC FETCH FIRST 1 ROWS WITH TIES
Details

The limit cannot be pushed into a scan, so these plans run unlimited and the ordered result is trimmed.

cte.in-subqueryPostgreSQL compatible
SELECT s.amount AS amount FROM (WITH inner_cte AS (SELECT amount FROM rows) SELECT amount FROM inner_cte) s
window.nth-valuePostgreSQL compatible
SELECT NTH_VALUE(amount, 2) OVER (ORDER BY amount) AS second FROM rows
window.namedPostgreSQL compatible
SELECT SUM(amount) OVER w AS running FROM rows WINDOW w AS (ORDER BY amount)
window.frame-groupsPostgreSQL compatible
SELECT COUNT(*) OVER (ORDER BY amount GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW) AS peers FROM rows
window.frame-excludePostgreSQL compatible
SELECT COUNT(*) OVER (ORDER BY amount RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING EXCLUDE CURRENT ROW) AS others FROM rows
aggregate.groupingPostgreSQL compatible
SELECT GROUPING(region) AS aggregated FROM rows GROUP BY ROLLUP(region)
Details

A bitmask over the arguments, most significant first.

aggregate.any-valuePostgreSQL compatible
SELECT ANY_VALUE(amount) AS sample FROM rows
Details

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 compatible
SELECT VAR_POP(amount) AS spread FROM rows
Details

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 compatible
SELECT STDDEV_POP(amount) AS spread FROM rows
aggregate.booleanPostgreSQL compatible
SELECT EVERY(amount > 1) AS all_positive FROM rows
json.agg-spellingsPostgreSQL compatible
SELECT region, JSON_AGG(amount ORDER BY amount) AS amounts, JSONB_AGG(amount ORDER BY amount DESC) AS amounts_desc FROM rows GROUP BY region
Details

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 compatible
SELECT JSON_BUILD_OBJECT('region', region, 'amount', amount) AS doc, JSONB_BUILD_ARRAY(amount, region) AS list FROM rows
Details

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 compatible
SELECT TO_JSON(amount) AS amount_doc, TO_JSONB(region) AS region_doc FROM rows
Details

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 compatible
SELECT 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.region
Details

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 compatible
SELECT JSON_VALUE('{"a": 1}', '$.a') AS a
Details

Accepts JSON text and stored JSON/JSONB values. Paths support $, member steps, and array subscripts.

json.queryPostgreSQL difference
SELECT JSON_QUERY('{"a": [1, 2]}', '$.a') AS a
Details

PostgreSQL: The JSON value agrees, but Minnow returns compact JSON text while PostgreSQL's text rendering includes spaces.

json.existsPostgreSQL compatible
SELECT JSON_EXISTS('{"a": 1}', '$.a') AS present
json.is-jsonPostgreSQL compatible
SELECT '{"a": 1}' IS JSON OBJECT AS shaped
json.objectPostgreSQL difference
SELECT JSON_OBJECT('a' VALUE 1, 'detail' VALUE JSON_OBJECT('name' VALUE 'Acme')) AS document
Details

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 difference
SELECT JSON_ARRAY(1, NULL, 2) AS document
Details

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 difference
SELECT CAST('{"a": {"b": [5, 6]}}' AS JSON) -> 'a' -> 'b' -> 1 AS element
Details

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 compatible
SELECT CAST('{"items": ["x", "y"]}' AS JSON) -> 'items' ->> 0 AS first_item
Details

->> 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 compatible
SELECT CAST('["x", "y", "z"]' AS JSON) ->> -1 AS last_item
Details

Negative element positions count from the end of the array; positions out of range in either direction select NULL.

json.arrow-untypedMinnow extension
SELECT '{"a": {"b": ["x", "y"]}}' -> 'a' -> 'b' ->> 1 AS second_item
Details

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 compatible
CREATE TABLE IF NOT EXISTS made (a INTEGER)
ddl.create-table-defaultPostgreSQL compatible
CREATE 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 compatible
CREATE 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 compatible
CREATE TABLE keyed_clause (a INTEGER, b TEXT, PRIMARY KEY (a))
ddl.alter-table-add-columnPostgreSQL compatible
ALTER TABLE rows ADD COLUMN note TEXT
Details

Existing rows have no value for the new column, so it is always nullable.

ddl.create-table-as-selectPostgreSQL compatible
CREATE TABLE copied AS SELECT region FROM rows
type.exact-numericPostgreSQL difference
SELECT CAST(1.25 AS DECIMAL(12, 2)) AS amount
Details

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 difference
SELECT CAST('{"a":1}' AS JSONB) AS document
Details

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 compatible
SELECT CAST('550e8400-e29b-41d4-a716-446655440000' AS UUID) AS id
Details

Validated and normalized to canonical lowercase text.

type.intervalPostgreSQL difference
SELECT joined + INTERVAL '1 day' AS next_day FROM rows
Details

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 compatible
CREATE TYPE mood AS ENUM ('sad', 'ok')
Details

Enum columns validate members and compare in declaration order. ALTER TYPE is not supported.

ddl.secondary-indexPostgreSQL compatible
CREATE 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 compatible
DROP INDEX keyed_score_idx
Details

Index names are catalog-global. IF EXISTS is also supported.

PostgreSQL: DROP INDEX follows PostgreSQL syntax.

ddl.composite-secondary-indexPostgreSQL compatible
CREATE 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 compatible
CREATE 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 compatible
CREATE 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 compatible
CREATE 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 compatible
CREATE TABLE composite_child (shop INTEGER, receipt INTEGER, FOREIGN KEY (shop, receipt) REFERENCES composite_key(shop, receipt))
ddl.alter-table-drop-columnPostgreSQL compatible
ALTER TABLE keyed DROP COLUMN bonus
Details

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 difference
INSERT INTO keyed (name, score) VALUES ('x', 2) ON CONFLICT (name) DO UPDATE SET score = score + EXCLUDED.score
Details

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 compatible
INSERT INTO keyed (name, score) VALUES ('x', 2) ON CONFLICT (name) DO UPDATE SET score = EXCLUDED.score WHERE EXCLUDED.score > keyed.score
Details

A false conflict predicate leaves the existing row unchanged.

transaction.savepointPostgreSQL compatible
SAVEPOINT line_item
Details

SAVEPOINT, ROLLBACK TO, and RELEASE operate on staged transaction state.

from.lateralPostgreSQL compatible
SELECT x.amount FROM rows r, LATERAL (SELECT amount FROM dims WHERE dims.region = r.region) x
Details

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 compatible
SELECT 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.amount
Details

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 compatible
SELECT 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.amount
Details

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 compatible
SELECT x.amount FROM rows r, LATERAL (SELECT q.amount FROM rows q WHERE q.amount < r.amount) x
Details

Non-equality correlation lowers to an ordinary general join rather than executing the derived source once per outer row.

aggregate.string-aggPostgreSQL compatible
SELECT STRING_AGG(region, ',') AS regions FROM rows
Details

DISTINCT, aggregate-local ORDER BY, and bounded spill execution are supported.

json.tablePostgreSQL compatible
SELECT j.a FROM JSON_TABLE('{"a":1}', '$' COLUMNS (a INTEGER PATH '$.a')) AS j
Details

Constant documents support $ and $[*] row paths. A document read from row data is refused (see json.table-correlated).

predicate.similar-toPostgreSQL compatible
SELECT amount FROM rows WHERE region SIMILAR TO 'w%'
Details

Whole-string SQL wildcard and regular-expression semantics.

collation.explicitPostgreSQL compatible
SELECT 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 difference
SELECT JSON_ARRAYAGG(JSON_OBJECT('region' VALUE region)) AS regions FROM rows
Details

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 difference
SELECT region, array_agg(amount ORDER BY amount) AS amounts FROM rows GROUP BY region ORDER BY region
Details

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 difference
SELECT ARRAY[1, 2] AS pair
Details

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 compatible
SELECT (ARRAY[1, 2, 3])[1] AS first_element
Details

One-based scalar subscripts return NULL for an out-of-range position. Slices and multidimensional access are not supported.

type.timePostgreSQL compatible
SELECT TIME '12:00:00' AS at
Details

A time of day without a time zone, returned as canonical text.

type.datePostgreSQL difference
SELECT CAST('2026-08-26' AS DATE) AS day
Details

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 compatible
CREATE SEQUENCE order_ids
Details

NEXTVAL and session-local CURRVAL are supported. Sequence options and ALTER SEQUENCE are not.

ddl.drop-tablePostgreSQL compatible
DROP TABLE doomed
Details

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 compatible
CREATE 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 compatible
DROP VIEW doomed_view
ddl.check-constraintPostgreSQL compatible
CREATE 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 compatible
SELECT amount::INTEGER AS whole, -amount::INTEGER * 2 AS scaled FROM rows
Details

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 compatible
SELECT 'order-' || amount || '/' || active AS tag FROM rows
Details

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 compatible
SELECT 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 compatible
SELECT 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 compatible
SELECT 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 compatible
SELECT 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 NULL
Details

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 compatible
SELECT 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 NULL
Details

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 compatible
SELECT 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 compatible
SELECT 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 NULL
Details

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 compatible
SELECT 2 ^ 10 AS kib, 2 ^ 3 ^ 2 AS left_assoc, -2 ^ 2 AS negated, 2 * 3 ^ 2 AS mixed
Details

PostgreSQL's ^ is exponentiation, binding above * and / and associating to the left: 2 ^ 3 ^ 2 is 64.

function.math-extendedPostgreSQL compatible
SELECT 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 rows
Details

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 compatible
SELECT 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 NULL
Details

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 compatible
SELECT 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 rows
Details

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 difference
SELECT 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 epoch
Details

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 difference
SELECT MAKE_DATE(2026, 1, 2) AS day, MAKE_TIMESTAMP(2026, 1, 2, 3, 4, 5.5) AS at
Details

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 difference
SELECT AGE(TIMESTAMP '2026-03-15 12:00:00', joined) AS since, AGE(joined) AS so_far FROM rows WHERE joined IS NOT NULL
Details

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 compatible
SELECT amount FROM rows WHERE DATE_TRUNC('month', joined) = TIMESTAMP '2026-01-01 00:00:00' OR EXTRACT(YEAR FROM joined) = 2026 ORDER BY amount
Details

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 compatible
SELECT 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 NULL
Details

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 compatible
SELECT x FROM unnest(ARRAY[3, 1, 2]) AS x
Details

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 compatible
SELECT UPPER(region) AS place, COUNT(*) AS c FROM rows GROUP BY 1 ORDER BY place
Details

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 compatible
SELECT COALESCE(region, 'none') AS place, COUNT(*) AS c FROM rows GROUP BY place ORDER BY place
Details

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 compatible
SELECT region, SUM(r.amount) AS total FROM rows r GROUP BY r.region ORDER BY region
Details

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 compatible
SELECT FLOOR(amount / 5) * 5 AS bucket, COUNT(*) AS c FROM rows GROUP BY FLOOR(amount / 5) ORDER BY bucket
Details

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 compatible
SELECT region, amount FROM rows AS r ORDER BY r.amount DESC
Details

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 compatible
SELECT region FROM rows ORDER BY amount LIMIT 0
Details

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 compatible
SELECT amount FROM rows ORDER BY amount LIMIT ALL OFFSET 1
Details

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 compatible
SELECT amount AS n FROM rows WHERE amount > 5 UNION SELECT amount FROM dims UNION ALL SELECT 100 ORDER BY n DESC LIMIT 4
Details

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 compatible
VALUES (2, 'two'), (1, 'one') ORDER BY 1 LIMIT 1
Details

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 compatible
SELECT NOW() > TIMESTAMP '2000-01-01 00:00:00' AS elapsed
Details

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 compatible
INSERT INTO keyed (name, score) VALUES ('x', 9), ('z', 1) ON CONFLICT DO NOTHING
Details

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 compatible
SELECT *, amount * 2 AS doubled, region IS NULL AS unplaced FROM rows ORDER BY doubled DESC
Details

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 compatible
SELECT DISTINCT active, COUNT(*) AS c FROM rows GROUP BY region, active ORDER BY active, c
Details

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 compatible
UPDATE keyed AS k SET score = k.score + 1 WHERE k.name = 'x' RETURNING k.name, k.score
Details

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 compatible
DELETE FROM keyed AS k WHERE k.score < 0 RETURNING k.name
Details

The alias resolves in the predicates and RETURNING exactly as a table alias does in SELECT.

mutation.update-subquery-assignmentPostgreSQL compatible
UPDATE 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 compatible
INSERT 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 compatible
INSERT INTO keyed (name, score) SELECT name, score + 100 FROM keyed WHERE score > 0 ON CONFLICT (name) DO UPDATE SET score = EXCLUDED.score
Details

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 compatible
INSERT INTO keyed (name, score) WITH top AS (SELECT name FROM keyed WHERE score > 0) SELECT name || '2', 2 FROM top UNION ALL SELECT 'u', 3
Details

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 compatible
CREATE 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 compatible
CREATE 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 compatible
ALTER 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 compatible
CREATE 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 compatible
SELECT SUBSTRING('abc' FROM 'a|ab') AS longest, 'A19!' ~ '^[[:alpha:]][[:digit:]]+[[:punct:]]$' AS classes
Details

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-setUnsupported
SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY amount) AS median FROM rows

Error: 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.concatUnsupported
SELECT ARRAY[1, 2] || ARRAY[3] AS combined

Error: 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-schemaUnsupported
SELECT 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-existsUnsupported
ALTER TABLE keyed ADD COLUMN IF NOT EXISTS note TEXT

Error: 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-constraintUnsupported
ALTER 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-columnUnsupported
ALTER TABLE keyed ALTER COLUMN score SET DEFAULT 7

Error: Expected ADD, found ALTER

Details

ALTER COLUMN forms are not supported; see ddl.alter-table-rename.

ddl.alter-table-renameUnsupported
ALTER TABLE keyed RENAME COLUMN score TO points

Error: 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-typeUnsupported
ALTER 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.byteaUnsupported
CREATE 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-likeUnsupported
CREATE 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-constraintsUnsupported
CREATE 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.domainUnsupported
CREATE 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-multipleUnsupported
DROP TABLE IF EXISTS keyed, rows

Error: Expected eof, found ,

Details

DROP TABLE takes one table per statement.

ddl.exclude-constraintUnsupported
CREATE TABLE slots (id INTEGER PRIMARY KEY, n INTEGER, EXCLUDE USING gist (n WITH =))

Error: Expected )

Details

EXCLUDE constraints are not supported.

ddl.expression-indexUnsupported
CREATE 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-updateUnsupported
CREATE 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-defaultUnsupported
CREATE 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.functionUnsupported
CREATE FUNCTION one() RETURNS integer AS $$ SELECT 1 $$ LANGUAGE sql

Error: 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-viewUnsupported
CREATE MATERIALIZED VIEW scores AS SELECT name, score FROM keyed

Error: Expected TABLE, found MATERIALIZED

Details

Materialized views are not supported; CREATE TABLE AS SELECT stores a snapshot.

ddl.partial-indexUnsupported
CREATE INDEX keyed_positive ON keyed (score) WHERE score > 0

Error: 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.schemaUnsupported
CREATE SCHEMA app

Error: 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-nameUnsupported
SELECT region FROM public.rows

Error: Expected eof, found .

Details

Schema-qualified names are not supported; there is one namespace.

ddl.sequence-functionsUnsupported
SELECT setval('numbering', 10) FROM rows

Error: Unsupported function: setval

Details

setval, currval, and ALTER SEQUENCE are not supported; nextval and CREATE SEQUENCE are.

ddl.serial-non-keyUnsupported
CREATE 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-listUnsupported
CREATE VIEW scored (person, points) AS SELECT name, score FROM keyed

Error: 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-zoneUnsupported
SELECT CURRENT_TIMESTAMP AT TIME ZONE 'UTC' AS local FROM rows

Error: 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-operatorsUnsupported
SELECT 1 & 3 AS both, 1 | 2 AS either, 1 # 3 AS differ, 1 << 2 AS shifted FROM rows

Error: Unsupported SQL character: &

Details

The bitwise operators &, |, #, ~, <<, and >> and the prefix operators |/ and @ are not supported.

expression.date-minus-dateUnsupported
SELECT DATE '2026-01-10' - DATE '2026-01-05' AS days FROM rows

Error: Arithmetic and numeric aggregates require numbers

Details

date - date and timestamp - timestamp are not supported; see expression.interval-values.

expression.interval-valuesUnsupported
SELECT INTERVAL '1 day' + INTERVAL '2 hours' AS total FROM rows

Error: 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-valueUnsupported
SELECT ROW(1, 2) AS pair FROM rows

Error: 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-seriesUnsupported
SELECT n FROM generate_series(1, 3) AS n

Error: Expected identifier, found 1

Details

generate_series is not supported; a recursive CTE produces a series.

function.regexp-arraysUnsupported
SELECT regexp_match(region, '(e.)') AS groups FROM rows

Error: 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-introspectionUnsupported
SELECT version() AS server FROM rows

Error: 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-multiUnsupported
SELECT r.region FROM rows r JOIN dims d ON d.region = r.region RIGHT JOIN dims e ON e.region = r.region

Error: 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-wildcardUnsupported
SELECT * FROM rows r RIGHT JOIN dims d ON d.region = r.region

Error: 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.concatUnsupported
SELECT CAST('{"a":1}' AS JSONB) || CAST('{"b":2}' AS JSONB) AS merged

Error: 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-operatorsUnsupported
SELECT 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-functionsUnsupported
SELECT jsonb_typeof('{"a":1}'::jsonb) AS kind FROM rows

Error: 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-functionsUnsupported
SELECT jsonb_set('{"a":1}'::jsonb, '{a}', '2') AS doc FROM rows

Error: 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-aggUnsupported
SELECT json_object_agg(region, amount) AS doc FROM rows

Error: 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-operatorsUnsupported
SELECT ('{"a":{"b":[1,2]}}'::jsonb) #>> '{a,b,0}' AS leaf FROM rows

Error: Unsupported SQL character: #

Details

The #> and #>> path operators are not supported; chain -> and ->> or use JSON_VALUE with a path.

json.table-correlatedUnsupported
SELECT jt.v FROM (SELECT '[1,2]' AS document) AS d, JSON_TABLE(d.document, '$[*]' COLUMNS (v INTEGER PATH '$')) AS jt

Error: 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-stringUnsupported
SELECT B'101' AS bits FROM rows

Error: Expected eof, found 101

Details

Bit-string literals and the BIT types are not supported.

mutation.data-modifying-cteUnsupported
WITH w AS (INSERT INTO keyed (name, score) VALUES ('w', 0) RETURNING name) SELECT name FROM w

Error: Expected SELECT, found INSERT

Details

A data-modifying statement inside WITH is not supported; run the write, then the read.

mutation.update-keylessUnsupported
UPDATE rows SET amount = 1

Error: 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-valueUnsupported
UPDATE 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-defaultUnsupported
UPDATE 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-uniqueUnsupported
INSERT INTO keyed (name, score) VALUES ('z', 3) ON CONFLICT (score) DO NOTHING

Error: 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-backreferencesUnsupported
SELECT '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-lookaroundUnsupported
SELECT '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-nongreedyUnsupported
SELECT '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 applicable
GRANT SELECT ON rows TO reader

Error: 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.tablesampleUnsupported
SELECT region FROM rows TABLESAMPLE SYSTEM (50)

Error: Expected eof, found SYSTEM

Details

TABLESAMPLE is not supported; ORDER BY RANDOM() LIMIT n samples rows.

statement.explainUnsupported
EXPLAIN SELECT region FROM rows

Error: Expected SELECT, found EXPLAIN

Details

EXPLAIN is not supported as SQL; the explain() API renders the plan for a query.

statement.multipleUnsupported
SELECT 1 AS a; SELECT 2 AS b

Error: 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-commandsUnsupported
VACUUM

Error: 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-constructorUnsupported
SELECT ARRAY(SELECT amount FROM rows) AS amounts FROM rows

Error: Expected )

Details

ARRAY(subquery) is not supported; a scalar subquery with json_agg builds the same list as a JSON document.

transaction.ddl-insideUnsupported
CREATE 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 applicable
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

Error: 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-tableUnsupported
LOCK TABLE keyed IN SHARE MODE

Error: Expected SELECT, found LOCK

Details

LOCK TABLE is not supported; a single-session engine has no other session to lock against.

type.array-anyUnsupported
SELECT 2 = ANY(ARRAY[1, 2, 3]) AS found

Error: 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-parameterUnsupported
SELECT 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-arithmeticUnsupported
SELECT INTERVAL '1 day' + INTERVAL '2 hours' AS total

Error: 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-aggregateUnsupported
SELECT SUM(DISTINCT amount) OVER (PARTITION BY region) AS total FROM rows

Error: 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.

On this page