@minnowdb/core 0.7.2 → 0.7.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.
@@ -7,7 +7,18 @@ import type { SqlDomain } from "../storage/types.js";
7
7
  export declare function protectedSqlTextValue(value: string): string;
8
8
  /** Removes only the ordinary-TEXT wrapper, leaving real domain values tagged. */
9
9
  export declare function externalSqlTextValue(value: unknown): unknown;
10
+ /** The fractional digits a finite number shows when written as a decimal: 1.25 has 2, 8 has 0. */
11
+ export declare function decimalScaleOfNumber(value: number): number;
10
12
  export declare function isExactNumeric(value: unknown): value is string;
13
+ /**
14
+ * PostgreSQL's numeric ROUND and TRUNC at `digits` fractional places, which may be negative to
15
+ * work left of the decimal point (`ROUND(12345.67, -1)` is 12350). ROUND is half away from
16
+ * zero, TRUNC toward zero. The result is canonical; a caller wanting display scale reads it
17
+ * from the inferred column domain.
18
+ */
19
+ export declare function exactNumericRounded(value: string, digits: number, mode: "round" | "trunc"): string;
20
+ /** ABS, FLOOR, CEIL, and SIGN over an exact NUMERIC value, as PostgreSQL's numeric variants. */
21
+ export declare function exactNumericUnary(name: "ABS" | "FLOOR" | "CEIL" | "SIGN", value: string): string;
11
22
  export declare function exactNumericValue(value: unknown, precision?: number, scale?: number): string | null;
12
23
  /**
13
24
  * Tags a SQL numeric constant with its exact digits, as written. Unlike `exactNumericValue`
@@ -87,9 +87,55 @@ function taggedDecimalParts(value) {
87
87
  return void 0;
88
88
  return decimalParts(value.slice(NUMERIC.length));
89
89
  }
90
+ function decimalScaleOfNumber(value) {
91
+ return normalizeDecimal(decimalParts(value)).scale;
92
+ }
90
93
  function isExactNumeric(value) {
91
94
  return taggedDecimalParts(value) !== void 0;
92
95
  }
96
+ function exactNumericRounded(value, digits, mode) {
97
+ const parts = taggedDecimalParts(value);
98
+ if (parts === void 0)
99
+ throw new TypeError("ROUND and TRUNC take an exact NUMERIC value");
100
+ if (!Number.isSafeInteger(digits) || digits > 1e5 || digits < -1e5) {
101
+ throw new RangeError(`NUMERIC scale is outside the supported range: ${String(digits)}`);
102
+ }
103
+ let result = parts;
104
+ if (parts.scale > digits) {
105
+ const divisor = pow10(parts.scale - digits);
106
+ let quotient = parts.coefficient / divisor;
107
+ if (mode === "round") {
108
+ const remainder = parts.coefficient % divisor;
109
+ if ((remainder < 0n ? -remainder : remainder) * 2n >= divisor) {
110
+ quotient += parts.coefficient < 0n ? -1n : 1n;
111
+ }
112
+ }
113
+ result = digits >= 0 ? { coefficient: quotient, scale: digits } : { coefficient: quotient * pow10(-digits), scale: 0 };
114
+ }
115
+ return boundedTaggedDomainValue(NUMERIC, formatDecimal(normalizeDecimal(result)), "NUMERIC result");
116
+ }
117
+ function exactNumericUnary(name, value) {
118
+ const parts = taggedDecimalParts(value);
119
+ if (parts === void 0)
120
+ throw new TypeError(`${name} takes an exact NUMERIC value`);
121
+ const { coefficient, scale } = parts;
122
+ let result;
123
+ if (name === "ABS") {
124
+ result = { coefficient: coefficient < 0n ? -coefficient : coefficient, scale };
125
+ } else if (name === "SIGN") {
126
+ result = { coefficient: coefficient === 0n ? 0n : coefficient < 0n ? -1n : 1n, scale: 0 };
127
+ } else {
128
+ const divisor = pow10(scale);
129
+ let quotient = coefficient / divisor;
130
+ const remainder = coefficient % divisor;
131
+ if (name === "FLOOR" && remainder < 0n)
132
+ quotient -= 1n;
133
+ if (name === "CEIL" && remainder > 0n)
134
+ quotient += 1n;
135
+ result = { coefficient: quotient, scale: 0 };
136
+ }
137
+ return boundedTaggedDomainValue(NUMERIC, formatDecimal(normalizeDecimal(result)), "NUMERIC result");
138
+ }
93
139
  function exactNumericValue(value, precision, scale) {
94
140
  if (value === null || value === void 0)
95
141
  return null;
@@ -582,6 +628,13 @@ function collatorFor(locale, displayName) {
582
628
  return created;
583
629
  }
584
630
  function externalSqlDomainColumnValue(value, domain) {
631
+ if (domain?.kind === "numeric" && typeof value === "number" && Number.isFinite(value)) {
632
+ try {
633
+ return externalSqlDomainValue(exactNumericValue(value));
634
+ } catch {
635
+ return value;
636
+ }
637
+ }
585
638
  if (domain?.kind === "numeric" && domain.scale !== void 0 && domain.scale > 0 && typeof value === "string" && value.startsWith(NUMERIC)) {
586
639
  const text = value.slice(NUMERIC.length);
587
640
  const dot = text.indexOf(".");
@@ -676,11 +729,14 @@ export {
676
729
  collatedDomainValue,
677
730
  concatenatedSqlValue,
678
731
  dateDomainValue,
732
+ decimalScaleOfNumber,
679
733
  enumDomainCompare,
680
734
  exactNumericAsNumber,
681
735
  exactNumericBinary,
682
736
  exactNumericCompare,
683
737
  exactNumericLiteral,
738
+ exactNumericRounded,
739
+ exactNumericUnary,
684
740
  exactNumericValue,
685
741
  externalSqlDomainColumnValue,
686
742
  externalSqlDomainValue,
@@ -1,6 +1,6 @@
1
1
  import { dateIsoString, dateMilliseconds, dateUtcDate, dateUtcDay, dateUtcFullYear, dateUtcHours, dateUtcMinutes, dateUtcMonth, dateUtcSeconds } from "../date-value.js";
2
2
  import { MAX_SQL_SCALAR_RESULT_CHARACTERS } from "./cache-limits.js";
3
- import { dateDomainValue, externalSqlDomainValue, intervalDomainValue, isDateDomainValue, protectedSqlTextValue } from "./sql-domains.js";
3
+ import { dateDomainValue, exactNumericRounded, exactNumericUnary, externalSqlDomainValue, intervalDomainValue, isDateDomainValue, isExactNumeric, protectedSqlTextValue } from "./sql-domains.js";
4
4
  import { compileRegexPattern, parseSqlTimestampText, stringArgument } from "./sql-semantics.js";
5
5
  function text(name, value) {
6
6
  const source = stringArgument(name, value);
@@ -561,6 +561,12 @@ function ageInterval(later, earlier) {
561
561
  }
562
562
  return intervalDomainValue(`${String(sign * months)} months ${String(sign * days)} days ${String(sign * milliseconds / 1e3)} seconds`);
563
563
  }
564
+ function integerArgument(name, value) {
565
+ if (typeof value !== "number" || !Number.isSafeInteger(value)) {
566
+ throw new TypeError(`${name} requires integers`);
567
+ }
568
+ return value;
569
+ }
564
570
  function nullish(value) {
565
571
  return value === null || value === void 0;
566
572
  }
@@ -782,6 +788,124 @@ const simpleScalarFunctions = /* @__PURE__ */ new Map([
782
788
  }
783
789
  }
