@minnowdb/core 0.6.5 → 0.6.7

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 (57) hide show
  1. package/dist/block-format/codecs.d.ts +0 -8
  2. package/dist/block-format/codecs.js +1 -1
  3. package/dist/engine/artifact-cache.d.ts +2 -1
  4. package/dist/engine/batch.d.ts +0 -1
  5. package/dist/engine/batch.js +1 -1
  6. package/dist/engine/buffered-writer.js +1 -12
  7. package/dist/engine/byte-estimates.d.ts +11 -0
  8. package/dist/engine/byte-estimates.js +25 -0
  9. package/dist/engine/database.js +58 -34
  10. package/dist/engine/fts.d.ts +0 -1
  11. package/dist/engine/fts.js +1 -1
  12. package/dist/engine/group-index.d.ts +2 -0
  13. package/dist/engine/group-index.js +4 -8
  14. package/dist/engine/join-index.d.ts +0 -1
  15. package/dist/engine/join-index.js +1 -15
  16. package/dist/engine/keyed-live.js +2 -36
  17. package/dist/engine/live-equal.d.ts +7 -0
  18. package/dist/engine/live-equal.js +42 -0
  19. package/dist/engine/optimizer.js +11 -16
  20. package/dist/engine/point-read.d.ts +1 -1
  21. package/dist/engine/point-read.js +3 -2
  22. package/dist/engine/query-cache.js +1 -12
  23. package/dist/engine/query.d.ts +20 -59
  24. package/dist/engine/query.js +286 -54
  25. package/dist/engine/result-wire.d.ts +2 -1
  26. package/dist/engine/schema.d.ts +15 -11
  27. package/dist/engine/schema.js +11 -19
  28. package/dist/engine/sort-keys.d.ts +2 -3
  29. package/dist/engine/sort-keys.js +1 -1
  30. package/dist/engine/sql-domains.d.ts +27 -2
  31. package/dist/engine/sql-domains.js +120 -6
  32. package/dist/engine/sql-json.d.ts +0 -2
  33. package/dist/engine/sql-json.js +1 -1
  34. package/dist/engine/sql-semantics.d.ts +2 -1
  35. package/dist/engine/typed-live.js +3 -41
  36. package/dist/engine/vector.d.ts +7 -7
  37. package/dist/engine/vector.js +11 -30
  38. package/dist/engine/write-block-planner.d.ts +2 -1
  39. package/dist/plan/model.d.ts +22 -0
  40. package/dist/plan/model.js +29 -1
  41. package/dist/storage/indexeddb.js +13 -23
  42. package/dist/storage/opfs/leader.d.ts +0 -4
  43. package/dist/storage/opfs/leader.js +5 -14
  44. package/dist/storage/opfs/snapshot-ledger.d.ts +3 -2
  45. package/dist/storage/toolkit/index.d.ts +1 -1
  46. package/dist/storage/toolkit/record-core.d.ts +0 -1
  47. package/dist/storage/toolkit/record-core.js +3 -19
  48. package/dist/storage/types.d.ts +4 -3
  49. package/dist/storage/types.js +13 -2
  50. package/dist/testing/block-store-conformance.js +28 -0
  51. package/dist/testing/opfs-shim.d.ts +2 -1
  52. package/dist/testing/simulator.js +3 -0
  53. package/dist/worker-protocol/index.d.ts +0 -32
  54. package/dist/worker-protocol/index.js +0 -40
  55. package/package.json +2 -1
  56. package/postgres-feature-profile.json +8 -3
  57. package/sql-feature-matrix.json +71 -11
@@ -1,4 +1,5 @@
1
1
  import { copyDate, dateIsoString, dateMilliseconds, dateUtcDate, dateUtcDay, dateUtcFullYear, dateUtcHours, dateUtcMinutes, dateUtcMonth, dateUtcSeconds, setDateUtcDate, setDateUtcMonth, } from "../date-value.js";
2
+ import { crossJoinPlan } from "../plan/model.js";
2
3
  import { assertWellFormedString, wellFormedUtf8ByteLength } from "../block-format/unicode.js";
3
4
  import { MAX_SQL_NESTING_DEPTH, MAX_SQL_PARAMETERS, MAX_SQL_SCALAR_RESULT_CHARACTERS, MAX_SQL_TEXT_CHARACTERS, MAX_SQL_TOKENS, } from "./cache-limits.js";
4
5
  import { SqlCompileError } from "./errors.js";
@@ -9,7 +10,7 @@ import { stringArgument } from "./sql-semantics.js";
9
10
  import { jsonArrowStep, jsonAtPath, jsonConstructor, jsonIsValid, parseJsonPath, } from "./sql-json.js";
10
11
  import { optimizePlan } from "./optimizer.js";
11
12
  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";
13
+ 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
14
  import { columnarTableFromRows, prepareVectorQuery, } from "./vector.js";
14
15
  /** Domain metadata for an execution path that has no catalog-backed type information. */
