@minnowdb/core 0.6.9 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,23 @@
1
1
  /**
2
- * SQL value semantics shared by the row and vector executors.
3
- *
4
- * Keep comparison, equality-key, scalar, and pattern behavior here so an execution-path
5
- * optimization cannot silently change query results.
2
+ * The standard's datetime text `2026-01-02`, `2026-01-02 03:04:05`, `2026-01-02T03:04:05.250Z`,
3
+ * an optional zone offset — read as an instant. A zoneless spelling is UTC, the reading every
4
+ * datetime in a Minnow database has; `undefined` for text in any other shape.
6
5
  */
6
+ export declare function parseSqlTimestampText(text: string): Date | undefined;
7
+ /**
8
+ * PostgreSQL reads an untyped string constant beside a typed value in that value's type:
9
+ * `joined >= '2026-01-01'`, `id = '5'`, `active = 't'`. Catalog-backed plans coerce such literals
10
+ * before execution; this runtime reading covers the schema-less row executor and anything the
11
+ * plan rewrite could not see. Text that does not parse in the other side's type keeps the
12
+ * comparable-types error below, so a genuine mismatch still fails.
13
+ */
14
+ export declare function coercedComparable(text: string, other: unknown): unknown;
15
+ /**
16
+ * Applies the untyped-literal reading to a comparison's two operands: a plain string beside a
17
+ * typed value (datetime, DATE, number, boolean) is read in that value's type when it parses.
18
+ * Both executors call this before their own comparisons, so the reading is one decision.
19
+ */
20
+ export declare function coerceComparisonOperands(left: unknown, right: unknown): [unknown, unknown];
7
21
  /**
8
22
  * Deterministic SQL ordering. Strings use Unicode codepoint order rather than host locale data,
9
23
  * and signed zero compares equal because SQL numeric equality does not distinguish it.
@@ -24,6 +38,13 @@ interface SqlPatternMatcher {
24
38
  }
25
39
  /** Compiles SQL LIKE without exposing input to the host regular-expression engine. */
26
40
  export declare function compileLikePattern(pattern: string, caseInsensitive?: boolean, escape?: string): SqlPatternMatcher;
41
+ /**
42
+ * PostgreSQL's ~ / ~* / !~ / !~* operators and REGEXP_REPLACE, compiled as JavaScript regular
43
+ * expressions. Advanced regular expressions and JavaScript agree on the everyday syntax; the
44
+ * `n` flag makes `.` and anchors newline-sensitive, `i` is case-insensitive, and `g` replaces
45
+ * every match. Patterns are bounded like every other SQL pattern.
46
+ */
47
+ export declare function compileRegexPattern(pattern: string, flags?: string): RegExp;
27
48
  /** PostgreSQL SIMILAR TO compiled to a Thompson NFA with bounded deterministic work. */
28
49
  export declare function compileSimilarPattern(pattern: string, escape?: string): SqlPatternMatcher;
29
50
  /** Defines an own enumerable result-column property, including the special name `__proto__`. */
@@ -1,18 +1,95 @@
1
1
  import { dateMilliseconds } from "../date-value.js";
2
2
  import { assertWellFormedString } from "../block-format/unicode.js";
3
3
  import { MAX_SQL_NESTING_DEPTH, MAX_SQL_PATTERN_CHARACTERS, MAX_SQL_PATTERN_MATCH_STEPS, } from "./cache-limits.js";
4
- import { collatedDomainCompare, enumDomainCompare, exactNumericCompare, externalSqlDomainValue, externalSqlTextValue, isDateDomainValue, } from "./sql-domains.js";
4
+ import { collatedDomainCompare, enumDomainCompare, exactNumericCompare, externalSqlDomainValue, externalSqlTextValue, intervalDomainCompare, isDateDomainValue, isSqlDomainValue, } from "./sql-domains.js";
5
5
  /**
6
6
  * SQL value semantics shared by the row and vector executors.
7
7
  *
8
8
  * Keep comparison, equality-key, scalar, and pattern behavior here so an execution-path
9
9
  * optimization cannot silently change query results.
10
10
  */