784
790
  ],
791
+ [
792
+ "REGEXP_SUBSTR",
793
+ {
794
+ minArgs: 2,
795
+ maxArgs: 3,
796
+ returns: "string",
797
+ evaluate: (values) => {
798
+ const expression = compileRegexPattern(text("SUBSTRING", values[1]), regexFlags("SUBSTRING", values[2]));
799
+ const match = expression.exec(text("SUBSTRING", values[0]));
800
+ if (match === null)
801
+ return null;
802
+ const group = match.length > 1 ? match[1] : match[0];
803
+ return group === void 0 ? null : bounded(group, "SUBSTRING");
804
+ }
805
+ }
806
+ ],
807
+ [
808
+ "NUM_NONNULLS",
809
+ {
810
+ minArgs: 1,
811
+ maxArgs: Number.POSITIVE_INFINITY,
812
+ returns: "number",
813
+ nullOnNull: false,
814
+ evaluate: (values) => values.filter((value) => !nullish(value)).length
815
+ }
816
+ ],
817
+ [
818
+ "NUM_NULLS",
819
+ {
820
+ minArgs: 1,
821
+ maxArgs: Number.POSITIVE_INFINITY,
822
+ returns: "number",
823
+ nullOnNull: false,
824
+ evaluate: (values) => values.filter((value) => nullish(value)).length
825
+ }
826
+ ],
827
+ [
828
+ "GCD",
829
+ {
830
+ minArgs: 2,
831
+ maxArgs: 2,
832
+ returns: "number",
833
+ evaluate: (values) => {
834
+ let a = Math.abs(integerArgument("GCD", values[0]));
835
+ let b = Math.abs(integerArgument("GCD", values[1]));
836
+ while (b !== 0)
837
+ [a, b] = [b, a % b];
838
+ return a;
839
+ }
840
+ }
841
+ ],
842
+ [
843
+ "LCM",
844
+ {
845
+ minArgs: 2,
846
+ maxArgs: 2,
847
+ returns: "number",
848
+ evaluate: (values) => {
849
+ const a = Math.abs(integerArgument("LCM", values[0]));
850
+ const b = Math.abs(integerArgument("LCM", values[1]));
851
+ if (a === 0 || b === 0)
852
+ return 0;
853
+ let x = a;
854
+ let y = b;
855
+ while (y !== 0)
856
+ [x, y] = [y, x % y];
857
+ const result = a / x * b;
858
+ if (!Number.isSafeInteger(result))
859
+ throw new RangeError("LCM result is out of range");
860
+ return result;
861
+ }
862
+ }
863
+ ],
864
+ [
865
+ "TO_HEX",
866
+ {
867
+ minArgs: 1,
868
+ maxArgs: 1,
869
+ returns: "string",
870
+ evaluate: (values) => {
871
+ const value = values[0];
872
+ if (typeof value !== "number" || !Number.isSafeInteger(value)) {
873
+ throw new TypeError("TO_HEX requires an integer");
874
+ }
875
+ if (value < 0) {
876
+ const width = value >= -2147483648 ? 32 : 64;
877
+ return BigInt.asUintN(width, BigInt(value)).toString(16);
878
+ }
879
+ return BigInt(value).toString(16);
880
+ }
881
+ }
882
+ ],
883
+ [
884
+ "QUOTE_LITERAL",
885
+ {
886
+ minArgs: 1,
887
+ maxArgs: 1,
888
+ returns: "string",
889
+ evaluate: (values) => {
890
+ const value = values[0];
891
+ const rendered2 = typeof value === "string" ? value : typeof value === "number" ? String(value) : String(value);
892
+ const escaped = rendered2.replace(/'/g, "''");
893
+ return bounded(escaped.includes("\\") ? `E'${escaped.replace(/\\/g, "\\\\")}'` : `'${escaped}'`, "QUOTE_LITERAL");
894
+ }
895
+ }
896
+ ],
897
+ [
898
+ "QUOTE_IDENT",
899
+ {
900
+ minArgs: 1,
901
+ maxArgs: 1,
902
+ returns: "string",
903
+ evaluate: (values) => {
904
+ const name = text("QUOTE_IDENT", values[0]);
905
+ return /^[a-z_][a-z0-9_]*$/.test(name) ? name : bounded(`"${name.replace(/"/g, '""')}"`, "QUOTE_IDENT");
906
+ }
907
+ }
908
+ ],
785
909
  [
786
910
  "MINNOW_REGEX_MATCH",
787
911
  {
@@ -849,7 +973,7 @@ const simpleScalarFunctions = /* @__PURE__ */ new Map([
849
973
  minArgs: 1,
850
974
  maxArgs: 1,
851
975
  returns: "number",
852
- evaluate: (values) => Math.sign(number("SIGN", values[0]))
976
+ evaluate: (values) => isExactNumeric(values[0]) ? exactNumericUnary("SIGN", values[0]) : Math.sign(number("SIGN", values[0]))
853
977
  }
854
978
  ],
855
979
  [
@@ -859,8 +983,10 @@ const simpleScalarFunctions = /* @__PURE__ */ new Map([
859
983
  maxArgs: 2,
860
984
  returns: "number",
861
985
  evaluate: (values) => {
862
- const operand = number("TRUNC", values[0]);
863
986
  const digits = values.length > 1 ? integer("TRUNC", values[1]) : 0;
987
+ if (isExactNumeric(values[0]))
988
+ return exactNumericRounded(values[0], digits, "trunc");
989
+ const operand = number("TRUNC", values[0]);
864
990
  const scale = 10 ** digits;
865
991
  return Math.trunc(operand * scale) / scale;
866
992
  }
@@ -35,4 +35,6 @@ export declare function jsonIsValid(document: unknown, kind: string): boolean;
35
35
  * than the default. Constructors return JSON text at the JavaScript boundary.
36
36
  */
37
37
  export declare function jsonConstructor(name: "JSON_ARRAY" | "JSON_OBJECT", values: readonly unknown[]): string;
38
+ /** to_json(x) / to_jsonb(x): one SQL value as a JSON document (a JSON-domain value as itself). */
39
+ export declare function jsonDocumentOf(value: unknown): string;
38
40
  export {};
@@ -175,6 +175,9 @@ function boundedJsonDocument(value, caller) {
175
175
  }
176
176
  return document;
177
177
  }
178
+ function jsonDocumentOf(value) {
179
+ return boundedJsonValue(value ?? null, "TO_JSON value");
180
+ }
178
181
  function boundedJsonValue(value, label) {
179
182
  const domainDocument = jsonDomainDocument(value);
180
183
  if (domainDocument !== void 0) {
@@ -215,6 +218,7 @@ export {
215
218
  jsonArrowStep,
216
219
  jsonAtPath,
217
220
  jsonConstructor,
221
+ jsonDocumentOf,
218
222
  jsonIsValid,
219
223
  parseJsonPath
220
224
  };
@@ -12,6 +12,13 @@ export declare function parseSqlTimestampText(text: string): Date | undefined;
12
12
  * comparable-types error below, so a genuine mismatch still fails.
13
13
  */
14
14
  export declare function coercedComparable(text: string, other: unknown): unknown;
15
+ /**
16
+ * Reads an untyped string constant in a primitive column type, the way PostgreSQL types an
17
+ * unknown-typed literal by its context: a timestamp spelling for datetime, a finite number for
18
+ * number, `t`/`true`/`1` and `f`/`false`/`0` for boolean. Text that does not parse is returned
19
+ * unchanged, so the caller's own type check still reports the mismatch.
20
+ */
21
+ export declare function readUntypedText(type: "datetime" | "number" | "boolean" | "string", text: string): unknown;
15
22
  /**
16
23
  * Applies the untyped-literal reading to a comparison's two operands: a plain string beside a
17
24
  * typed value (datetime, DATE, number, boolean) is read in that value's type when it parses.
@@ -29,7 +36,8 @@ export declare function compareSqlStrings(left: string, right: string): number;
29
36
  export declare function encodeSqlEqualityValue(value: unknown): readonly unknown[];
30
37
  /**
31
38
  * SQLite-compatible ROUND behavior for Minnow's finite number type: precision truncates to an
32
- * integer and clamps to 0..30, with ties rounded away from zero.
39
+ * integer and clamps to -30..30, with ties rounded away from zero. A negative precision rounds
40
+ * left of the decimal point as PostgreSQL does: ROUND(1250, -2) is 1300.
33
41
  */
34
42
  export declare function roundSqlNumber(value: number, precision?: number): number;
35
43
  /** A whole-string SQL pattern matcher. Unlike RegExp, test never coerces its input. */
@@ -14,15 +14,24 @@ function parseSqlTimestampText(text) {
14
14
  }
15
15
  function coercedComparable(text, other) {
16
16
  if (other instanceof Date || isDateDomainValue(other))
17
+ return readUntypedText("datetime", text);
18
+ if (typeof other === "number")
19
+ return readUntypedText("number", text);
20
+ if (typeof other === "boolean")
21
+ return readUntypedText("boolean", text);
22
+ return text;
23
+ }
24
+ function readUntypedText(type, text) {
25
+ if (type === "datetime")
17
26
  return parseSqlTimestampText(text) ?? text;
18
- if (typeof other === "number") {
27
+ if (type === "number") {
19
28
  const trimmed = text.trim();
20
29
  if (trimmed === "")
21
30
  return text;
22
31
  const parsed = Number(trimmed);
23
32
  return Number.isFinite(parsed) ? parsed : text;
24
33
  }
25
- if (typeof other === "boolean") {
34
+ if (type === "boolean") {
26
35
  const lowered = text.trim().toLowerCase();
27
36
  if (lowered === "t" || lowered === "true" || lowered === "1")
28
37
  return true;
@@ -133,7 +142,13 @@ function encodeSqlEqualityValue(value) {
133
142
  throw new TypeError("Query produced an unsupported value");
134
143
  }
135
144
  function roundSqlNumber(value, precision = 0) {
136
- const digits = Math.min(30, Math.max(0, Math.trunc(precision)));
145
+ const digits = Math.min(30, Math.max(-30, Math.trunc(precision)));
146
+ if (digits < 0) {
147
+ if (!Number.isFinite(value))
148
+ return value;
149
+ const scale = 10 ** -digits;
150
+ return roundSqlNumber(value / scale) * scale;
151
+ }
137
152
  if (digits === 0 && Number.isFinite(value)) {
138
153
  const magnitude = Math.abs(value);
139
154
  if (magnitude >= 4503599627370496)
@@ -628,6 +643,7 @@ export {
628
643
  defineSqlResultProperty,
629
644
  encodeSqlEqualityValue,
630
645
  parseSqlTimestampText,
646
+ readUntypedText,
631
647
  roundSqlNumber,
632
648
  stringArgument
633
649
  };
@@ -2,15 +2,15 @@ import { dateMilliseconds } from "../date-value.js";
2
2
  import { crossJoinPlan, isCrossJoinPlan } from "../plan/model.js";
3
3
  import { MAX_TEMP_RUN_BATCH_BYTES, MAX_TEMP_RUN_PAGE_BYTES, MAX_TEMP_RUN_PAGES_PER_BATCH } from "../storage/types.js";
4
4
  import { throwIfAborted } from "./cancellation.js";
5
- import { cachedListMembership, childExpressions, distinctFromComparison, nullOrder, isScalarFunctionName, likeMatches, orderOutputName, parseQuantified, quantifiedComparison, scalarFunctionEvaluator, scalarFunctionValue, unknownColumnDomains } from "./query.js";
5
+ import { cachedListMembership, childExpressions, distinctFromComparison, nullOrder, isScalarFunctionName, likeMatches, orderOutputName, parseQuantified, quantifiedComparison, scalarFunctionEvaluator, scalarFunctionValue, unknownColumnDomains, integerQuotient } from "./query.js";
6
6
  import { jsonConstructor } from "./sql-json.js";
7
7
  import { bm25DocumentScore, cachedQueryTerms, FtsStatsAccumulator, fullTermsMask, renderDocumentValue, termFrequencies, termsMask, tokenize } from "./fts.js";
8
8
  import { ByteGroupIndex } from "./group-index.js";
9
9
  import { ByteJoinIndex } from "./join-index.js";
10
10
  import { UnknownTableError } from "./errors.js";
11
11
  import { QueryMemoryBudgetError, QueryMemoryContext } from "./memory.js";
12
- import { coerceComparisonOperands, compareSqlValues, compileSimilarPattern, defineSqlResultProperty } from "./sql-semantics.js";
13
- import { concatenatedSqlValue, exactNumericBinary, externalSqlDomainValue, isDateDomainValue, isExactNumeric, preservedJsonDomainValue, protectedSqlTextValue } from "./sql-domains.js";
12
+ import { coerceComparisonOperands, compareSqlValues, compileSimilarPattern, defineSqlResultProperty, readUntypedText } from "./sql-semantics.js";
13
+ import { concatenatedSqlValue, exactNumericBinary, externalSqlDomainValue, isDateDomainValue, isExactNumeric, preservedJsonDomainValue, protectedSqlTextValue, isSqlDomainValue } from "./sql-domains.js";
14
14
  import { buildSortKeyColumn, sortKeyIndexes } from "./sort-keys.js";
15
15
  const DEFAULT_BATCH_ROWS = 2048;
16
16
  const MAX_SPLIT_LIST_MEMBERS = 64;
@@ -899,6 +899,7 @@ function bindExpression(expression, sources, aggregateSpecs, aggregateIndexes, m
899
899
  left: bindExpression(expression.left, sources, aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats),
900
900
  right: bindExpression(expression.right, sources, aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats),
901
901
  ...expression.kind === "condition" && expression.escape !== void 0 ? { escape: expression.escape } : {},
902
+ ...expression.kind === "binary" && expression.integer === true ? { integer: true } : {},
902
903
  signature
903
904
  };
904
905
  }
@@ -3865,7 +3866,7 @@ function evaluateFinalExpression(plan, expression, group) {
3865
3866
  throw new TypeError("Selected full-text expression must appear in GROUP BY");
3866
3867
  }
3867
3868
  if (expression.kind === "binary") {
3868
- return binaryValue(expression.operator, evaluateFinalExpression(plan, expression.left, group), evaluateFinalExpression(plan, expression.right, group));
3869
+ return binaryValue(expression.operator, evaluateFinalExpression(plan, expression.left, group), evaluateFinalExpression(plan, expression.right, group), expression.integer === true);
3869
3870
  }
3870
3871
  if (expression.name === "COALESCE") {
3871
3872
  for (const argument of expression.arguments) {
@@ -4218,7 +4219,9 @@ function constantBoundExpressionValue(expression) {
4218
4219
  if (left === void 0 || right === void 0)
4219
4220
  return void 0;
4220
4221
  try {
4221
- return { value: binaryValue(expression.operator, left.value, right.value) };
4222
+ return {
4223
+ value: binaryValue(expression.operator, left.value, right.value, expression.integer === true)
4224
+ };
4222
4225
  } catch {
4223
4226
  return void 0;
4224
4227
  }
@@ -4269,7 +4272,7 @@ function dictionaryNumericExpression(expression) {
4269
4272
  source: column.source,
4270
4273
  vector: column.vector,
4271
4274
  signature: expression.signature,
4272
- evaluate: columnFirst ? (dictionaryValue) => binaryValue(expression.operator, dictionaryValue, value) : (dictionaryValue) => binaryValue(expression.operator, value, dictionaryValue)
4275
+ evaluate: columnFirst ? (dictionaryValue) => binaryValue(expression.operator, dictionaryValue, value, expression.integer === true) : (dictionaryValue) => binaryValue(expression.operator, value, dictionaryValue, expression.integer === true)
4273
4276
  };
4274
4277
  }
4275
4278
  return void 0;
@@ -4404,10 +4407,12 @@ function booleanTruth(expression, evaluateValue) {
4404
4407
  const distinct = distinctFromComparison(evaluateValue(expression.left), evaluateValue(expression.right));
4405
4408
  return operator === "IS DISTINCT FROM" ? distinct : !distinct;
4406
4409
  }
4407
- const left = evaluateValue(expression.left);
4408
- const right = evaluateValue(expression.right);
4409
- if (left === null || left === void 0 || right === null || right === void 0)
4410
+ const evaluatedLeft = evaluateValue(expression.left);
4411
+ const evaluatedRight = evaluateValue(expression.right);
4412
+ if (evaluatedLeft === null || evaluatedLeft === void 0 || evaluatedRight === null || evaluatedRight === void 0) {
4410
4413
  return null;
4414
+ }
4415
+ const [left, right] = coerceComparisonOperands(evaluatedLeft, evaluatedRight);
4411
4416
  const a = comparable(left);
4412
4417
  const b = comparable(right);
4413
4418
  if (operator === "=")
@@ -4544,9 +4549,10 @@ function compiledBatchExpression(plan, expression) {
4544
4549
  }
4545
4550
  case "binary": {
4546
4551
  const operator = expression.operator;
4552
+ const integer = expression.integer === true;
4547
4553
  const left = compiledBatchExpression(plan, expression.left);
4548
4554
  const right = compiledBatchExpression(plan, expression.right);
4549
- compiled = (batch, row) => binaryValue(operator, left(batch, row), right(batch, row));
4555
+ compiled = (batch, row) => binaryValue(operator, left(batch, row), right(batch, row), integer);
4550
4556
  break;
4551
4557
  }
4552
4558
  case "case": {
@@ -4659,7 +4665,7 @@ function evaluateExpression(expression, rowsBySource) {
4659
4665
  return expression.op === "match" ? ftsBatchTruth(expression, null, rowsBySource, 0) : ftsBm25BatchValue(expression, null, rowsBySource, 0);
4660
4666
  }
4661
4667
  if (expression.kind === "binary") {
4662
- return binaryValue(expression.operator, evaluateExpression(expression.left, rowsBySource), evaluateExpression(expression.right, rowsBySource));
4668
+ return binaryValue(expression.operator, evaluateExpression(expression.left, rowsBySource), evaluateExpression(expression.right, rowsBySource), expression.integer === true);
4663
4669
  }
4664
4670
  if (expression.name === "COALESCE") {
4665
4671
  for (const argument of expression.arguments) {
@@ -4673,7 +4679,7 @@ function evaluateExpression(expression, rowsBySource) {
4673
4679
  throw new TypeError(`${expression.name} requires grouped execution`);
4674
4680
  return scalarFunctionValue(expression.name, expression.arguments.map((argument) => evaluateExpression(argument, rowsBySource)));
4675
4681
  }
4676
- function binaryValue(operator, left, right) {
4682
+ function binaryValue(operator, left, right, integer = false) {
4677
4683
  if (left === null || left === void 0 || right === null || right === void 0)
4678
4684
  return null;
4679
4685
  if (typeof left === "number" && typeof right === "number") {
@@ -4684,12 +4690,22 @@ function binaryValue(operator, left, right) {
4684
4690
  if (operator === "*")
4685
4691
  return left * right;
4686
4692
  if (operator === "/")
4687
- return right === 0 ? null : left / right;
4693
+ return integer ? integerQuotient(left, right) : right === 0 ? null : left / right;
4688
4694
  if (operator === "%")
4689
4695
  return right === 0 ? null : left % right;
4690
4696
  }
4691
4697
  if (operator === "||")
4692
4698
  return concatenatedSqlValue(left, right);
4699
+ if (typeof left === "string" && typeof right === "number" && !isSqlDomainValue(left)) {
4700
+ const read = readUntypedText("number", left);
4701
+ if (typeof read === "number")
4702
+ return binaryValue(operator, read, right, integer);
4703
+ }
4704
+ if (typeof right === "string" && typeof left === "number" && !isSqlDomainValue(right)) {
4705
+ const read = readUntypedText("number", right);
4706
+ if (typeof read === "number")
4707
+ return binaryValue(operator, left, read, integer);
4708
+ }
4693
4709
  const exact = exactNumericBinary(operator, left, right);
4694
4710
  if (exact !== void 0)
4695
4711
  return exact;
@@ -554,10 +554,13 @@ class DatabaseRpcServer {
554
554
  return handle.session.execute(sql, params);
555
555
  }
556
556
  if (method === "stage") {
557
- const [op, tableName, input] = args;
557
+ const [op, tableName, input, options] = args;
558
558
  if (!isStageOp(op)) {
559
559
  throw new Error(`Unsupported write stage operation: ${String(op)}`);
560
560
  }
561
+ if (op === "upsertBatch") {
562
+ return handle.session.upsertBatch(tableName, input, options);
563
+ }
561
564
  return handle.session[op](tableName, input);
562
565
  }
563
566
  if (method === "commit")
@@ -17,7 +17,7 @@ export type ComparisonOperator = "=" | "!=" | "<>" | ">" | ">=" | "<" | "<=";
17
17
  export type AggregateName = "COUNT" | "SUM" | "AVG" | "MIN" | "MAX" | "JSON_ARRAYAGG" | "STRING_AGG"
18
18
  /** Optimizer-only aggregate that enforces scalar-subquery cardinality. */
19
19
  | "MINNOW_SINGLE_VALUE";
20
- export type ScalarFunctionName = "ROUND" | "COALESCE" | "DATE_TRUNC" | "DATE_ADD" | "UPPER" | "LOWER" | "LENGTH" | "ABS" | "TRIM" | "LTRIM" | "RTRIM" | "SUBSTR" | "REPLACE" | "INSTR" | "NULLIF" | "GREATEST" | "LEAST" | "FLOOR" | "CEIL" | "MOD" | "POWER" | "SQRT" | "EXTRACT" | "CAST" | "OCTET_LENGTH" | "LPAD" | "RPAD" | "OVERLAY" | "CURRENT_DATE" | "CURRENT_TIMESTAMP" | "LOCALTIME" | "GROUPING" | "JSON_VALUE" | "JSON_QUERY" | "JSON_EXISTS" | "JSON_OBJECT" | "JSON_ARRAY" | "IS_JSON" | "ARRAY"
20
+ export type ScalarFunctionName = "ROUND" | "COALESCE" | "DATE_TRUNC" | "DATE_ADD" | "UPPER" | "LOWER" | "LENGTH" | "ABS" | "TRIM" | "LTRIM" | "RTRIM" | "SUBSTR" | "REPLACE" | "INSTR" | "NULLIF" | "GREATEST" | "LEAST" | "FLOOR" | "CEIL" | "MOD" | "POWER" | "SQRT" | "EXTRACT" | "CAST" | "OCTET_LENGTH" | "LPAD" | "RPAD" | "OVERLAY" | "CURRENT_DATE" | "CURRENT_TIMESTAMP" | "LOCALTIME" | "GROUPING" | "JSON_VALUE" | "JSON_QUERY" | "JSON_EXISTS" | "JSON_OBJECT" | "JSON_ARRAY" | "TO_JSON" | "IS_JSON" | "ARRAY"
21
21
  /** Parser-produced `->` JSON member/element access returning a JSON value. */
22
22
  | "MINNOW_JSON_GET"
23
23
  /** Parser-produced `->>` JSON member/element access returning text. */
@@ -27,7 +27,7 @@ export type ScalarFunctionName = "ROUND" | "COALESCE" | "DATE_TRUNC" | "DATE_ADD
27
27
  /** Parser-produced wrapper carrying one explicit collation through ordering/comparison. */
28
28
  | "MINNOW_COLLATE" | "NEXTVAL" | "CURRVAL" | "RANDOM" | "GEN_RANDOM_UUID"
29
29
  /** Table-driven PostgreSQL string, math, datetime, regex, and formatting functions. */
30
- | "CONCAT" | "CONCAT_WS" | "LEFT" | "RIGHT" | "REVERSE" | "REPEAT" | "INITCAP" | "SPLIT_PART" | "STRPOS" | "STARTS_WITH" | "TRANSLATE" | "ASCII" | "CHR" | "BTRIM" | "MD5" | "FORMAT" | "REGEXP_REPLACE" | "MINNOW_REGEX_MATCH" | "EXP" | "LN" | "LOG" | "LOG10" | "SIGN" | "TRUNC" | "PI" | "CBRT" | "DIV" | "WIDTH_BUCKET" | "SIN" | "COS" | "TAN" | "ASIN" | "ACOS" | "ATAN" | "ATAN2" | "DEGREES" | "RADIANS" | "TO_CHAR" | "TO_DATE" | "TO_TIMESTAMP" | "MAKE_DATE" | "MAKE_TIMESTAMP" | "AGE";
30
+ | "CONCAT" | "CONCAT_WS" | "LEFT" | "RIGHT" | "REVERSE" | "REPEAT" | "INITCAP" | "SPLIT_PART" | "STRPOS" | "STARTS_WITH" | "TRANSLATE" | "ASCII" | "CHR" | "BTRIM" | "MD5" | "REGEXP_SUBSTR" | "NUM_NONNULLS" | "NUM_NULLS" | "GCD" | "LCM" | "TO_HEX" | "QUOTE_LITERAL" | "QUOTE_IDENT" | "FORMAT" | "REGEXP_REPLACE" | "MINNOW_REGEX_MATCH" | "EXP" | "LN" | "LOG" | "LOG10" | "SIGN" | "TRUNC" | "PI" | "CBRT" | "DIV" | "WIDTH_BUCKET" | "SIN" | "COS" | "TAN" | "ASIN" | "ACOS" | "ATAN" | "ATAN2" | "DEGREES" | "RADIANS" | "TO_CHAR" | "TO_DATE" | "TO_TIMESTAMP" | "MAKE_DATE" | "MAKE_TIMESTAMP" | "AGE";
31
31
  /** Exact BM25 corpus statistics attached to a cloned scoring node before execution. */
32
32
  export interface FtsStats {
33
33
  /** Every row of the corpus, including all-null documents. */
@@ -50,6 +50,12 @@ export type Expression = {
50
50
  * PostgreSQL types every decimal constant NUMERIC before evaluating it.
51
51
  */
52
52
  exactText?: string;
53
+ /**
54
+ * Written with a decimal point or exponent, or folded from such a constant: PostgreSQL
55
+ * types it NUMERIC, so it is never an integer operand even when its value is whole
56
+ * (`7 / 2.0` is 3.5 where `7 / 2` is 3).
57
+ */
58
+ decimal?: true;
53
59
  }
54
60
  /** A `?` or `$n` placeholder; `index` is 0-based. Replaced by a literal at bind time. */
55
61
  | {
@@ -68,6 +74,12 @@ export type Expression = {
68
74
  operator: BinaryOperator;
69
75
  left: Expression;
70
76
  right: Expression;
77
+ /**
78
+ * Integer division: both operands are integer-typed, so `/` truncates toward zero as
79
+ * PostgreSQL's integer `/` does. Set by the compiler for constants and by catalog
80
+ * binding for columns; absent, `/` is ordinary Float64 or exact-NUMERIC division.
81
+ */
82
+ integer?: true;
71
83
  } | {
72
84
  kind: "call";
73
85
  name: AggregateName | ScalarFunctionName;
@@ -3919,7 +3919,12 @@ class IndexedDbBlockStore {
3919
3919
  await assertActiveGarbageCollectionMarker(gcStore, current);
3920
3920
  }
3921
3921
  const updated = updateGarbageCollectionPlanningRecord(current, input);
3922
- await assertGarbageCollectionCandidateProvenanceInTransaction(transaction, updated);
3922
+ await assertGarbageCollectionCandidateProvenanceInTransaction(transaction, {
3923
+ candidateManifestVersions: input.candidateManifestVersions ?? [],
3924
+ candidateSegmentIds: input.candidateSegmentIds ?? [],
3925
+ candidateBlockIds: input.candidateBlockIds ?? [],
3926
+ candidateTransactionIds: input.candidateTransactionIds ?? []
3927
+ });
3923
3928
  gcStore.put(garbageCollectionJobEnvelope(updated), key);
3924
3929
  await transactionDone(transaction);
3925
3930
  return structuredClone(updated);
@@ -9717,15 +9722,15 @@ function assertGenericTransactionUpdateAllowed(record, update) {
9717
9722
  throw new TypeError("Only commitTransaction can set a committed transaction version");
9718
9723
  }
9719
9724
  }
9720
- async function assertGarbageCollectionCandidateProvenanceInTransaction(transaction, job) {
9725
+ async function assertGarbageCollectionCandidateProvenanceInTransaction(transaction, candidates) {
9721
9726
  const manifestStore = transaction.objectStore("manifests");
9722
- for (const version of job.candidateManifestVersions) {
9727
+ for (const version of candidates.candidateManifestVersions) {
9723
9728
  const value = await requestResult(manifestStore.get(version));
9724
9729
  if (value === void 0) {
9725
9730
  throw new Error(`Garbage collection candidate manifest is missing: ${String(version)}`);
9726
9731
  }
9727
9732
  }
9728
- for (const id of job.candidateTransactionIds) {
9733
+ for (const id of candidates.candidateTransactionIds) {
9729
9734
  const value = await requestResult(transaction.objectStore("transactions").get(id));
9730
9735
  const record = value === void 0 ? void 0 : asTransactionRecord(value);
9731
9736
  if (record === void 0 || record.status !== "aborted" && (record.status !== "committed" || record.committedVersion === null)) {
@@ -9733,10 +9738,10 @@ async function assertGarbageCollectionCandidateProvenanceInTransaction(transacti
9733
9738
  }
9734
9739
  }
9735
9740
  const manifestProvenBlockIds = /* @__PURE__ */ new Set();
9736
- const manifestValues = await Promise.all(job.candidateBlockIds.map((id) => requestResult(transaction.objectStore("catalog").get(manifestBlockKey(id)))));
9741
+ const manifestValues = await Promise.all(candidates.candidateBlockIds.map((id) => requestResult(transaction.objectStore("catalog").get(manifestBlockKey(id)))));
9737
9742
  for (const [index, value] of manifestValues.entries()) {
9738
9743
  if (value !== void 0) {
9739
- const id = job.candidateBlockIds[index] ?? "";
9744
+ const id = candidates.candidateBlockIds[index] ?? "";
9740
9745
  asManifestBlockRecord(value, id);
9741
9746
  manifestProvenBlockIds.add(id);
9742
9747
  }
@@ -9757,12 +9762,12 @@ async function assertGarbageCollectionCandidateProvenanceInTransaction(transacti
9757
9762
  return isTerminalCompactionJob(record) && (record.sourceBlockIds.includes(id) || record.outputBlockIds.includes(id));
9758
9763
  });
9759
9764
  };
9760
- for (const id of job.candidateBlockIds) {
9765
+ for (const id of candidates.candidateBlockIds) {
9761
9766
  if (await blockHasProvenance(id))
9762
9767
  continue;
9763
9768
  throw new Error(`Garbage collection block candidate has no persisted provenance: ${id}`);
9764
9769
  }
9765
- for (const id of job.candidateSegmentIds) {
9770
+ for (const id of candidates.candidateSegmentIds) {
9766
9771
  const segmentValue = await requestResult(transaction.objectStore("segments").get(id));
9767
9772
  if (segmentValue !== void 0)
9768
9773
  continue;
@@ -3,7 +3,8 @@ import { type BeginSnapshotFrameExportInput, type SnapshotFrameExportSession, ty
3
3
  * The in-process store: record semantics live in `RecordCore` (shared with the OPFS store),
4
4
  * block and temp-page bytes live in Maps here, and atomicity comes from running every mutating
5
5
  * record operation on a promise-chain queue — each queued body is synchronous, so no operation
6
- * ever observes another mid-mutation.
6
+ * ever observes another mid-mutation. Every commit acknowledges on the next event-loop turn
7
+ * (`commitTurn`), so a write loop yields to background work as it would on disk.
7
8
  */
8
9
  export declare class MemoryBlockStore implements BlockStore {
9
10
  #private;