@minnowdb/core 0.6.9 → 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.d.ts +5 -0
- package/dist/date-value.js +71 -41
- 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 +15952 -18465
- 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 +2642 -2515
- package/dist/engine/point-read.d.ts +4 -2
- package/dist/engine/point-read.js +121 -171
- package/dist/engine/query-api.js +12 -3
- package/dist/engine/query-cache.js +61 -69
- package/dist/engine/query.d.ts +45 -0
- package/dist/engine/query.js +8506 -9375
- 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.d.ts +7 -1
- package/dist/engine/sql-domains.js +648 -734
- package/dist/engine/sql-driver.js +0 -1
- package/dist/engine/sql-functions.d.ts +11 -0
- package/dist/engine/sql-functions.js +1092 -0
- package/dist/engine/sql-json.js +199 -218
- package/dist/engine/sql-semantics.d.ts +25 -4
- package/dist/engine/sql-semantics.js +579 -513
- package/dist/engine/typed-live.js +271 -293
- package/dist/engine/vector.d.ts +7 -1
- package/dist/engine/vector.js +4513 -4580
- 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 +9 -1
- 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 +2952 -3228
- 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
- package/postgres-feature-profile.json +15 -5
- package/sql-feature-matrix.json +252 -2
|
@@ -1,434 +1,494 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
class SqlLogicParseError extends Error {
|
|
2
|
+
location;
|
|
3
|
+
constructor(message, location) {
|
|
4
|
+
super(`${location.file}:${String(location.line)}: ${message}`);
|
|
5
|
+
this.location = location;
|
|
6
|
+
this.name = "SqlLogicParseError";
|
|
7
|
+
}
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
class SqlLogicFailure extends Error {
|
|
10
|
+
location;
|
|
11
|
+
sql;
|
|
12
|
+
constructor(message, location, sql, options) {
|
|
13
|
+
super(`${location.file}:${String(location.line)}: ${message}${sql === void 0 ? "" : `
|
|
14
|
+
${sql}`}`, options);
|
|
15
|
+
this.location = location;
|
|
16
|
+
this.sql = sql;
|
|
17
|
+
this.name = "SqlLogicFailure";
|
|
18
|
+
}
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return groups.map((group) => parseRecord(group, file));
|
|
20
|
+
function parseSqlLogicTest(source, file = "<memory>") {
|
|
21
|
+
const groups = splitRecordGroups(source);
|
|
22
|
+
return groups.map((group) => parseRecord(group, file));
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const record = finish();
|
|
45
|
-
if (record !== undefined)
|
|
46
|
-
yield record;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
current.push({ text, number: lineNumber });
|
|
24
|
+
async function* parseSqlLogicTestLines(lines, file = "<stream>") {
|
|
25
|
+
let current = [];
|
|
26
|
+
let lineNumber = 0;
|
|
27
|
+
const finish = () => {
|
|
28
|
+
if (current.length === 0)
|
|
29
|
+
return void 0;
|
|
30
|
+
const record2 = parseRecord({ lines: current }, file);
|
|
31
|
+
current = [];
|
|
32
|
+
return record2;
|
|
33
|
+
};
|
|
34
|
+
for await (const original of lines) {
|
|
35
|
+
lineNumber++;
|
|
36
|
+
const text = original.endsWith("\r") ? original.slice(0, -1) : original;
|
|
37
|
+
if (text.startsWith("#"))
|
|
38
|
+
continue;
|
|
39
|
+
if (text.trim().length === 0) {
|
|
40
|
+
const record2 = finish();
|
|
41
|
+
if (record2 !== void 0)
|
|
42
|
+
yield record2;
|
|
43
|
+
continue;
|
|
50
44
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
current.push({ text, number: lineNumber });
|
|
46
|
+
}
|
|
47
|
+
const record = finish();
|
|
48
|
+
if (record !== void 0)
|
|
49
|
+
yield record;
|
|
54
50
|
}
|
|
55
51
|
function splitRecordGroups(source) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
current.push({ text, number: index + 1 });
|
|
52
|
+
const lines = source.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
|
|
53
|
+
const groups = [];
|
|
54
|
+
let current = [];
|
|
55
|
+
const finish = () => {
|
|
56
|
+
if (current.length > 0)
|
|
57
|
+
groups.push({ lines: current });
|
|
58
|
+
current = [];
|
|
59
|
+
};
|
|
60
|
+
for (const [index, text] of lines.entries()) {
|
|
61
|
+
if (text.startsWith("#"))
|
|
62
|
+
continue;
|
|
63
|
+
if (text.trim().length === 0) {
|
|
64
|
+
finish();
|
|
65
|
+
continue;
|
|
72
66
|
}
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
current.push({ text, number: index + 1 });
|
|
68
|
+
}
|
|
69
|
+
finish();
|
|
70
|
+
return groups;
|
|
75
71
|
}
|
|
76
72
|
function parseRecord(group, file) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
73
|
+
let index = 0;
|
|
74
|
+
const conditions = [];
|
|
75
|
+
while (index < group.lines.length) {
|
|
76
|
+
const line = requireLine(group, index, file);
|
|
77
|
+
const tokens2 = tokenizeDirective(line.text);
|
|
78
|
+
if (tokens2[0] !== "skipif" && tokens2[0] !== "onlyif")
|
|
79
|
+
break;
|
|
80
|
+
if (tokens2.length !== 2)
|
|
81
|
+
fail("conditional must name exactly one engine", file, line.number);
|
|
82
|
+
conditions.push({
|
|
83
|
+
kind: tokens2[0],
|
|
84
|
+
engine: tokens2[1] ?? "",
|
|
85
|
+
location: { file, line: line.number }
|
|
86
|
+
});
|
|
87
|
+
index++;
|
|
88
|
+
}
|
|
89
|
+
const header = requireLine(group, index, file);
|
|
90
|
+
const tokens = tokenizeDirective(header.text);
|
|
91
|
+
const location = { file, line: header.number };
|
|
92
|
+
if (tokens[0] === "statement") {
|
|
93
|
+
if (tokens.length !== 2 || tokens[1] !== "ok" && tokens[1] !== "error") {
|
|
94
|
+
fail("statement must be 'statement ok' or 'statement error'", file, header.number);
|
|
92
95
|
}
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
conditions,
|
|
108
|
-
location,
|
|
109
|
-
};
|
|
96
|
+
const sql = sqlLines(group.lines.slice(index + 1));
|
|
97
|
+
if (sql.length === 0)
|
|
98
|
+
fail("statement has no SQL", file, header.number);
|
|
99
|
+
return {
|
|
100
|
+
kind: "statement",
|
|
101
|
+
expectation: tokens[1],
|
|
102
|
+
sql,
|
|
103
|
+
conditions,
|
|
104
|
+
location
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (tokens[0] === "query") {
|
|
108
|
+
if (tokens.length < 2 || tokens.length > 4) {
|
|
109
|
+
fail("query must be 'query <types> [sort-mode] [label]'", file, header.number);
|
|
110
110
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
for (const type of typeString)
|
|
118
|
-
types.push(type);
|
|
119
|
-
if (types.length === 0 || types.some((type) => type !== "I" && type !== "R" && type !== "T")) {
|
|
120
|
-
fail("query type string must contain only I, R, and T", file, header.number);
|
|
121
|
-
}
|
|
122
|
-
const sortMode = tokens[2] ?? "nosort";
|
|
123
|
-
if (sortMode !== "nosort" && sortMode !== "rowsort" && sortMode !== "valuesort") {
|
|
124
|
-
fail(`unknown query sort mode '${sortMode}'`, file, header.number);
|
|
125
|
-
}
|
|
126
|
-
const body = group.lines.slice(index + 1);
|
|
127
|
-
const separator = body.findIndex((line) => line.text === "----");
|
|
128
|
-
const queryLines = separator < 0 ? body : body.slice(0, separator);
|
|
129
|
-
const sql = sqlLines(queryLines);
|
|
130
|
-
if (sql.length === 0)
|
|
131
|
-
fail("query has no SQL", file, header.number);
|
|
132
|
-
const expectedLines = separator < 0 ? [] : body.slice(separator + 1).map((line) => line.text);
|
|
133
|
-
return {
|
|
134
|
-
kind: "query",
|
|
135
|
-
types: types,
|
|
136
|
-
sortMode,
|
|
137
|
-
...(tokens[3] === undefined ? {} : { label: tokens[3] }),
|
|
138
|
-
sql,
|
|
139
|
-
expected: parseExpected(expectedLines, file, header.number),
|
|
140
|
-
conditions,
|
|
141
|
-
location,
|
|
142
|
-
};
|
|
111
|
+
const typeString = tokens[1] ?? "";
|
|
112
|
+
const types = [];
|
|
113
|
+
for (const type of typeString)
|
|
114
|
+
types.push(type);
|
|
115
|
+
if (types.length === 0 || types.some((type) => type !== "I" && type !== "R" && type !== "T")) {
|
|
116
|
+
fail("query type string must contain only I, R, and T", file, header.number);
|
|
143
117
|
}
|
|
144
|
-
|
|
145
|
-
|
|
118
|
+
const sortMode = tokens[2] ?? "nosort";
|
|
119
|
+
if (sortMode !== "nosort" && sortMode !== "rowsort" && sortMode !== "valuesort") {
|
|
120
|
+
fail(`unknown query sort mode '${sortMode}'`, file, header.number);
|
|
146
121
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
122
|
+
const body = group.lines.slice(index + 1);
|
|
123
|
+
const separator = body.findIndex((line) => line.text === "----");
|
|
124
|
+
const queryLines = separator < 0 ? body : body.slice(0, separator);
|
|
125
|
+
const sql = sqlLines(queryLines);
|
|
126
|
+
if (sql.length === 0)
|
|
127
|
+
fail("query has no SQL", file, header.number);
|
|
128
|
+
const expectedLines = separator < 0 ? [] : body.slice(separator + 1).map((line) => line.text);
|
|
129
|
+
return {
|
|
130
|
+
kind: "query",
|
|
131
|
+
types,
|
|
132
|
+
sortMode,
|
|
133
|
+
...tokens[3] === void 0 ? {} : { label: tokens[3] },
|
|
134
|
+
sql,
|
|
135
|
+
expected: parseExpected(expectedLines, file, header.number),
|
|
136
|
+
conditions,
|
|
137
|
+
location
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (conditions.length > 0) {
|
|
141
|
+
fail("conditionals may prefix only a statement or query", file, header.number);
|
|
142
|
+
}
|
|
143
|
+
if (tokens[0] === "hash-threshold") {
|
|
144
|
+
if (tokens.length !== 2 || !isNonNegativeInteger(tokens[1])) {
|
|
145
|
+
fail("hash-threshold must be a non-negative whole number", file, header.number);
|
|
152
146
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
147
|
+
return { kind: "hash-threshold", valueCount: Number(tokens[1]), location };
|
|
148
|
+
}
|
|
149
|
+
if (tokens[0] === "halt") {
|
|
150
|
+
if (tokens.length !== 1 || group.lines.length !== 1) {
|
|
151
|
+
fail("halt does not accept arguments", file, header.number);
|
|
158
152
|
}
|
|
159
|
-
|
|
153
|
+
return { kind: "halt", location };
|
|
154
|
+
}
|
|
155
|
+
fail(`unknown SQLLogicTest directive '${tokens[0] ?? ""}'`, file, header.number);
|
|
160
156
|
}
|
|
161
157
|
function parseExpected(lines, file, line) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
158
|
+
if (lines.length !== 1)
|
|
159
|
+
return { kind: "values", values: lines };
|
|
160
|
+
const match = /^(\d+) values hashing to ([0-9a-fA-F]{32})$/.exec(lines[0] ?? "");
|
|
161
|
+
if (match === null)
|
|
162
|
+
return { kind: "values", values: lines };
|
|
163
|
+
const valueCount = Number(match[1]);
|
|
164
|
+
if (!Number.isSafeInteger(valueCount))
|
|
165
|
+
fail("hashed value count is too large", file, line);
|
|
166
|
+
return { kind: "hash", valueCount, hash: (match[2] ?? "").toLowerCase() };
|
|
171
167
|
}
|
|
172
168
|
function tokenizeDirective(line) {
|
|
173
|
-
|
|
169
|
+
return line.trim().split(/\s+/u);
|
|
174
170
|
}
|
|
175
171
|
function sqlLines(lines) {
|
|
176
|
-
|
|
172
|
+
return lines.map((line) => line.text).join("\n");
|
|
177
173
|
}
|
|
178
174
|
function requireLine(group, index, file) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
175
|
+
const line = group.lines[index];
|
|
176
|
+
if (line === void 0) {
|
|
177
|
+
const last = group.lines.at(-1)?.number ?? 1;
|
|
178
|
+
fail("conditional is missing its statement or query", file, last);
|
|
179
|
+
}
|
|
180
|
+
return line;
|
|
185
181
|
}
|
|
186
182
|
function isNonNegativeInteger(value) {
|
|
187
|
-
|
|
183
|
+
return value !== void 0 && /^\d+$/u.test(value) && Number.isSafeInteger(Number(value));
|
|
188
184
|
}
|
|
189
185
|
function fail(message, file, line) {
|
|
190
|
-
|
|
186
|
+
throw new SqlLogicParseError(message, { file, line });
|
|
191
187
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
rememberExpectedLabel(record, labels);
|
|
215
|
-
}
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
try {
|
|
219
|
-
if (record.kind === "statement") {
|
|
220
|
-
statements++;
|
|
221
|
-
await runStatement(record, database);
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
queries++;
|
|
225
|
-
const actual = await runQuery(record, database);
|
|
226
|
-
values += actual.length;
|
|
227
|
-
compareExpected(record, actual, hashThreshold);
|
|
228
|
-
rememberActualLabel(record, actual, labels);
|
|
229
|
-
}
|
|
230
|
-
catch (error) {
|
|
231
|
-
if (error instanceof SqlLogicFailure && options.onFailure?.(error, record) === "continue") {
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
throw error;
|
|
235
|
-
}
|
|
188
|
+
async function runSqlLogicTest(records, database, options = {}) {
|
|
189
|
+
let statements = 0;
|
|
190
|
+
let queries = 0;
|
|
191
|
+
let values = 0;
|
|
192
|
+
let skipped = 0;
|
|
193
|
+
let hashThreshold = 8;
|
|
194
|
+
let halted = false;
|
|
195
|
+
const labels = /* @__PURE__ */ new Map();
|
|
196
|
+
try {
|
|
197
|
+
for await (const record of records) {
|
|
198
|
+
if (record.kind === "hash-threshold") {
|
|
199
|
+
hashThreshold = record.valueCount;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (record.kind === "halt") {
|
|
203
|
+
halted = true;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
if (shouldSkip(record.conditions, database.engineName)) {
|
|
207
|
+
skipped++;
|
|
208
|
+
if (record.kind === "query" && record.label !== void 0) {
|
|
209
|
+
rememberExpectedLabel(record, labels);
|
|
236
210
|
}
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
if (record.kind === "statement") {
|
|
215
|
+
statements++;
|
|
216
|
+
await runStatement(record, database);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
queries++;
|
|
220
|
+
const actual = await runQuery(record, database);
|
|
221
|
+
values += actual.length;
|
|
222
|
+
compareExpected(record, actual, hashThreshold);
|
|
223
|
+
rememberActualLabel(record, actual, labels);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (error instanceof SqlLogicFailure && options.onFailure?.(error, record) === "continue") {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
237
230
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
231
|
+
} finally {
|
|
232
|
+
await database.close();
|
|
233
|
+
}
|
|
234
|
+
return { files: 1, statements, queries, values, skipped, halted, hashThreshold };
|
|
242
235
|
}
|
|
243
236
|
function shouldSkip(conditions, engineName) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
237
|
+
const engine = engineName.toLowerCase();
|
|
238
|
+
return conditions.some((condition) => {
|
|
239
|
+
const matches = condition.engine.toLowerCase() === engine;
|
|
240
|
+
return condition.kind === "skipif" ? matches : !matches;
|
|
241
|
+
});
|
|
249
242
|
}
|
|
250
243
|
async function runStatement(record, database) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
244
|
+
let failure;
|
|
245
|
+
try {
|
|
246
|
+
await database.execute(record.sql);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
failure = error;
|
|
249
|
+
}
|
|
250
|
+
if (record.expectation === "error") {
|
|
251
|
+
if (failure !== void 0)
|
|
252
|
+
return;
|
|
253
|
+
throw new SqlLogicFailure("statement succeeded; expected an error", record.location, record.sql);
|
|
254
|
+
}
|
|
255
|
+
if (failure !== void 0) {
|
|
256
|
+
throw new SqlLogicFailure("statement failed; expected success", record.location, record.sql, {
|
|
257
|
+
cause: failure
|
|
258
|
+
});
|
|
259
|
+
}
|
|
268
260
|
}
|
|
269
261
|
async function runQuery(record, database) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
return flattened;
|
|
262
|
+
let result;
|
|
263
|
+
try {
|
|
264
|
+
result = await database.query(record.sql);
|
|
265
|
+
} catch (cause) {
|
|
266
|
+
throw new SqlLogicFailure("query failed", record.location, record.sql, { cause });
|
|
267
|
+
}
|
|
268
|
+
if (result.columns.length !== record.types.length) {
|
|
269
|
+
throw new SqlLogicFailure(`wrong column count: expected ${String(record.types.length)}, received ${String(result.columns.length)}`, record.location, record.sql);
|
|
270
|
+
}
|
|
271
|
+
const rows = result.rows.map((row) => result.columns.map((column, index) => renderSqlLogicValue(row[column] ?? null, record.types[index])));
|
|
272
|
+
if (record.sortMode === "rowsort")
|
|
273
|
+
rows.sort(compareRows);
|
|
274
|
+
const flattened = rows.flat();
|
|
275
|
+
if (record.sortMode === "valuesort")
|
|
276
|
+
flattened.sort(compareText);
|
|
277
|
+
return flattened;
|
|
287
278
|
}
|
|
288
279
|
function compareRows(left, right) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
280
|
+
for (let index = 0; index < left.length; index++) {
|
|
281
|
+
const comparison = compareText(left[index] ?? "", right[index] ?? "");
|
|
282
|
+
if (comparison !== 0)
|
|
283
|
+
return comparison;
|
|
284
|
+
}
|
|
285
|
+
return 0;
|
|
295
286
|
}
|
|
296
287
|
function compareText(left, right) {
|
|
297
|
-
|
|
288
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
298
289
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
return rendered.replaceAll(/[^ -~]/gu, "@");
|
|
290
|
+
function renderSqlLogicValue(value, type) {
|
|
291
|
+
if (value === null)
|
|
292
|
+
return "NULL";
|
|
293
|
+
if (type === "I") {
|
|
294
|
+
const numeric = typeof value === "boolean" ? Number(value) : Number(value);
|
|
295
|
+
if (!Number.isFinite(numeric))
|
|
296
|
+
return "0";
|
|
297
|
+
return String(Math.trunc(numeric));
|
|
298
|
+
}
|
|
299
|
+
if (type === "R") {
|
|
300
|
+
const numeric = typeof value === "boolean" ? Number(value) : Number(value);
|
|
301
|
+
return Number.isFinite(numeric) ? numeric.toFixed(3) : "0.000";
|
|
302
|
+
}
|
|
303
|
+
const rendered = value instanceof Date ? value.toISOString() : String(value);
|
|
304
|
+
if (rendered.length === 0)
|
|
305
|
+
return "(empty)";
|
|
306
|
+
return rendered.replaceAll(/[^ -~]/gu, "@");
|
|
317
307
|
}
|
|
318
308
|
function compareExpected(record, actual, hashThreshold) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
const hash = md5Hex(withTrailingNewlines(actual));
|
|
325
|
-
if (record.expected.valueCount !== actual.length || record.expected.hash !== hash) {
|
|
326
|
-
throw new SqlLogicFailure(`wrong result hash: expected ${String(record.expected.valueCount)} values hashing to ${record.expected.hash}, received ${String(actual.length)} values hashing to ${hash}`, record.location, record.sql);
|
|
327
|
-
}
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
if (record.expected.kind === "hash") {
|
|
331
|
-
throw new SqlLogicFailure(`expected result is hashed but ${String(actual.length)} values do not exceed hash-threshold ${String(hashThreshold)}`, record.location, record.sql);
|
|
309
|
+
const hashed = hashThreshold > 0 && actual.length > hashThreshold;
|
|
310
|
+
if (hashed) {
|
|
311
|
+
if (record.expected.kind !== "hash") {
|
|
312
|
+
throw new SqlLogicFailure(`result exceeds hash-threshold ${String(hashThreshold)} but the expected result is not hashed`, record.location, record.sql);
|
|
332
313
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
throw new SqlLogicFailure(`wrong result:\nexpected ${expected}\nreceived ${received}`, record.location, record.sql);
|
|
314
|
+
const hash = md5Hex(withTrailingNewlines(actual));
|
|
315
|
+
if (record.expected.valueCount !== actual.length || record.expected.hash !== hash) {
|
|
316
|
+
throw new SqlLogicFailure(`wrong result hash: expected ${String(record.expected.valueCount)} values hashing to ${record.expected.hash}, received ${String(actual.length)} values hashing to ${hash}`, record.location, record.sql);
|
|
337
317
|
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (record.expected.kind === "hash") {
|
|
321
|
+
throw new SqlLogicFailure(`expected result is hashed but ${String(actual.length)} values do not exceed hash-threshold ${String(hashThreshold)}`, record.location, record.sql);
|
|
322
|
+
}
|
|
323
|
+
if (!equalValues(record.expected.values, actual)) {
|
|
324
|
+
const expected = describeValues(record.expected.values, hashThreshold);
|
|
325
|
+
const received = describeValues(actual, hashThreshold);
|
|
326
|
+
throw new SqlLogicFailure(`wrong result:
|
|
327
|
+
expected ${expected}
|
|
328
|
+
received ${received}`, record.location, record.sql);
|
|
329
|
+
}
|
|
338
330
|
}
|
|
339
331
|
function equalValues(expected, actual) {
|
|
340
|
-
|
|
332
|
+
return expected.length === actual.length && expected.every((value, index) => value === actual[index]);
|
|
341
333
|
}
|
|
342
334
|
function describeValues(values, hashThreshold) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
335
|
+
if (hashThreshold > 0 && values.length > hashThreshold) {
|
|
336
|
+
return `${String(values.length)} values hashing to ${md5Hex(withTrailingNewlines(values))}`;
|
|
337
|
+
}
|
|
338
|
+
return JSON.stringify(values);
|
|
347
339
|
}
|
|
348
340
|
function rememberExpectedLabel(record, labels) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
: md5Hex(withTrailingNewlines(record.expected.values));
|
|
354
|
-
rememberLabel(record.label, hash, record.location, labels);
|
|
341
|
+
if (record.label === void 0)
|
|
342
|
+
return;
|
|
343
|
+
const hash = record.expected.kind === "hash" ? record.expected.hash : md5Hex(withTrailingNewlines(record.expected.values));
|
|
344
|
+
rememberLabel(record.label, hash, record.location, labels);
|
|
355
345
|
}
|
|
356
346
|
function rememberActualLabel(record, values, labels) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
347
|
+
if (record.label === void 0)
|
|
348
|
+
return;
|
|
349
|
+
rememberLabel(record.label, md5Hex(withTrailingNewlines(values)), record.location, labels);
|
|
360
350
|
}
|
|
361
351
|
function rememberLabel(label, hash, location, labels) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
352
|
+
const previous = labels.get(label);
|
|
353
|
+
if (previous !== void 0 && previous.hash !== hash) {
|
|
354
|
+
throw new SqlLogicFailure(`labeled result '${label}' differs from ${previous.location.file}:${String(previous.location.line)}`, location);
|
|
355
|
+
}
|
|
356
|
+
labels.set(label, { hash, location });
|
|
367
357
|
}
|
|
368
358
|
function withTrailingNewlines(values) {
|
|
369
|
-
|
|
359
|
+
return values.map((value) => `${value}
|
|
360
|
+
`).join("");
|
|
370
361
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
for (let
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
b = (b + rotated) >>> 0;
|
|
401
|
-
}
|
|
402
|
-
a0 = (a0 + a) >>> 0;
|
|
403
|
-
b0 = (b0 + b) >>> 0;
|
|
404
|
-
c0 = (c0 + c) >>> 0;
|
|
405
|
-
d0 = (d0 + d) >>> 0;
|
|
362
|
+
function md5Hex(input) {
|
|
363
|
+
const bytes = new TextEncoder().encode(input);
|
|
364
|
+
const paddedLength = Math.ceil((bytes.length + 9) / 64) * 64;
|
|
365
|
+
const padded = new Uint8Array(paddedLength);
|
|
366
|
+
padded.set(bytes);
|
|
367
|
+
padded[bytes.length] = 128;
|
|
368
|
+
const bitLength = BigInt(bytes.length) * 8n;
|
|
369
|
+
const view = new DataView(padded.buffer);
|
|
370
|
+
view.setUint32(paddedLength - 8, Number(bitLength & 0xffffffffn), true);
|
|
371
|
+
view.setUint32(paddedLength - 4, Number(bitLength >> 32n & 0xffffffffn), true);
|
|
372
|
+
let a0 = 1732584193;
|
|
373
|
+
let b0 = 4023233417;
|
|
374
|
+
let c0 = 2562383102;
|
|
375
|
+
let d0 = 271733878;
|
|
376
|
+
const words = new Uint32Array(16);
|
|
377
|
+
for (let offset = 0; offset < paddedLength; offset += 64) {
|
|
378
|
+
for (let index = 0; index < 16; index++)
|
|
379
|
+
words[index] = view.getUint32(offset + index * 4, true);
|
|
380
|
+
let a = a0;
|
|
381
|
+
let b = b0;
|
|
382
|
+
let c = c0;
|
|
383
|
+
let d = d0;
|
|
384
|
+
for (let index = 0; index < 64; index++) {
|
|
385
|
+
const { mix, word } = md5Round(index, b, c, d);
|
|
386
|
+
const rotated = rotateLeft(a + mix + (MD5_CONSTANTS[index] ?? 0) + (words[word] ?? 0) >>> 0, MD5_SHIFTS[index]);
|
|
387
|
+
a = d;
|
|
388
|
+
d = c;
|
|
389
|
+
c = b;
|
|
390
|
+
b = b + rotated >>> 0;
|
|
406
391
|
}
|
|
407
|
-
|
|
392
|
+
a0 = a0 + a >>> 0;
|
|
393
|
+
b0 = b0 + b >>> 0;
|
|
394
|
+
c0 = c0 + c >>> 0;
|
|
395
|
+
d0 = d0 + d >>> 0;
|
|
396
|
+
}
|
|
397
|
+
return [a0, b0, c0, d0].map(littleEndianHex).join("");
|
|
408
398
|
}
|
|
409
399
|
function md5Round(index, b, c, d) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
400
|
+
if (index < 16)
|
|
401
|
+
return { mix: b & c | ~b & d, word: index };
|
|
402
|
+
if (index < 32)
|
|
403
|
+
return { mix: d & b | ~d & c, word: (5 * index + 1) % 16 };
|
|
404
|
+
if (index < 48)
|
|
405
|
+
return { mix: b ^ c ^ d, word: (3 * index + 5) % 16 };
|
|
406
|
+
return { mix: c ^ (b | ~d), word: 7 * index % 16 };
|
|
417
407
|
}
|
|
418
408
|
function rotateLeft(value, shift) {
|
|
419
|
-
|
|
420
|
-
|
|
409
|
+
const bits = shift ?? 0;
|
|
410
|
+
return (value << bits | value >>> 32 - bits) >>> 0;
|
|
421
411
|
}
|
|
422
412
|
function littleEndianHex(value) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
413
|
+
let output = "";
|
|
414
|
+
for (let index = 0; index < 4; index++) {
|
|
415
|
+
output += (value >>> index * 8 & 255).toString(16).padStart(2, "0");
|
|
416
|
+
}
|
|
417
|
+
return output;
|
|
428
418
|
}
|
|
429
419
|
const MD5_SHIFTS = [
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
420
|
+
7,
|
|
421
|
+
12,
|
|
422
|
+
17,
|
|
423
|
+
22,
|
|
424
|
+
7,
|
|
425
|
+
12,
|
|
426
|
+
17,
|
|
427
|
+
22,
|
|
428
|
+
7,
|
|
429
|
+
12,
|
|
430
|
+
17,
|
|
431
|
+
22,
|
|
432
|
+
7,
|
|
433
|
+
12,
|
|
434
|
+
17,
|
|
435
|
+
22,
|
|
436
|
+
5,
|
|
437
|
+
9,
|
|
438
|
+
14,
|
|
439
|
+
20,
|
|
440
|
+
5,
|
|
441
|
+
9,
|
|
442
|
+
14,
|
|
443
|
+
20,
|
|
444
|
+
5,
|
|
445
|
+
9,
|
|
446
|
+
14,
|
|
447
|
+
20,
|
|
448
|
+
5,
|
|
449
|
+
9,
|
|
450
|
+
14,
|
|
451
|
+
20,
|
|
452
|
+
4,
|
|
453
|
+
11,
|
|
454
|
+
16,
|
|
455
|
+
23,
|
|
456
|
+
4,
|
|
457
|
+
11,
|
|
458
|
+
16,
|
|
459
|
+
23,
|
|
460
|
+
4,
|
|
461
|
+
11,
|
|
462
|
+
16,
|
|
463
|
+
23,
|
|
464
|
+
4,
|
|
465
|
+
11,
|
|
466
|
+
16,
|
|
467
|
+
23,
|
|
468
|
+
6,
|
|
469
|
+
10,
|
|
470
|
+
15,
|
|
471
|
+
21,
|
|
472
|
+
6,
|
|
473
|
+
10,
|
|
474
|
+
15,
|
|
475
|
+
21,
|
|
476
|
+
6,
|
|
477
|
+
10,
|
|
478
|
+
15,
|
|
479
|
+
21,
|
|
480
|
+
6,
|
|
481
|
+
10,
|
|
482
|
+
15,
|
|
483
|
+
21
|
|
433
484
|
];
|
|
434
485
|
const MD5_CONSTANTS = Array.from({ length: 64 }, (_, index) => Math.floor(Math.abs(Math.sin(index + 1)) * 2 ** 32));
|
|
486
|
+
export {
|
|
487
|
+
SqlLogicFailure,
|
|
488
|
+
SqlLogicParseError,
|
|
489
|
+
md5Hex,
|
|
490
|
+
parseSqlLogicTest,
|
|
491
|
+
parseSqlLogicTestLines,
|
|
492
|
+
renderSqlLogicValue,
|
|
493
|
+
runSqlLogicTest
|
|
494
|
+
};
|