11
+ const SQL_TIMESTAMP_TEXT = /^(\d{4}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?))?(Z|[+-]\d{2}:?\d{2})?$/;
12
+ /**
13
+ * The standard's datetime text — `2026-01-02`, `2026-01-02 03:04:05`, `2026-01-02T03:04:05.250Z`,
14
+ * an optional zone offset — read as an instant. A zoneless spelling is UTC, the reading every
15
+ * datetime in a Minnow database has; `undefined` for text in any other shape.
16
+ */
17
+ export function parseSqlTimestampText(text) {
18
+ const match = SQL_TIMESTAMP_TEXT.exec(text.trim());
19
+ if (match === null)
20
+ return undefined;
21
+ const [, day, time = "00:00:00", zone = "Z"] = match;
22
+ const seconds = time.length === 5 ? `${time}:00` : time;
23
+ const date = new Date(`${String(day)}T${seconds}${zone === "Z" ? "Z" : zone}`);
24
+ return Number.isFinite(dateMilliseconds(date)) ? date : undefined;
25
+ }
26
+ /**
27
+ * PostgreSQL reads an untyped string constant beside a typed value in that value's type:
28
+ * `joined >= '2026-01-01'`, `id = '5'`, `active = 't'`. Catalog-backed plans coerce such literals
29
+ * before execution; this runtime reading covers the schema-less row executor and anything the
30
+ * plan rewrite could not see. Text that does not parse in the other side's type keeps the
31
+ * comparable-types error below, so a genuine mismatch still fails.
32
+ */
33
+ export function coercedComparable(text, other) {
34
+ if (other instanceof Date || isDateDomainValue(other))
35
+ return parseSqlTimestampText(text) ?? text;
36
+ if (typeof other === "number") {
37
+ const trimmed = text.trim();
38
+ if (trimmed === "")
39
+ return text;
40
+ const parsed = Number(trimmed);
41
+ return Number.isFinite(parsed) ? parsed : text;
42
+ }
43
+ if (typeof other === "boolean") {
44
+ const lowered = text.trim().toLowerCase();
45
+ if (lowered === "t" || lowered === "true" || lowered === "1")
46
+ return true;
47
+ if (lowered === "f" || lowered === "false" || lowered === "0")
48
+ return false;
49
+ }
50
+ return text;
51
+ }
52
+ /**
53
+ * Applies the untyped-literal reading to a comparison's two operands: a plain string beside a
54
+ * typed value (datetime, DATE, number, boolean) is read in that value's type when it parses.
55
+ * Both executors call this before their own comparisons, so the reading is one decision.
56
+ */
57
+ export function coerceComparisonOperands(left, right) {
58
+ const plainText = (value) => typeof value === "string" && !isSqlDomainValue(value);
59
+ const typed = (value) => typeof value !== "string" || isDateDomainValue(value);
60
+ if (plainText(left) && typed(right))
61
+ return [coercedComparable(left, right), right];
62
+ if (plainText(right) && typed(left))
63
+ return [left, coercedComparable(right, left)];
64
+ return [left, right];
65
+ }
11
66
  /**
12
67
  * Deterministic SQL ordering. Strings use Unicode codepoint order rather than host locale data,
13
68
  * and signed zero compares equal because SQL numeric equality does not distinguish it.
14
69
  */