15
16
  export function unknownColumnDomains(columns) {
@@ -81,7 +82,7 @@ export const volatileScalarFunctionNames = new Set([
81
82
  * reading, so they are resolved once per execution rather than evaluated per row: every row of
82
83
  * one statement sees one instant, both executors agree, and constant folding leaves them alone.
83
84
  */
84
- export const statementDatetimeNames = new Set([
85
+ const statementDatetimeNames = new Set([
85
86
  "CURRENT_DATE",
86
87
  "CURRENT_TIMESTAMP",
87
88
  "LOCALTIME",
@@ -607,7 +608,7 @@ function assertReplacementResultLength(source, search, replacement) {
607
608
  throw new RangeError(`REPLACE result exceeds ${String(MAX_SQL_SCALAR_RESULT_CHARACTERS)} characters`);
608
609
  }
609
610
  }
610
- export const dateTruncUnits = new Set([
611
+ const dateTruncUnits = new Set([
611
612
  "year",
612
613
  "quarter",
613
614
  "month",
@@ -637,7 +638,7 @@ const intervalUnits = new Map([
637
638
  * rather than converted, because a month is not a fixed number of them: adding one to January 31
638
639
  * has to land on the end of February, which only calendar arithmetic can do.
639
640
  */
640
- export function intervalLiteral(text) {
641
+ function intervalLiteral(text) {
641
642
  let months = 0;
642
643
  let milliseconds = 0;
643
644
  let matched = 0;
@@ -663,7 +664,7 @@ export function intervalLiteral(text) {
663
664
  * does not exist in the target month clamps to that month's last day, which is what both SQLite
664
665
  * and PostgreSQL do with 31 January plus a month.
665
666
  */
666
- export function dateAddValue(value, months, milliseconds) {
667
+ function dateAddValue(value, months, milliseconds) {
667
668
  if (value === null || value === undefined)
668
669
  return null;
669
670
  const calendarDate = isDateDomainValue(value);
@@ -690,7 +691,7 @@ export function dateAddValue(value, months, milliseconds) {
690
691
  ? dateDomainValue(dateIsoString(result).slice(0, 10))
691
692
  : result;
692
693
  }
693
- export function dateTruncValue(unit, value) {
694
+ function dateTruncValue(unit, value) {
694
695
  if (typeof unit !== "string" || !dateTruncUnits.has(unit.toLowerCase())) {
695
696
  throw new TypeError("DATE_TRUNC requires a unit of year, quarter, month, week, day, hour, minute, or second");
696
697
  }
@@ -888,6 +889,7 @@ export function compileQuery(sql, options = {}) {
888
889
  }
889
890
  let compiled;
890
891
  try {
892
+ resolvePlanExactNumericConstants(plan);
891
893
  compiled = options.optimize === false ? plan : optimizePlan(plan);
892
894
  }
893
895
  catch (error) {
@@ -995,7 +997,7 @@ export function compileCheckExpression(sql, name) {
995
997
  if (hasAggregate(expression) || containsWindow(expression) || containsParameter(expression)) {
996
998
  throw new TypeError(`CHECK ${name} takes a row condition over this table's own columns`);
997
999
  }
998
- return expression;
1000
+ return resolveExactNumericConstants(expression);
999
1001
  }
1000
1002
  /**
1001
1003
  * Parses and type-checks one catalog default. PostgreSQL defaults are variable-free scalar
@@ -1529,7 +1531,7 @@ export function blockHasSubqueries(plan) {
1529
1531
  return blockHas(plan);
1530
1532
  }
1531
1533
  /** True when any `?`/`$n` placeholder remains anywhere in the expression tree. */
1532
- export function containsParameter(expression) {
1534
+ function containsParameter(expression) {
1533
1535
  if (expression.kind === "parameter")
1534
1536
  return true;
1535
1537
  if (expression.kind === "subquery" || expression.kind === "exists") {
@@ -1537,7 +1539,7 @@ export function containsParameter(expression) {
1537
1539
  }
1538
1540
  return childExpressions(expression).some(containsParameter);
1539
1541
  }
1540
- export function blockHasParameters(block) {
1542
+ function blockHasParameters(block) {
1541
1543
  if (block.limitParameter !== undefined ||
1542
1544
  block.offsetParameter !== undefined ||
1543
1545
  (block.limitValidationParameters?.length ?? 0) > 0 ||
@@ -2548,6 +2550,9 @@ function createPreparedRowQuery(plan, tables, memory) {
2548
2550
  },
2549
2551
  };
2550
2552
  }
2553
+ // protectText defaults OFF here — the row engine's own tables are already inside the SQL
2554
+ // boundary — but ON in vector.ts's columnarTableFromRows, which ingests caller-owned rows.
2555
+ // A new call site must pick deliberately, not inherit whichever default is nearest.
2551
2556
  function cloneRowTables(tables, protectText = false) {
2552
2557
  return new Map([...tables].map(([name, rows]) => [
2553
2558
  name,
@@ -2991,12 +2996,188 @@ export function expandFtsColumns(plan, searchableColumnsFor) {
2991
2996
  expandBlock(plan);
2992
2997
  return plan;
2993
2998
  }
2999
+ /**
3000
+ * Annotates every `AVG(column)` in this block's expression positions with the argument column's
3001
+ * declared NUMERIC scale, resolved against the given schemas. PostgreSQL floors an AVG's
3002
+ * internal division scale at the summed values' display scale; the canonical NUMERIC encoding
3003
+ * strips trailing fractional zeros, so without the annotation the divide cannot know the digits
3004
+ * a declared scale above its own selection would render, and the display padding would fabricate
3005
+ * zeros where PostgreSQL computes real digits. Runs where the catalog is known, per block —
3006
+ * nested blocks execute through their own schema-aware entry. Copy-on-write: plans without an
3007
+ * AVG pass through untouched, and the input is often the compile cache's own object.
3008
+ */
3009
+ export function annotateAvgArgumentScales(plan, schemas) {
3010
+ const containsAvg = (expression) => (expression.kind === "call" && expression.name === "AVG") ||
3011
+ childExpressions(expression).some(containsAvg);
3012
+ const roots = [];
3013
+ forEachBlockExpression(plan, (expression) => roots.push(expression));
3014
+ if (!roots.some(containsAvg))
3015
+ return plan;
3016
+ plan = clonePlanTree(plan);
3017
+ const sources = [plan.base, ...plan.joins];
3018
+ const declaredScale = (reference) => {
3019
+ const parts = reference.split(".");
3020
+ const matches = parts.length === 2
3021
+ ? sources
3022
+ .filter(({ alias }) => alias === parts[0])
3023
+ .flatMap((source) => (schemas.get(source.table) ?? []).filter(({ name }) => name === parts[1]))
3024
+ : sources.flatMap((source) => (schemas.get(source.table) ?? []).filter(({ name }) => name === parts[0]));
3025
+ const domain = matches.length === 1 ? matches[0]?.sqlDomain : undefined;
3026
+ return domain?.kind === "numeric" ? domain.scale : undefined;
3027
+ };
3028
+ const annotate = (expression) => {
3029
+ if (expression.kind === "call" && expression.name === "AVG") {
3030
+ const argument = expression.arguments[0];
3031
+ if (argument?.kind === "column") {
3032
+ const scale = declaredScale(argument.reference);
3033
+ if (scale !== undefined && scale > 0)
3034
+ expression.avgArgumentScale = scale;
3035
+ }
3036
+ }
3037
+ for (const child of childExpressions(expression))
3038
+ annotate(child);
3039
+ };
3040
+ forEachBlockExpression(plan, annotate);
3041
+ return plan;
3042
+ }
3043
+ /**
3044
+ * The exact fold of a constant arithmetic subtree, or undefined where none applies. PostgreSQL
3045
+ * types every decimal constant — and every integer constant too large for its integer types —
3046
+ * as NUMERIC, so arithmetic among constants happens in exact decimal space before any float8
3047
+ * context sees the result: `0.1 + 0.2` is exactly 0.3. A subtree folds only when it is
3048
+ * "seeded" by such a constant (one carrying exact digits); pure safe-integer arithmetic keeps
3049
+ * its historical Float64 evaluation, including non-truncating division.
3050
+ */
3051
+ function exactConstantFold(expression) {
3052
+ if (expression.kind === "literal") {
3053
+ if (expression.exactText !== undefined) {
3054
+ return { value: exactNumericLiteral(expression.exactText), seeded: true };
3055
+ }
3056
+ const value = expression.value;
3057
+ if (typeof value === "string" && expression.sqlDomain?.kind === "numeric") {
3058
+ return isExactNumeric(value) ? { value, seeded: true } : undefined;
3059
+ }
3060
+ if (typeof value === "number" && Number.isFinite(value))
3061
+ return { value, seeded: false };
3062
+ if (value === null)
3063
+ return { value: null, seeded: false };
3064
+ return undefined;
3065
+ }
3066
+ if (expression.kind !== "binary" || expression.operator === "||")
3067
+ return undefined;
3068
+ const left = exactConstantFold(expression.left);
3069
+ if (left === undefined)
3070
+ return undefined;
3071
+ const right = exactConstantFold(expression.right);
3072
+ if (right === undefined)
3073
+ return undefined;
3074
+ const seeded = left.seeded || right.seeded;
3075
+ if (left.value === null || right.value === null)
3076
+ return { value: null, seeded };
3077
+ const folded = exactNumericBinary(expression.operator, left.value, right.value, 0, false);
3078
+ return folded === undefined ? undefined : { value: folded, seeded };
3079
+ }
3080
+ /**
3081
+ * Rewrites every seeded constant arithmetic subtree of this expression to its exact fold. A
3082
+ * folded value demotes back to an ordinary number literal when it reads back identically from
3083
+ * a Float64 — PostgreSQL's own cast when a numeric constant meets a float8 operand, and it
3084
+ * keeps every number-typed fast path — and stays a tagged exact-NUMERIC literal when the
3085
+ * number boundary would visibly round it. A fold that overflows its bounds is left unfolded
3086
+ * for execution to report.
3087
+ */
3088
+ function resolveExactNumericConstants(expression) {
3089
+ let folded;
3090
+ try {
3091
+ folded = exactConstantFold(expression);
3092
+ }
3093
+ catch {
3094
+ folded = undefined;
3095
+ }
3096
+ if (folded?.seeded === true) {
3097
+ const value = folded.value;
3098
+ if (value === null || typeof value === "number")
3099
+ return { kind: "literal", value };
3100
+ const fits = exactNumericAsNumber(value);
3101
+ if (fits !== undefined)
3102
+ return { kind: "literal", value: fits };
3103
+ return { kind: "literal", value, internalSqlValue: true, sqlDomain: { kind: "numeric" } };
3104
+ }
3105
+ if (expression.kind === "subquery" || expression.kind === "exists") {
3106
+ resolvePlanExactNumericConstants(expression.block);
3107
+ return expression;
3108
+ }
3109
+ if (expression.kind === "window") {
3110
+ return {
3111
+ ...expression,
3112
+ partitionBy: expression.partitionBy.map(resolveExactNumericConstants),
3113
+ orderBy: expression.orderBy.map((order) => ({
3114
+ ...order,
3115
+ expression: resolveExactNumericConstants(order.expression),
3116
+ })),
3117
+ ...(expression.argument === undefined
3118
+ ? {}
3119
+ : { argument: resolveExactNumericConstants(expression.argument) }),
3120
+ };
3121
+ }
3122
+ return mapChildExpressions(expression, resolveExactNumericConstants);
3123
+ }
3124
+ /** Applies `resolveExactNumericConstants` to every expression of a plan, nested blocks included. */
3125
+ function resolvePlanExactNumericConstants(plan) {
3126
+ mapBlockExpressions(plan, resolveExactNumericConstants);
3127
+ forEachNestedBlock(plan, resolvePlanExactNumericConstants);
3128
+ }
3129
+ /**
3130
+ * The statement counterpart of `resolvePlanExactNumericConstants`: every expression slot a
3131
+ * mutation evaluates later. INSERT VALUES cells are absent because the parser evaluates them
3132
+ * to values on the spot, resolving each expression first.
3133
+ */
3134
+ function resolveStatementExactNumericConstants(statement) {
3135
+ const resolve = resolveExactNumericConstants;
3136
+ if (statement.kind === "insert") {
3137
+ const conflict = statement.onConflict;
3138
+ if (conflict?.assignments !== undefined) {
3139
+ for (const assignment of conflict.assignments) {
3140
+ assignment.expression = resolve(assignment.expression);
3141
+ }
3142
+ }
3143
+ if (conflict?.where !== undefined)
3144
+ conflict.where = resolve(conflict.where);
3145
+ return;
3146
+ }
3147
+ if (statement.kind === "update" || statement.kind === "delete") {
3148
+ if (statement.kind === "update") {
3149
+ for (const assignment of statement.assignments) {
3150
+ assignment.expression = resolve(assignment.expression);
3151
+ }
3152
+ }
3153
+ for (const predicate of statement.predicates) {
3154
+ predicate.left = resolve(predicate.left);
3155
+ predicate.right = resolve(predicate.right);
3156
+ }
3157
+ return;
3158
+ }
3159
+ if (statement.kind === "merge") {
3160
+ statement.on = resolve(statement.on);
3161
+ for (const branch of statement.branches) {
3162
+ if (branch.condition !== undefined)
3163
+ branch.condition = resolve(branch.condition);
3164
+ if (branch.action.kind === "update") {
3165
+ for (const assignment of branch.action.assignments) {
3166
+ assignment.expression = resolve(assignment.expression);
3167
+ }
3168
+ }
3169
+ if (branch.action.kind === "insert") {
3170
+ branch.action.values = branch.action.values.map(resolve);
3171
+ }
3172
+ }
3173
+ }
3174
+ }
2994
3175
  /**
2995
3176
  * Aggregates the frame members named by position, for the frames whose rows are not contiguous.
2996
3177
  * EXCLUDE puts a hole in the middle of a frame, which the prefix sums the common path uses
2997
3178
  * cannot represent, so those positions walk their members instead.
2998
3179
  */
2999
- function aggregateWindowMembers(window, values, members) {
3180
+ function aggregateWindowMembers(window, values, members, avgScale) {
3000
3181
  const present = members.filter((member) => {
3001
3182
  const value = values[member];
3002
3183
  return value !== null && value !== undefined;
@@ -3026,7 +3207,9 @@ function aggregateWindowMembers(window, values, members) {
3026
3207
  throw new Error("Exact NUMERIC sum disappeared");
3027
3208
  total = next;
3028
3209
  }
3029
- return window.name === "SUM" ? total : exactNumericBinary("/", total, present.length);
3210
+ return window.name === "SUM"
3211
+ ? total
3212
+ : exactNumericBinary("/", total, present.length, avgScale);
3030
3213
  }
3031
3214
  const total = present.reduce((sum, member) => sum + numeric(values[member]), 0);
3032
3215
  return window.name === "SUM" ? total : total / present.length;
@@ -3051,7 +3234,7 @@ function aggregateWindowMembers(window, values, members) {
3051
3234
  * RANGE frames take only UNBOUNDED and CURRENT ROW bounds, where CURRENT ROW spans the peer
3052
3235
  * group. COUNT of an empty frame is 0 and every other aggregate NULL.
3053
3236
  */
3054
- function applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKeys) {
3237
+ function applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKeys, avgScale) {
3055
3238
  const frame = window.frame ?? {
3056
3239
  unit: "range",
3057
3240
  start: { kind: "unbounded-preceding" },
@@ -3063,11 +3246,11 @@ function applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKey
3063
3246
  while (end < indexes.length && samePartition(indexes[start] ?? 0, indexes[end] ?? 0)) {
3064
3247
  end += 1;
3065
3248
  }
3066
- applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end);
3249
+ applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end, avgScale);
3067
3250
  start = end;
3068
3251
  }
3069
3252
  }
3070
- function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end) {
3253
+ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKeys, start, end, avgScale) {
3071
3254
  const size = end - start;
3072
3255
  // Peer-group bounds per position; with no OVER ordering the whole partition is one peer group.
3073
3256
  const peerStart = new Array(size).fill(0);
@@ -3198,7 +3381,7 @@ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKe
3198
3381
  if (!dropped)
3199
3382
  members.push(member);
3200
3383
  }
3201
- value = aggregateWindowMembers(window, values, members);
3384
+ value = aggregateWindowMembers(window, values, members, avgScale);
3202
3385
  const row = rows[indexes[start + position] ?? -1];
3203
3386
  if (row !== undefined)
3204
3387
  row[window.alias] = asQueryValue(value);
@@ -3230,7 +3413,7 @@ function applyAggregateWindowPartition(rows, indexes, window, frame, sameOrderKe
3230
3413
  }
3231
3414
  else if (prefixExact !== undefined) {
3232
3415
  const total = exactNumericBinary("-", prefixExact[high] ?? exactZero, prefixExact[low] ?? exactZero);
3233
- value = window.name === "SUM" ? total : exactNumericBinary("/", total, nonNull);
3416
+ value = window.name === "SUM" ? total : exactNumericBinary("/", total, nonNull, avgScale);
3234
3417
  }
3235
3418
  else {
3236
3419
  const total = (prefixSum?.[high] ?? 0) - (prefixSum?.[low] ?? 0);
@@ -3407,7 +3590,13 @@ export function applyWindowFunctions(result, windows, options = {}) {
3407
3590
  continue;
3408
3591
  }
3409
3592
  if (window.name !== "ROW_NUMBER" && window.name !== "RANK" && window.name !== "DENSE_RANK") {
3410
- applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKeys);
3593
+ // PostgreSQL computes a window AVG's quotient to at least the summed values' display
3594
+ // scale; canonical NUMERIC values no longer carry it, so read the argument column's
3595
+ // declared scale from the inner result the way the summed dscale would have carried it.
3596
+ const argumentDomain = window.name === "AVG" && window.argumentAlias !== undefined
3597
+ ? result.columnDomains[result.columns.indexOf(window.argumentAlias)]
3598
+ : undefined;
3599
+ applyAggregateWindow(rows, indexes, window, samePartition, sameOrderKeys, argumentDomain?.kind === "numeric" ? argumentDomain.scale : undefined);
3411
3600
  continue;
3412
3601
  }
3413
3602
  let rowNumber = 0;
@@ -3819,7 +4008,7 @@ function evaluate(expression, context, group) {
3819
4008
  if (typeof left !== "string" || typeof right !== "string") {
3820
4009
  throw new TypeError("|| requires string operands");
3821
4010
  }
3822
- return protectedSqlTextValue(String(externalSqlDomainValue(left)) + String(externalSqlDomainValue(right)));
4011
+ return concatenatedSqlValue(left, right);
3823
4012
  }
3824
4013
  const exact = exactNumericBinary(expression.operator, left, right);
3825
4014
  if (exact !== undefined)
@@ -3963,7 +4152,8 @@ function evaluate(expression, context, group) {
3963
4152
  ? null
3964
4153
  : (() => {
3965
4154
  const sum = sumNumericValues(values);
3966
- return exactNumericBinary("/", sum, values.length) ?? numeric(sum) / values.length;
4155
+ return (exactNumericBinary("/", sum, values.length, expression.avgArgumentScale) ??
4156
+ numeric(sum) / values.length);
3967
4157
  })();
3968
4158
  if (expression.name === "MIN")
3969
4159
  return values.reduce((best, value) => (best === undefined || compareValues(value, best) < 0 ? value : best), undefined);
@@ -4416,6 +4606,11 @@ export function hasAggregate(expression) {
4416
4606
  }
4417
4607
  return childExpressions(expression).some(hasAggregate);
4418
4608
  }
4609
+ function hasWindow(expression) {
4610
+ if (expression.kind === "window")
4611
+ return true;
4612
+ return childExpressions(expression).some(hasWindow);
4613
+ }
4419
4614
  /** One root aggregate call, using the same canonical set as parsing and execution. */
4420
4615
  export function isAggregateCall(expression) {
4421
4616
  return expression.kind === "call" && aggregateNames.has(expression.name);
@@ -5308,6 +5503,14 @@ class Parser {
5308
5503
  },
5309
5504
  };
5310
5505
  }
5506
+ /** A type name's precision, scale, or width: plain digits, as PostgreSQL requires there. */
5507
+ #typeWidth() {
5508
+ const token = this.#take("number");
5509
+ if (!/^\d+$/.test(token.text)) {
5510
+ throw new SqlCompileError(`A type width must be an integer: ${token.text}`, token.start, token.end - token.start);
5511
+ }
5512
+ return Number(token.text);
5513
+ }
5311
5514
  /** A CAST target: the SqlColumnType, or "number-integer" for the truncating integer names. */
5312
5515
  #castTarget() {
5313
5516
  const word = this.#identifier().toUpperCase();
@@ -5315,9 +5518,9 @@ class Parser {
5315
5518
  let precision;
5316
5519
  let scale;
5317
5520
  if (this.#punctuation("(")) {
5318
- precision = Number(this.#take("number").text);
5521
+ precision = this.#typeWidth();
5319
5522
  if (this.#punctuation(","))
5320
- scale = Number(this.#take("number").text);
5523
+ scale = this.#typeWidth();
5321
5524
  else
5322
5525
  scale = 0;
5323
5526
  this.#expectPunctuation(")");
@@ -5335,9 +5538,9 @@ class Parser {
5335
5538
  if (mapped === undefined)
5336
5539
  throw new TypeError(`Unsupported CAST target: ${word}`);
5337
5540
  if (this.#punctuation("(")) {
5338
- this.#take("number");
5541
+ this.#typeWidth();
5339
5542
  if (this.#punctuation(","))
5340
- this.#take("number");
5543
+ this.#typeWidth();
5341
5544
  this.#expectPunctuation(")");
5342
5545
  }
5343
5546
  const integer = word === "INTEGER" || word === "INT" || word === "BIGINT" || word === "SMALLINT";
@@ -5350,8 +5553,8 @@ class Parser {
5350
5553
  let precision;
5351
5554
  let scale;
5352
5555
  if (this.#punctuation("(")) {
5353
- precision = Number(this.#take("number").text);
5354
- scale = this.#punctuation(",") ? Number(this.#take("number").text) : 0;
5556
+ precision = this.#typeWidth();
5557
+ scale = this.#punctuation(",") ? this.#typeWidth() : 0;
5355
5558
  this.#expectPunctuation(")");
5356
5559
  }
5357
5560
  return {
@@ -5376,7 +5579,7 @@ class Parser {
5376
5579
  }
5377
5580
  // Character widths document intent and do not truncate values.
5378
5581
  if (this.#punctuation("(")) {
5379
- this.#take("number");
5582
+ this.#typeWidth();
5380
5583
  if (this.#punctuation(",")) {
5381
5584
  throw new TypeError(`${word} takes one width`);
5382
5585
  }
@@ -5396,6 +5599,7 @@ class Parser {
5396
5599
  ? this.#updateStatement()
5397
5600
  : this.#deleteStatement();
5398
5601
  this.#take("eof");
5602
+ resolveStatementExactNumericConstants(statement);
5399
5603
  return statement;
5400
5604
  }
5401
5605
  #insertStatement() {
@@ -5428,7 +5632,9 @@ class Parser {
5428
5632
  };
5429
5633
  }
5430
5634
  if (this.#isKeyword("SELECT")) {
5431
- const query = optimizePlan(this.#selectBlock("(insert select)"));
5635
+ const insertSource = this.#selectBlock("(insert select)");
5636
+ resolvePlanExactNumericConstants(insertSource);
5637
+ const query = optimizePlan(insertSource);
5432
5638
  if (query.select.some((item) => item.expression.kind === "wildcard")) {
5433
5639
  throw new TypeError("INSERT ... SELECT requires an explicit select list");
5434
5640
  }
@@ -5691,7 +5897,9 @@ class Parser {
5691
5897
  throw new TypeError("MERGE does not support RETURNING; read the rows back with a SELECT");
5692
5898
  }
5693
5899
  this.#take("eof");
5694
- return { kind: "merge", table, alias, source, on, branches };
5900
+ const statement = { kind: "merge", table, alias, source, on, branches };
5901
+ resolveStatementExactNumericConstants(statement);
5902
+ return statement;
5695
5903
  }
5696
5904
  #updateStatement() {
5697
5905
  this.#keyword("UPDATE");
@@ -5751,7 +5959,7 @@ class Parser {
5751
5959
  if (hasAggregate(expression) || expressionColumns(expression).length > 0) {
5752
5960
  throw new TypeError(`${label} must be constant expressions`);
5753
5961
  }
5754
- return asQueryValue(evaluate(expression, {}));
5962
+ return asQueryValue(evaluate(resolveExactNumericConstants(expression), {}));
5755
5963
  }
5756
5964
  #unionMember(sql) {
5757
5965
  if (this.#punctuation("(")) {
@@ -5959,19 +6167,6 @@ class Parser {
5959
6167
  }
5960
6168
  const joins = [];
5961
6169
  let rightJoins = 0;
5962
- /** A cross join: the nested-loop path with a condition every row pair satisfies. */
5963
- const crossJoin = (source) => ({
5964
- ...source,
5965
- kind: "inner",
5966
- left: { kind: "literal", value: null },
5967
- right: { kind: "literal", value: null },
5968
- on: {
5969
- kind: "condition",
5970
- operator: "=",
5971
- left: { kind: "literal", value: 1 },
5972
- right: { kind: "literal", value: 1 },
5973
- },
5974
- });
5975
6170
  while (this.#peek().text === "," ||
5976
6171
  this.#isKeyword("NATURAL") ||
5977
6172
  this.#isKeyword("JOIN") ||
@@ -5982,13 +6177,13 @@ class Parser {
5982
6177
  this.#isKeyword("CROSS")) {
5983
6178
  if (this.#punctuation(",")) {
5984
6179
  // F041-07: a comma between table references is a cross join.
5985
- joins.push(crossJoin(this.#source()));
6180
+ joins.push(crossJoinPlan(this.#source()));
5986
6181
  continue;
5987
6182
  }
5988
6183
  if (this.#isKeyword("CROSS")) {
5989
6184
  this.#keyword("CROSS");
5990
6185
  this.#keyword("JOIN");
5991
- joins.push(crossJoin(this.#source()));
6186
+ joins.push(crossJoinPlan(this.#source()));
5992
6187
  continue;
5993
6188
  }
5994
6189
  let kind = "inner";
@@ -6932,6 +7127,11 @@ class Parser {
6932
7127
  continue;
6933
7128
  }
6934
7129
  const operator = this.#peek().text;
7130
+ // A bracket after a complete expression is PostgreSQL's array subscript, which has no
7131
+ // other reading in this grammar — name the missing feature instead of "Expected eof".
7132
+ if (operator === "[") {
7133
+ throw new TypeError("Array subscripts are not supported");
7134
+ }
6935
7135
  // || and the JSON arrows share PostgreSQL's loosest "any other operator" level, applying
6936
7136
  // left-to-right to whole arithmetic terms: `'a' || d ->> 'k'` is `('a' || d) ->> 'k'`.
6937
7137
  const precedence = operator === "*" || operator === "/" || operator === "%"
@@ -7021,10 +7221,29 @@ class Parser {
7021
7221
  if (token.kind === "number") {
7022
7222
  this.#index += 1;
7023
7223
  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);
7224
+ // A safe integer spelled as one is exactly itself; every other spelling — a decimal
7225
+ // point, an exponent, or digits beyond 2^53 is a numeric constant PostgreSQL would
7226
+ // type NUMERIC. Keep its exact digits: as an annotation when the value reads back
7227
+ // identically from a number, or as a tagged exact-NUMERIC literal when it would not.
7228
+ if (!/[.eE]/.test(token.text) && Number.isSafeInteger(value)) {
7229
+ return { kind: "literal", value };
7230
+ }
7231
+ let tagged;
7232
+ try {
7233
+ tagged = exactNumericLiteral(token.text);
7026
7234
  }
7027
- return { kind: "literal", value };
7235
+ catch (error) {
7236
+ throw new SqlCompileError(error instanceof Error ? error.message : `Invalid number: ${token.text}`, token.start, token.end - token.start);
7237
+ }
7238
+ const fits = exactNumericAsNumber(tagged);
7239
+ if (fits !== undefined)
7240
+ return { kind: "literal", value: fits, exactText: token.text };
7241
+ return {
7242
+ kind: "literal",
7243
+ value: tagged,
7244
+ internalSqlValue: true,
7245
+ sqlDomain: { kind: "numeric" },
7246
+ };
7028
7247
  }
7029
7248
  if (token.kind === "string") {
7030
7249
  this.#index += 1;
@@ -8115,6 +8334,8 @@ export function assembleSelectBlock(parts, nextSequence) {
8115
8334
  if (distinct) {
8116
8335
  if (select.some((item) => hasAggregate(item.expression)))
8117
8336
  throw new TypeError("SELECT DISTINCT cannot be combined with aggregate functions");
8337
+ if (select.some((item) => hasWindow(item.expression)))
8338
+ throw new TypeError("SELECT DISTINCT cannot be combined with window functions");
8118
8339
  if (groupBy.length > 0)
8119
8340
  throw new TypeError("SELECT DISTINCT cannot be combined with GROUP BY");
8120
8341
  if (having.length > 0)
@@ -8193,7 +8414,7 @@ export function assembleSelectBlock(parts, nextSequence) {
8193
8414
  * over those same columns provides the deduplication through the grouped executor. Runs
8194
8415
  * exactly once per execution entry, like MATCH(*) expansion.
8195
8416
  */
8196
- export function expandDistinctWildcard(plan, columnsOf) {
8417
+ function expandDistinctWildcard(plan, columnsOf) {
8197
8418
  if (plan.distinctWildcard !== true)
8198
8419
  return plan;
8199
8420
  const shaped = [plan.base, ...plan.joins].map((source) => ({
@@ -8285,7 +8506,7 @@ export function planHasSourceColumnAliases(plan) {
8285
8506
  * scan cannot know whether the next row ties — and the ordered result is trimmed here: rows up
8286
8507
  * to the limit, plus every following row equal to the last one on all ORDER BY columns.
8287
8508
  */
8288
- export function withTiesPlan(plan) {
8509
+ function withTiesPlan(plan) {
8289
8510
  // Ordering by an expression or an unselected column wraps the real block in a projection that
8290
8511
  // hides the sort column. The tie test needs that column, so the inner block runs and the
8291
8512
  // projection is applied after trimming instead of before.
@@ -8472,7 +8693,7 @@ export function expandSourceColumnAliases(plan, columnsOf) {
8472
8693
  * one source, and `alias.column` when it reads several, so two sources cannot collide.
8473
8694
  * Runs once per execution entry, like MATCH(*) and DISTINCT * expansion.
8474
8695
  */
8475
- export function expandQualifiedWildcards(plan, columnsOf) {
8696
+ function expandQualifiedWildcards(plan, columnsOf) {
8476
8697
  const qualified = (block) => {
8477
8698
  if (block.select.some((item) => item.expression.kind === "wildcard" && item.expression.table))
8478
8699
  return true;
@@ -8748,7 +8969,7 @@ function jsonTableColumnValue(column, value) {
8748
8969
  }
8749
8970
  return date;
8750
8971
  }
8751
- export function timestampLiteral(text) {
8972
+ function timestampLiteral(text) {
8752
8973
  const trimmed = text.trim();
8753
8974
  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
8975
  if (match === null)
@@ -8922,13 +9143,14 @@ function defaultAlias(expression) {
8922
9143
  return "expression";
8923
9144
  }
8924
9145
  /**
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).
9146
+ * Whether a numeric literal's digits are well formed: at most one decimal point and an optional
9147
+ * exponent (radix 10 only), and underscores only between digits, never leading, trailing, or
9148
+ * doubled (T662). Exponent digits are plain, as in PostgreSQL.
8927
9149
  */
8928
9150
  function validNumericLiteral(text, radix) {
8929
9151
  const digits = radix === 16 ? "0-9a-fA-F" : radix === 8 ? "0-7" : radix === 2 ? "01" : "0-9";
8930
9152
  const group = `[${digits}]+(?:_[${digits}]+)*`;
8931
- const pattern = radix === 10 ? `^${group}(?:\\.${group})?$` : `^${group}$`;
9153
+ const pattern = radix === 10 ? `^${group}(?:\\.${group})?(?:[eE][+-]?\\d+)?$` : `^${group}$`;
8932
9154
  return new RegExp(pattern).test(text);
8933
9155
  }
8934
9156
  function tokenize(sql) {
@@ -8986,6 +9208,16 @@ function tokenize(sql) {
8986
9208
  // T662: underscores may separate digits.
8987
9209
  while (index < sql.length && /[\d._]/.test(sql[index] ?? ""))
8988
9210
  index += 1;
9211
+ // Scientific notation: an exponent marker joins the token only when digits follow, so
9212
+ // `1e2` is the numeric constant 100 while `SELECT 1 e` still reads `e` as an alias.
9213
+ if (/[eE]/.test(sql[index] ?? "")) {
9214
+ const signed = /[+-]/.test(sql[index + 1] ?? "") ? 1 : 0;
9215
+ if (/\d/.test(sql[index + 1 + signed] ?? "")) {
9216
+ index += 2 + signed;
9217
+ while (index < sql.length && /\d/.test(sql[index] ?? ""))
9218
+ index += 1;
9219
+ }
9220
+ }
8989
9221
  const text = sql.slice(start, index);
8990
9222
  if (!validNumericLiteral(text, 10))
8991
9223
  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 {};