@minnowdb/core 0.6.4 → 0.6.6

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.
Files changed (43) hide show
  1. package/dist/engine/artifact-cache.d.ts +2 -1
  2. package/dist/engine/batch.d.ts +0 -1
  3. package/dist/engine/batch.js +1 -1
  4. package/dist/engine/buffered-writer.js +1 -12
  5. package/dist/engine/byte-estimates.d.ts +11 -0
  6. package/dist/engine/byte-estimates.js +25 -0
  7. package/dist/engine/database.js +78 -38
  8. package/dist/engine/fts.d.ts +0 -1
  9. package/dist/engine/fts.js +1 -1
  10. package/dist/engine/join-index.d.ts +0 -1
  11. package/dist/engine/optimizer.js +11 -3
  12. package/dist/engine/point-read.d.ts +1 -1
  13. package/dist/engine/point-read.js +3 -2
  14. package/dist/engine/query-cache.js +1 -12
  15. package/dist/engine/query.d.ts +20 -59
  16. package/dist/engine/query.js +374 -71
  17. package/dist/engine/result-wire.d.ts +2 -1
  18. package/dist/engine/schema.d.ts +18 -2
  19. package/dist/engine/schema.js +11 -2
  20. package/dist/engine/sort-keys.d.ts +2 -3
  21. package/dist/engine/sort-keys.js +1 -1
  22. package/dist/engine/sql-domains.d.ts +36 -2
  23. package/dist/engine/sql-domains.js +143 -6
  24. package/dist/engine/sql-json.d.ts +12 -2
  25. package/dist/engine/sql-json.js +41 -1
  26. package/dist/engine/sql-semantics.d.ts +2 -1
  27. package/dist/engine/vector.d.ts +7 -7
  28. package/dist/engine/vector.js +8 -4
  29. package/dist/engine/write-block-planner.d.ts +2 -1
  30. package/dist/plan/model.d.ts +19 -0
  31. package/dist/storage/opfs/leader.d.ts +0 -4
  32. package/dist/storage/opfs/leader.js +2 -2
  33. package/dist/storage/opfs/snapshot-ledger.d.ts +3 -2
  34. package/dist/storage/toolkit/index.d.ts +1 -1
  35. package/dist/storage/toolkit/record-core.d.ts +0 -1
  36. package/dist/storage/toolkit/record-core.js +0 -7
  37. package/dist/testing/opfs-shim.d.ts +2 -1
  38. package/dist/testing/simulator.js +3 -0
  39. package/dist/worker-protocol/index.d.ts +1 -1
  40. package/dist/worker-protocol/index.js +0 -1
  41. package/package.json +2 -1
  42. package/postgres-feature-profile.json +18 -3
  43. package/sql-feature-matrix.json +95 -11