15
70
  export function compareSqlValues(left, right) {
71
+ // Plain numbers and untagged strings are the bulk of every comparison; settle them before the
72
+ // domain checks. NaN falls through so its placement stays with the general ordering below, and
73
+ // a string starting with NUL may carry a domain tag, so only tag-free strings take the short path.
74
+ if (typeof left === "number" && typeof right === "number") {
75
+ if (left < right)
76
+ return -1;
77
+ if (left > right)
78
+ return 1;
79
+ if (left === right)
80
+ return 0;
81
+ }
82
+ else if (typeof left === "string" &&
83
+ typeof right === "string" &&
84
+ left.charCodeAt(0) !== 0 &&
85
+ right.charCodeAt(0) !== 0) {
86
+ return compareSqlStrings(left, right);
87
+ }
88
+ const [coercedLeft, coercedRight] = coerceComparisonOperands(left, right);
89
+ // Object.is, not !==: NaN would otherwise look changed on every pass and recurse forever.
90
+ if (!Object.is(coercedLeft, left) || !Object.is(coercedRight, right)) {
91
+ return compareSqlValues(coercedLeft, coercedRight);
92
+ }
16
93
  const plainLeft = externalSqlTextValue(left);
17
94
  const plainRight = externalSqlTextValue(right);
18
95
  if ((typeof left === "string" && plainLeft !== left) ||
@@ -25,6 +102,9 @@ export function compareSqlValues(left, right) {
25
102
  const collated = collatedDomainCompare(left, right);
26
103
  if (collated !== undefined)
27
104
  return collated;
105
+ const interval = intervalDomainCompare(left, right);
106
+ if (interval !== undefined)
107
+ return interval;
28
108
  const enumOrder = enumDomainCompare(left, right);
29
109
  if (enumOrder !== undefined)
30
110
  return enumOrder;
@@ -96,6 +176,17 @@ export function encodeSqlEqualityValue(value) {
96
176
  */
97
177
  export function roundSqlNumber(value, precision = 0) {
98
178
  const digits = Math.min(30, Math.max(0, Math.trunc(precision)));
179
+ // Whole-number rounding is arithmetic: toFixed(0) picks the integer nearest the exact binary
180
+ // value, ties toward larger magnitude. Below 2^52 the fractional part of a double is exact,
181
+ // so floor and compare reproduce it without formatting a string per row.
182
+ if (digits === 0 && Number.isFinite(value)) {
183
+ const magnitude = Math.abs(value);
184
+ if (magnitude >= 4_503_599_627_370_496)
185
+ return value === 0 ? 0 : value;
186
+ const whole = Math.floor(magnitude);
187
+ const rounded = magnitude - whole >= 0.5 ? whole + 1 : whole;
188
+ return rounded === 0 ? 0 : value < 0 ? -rounded : rounded;
189
+ }
99
190
  // SQLite formats with the requested decimal precision and parses the result back. toFixed
100
191
  // follows the same decimal path, avoiding multiplication overflow and binary scaling drift.
101
192
  const rounded = Number(value.toFixed(digits));
@@ -525,6 +616,48 @@ function nfaMatcher(pattern, escape) {
525
616
  // the cache instead of pretending their Uint32-sized indexes are the whole retained graph.
526
617
  return { test, retainedSize: states.length * 32 };
527
618
  }
619
+ const regexCache = new Map();
620
+ /**
621
+ * PostgreSQL's ~ / ~* / !~ / !~* operators and REGEXP_REPLACE, compiled as JavaScript regular
622
+ * expressions. Advanced regular expressions and JavaScript agree on the everyday syntax; the
623
+ * `n` flag makes `.` and anchors newline-sensitive, `i` is case-insensitive, and `g` replaces
624
+ * every match. Patterns are bounded like every other SQL pattern.
625
+ */
626
+ export function compileRegexPattern(pattern, flags = "") {
627
+ assertBoundedPattern(pattern, "regular expression");
628
+ const normalized = [...new Set(flags)].sort().join("");
629
+ const key = `${normalized}\u0000${pattern}`;
630
+ const cached = regexCache.get(key);
631
+ if (cached !== undefined) {
632
+ cached.lastIndex = 0;
633
+ return cached;
634
+ }
635
+ let jsFlags = "u";
636
+ if (normalized.includes("i"))
637
+ jsFlags += "i";
638
+ if (normalized.includes("g"))
639
+ jsFlags += "g";
640
+ if (normalized.includes("n"))
641
+ jsFlags += "m";
642
+ else
643
+ jsFlags += "s";
644
+ let expression;
645
+ try {
646
+ expression = new RegExp(pattern, jsFlags);
647
+ }
648
+ catch {
649
+ try {
650
+ expression = new RegExp(pattern, jsFlags.replace("u", ""));
651
+ }
652
+ catch (error) {
653
+ throw new TypeError(`Invalid regular expression: ${error instanceof Error ? error.message : pattern}`, { cause: error });
654
+ }
655
+ }
656
+ if (regexCache.size >= 256)
657
+ regexCache.clear();
658
+ regexCache.set(key, expression);
659
+ return expression;
660
+ }
528
661
  /** PostgreSQL SIMILAR TO compiled to a Thompson NFA with bounded deterministic work. */
529
662
  export function compileSimilarPattern(pattern, escape = "\\") {
530
663
  assertBoundedPattern(pattern, "SIMILAR TO pattern");
@@ -34,7 +34,7 @@ interface StringVector extends VectorBase {
34
34
  readonly dictionary: readonly string[];
35
35
  }
36
36
  export type ColumnVector = BooleanVector | NumberVector | DateTimeVector | StringVector;
37
- interface ColumnarColumnInput {
37
+ export interface ColumnarColumnInput {
38
38
  readonly type: VectorType;
39
39
  readonly values: readonly QueryValue[];
40
40
  }
@@ -86,6 +86,12 @@ export interface AsyncQueryExecutionOptions {
86
86
  * reference instead of stitching copies.
87
87
  */
88
88
  readonly loadScanWindow?: (start: number, length: number) => number | Promise<number>;
89
+ /**
90
+ * Ascending scan row positions an index has already narrowed the plan to. The predicates
91
+ * still run on every visited row, so the selection is a pure skip list: rows outside it are
92
+ * never loaded or evaluated. Only the streamed scan honors it.
93
+ */
94
+ readonly scanRows?: readonly number[];
89
95
  }
90
96
  export interface QueryBatchExecutionOptions extends AsyncQueryExecutionOptions {
91
97
  /** Maximum result rows handed to the consumer at once. */