@minnowdb/core 0.6.5 → 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 +58 -34
  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 +9 -2
  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 +280 -39
  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 +27 -2
  23. package/dist/engine/sql-domains.js +120 -6
  24. package/dist/engine/sql-json.d.ts +0 -2
  25. package/dist/engine/sql-json.js +1 -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 +15 -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 +8 -3
  43. package/sql-feature-matrix.json +71 -11
@@ -9,7 +9,7 @@ import { stringArgument } from "./sql-semantics.js";
9
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, externalSqlDomainColumnValue, 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) {
@@ -81,7 +81,7 @@ export const volatileScalarFunctionNames = new Set([
81
81
  * reading, so they are resolved once per execution rather than evaluated per row: every row of
82
82
  * one statement sees one instant, both executors agree, and constant folding leaves them alone.
83
83
  */
84
- export const statementDatetimeNames = new Set([
84
+ const statementDatetimeNames = new Set([
85
85
  "CURRENT_DATE",
86
86
  "CURRENT_TIMESTAMP",
87
87
  "LOCALTIME",
@@ -607,7 +607,7 @@ function assertReplacementResultLength(source, search, replacement) {
607
607
  throw new RangeError(`REPLACE result exceeds ${String(MAX_SQL_SCALAR_RESULT_CHARACTERS)} characters`);
608
608
  }
609
609
  }
610
- export const dateTruncUnits = new Set([
610
+ const dateTruncUnits = new Set([
611
611
  "year",
612
612
  "quarter",
613
613
  "month",
@@ -637,7 +637,7 @@ const intervalUnits = new Map([
637
637
  * rather than converted, because a month is not a fixed number of them: adding one to January 31
638
638
  * has to land on the end of February, which only calendar arithmetic can do.
639
639
  */
640
- export function intervalLiteral(text) {
640
+ function intervalLiteral(text) {
641
641
  let months = 0;
642
642
  let milliseconds = 0;
643
643
  let matched = 0;
@@ -663,7 +663,7 @@ export function intervalLiteral(text) {
663
663
  * does not exist in the target month clamps to that month's last day, which is what both SQLite
664
664
  * and PostgreSQL do with 31 January plus a month.
665
665
  */
666
- export function dateAddValue(value, months, milliseconds) {
666
+ function dateAddValue(value, months, milliseconds) {
667
667
  if (value === null || value === undefined)
668
668
  return null;
669
669
  const calendarDate = isDateDomainValue(value);
@@ -690,7 +690,7 @@ export function dateAddValue(value, months, milliseconds) {
690
690
  ? dateDomainValue(dateIsoString(result).slice(0, 10))
691
691
  : result;
692
692
  }
693
- export function dateTruncValue(unit, value) {
693
+ function dateTruncValue(unit, value) {
694
694
  if (typeof unit !== "string" || !dateTruncUnits.has(unit.toLowerCase())) {
695
695
  throw new TypeError("DATE_TRUNC requires a unit of year, quarter, month, week, day, hour, minute, or second");
696
696
  }
@@ -888,6 +888,7 @@ export function compileQuery(sql, options = {}) {
888
888
  }
889
889
  let compiled;
890
890
  try {
891
+ resolvePlanExactNumericConstants(plan);
891
892
  compiled = options.optimize === false ? plan : optimizePlan(plan);
892
893
  }
893
894
  catch (error) {
@@ -995,7 +996,7 @@ export function compileCheckExpression(sql, name) {
995
996
  if (hasAggregate(expression) || containsWindow(expression) || containsParameter(expression)) {
996
997
  throw new TypeError(`CHECK ${name} takes a row condition over this table's own columns`);
997
998
  }
998
- return expression;
999
+ return resolveExactNumericConstants(expression);
999
1000
  }
1000
1001
  /**
1001
1002
  * Parses and type-checks one catalog default. PostgreSQL defaults are variable-free scalar
@@ -1529,7 +1530,7 @@ export function blockHasSubqueries(plan) {
1529
1530
  return blockHas(plan);
1530
1531
  }
1531
1532
  /** True when any `?`/`$n` placeholder remains anywhere in the expression tree. */
1532
- export function containsParameter(expression) {
1533
+ function containsParameter(expression) {
1533
1534
  if (expression.kind === "parameter")
1534
1535
  return true;
1535
1536
  if (expression.kind === "subquery" || expression.kind === "exists") {
@@ -1537,7 +1538,7 @@ export function containsParameter(expression) {
1537
1538
  }
1538
1539
  return childExpressions(expression).some(containsParameter);
1539
1540
  }
1540
- export function blockHasParameters(block) {
1541
+ function blockHasParameters(block) {
1541
1542
  if (block.limitParameter !== undefined ||
1542
1543
  block.offsetParameter !== undefined ||
1543
1544
  (block.limitValidationParameters?.length ?? 0) > 0 ||
@@ -2991,12 +2992,188 @@ export function expandFtsColumns(plan, searchableColumnsFor) {
2991
2992
  expandBlock(plan);
2992
2993
  return plan;
2993
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
+ }
2994
3171
  /**
2995
3172
  * Aggregates the frame members named by position, for the frames whose rows are not contiguous.
2996
3173
  * EXCLUDE puts a hole in the middle of a frame, which the prefix sums the common path uses
2997
3174
  * cannot represent, so those positions walk their members instead.
2998
3175
  */
2999
- function aggregateWindowMembers(window, values, members) {
3176
+ function aggregateWindowMembers(window, values, members, avgScale) {
3000
3177
  const present = members.filter((member) => {
3001
3178
  const value = values[member];
3002
3179
  return value !== null && value !== undefined;
@@ -3026,7 +3203,9 @@ function aggregateWindowMembers(window, values, members) {
3026
3203
  throw new Error("Exact NUMERIC sum disappeared");
3027
3204
  total = next;
3028
3205
  }
3029
- return window.name === "SUM" ? total : exactNumericBinary("/", total, present.length);
3206
+ return window.name === "SUM"
3207
+ ? total
3208
+ : exactNumericBinary("/", total, present.length, avgScale);
3030
3209
  }
3031
3210
  const total = present.reduce((sum, member) => sum + numeric(values[member]), 0);
3032
3211
  return window.name === "SUM" ? total : total / present.length;
@@ -3051,7 +3230,7 @@ function aggregateWindowMembers(window, values, members) {
3051
3230
  * RANGE frames take only UNBOUNDED and CURRENT ROW bounds, where CURRENT ROW spans the peer
3052
3231
  * group. COUNT of an empty frame is 0 and every other aggregate NULL.
3053
3232
  */
3054
- function applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKeys) {
3233
+ function applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKeys, avgScale) {
3055
3234
  const frame = window.frame ?? {
3056
3235
  unit: "range",
3057
3236
  start: { kind: "unbounded-preceding" },
@@ -3063,11 +3242,11 @@ function applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKey
3063
3242
  while (end < indexes.length && samePartition(indexes[start] ?? 0, indexes[end] ?? 0)) {
3064
3243
  end += 1;
3065
3244
  }
3066
- applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end);
3245
+ applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end, avgScale);
3067
3246
  start = end;
3068
3247
  }
3069
3248
  }
3070
- function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end) {
3249
+ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end, avgScale) {
3071
3250
  const size = end - start;
3072
3251
  // Peer-group bounds per position; with no OVER ordering the whole partition is one peer group.
3073
3252
  const peerStart = new Array(size).fill(0);
@@ -3198,7 +3377,7 @@ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKe
3198
3377
  if (!dropped)
3199
3378
  members.push(member);
3200
3379
  }
3201
- value = aggregateWindowMembers(window, values, members);
3380
+ value = aggregateWindowMembers(window, values, members, avgScale);
3202
3381
  const row = rows[indexes[start + position] ?? -1];
3203
3382
  if (row !== undefined)
3204
3383
  row[window.alias] = asQueryValue(value);
@@ -3230,7 +3409,7 @@ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKe
3230
3409
  }
3231
3410
  else if (prefixExact !== undefined) {
3232
3411
  const total = exactNumericBinary("-", prefixExact[high] ?? exactZero, prefixExact[low] ?? exactZero);
3233
- value = window.name === "SUM" ? total : exactNumericBinary("/", total, nonNull);
3412
+ value = window.name === "SUM" ? total : exactNumericBinary("/", total, nonNull, avgScale);
3234
3413
  }
3235
3414
  else {
3236
3415
  const total = (prefixSum?.[high] ?? 0) - (prefixSum?.[low] ?? 0);
@@ -3407,7 +3586,13 @@ export function applyWindowFunctions(result, windows, options = {}) {
3407
3586
  continue;
3408
3587
  }
3409
3588
  if (window.name !== "ROW_NUMBER" && window.name !== "RANK" && window.name !== "DENSE_RANK") {
3410
- 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);
3411
3596
  continue;
3412
3597
  }
3413
3598
  let rowNumber = 0;
@@ -3819,7 +4004,7 @@ function evaluate(expression, context, group) {
3819
4004
  if (typeof left !== "string" || typeof right !== "string") {
3820
4005
  throw new TypeError("|| requires string operands");
3821
4006
  }
3822
- return protectedSqlTextValue(String(externalSqlDomainValue(left)) + String(externalSqlDomainValue(right)));
4007
+ return concatenatedSqlValue(left, right);
3823
4008
  }
3824
4009
  const exact = exactNumericBinary(expression.operator, left, right);
3825
4010
  if (exact !== undefined)
@@ -3963,7 +4148,8 @@ function evaluate(expression, context, group) {
3963
4148
  ? null
3964
4149
  : (() => {
3965
4150
  const sum = sumNumericValues(values);
3966
- return exactNumericBinary("/", sum, values.length) ?? numeric(sum) / values.length;
4151
+ return (exactNumericBinary("/", sum, values.length, expression.avgArgumentScale) ??
4152
+ numeric(sum) / values.length);
3967
4153
  })();
3968
4154
  if (expression.name === "MIN")
3969
4155
  return values.reduce((best, value) => (best === undefined || compareValues(value, best) < 0 ? value : best), undefined);
@@ -4416,6 +4602,11 @@ export function hasAggregate(expression) {
4416
4602
  }
4417
4603
  return childExpressions(expression).some(hasAggregate);
4418
4604
  }
4605
+ function hasWindow(expression) {
4606
+ if (expression.kind === "window")
4607
+ return true;
4608
+ return childExpressions(expression).some(hasWindow);
4609
+ }
4419
4610
  /** One root aggregate call, using the same canonical set as parsing and execution. */
4420
4611
  export function isAggregateCall(expression) {
4421
4612
  return expression.kind === "call" && aggregateNames.has(expression.name);
@@ -5308,6 +5499,14 @@ class Parser {
5308
5499
  },
5309
5500
  };
5310
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
+ }
5311
5510
  /** A CAST target: the SqlColumnType, or "number-integer" for the truncating integer names. */
5312
5511
  #castTarget() {
5313
5512
  const word = this.#identifier().toUpperCase();
@@ -5315,9 +5514,9 @@ class Parser {
5315
5514
  let precision;
5316
5515
  let scale;
5317
5516
  if (this.#punctuation("(")) {
5318
- precision = Number(this.#take("number").text);
5517
+ precision = this.#typeWidth();
5319
5518
  if (this.#punctuation(","))
5320
- scale = Number(this.#take("number").text);
5519
+ scale = this.#typeWidth();
5321
5520
  else
5322
5521
  scale = 0;
5323
5522
  this.#expectPunctuation(")");
@@ -5335,9 +5534,9 @@ class Parser {
5335
5534
  if (mapped === undefined)
5336
5535
  throw new TypeError(`Unsupported CAST target: ${word}`);
5337
5536
  if (this.#punctuation("(")) {
5338
- this.#take("number");
5537
+ this.#typeWidth();
5339
5538
  if (this.#punctuation(","))
5340
- this.#take("number");
5539
+ this.#typeWidth();
5341
5540
  this.#expectPunctuation(")");
5342
5541
  }
5343
5542
  const integer = word === "INTEGER" || word === "INT" || word === "BIGINT" || word === "SMALLINT";
@@ -5350,8 +5549,8 @@ class Parser {
5350
5549
  let precision;
5351
5550
  let scale;
5352
5551
  if (this.#punctuation("(")) {
5353
- precision = Number(this.#take("number").text);
5354
- scale = this.#punctuation(",") ? Number(this.#take("number").text) : 0;
5552
+ precision = this.#typeWidth();
5553
+ scale = this.#punctuation(",") ? this.#typeWidth() : 0;
5355
5554
  this.#expectPunctuation(")");
5356
5555
  }
5357
5556
  return {
@@ -5376,7 +5575,7 @@ class Parser {
5376
5575
  }
5377
5576
  // Character widths document intent and do not truncate values.
5378
5577
  if (this.#punctuation("(")) {
5379
- this.#take("number");
5578
+ this.#typeWidth();
5380
5579
  if (this.#punctuation(",")) {
5381
5580
  throw new TypeError(`${word} takes one width`);
5382
5581
  }
@@ -5396,6 +5595,7 @@ class Parser {
5396
5595
  ? this.#updateStatement()
5397
5596
  : this.#deleteStatement();
5398
5597
  this.#take("eof");
5598
+ resolveStatementExactNumericConstants(statement);
5399
5599
  return statement;
5400
5600
  }
5401
5601
  #insertStatement() {
@@ -5428,7 +5628,9 @@ class Parser {
5428
5628
  };
5429
5629
  }
5430
5630
  if (this.#isKeyword("SELECT")) {
5431
- const query = optimizePlan(this.#selectBlock("(insert select)"));
5631
+ const insertSource = this.#selectBlock("(insert select)");
5632
+ resolvePlanExactNumericConstants(insertSource);
5633
+ const query = optimizePlan(insertSource);
5432
5634
  if (query.select.some((item) => item.expression.kind === "wildcard")) {
5433
5635
  throw new TypeError("INSERT ... SELECT requires an explicit select list");
5434
5636
  }
@@ -5691,7 +5893,9 @@ class Parser {
5691
5893
  throw new TypeError("MERGE does not support RETURNING; read the rows back with a SELECT");
5692
5894
  }
5693
5895
  this.#take("eof");
5694
- return { kind: "merge", table, alias, source, on, branches };
5896
+ const statement = { kind: "merge", table, alias, source, on, branches };
5897
+ resolveStatementExactNumericConstants(statement);
5898
+ return statement;
5695
5899
  }
5696
5900
  #updateStatement() {
5697
5901
  this.#keyword("UPDATE");
@@ -5751,7 +5955,7 @@ class Parser {
5751
5955
  if (hasAggregate(expression) || expressionColumns(expression).length > 0) {
5752
5956
  throw new TypeError(`${label} must be constant expressions`);
5753
5957
  }
5754
- return asQueryValue(evaluate(expression, {}));
5958
+ return asQueryValue(evaluate(resolveExactNumericConstants(expression), {}));
5755
5959
  }
5756
5960
  #unionMember(sql) {
5757
5961
  if (this.#punctuation("(")) {
@@ -6932,6 +7136,11 @@ class Parser {
6932
7136
  continue;
6933
7137
  }
6934
7138
  const operator = this.#peek().text;
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
+ }
6935
7144
  // || and the JSON arrows share PostgreSQL's loosest "any other operator" level, applying
6936
7145
  // left-to-right to whole arithmetic terms: `'a' || d ->> 'k'` is `('a' || d) ->> 'k'`.
6937
7146
  const precedence = operator === "*" || operator === "/" || operator === "%"
@@ -7021,10 +7230,29 @@ class Parser {
7021
7230
  if (token.kind === "number") {
7022
7231
  this.#index += 1;
7023
7232
  const value = Number(token.text);
7024
- if (!token.text.includes(".") && !Number.isSafeInteger(value)) {
7025
- 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);
7026
7243
  }
7027
- 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
+ };
7028
7256
  }
7029
7257
  if (token.kind === "string") {
7030
7258
  this.#index += 1;
@@ -8115,6 +8343,8 @@ export function assembleSelectBlock(parts, nextSequence) {
8115
8343
  if (distinct) {
8116
8344
  if (select.some((item) => hasAggregate(item.expression)))
8117
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");
8118
8348
  if (groupBy.length > 0)
8119
8349
  throw new TypeError("SELECT DISTINCT cannot be combined with GROUP BY");
8120
8350
  if (having.length > 0)
@@ -8193,7 +8423,7 @@ export function assembleSelectBlock(parts, nextSequence) {
8193
8423
  * over those same columns provides the deduplication through the grouped executor. Runs
8194
8424
  * exactly once per execution entry, like MATCH(*) expansion.
8195
8425
  */
8196
- export function expandDistinctWildcard(plan, columnsOf) {
8426
+ function expandDistinctWildcard(plan, columnsOf) {
8197
8427
  if (plan.distinctWildcard !== true)
8198
8428
  return plan;
8199
8429
  const shaped = [plan.base, ...plan.joins].map((source) => ({
@@ -8285,7 +8515,7 @@ export function planHasSourceColumnAliases(plan) {
8285
8515
  * scan cannot know whether the next row ties — and the ordered result is trimmed here: rows up
8286
8516
  * to the limit, plus every following row equal to the last one on all ORDER BY columns.
8287
8517
  */
8288
- export function withTiesPlan(plan) {
8518
+ function withTiesPlan(plan) {
8289
8519
  // Ordering by an expression or an unselected column wraps the real block in a projection that
8290
8520
  // hides the sort column. The tie test needs that column, so the inner block runs and the
8291
8521
  // projection is applied after trimming instead of before.
@@ -8472,7 +8702,7 @@ export function expandSourceColumnAliases(plan, columnsOf) {
8472
8702
  * one source, and `alias.column` when it reads several, so two sources cannot collide.
8473
8703
  * Runs once per execution entry, like MATCH(*) and DISTINCT * expansion.
8474
8704
  */
8475
- export function expandQualifiedWildcards(plan, columnsOf) {
8705
+ function expandQualifiedWildcards(plan, columnsOf) {
8476
8706
  const qualified = (block) => {
8477
8707
  if (block.select.some((item) => item.expression.kind === "wildcard" && item.expression.table))
8478
8708
  return true;
@@ -8748,7 +8978,7 @@ function jsonTableColumnValue(column, value) {
8748
8978
  }
8749
8979
  return date;
8750
8980
  }
8751
- export function timestampLiteral(text) {
8981
+ function timestampLiteral(text) {
8752
8982
  const trimmed = text.trim();
8753
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);
8754
8984
  if (match === null)
@@ -8922,13 +9152,14 @@ function defaultAlias(expression) {
8922
9152
  return "expression";
8923
9153
  }
8924
9154
  /**
8925
- * Whether a numeric literal's digits are well formed: at most one decimal point (radix 10 only)
8926
- * 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.
8927
9158
  */
8928
9159
  function validNumericLiteral(text, radix) {
8929
9160
  const digits = radix === 16 ? "0-9a-fA-F" : radix === 8 ? "0-7" : radix === 2 ? "01" : "0-9";
8930
9161
  const group = `[${digits}]+(?:_[${digits}]+)*`;
8931
- const pattern = radix === 10 ? `^${group}(?:\\.${group})?$` : `^${group}$`;
9162
+ const pattern = radix === 10 ? `^${group}(?:\\.${group})?(?:[eE][+-]?\\d+)?$` : `^${group}$`;
8932
9163
  return new RegExp(pattern).test(text);
8933
9164
  }
8934
9165
  function tokenize(sql) {
@@ -8986,6 +9217,16 @@ function tokenize(sql) {
8986
9217
  // T662: underscores may separate digits.
8987
9218
  while (index < sql.length && /[\d._]/.test(sql[index] ?? ""))
8988
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
+ }
8989
9230
  const text = sql.slice(start, index);
8990
9231
  if (!validNumericLiteral(text, 10))
8991
9232
  throw new SqlCompileError(`Invalid number: ${text}`, start, index - start);
@@ -13,7 +13,7 @@ import type { QueryResult, QueryRow, QueryValue } from "./query.js";
13
13
  * a byte mask, and a column name like `__proto__` comes back as the own property the engine
14
14
  * defined — never as a prototype assignment.
15
15
  */
16
- export type WireResultColumn = {
16
+ type WireResultColumn = {
17
17
  kind: "number";
18
18
  values: Float64Array;
19
19
  nulls?: Uint8Array;
@@ -67,3 +67,4 @@ export declare function encodeQueryResult(result: QueryResult): EncodedQueryResu
67
67
  export declare function encodeQueryRows(rows: readonly QueryRow[]): EncodedQueryResult;
68
68
  export declare function decodeQueryResult(payload: unknown): QueryResult;
69
69
  export declare function isWireQueryResult(value: unknown): value is WireQueryResult;
70
+ export {};
@@ -55,6 +55,17 @@ export declare function foreignKeyName(tableName: string, columnName: string): s
55
55
  export type HasDefault<TValue> = TValue & {
56
56
  readonly __minnowHasDefault?: true;
57
57
  };
58
+ /**
59
+ * A JSON column's select value carrying a declared document shape. The value is still JSON
60
+ * text — the brand is an optional phantom property, so plain strings stay assignable in both
61
+ * directions — and adapters read the shape to offer typed traversal. The tuple wrapper keeps
62
+ * `never` and union shapes intact through inference.
63
+ */
64
+ export type JsonShape<TShape> = string & {
65
+ readonly __minnowJsonShape?: readonly [TShape];
66
+ };
67
+ /** A JSON column without a declared shape selects as exactly `string`, with no brand. */
68
+ type JsonColumnValue<TShape> = [TShape] extends [never] ? string : JsonShape<TShape>;
58
69
  export interface ColumnBuilder<TValue extends SchemaValue, TNullable extends boolean, TUnique extends boolean = false, THasDefault extends boolean = false, TInput extends SchemaValue = TValue, TDomain extends SqlDomain["kind"] | undefined = undefined, TGenerated extends boolean = false> {
59
70
  readonly kind: "column";
60
71
  readonly type: SchemaColumnType;
@@ -179,8 +190,13 @@ export declare const column: {
179
190
  precision?: number;
180
191
  scale?: number;
181
192
  }) => ColumnBuilder<string, false, false, false, string | number, "numeric", false>;
182
- json: () => ColumnBuilder<string, false, false, false, string, "json", false>;
183
- jsonb: () => ColumnBuilder<string, false, false, false, string, "jsonb", false>;
193
+ /**
194
+ * JSON text at the JavaScript boundary. Optionally declare the document's shape —
195
+ * `column.json<{ name: string }>()` — for adapters to type `->`/`->>` traversal; the runtime
196
+ * value stays JSON text either way.
197
+ */
198
+ json: <TShape = never>() => ColumnBuilder<JsonColumnValue<TShape>, false, false, false, string, "json", false>;
199
+ jsonb: <TShape = never>() => ColumnBuilder<JsonColumnValue<TShape>, false, false, false, string, "jsonb", false>;
184
200
  uuid: () => ColumnBuilder<string, false, false, false, string, "uuid", false>;
185
201
  /** A zoneless calendar date, represented publicly as canonical `YYYY-MM-DD` text. */
186
202
  date: () => ColumnBuilder<string, false, false, false, string, "date", false>;
@@ -228,8 +228,17 @@ export const column = {
228
228
  const sqlDomain = validateSqlDomain({ kind: "numeric", ...options }, "numeric column");
229
229
  return createColumn("string", { sqlDomain });
230
230
  },
231
- json: () => createColumn("string", { sqlDomain: { kind: "json" } }),
232
- jsonb: () => createColumn("string", { sqlDomain: { kind: "jsonb" } }),
231
+ /**
232
+ * JSON text at the JavaScript boundary. Optionally declare the document's shape —
233
+ * `column.json<{ name: string }>()` — for adapters to type `->`/`->>` traversal; the runtime
234
+ * value stays JSON text either way.
235
+ */
236
+ json: () => createColumn("string", {
237
+ sqlDomain: { kind: "json" },
238
+ }),
239
+ jsonb: () => createColumn("string", {
240
+ sqlDomain: { kind: "jsonb" },
241
+ }),
233
242
  uuid: () => createColumn("string", { sqlDomain: { kind: "uuid" } }),
234
243
  /** A zoneless calendar date, represented publicly as canonical `YYYY-MM-DD` text. */
235
244
  date: () => createColumn("string", { sqlDomain: { kind: "date" } }),