@minnowdb/core 0.6.9 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,7 +25,9 @@ export type ScalarFunctionName = "ROUND" | "COALESCE" | "DATE_TRUNC" | "DATE_ADD
25
25
  /** Optimizer-only, prefix-free equality key for hashable multi-column decorrelation. */
26
26
  | "MINNOW_TUPLE_KEY"
27
27
  /** Parser-produced wrapper carrying one explicit collation through ordering/comparison. */
28
- | "MINNOW_COLLATE" | "NEXTVAL" | "CURRVAL" | "RANDOM" | "GEN_RANDOM_UUID";
28
+ | "MINNOW_COLLATE" | "NEXTVAL" | "CURRVAL" | "RANDOM" | "GEN_RANDOM_UUID"
29
+ /** Table-driven PostgreSQL string, math, datetime, regex, and formatting functions. */
30
+ | "CONCAT" | "CONCAT_WS" | "LEFT" | "RIGHT" | "REVERSE" | "REPEAT" | "INITCAP" | "SPLIT_PART" | "STRPOS" | "STARTS_WITH" | "TRANSLATE" | "ASCII" | "CHR" | "BTRIM" | "MD5" | "FORMAT" | "REGEXP_REPLACE" | "MINNOW_REGEX_MATCH" | "EXP" | "LN" | "LOG" | "LOG10" | "SIGN" | "TRUNC" | "PI" | "CBRT" | "DIV" | "WIDTH_BUCKET" | "SIN" | "COS" | "TAN" | "ASIN" | "ACOS" | "ATAN" | "ATAN2" | "DEGREES" | "RADIANS" | "TO_CHAR" | "TO_DATE" | "TO_TIMESTAMP" | "MAKE_DATE" | "MAKE_TIMESTAMP" | "AGE";
29
31
  /** Exact BM25 corpus statistics attached to a cloned scoring node before execution. */
