@minnowdb/core 0.7.0 → 0.7.1
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.
- package/dist/block-format/block.js +254 -325
- package/dist/block-format/checksum.js +30 -52
- package/dist/block-format/codecs.js +112 -125
- package/dist/block-format/column.js +187 -205
- package/dist/block-format/index.js +5 -1
- package/dist/block-format/physical.js +371 -387
- package/dist/block-format/types.js +4 -1
- package/dist/block-format/unicode.js +30 -39
- package/dist/date-value.js +67 -78
- package/dist/engine/artifact-cache.js +106 -116
- package/dist/engine/batch.js +36 -44
- package/dist/engine/buffered-writer.js +156 -158
- package/dist/engine/byte-estimates.js +20 -22
- package/dist/engine/cache-limits.js +24 -25
- package/dist/engine/cancellation.js +5 -3
- package/dist/engine/catalog.js +64 -73
- package/dist/engine/client.js +715 -780
- package/dist/engine/database.js +15941 -19313
- package/dist/engine/defaults.js +104 -111
- package/dist/engine/errors.js +113 -128
- package/dist/engine/fts.js +233 -280
- package/dist/engine/group-index.js +287 -321
- package/dist/engine/index.js +23 -12
- package/dist/engine/join-index.js +177 -186
- package/dist/engine/keyed-live.js +168 -173
- package/dist/engine/live-api.js +0 -1
- package/dist/engine/live-equal.js +32 -37
- package/dist/engine/live.js +603 -647
- package/dist/engine/memory.js +109 -111
- package/dist/engine/optimizer.d.ts +19 -1
- package/dist/engine/optimizer.js +2633 -2934
- package/dist/engine/point-read.js +122 -182
- package/dist/engine/query-api.js +12 -3
- package/dist/engine/query-cache.js +61 -69
- package/dist/engine/query.d.ts +6 -0
- package/dist/engine/query.js +8503 -9980
- package/dist/engine/result-wire.js +221 -252
- package/dist/engine/schema-wire.js +97 -112
- package/dist/engine/schema.js +1099 -1254
- package/dist/engine/sort-keys.js +315 -389
- package/dist/engine/sql-domains.js +646 -769
- package/dist/engine/sql-driver.js +0 -1
- package/dist/engine/sql-functions.js +1031 -1125
- package/dist/engine/sql-json.js +199 -218
- package/dist/engine/sql-semantics.js +572 -639
- package/dist/engine/typed-live.js +271 -293
- package/dist/engine/vector.js +4484 -5145
- package/dist/engine/worker-host.d.ts +8 -63
- package/dist/engine/worker-host.js +28 -1130
- package/dist/engine/worker-indexeddb.d.ts +1 -0
- package/dist/engine/worker-indexeddb.js +3 -0
- package/dist/engine/worker-memory.d.ts +1 -0
- package/dist/engine/worker-memory.js +3 -0
- package/dist/engine/worker-opfs.d.ts +1 -0
- package/dist/engine/worker-opfs.js +3 -0
- package/dist/engine/worker-server.d.ts +93 -0
- package/dist/engine/worker-server.js +999 -0
- package/dist/engine/worker-store-indexeddb.d.ts +2 -0
- package/dist/engine/worker-store-indexeddb.js +11 -0
- package/dist/engine/worker-store-memory.d.ts +2 -0
- package/dist/engine/worker-store-memory.js +6 -0
- package/dist/engine/worker-store-opfs.d.ts +2 -0
- package/dist/engine/worker-store-opfs.js +9 -0
- package/dist/engine/worker.js +0 -12
- package/dist/engine/write-block-planner.js +93 -100
- package/dist/plan/index.js +15 -16
- package/dist/plan/model.d.ts +6 -0
- package/dist/plan/model.js +20 -27
- package/dist/storage/index.js +0 -12
- package/dist/storage/indexeddb.js +11776 -13171
- package/dist/storage/memory.js +1198 -1241
- package/dist/storage/opfs/files.js +238 -295
- package/dist/storage/opfs/index.js +9 -3
- package/dist/storage/opfs/leader.js +4386 -4856
- package/dist/storage/opfs/rpc.js +223 -259
- package/dist/storage/opfs/snapshot-ledger.js +219 -260
- package/dist/storage/opfs/store.js +884 -1003
- package/dist/storage/persistence.js +59 -74
- package/dist/storage/snapshot-stream.js +788 -826
- package/dist/storage/snapshot.js +36 -37
- package/dist/storage/toolkit/extents.js +444 -520
- package/dist/storage/toolkit/index.js +28 -29
- package/dist/storage/toolkit/record-core.js +5792 -6377
- package/dist/storage/toolkit/sync-file.js +34 -42
- package/dist/storage/toolkit/wal.js +92 -127
- package/dist/storage/toolkit/wire.js +232 -256
- package/dist/storage/types.js +2948 -3250
- package/dist/testing/block-store-conformance.js +1556 -1629
- package/dist/testing/index.js +310 -291
- package/dist/testing/opfs-shim.js +280 -312
- package/dist/testing/simulator.js +496 -534
- package/dist/testing/sqllogictest.js +425 -365
- package/dist/transactions/index.js +1464 -1712
- package/dist/worker-protocol/index.js +70 -69
- package/package.json +20 -2
|
@@ -1,700 +1,633 @@
|
|
|
1
1
|
import { dateMilliseconds } from "../date-value.js";
|
|
2
2
|
import { assertWellFormedString } from "../block-format/unicode.js";
|
|
3
|
-
import { MAX_SQL_NESTING_DEPTH, MAX_SQL_PATTERN_CHARACTERS, MAX_SQL_PATTERN_MATCH_STEPS
|
|
4
|
-
import { collatedDomainCompare, enumDomainCompare, exactNumericCompare, externalSqlDomainValue, externalSqlTextValue, intervalDomainCompare, isDateDomainValue, isSqlDomainValue
|
|
5
|
-
/**
|
|
6
|
-
* SQL value semantics shared by the row and vector executors.
|
|
7
|
-
*
|
|
8
|
-
* Keep comparison, equality-key, scalar, and pattern behavior here so an execution-path
|
|
9
|
-
* optimization cannot silently change query results.
|
|
10
|
-
*/
|
|
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, intervalDomainCompare, isDateDomainValue, isSqlDomainValue } from "./sql-domains.js";
|
|
11
5
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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;
|
|
6
|
+
function parseSqlTimestampText(text) {
|
|
7
|
+
const match = SQL_TIMESTAMP_TEXT.exec(text.trim());
|
|
8
|
+
if (match === null)
|
|
9
|
+
return void 0;
|
|
10
|
+
const [, day, time = "00:00:00", zone = "Z"] = match;
|
|
11
|
+
const seconds = time.length === 5 ? `${time}:00` : time;
|
|
12
|
+
const date = /* @__PURE__ */ new Date(`${String(day)}T${seconds}${zone === "Z" ? "Z" : zone}`);
|
|
13
|
+
return Number.isFinite(dateMilliseconds(date)) ? date : void 0;
|
|
25
14
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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;
|
|
15
|
+
function coercedComparable(text, other) {
|
|
16
|
+
if (other instanceof Date || isDateDomainValue(other))
|
|
17
|
+
return parseSqlTimestampText(text) ?? text;
|
|
18
|
+
if (typeof other === "number") {
|
|
19
|
+
const trimmed = text.trim();
|
|
20
|
+
if (trimmed === "")
|
|
21
|
+
return text;
|
|
22
|
+
const parsed = Number(trimmed);
|
|
23
|
+
return Number.isFinite(parsed) ? parsed : text;
|
|
24
|
+
}
|
|
25
|
+
if (typeof other === "boolean") {
|
|
26
|
+
const lowered = text.trim().toLowerCase();
|
|
27
|
+
if (lowered === "t" || lowered === "true" || lowered === "1")
|
|
28
|
+
return true;
|
|
29
|
+
if (lowered === "f" || lowered === "false" || lowered === "0")
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return text;
|
|
51
33
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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];
|
|
34
|
+
function coerceComparisonOperands(left, right) {
|
|
35
|
+
const plainText = (value) => typeof value === "string" && !isSqlDomainValue(value);
|
|
36
|
+
const typed = (value) => typeof value !== "string" || isDateDomainValue(value);
|
|
37
|
+
if (plainText(left) && typed(right))
|
|
38
|
+
return [coercedComparable(left, right), right];
|
|
39
|
+
if (plainText(right) && typed(left))
|
|
40
|
+
return [left, coercedComparable(right, left)];
|
|
41
|
+
return [left, right];
|
|
65
42
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
}
|
|
93
|
-
const plainLeft = externalSqlTextValue(left);
|
|
94
|
-
const plainRight = externalSqlTextValue(right);
|
|
95
|
-
if ((typeof left === "string" && plainLeft !== left) ||
|
|
96
|
-
(typeof right === "string" && plainRight !== right)) {
|
|
97
|
-
if (typeof plainLeft !== "string" || typeof plainRight !== "string") {
|
|
98
|
-
throw new TypeError("Values must have comparable SQL types");
|
|
99
|
-
}
|
|
100
|
-
return compareSqlStrings(plainLeft, plainRight);
|
|
101
|
-
}
|
|
102
|
-
const collated = collatedDomainCompare(left, right);
|
|
103
|
-
if (collated !== undefined)
|
|
104
|
-
return collated;
|
|
105
|
-
const interval = intervalDomainCompare(left, right);
|
|
106
|
-
if (interval !== undefined)
|
|
107
|
-
return interval;
|
|
108
|
-
const enumOrder = enumDomainCompare(left, right);
|
|
109
|
-
if (enumOrder !== undefined)
|
|
110
|
-
return enumOrder;
|
|
111
|
-
const exact = exactNumericCompare(left, right);
|
|
112
|
-
if (exact !== undefined)
|
|
113
|
-
return exact;
|
|
114
|
-
const temporal = (value) => {
|
|
115
|
-
if (value instanceof Date)
|
|
116
|
-
return dateMilliseconds(value);
|
|
117
|
-
if (!isDateDomainValue(value))
|
|
118
|
-
return undefined;
|
|
119
|
-
const external = externalSqlDomainValue(value);
|
|
120
|
-
return typeof external === "string" ? Date.parse(`${external}T00:00:00.000Z`) : undefined;
|
|
121
|
-
};
|
|
122
|
-
const temporalLeft = temporal(left);
|
|
123
|
-
const temporalRight = temporal(right);
|
|
124
|
-
if (temporalLeft !== undefined || temporalRight !== undefined) {
|
|
125
|
-
const comparableLeft = temporalLeft ?? (typeof left === "number" ? left : undefined);
|
|
126
|
-
const comparableRight = temporalRight ?? (typeof right === "number" ? right : undefined);
|
|
127
|
-
if (comparableLeft === undefined || comparableRight === undefined) {
|
|
128
|
-
throw new TypeError("Values must have comparable SQL types");
|
|
129
|
-
}
|
|
130
|
-
return comparableLeft - comparableRight;
|
|
43
|
+
function compareSqlValues(left, right) {
|
|
44
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
45
|
+
if (left < right)
|
|
46
|
+
return -1;
|
|
47
|
+
if (left > right)
|
|
48
|
+
return 1;
|
|
49
|
+
if (left === right)
|
|
50
|
+
return 0;
|
|
51
|
+
} else if (typeof left === "string" && typeof right === "string" && left.charCodeAt(0) !== 0 && right.charCodeAt(0) !== 0) {
|
|
52
|
+
return compareSqlStrings(left, right);
|
|
53
|
+
}
|
|
54
|
+
const [coercedLeft, coercedRight] = coerceComparisonOperands(left, right);
|
|
55
|
+
if (!Object.is(coercedLeft, left) || !Object.is(coercedRight, right)) {
|
|
56
|
+
return compareSqlValues(coercedLeft, coercedRight);
|
|
57
|
+
}
|
|
58
|
+
const plainLeft = externalSqlTextValue(left);
|
|
59
|
+
const plainRight = externalSqlTextValue(right);
|
|
60
|
+
if (typeof left === "string" && plainLeft !== left || typeof right === "string" && plainRight !== right) {
|
|
61
|
+
if (typeof plainLeft !== "string" || typeof plainRight !== "string") {
|
|
62
|
+
throw new TypeError("Values must have comparable SQL types");
|
|
131
63
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
64
|
+
return compareSqlStrings(plainLeft, plainRight);
|
|
65
|
+
}
|
|
66
|
+
const collated = collatedDomainCompare(left, right);
|
|
67
|
+
if (collated !== void 0)
|
|
68
|
+
return collated;
|
|
69
|
+
const interval = intervalDomainCompare(left, right);
|
|
70
|
+
if (interval !== void 0)
|
|
71
|
+
return interval;
|
|
72
|
+
const enumOrder = enumDomainCompare(left, right);
|
|
73
|
+
if (enumOrder !== void 0)
|
|
74
|
+
return enumOrder;
|
|
75
|
+
const exact = exactNumericCompare(left, right);
|
|
76
|
+
if (exact !== void 0)
|
|
77
|
+
return exact;
|
|
78
|
+
const temporal = (value) => {
|
|
79
|
+
if (value instanceof Date)
|
|
80
|
+
return dateMilliseconds(value);
|
|
81
|
+
if (!isDateDomainValue(value))
|
|
82
|
+
return void 0;
|
|
83
|
+
const external = externalSqlDomainValue(value);
|
|
84
|
+
return typeof external === "string" ? Date.parse(`${external}T00:00:00.000Z`) : void 0;
|
|
85
|
+
};
|
|
86
|
+
const temporalLeft = temporal(left);
|
|
87
|
+
const temporalRight = temporal(right);
|
|
88
|
+
if (temporalLeft !== void 0 || temporalRight !== void 0) {
|
|
89
|
+
const comparableLeft = temporalLeft ?? (typeof left === "number" ? left : void 0);
|
|
90
|
+
const comparableRight = temporalRight ?? (typeof right === "number" ? right : void 0);
|
|
91
|
+
if (comparableLeft === void 0 || comparableRight === void 0) {
|
|
92
|
+
throw new TypeError("Values must have comparable SQL types");
|
|
146
93
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
94
|
+
return comparableLeft - comparableRight;
|
|
95
|
+
}
|
|
96
|
+
const a = left;
|
|
97
|
+
const b = right;
|
|
98
|
+
if (a === b)
|
|
99
|
+
return 0;
|
|
100
|
+
if (a === null || a === void 0)
|
|
101
|
+
return -1;
|
|
102
|
+
if (b === null || b === void 0)
|
|
103
|
+
return 1;
|
|
104
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
105
|
+
if (Number.isNaN(a))
|
|
106
|
+
return Number.isNaN(b) ? 0 : 1;
|
|
107
|
+
if (Number.isNaN(b))
|
|
108
|
+
return -1;
|
|
109
|
+
return a - b;
|
|
110
|
+
}
|
|
111
|
+
if (typeof a === "string" && typeof b === "string")
|
|
112
|
+
return compareSqlStrings(a, b);
|
|
113
|
+
if (typeof a === "boolean" && typeof b === "boolean")
|
|
114
|
+
return Number(a) - Number(b);
|
|
115
|
+
throw new TypeError("Values must have comparable SQL types");
|
|
152
116
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return left < right ? -1 : 1;
|
|
117
|
+
function compareSqlStrings(left, right) {
|
|
118
|
+
if (left === right)
|
|
119
|
+
return 0;
|
|
120
|
+
return left < right ? -1 : 1;
|
|
158
121
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
throw new TypeError("Query produced an unsupported value");
|
|
122
|
+
function encodeSqlEqualityValue(value) {
|
|
123
|
+
if (value === null || value === void 0)
|
|
124
|
+
return [0];
|
|
125
|
+
if (typeof value === "boolean")
|
|
126
|
+
return [1, value];
|
|
127
|
+
if (typeof value === "number")
|
|
128
|
+
return [2, String(value === 0 ? 0 : value)];
|
|
129
|
+
if (typeof value === "string")
|
|
130
|
+
return [3, externalSqlTextValue(value)];
|
|
131
|
+
if (value instanceof Date)
|
|
132
|
+
return [4, dateMilliseconds(value)];
|
|
133
|
+
throw new TypeError("Query produced an unsupported value");
|
|
172
134
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
}
|
|
190
|
-
// SQLite formats with the requested decimal precision and parses the result back. toFixed
|
|
191
|
-
// follows the same decimal path, avoiding multiplication overflow and binary scaling drift.
|
|
192
|
-
const rounded = Number(value.toFixed(digits));
|
|
193
|
-
return rounded === 0 ? 0 : rounded;
|
|
135
|
+
function roundSqlNumber(value, precision = 0) {
|
|
136
|
+
const digits = Math.min(30, Math.max(0, Math.trunc(precision)));
|
|
137
|
+
if (digits === 0 && Number.isFinite(value)) {
|
|
138
|
+
const magnitude = Math.abs(value);
|
|
139
|
+
if (magnitude >= 4503599627370496)
|
|
140
|
+
return value === 0 ? 0 : value;
|
|
141
|
+
const whole = Math.floor(magnitude);
|
|
142
|
+
const rounded2 = magnitude - whole >= 0.5 ? whole + 1 : whole;
|
|
143
|
+
return rounded2 === 0 ? 0 : value < 0 ? -rounded2 : rounded2;
|
|
144
|
+
}
|
|
145
|
+
const rounded = Number(value.toFixed(digits));
|
|
146
|
+
return rounded === 0 ? 0 : rounded;
|
|
194
147
|
}
|
|
195
148
|
const MAX_PATTERN_CACHE_ENTRIES = 128;
|
|
196
149
|
const MAX_PATTERN_CACHE_RETAINED_SIZE = 64 * 1024;
|
|
197
|
-
const likeCache = new Map();
|
|
198
|
-
const similarCache = new Map();
|
|
150
|
+
const likeCache = /* @__PURE__ */ new Map();
|
|
151
|
+
const similarCache = /* @__PURE__ */ new Map();
|
|
199
152
|
let likeCacheRetainedSize = 0;
|
|
200
153
|
let similarCacheRetainedSize = 0;
|
|
201
154
|
function cachedPattern(cache, key) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
155
|
+
const entry = cache.get(key);
|
|
156
|
+
if (entry === void 0)
|
|
157
|
+
return void 0;
|
|
158
|
+
cache.delete(key);
|
|
159
|
+
cache.set(key, entry);
|
|
160
|
+
return entry[0];
|
|
208
161
|
}
|
|
209
162
|
function retainPattern(cache, key, matcher, retainedSize, setRetainedSize) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
setRetainedSize(retainedSize + entrySize);
|
|
163
|
+
const entrySize = key.length + matcher.retainedSize;
|
|
164
|
+
if (entrySize > MAX_PATTERN_CACHE_RETAINED_SIZE)
|
|
165
|
+
return;
|
|
166
|
+
while (cache.size >= MAX_PATTERN_CACHE_ENTRIES || retainedSize + entrySize > MAX_PATTERN_CACHE_RETAINED_SIZE) {
|
|
167
|
+
const oldest = cache.keys().next().value;
|
|
168
|
+
if (oldest === void 0)
|
|
169
|
+
break;
|
|
170
|
+
const removed = cache.get(oldest);
|
|
171
|
+
cache.delete(oldest);
|
|
172
|
+
retainedSize -= removed?.[1] ?? 0;
|
|
173
|
+
}
|
|
174
|
+
cache.set(key, [matcher, entrySize]);
|
|
175
|
+
setRetainedSize(retainedSize + entrySize);
|
|
224
176
|
}
|
|
225
177
|
function throwPatternWorkLimit() {
|
|
226
|
-
|
|
178
|
+
throw new RangeError(`SQL pattern match exceeds ${String(MAX_SQL_PATTERN_MATCH_STEPS)} deterministic steps`);
|
|
227
179
|
}
|
|
228
180
|
function exactSingleCharacter(value, label) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
181
|
+
assertBoundedPattern(value, label, 2);
|
|
182
|
+
const characters = value[Symbol.iterator]();
|
|
183
|
+
const first = characters.next();
|
|
184
|
+
if (first.done === true || characters.next().done !== true) {
|
|
185
|
+
throw new TypeError(`${label.replace(" escape", " ESCAPE")} takes one character`);
|
|
186
|
+
}
|
|
187
|
+
return first.value;
|
|
236
188
|
}
|
|
237
189
|
function tokenizeLike(pattern, escape) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (escaped)
|
|
258
|
-
throw new TypeError("LIKE pattern ends with a dangling escape character");
|
|
259
|
-
return tokens;
|
|
190
|
+
const tokens = [];
|
|
191
|
+
let escaped = false;
|
|
192
|
+
for (const character of pattern) {
|
|
193
|
+
if (escaped) {
|
|
194
|
+
tokens.push(character);
|
|
195
|
+
escaped = false;
|
|
196
|
+
} else if (escape !== void 0 && character === escape) {
|
|
197
|
+
escaped = true;
|
|
198
|
+
} else if (character === "%") {
|
|
199
|
+
if (tokens.at(-1) !== 0)
|
|
200
|
+
tokens.push(0);
|
|
201
|
+
} else if (character === "_")
|
|
202
|
+
tokens.push(1);
|
|
203
|
+
else
|
|
204
|
+
tokens.push(character);
|
|
205
|
+
}
|
|
206
|
+
if (escaped)
|
|
207
|
+
throw new TypeError("LIKE pattern ends with a dangling escape character");
|
|
208
|
+
return tokens;
|
|
260
209
|
}
|
|
261
|
-
/** Unicode simple-case approximation used consistently by row and vector LIKE execution. */
|
|
262
210
|
function simpleCaseFold(character) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
211
|
+
const upper = character.toUpperCase();
|
|
212
|
+
const codePoints = upper[Symbol.iterator]();
|
|
213
|
+
return codePoints.next().done !== true && codePoints.next().done === true ? upper : character;
|
|
266
214
|
}
|
|
267
215
|
function likeCharactersEqual(left, right, caseInsensitive) {
|
|
268
|
-
|
|
216
|
+
return left === right || caseInsensitive && simpleCaseFold(left) === simpleCaseFold(right);
|
|
269
217
|
}
|
|
270
218
|
function literalLikeMatcher(tokens, caseInsensitive) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (trailing)
|
|
292
|
-
return value.startsWith(body);
|
|
293
|
-
return value === body;
|
|
294
|
-
};
|
|
295
|
-
return { test, retainedSize: tokens.length * 16 + body.length * 2 };
|
|
296
|
-
}
|
|
297
|
-
const needle = Array.from(body, simpleCaseFold);
|
|
298
|
-
const prefix = new Uint32Array(needle.length);
|
|
299
|
-
for (let index = 1, matched = 0; index < needle.length; index += 1) {
|
|
300
|
-
while (matched > 0 && needle[index] !== needle[matched])
|
|
301
|
-
matched = prefix[matched - 1] ?? 0;
|
|
302
|
-
if (needle[index] === needle[matched])
|
|
303
|
-
matched += 1;
|
|
304
|
-
prefix[index] = matched;
|
|
305
|
-
}
|
|
306
|
-
const test = (value) => {
|
|
307
|
-
if (typeof value !== "string")
|
|
308
|
-
throw new TypeError("LIKE input must be a string");
|
|
309
|
-
if (needle.length === 0)
|
|
310
|
-
return leading || trailing || value.length === 0;
|
|
311
|
-
let matched = 0;
|
|
312
|
-
let position = 0;
|
|
313
|
-
let matchedAtEnd = false;
|
|
314
|
-
for (const rawCharacter of value) {
|
|
315
|
-
matchedAtEnd = false;
|
|
316
|
-
const character = simpleCaseFold(rawCharacter);
|
|
317
|
-
while (matched > 0 && character !== needle[matched])
|
|
318
|
-
matched = prefix[matched - 1] ?? 0;
|
|
319
|
-
if (character === needle[matched])
|
|
320
|
-
matched += 1;
|
|
321
|
-
position += 1;
|
|
322
|
-
if (matched === needle.length) {
|
|
323
|
-
const startsAt = position - needle.length;
|
|
324
|
-
if (leading && trailing)
|
|
325
|
-
return true;
|
|
326
|
-
if (!leading && startsAt === 0) {
|
|
327
|
-
if (trailing)
|
|
328
|
-
return true;
|
|
329
|
-
matchedAtEnd = true;
|
|
330
|
-
}
|
|
331
|
-
else if (leading && !trailing)
|
|
332
|
-
matchedAtEnd = true;
|
|
333
|
-
matched = prefix[matched - 1] ?? 0;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
return matchedAtEnd;
|
|
337
|
-
};
|
|
338
|
-
return {
|
|
339
|
-
test,
|
|
340
|
-
retainedSize: tokens.length * 16 + body.length * 2 + prefix.byteLength,
|
|
219
|
+
if (tokens.includes(1))
|
|
220
|
+
return void 0;
|
|
221
|
+
const leading = tokens[0] === 0;
|
|
222
|
+
const trailing = tokens.at(-1) === 0;
|
|
223
|
+
const begin = leading ? 1 : 0;
|
|
224
|
+
const end = trailing ? tokens.length - 1 : tokens.length;
|
|
225
|
+
if (tokens.slice(begin, end).some((token) => typeof token !== "string"))
|
|
226
|
+
return void 0;
|
|
227
|
+
const body = tokens.slice(begin, end).map((token) => typeof token === "string" ? token : "").join("");
|
|
228
|
+
if (!caseInsensitive) {
|
|
229
|
+
const test2 = (value) => {
|
|
230
|
+
if (typeof value !== "string")
|
|
231
|
+
throw new TypeError("LIKE input must be a string");
|
|
232
|
+
if (leading && trailing)
|
|
233
|
+
return value.includes(body);
|
|
234
|
+
if (leading)
|
|
235
|
+
return value.endsWith(body);
|
|
236
|
+
if (trailing)
|
|
237
|
+
return value.startsWith(body);
|
|
238
|
+
return value === body;
|
|
341
239
|
};
|
|
240
|
+
return { test: test2, retainedSize: tokens.length * 16 + body.length * 2 };
|
|
241
|
+
}
|
|
242
|
+
const needle = Array.from(body, simpleCaseFold);
|
|
243
|
+
const prefix = new Uint32Array(needle.length);
|
|
244
|
+
for (let index = 1, matched = 0; index < needle.length; index += 1) {
|
|
245
|
+
while (matched > 0 && needle[index] !== needle[matched])
|
|
246
|
+
matched = prefix[matched - 1] ?? 0;
|
|
247
|
+
if (needle[index] === needle[matched])
|
|
248
|
+
matched += 1;
|
|
249
|
+
prefix[index] = matched;
|
|
250
|
+
}
|
|
251
|
+
const test = (value) => {
|
|
252
|
+
if (typeof value !== "string")
|
|
253
|
+
throw new TypeError("LIKE input must be a string");
|
|
254
|
+
if (needle.length === 0)
|
|
255
|
+
return leading || trailing || value.length === 0;
|
|
256
|
+
let matched = 0;
|
|
257
|
+
let position = 0;
|
|
258
|
+
let matchedAtEnd = false;
|
|
259
|
+
for (const rawCharacter of value) {
|
|
260
|
+
matchedAtEnd = false;
|
|
261
|
+
const character = simpleCaseFold(rawCharacter);
|
|
262
|
+
while (matched > 0 && character !== needle[matched])
|
|
263
|
+
matched = prefix[matched - 1] ?? 0;
|
|
264
|
+
if (character === needle[matched])
|
|
265
|
+
matched += 1;
|
|
266
|
+
position += 1;
|
|
267
|
+
if (matched === needle.length) {
|
|
268
|
+
const startsAt = position - needle.length;
|
|
269
|
+
if (leading && trailing)
|
|
270
|
+
return true;
|
|
271
|
+
if (!leading && startsAt === 0) {
|
|
272
|
+
if (trailing)
|
|
273
|
+
return true;
|
|
274
|
+
matchedAtEnd = true;
|
|
275
|
+
} else if (leading && !trailing)
|
|
276
|
+
matchedAtEnd = true;
|
|
277
|
+
matched = prefix[matched - 1] ?? 0;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return matchedAtEnd;
|
|
281
|
+
};
|
|
282
|
+
return {
|
|
283
|
+
test,
|
|
284
|
+
retainedSize: tokens.length * 16 + body.length * 2 + prefix.byteLength
|
|
285
|
+
};
|
|
342
286
|
}
|
|
343
287
|
function generalLikeMatcher(tokens, caseInsensitive) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
return { test, retainedSize: tokens.length * 16 };
|
|
288
|
+
const test = (value) => {
|
|
289
|
+
if (typeof value !== "string")
|
|
290
|
+
throw new TypeError("LIKE input must be a string");
|
|
291
|
+
let remaining = MAX_SQL_PATTERN_MATCH_STEPS;
|
|
292
|
+
let tokenIndex = 0;
|
|
293
|
+
let valueIndex = 0;
|
|
294
|
+
let manyIndex = -1;
|
|
295
|
+
let retryValueIndex = 0;
|
|
296
|
+
while (valueIndex < value.length) {
|
|
297
|
+
if (--remaining < 0)
|
|
298
|
+
throwPatternWorkLimit();
|
|
299
|
+
const token = tokens[tokenIndex];
|
|
300
|
+
if (token === 0) {
|
|
301
|
+
manyIndex = tokenIndex;
|
|
302
|
+
tokenIndex += 1;
|
|
303
|
+
retryValueIndex = valueIndex;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const codePoint = value.codePointAt(valueIndex);
|
|
307
|
+
if (codePoint === void 0)
|
|
308
|
+
break;
|
|
309
|
+
const character = String.fromCodePoint(codePoint);
|
|
310
|
+
if (token === 1 || typeof token === "string" && likeCharactersEqual(token, character, caseInsensitive)) {
|
|
311
|
+
tokenIndex += 1;
|
|
312
|
+
valueIndex += character.length;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (manyIndex < 0)
|
|
316
|
+
return false;
|
|
317
|
+
const retryCodePoint = value.codePointAt(retryValueIndex);
|
|
318
|
+
if (retryCodePoint === void 0)
|
|
319
|
+
return false;
|
|
320
|
+
retryValueIndex += String.fromCodePoint(retryCodePoint).length;
|
|
321
|
+
valueIndex = retryValueIndex;
|
|
322
|
+
tokenIndex = manyIndex + 1;
|
|
323
|
+
}
|
|
324
|
+
while (tokens[tokenIndex] === 0) {
|
|
325
|
+
if (--remaining < 0)
|
|
326
|
+
throwPatternWorkLimit();
|
|
327
|
+
tokenIndex += 1;
|
|
328
|
+
}
|
|
329
|
+
return tokenIndex === tokens.length;
|
|
330
|
+
};
|
|
331
|
+
return { test, retainedSize: tokens.length * 16 };
|
|
389
332
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
return matcher;
|
|
333
|
+
function compileLikePattern(pattern, caseInsensitive = false, escape) {
|
|
334
|
+
assertBoundedPattern(pattern, "LIKE pattern");
|
|
335
|
+
const exactEscape = escape === void 0 ? void 0 : exactSingleCharacter(escape, "LIKE escape");
|
|
336
|
+
const key = JSON.stringify([caseInsensitive, exactEscape ?? null, pattern]);
|
|
337
|
+
const cached = cachedPattern(likeCache, key);
|
|
338
|
+
if (cached !== void 0)
|
|
339
|
+
return cached;
|
|
340
|
+
const tokens = tokenizeLike(pattern, exactEscape);
|
|
341
|
+
const matcher = literalLikeMatcher(tokens, caseInsensitive) ?? generalLikeMatcher(tokens, caseInsensitive);
|
|
342
|
+
retainPattern(likeCache, key, matcher, likeCacheRetainedSize, (size) => {
|
|
343
|
+
likeCacheRetainedSize = size;
|
|
344
|
+
});
|
|
345
|
+
return matcher;
|
|
404
346
|
}
|
|
405
347
|
const ANY_CHARACTER = () => true;
|
|
406
|
-
/** Parses SIMILAR TO directly into an NFA, avoiding an allocation-heavy intermediate AST. */
|
|
407
348
|
class SimilarCompiler {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
349
|
+
escape;
|
|
350
|
+
#characters;
|
|
351
|
+
states = [];
|
|
352
|
+
#index = 0;
|
|
353
|
+
constructor(pattern, escape) {
|
|
354
|
+
this.escape = escape;
|
|
355
|
+
this.#characters = Array.from(pattern);
|
|
356
|
+
}
|
|
357
|
+
compile() {
|
|
358
|
+
const fragment = this.#alternative(0);
|
|
359
|
+
if (this.#index !== this.#characters.length) {
|
|
360
|
+
throw new TypeError("SIMILAR TO pattern has an unmatched closing parenthesis");
|
|
415
361
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
362
|
+
return fragment;
|
|
363
|
+
}
|
|
364
|
+
#state() {
|
|
365
|
+
this.states.push([[]]);
|
|
366
|
+
return this.states.length - 1;
|
|
367
|
+
}
|
|
368
|
+
#empty() {
|
|
369
|
+
const start = this.#state();
|
|
370
|
+
const end = this.#state();
|
|
371
|
+
this.states[start]?.[0].push(end);
|
|
372
|
+
return [start, end];
|
|
373
|
+
}
|
|
374
|
+
#consume(matches) {
|
|
375
|
+
const start = this.#state();
|
|
376
|
+
const end = this.#state();
|
|
377
|
+
const state = this.states[start];
|
|
378
|
+
if (state !== void 0)
|
|
379
|
+
state[1] = [end, matches];
|
|
380
|
+
return [start, end];
|
|
381
|
+
}
|
|
382
|
+
#repeat(fragment, plus) {
|
|
383
|
+
const start = this.#state();
|
|
384
|
+
const end = this.#state();
|
|
385
|
+
this.states[start]?.[0].push(fragment[0]);
|
|
386
|
+
if (!plus)
|
|
387
|
+
this.states[start]?.[0].push(end);
|
|
388
|
+
this.states[fragment[1]]?.[0].push(fragment[0], end);
|
|
389
|
+
return [start, end];
|
|
390
|
+
}
|
|
391
|
+
#alternative(depth) {
|
|
392
|
+
const first = this.#sequence(depth);
|
|
393
|
+
if (this.#characters[this.#index] !== "|" || this.#characters[this.#index] === this.escape) {
|
|
394
|
+
return first;
|
|
422
395
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
396
|
+
const start = this.#state();
|
|
397
|
+
const end = this.#state();
|
|
398
|
+
this.states[start]?.[0].push(first[0]);
|
|
399
|
+
this.states[first[1]]?.[0].push(end);
|
|
400
|
+
while (this.#characters[this.#index] === "|" && this.#characters[this.#index] !== this.escape) {
|
|
401
|
+
this.#index += 1;
|
|
402
|
+
const next = this.#sequence(depth);
|
|
403
|
+
this.states[start]?.[0].push(next[0]);
|
|
404
|
+
this.states[next[1]]?.[0].push(end);
|
|
426
405
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
406
|
+
return [start, end];
|
|
407
|
+
}
|
|
408
|
+
#sequence(depth) {
|
|
409
|
+
let result;
|
|
410
|
+
for (; ; ) {
|
|
411
|
+
const character = this.#characters[this.#index];
|
|
412
|
+
if (character === void 0 || (character === "|" || character === ")") && character !== this.escape) {
|
|
413
|
+
return result ?? this.#empty();
|
|
414
|
+
}
|
|
415
|
+
let fragment = this.#atom(depth);
|
|
416
|
+
const quantifier = this.#characters[this.#index];
|
|
417
|
+
if ((quantifier === "*" || quantifier === "+") && quantifier !== this.escape) {
|
|
418
|
+
this.#index += 1;
|
|
419
|
+
fragment = this.#repeat(fragment, quantifier === "+");
|
|
420
|
+
const repeated = this.#characters[this.#index];
|
|
421
|
+
if ((repeated === "*" || repeated === "+") && repeated !== this.escape) {
|
|
422
|
+
throw new TypeError("SIMILAR TO pattern repeats a quantifier");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (result === void 0)
|
|
426
|
+
result = fragment;
|
|
427
|
+
else {
|
|
428
|
+
this.states[result[1]]?.[0].push(fragment[0]);
|
|
429
|
+
result = [result[0], fragment[1]];
|
|
430
|
+
}
|
|
432
431
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
432
|
+
}
|
|
433
|
+
#atom(depth) {
|
|
434
|
+
const character = this.#characters[this.#index];
|
|
435
|
+
if (character === void 0)
|
|
436
|
+
return this.#empty();
|
|
437
|
+
this.#index += 1;
|
|
438
|
+
if (character === this.escape) {
|
|
439
|
+
const literal = this.#characters[this.#index];
|
|
440
|
+
if (literal === void 0) {
|
|
441
|
+
throw new TypeError("SIMILAR TO pattern ends with its escape character");
|
|
442
|
+
}
|
|
443
|
+
this.#index += 1;
|
|
444
|
+
return this.#consume((candidate) => candidate === literal);
|
|
440
445
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const end = this.#state();
|
|
444
|
-
this.states[start]?.[0].push(fragment[0]);
|
|
445
|
-
if (!plus)
|
|
446
|
-
this.states[start]?.[0].push(end);
|
|
447
|
-
this.states[fragment[1]]?.[0].push(fragment[0], end);
|
|
448
|
-
return [start, end];
|
|
446
|
+
if (character === "*" || character === "+") {
|
|
447
|
+
throw new TypeError("SIMILAR TO quantifier has no preceding expression");
|
|
449
448
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
this.#index += 1;
|
|
461
|
-
const next = this.#sequence(depth);
|
|
462
|
-
this.states[start]?.[0].push(next[0]);
|
|
463
|
-
this.states[next[1]]?.[0].push(end);
|
|
464
|
-
}
|
|
465
|
-
return [start, end];
|
|
449
|
+
if (character === "(") {
|
|
450
|
+
if (depth >= MAX_SQL_NESTING_DEPTH) {
|
|
451
|
+
throw new RangeError(`SIMILAR TO nesting exceeds ${String(MAX_SQL_NESTING_DEPTH)} levels`);
|
|
452
|
+
}
|
|
453
|
+
const child = this.#alternative(depth + 1);
|
|
454
|
+
if (this.#characters[this.#index] !== ")") {
|
|
455
|
+
throw new TypeError("SIMILAR TO pattern has an unmatched opening parenthesis");
|
|
456
|
+
}
|
|
457
|
+
this.#index += 1;
|
|
458
|
+
return child;
|
|
466
459
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
result = fragment;
|
|
487
|
-
else {
|
|
488
|
-
this.states[result[1]]?.[0].push(fragment[0]);
|
|
489
|
-
result = [result[0], fragment[1]];
|
|
490
|
-
}
|
|
460
|
+
if (character === "[")
|
|
461
|
+
return this.#consume(this.#characterClass());
|
|
462
|
+
if (character === "%")
|
|
463
|
+
return this.#repeat(this.#consume(ANY_CHARACTER), false);
|
|
464
|
+
if (character === "_")
|
|
465
|
+
return this.#consume(ANY_CHARACTER);
|
|
466
|
+
return this.#consume((candidate) => candidate === character);
|
|
467
|
+
}
|
|
468
|
+
#characterClass() {
|
|
469
|
+
let source = "[";
|
|
470
|
+
let closed = false;
|
|
471
|
+
let hasMember = false;
|
|
472
|
+
while (this.#index < this.#characters.length) {
|
|
473
|
+
const character = this.#characters[this.#index] ?? "";
|
|
474
|
+
this.#index += 1;
|
|
475
|
+
if (character === this.escape) {
|
|
476
|
+
const literal = this.#characters[this.#index];
|
|
477
|
+
if (literal === void 0) {
|
|
478
|
+
throw new TypeError("SIMILAR TO pattern ends with its escape character");
|
|
491
479
|
}
|
|
492
|
-
}
|
|
493
|
-
#atom(depth) {
|
|
494
|
-
const character = this.#characters[this.#index];
|
|
495
|
-
if (character === undefined)
|
|
496
|
-
return this.#empty();
|
|
497
480
|
this.#index += 1;
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if (character === "(") {
|
|
510
|
-
if (depth >= MAX_SQL_NESTING_DEPTH) {
|
|
511
|
-
throw new RangeError(`SIMILAR TO nesting exceeds ${String(MAX_SQL_NESTING_DEPTH)} levels`);
|
|
512
|
-
}
|
|
513
|
-
const child = this.#alternative(depth + 1);
|
|
514
|
-
if (this.#characters[this.#index] !== ")") {
|
|
515
|
-
throw new TypeError("SIMILAR TO pattern has an unmatched opening parenthesis");
|
|
516
|
-
}
|
|
517
|
-
this.#index += 1;
|
|
518
|
-
return child;
|
|
519
|
-
}
|
|
520
|
-
if (character === "[")
|
|
521
|
-
return this.#consume(this.#characterClass());
|
|
522
|
-
if (character === "%")
|
|
523
|
-
return this.#repeat(this.#consume(ANY_CHARACTER), false);
|
|
524
|
-
if (character === "_")
|
|
525
|
-
return this.#consume(ANY_CHARACTER);
|
|
526
|
-
return this.#consume((candidate) => candidate === character);
|
|
481
|
+
source += ["\\", "]", "^", "-"].includes(literal) ? `\\${literal}` : literal;
|
|
482
|
+
hasMember = true;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (character === "]") {
|
|
486
|
+
closed = true;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
source += character;
|
|
490
|
+
if (character !== "^" || source.length > 2)
|
|
491
|
+
hasMember = true;
|
|
527
492
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
throw new TypeError("SIMILAR TO pattern ends with its escape character");
|
|
539
|
-
}
|
|
540
|
-
this.#index += 1;
|
|
541
|
-
source += ["\\", "]", "^", "-"].includes(literal) ? `\\${literal}` : literal;
|
|
542
|
-
hasMember = true;
|
|
543
|
-
continue;
|
|
544
|
-
}
|
|
545
|
-
if (character === "]") {
|
|
546
|
-
closed = true;
|
|
547
|
-
break;
|
|
548
|
-
}
|
|
549
|
-
source += character;
|
|
550
|
-
if (character !== "^" || source.length > 2)
|
|
551
|
-
hasMember = true;
|
|
552
|
-
}
|
|
553
|
-
if (!closed)
|
|
554
|
-
throw new TypeError("SIMILAR TO pattern has an unterminated character class");
|
|
555
|
-
if (!hasMember)
|
|
556
|
-
throw new TypeError("SIMILAR TO pattern has an empty character class");
|
|
557
|
-
source += "]";
|
|
558
|
-
let expression;
|
|
559
|
-
try {
|
|
560
|
-
// The host expression is deliberately limited to one character class and one codepoint.
|
|
561
|
-
// It cannot contain repetition, grouping, or alternation, so backtracking work is constant.
|
|
562
|
-
expression = new RegExp(`^(?:${source})$`, "u");
|
|
563
|
-
}
|
|
564
|
-
catch {
|
|
565
|
-
throw new TypeError("SIMILAR TO pattern has an invalid character class");
|
|
566
|
-
}
|
|
567
|
-
return (candidate) => expression.test(candidate);
|
|
493
|
+
if (!closed)
|
|
494
|
+
throw new TypeError("SIMILAR TO pattern has an unterminated character class");
|
|
495
|
+
if (!hasMember)
|
|
496
|
+
throw new TypeError("SIMILAR TO pattern has an empty character class");
|
|
497
|
+
source += "]";
|
|
498
|
+
let expression;
|
|
499
|
+
try {
|
|
500
|
+
expression = new RegExp(`^(?:${source})$`, "u");
|
|
501
|
+
} catch {
|
|
502
|
+
throw new TypeError("SIMILAR TO pattern has an invalid character class");
|
|
568
503
|
}
|
|
504
|
+
return (candidate) => expression.test(candidate);
|
|
505
|
+
}
|
|
569
506
|
}
|
|
570
507
|
function nfaMatcher(pattern, escape) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
};
|
|
597
|
-
addClosure(fragment[0], active);
|
|
598
|
-
for (const character of value) {
|
|
599
|
-
generation += 1;
|
|
600
|
-
next.length = 0;
|
|
601
|
-
for (const stateIndex of active) {
|
|
602
|
-
if (--remaining < 0)
|
|
603
|
-
throwPatternWorkLimit();
|
|
604
|
-
const transition = states[stateIndex]?.[1];
|
|
605
|
-
if (transition?.[1](character) === true)
|
|
606
|
-
addClosure(transition[0], next);
|
|
607
|
-
}
|
|
608
|
-
if (next.length === 0)
|
|
609
|
-
return false;
|
|
610
|
-
[active, next] = [next, active];
|
|
611
|
-
}
|
|
612
|
-
return active.includes(fragment[1]);
|
|
508
|
+
const compiler = new SimilarCompiler(pattern, escape);
|
|
509
|
+
const fragment = compiler.compile();
|
|
510
|
+
const { states } = compiler;
|
|
511
|
+
const test = (value) => {
|
|
512
|
+
if (typeof value !== "string")
|
|
513
|
+
throw new TypeError("SIMILAR TO input must be a string");
|
|
514
|
+
let remaining = MAX_SQL_PATTERN_MATCH_STEPS;
|
|
515
|
+
const marks = new Uint32Array(states.length);
|
|
516
|
+
const stack = [];
|
|
517
|
+
let active = [];
|
|
518
|
+
let next = [];
|
|
519
|
+
let generation = 1;
|
|
520
|
+
const addClosure = (seed, output) => {
|
|
521
|
+
stack.push(seed);
|
|
522
|
+
while (stack.length > 0) {
|
|
523
|
+
if (--remaining < 0)
|
|
524
|
+
throwPatternWorkLimit();
|
|
525
|
+
const stateIndex = stack.pop();
|
|
526
|
+
if (stateIndex === void 0 || marks[stateIndex] === generation)
|
|
527
|
+
continue;
|
|
528
|
+
marks[stateIndex] = generation;
|
|
529
|
+
output.push(stateIndex);
|
|
530
|
+
for (const target of states[stateIndex]?.[0] ?? [])
|
|
531
|
+
stack.push(target);
|
|
532
|
+
}
|
|
613
533
|
};
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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;
|
|
534
|
+
addClosure(fragment[0], active);
|
|
535
|
+
for (const character of value) {
|
|
536
|
+
generation += 1;
|
|
537
|
+
next.length = 0;
|
|
538
|
+
for (const stateIndex of active) {
|
|
539
|
+
if (--remaining < 0)
|
|
540
|
+
throwPatternWorkLimit();
|
|
541
|
+
const transition = states[stateIndex]?.[1];
|
|
542
|
+
if (transition?.[1](character) === true)
|
|
543
|
+
addClosure(transition[0], next);
|
|
544
|
+
}
|
|
545
|
+
if (next.length === 0)
|
|
546
|
+
return false;
|
|
547
|
+
[active, next] = [next, active];
|
|
634
548
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
549
|
+
return active.includes(fragment[1]);
|
|
550
|
+
};
|
|
551
|
+
return { test, retainedSize: states.length * 32 };
|
|
552
|
+
}
|
|
553
|
+
const regexCache = /* @__PURE__ */ new Map();
|
|
554
|
+
function compileRegexPattern(pattern, flags = "") {
|
|
555
|
+
assertBoundedPattern(pattern, "regular expression");
|
|
556
|
+
const normalized = [...new Set(flags)].sort().join("");
|
|
557
|
+
const key = `${normalized}\0${pattern}`;
|
|
558
|
+
const cached = regexCache.get(key);
|
|
559
|
+
if (cached !== void 0) {
|
|
560
|
+
cached.lastIndex = 0;
|
|
561
|
+
return cached;
|
|
562
|
+
}
|
|
563
|
+
let jsFlags = "u";
|
|
564
|
+
if (normalized.includes("i"))
|
|
565
|
+
jsFlags += "i";
|
|
566
|
+
if (normalized.includes("g"))
|
|
567
|
+
jsFlags += "g";
|
|
568
|
+
if (normalized.includes("n"))
|
|
569
|
+
jsFlags += "m";
|
|
570
|
+
else
|
|
571
|
+
jsFlags += "s";
|
|
572
|
+
let expression;
|
|
573
|
+
try {
|
|
574
|
+
expression = new RegExp(pattern, jsFlags);
|
|
575
|
+
} catch {
|
|
645
576
|
try {
|
|
646
|
-
|
|
577
|
+
expression = new RegExp(pattern, jsFlags.replace("u", ""));
|
|
578
|
+
} catch (error) {
|
|
579
|
+
throw new TypeError(`Invalid regular expression: ${error instanceof Error ? error.message : pattern}`, { cause: error });
|
|
647
580
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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;
|
|
581
|
+
}
|
|
582
|
+
if (regexCache.size >= 256)
|
|
583
|
+
regexCache.clear();
|
|
584
|
+
regexCache.set(key, expression);
|
|
585
|
+
return expression;
|
|
660
586
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
return matcher;
|
|
587
|
+
function compileSimilarPattern(pattern, escape = "\\") {
|
|
588
|
+
assertBoundedPattern(pattern, "SIMILAR TO pattern");
|
|
589
|
+
const exactEscape = exactSingleCharacter(escape, "SIMILAR TO escape");
|
|
590
|
+
const key = JSON.stringify([exactEscape, pattern]);
|
|
591
|
+
const cached = cachedPattern(similarCache, key);
|
|
592
|
+
if (cached !== void 0)
|
|
593
|
+
return cached;
|
|
594
|
+
const matcher = nfaMatcher(pattern, exactEscape);
|
|
595
|
+
retainPattern(similarCache, key, matcher, similarCacheRetainedSize, (size) => {
|
|
596
|
+
similarCacheRetainedSize = size;
|
|
597
|
+
});
|
|
598
|
+
return matcher;
|
|
674
599
|
}
|
|
675
600
|
function assertBoundedPattern(value, label, limit = MAX_SQL_PATTERN_CHARACTERS) {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
601
|
+
if (value.length > limit) {
|
|
602
|
+
throw new RangeError(`${label} exceeds ${String(limit)} characters`);
|
|
603
|
+
}
|
|
604
|
+
assertWellFormedString(value, label);
|
|
680
605
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
});
|
|
606
|
+
function defineSqlResultProperty(target, name, value) {
|
|
607
|
+
Object.defineProperty(target, name, {
|
|
608
|
+
value,
|
|
609
|
+
enumerable: true,
|
|
610
|
+
configurable: true,
|
|
611
|
+
writable: true
|
|
612
|
+
});
|
|
689
613
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
export function stringArgument(name, value) {
|
|
696
|
-
const external = externalSqlDomainValue(value);
|
|
697
|
-
if (typeof external !== "string")
|
|
698
|
-
throw new TypeError(`${name} requires a string argument`);
|
|
699
|
-
return external;
|
|
614
|
+
function stringArgument(name, value) {
|
|
615
|
+
const external = externalSqlDomainValue(value);
|
|
616
|
+
if (typeof external !== "string")
|
|
617
|
+
throw new TypeError(`${name} requires a string argument`);
|
|
618
|
+
return external;
|
|
700
619
|
}
|
|
620
|
+
export {
|
|
621
|
+
coerceComparisonOperands,
|
|
622
|
+
coercedComparable,
|
|
623
|
+
compareSqlStrings,
|
|
624
|
+
compareSqlValues,
|
|
625
|
+
compileLikePattern,
|
|
626
|
+
compileRegexPattern,
|
|
627
|
+
compileSimilarPattern,
|
|
628
|
+
defineSqlResultProperty,
|
|
629
|
+
encodeSqlEqualityValue,
|
|
630
|
+
parseSqlTimestampText,
|
|
631
|
+
roundSqlNumber,
|
|
632
|
+
stringArgument
|
|
633
|
+
};
|