@@ -6,10 +6,10 @@ import { cachedQueryTerms, ftsBm25Row, FtsStatsAccumulator, ftsMatchTruth, rende
6
6
  import { QueryMemoryContext } from "./memory.js";
7
7
  import { buildSortKeyColumn, sortKeyIndexes } from "./sort-keys.js";
8
8
  import { stringArgument } from "./sql-semantics.js";
9
- import { jsonAtPath, jsonConstructor, jsonIsValid, parseJsonPath } from "./sql-json.js";
9
+ import { jsonArrowStep, jsonAtPath, jsonConstructor, jsonIsValid, parseJsonPath, } from "./sql-json.js";
10
10
  import { optimizePlan } from "./optimizer.js";
11
11
  import { compareSqlValues as compareValues, compileLikePattern, compileSimilarPattern, encodeSqlEqualityValue, roundSqlNumber, } from "./sql-semantics.js";
12
- import { arrayDomainValue, boundedJsonText, collatedDomainValue, dateDomainValue, exactNumericBinary, exactNumericValue, externalSqlDomainValue, intervalDomainValue, isDateDomainValue, isExactNumeric, isSqlDomainValue, jsonDomainValue, normalizeSqlDomainValue, preservedJsonDomainValue, protectedSqlTextValue, timeDomainValue, uuidDomainValue, } from "./sql-domains.js";
12
+ import { arrayDomainValue, boundedJsonText, collatedDomainValue, concatenatedSqlValue, dateDomainValue, exactNumericAsNumber, exactNumericBinary, exactNumericLiteral, exactNumericValue, externalSqlDomainColumnValue, externalSqlDomainValue, intervalDomainValue, isDateDomainValue, isExactNumeric, isSqlDomainValue, jsonDomainValue, normalizeSqlDomainValue, preservedJsonDomainValue, protectedSqlTextValue, timeDomainValue, uuidDomainValue, } from "./sql-domains.js";
13
13
  import { columnarTableFromRows, prepareVectorQuery, } from "./vector.js";
14
14
  /** Domain metadata for an execution path that has no catalog-backed type information. */
15
15
  export function unknownColumnDomains(columns) {
@@ -62,6 +62,8 @@ export const scalarFunctionNames = new Set([
62
62
  "JSON_ARRAY",
63
63
  "IS_JSON",
64
64
  "ARRAY",
65
+ "MINNOW_JSON_GET",
66
+ "MINNOW_JSON_GET_TEXT",
65
67
  "MINNOW_TUPLE_KEY",
66
68
  "MINNOW_COLLATE",
67
69
  "NEXTVAL",
@@ -79,7 +81,7 @@ export const volatileScalarFunctionNames = new Set([
79
81
  * reading, so they are resolved once per execution rather than evaluated per row: every row of
80
82
  * one statement sees one instant, both executors agree, and constant folding leaves them alone.
81
83
  */
82
- export const statementDatetimeNames = new Set([
84
+ const statementDatetimeNames = new Set([
83
85
  "CURRENT_DATE",
84
86
  "CURRENT_TIMESTAMP",
85
87
  "LOCALTIME",
@@ -167,16 +169,20 @@ function castValue(value, target) {
167
169
  return protectedSqlTextValue(dateIsoString(value));
168
170
  }
169
171
  if (target === "number" || target === "number-integer") {
172
+ // Externalize first, exactly as the string and datetime targets do: a NUMERIC (or other
173
+ // domain) value is an internally tagged string, and CAST(numeric_column AS DOUBLE
174
+ // PRECISION) must read its decimal text, not fail on the tag (T703).
175
+ const external = externalSqlDomainValue(value);
170
176
  let parsed;
171
- if (typeof value === "number")
172
- parsed = value;
173
- else if (typeof value === "boolean")
174
- parsed = value ? 1 : 0;
175
- else if (typeof value === "string") {
176
- const text = value.trim();
177
+ if (typeof external === "number")
178
+ parsed = external;
179
+ else if (typeof external === "boolean")
180
+ parsed = external ? 1 : 0;
181
+ else if (typeof external === "string") {
182
+ const text = external.trim();
177
183
  const candidate = text === "" ? Number.NaN : Number(text);
178
184
  if (!Number.isFinite(candidate)) {
179
- throw new TypeError(`Cannot cast this string to a number: ${value}`);
185
+ throw new TypeError(`Cannot cast this string to a number: ${text}`);
180
186
  }
181
187
  parsed = candidate;
182
188
  }
@@ -201,12 +207,13 @@ function castValue(value, target) {
201
207
  throw new TypeError(`Only 0 and 1 cast to boolean, got ${String(value)}`);
202
208
  }
203
209
  if (typeof value === "string") {
204
- const text = value.trim().toLowerCase();
210
+ const external = externalSqlDomainValue(value);
211
+ const text = typeof external === "string" ? external.trim().toLowerCase() : "";
205
212
  if (text === "true" || text === "t" || text === "1")
206
213
  return true;
207
214
  if (text === "false" || text === "f" || text === "0")
208
215
  return false;
209
- throw new TypeError(`Cannot cast this string to a boolean: ${value}`);
216
+ throw new TypeError(`Cannot cast this string to a boolean: ${typeof external === "string" ? external : value}`);
210
217
  }
211
218
  }
212
219
  if (target === "datetime") {
@@ -396,6 +403,27 @@ export function scalarFunctionValue(name, values) {
396
403
  // JSON_QUERY returns JSON text, so a selected string keeps its quotes.
397
404
  return preservedJsonDomainValue(JSON.stringify(found.value));
398
405
  }
406
+ case "MINNOW_JSON_GET": {
407
+ if (values[1] === null || values[1] === undefined)
408
+ return null;
409
+ const found = jsonArrowStep(first, values[1], "->");
410
+ if (!found.found)
411
+ return null;
412
+ // -> returns a JSON value: a selected string keeps its quotes, a JSON null is the
413
+ // one-character document "null" rather than SQL NULL, exactly as PostgreSQL has it.
414
+ return preservedJsonDomainValue(JSON.stringify(found.value));
415
+ }
416
+ case "MINNOW_JSON_GET_TEXT": {
417
+ if (values[1] === null || values[1] === undefined)
418
+ return null;
419
+ const found = jsonArrowStep(first, values[1], "->>");
420
+ // ->> returns text: strings unquoted, other scalars as their JSON rendering, objects
421
+ // and arrays serialized, and a JSON null as SQL NULL.
422
+ if (!found.found || found.value === null || found.value === undefined)
423
+ return null;
424
+ const value = found.value;
425
+ return protectedSqlTextValue(typeof value === "string" ? value : JSON.stringify(value));
426
+ }
399
427
  case "LPAD":
400
428
  case "RPAD": {
401
429
  if (values[1] === null || values[1] === undefined)
@@ -579,7 +607,7 @@ function assertReplacementResultLength(source, search, replacement) {
579
607
  throw new RangeError(`REPLACE result exceeds ${String(MAX_SQL_SCALAR_RESULT_CHARACTERS)} characters`);
580
608
  }
581
609
  }
582
- export const dateTruncUnits = new Set([
610
+ const dateTruncUnits = new Set([
583
611
  "year",
584
612
  "quarter",
585
613
  "month",
@@ -609,7 +637,7 @@ const intervalUnits = new Map([
609
637
  * rather than converted, because a month is not a fixed number of them: adding one to January 31
610
638
  * has to land on the end of February, which only calendar arithmetic can do.
611
639
  */
612
- export function intervalLiteral(text) {
640
+ function intervalLiteral(text) {
613
641
  let months = 0;
614
642
  let milliseconds = 0;
615
643
  let matched = 0;
@@ -635,7 +663,7 @@ export function intervalLiteral(text) {
635
663
  * does not exist in the target month clamps to that month's last day, which is what both SQLite
636
664
  * and PostgreSQL do with 31 January plus a month.
637
665
  */
638
- export function dateAddValue(value, months, milliseconds) {
666
+ function dateAddValue(value, months, milliseconds) {
639
667
  if (value === null || value === undefined)
640
668
  return null;
641
669
  const calendarDate = isDateDomainValue(value);
@@ -662,7 +690,7 @@ export function dateAddValue(value, months, milliseconds) {
662
690
  ? dateDomainValue(dateIsoString(result).slice(0, 10))
663
691
  : result;
664
692
  }
665
- export function dateTruncValue(unit, value) {
693
+ function dateTruncValue(unit, value) {
666
694
  if (typeof unit !== "string" || !dateTruncUnits.has(unit.toLowerCase())) {
667
695
  throw new TypeError("DATE_TRUNC requires a unit of year, quarter, month, week, day, hour, minute, or second");
668
696
  }
@@ -860,6 +888,7 @@ export function compileQuery(sql, options = {}) {
860
888
  }
861
889
  let compiled;
862
890
  try {
891
+ resolvePlanExactNumericConstants(plan);
863
892
  compiled = options.optimize === false ? plan : optimizePlan(plan);
864
893
  }
865
894
  catch (error) {
@@ -967,7 +996,7 @@ export function compileCheckExpression(sql, name) {
967
996
  if (hasAggregate(expression) || containsWindow(expression) || containsParameter(expression)) {
968
997
  throw new TypeError(`CHECK ${name} takes a row condition over this table's own columns`);
969
998
  }
970
- return expression;
999
+ return resolveExactNumericConstants(expression);
971
1000
  }
972
1001
  /**
973
1002
  * Parses and type-checks one catalog default. PostgreSQL defaults are variable-free scalar
@@ -1314,6 +1343,22 @@ export function evaluateJoinedRowExpression(expression, rows) {
1314
1343
  export function evaluateRowExpression(expression, alias, row) {
1315
1344
  return asQueryValue(evaluate(expression, { [alias]: row }));
1316
1345
  }
1346
+ /**
1347
+ * Wraps one value from an executed subquery block as a substituted literal. Block results are
1348
+ * internal: domain values keep their tags and protected text keeps its wrapper. The literal must
1349
+ * say so — an unmarked string literal is re-protected as user text at evaluation, which both
1350
+ * leaks the internal tag into the outer result and makes equality against internal column
1351
+ * values never match (T694: `WHERE amount = (SELECT amount ...)` on a NUMERIC column returned
1352
+ * no rows). Carrying the block's column domain also keeps the outer result's domain metadata.
1353
+ */
1354
+ function substitutedResultLiteral(value, sqlDomain) {
1355
+ return {
1356
+ kind: "literal",
1357
+ value: value ?? null,
1358
+ ...(typeof value === "string" ? { internalSqlValue: true } : {}),
1359
+ ...(sqlDomain === null || sqlDomain === undefined ? {} : { sqlDomain }),
1360
+ };
1361
+ }
1317
1362
  /**
1318
1363
  * Clones a plan and returns its subquery sites in post-order: executing each step's block and
1319
1364
  * substituting its result leaves the returned plan free of subquery nodes. A scalar subquery must
@@ -1338,10 +1383,7 @@ export function subqueryResolutionSteps(plan) {
1338
1383
  if (result.rows.length > 1) {
1339
1384
  throw new TypeError(`A scalar subquery returned ${String(result.rows.length)} rows`);
1340
1385
  }
1341
- replace({
1342
- kind: "literal",
1343
- value: result.rows[0]?.[result.columns[0] ?? ""] ?? null,
1344
- });
1386
+ replace(substitutedResultLiteral(result.rows[0]?.[result.columns[0] ?? ""], result.columnDomains[0]));
1345
1387
  },
1346
1388
  });
1347
1389
  return;
@@ -1412,10 +1454,7 @@ export function subqueryResolutionSteps(plan) {
1412
1454
  }
1413
1455
  expression.right = {
1414
1456
  kind: "list",
1415
- items: result.rows.map((row) => ({
1416
- kind: "literal",
1417
- value: row[result.columns[0] ?? ""] ?? null,
1418
- })),
1457
+ items: result.rows.map((row) => substitutedResultLiteral(row[result.columns[0] ?? ""], result.columnDomains[0])),
1419
1458
  };
1420
1459
  },
1421
1460
  });
@@ -1458,10 +1497,7 @@ export function subqueryResolutionSteps(plan) {
1458
1497
  }
1459
1498
  predicate.right = {
1460
1499
  kind: "list",
1461
- items: result.rows.map((row) => ({
1462
- kind: "literal",
1463
- value: row[result.columns[0] ?? ""] ?? null,
1464
- })),
1500
+ items: result.rows.map((row) => substitutedResultLiteral(row[result.columns[0] ?? ""], result.columnDomains[0])),
1465
1501
  };
1466
1502
  },
1467
1503
  });
@@ -1494,7 +1530,7 @@ export function blockHasSubqueries(plan) {
1494
1530
  return blockHas(plan);
1495
1531
  }
1496
1532
  /** True when any `?`/`$n` placeholder remains anywhere in the expression tree. */
1497
- export function containsParameter(expression) {
1533
+ function containsParameter(expression) {
1498
1534
  if (expression.kind === "parameter")
1499
1535
  return true;
1500
1536
  if (expression.kind === "subquery" || expression.kind === "exists") {
@@ -1502,7 +1538,7 @@ export function containsParameter(expression) {
1502
1538
  }
1503
1539
  return childExpressions(expression).some(containsParameter);
1504
1540
  }
1505
- export function blockHasParameters(block) {
1541
+ function blockHasParameters(block) {
1506
1542
  if (block.limitParameter !== undefined ||
1507
1543
  block.offsetParameter !== undefined ||
1508
1544
  (block.limitValidationParameters?.length ?? 0) > 0 ||
@@ -1886,7 +1922,8 @@ export function inferBlockSchema(plan, schemas) {
1886
1922
  if (expression.name === "JSON_ARRAYAGG" ||
1887
1923
  expression.name === "JSON_QUERY" ||
1888
1924
  expression.name === "JSON_OBJECT" ||
1889
- expression.name === "JSON_ARRAY") {
1925
+ expression.name === "JSON_ARRAY" ||
1926
+ expression.name === "MINNOW_JSON_GET") {
1890
1927
  return { kind: "json" };
1891
1928
  }
1892
1929
  if (expression.name === "GEN_RANDOM_UUID")
@@ -2025,6 +2062,8 @@ export function inferBlockSchema(plan, schemas) {
2025
2062
  expression.name === "JSON_OBJECT" ||
2026
2063
  expression.name === "JSON_ARRAY" ||
2027
2064
  expression.name === "ARRAY" ||
2065
+ expression.name === "MINNOW_JSON_GET" ||
2066
+ expression.name === "MINNOW_JSON_GET_TEXT" ||
2028
2067
  expression.name === "MINNOW_TUPLE_KEY" ||
2029
2068
  expression.name === "MINNOW_COLLATE" ||
2030
2069
  expression.name === "GEN_RANDOM_UUID") {
@@ -2953,12 +2992,188 @@ export function expandFtsColumns(plan, searchableColumnsFor) {
2953
2992
  expandBlock(plan);
2954
2993
  return plan;
2955
2994
  }
2995
+ /**
2996
+ * Annotates every `AVG(column)` in this block's expression positions with the argument column's
2997
+ * declared NUMERIC scale, resolved against the given schemas. PostgreSQL floors an AVG's
2998
+ * internal division scale at the summed values' display scale; the canonical NUMERIC encoding
2999
+ * strips trailing fractional zeros, so without the annotation the divide cannot know the digits
3000
+ * a declared scale above its own selection would render, and the display padding would fabricate
3001
+ * zeros where PostgreSQL computes real digits. Runs where the catalog is known, per block —
3002
+ * nested blocks execute through their own schema-aware entry. Copy-on-write: plans without an
3003
+ * AVG pass through untouched, and the input is often the compile cache's own object.
3004
+ */
3005
+ export function annotateAvgArgumentScales(plan, schemas) {
3006
+ const containsAvg = (expression) => (expression.kind === "call" && expression.name === "AVG") ||
3007
+ childExpressions(expression).some(containsAvg);
3008
+ const roots = [];
3009
+ forEachBlockExpression(plan, (expression) => roots.push(expression));
3010
+ if (!roots.some(containsAvg))
3011
+ return plan;
3012
+ plan = clonePlanTree(plan);
3013
+ const sources = [plan.base, ...plan.joins];
3014
+ const declaredScale = (reference) => {
3015
+ const parts = reference.split(".");
3016
+ const matches = parts.length === 2
3017
+ ? sources
3018
+ .filter(({ alias }) => alias === parts[0])
3019
+ .flatMap((source) => (schemas.get(source.table) ?? []).filter(({ name }) => name === parts[1]))
3020
+ : sources.flatMap((source) => (schemas.get(source.table) ?? []).filter(({ name }) => name === parts[0]));
3021
+ const domain = matches.length === 1 ? matches[0]?.sqlDomain : undefined;
3022
+ return domain?.kind === "numeric" ? domain.scale : undefined;
3023
+ };
3024
+ const annotate = (expression) => {
3025
+ if (expression.kind === "call" && expression.name === "AVG") {
3026
+ const argument = expression.arguments[0];
3027
+ if (argument?.kind === "column") {
3028
+ const scale = declaredScale(argument.reference);
3029
+ if (scale !== undefined && scale > 0)
3030
+ expression.avgArgumentScale = scale;
3031
+ }
3032
+ }
3033
+ for (const child of childExpressions(expression))
3034
+ annotate(child);
3035
+ };
3036
+ forEachBlockExpression(plan, annotate);
3037
+ return plan;
3038
+ }
3039
+ /**
3040
+ * The exact fold of a constant arithmetic subtree, or undefined where none applies. PostgreSQL
3041
+ * types every decimal constant — and every integer constant too large for its integer types —
3042
+ * as NUMERIC, so arithmetic among constants happens in exact decimal space before any float8
3043
+ * context sees the result: `0.1 + 0.2` is exactly 0.3. A subtree folds only when it is
3044
+ * "seeded" by such a constant (one carrying exact digits); pure safe-integer arithmetic keeps
3045
+ * its historical Float64 evaluation, including non-truncating division.
3046
+ */
3047
+ function exactConstantFold(expression) {
3048
+ if (expression.kind === "literal") {
3049
+ if (expression.exactText !== undefined) {
3050
+ return { value: exactNumericLiteral(expression.exactText), seeded: true };
3051
+ }
3052
+ const value = expression.value;
3053
+ if (typeof value === "string" && expression.sqlDomain?.kind === "numeric") {
3054
+ return isExactNumeric(value) ? { value, seeded: true } : undefined;
3055
+ }
3056
+ if (typeof value === "number" && Number.isFinite(value))
3057
+ return { value, seeded: false };
3058
+ if (value === null)
3059
+ return { value: null, seeded: false };
3060
+ return undefined;
3061
+ }
3062
+ if (expression.kind !== "binary" || expression.operator === "||")
3063
+ return undefined;
3064
+ const left = exactConstantFold(expression.left);
3065
+ if (left === undefined)
3066
+ return undefined;
3067
+ const right = exactConstantFold(expression.right);
3068
+ if (right === undefined)
3069
+ return undefined;
3070
+ const seeded = left.seeded || right.seeded;
3071
+ if (left.value === null || right.value === null)
3072
+ return { value: null, seeded };
3073
+ const folded = exactNumericBinary(expression.operator, left.value, right.value, 0, false);
3074
+ return folded === undefined ? undefined : { value: folded, seeded };
3075
+ }
3076
+ /**
3077
+ * Rewrites every seeded constant arithmetic subtree of this expression to its exact fold. A
3078
+ * folded value demotes back to an ordinary number literal when it reads back identically from
3079
+ * a Float64 — PostgreSQL's own cast when a numeric constant meets a float8 operand, and it
3080
+ * keeps every number-typed fast path — and stays a tagged exact-NUMERIC literal when the
3081
+ * number boundary would visibly round it. A fold that overflows its bounds is left unfolded
3082
+ * for execution to report.
3083
+ */
3084
+ function resolveExactNumericConstants(expression) {
3085
+ let folded;
3086
+ try {
3087
+ folded = exactConstantFold(expression);
3088
+ }
3089
+ catch {
3090
+ folded = undefined;
3091
+ }
3092
+ if (folded?.seeded === true) {
3093
+ const value = folded.value;
3094
+ if (value === null || typeof value === "number")
3095
+ return { kind: "literal", value };
3096
+ const fits = exactNumericAsNumber(value);
3097
+ if (fits !== undefined)
3098
+ return { kind: "literal", value: fits };
3099
+ return { kind: "literal", value, internalSqlValue: true, sqlDomain: { kind: "numeric" } };
3100
+ }
3101
+ if (expression.kind === "subquery" || expression.kind === "exists") {
3102
+ resolvePlanExactNumericConstants(expression.block);
3103
+ return expression;
3104
+ }
3105
+ if (expression.kind === "window") {
3106
+ return {
3107
+ ...expression,
3108
+ partitionBy: expression.partitionBy.map(resolveExactNumericConstants),
3109
+ orderBy: expression.orderBy.map((order) => ({
3110
+ ...order,
3111
+ expression: resolveExactNumericConstants(order.expression),
3112
+ })),
3113
+ ...(expression.argument === undefined
3114
+ ? {}
3115
+ : { argument: resolveExactNumericConstants(expression.argument) }),
3116
+ };
3117
+ }
3118
+ return mapChildExpressions(expression, resolveExactNumericConstants);
3119
+ }
3120
+ /** Applies `resolveExactNumericConstants` to every expression of a plan, nested blocks included. */
3121
+ function resolvePlanExactNumericConstants(plan) {
3122
+ mapBlockExpressions(plan, resolveExactNumericConstants);
3123
+ forEachNestedBlock(plan, resolvePlanExactNumericConstants);
3124
+ }
3125
+ /**
3126
+ * The statement counterpart of `resolvePlanExactNumericConstants`: every expression slot a
3127
+ * mutation evaluates later. INSERT VALUES cells are absent because the parser evaluates them
3128
+ * to values on the spot, resolving each expression first.
3129
+ */
3130
+ function resolveStatementExactNumericConstants(statement) {
3131
+ const resolve = resolveExactNumericConstants;
3132
+ if (statement.kind === "insert") {
3133
+ const conflict = statement.onConflict;
3134
+ if (conflict?.assignments !== undefined) {
3135
+ for (const assignment of conflict.assignments) {
3136
+ assignment.expression = resolve(assignment.expression);
3137
+ }
3138
+ }
3139
+ if (conflict?.where !== undefined)
3140
+ conflict.where = resolve(conflict.where);
3141
+ return;
3142
+ }
3143
+ if (statement.kind === "update" || statement.kind === "delete") {
3144
+ if (statement.kind === "update") {
3145
+ for (const assignment of statement.assignments) {
3146
+ assignment.expression = resolve(assignment.expression);
3147
+ }
3148
+ }
3149
+ for (const predicate of statement.predicates) {
3150
+ predicate.left = resolve(predicate.left);
3151
+ predicate.right = resolve(predicate.right);
3152
+ }
3153
+ return;
3154
+ }
3155
+ if (statement.kind === "merge") {
3156
+ statement.on = resolve(statement.on);
3157
+ for (const branch of statement.branches) {
3158
+ if (branch.condition !== undefined)
3159
+ branch.condition = resolve(branch.condition);
3160
+ if (branch.action.kind === "update") {
3161
+ for (const assignment of branch.action.assignments) {
3162
+ assignment.expression = resolve(assignment.expression);
3163
+ }
3164
+ }
3165
+ if (branch.action.kind === "insert") {
3166
+ branch.action.values = branch.action.values.map(resolve);
3167
+ }
3168
+ }
3169
+ }
3170
+ }
2956
3171
  /**
2957
3172
  * Aggregates the frame members named by position, for the frames whose rows are not contiguous.
2958
3173
  * EXCLUDE puts a hole in the middle of a frame, which the prefix sums the common path uses
2959
3174
  * cannot represent, so those positions walk their members instead.
2960
3175
  */
2961
- function aggregateWindowMembers(window, values, members) {
3176
+ function aggregateWindowMembers(window, values, members, avgScale) {
2962
3177
  const present = members.filter((member) => {
2963
3178
  const value = values[member];
2964
3179
  return value !== null && value !== undefined;
@@ -2988,7 +3203,9 @@ function aggregateWindowMembers(window, values, members) {
2988
3203
  throw new Error("Exact NUMERIC sum disappeared");
2989
3204
  total = next;
2990
3205
  }
2991
- return window.name === "SUM" ? total : exactNumericBinary("/", total, present.length);
3206
+ return window.name === "SUM"
3207
+ ? total
3208
+ : exactNumericBinary("/", total, present.length, avgScale);
2992
3209
  }
2993
3210
  const total = present.reduce((sum, member) => sum + numeric(values[member]), 0);
2994
3211
  return window.name === "SUM" ? total : total / present.length;
@@ -3013,7 +3230,7 @@ function aggregateWindowMembers(window, values, members) {
3013
3230
  * RANGE frames take only UNBOUNDED and CURRENT ROW bounds, where CURRENT ROW spans the peer
3014
3231
  * group. COUNT of an empty frame is 0 and every other aggregate NULL.
3015
3232
  */
3016
- function applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKeys) {
3233
+ function applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKeys, avgScale) {
3017
3234
  const frame = window.frame ?? {
3018
3235
  unit: "range",
3019
3236
  start: { kind: "unbounded-preceding" },
@@ -3025,11 +3242,11 @@ function applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKey
3025
3242
  while (end < indexes.length && samePartition(indexes[start] ?? 0, indexes[end] ?? 0)) {
3026
3243
  end += 1;
3027
3244
  }
3028
- applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end);
3245
+ applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end, avgScale);
3029
3246
  start = end;
3030
3247
  }
3031
3248
  }
3032
- function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end) {
3249
+ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end, avgScale) {
3033
3250
  const size = end - start;
3034
3251
  // Peer-group bounds per position; with no OVER ordering the whole partition is one peer group.
3035
3252
  const peerStart = new Array(size).fill(0);
@@ -3160,7 +3377,7 @@ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKe
3160
3377
  if (!dropped)
3161
3378
  members.push(member);
3162
3379
  }
3163
- value = aggregateWindowMembers(window, values, members);
3380
+ value = aggregateWindowMembers(window, values, members, avgScale);
3164
3381
  const row = rows[indexes[start + position] ?? -1];
3165
3382
  if (row !== undefined)
3166
3383
  row[window.alias] = asQueryValue(value);
@@ -3192,7 +3409,7 @@ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKe
3192
3409
  }
3193
3410
  else if (prefixExact !== undefined) {
3194
3411
  const total = exactNumericBinary("-", prefixExact[high] ?? exactZero, prefixExact[low] ?? exactZero);
3195
- value = window.name === "SUM" ? total : exactNumericBinary("/", total, nonNull);
3412
+ value = window.name === "SUM" ? total : exactNumericBinary("/", total, nonNull, avgScale);
3196
3413
  }
3197
3414
  else {
3198
3415
  const total = (prefixSum?.[high] ?? 0) - (prefixSum?.[low] ?? 0);
@@ -3369,7 +3586,13 @@ export function applyWindowFunctions(result, windows, options = {}) {
3369
3586
  continue;
3370
3587
  }
3371
3588
  if (window.name !== "ROW_NUMBER" && window.name !== "RANK" && window.name !== "DENSE_RANK") {
3372
- applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKeys);
3589
+ // PostgreSQL computes a window AVG's quotient to at least the summed values' display
3590
+ // scale; canonical NUMERIC values no longer carry it, so read the argument column's
3591
+ // declared scale from the inner result the way the summed dscale would have carried it.
3592
+ const argumentDomain = window.name === "AVG" && window.argumentAlias !== undefined
3593
+ ? result.columnDomains[result.columns.indexOf(window.argumentAlias)]
3594
+ : undefined;
3595
+ applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKeys, argumentDomain?.kind === "numeric" ? argumentDomain.scale : undefined);
3373
3596
  continue;
3374
3597
  }
3375
3598
  let rowNumber = 0;
@@ -3781,7 +4004,7 @@ function evaluate(expression, context, group) {
3781
4004
  if (typeof left !== "string" || typeof right !== "string") {
3782
4005
  throw new TypeError("|| requires string operands");
3783
4006
  }
3784
- return protectedSqlTextValue(String(externalSqlDomainValue(left)) + String(externalSqlDomainValue(right)));
4007
+ return concatenatedSqlValue(left, right);
3785
4008
  }
3786
4009
  const exact = exactNumericBinary(expression.operator, left, right);
3787
4010
  if (exact !== undefined)
@@ -3925,7 +4148,8 @@ function evaluate(expression, context, group) {
3925
4148
  ? null
3926
4149
  : (() => {
3927
4150
  const sum = sumNumericValues(values);
3928
- return exactNumericBinary("/", sum, values.length) ?? numeric(sum) / values.length;
4151
+ return (exactNumericBinary("/", sum, values.length, expression.avgArgumentScale) ??
4152
+ numeric(sum) / values.length);
3929
4153
  })();
3930
4154
  if (expression.name === "MIN")
3931
4155
  return values.reduce((best, value) => (best === undefined || compareValues(value, best) < 0 ? value : best), undefined);
@@ -4378,6 +4602,11 @@ export function hasAggregate(expression) {
4378
4602
  }
4379
4603
  return childExpressions(expression).some(hasAggregate);
4380
4604
  }
4605
+ function hasWindow(expression) {
4606
+ if (expression.kind === "window")
4607
+ return true;
4608
+ return childExpressions(expression).some(hasWindow);
4609
+ }
4381
4610
  /** One root aggregate call, using the same canonical set as parsing and execution. */
4382
4611
  export function isAggregateCall(expression) {
4383
4612
  return expression.kind === "call" && aggregateNames.has(expression.name);
@@ -4478,9 +4707,6 @@ function asQueryValue(value) {
4478
4707
  return null;
4479
4708
  throw new TypeError("Query produced an unsupported value");
4480
4709
  }
4481
- function asExternalQueryValue(value) {
4482
- return asQueryValue(externalSqlDomainValue(value));
4483
- }
4484
4710
  const alreadyExternalResults = new WeakSet();
4485
4711
  function markExternalizationState(result, outputNeedsExternalization) {
4486
4712
  if (outputNeedsExternalization === false)
@@ -4505,11 +4731,12 @@ export function externalizeQueryResult(result) {
4505
4731
  // Ordinary primitive results are already public values. Most queries never touch one of the
4506
4732
  // tagged PostgreSQL domains, so keep their row objects and avoid rebuilding a large result
4507
4733
  // set merely to discover that every value is unchanged.
4508
- for (const name of result.columns) {
4734
+ for (let position = 0; position < result.columns.length; position += 1) {
4735
+ const name = result.columns[position] ?? "";
4509
4736
  const value = row[name];
4510
4737
  if (value !== undefined && !isSqlDomainValue(value))
4511
4738
  continue;
4512
- const external = asExternalQueryValue(value);
4739
+ const external = asQueryValue(externalSqlDomainColumnValue(value, result.columnDomains[position]));
4513
4740
  if (external === value)
4514
4741
  continue;
4515
4742
  if (output === row)
@@ -5272,6 +5499,14 @@ class Parser {
5272
5499
  },
5273
5500
  };
5274
5501
  }
5502
+ /** A type name's precision, scale, or width: plain digits, as PostgreSQL requires there. */
5503
+ #typeWidth() {
5504
+ const token = this.#take("number");
5505
+ if (!/^\d+$/.test(token.text)) {
5506
+ throw new SqlCompileError(`A type width must be an integer: ${token.text}`, token.start, token.end - token.start);
5507
+ }
5508
+ return Number(token.text);
5509
+ }
5275
5510
  /** A CAST target: the SqlColumnType, or "number-integer" for the truncating integer names. */
5276
5511
  #castTarget() {
5277
5512
  const word = this.#identifier().toUpperCase();
@@ -5279,9 +5514,9 @@ class Parser {
5279
5514
  let precision;
5280
5515
  let scale;
5281
5516
  if (this.#punctuation("(")) {
5282
- precision = Number(this.#take("number").text);
5517
+ precision = this.#typeWidth();
5283
5518
  if (this.#punctuation(","))
5284
- scale = Number(this.#take("number").text);
5519
+ scale = this.#typeWidth();
5285
5520
  else
5286
5521
  scale = 0;
5287
5522
  this.#expectPunctuation(")");
@@ -5299,9 +5534,9 @@ class Parser {
5299
5534
  if (mapped === undefined)
5300
5535
  throw new TypeError(`Unsupported CAST target: ${word}`);
5301
5536
  if (this.#punctuation("(")) {
5302
- this.#take("number");
5537
+ this.#typeWidth();
5303
5538
  if (this.#punctuation(","))
5304
- this.#take("number");
5539
+ this.#typeWidth();
5305
5540
  this.#expectPunctuation(")");
5306
5541
  }
5307
5542
  const integer = word === "INTEGER" || word === "INT" || word === "BIGINT" || word === "SMALLINT";
@@ -5314,8 +5549,8 @@ class Parser {
5314
5549
  let precision;
5315
5550
  let scale;
5316
5551
  if (this.#punctuation("(")) {
5317
- precision = Number(this.#take("number").text);
5318
- scale = this.#punctuation(",") ? Number(this.#take("number").text) : 0;
5552
+ precision = this.#typeWidth();
5553
+ scale = this.#punctuation(",") ? this.#typeWidth() : 0;
5319
5554
  this.#expectPunctuation(")");
5320
5555
  }
5321
5556
  return {
@@ -5340,7 +5575,7 @@ class Parser {
5340
5575
  }
5341
5576
  // Character widths document intent and do not truncate values.
5342
5577
  if (this.#punctuation("(")) {
5343
- this.#take("number");
5578
+ this.#typeWidth();
5344
5579
  if (this.#punctuation(",")) {
5345
5580
  throw new TypeError(`${word} takes one width`);
5346
5581
  }
@@ -5360,6 +5595,7 @@ class Parser {
5360
5595
  ? this.#updateStatement()
5361
5596
  : this.#deleteStatement();
5362
5597
  this.#take("eof");
5598
+ resolveStatementExactNumericConstants(statement);
5363
5599
  return statement;
5364
5600
  }
5365
5601
  #insertStatement() {
@@ -5392,7 +5628,9 @@ class Parser {
5392
5628
  };
5393
5629
  }
5394
5630
  if (this.#isKeyword("SELECT")) {
5395
- const query = optimizePlan(this.#selectBlock("(insert select)"));
5631
+ const insertSource = this.#selectBlock("(insert select)");
5632
+ resolvePlanExactNumericConstants(insertSource);
5633
+ const query = optimizePlan(insertSource);
5396
5634
  if (query.select.some((item) => item.expression.kind === "wildcard")) {
5397
5635
  throw new TypeError("INSERT ... SELECT requires an explicit select list");
5398
5636
  }
@@ -5655,7 +5893,9 @@ class Parser {
5655
5893
  throw new TypeError("MERGE does not support RETURNING; read the rows back with a SELECT");
5656
5894
  }
5657
5895
  this.#take("eof");
5658
- return { kind: "merge", table, alias, source, on, branches };
5896
+ const statement = { kind: "merge", table, alias, source, on, branches };
5897
+ resolveStatementExactNumericConstants(statement);
5898
+ return statement;
5659
5899
  }
5660
5900
  #updateStatement() {
5661
5901
  this.#keyword("UPDATE");
@@ -5715,7 +5955,7 @@ class Parser {
5715
5955
  if (hasAggregate(expression) || expressionColumns(expression).length > 0) {
5716
5956
  throw new TypeError(`${label} must be constant expressions`);
5717
5957
  }
5718
- return asQueryValue(evaluate(expression, {}));
5958
+ return asQueryValue(evaluate(resolveExactNumericConstants(expression), {}));
5719
5959
  }
5720
5960
  #unionMember(sql) {
5721
5961
  if (this.#punctuation("(")) {
@@ -6896,17 +7136,41 @@ class Parser {
6896
7136
  continue;
6897
7137
  }
6898
7138
  const operator = this.#peek().text;
6899
- // || binds loosest, matching PostgreSQL: concatenation applies to whole arithmetic terms.
7139
+ // A bracket after a complete expression is PostgreSQL's array subscript, which has no
7140
+ // other reading in this grammar — name the missing feature instead of "Expected eof".
7141
+ if (operator === "[") {
7142
+ throw new TypeError("Array subscripts are not supported");
7143
+ }
7144
+ // || and the JSON arrows share PostgreSQL's loosest "any other operator" level, applying
7145
+ // left-to-right to whole arithmetic terms: `'a' || d ->> 'k'` is `('a' || d) ->> 'k'`.
6900
7146
  const precedence = operator === "*" || operator === "/" || operator === "%"
6901
7147
  ? 20
6902
7148
  : operator === "+" || operator === "-"
6903
7149
  ? 10
6904
- : operator === "||"
7150
+ : operator === "||" || operator === "->" || operator === "->>"
6905
7151
  ? 5
6906
7152
  : -1;
6907
7153
  if (precedence < minimumPrecedence)
6908
7154
  break;
6909
7155
  this.#index += 1;
7156
+ if (operator === "->" || operator === "->>") {
7157
+ const key = this.#additive(precedence + 1);
7158
+ // A key fixed at compile time fails here rather than per row, like SQL/JSON paths.
7159
+ if (key.kind === "literal" && typeof key.value === "number") {
7160
+ if (!Number.isInteger(key.value)) {
7161
+ throw new TypeError(`${operator} array positions are integers`);
7162
+ }
7163
+ }
7164
+ else if (key.kind === "literal" && typeof key.value === "boolean") {
7165
+ throw new TypeError(`${operator} keys are member names or array positions`);
7166
+ }
7167
+ left = {
7168
+ kind: "call",
7169
+ name: operator === "->" ? "MINNOW_JSON_GET" : "MINNOW_JSON_GET_TEXT",
7170
+ arguments: [left, key],
7171
+ };
7172
+ continue;
7173
+ }
6910
7174
  // `placed_at + INTERVAL '1 month'`. An interval is not a value any column can hold, so it
6911
7175
  // never becomes an expression of its own: it is read here, where the thing it applies to is
6912
7176
  // already in hand, and folds into the date arithmetic DATE_ADD performs.
@@ -6966,10 +7230,29 @@ class Parser {
6966
7230
  if (token.kind === "number") {
6967
7231
  this.#index += 1;
6968
7232
  const value = Number(token.text);
6969
- if (!token.text.includes(".") && !Number.isSafeInteger(value)) {
6970
- throw new SqlCompileError(`Integer literal is outside the exact safe range: ${token.text}`, token.start, token.end - token.start);
7233
+ // A safe integer spelled as one is exactly itself; every other spelling — a decimal
7234
+ // point, an exponent, or digits beyond 2^53 is a numeric constant PostgreSQL would
7235
+ // type NUMERIC. Keep its exact digits: as an annotation when the value reads back
7236
+ // identically from a number, or as a tagged exact-NUMERIC literal when it would not.
7237
+ if (!/[.eE]/.test(token.text) && Number.isSafeInteger(value)) {
7238
+ return { kind: "literal", value };
7239
+ }
7240
+ let tagged;
7241
+ try {
7242
+ tagged = exactNumericLiteral(token.text);
6971
7243
  }
6972
- return { kind: "literal", value };
7244
+ catch (error) {
7245
+ throw new SqlCompileError(error instanceof Error ? error.message : `Invalid number: ${token.text}`, token.start, token.end - token.start);
7246
+ }
7247
+ const fits = exactNumericAsNumber(tagged);
7248
+ if (fits !== undefined)
7249
+ return { kind: "literal", value: fits, exactText: token.text };
7250
+ return {
7251
+ kind: "literal",
7252
+ value: tagged,
7253
+ internalSqlValue: true,
7254
+ sqlDomain: { kind: "numeric" },
7255
+ };
6973
7256
  }
6974
7257
  if (token.kind === "string") {
6975
7258
  this.#index += 1;
@@ -7380,7 +7663,9 @@ class Parser {
7380
7663
  const name = (upper === "ANY_VALUE" ? "MIN" : (functionSpellings.get(upper) ?? upper));
7381
7664
  if (name === "MINNOW_TUPLE_KEY" ||
7382
7665
  name === "MINNOW_COLLATE" ||
7383
- name === "MINNOW_SINGLE_VALUE") {
7666
+ name === "MINNOW_SINGLE_VALUE" ||
7667
+ name === "MINNOW_JSON_GET" ||
7668
+ name === "MINNOW_JSON_GET_TEXT") {
7384
7669
  throw new TypeError(`Unsupported function: ${identifier}`);
7385
7670
  }
7386
7671
  if (!aggregateNames.has(name) && !scalarFunctionNames.has(name))
@@ -8058,6 +8343,8 @@ export function assembleSelectBlock(parts, nextSequence) {
8058
8343
  if (distinct) {
8059
8344
  if (select.some((item) => hasAggregate(item.expression)))
8060
8345
  throw new TypeError("SELECT DISTINCT cannot be combined with aggregate functions");
8346
+ if (select.some((item) => hasWindow(item.expression)))
8347
+ throw new TypeError("SELECT DISTINCT cannot be combined with window functions");
8061
8348
  if (groupBy.length > 0)
8062
8349
  throw new TypeError("SELECT DISTINCT cannot be combined with GROUP BY");
8063
8350
  if (having.length > 0)
@@ -8136,7 +8423,7 @@ export function assembleSelectBlock(parts, nextSequence) {
8136
8423
  * over those same columns provides the deduplication through the grouped executor. Runs
8137
8424
  * exactly once per execution entry, like MATCH(*) expansion.
8138
8425
  */
8139
- export function expandDistinctWildcard(plan, columnsOf) {
8426
+ function expandDistinctWildcard(plan, columnsOf) {
8140
8427
  if (plan.distinctWildcard !== true)
8141
8428
  return plan;
8142
8429
  const shaped = [plan.base, ...plan.joins].map((source) => ({
@@ -8228,7 +8515,7 @@ export function planHasSourceColumnAliases(plan) {
8228
8515
  * scan cannot know whether the next row ties — and the ordered result is trimmed here: rows up
8229
8516
  * to the limit, plus every following row equal to the last one on all ORDER BY columns.
8230
8517
  */
8231
- export function withTiesPlan(plan) {
8518
+ function withTiesPlan(plan) {
8232
8519
  // Ordering by an expression or an unselected column wraps the real block in a projection that
8233
8520
  // hides the sort column. The tie test needs that column, so the inner block runs and the
8234
8521
  // projection is applied after trimming instead of before.
@@ -8415,7 +8702,7 @@ export function expandSourceColumnAliases(plan, columnsOf) {
8415
8702
  * one source, and `alias.column` when it reads several, so two sources cannot collide.
8416
8703
  * Runs once per execution entry, like MATCH(*) and DISTINCT * expansion.
8417
8704
  */
8418
- export function expandQualifiedWildcards(plan, columnsOf) {
8705
+ function expandQualifiedWildcards(plan, columnsOf) {
8419
8706
  const qualified = (block) => {
8420
8707
  if (block.select.some((item) => item.expression.kind === "wildcard" && item.expression.table))
8421
8708
  return true;
@@ -8691,7 +8978,7 @@ function jsonTableColumnValue(column, value) {
8691
8978
  }
8692
8979
  return date;
8693
8980
  }
8694
- export function timestampLiteral(text) {
8981
+ function timestampLiteral(text) {
8695
8982
  const trimmed = text.trim();
8696
8983
  const match = /^(\d{4}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?))?(Z|[+-]\d{2}:?\d{2})?$/.exec(trimmed);
8697
8984
  if (match === null)
@@ -8865,13 +9152,14 @@ function defaultAlias(expression) {
8865
9152
  return "expression";
8866
9153
  }
8867
9154
  /**
8868
- * Whether a numeric literal's digits are well formed: at most one decimal point (radix 10 only)
8869
- * and underscores only between digits, never leading, trailing, or doubled (T662).
9155
+ * Whether a numeric literal's digits are well formed: at most one decimal point and an optional
9156
+ * exponent (radix 10 only), and underscores only between digits, never leading, trailing, or
9157
+ * doubled (T662). Exponent digits are plain, as in PostgreSQL.
8870
9158
  */
8871
9159
  function validNumericLiteral(text, radix) {
8872
9160
  const digits = radix === 16 ? "0-9a-fA-F" : radix === 8 ? "0-7" : radix === 2 ? "01" : "0-9";
8873
9161
  const group = `[${digits}]+(?:_[${digits}]+)*`;
8874
- const pattern = radix === 10 ? `^${group}(?:\\.${group})?$` : `^${group}$`;
9162
+ const pattern = radix === 10 ? `^${group}(?:\\.${group})?(?:[eE][+-]?\\d+)?$` : `^${group}$`;
8875
9163
  return new RegExp(pattern).test(text);
8876
9164
  }
8877
9165
  function tokenize(sql) {
@@ -8929,6 +9217,16 @@ function tokenize(sql) {
8929
9217
  // T662: underscores may separate digits.
8930
9218
  while (index < sql.length && /[\d._]/.test(sql[index] ?? ""))
8931
9219
  index += 1;
9220
+ // Scientific notation: an exponent marker joins the token only when digits follow, so
9221
+ // `1e2` is the numeric constant 100 while `SELECT 1 e` still reads `e` as an alias.
9222
+ if (/[eE]/.test(sql[index] ?? "")) {
9223
+ const signed = /[+-]/.test(sql[index + 1] ?? "") ? 1 : 0;
9224
+ if (/\d/.test(sql[index + 1 + signed] ?? "")) {
9225
+ index += 2 + signed;
9226
+ while (index < sql.length && /\d/.test(sql[index] ?? ""))
9227
+ index += 1;
9228
+ }
9229
+ }
8932
9230
  const text = sql.slice(start, index);
8933
9231
  if (!validNumericLiteral(text, 10))
8934
9232
  throw new SqlCompileError(`Invalid number: ${text}`, start, index - start);
@@ -9022,7 +9320,12 @@ function tokenize(sql) {
9022
9320
  index += 1;
9023
9321
  continue;
9024
9322
  }
9025
- if ([">=", "<=", "!=", "<>", "||"].includes(pair)) {
9323
+ if (pair === "->" && sql[index + 2] === ">") {
9324
+ push({ kind: "operator", text: "->>", start: index, end: index + 3 });
9325
+ index += 3;
9326
+ continue;
9327
+ }
9328
+ if ([">=", "<=", "!=", "<>", "||", "->"].includes(pair)) {
9026
9329
  push({ kind: "operator", text: pair, start: index, end: index + 2 });
9027
9330
  index += 2;
9028
9331
  continue;