30
32
  export interface FtsStats {
31
33
  /** Every row of the corpus, including all-null documents. */
@@ -1397,26 +1397,39 @@ export function collectFtsCandidates(chunkLists, terms, maxRowIds = MAX_FTS_CAND
1397
1397
  }
1398
1398
  const sets = terms.map(() => new Set());
1399
1399
  let retainedRowIds = 0;
1400
+ // Every chunk is strictly sorted by term (the write path refuses anything else), so each
1401
+ // query seeks to its first possible posting by binary search and walks forward only while
1402
+ // postings can still match: an exact term touches one posting, a prefix or range its run.
1400
1403
  for (const postings of chunkLists) {
1401
- for (const posting of postings) {
1402
- for (let index = 0; index < terms.length; index += 1) {
1403
- const term = terms[index];
1404
- if (term === undefined)
1405
- continue;
1406
- const matches = ftsPostingQueryMatches(posting.term, term);
1407
- if (!matches)
1408
- continue;
1409
- const set = sets[index];
1410
- if (set !== undefined) {
1411
- for (const rowId of posting.rowIds) {
1412
- if (set.has(rowId))
1413
- continue;
1414
- if (retainedRowIds === maxRowIds) {
1415
- return { rowIdsByTerm: terms.map(() => []), overflow: true };
1416
- }
1417
- set.add(rowId);
1418
- retainedRowIds += 1;
1404
+ for (let index = 0; index < terms.length; index += 1) {
1405
+ const term = terms[index];
1406
+ const set = sets[index];
1407
+ if (term === undefined || set === undefined)
1408
+ continue;
1409
+ const seek = "term" in term ? term.term : term.lower;
1410
+ let position = seek === undefined ? 0 : lowerBoundPosting(postings, seek);
1411
+ // An exclusive lower bound starts one past its own term, which the seek lands on.
1412
+ if (!("term" in term) &&
1413
+ term.lowerInclusive === false &&
1414
+ postings[position]?.term === term.lower) {
1415
+ position += 1;
1416
+ }
1417
+ for (; position < postings.length; position += 1) {
1418
+ const posting = postings[position];
1419
+ if (posting === undefined)
1420
+ break;
1421
+ if (!ftsPostingQueryMatches(posting.term, term)) {
1422
+ // Past the seek point, the first miss ends the run for every query shape.
1423
+ break;
1424
+ }
1425
+ for (const rowId of posting.rowIds) {
1426
+ if (set.has(rowId))
1427
+ continue;
1428
+ if (retainedRowIds === maxRowIds) {
1429
+ return { rowIdsByTerm: terms.map(() => []), overflow: true };
1419
1430
  }
1431
+ set.add(rowId);
1432
+ retainedRowIds += 1;
1420
1433
  }
1421
1434
  }
1422
1435
  }
@@ -1426,6 +1439,19 @@ export function collectFtsCandidates(chunkLists, terms, maxRowIds = MAX_FTS_CAND
1426
1439
  overflow: false,
1427
1440
  };
1428
1441
  }
1442
+ /** Index of the first posting whose term is not below `term` in a term-sorted chunk. */
1443
+ function lowerBoundPosting(postings, term) {
1444
+ let low = 0;
1445
+ let high = postings.length;
1446
+ while (low < high) {
1447
+ const middle = (low + high) >>> 1;
1448
+ if ((postings[middle]?.term ?? "") < term)
1449
+ low = middle + 1;
1450
+ else
1451
+ high = middle;
1452
+ }
1453
+ return low;
1454
+ }
1429
1455
  /** Validates the snapshot bound of one full-text read; -1 selects the base view alone. */
1430
1456
  export function validateFtsReadVersion(upToVersion) {
1431
1457
  if (!Number.isSafeInteger(upToVersion) || upToVersion < -1) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minnowdb/core",
3
- "version": "0.6.9",
3
+ "version": "0.7.0",
4
4
  "description": "A columnar SQL database for the browser: PostgreSQL-style SQL over durable IndexedDB or OPFS data, with no server or WebAssembly module.",
5
5
  "license": "MIT",
6
6
  "author": "Eric Wilhite",
@@ -93,11 +93,6 @@
93
93
  "classification": "different",
94
94
  "reason": "Minnow permits % on its number type; PostgreSQL has no % operator for double precision."
95
95
  },
96
- {
97
- "id": "expression.cast",
98
- "classification": "different",
99
- "reason": "Casting an approximate number to INTEGER truncates in Minnow; PostgreSQL rounds."
100
- },
101
96
  {
102
97
  "id": "expression.date-trunc",
103
98
  "classification": "compatible",
@@ -159,6 +154,21 @@
159
154
  "classification": "different",
160
155
  "reason": "Minnow returns arrays as canonical JSON text at the JavaScript boundary while PostgreSQL clients commonly return native arrays."
161
156
  },
157
+ {
158
+ "id": "function.to-date-timestamp",
159
+ "classification": "different",
160
+ "reason": "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."
161
+ },
162
+ {
163
+ "id": "function.make-date",
164
+ "classification": "different",
165
+ "reason": "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."
166
+ },
167
+ {
168
+ "id": "function.age",
169
+ "classification": "different",
170
+ "reason": "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."
171
+ },
162
172
  {
163
173
  "id": "type.date",
164
174
  "classification": "different",
@@ -275,6 +275,12 @@
275
275
  "example": "DELETE FROM keyed WHERE name = 'x' RETURNING keyed.name, keyed.score",
276
276
  "notes": "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."
277
277
  },
278
+ {
279
+ "id": "mutation.returning-expression",
280
+ "status": "supported",
281
+ "example": "UPDATE keyed SET score = score + 1 WHERE name = 'x' RETURNING name, score * 2 AS doubled, UPPER(name) AS label",
282
+ "notes": "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."
283
+ },
278
284
  {
279
285
  "id": "mutation.upsert",
280
286
  "status": "supported",
@@ -727,7 +733,7 @@
727
733
  "id": "expression.cast",
728
734
  "status": "supported",
729
735
  "example": "SELECT CAST(amount AS INTEGER) AS whole, CAST(amount AS TEXT) AS label FROM rows",
730
- "notes": "Common PostgreSQL type names map to the four stored value kinds; integer targets truncate toward zero, unlike PostgreSQL's rounding, and non-numeric strings fail rather than becoming 0."
736
+ "notes": "Common PostgreSQL type names map to the four stored value kinds; integer targets round to the nearest integer with ties to even, PostgreSQL's float8 cast (SQLite truncates), and non-numeric strings fail rather than becoming 0. The postfix spelling amount::INTEGER is the same cast."
731
737
  },
732
738
  {
733
739
  "id": "identifier.quoted",
@@ -1291,7 +1297,19 @@
1291
1297
  "id": "from.lateral",
1292
1298
  "status": "supported",
1293
1299
  "example": "SELECT x.amount FROM rows r, LATERAL (SELECT amount FROM dims WHERE dims.region = r.region) x",
1294
- "notes": "Equality and range-correlated derived sources become set-at-a-time joins. Correlated grouping, ordering, and LIMIT remain unsupported."
1300
+ "notes": "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."
1301
+ },
1302
+ {
1303
+ "id": "from.lateral-aggregate",
1304
+ "status": "supported",
1305
+ "example": "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",
1306
+ "notes": "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."
1307
+ },
1308
+ {
1309
+ "id": "from.lateral-limit",
1310
+ "status": "supported",
1311
+ "example": "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",
1312
+ "notes": "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."
1295
1313
  },
1296
1314
  {
1297
1315
  "id": "from.lateral-non-equi",
@@ -1413,6 +1431,238 @@
1413
1431
  "example": "CREATE TABLE checked (a INTEGER NOT NULL CHECK (a > 0), CONSTRAINT small CHECK (a < 100))",
1414
1432
  "notes": "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."
1415
1433
  },
1434
+ {
1435
+ "id": "expression.cast-postfix",
1436
+ "status": "supported",
1437
+ "example": "SELECT amount::INTEGER AS whole, -amount::INTEGER * 2 AS scaled FROM rows",
1438
+ "notes": "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."
1439
+ },
1440
+ {
1441
+ "id": "expression.concat-typed",
1442
+ "status": "supported",
1443
+ "example": "SELECT 'order-' || amount || '/' || active AS tag FROM rows",
1444
+ "notes": "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."
1445
+ },
1446
+ {
1447
+ "id": "where.datetime-text",
1448
+ "status": "supported",
1449
+ "example": "SELECT region FROM rows WHERE joined >= '2026-01-01' AND joined < '2026-02-01 00:00:00'",
1450
+ "notes": "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."
1451
+ },
1452
+ {
1453
+ "id": "where.number-text",
1454
+ "status": "supported",
1455
+ "example": "SELECT region FROM rows WHERE amount = '10' OR amount IN ('3', '6')",
1456
+ "notes": "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."
1457
+ },
1458
+ {
1459
+ "id": "where.boolean-text",
1460
+ "status": "supported",
1461
+ "example": "SELECT region FROM rows WHERE active = 't' AND active <> 'false'",
1462
+ "notes": "PostgreSQL's boolean input spellings t, true, 1, f, false, and 0 read as booleans beside a boolean column."
1463
+ },
1464
+ {
1465
+ "id": "function.string-postgres",
1466
+ "status": "supported",
1467
+ "example": "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",
1468
+ "notes": "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."
1469
+ },
1470
+ {
1471
+ "id": "function.md5-format",
1472
+ "status": "supported",
1473
+ "example": "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",
1474
+ "notes": "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."
1475
+ },
1476
+ {
1477
+ "id": "predicate.regex",
1478
+ "status": "supported",
1479
+ "example": "SELECT region FROM rows WHERE region ~ '^w' AND region !~* 'EAST$' AND region ~* 'W.ST'",
1480
+ "notes": "PostgreSQL's ~, ~*, !~, and !~* run as JavaScript regular expressions, which agree with advanced regular expressions on everyday syntax. They sit at PostgreSQL's operator level, so `label ~ 'a' || 'b'` matches against the concatenation."
1481
+ },
1482
+ {
1483
+ "id": "function.regexp-replace",
1484
+ "status": "supported",
1485
+ "example": "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",
1486
+ "notes": "Flags g (every match), i (case-insensitive), and n (newline-sensitive); \\1 back-references and \\& in the replacement follow PostgreSQL."
1487
+ },
1488
+ {
1489
+ "id": "expression.power-operator",
1490
+ "status": "supported",
1491
+ "example": "SELECT 2 ^ 10 AS kib, 2 ^ 3 ^ 2 AS left_assoc, -2 ^ 2 AS negated, 2 * 3 ^ 2 AS mixed",
1492
+ "notes": "PostgreSQL's ^ is exponentiation, binding above * and / and associating to the left: 2 ^ 3 ^ 2 is 64."
1493
+ },
1494
+ {
1495
+ "id": "function.math-extended",
1496
+ "status": "supported",
1497
+ "example": "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",
1498
+ "notes": "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."
1499
+ },
1500
+ {
1501
+ "id": "function.to-char-datetime",
1502
+ "status": "supported",
1503
+ "example": "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",
1504
+ "notes": "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."
1505
+ },
1506
+ {
1507
+ "id": "function.to-char-numeric",
1508
+ "status": "supported",
1509
+ "example": "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",
1510
+ "notes": "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."
1511
+ },
1512
+ {
1513
+ "id": "function.to-date-timestamp",
1514
+ "status": "supported",
1515
+ "example": "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",
1516
+ "notes": "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."
1517
+ },
1518
+ {
1519
+ "id": "function.make-date",
1520
+ "status": "supported",
1521
+ "example": "SELECT MAKE_DATE(2026, 1, 2) AS day, MAKE_TIMESTAMP(2026, 1, 2, 3, 4, 5.5) AS at",
1522
+ "notes": "Fields that do not form a real date or timestamp are an error."
1523
+ },
1524
+ {
1525
+ "id": "function.age",
1526
+ "status": "supported",
1527
+ "example": "SELECT AGE(TIMESTAMP '2026-03-15 12:00:00', joined) AS since, AGE(joined) AS so_far FROM rows WHERE joined IS NOT NULL",
1528
+ "notes": "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."
1529
+ },
1530
+ {
1531
+ "id": "where.calendar-equality",
1532
+ "status": "supported",
1533
+ "example": "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",
1534
+ "notes": "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."
1535
+ },
1536
+ {
1537
+ "id": "function.date-part",
1538
+ "status": "supported",
1539
+ "example": "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",
1540
+ "notes": "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."
1541
+ },
1542
+ {
1543
+ "id": "group-by.ordinal",
1544
+ "status": "supported",
1545
+ "example": "SELECT UPPER(region) AS place, COUNT(*) AS c FROM rows GROUP BY 1 ORDER BY place",
1546
+ "notes": "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."
1547
+ },
1548
+ {
1549
+ "id": "group-by.alias",
1550
+ "status": "supported",
1551
+ "example": "SELECT COALESCE(region, 'none') AS place, COUNT(*) AS c FROM rows GROUP BY place ORDER BY place",
1552
+ "notes": "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."
1553
+ },
1554
+ {
1555
+ "id": "group-by.expression-over-key",
1556
+ "status": "supported",
1557
+ "example": "SELECT FLOOR(amount / 5) * 5 AS bucket, COUNT(*) AS c FROM rows GROUP BY FLOOR(amount / 5) ORDER BY bucket",
1558
+ "notes": "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."
1559
+ },
1560
+ {
1561
+ "id": "order-by.qualified-selected-column",
1562
+ "status": "supported",
1563
+ "example": "SELECT region, amount FROM rows AS r ORDER BY r.amount DESC",
1564
+ "notes": "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."
1565
+ },
1566
+ {
1567
+ "id": "limit.zero",
1568
+ "status": "supported",
1569
+ "example": "SELECT region FROM rows ORDER BY amount LIMIT 0",
1570
+ "notes": "LIMIT 0 returns no rows and still reports the result's column shape, the way clients read a query's columns without fetching data."
1571
+ },
1572
+ {
1573
+ "id": "set.trailing-order",
1574
+ "status": "supported",
1575
+ "example": "SELECT amount AS n FROM rows WHERE amount > 5 UNION SELECT amount FROM dims UNION ALL SELECT 100 ORDER BY n DESC LIMIT 4",
1576
+ "notes": "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."
1577
+ },
1578
+ {
1579
+ "id": "select.values-ordered",
1580
+ "status": "supported",
1581
+ "example": "VALUES (2, 'two'), (1, 'one') ORDER BY 1 LIMIT 1",
1582
+ "notes": "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."
1583
+ },
1584
+ {
1585
+ "id": "datetime.now",
1586
+ "status": "supported",
1587
+ "example": "SELECT NOW() > TIMESTAMP '2000-01-01 00:00:00' AS elapsed",
1588
+ "notes": "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."
1589
+ },
1590
+ {
1591
+ "id": "mutation.insert-do-nothing-any-key",
1592
+ "status": "supported",
1593
+ "example": "INSERT INTO keyed (name, score) VALUES ('x', 9), ('z', 1) ON CONFLICT DO NOTHING",
1594
+ "notes": "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."
1595
+ },
1596
+ {
1597
+ "id": "select.wildcard-with-expressions",
1598
+ "status": "supported",
1599
+ "example": "SELECT *, amount * 2 AS doubled, region IS NULL AS unplaced FROM rows ORDER BY doubled DESC",
1600
+ "notes": "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."
1601
+ },
1602
+ {
1603
+ "id": "select.distinct-grouped",
1604
+ "status": "supported",
1605
+ "example": "SELECT DISTINCT active, COUNT(*) AS c FROM rows GROUP BY region, active ORDER BY active, c",
1606
+ "notes": "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."
1607
+ },
1608
+ {
1609
+ "id": "mutation.update-alias",
1610
+ "status": "supported",
1611
+ "example": "UPDATE keyed AS k SET score = k.score + 1 WHERE k.name = 'x' RETURNING k.name, k.score",
1612
+ "notes": "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."
1613
+ },
1614
+ {
1615
+ "id": "mutation.delete-alias",
1616
+ "status": "supported",
1617
+ "example": "DELETE FROM keyed AS k WHERE k.score < 0 RETURNING k.name",
1618
+ "notes": "The alias resolves in the predicates and RETURNING exactly as a table alias does in SELECT."
1619
+ },
1620
+ {
1621
+ "id": "mutation.update-subquery-assignment",
1622
+ "status": "supported",
1623
+ "example": "UPDATE keyed SET bonus = (SELECT MAX(score) FROM keyed) + (SELECT COUNT(*) FROM keyed k WHERE k.score > keyed.score) WHERE name = 'x'",
1624
+ "notes": "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."
1625
+ },
1626
+ {
1627
+ "id": "mutation.insert-subquery-value",
1628
+ "status": "supported",
1629
+ "example": "INSERT INTO keyed (name, score) VALUES ('q', (SELECT MAX(score) + 1 FROM keyed))",
1630
+ "notes": "A scalar subquery in VALUES is evaluated when the statement runs, at its snapshot, like the statement clock and sequence calls."
1631
+ },
1632
+ {
1633
+ "id": "mutation.insert-select-on-conflict",
1634
+ "status": "supported",
1635
+ "example": "INSERT INTO keyed (name, score) SELECT name, score + 100 FROM keyed WHERE score > 0 ON CONFLICT (name) DO UPDATE SET score = EXCLUDED.score",
1636
+ "notes": "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."
1637
+ },
1638
+ {
1639
+ "id": "mutation.insert-select-compound",
1640
+ "status": "supported",
1641
+ "example": "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",
1642
+ "notes": "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."
1643
+ },
1644
+ {
1645
+ "id": "ddl.serial",
1646
+ "status": "supported",
1647
+ "example": "CREATE TABLE ticketed (id SERIAL PRIMARY KEY, label TEXT)",
1648
+ "notes": "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."
1649
+ },
1650
+ {
1651
+ "id": "ddl.identity",
1652
+ "status": "supported",
1653
+ "example": "CREATE TABLE identified (id INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, label TEXT)",
1654
+ "notes": "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."
1655
+ },
1656
+ {
1657
+ "id": "ddl.alter-table-add-column-default",
1658
+ "status": "supported",
1659
+ "setup": [
1660
+ "CREATE TABLE filled (id INTEGER PRIMARY KEY)",
1661
+ "INSERT INTO filled VALUES (1), (2)"
1662
+ ],
1663
+ "example": "ALTER TABLE filled ADD COLUMN tier TEXT NOT NULL DEFAULT 'basic'",
1664
+ "notes": "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."
1665
+ },
1416
1666
  {
1417
1667
  "id": "ddl.foreign-key",
1418
1668
  "status": "supported",