@b9g/zen 0.1.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.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +1044 -0
- package/chunk-2IEEEMRN.js +38 -0
- package/chunk-56M5Z3A6.js +1346 -0
- package/chunk-QXGEP5PB.js +310 -0
- package/ddl-NAJM37GQ.js +9 -0
- package/package.json +102 -0
- package/src/bun.d.ts +50 -0
- package/src/bun.js +906 -0
- package/src/mysql.d.ts +62 -0
- package/src/mysql.js +573 -0
- package/src/postgres.d.ts +62 -0
- package/src/postgres.js +555 -0
- package/src/sqlite.d.ts +43 -0
- package/src/sqlite.js +447 -0
- package/src/zen.d.ts +14 -0
- package/src/zen.js +2143 -0
package/src/zen.js
ADDED
|
@@ -0,0 +1,2143 @@
|
|
|
1
|
+
/// <reference types="./zen.d.ts" />
|
|
2
|
+
import "../chunk-2IEEEMRN.js";
|
|
3
|
+
import {
|
|
4
|
+
AlreadyExistsError,
|
|
5
|
+
CURRENT_DATE,
|
|
6
|
+
CURRENT_TIME,
|
|
7
|
+
CURRENT_TIMESTAMP,
|
|
8
|
+
ConnectionError,
|
|
9
|
+
ConstraintPreflightError,
|
|
10
|
+
ConstraintViolationError,
|
|
11
|
+
DatabaseError,
|
|
12
|
+
EnsureError,
|
|
13
|
+
MigrationError,
|
|
14
|
+
MigrationLockError,
|
|
15
|
+
NOW,
|
|
16
|
+
NotFoundError,
|
|
17
|
+
QueryError,
|
|
18
|
+
SchemaDriftError,
|
|
19
|
+
TODAY,
|
|
20
|
+
TableDefinitionError,
|
|
21
|
+
TransactionError,
|
|
22
|
+
ValidationError,
|
|
23
|
+
decodeData,
|
|
24
|
+
extendZod,
|
|
25
|
+
hasErrorCode,
|
|
26
|
+
ident,
|
|
27
|
+
isDatabaseError,
|
|
28
|
+
isSQLBuiltin,
|
|
29
|
+
isSQLIdentifier,
|
|
30
|
+
isSQLTemplate,
|
|
31
|
+
makeTemplate,
|
|
32
|
+
resolveSQLBuiltin,
|
|
33
|
+
table,
|
|
34
|
+
validateWithStandardSchema
|
|
35
|
+
} from "../chunk-56M5Z3A6.js";
|
|
36
|
+
|
|
37
|
+
// src/zen.ts
|
|
38
|
+
import { z as zod } from "zod";
|
|
39
|
+
|
|
40
|
+
// src/impl/database.ts
|
|
41
|
+
import { z } from "zod";
|
|
42
|
+
|
|
43
|
+
// src/impl/query.ts
|
|
44
|
+
function extractEntityData(row, tableName) {
|
|
45
|
+
const prefix = `${tableName}.`;
|
|
46
|
+
const entity = {};
|
|
47
|
+
let hasData = false;
|
|
48
|
+
for (const [key, value] of Object.entries(row)) {
|
|
49
|
+
if (key.startsWith(prefix)) {
|
|
50
|
+
const fieldName = key.slice(prefix.length);
|
|
51
|
+
entity[fieldName] = value;
|
|
52
|
+
if (value !== null && value !== void 0) {
|
|
53
|
+
hasData = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return hasData ? entity : null;
|
|
58
|
+
}
|
|
59
|
+
function getPrimaryKeyValue(entity, table2) {
|
|
60
|
+
const pk = table2.meta.primary;
|
|
61
|
+
if (pk === null) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const value = entity[pk];
|
|
65
|
+
return value !== null && value !== void 0 ? String(value) : null;
|
|
66
|
+
}
|
|
67
|
+
function entityKey(tableName, primaryKey) {
|
|
68
|
+
return `${tableName}:${primaryKey}`;
|
|
69
|
+
}
|
|
70
|
+
function buildEntityMap(rows, tables) {
|
|
71
|
+
const entities = /* @__PURE__ */ new Map();
|
|
72
|
+
for (const row of rows) {
|
|
73
|
+
for (const table2 of tables) {
|
|
74
|
+
const data = extractEntityData(row, table2.name);
|
|
75
|
+
if (!data)
|
|
76
|
+
continue;
|
|
77
|
+
const pk = getPrimaryKeyValue(data, table2);
|
|
78
|
+
if (!pk)
|
|
79
|
+
continue;
|
|
80
|
+
const key = entityKey(table2.name, pk);
|
|
81
|
+
if (!entities.has(key)) {
|
|
82
|
+
const decoded = decodeData(table2, data);
|
|
83
|
+
const parsed = validateWithStandardSchema(
|
|
84
|
+
table2.schema,
|
|
85
|
+
decoded
|
|
86
|
+
);
|
|
87
|
+
entities.set(key, parsed);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return entities;
|
|
92
|
+
}
|
|
93
|
+
function resolveReferences(entities, tables) {
|
|
94
|
+
for (const table2 of tables) {
|
|
95
|
+
const refs = table2.references();
|
|
96
|
+
if (refs.length === 0)
|
|
97
|
+
continue;
|
|
98
|
+
const prefix = `${table2.name}:`;
|
|
99
|
+
for (const [key, entity] of entities) {
|
|
100
|
+
if (!key.startsWith(prefix))
|
|
101
|
+
continue;
|
|
102
|
+
for (const ref of refs) {
|
|
103
|
+
const foreignKeyValue = entity[ref.fieldName];
|
|
104
|
+
const refEntity = foreignKeyValue === null || foreignKeyValue === void 0 ? null : entities.get(
|
|
105
|
+
entityKey(ref.table.name, String(foreignKeyValue))
|
|
106
|
+
) ?? null;
|
|
107
|
+
Object.defineProperty(entity, ref.as, {
|
|
108
|
+
value: refEntity,
|
|
109
|
+
enumerable: true,
|
|
110
|
+
writable: false,
|
|
111
|
+
configurable: true
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const reverseIndex = /* @__PURE__ */ new Map();
|
|
117
|
+
for (const table2 of tables) {
|
|
118
|
+
const refs = table2.references();
|
|
119
|
+
if (refs.length === 0)
|
|
120
|
+
continue;
|
|
121
|
+
for (const ref of refs) {
|
|
122
|
+
if (!ref.reverseAs)
|
|
123
|
+
continue;
|
|
124
|
+
const indexKey = `${table2.name}:${ref.fieldName}`;
|
|
125
|
+
const fkIndex = /* @__PURE__ */ new Map();
|
|
126
|
+
const prefix = `${table2.name}:`;
|
|
127
|
+
for (const [key, entity] of entities) {
|
|
128
|
+
if (!key.startsWith(prefix))
|
|
129
|
+
continue;
|
|
130
|
+
const fkValue = entity[ref.fieldName];
|
|
131
|
+
if (fkValue === null || fkValue === void 0)
|
|
132
|
+
continue;
|
|
133
|
+
const fkStr = String(fkValue);
|
|
134
|
+
if (!fkIndex.has(fkStr)) {
|
|
135
|
+
fkIndex.set(fkStr, []);
|
|
136
|
+
}
|
|
137
|
+
fkIndex.get(fkStr).push(entity);
|
|
138
|
+
}
|
|
139
|
+
reverseIndex.set(indexKey, fkIndex);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const table2 of tables) {
|
|
143
|
+
const refs = table2.references();
|
|
144
|
+
if (refs.length === 0)
|
|
145
|
+
continue;
|
|
146
|
+
for (const ref of refs) {
|
|
147
|
+
if (!ref.reverseAs)
|
|
148
|
+
continue;
|
|
149
|
+
const indexKey = `${table2.name}:${ref.fieldName}`;
|
|
150
|
+
const fkIndex = reverseIndex.get(indexKey);
|
|
151
|
+
if (!fkIndex)
|
|
152
|
+
continue;
|
|
153
|
+
const targetPrefix = `${ref.table.name}:`;
|
|
154
|
+
for (const [key, entity] of entities) {
|
|
155
|
+
if (!key.startsWith(targetPrefix))
|
|
156
|
+
continue;
|
|
157
|
+
const pk = entity[ref.table.meta.primary];
|
|
158
|
+
if (pk === null || pk === void 0)
|
|
159
|
+
continue;
|
|
160
|
+
const pkStr = String(pk);
|
|
161
|
+
Object.defineProperty(entity, ref.reverseAs, {
|
|
162
|
+
value: fkIndex.get(pkStr) ?? [],
|
|
163
|
+
enumerable: false,
|
|
164
|
+
writable: false,
|
|
165
|
+
configurable: true
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function applyDerivedProperties(entities, tables) {
|
|
172
|
+
for (const table2 of tables) {
|
|
173
|
+
const derive = table2.meta.derive;
|
|
174
|
+
if (!derive)
|
|
175
|
+
continue;
|
|
176
|
+
const prefix = `${table2.name}:`;
|
|
177
|
+
for (const [key, entity] of entities) {
|
|
178
|
+
if (!key.startsWith(prefix))
|
|
179
|
+
continue;
|
|
180
|
+
for (const [propName, deriveFn] of Object.entries(derive)) {
|
|
181
|
+
Object.defineProperty(entity, propName, {
|
|
182
|
+
get() {
|
|
183
|
+
return deriveFn(this);
|
|
184
|
+
},
|
|
185
|
+
enumerable: false,
|
|
186
|
+
configurable: true
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function extractMainEntities(rows, mainTable, entities) {
|
|
193
|
+
const results = [];
|
|
194
|
+
const seen = /* @__PURE__ */ new Set();
|
|
195
|
+
for (const row of rows) {
|
|
196
|
+
const data = extractEntityData(row, mainTable.name);
|
|
197
|
+
if (!data)
|
|
198
|
+
continue;
|
|
199
|
+
const pk = getPrimaryKeyValue(data, mainTable);
|
|
200
|
+
if (!pk)
|
|
201
|
+
continue;
|
|
202
|
+
const key = entityKey(mainTable.name, pk);
|
|
203
|
+
if (seen.has(key))
|
|
204
|
+
continue;
|
|
205
|
+
seen.add(key);
|
|
206
|
+
const entity = entities.get(key);
|
|
207
|
+
if (entity) {
|
|
208
|
+
results.push(entity);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return results;
|
|
212
|
+
}
|
|
213
|
+
function validateRegisteredTables(rows, tables) {
|
|
214
|
+
if (rows.length === 0)
|
|
215
|
+
return;
|
|
216
|
+
const registeredPrefixes = new Set(tables.map((t) => `${t.name}.`));
|
|
217
|
+
const unregisteredTables = [];
|
|
218
|
+
const firstRow = rows[0];
|
|
219
|
+
for (const key of Object.keys(firstRow)) {
|
|
220
|
+
const dotIndex = key.indexOf(".");
|
|
221
|
+
if (dotIndex === -1)
|
|
222
|
+
continue;
|
|
223
|
+
const prefix = key.slice(0, dotIndex + 1);
|
|
224
|
+
if (!registeredPrefixes.has(prefix)) {
|
|
225
|
+
const tableName = key.slice(0, dotIndex);
|
|
226
|
+
if (!unregisteredTables.includes(tableName)) {
|
|
227
|
+
unregisteredTables.push(tableName);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (unregisteredTables.length > 0) {
|
|
232
|
+
const tableList = unregisteredTables.map((t) => `"${t}"`).join(", ");
|
|
233
|
+
throw new Error(
|
|
234
|
+
`Query results contain columns for table(s) ${tableList} not passed to all()/one(). Add them to the tables array, or use query() for raw results.`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function normalize(rows, tables) {
|
|
239
|
+
if (tables.length === 0) {
|
|
240
|
+
throw new Error("At least one table is required");
|
|
241
|
+
}
|
|
242
|
+
if (rows.length === 0) {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
validateRegisteredTables(rows, tables);
|
|
246
|
+
const entities = buildEntityMap(rows, tables);
|
|
247
|
+
resolveReferences(entities, tables);
|
|
248
|
+
applyDerivedProperties(entities, tables);
|
|
249
|
+
const mainTable = tables[0];
|
|
250
|
+
return extractMainEntities(rows, mainTable, entities);
|
|
251
|
+
}
|
|
252
|
+
function normalizeOne(row, tables) {
|
|
253
|
+
if (!row)
|
|
254
|
+
return null;
|
|
255
|
+
const results = normalize([row], tables);
|
|
256
|
+
return results[0] ?? null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/impl/database.ts
|
|
260
|
+
var DB_EXPR = Symbol.for("@b9g/zen:db-expr");
|
|
261
|
+
function isDBExpression(value) {
|
|
262
|
+
return value !== null && typeof value === "object" && DB_EXPR in value && value[DB_EXPR] === true;
|
|
263
|
+
}
|
|
264
|
+
function createDBExpr(strings, values = []) {
|
|
265
|
+
return { [DB_EXPR]: true, strings, values };
|
|
266
|
+
}
|
|
267
|
+
function extractDBExpressions(data, table2) {
|
|
268
|
+
const regularData = {};
|
|
269
|
+
const expressions = {};
|
|
270
|
+
const symbols = {};
|
|
271
|
+
for (const [key, value] of Object.entries(data)) {
|
|
272
|
+
if (isDBExpression(value)) {
|
|
273
|
+
if (table2) {
|
|
274
|
+
const fieldMeta = table2.meta.fields[key];
|
|
275
|
+
if (fieldMeta?.encode || fieldMeta?.decode) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`Cannot use DB expression for field "${key}" which has encode/decode. DB expressions bypass encoding and are sent directly to the database.`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
expressions[key] = value;
|
|
282
|
+
} else if (isSQLBuiltin(value)) {
|
|
283
|
+
if (table2) {
|
|
284
|
+
const fieldMeta = table2.meta.fields[key];
|
|
285
|
+
if (fieldMeta?.encode || fieldMeta?.decode) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`Cannot use SQL symbol for field "${key}" which has encode/decode. SQL symbols bypass encoding and are sent directly to the database.`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
symbols[key] = value;
|
|
292
|
+
} else {
|
|
293
|
+
regularData[key] = value;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return { regularData, expressions, symbols };
|
|
297
|
+
}
|
|
298
|
+
function injectSchemaExpressions(table2, data, operation) {
|
|
299
|
+
const result = { ...data };
|
|
300
|
+
for (const [fieldName, fieldMeta] of Object.entries(table2.meta.fields)) {
|
|
301
|
+
if (fieldName in data)
|
|
302
|
+
continue;
|
|
303
|
+
if (fieldMeta.autoIncrement)
|
|
304
|
+
continue;
|
|
305
|
+
const meta = operation === "insert" ? fieldMeta.inserted ?? fieldMeta.upserted : fieldMeta.updated ?? fieldMeta.upserted;
|
|
306
|
+
if (!meta)
|
|
307
|
+
continue;
|
|
308
|
+
if (meta.type === "sql") {
|
|
309
|
+
result[fieldName] = createDBExpr(
|
|
310
|
+
meta.template[0],
|
|
311
|
+
meta.template.slice(1)
|
|
312
|
+
);
|
|
313
|
+
} else if (meta.type === "symbol") {
|
|
314
|
+
result[fieldName] = meta.symbol;
|
|
315
|
+
} else if (meta.type === "function") {
|
|
316
|
+
result[fieldName] = meta.fn();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
function encodeData(table2, data) {
|
|
322
|
+
const encoded = {};
|
|
323
|
+
const shape = table2.schema.shape;
|
|
324
|
+
for (const [key, value] of Object.entries(data)) {
|
|
325
|
+
const fieldMeta = table2.meta.fields[key];
|
|
326
|
+
const fieldSchema = shape?.[key];
|
|
327
|
+
if (fieldMeta?.encode && typeof fieldMeta.encode === "function") {
|
|
328
|
+
encoded[key] = fieldMeta.encode(value);
|
|
329
|
+
} else if (fieldSchema) {
|
|
330
|
+
let core = fieldSchema;
|
|
331
|
+
while (typeof core.unwrap === "function") {
|
|
332
|
+
if (core instanceof z.ZodArray || core instanceof z.ZodObject) {
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
core = core.unwrap();
|
|
336
|
+
}
|
|
337
|
+
if ((core instanceof z.ZodObject || core instanceof z.ZodArray) && value !== null && value !== void 0) {
|
|
338
|
+
encoded[key] = JSON.stringify(value);
|
|
339
|
+
} else {
|
|
340
|
+
encoded[key] = value;
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
encoded[key] = value;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return encoded;
|
|
347
|
+
}
|
|
348
|
+
function mergeExpression(baseStrings, baseValues, expr) {
|
|
349
|
+
baseStrings[baseStrings.length - 1] += expr.strings[0];
|
|
350
|
+
for (let i = 1; i < expr.strings.length; i++) {
|
|
351
|
+
baseStrings.push(expr.strings[i]);
|
|
352
|
+
}
|
|
353
|
+
baseValues.push(...expr.values);
|
|
354
|
+
}
|
|
355
|
+
function buildInsertParts(tableName, data, expressions, symbols = {}) {
|
|
356
|
+
const regularCols = Object.keys(data);
|
|
357
|
+
const exprCols = Object.keys(expressions);
|
|
358
|
+
const symbolCols = Object.keys(symbols);
|
|
359
|
+
const allCols = [...regularCols, ...symbolCols, ...exprCols];
|
|
360
|
+
if (allCols.length === 0) {
|
|
361
|
+
throw new Error("Insert requires at least one column");
|
|
362
|
+
}
|
|
363
|
+
const strings = ["INSERT INTO "];
|
|
364
|
+
const values = [];
|
|
365
|
+
values.push(ident(tableName));
|
|
366
|
+
strings.push(" (");
|
|
367
|
+
for (let i = 0; i < allCols.length; i++) {
|
|
368
|
+
values.push(ident(allCols[i]));
|
|
369
|
+
strings.push(i < allCols.length - 1 ? ", " : ") VALUES (");
|
|
370
|
+
}
|
|
371
|
+
const valueCols = [...regularCols, ...symbolCols];
|
|
372
|
+
for (let i = 0; i < valueCols.length; i++) {
|
|
373
|
+
const col = valueCols[i];
|
|
374
|
+
values.push(col in data ? data[col] : symbols[col]);
|
|
375
|
+
strings.push(i < valueCols.length - 1 ? ", " : "");
|
|
376
|
+
}
|
|
377
|
+
for (let i = 0; i < exprCols.length; i++) {
|
|
378
|
+
if (valueCols.length > 0 || i > 0) {
|
|
379
|
+
strings[strings.length - 1] += ", ";
|
|
380
|
+
}
|
|
381
|
+
mergeExpression(strings, values, expressions[exprCols[i]]);
|
|
382
|
+
}
|
|
383
|
+
strings[strings.length - 1] += ")";
|
|
384
|
+
return {
|
|
385
|
+
strings: makeTemplate(strings),
|
|
386
|
+
values
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function buildUpdateByIdParts(tableName, pk, data, expressions, id, symbols = {}) {
|
|
390
|
+
const regularCols = Object.keys(data);
|
|
391
|
+
const exprCols = Object.keys(expressions);
|
|
392
|
+
const symbolCols = Object.keys(symbols);
|
|
393
|
+
const valueCols = [...regularCols, ...symbolCols];
|
|
394
|
+
const strings = ["UPDATE "];
|
|
395
|
+
const values = [];
|
|
396
|
+
values.push(ident(tableName));
|
|
397
|
+
strings.push(" SET ");
|
|
398
|
+
for (let i = 0; i < valueCols.length; i++) {
|
|
399
|
+
const col = valueCols[i];
|
|
400
|
+
values.push(ident(col));
|
|
401
|
+
strings.push(" = ");
|
|
402
|
+
values.push(col in data ? data[col] : symbols[col]);
|
|
403
|
+
strings.push(i < valueCols.length - 1 ? ", " : "");
|
|
404
|
+
}
|
|
405
|
+
for (let i = 0; i < exprCols.length; i++) {
|
|
406
|
+
if (valueCols.length > 0 || i > 0) {
|
|
407
|
+
strings[strings.length - 1] += ", ";
|
|
408
|
+
}
|
|
409
|
+
values.push(ident(exprCols[i]));
|
|
410
|
+
strings.push(" = ");
|
|
411
|
+
mergeExpression(strings, values, expressions[exprCols[i]]);
|
|
412
|
+
}
|
|
413
|
+
strings[strings.length - 1] += " WHERE ";
|
|
414
|
+
values.push(ident(pk));
|
|
415
|
+
strings.push(" = ");
|
|
416
|
+
values.push(id);
|
|
417
|
+
strings.push("");
|
|
418
|
+
return {
|
|
419
|
+
strings: makeTemplate(strings),
|
|
420
|
+
values
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function buildUpdateByIdsParts(tableName, pk, data, expressions, ids, symbols = {}) {
|
|
424
|
+
const regularCols = Object.keys(data);
|
|
425
|
+
const exprCols = Object.keys(expressions);
|
|
426
|
+
const symbolCols = Object.keys(symbols);
|
|
427
|
+
const valueCols = [...regularCols, ...symbolCols];
|
|
428
|
+
const strings = ["UPDATE "];
|
|
429
|
+
const values = [];
|
|
430
|
+
values.push(ident(tableName));
|
|
431
|
+
strings.push(" SET ");
|
|
432
|
+
for (let i = 0; i < valueCols.length; i++) {
|
|
433
|
+
const col = valueCols[i];
|
|
434
|
+
values.push(ident(col));
|
|
435
|
+
strings.push(" = ");
|
|
436
|
+
values.push(col in data ? data[col] : symbols[col]);
|
|
437
|
+
strings.push(i < valueCols.length - 1 ? ", " : "");
|
|
438
|
+
}
|
|
439
|
+
for (let i = 0; i < exprCols.length; i++) {
|
|
440
|
+
if (valueCols.length > 0 || i > 0) {
|
|
441
|
+
strings[strings.length - 1] += ", ";
|
|
442
|
+
}
|
|
443
|
+
values.push(ident(exprCols[i]));
|
|
444
|
+
strings.push(" = ");
|
|
445
|
+
mergeExpression(strings, values, expressions[exprCols[i]]);
|
|
446
|
+
}
|
|
447
|
+
strings[strings.length - 1] += " WHERE ";
|
|
448
|
+
values.push(ident(pk));
|
|
449
|
+
strings.push(" IN (");
|
|
450
|
+
for (let i = 0; i < ids.length; i++) {
|
|
451
|
+
values.push(ids[i]);
|
|
452
|
+
strings.push(i < ids.length - 1 ? ", " : ")");
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
strings: makeTemplate(strings),
|
|
456
|
+
values
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function buildSelectCols(tables) {
|
|
460
|
+
const strings = [""];
|
|
461
|
+
const values = [];
|
|
462
|
+
let needsComma = false;
|
|
463
|
+
for (const table2 of tables) {
|
|
464
|
+
const tableName = table2.name;
|
|
465
|
+
const shape = table2.schema.shape;
|
|
466
|
+
const derivedFields = new Set(
|
|
467
|
+
table2.meta.derivedFields ?? []
|
|
468
|
+
);
|
|
469
|
+
for (const fieldName of Object.keys(shape)) {
|
|
470
|
+
if (derivedFields.has(fieldName))
|
|
471
|
+
continue;
|
|
472
|
+
if (needsComma) {
|
|
473
|
+
strings[strings.length - 1] += ", ";
|
|
474
|
+
}
|
|
475
|
+
values.push(ident(tableName));
|
|
476
|
+
strings.push(".");
|
|
477
|
+
values.push(ident(fieldName));
|
|
478
|
+
strings.push(" AS ");
|
|
479
|
+
values.push(ident(`${tableName}.${fieldName}`));
|
|
480
|
+
strings.push("");
|
|
481
|
+
needsComma = true;
|
|
482
|
+
}
|
|
483
|
+
const derivedExprs = table2.meta.derivedExprs ?? [];
|
|
484
|
+
for (const expr of derivedExprs) {
|
|
485
|
+
if (needsComma) {
|
|
486
|
+
strings[strings.length - 1] += ", ";
|
|
487
|
+
}
|
|
488
|
+
const alias = `${tableName}.${expr.fieldName}`;
|
|
489
|
+
strings[strings.length - 1] += "(";
|
|
490
|
+
const exprStrings = expr.template[0];
|
|
491
|
+
const exprValues = expr.template.slice(1);
|
|
492
|
+
strings[strings.length - 1] += exprStrings[0];
|
|
493
|
+
for (let i = 1; i < exprStrings.length; i++) {
|
|
494
|
+
strings.push(exprStrings[i]);
|
|
495
|
+
}
|
|
496
|
+
values.push(...exprValues);
|
|
497
|
+
strings[strings.length - 1] += ") AS ";
|
|
498
|
+
values.push(ident(alias));
|
|
499
|
+
strings.push("");
|
|
500
|
+
needsComma = true;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return { strings, values };
|
|
504
|
+
}
|
|
505
|
+
function buildSelectByPkParts(tableName, pk, id) {
|
|
506
|
+
return {
|
|
507
|
+
strings: makeTemplate(["SELECT * FROM ", " WHERE ", " = ", ""]),
|
|
508
|
+
values: [ident(tableName), ident(pk), id]
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function buildSelectByPksParts(tableName, pk, ids) {
|
|
512
|
+
const strings = ["SELECT * FROM ", " WHERE ", " IN ("];
|
|
513
|
+
const values = [ident(tableName), ident(pk)];
|
|
514
|
+
for (let i = 0; i < ids.length; i++) {
|
|
515
|
+
values.push(ids[i]);
|
|
516
|
+
strings.push(i < ids.length - 1 ? ", " : ")");
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
strings: makeTemplate(strings),
|
|
520
|
+
values
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function buildDeleteByPkParts(tableName, pk, id) {
|
|
524
|
+
return {
|
|
525
|
+
strings: makeTemplate(["DELETE FROM ", " WHERE ", " = ", ""]),
|
|
526
|
+
values: [ident(tableName), ident(pk), id]
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function buildDeleteByPksParts(tableName, pk, ids) {
|
|
530
|
+
const strings = ["DELETE FROM ", " WHERE ", " IN ("];
|
|
531
|
+
const values = [ident(tableName), ident(pk)];
|
|
532
|
+
for (let i = 0; i < ids.length; i++) {
|
|
533
|
+
values.push(ids[i]);
|
|
534
|
+
strings.push(i < ids.length - 1 ? ", " : ")");
|
|
535
|
+
}
|
|
536
|
+
return {
|
|
537
|
+
strings: makeTemplate(strings),
|
|
538
|
+
values
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function appendReturning(parts) {
|
|
542
|
+
const strings = [...parts.strings];
|
|
543
|
+
strings[strings.length - 1] += " RETURNING *";
|
|
544
|
+
return {
|
|
545
|
+
strings: makeTemplate(strings),
|
|
546
|
+
values: parts.values
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
function expandFragments(strings, values) {
|
|
550
|
+
const newStrings = [strings[0]];
|
|
551
|
+
const newValues = [];
|
|
552
|
+
for (let i = 0; i < values.length; i++) {
|
|
553
|
+
const value = values[i];
|
|
554
|
+
if (isSQLTemplate(value)) {
|
|
555
|
+
const valueStrings = value[0];
|
|
556
|
+
const valueValues = value.slice(1);
|
|
557
|
+
newStrings[newStrings.length - 1] += valueStrings[0];
|
|
558
|
+
for (let j = 1; j < valueStrings.length; j++) {
|
|
559
|
+
newStrings.push(valueStrings[j]);
|
|
560
|
+
}
|
|
561
|
+
newValues.push(...valueValues);
|
|
562
|
+
newStrings[newStrings.length - 1] += strings[i + 1];
|
|
563
|
+
} else {
|
|
564
|
+
newStrings.push(strings[i + 1]);
|
|
565
|
+
newValues.push(value);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
strings: makeTemplate(newStrings),
|
|
570
|
+
values: newValues
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
var DatabaseUpgradeEvent = class extends Event {
|
|
574
|
+
oldVersion;
|
|
575
|
+
newVersion;
|
|
576
|
+
#promises = [];
|
|
577
|
+
constructor(type, init) {
|
|
578
|
+
super(type);
|
|
579
|
+
this.oldVersion = init.oldVersion;
|
|
580
|
+
this.newVersion = init.newVersion;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Extend the event lifetime until the promise settles.
|
|
584
|
+
* Like ExtendableEvent.waitUntil() from ServiceWorker.
|
|
585
|
+
*/
|
|
586
|
+
waitUntil(promise) {
|
|
587
|
+
this.#promises.push(promise);
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* @internal Wait for all waitUntil promises to settle.
|
|
591
|
+
*/
|
|
592
|
+
async _settle() {
|
|
593
|
+
await Promise.all(this.#promises);
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
var Transaction = class {
|
|
597
|
+
#driver;
|
|
598
|
+
constructor(driver) {
|
|
599
|
+
this.#driver = driver;
|
|
600
|
+
}
|
|
601
|
+
all(tables) {
|
|
602
|
+
const tableArray = Array.isArray(tables) ? tables : [tables];
|
|
603
|
+
return async (strings, ...values) => {
|
|
604
|
+
const { strings: colStrings, values: colValues } = buildSelectCols(tableArray);
|
|
605
|
+
const { strings: expandedStrings, values: expandedValues } = expandFragments(strings, values);
|
|
606
|
+
const queryStrings = ["SELECT "];
|
|
607
|
+
const queryValues = [];
|
|
608
|
+
queryStrings[0] += colStrings[0];
|
|
609
|
+
for (let i = 1; i < colStrings.length; i++) {
|
|
610
|
+
queryStrings.push(colStrings[i]);
|
|
611
|
+
}
|
|
612
|
+
queryValues.push(...colValues);
|
|
613
|
+
queryStrings[queryStrings.length - 1] += " FROM ";
|
|
614
|
+
queryValues.push(ident(tableArray[0].name));
|
|
615
|
+
queryStrings.push(" ");
|
|
616
|
+
queryStrings[queryStrings.length - 1] += expandedStrings[0];
|
|
617
|
+
for (let i = 1; i < expandedStrings.length; i++) {
|
|
618
|
+
queryStrings.push(expandedStrings[i]);
|
|
619
|
+
}
|
|
620
|
+
queryValues.push(...expandedValues);
|
|
621
|
+
const rows = await this.#driver.all(
|
|
622
|
+
makeTemplate(queryStrings),
|
|
623
|
+
queryValues
|
|
624
|
+
);
|
|
625
|
+
return normalize(rows, tableArray);
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
get(tables, id) {
|
|
629
|
+
if (id !== void 0) {
|
|
630
|
+
const table2 = tables;
|
|
631
|
+
const pk = table2.meta.primary;
|
|
632
|
+
if (!pk) {
|
|
633
|
+
return Promise.reject(
|
|
634
|
+
new Error(`Table ${table2.name} has no primary key defined`)
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
const { strings, values } = buildSelectByPkParts(table2.name, pk, id);
|
|
638
|
+
return this.#driver.get(strings, values).then((row) => {
|
|
639
|
+
if (!row)
|
|
640
|
+
return null;
|
|
641
|
+
return decodeData(table2, row);
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
const tableArray = Array.isArray(tables) ? tables : [tables];
|
|
645
|
+
return async (strings, ...values) => {
|
|
646
|
+
const { strings: colStrings, values: colValues } = buildSelectCols(tableArray);
|
|
647
|
+
const { strings: expandedStrings, values: expandedValues } = expandFragments(strings, values);
|
|
648
|
+
const queryStrings = ["SELECT "];
|
|
649
|
+
const queryValues = [];
|
|
650
|
+
queryStrings[0] += colStrings[0];
|
|
651
|
+
for (let i = 1; i < colStrings.length; i++) {
|
|
652
|
+
queryStrings.push(colStrings[i]);
|
|
653
|
+
}
|
|
654
|
+
queryValues.push(...colValues);
|
|
655
|
+
queryStrings[queryStrings.length - 1] += " FROM ";
|
|
656
|
+
queryValues.push(ident(tableArray[0].name));
|
|
657
|
+
queryStrings.push(" ");
|
|
658
|
+
queryStrings[queryStrings.length - 1] += expandedStrings[0];
|
|
659
|
+
for (let i = 1; i < expandedStrings.length; i++) {
|
|
660
|
+
queryStrings.push(expandedStrings[i]);
|
|
661
|
+
}
|
|
662
|
+
queryValues.push(...expandedValues);
|
|
663
|
+
const row = await this.#driver.get(
|
|
664
|
+
makeTemplate(queryStrings),
|
|
665
|
+
queryValues
|
|
666
|
+
);
|
|
667
|
+
return normalizeOne(row, tableArray);
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
async insert(table2, data) {
|
|
671
|
+
if (Array.isArray(data)) {
|
|
672
|
+
if (data.length === 0) {
|
|
673
|
+
return [];
|
|
674
|
+
}
|
|
675
|
+
const results = [];
|
|
676
|
+
for (const row of data) {
|
|
677
|
+
results.push(await this.#insertOne(table2, row));
|
|
678
|
+
}
|
|
679
|
+
return results;
|
|
680
|
+
}
|
|
681
|
+
return this.#insertOne(table2, data);
|
|
682
|
+
}
|
|
683
|
+
async #insertOne(table2, data) {
|
|
684
|
+
if (table2.meta.isPartial) {
|
|
685
|
+
throw new Error(
|
|
686
|
+
`Cannot insert into partial table "${table2.name}". Use the full table definition instead.`
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
if (table2.meta.isDerived) {
|
|
690
|
+
throw new Error(
|
|
691
|
+
`Cannot insert into derived table "${table2.name}". Derived tables are SELECT-only.`
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
const dataWithSchemaExprs = injectSchemaExpressions(
|
|
695
|
+
table2,
|
|
696
|
+
data,
|
|
697
|
+
"insert"
|
|
698
|
+
);
|
|
699
|
+
const { regularData, expressions, symbols } = extractDBExpressions(
|
|
700
|
+
dataWithSchemaExprs,
|
|
701
|
+
table2
|
|
702
|
+
);
|
|
703
|
+
let schema = table2.schema;
|
|
704
|
+
const skipFields = { ...expressions, ...symbols };
|
|
705
|
+
if (Object.keys(skipFields).length > 0) {
|
|
706
|
+
const skipFieldSchemas = Object.keys(skipFields).reduce(
|
|
707
|
+
(acc, key) => {
|
|
708
|
+
acc[key] = table2.schema.shape[key].optional();
|
|
709
|
+
return acc;
|
|
710
|
+
},
|
|
711
|
+
{}
|
|
712
|
+
);
|
|
713
|
+
schema = table2.schema.extend(skipFieldSchemas);
|
|
714
|
+
}
|
|
715
|
+
const validated = validateWithStandardSchema(
|
|
716
|
+
schema,
|
|
717
|
+
regularData
|
|
718
|
+
);
|
|
719
|
+
const encoded = encodeData(table2, validated);
|
|
720
|
+
const insertParts = buildInsertParts(
|
|
721
|
+
table2.name,
|
|
722
|
+
encoded,
|
|
723
|
+
expressions,
|
|
724
|
+
symbols
|
|
725
|
+
);
|
|
726
|
+
if (this.#driver.supportsReturning) {
|
|
727
|
+
const { strings, values } = appendReturning(insertParts);
|
|
728
|
+
const row = await this.#driver.get(
|
|
729
|
+
strings,
|
|
730
|
+
values
|
|
731
|
+
);
|
|
732
|
+
return decodeData(table2, row);
|
|
733
|
+
}
|
|
734
|
+
await this.#driver.run(insertParts.strings, insertParts.values);
|
|
735
|
+
const pk = table2.meta.primary;
|
|
736
|
+
const pkValue = pk ? encoded[pk] ?? (expressions[pk] ? void 0 : null) : null;
|
|
737
|
+
if (pk && pkValue !== void 0 && pkValue !== null) {
|
|
738
|
+
const { strings, values } = buildSelectByPkParts(table2.name, pk, pkValue);
|
|
739
|
+
const row = await this.#driver.get(
|
|
740
|
+
strings,
|
|
741
|
+
values
|
|
742
|
+
);
|
|
743
|
+
if (row) {
|
|
744
|
+
return decodeData(table2, row);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return validated;
|
|
748
|
+
}
|
|
749
|
+
update(table2, data, idOrIds) {
|
|
750
|
+
if (idOrIds === void 0) {
|
|
751
|
+
return async (strings, ...values) => {
|
|
752
|
+
return this.#updateWithWhere(table2, data, strings, values);
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
if (Array.isArray(idOrIds)) {
|
|
756
|
+
return this.#updateByIds(table2, data, idOrIds);
|
|
757
|
+
}
|
|
758
|
+
return this.#updateById(table2, data, idOrIds);
|
|
759
|
+
}
|
|
760
|
+
async #updateById(table2, data, id) {
|
|
761
|
+
const pk = table2.meta.primary;
|
|
762
|
+
if (!pk) {
|
|
763
|
+
throw new Error(`Table ${table2.name} has no primary key defined`);
|
|
764
|
+
}
|
|
765
|
+
if (table2.meta.isDerived) {
|
|
766
|
+
throw new Error(
|
|
767
|
+
`Cannot update derived table "${table2.name}". Derived tables are SELECT-only.`
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
const dataWithSchemaExprs = injectSchemaExpressions(
|
|
771
|
+
table2,
|
|
772
|
+
data,
|
|
773
|
+
"update"
|
|
774
|
+
);
|
|
775
|
+
const { regularData, expressions, symbols } = extractDBExpressions(
|
|
776
|
+
dataWithSchemaExprs,
|
|
777
|
+
table2
|
|
778
|
+
);
|
|
779
|
+
const partialSchema = table2.schema.partial();
|
|
780
|
+
const validated = validateWithStandardSchema(
|
|
781
|
+
partialSchema,
|
|
782
|
+
regularData
|
|
783
|
+
);
|
|
784
|
+
const allColumns = [
|
|
785
|
+
...Object.keys(validated),
|
|
786
|
+
...Object.keys(expressions),
|
|
787
|
+
...Object.keys(symbols)
|
|
788
|
+
];
|
|
789
|
+
if (allColumns.length === 0) {
|
|
790
|
+
throw new Error("No fields to update");
|
|
791
|
+
}
|
|
792
|
+
const encoded = encodeData(table2, validated);
|
|
793
|
+
const updateParts = buildUpdateByIdParts(
|
|
794
|
+
table2.name,
|
|
795
|
+
pk,
|
|
796
|
+
encoded,
|
|
797
|
+
expressions,
|
|
798
|
+
id,
|
|
799
|
+
symbols
|
|
800
|
+
);
|
|
801
|
+
if (this.#driver.supportsReturning) {
|
|
802
|
+
const { strings, values } = appendReturning(updateParts);
|
|
803
|
+
const row2 = await this.#driver.get(
|
|
804
|
+
strings,
|
|
805
|
+
values
|
|
806
|
+
);
|
|
807
|
+
if (!row2)
|
|
808
|
+
return null;
|
|
809
|
+
return decodeData(table2, row2);
|
|
810
|
+
}
|
|
811
|
+
await this.#driver.run(updateParts.strings, updateParts.values);
|
|
812
|
+
const { strings: selectStrings, values: selectValues } = buildSelectByPkParts(
|
|
813
|
+
table2.name,
|
|
814
|
+
pk,
|
|
815
|
+
id
|
|
816
|
+
);
|
|
817
|
+
const row = await this.#driver.get(
|
|
818
|
+
selectStrings,
|
|
819
|
+
selectValues
|
|
820
|
+
);
|
|
821
|
+
if (!row)
|
|
822
|
+
return null;
|
|
823
|
+
return decodeData(table2, row);
|
|
824
|
+
}
|
|
825
|
+
async #updateByIds(table2, data, ids) {
|
|
826
|
+
if (ids.length === 0) {
|
|
827
|
+
return [];
|
|
828
|
+
}
|
|
829
|
+
const pk = table2.meta.primary;
|
|
830
|
+
if (!pk) {
|
|
831
|
+
throw new Error(`Table ${table2.name} has no primary key defined`);
|
|
832
|
+
}
|
|
833
|
+
if (table2.meta.isDerived) {
|
|
834
|
+
throw new Error(
|
|
835
|
+
`Cannot update derived table "${table2.name}". Derived tables are SELECT-only.`
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
const dataWithSchemaExprs = injectSchemaExpressions(
|
|
839
|
+
table2,
|
|
840
|
+
data,
|
|
841
|
+
"update"
|
|
842
|
+
);
|
|
843
|
+
const { regularData, expressions, symbols } = extractDBExpressions(
|
|
844
|
+
dataWithSchemaExprs,
|
|
845
|
+
table2
|
|
846
|
+
);
|
|
847
|
+
const partialSchema = table2.schema.partial();
|
|
848
|
+
const validated = validateWithStandardSchema(
|
|
849
|
+
partialSchema,
|
|
850
|
+
regularData
|
|
851
|
+
);
|
|
852
|
+
const allColumns = [
|
|
853
|
+
...Object.keys(validated),
|
|
854
|
+
...Object.keys(expressions),
|
|
855
|
+
...Object.keys(symbols)
|
|
856
|
+
];
|
|
857
|
+
if (allColumns.length === 0) {
|
|
858
|
+
throw new Error("No fields to update");
|
|
859
|
+
}
|
|
860
|
+
const encoded = encodeData(table2, validated);
|
|
861
|
+
const updateParts = buildUpdateByIdsParts(
|
|
862
|
+
table2.name,
|
|
863
|
+
pk,
|
|
864
|
+
encoded,
|
|
865
|
+
expressions,
|
|
866
|
+
ids,
|
|
867
|
+
symbols
|
|
868
|
+
);
|
|
869
|
+
if (this.#driver.supportsReturning) {
|
|
870
|
+
const { strings, values } = appendReturning(updateParts);
|
|
871
|
+
const rows2 = await this.#driver.all(
|
|
872
|
+
strings,
|
|
873
|
+
values
|
|
874
|
+
);
|
|
875
|
+
const resultMap2 = /* @__PURE__ */ new Map();
|
|
876
|
+
for (const row of rows2) {
|
|
877
|
+
const entity = decodeData(table2, row);
|
|
878
|
+
resultMap2.set(row[pk], entity);
|
|
879
|
+
}
|
|
880
|
+
return ids.map((id) => resultMap2.get(id) ?? null);
|
|
881
|
+
}
|
|
882
|
+
await this.#driver.run(updateParts.strings, updateParts.values);
|
|
883
|
+
const { strings: selectStrings, values: selectValues } = buildSelectByPksParts(table2.name, pk, ids);
|
|
884
|
+
const rows = await this.#driver.all(
|
|
885
|
+
selectStrings,
|
|
886
|
+
selectValues
|
|
887
|
+
);
|
|
888
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
889
|
+
for (const row of rows) {
|
|
890
|
+
const entity = decodeData(table2, row);
|
|
891
|
+
resultMap.set(row[pk], entity);
|
|
892
|
+
}
|
|
893
|
+
return ids.map((id) => resultMap.get(id) ?? null);
|
|
894
|
+
}
|
|
895
|
+
async #updateWithWhere(table2, data, strings, templateValues) {
|
|
896
|
+
if (table2.meta.isDerived) {
|
|
897
|
+
throw new Error(
|
|
898
|
+
`Cannot update derived table "${table2.name}". Derived tables are SELECT-only.`
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
const pk = table2.meta.primary;
|
|
902
|
+
if (!pk) {
|
|
903
|
+
throw new Error(`Table ${table2.name} has no primary key defined`);
|
|
904
|
+
}
|
|
905
|
+
const dataWithSchemaExprs = injectSchemaExpressions(
|
|
906
|
+
table2,
|
|
907
|
+
data,
|
|
908
|
+
"update"
|
|
909
|
+
);
|
|
910
|
+
const { regularData, expressions } = extractDBExpressions(
|
|
911
|
+
dataWithSchemaExprs,
|
|
912
|
+
table2
|
|
913
|
+
);
|
|
914
|
+
const partialSchema = table2.schema.partial();
|
|
915
|
+
const validated = validateWithStandardSchema(
|
|
916
|
+
partialSchema,
|
|
917
|
+
regularData
|
|
918
|
+
);
|
|
919
|
+
const allColumns = [...Object.keys(validated), ...Object.keys(expressions)];
|
|
920
|
+
if (allColumns.length === 0) {
|
|
921
|
+
throw new Error("No fields to update");
|
|
922
|
+
}
|
|
923
|
+
const encoded = encodeData(table2, validated);
|
|
924
|
+
const { strings: whereStrings, values: whereValues } = expandFragments(
|
|
925
|
+
strings,
|
|
926
|
+
templateValues
|
|
927
|
+
);
|
|
928
|
+
const setCols = Object.keys(encoded);
|
|
929
|
+
const exprCols = Object.keys(expressions);
|
|
930
|
+
const queryStrings = ["UPDATE "];
|
|
931
|
+
const queryValues = [];
|
|
932
|
+
queryValues.push(ident(table2.name));
|
|
933
|
+
queryStrings.push(" SET ");
|
|
934
|
+
for (let i = 0; i < setCols.length; i++) {
|
|
935
|
+
const col = setCols[i];
|
|
936
|
+
queryValues.push(ident(col));
|
|
937
|
+
queryStrings.push(" = ");
|
|
938
|
+
queryValues.push(encoded[col]);
|
|
939
|
+
queryStrings.push(i < setCols.length - 1 ? ", " : "");
|
|
940
|
+
}
|
|
941
|
+
for (let i = 0; i < exprCols.length; i++) {
|
|
942
|
+
if (setCols.length > 0 || i > 0) {
|
|
943
|
+
queryStrings[queryStrings.length - 1] += ", ";
|
|
944
|
+
}
|
|
945
|
+
queryValues.push(ident(exprCols[i]));
|
|
946
|
+
queryStrings.push(" = ");
|
|
947
|
+
mergeExpression(queryStrings, queryValues, expressions[exprCols[i]]);
|
|
948
|
+
}
|
|
949
|
+
queryStrings[queryStrings.length - 1] += " " + whereStrings[0];
|
|
950
|
+
for (let i = 1; i < whereStrings.length; i++) {
|
|
951
|
+
queryStrings.push(whereStrings[i]);
|
|
952
|
+
}
|
|
953
|
+
queryValues.push(...whereValues);
|
|
954
|
+
if (this.#driver.supportsReturning) {
|
|
955
|
+
queryStrings[queryStrings.length - 1] += " RETURNING *";
|
|
956
|
+
const rows2 = await this.#driver.all(
|
|
957
|
+
makeTemplate(queryStrings),
|
|
958
|
+
queryValues
|
|
959
|
+
);
|
|
960
|
+
return rows2.map((row) => decodeData(table2, row));
|
|
961
|
+
}
|
|
962
|
+
const selectIdStrings = ["SELECT "];
|
|
963
|
+
const selectIdValues = [];
|
|
964
|
+
selectIdValues.push(ident(pk));
|
|
965
|
+
selectIdStrings.push(" FROM ");
|
|
966
|
+
selectIdValues.push(ident(table2.name));
|
|
967
|
+
selectIdStrings.push(" " + whereStrings[0]);
|
|
968
|
+
for (let i = 1; i < whereStrings.length; i++) {
|
|
969
|
+
selectIdStrings.push(whereStrings[i]);
|
|
970
|
+
}
|
|
971
|
+
selectIdValues.push(...whereValues);
|
|
972
|
+
const idRows = await this.#driver.all(
|
|
973
|
+
makeTemplate(selectIdStrings),
|
|
974
|
+
selectIdValues
|
|
975
|
+
);
|
|
976
|
+
const ids = idRows.map((r) => r[pk]);
|
|
977
|
+
if (ids.length === 0) {
|
|
978
|
+
return [];
|
|
979
|
+
}
|
|
980
|
+
await this.#driver.run(makeTemplate(queryStrings), queryValues);
|
|
981
|
+
const { strings: selectStrings, values: selectVals } = buildSelectByPksParts(
|
|
982
|
+
table2.name,
|
|
983
|
+
pk,
|
|
984
|
+
ids
|
|
985
|
+
);
|
|
986
|
+
const rows = await this.#driver.all(
|
|
987
|
+
selectStrings,
|
|
988
|
+
selectVals
|
|
989
|
+
);
|
|
990
|
+
return rows.map((row) => decodeData(table2, row));
|
|
991
|
+
}
|
|
992
|
+
delete(table2, idOrIds) {
|
|
993
|
+
if (idOrIds === void 0) {
|
|
994
|
+
return async (strings, ...values) => {
|
|
995
|
+
const { strings: expandedStrings, values: expandedValues } = expandFragments(strings, values);
|
|
996
|
+
const deleteStrings = ["DELETE FROM "];
|
|
997
|
+
const deleteValues = [ident(table2.name)];
|
|
998
|
+
deleteStrings.push(" " + expandedStrings[0]);
|
|
999
|
+
for (let i = 1; i < expandedStrings.length; i++) {
|
|
1000
|
+
deleteStrings.push(expandedStrings[i]);
|
|
1001
|
+
}
|
|
1002
|
+
deleteValues.push(...expandedValues);
|
|
1003
|
+
return this.#driver.run(makeTemplate(deleteStrings), deleteValues);
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
if (Array.isArray(idOrIds)) {
|
|
1007
|
+
return this.#deleteByIds(table2, idOrIds);
|
|
1008
|
+
}
|
|
1009
|
+
return this.#deleteById(table2, idOrIds);
|
|
1010
|
+
}
|
|
1011
|
+
async #deleteById(table2, id) {
|
|
1012
|
+
const pk = table2.meta.primary;
|
|
1013
|
+
if (!pk) {
|
|
1014
|
+
throw new Error(`Table ${table2.name} has no primary key defined`);
|
|
1015
|
+
}
|
|
1016
|
+
const { strings, values } = buildDeleteByPkParts(table2.name, pk, id);
|
|
1017
|
+
return this.#driver.run(strings, values);
|
|
1018
|
+
}
|
|
1019
|
+
async #deleteByIds(table2, ids) {
|
|
1020
|
+
if (ids.length === 0) {
|
|
1021
|
+
return 0;
|
|
1022
|
+
}
|
|
1023
|
+
const pk = table2.meta.primary;
|
|
1024
|
+
if (!pk) {
|
|
1025
|
+
throw new Error(`Table ${table2.name} has no primary key defined`);
|
|
1026
|
+
}
|
|
1027
|
+
const { strings, values } = buildDeleteByPksParts(table2.name, pk, ids);
|
|
1028
|
+
return this.#driver.run(strings, values);
|
|
1029
|
+
}
|
|
1030
|
+
softDelete(table2, idOrIds) {
|
|
1031
|
+
const softDeleteField = table2.meta.softDeleteField;
|
|
1032
|
+
if (!softDeleteField) {
|
|
1033
|
+
throw new Error(
|
|
1034
|
+
`Table ${table2.name} does not have a soft delete field. Use softDelete() wrapper to mark a field.`
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
if (idOrIds === void 0) {
|
|
1038
|
+
return async (strings, ...values) => {
|
|
1039
|
+
return this.#softDeleteWithWhere(table2, strings, values);
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
if (Array.isArray(idOrIds)) {
|
|
1043
|
+
return this.#softDeleteByIds(table2, idOrIds);
|
|
1044
|
+
}
|
|
1045
|
+
return this.#softDeleteById(table2, idOrIds);
|
|
1046
|
+
}
|
|
1047
|
+
async #softDeleteById(table2, id) {
|
|
1048
|
+
const pk = table2.meta.primary;
|
|
1049
|
+
if (!pk) {
|
|
1050
|
+
throw new Error(`Table ${table2.name} has no primary key defined`);
|
|
1051
|
+
}
|
|
1052
|
+
const softDeleteField = table2.meta.softDeleteField;
|
|
1053
|
+
const schemaExprs = injectSchemaExpressions(table2, {}, "update");
|
|
1054
|
+
const { expressions, symbols } = extractDBExpressions(schemaExprs);
|
|
1055
|
+
const queryStrings = ["UPDATE "];
|
|
1056
|
+
const queryValues = [];
|
|
1057
|
+
queryValues.push(ident(table2.name));
|
|
1058
|
+
queryStrings.push(" SET ");
|
|
1059
|
+
queryValues.push(ident(softDeleteField));
|
|
1060
|
+
queryStrings.push(" = CURRENT_TIMESTAMP");
|
|
1061
|
+
for (const [field, expr] of Object.entries(expressions)) {
|
|
1062
|
+
if (field !== softDeleteField) {
|
|
1063
|
+
queryStrings[queryStrings.length - 1] += ", ";
|
|
1064
|
+
queryValues.push(ident(field));
|
|
1065
|
+
queryStrings.push(" = ");
|
|
1066
|
+
mergeExpression(queryStrings, queryValues, expr);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
for (const [field, sym] of Object.entries(symbols)) {
|
|
1070
|
+
if (field !== softDeleteField) {
|
|
1071
|
+
queryStrings[queryStrings.length - 1] += ", ";
|
|
1072
|
+
queryValues.push(ident(field));
|
|
1073
|
+
queryStrings.push(` = ${resolveSQLBuiltin(sym)}`);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
queryStrings[queryStrings.length - 1] += " WHERE ";
|
|
1077
|
+
queryValues.push(ident(pk));
|
|
1078
|
+
queryStrings.push(" = ");
|
|
1079
|
+
queryValues.push(id);
|
|
1080
|
+
queryStrings.push("");
|
|
1081
|
+
return this.#driver.run(makeTemplate(queryStrings), queryValues);
|
|
1082
|
+
}
|
|
1083
|
+
async #softDeleteByIds(table2, ids) {
|
|
1084
|
+
if (ids.length === 0) {
|
|
1085
|
+
return 0;
|
|
1086
|
+
}
|
|
1087
|
+
const pk = table2.meta.primary;
|
|
1088
|
+
if (!pk) {
|
|
1089
|
+
throw new Error(`Table ${table2.name} has no primary key defined`);
|
|
1090
|
+
}
|
|
1091
|
+
const softDeleteField = table2.meta.softDeleteField;
|
|
1092
|
+
const schemaExprs = injectSchemaExpressions(table2, {}, "update");
|
|
1093
|
+
const { expressions, symbols } = extractDBExpressions(schemaExprs);
|
|
1094
|
+
const queryStrings = ["UPDATE "];
|
|
1095
|
+
const queryValues = [];
|
|
1096
|
+
queryValues.push(ident(table2.name));
|
|
1097
|
+
queryStrings.push(" SET ");
|
|
1098
|
+
queryValues.push(ident(softDeleteField));
|
|
1099
|
+
queryStrings.push(" = CURRENT_TIMESTAMP");
|
|
1100
|
+
for (const [field, expr] of Object.entries(expressions)) {
|
|
1101
|
+
if (field !== softDeleteField) {
|
|
1102
|
+
queryStrings[queryStrings.length - 1] += ", ";
|
|
1103
|
+
queryValues.push(ident(field));
|
|
1104
|
+
queryStrings.push(" = ");
|
|
1105
|
+
mergeExpression(queryStrings, queryValues, expr);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
for (const [field, sym] of Object.entries(symbols)) {
|
|
1109
|
+
if (field !== softDeleteField) {
|
|
1110
|
+
queryStrings[queryStrings.length - 1] += ", ";
|
|
1111
|
+
queryValues.push(ident(field));
|
|
1112
|
+
queryStrings.push(` = ${resolveSQLBuiltin(sym)}`);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
queryStrings[queryStrings.length - 1] += " WHERE ";
|
|
1116
|
+
queryValues.push(ident(pk));
|
|
1117
|
+
queryStrings.push(" IN (");
|
|
1118
|
+
for (let i = 0; i < ids.length; i++) {
|
|
1119
|
+
queryValues.push(ids[i]);
|
|
1120
|
+
queryStrings.push(i < ids.length - 1 ? ", " : ")");
|
|
1121
|
+
}
|
|
1122
|
+
return this.#driver.run(makeTemplate(queryStrings), queryValues);
|
|
1123
|
+
}
|
|
1124
|
+
async #softDeleteWithWhere(table2, strings, templateValues) {
|
|
1125
|
+
const softDeleteField = table2.meta.softDeleteField;
|
|
1126
|
+
const schemaExprs = injectSchemaExpressions(table2, {}, "update");
|
|
1127
|
+
const { expressions, symbols } = extractDBExpressions(schemaExprs);
|
|
1128
|
+
const { strings: expandedStrings, values: expandedValues } = expandFragments(
|
|
1129
|
+
strings,
|
|
1130
|
+
templateValues
|
|
1131
|
+
);
|
|
1132
|
+
const queryStrings = ["UPDATE "];
|
|
1133
|
+
const queryValues = [];
|
|
1134
|
+
queryValues.push(ident(table2.name));
|
|
1135
|
+
queryStrings.push(" SET ");
|
|
1136
|
+
queryValues.push(ident(softDeleteField));
|
|
1137
|
+
queryStrings.push(" = CURRENT_TIMESTAMP");
|
|
1138
|
+
for (const [field, expr] of Object.entries(expressions)) {
|
|
1139
|
+
if (field !== softDeleteField) {
|
|
1140
|
+
queryStrings[queryStrings.length - 1] += ", ";
|
|
1141
|
+
queryValues.push(ident(field));
|
|
1142
|
+
queryStrings.push(" = ");
|
|
1143
|
+
mergeExpression(queryStrings, queryValues, expr);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
for (const [field, sym] of Object.entries(symbols)) {
|
|
1147
|
+
if (field !== softDeleteField) {
|
|
1148
|
+
queryStrings[queryStrings.length - 1] += ", ";
|
|
1149
|
+
queryValues.push(ident(field));
|
|
1150
|
+
queryStrings.push(` = ${resolveSQLBuiltin(sym)}`);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
queryStrings[queryStrings.length - 1] += " " + expandedStrings[0];
|
|
1154
|
+
for (let i = 1; i < expandedStrings.length; i++) {
|
|
1155
|
+
queryStrings.push(expandedStrings[i]);
|
|
1156
|
+
}
|
|
1157
|
+
queryValues.push(...expandedValues);
|
|
1158
|
+
return this.#driver.run(makeTemplate(queryStrings), queryValues);
|
|
1159
|
+
}
|
|
1160
|
+
// ==========================================================================
|
|
1161
|
+
// Raw - No Normalization
|
|
1162
|
+
// ==========================================================================
|
|
1163
|
+
async query(strings, ...values) {
|
|
1164
|
+
const { strings: expandedStrings, values: expandedValues } = expandFragments(
|
|
1165
|
+
strings,
|
|
1166
|
+
values
|
|
1167
|
+
);
|
|
1168
|
+
return this.#driver.all(expandedStrings, expandedValues);
|
|
1169
|
+
}
|
|
1170
|
+
async exec(strings, ...values) {
|
|
1171
|
+
const { strings: expandedStrings, values: expandedValues } = expandFragments(
|
|
1172
|
+
strings,
|
|
1173
|
+
values
|
|
1174
|
+
);
|
|
1175
|
+
return this.#driver.run(expandedStrings, expandedValues);
|
|
1176
|
+
}
|
|
1177
|
+
async val(strings, ...values) {
|
|
1178
|
+
const { strings: expandedStrings, values: expandedValues } = expandFragments(
|
|
1179
|
+
strings,
|
|
1180
|
+
values
|
|
1181
|
+
);
|
|
1182
|
+
return this.#driver.val(expandedStrings, expandedValues);
|
|
1183
|
+
}
|
|
1184
|
+
// ==========================================================================
|
|
1185
|
+
// Debugging
|
|
1186
|
+
// ==========================================================================
|
|
1187
|
+
/**
|
|
1188
|
+
* Print the generated SQL and parameters without executing.
|
|
1189
|
+
* Useful for debugging query composition and fragment expansion.
|
|
1190
|
+
* Note: SQL shown uses ? placeholders; actual query may use $1, $2 etc.
|
|
1191
|
+
*/
|
|
1192
|
+
print(strings, ...values) {
|
|
1193
|
+
const { strings: expandedStrings, values: expandedValues } = expandFragments(
|
|
1194
|
+
strings,
|
|
1195
|
+
values
|
|
1196
|
+
);
|
|
1197
|
+
let sql = expandedStrings[0];
|
|
1198
|
+
for (let i = 1; i < expandedStrings.length; i++) {
|
|
1199
|
+
sql += "?" + expandedStrings[i];
|
|
1200
|
+
}
|
|
1201
|
+
return { sql, params: expandedValues };
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
var Database = class extends EventTarget {
|
|
1205
|
+
#driver;
|
|
1206
|
+
#version = 0;
|
|
1207
|
+
#opened = false;
|
|
1208
|
+
constructor(driver) {
|
|
1209
|
+
super();
|
|
1210
|
+
this.#driver = driver;
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Current database schema version.
|
|
1214
|
+
* Returns 0 if database has never been opened.
|
|
1215
|
+
*/
|
|
1216
|
+
get version() {
|
|
1217
|
+
return this.#version;
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Open the database at a specific version.
|
|
1221
|
+
*
|
|
1222
|
+
* If the requested version is higher than the current version,
|
|
1223
|
+
* fires an "upgradeneeded" event and waits for all waitUntil()
|
|
1224
|
+
* promises before completing.
|
|
1225
|
+
*
|
|
1226
|
+
* Migration safety: Uses exclusive locking to prevent race conditions
|
|
1227
|
+
* when multiple processes attempt migrations simultaneously.
|
|
1228
|
+
*
|
|
1229
|
+
* @example
|
|
1230
|
+
* db.addEventListener("upgradeneeded", (e) => {
|
|
1231
|
+
* e.waitUntil(runMigrations(e));
|
|
1232
|
+
* });
|
|
1233
|
+
* await db.open(2);
|
|
1234
|
+
*/
|
|
1235
|
+
async open(version) {
|
|
1236
|
+
if (this.#opened) {
|
|
1237
|
+
throw new Error("Database already opened");
|
|
1238
|
+
}
|
|
1239
|
+
const runMigration = async () => {
|
|
1240
|
+
await this.#ensureMigrationsTable();
|
|
1241
|
+
const currentVersion = await this.#getCurrentVersionLocked();
|
|
1242
|
+
if (version > currentVersion) {
|
|
1243
|
+
const event = new DatabaseUpgradeEvent("upgradeneeded", {
|
|
1244
|
+
oldVersion: currentVersion,
|
|
1245
|
+
newVersion: version
|
|
1246
|
+
});
|
|
1247
|
+
this.dispatchEvent(event);
|
|
1248
|
+
await event._settle();
|
|
1249
|
+
await this.#setVersion(version);
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
if (this.#driver.withMigrationLock) {
|
|
1253
|
+
await this.#driver.withMigrationLock(runMigration);
|
|
1254
|
+
} else {
|
|
1255
|
+
await this.#driver.transaction(runMigration);
|
|
1256
|
+
}
|
|
1257
|
+
this.#version = version;
|
|
1258
|
+
this.#opened = true;
|
|
1259
|
+
}
|
|
1260
|
+
// ==========================================================================
|
|
1261
|
+
// Migration Table Helpers
|
|
1262
|
+
// ==========================================================================
|
|
1263
|
+
async #ensureMigrationsTable() {
|
|
1264
|
+
const createTable = makeTemplate([
|
|
1265
|
+
`CREATE TABLE IF NOT EXISTS _migrations (
|
|
1266
|
+
version INTEGER PRIMARY KEY
|
|
1267
|
+
)`
|
|
1268
|
+
]);
|
|
1269
|
+
await this.#driver.run(createTable, []);
|
|
1270
|
+
}
|
|
1271
|
+
async #getCurrentVersionLocked() {
|
|
1272
|
+
const selectVersion = makeTemplate([
|
|
1273
|
+
`SELECT MAX(version) as version FROM _migrations`
|
|
1274
|
+
]);
|
|
1275
|
+
const row = await this.#driver.get(selectVersion, []);
|
|
1276
|
+
return row?.version ?? 0;
|
|
1277
|
+
}
|
|
1278
|
+
async #setVersion(version) {
|
|
1279
|
+
const insertVersion = makeTemplate([
|
|
1280
|
+
`INSERT INTO _migrations (version) VALUES (`,
|
|
1281
|
+
`)`
|
|
1282
|
+
]);
|
|
1283
|
+
await this.#driver.run(insertVersion, [version]);
|
|
1284
|
+
}
|
|
1285
|
+
all(tables) {
|
|
1286
|
+
const tableArray = Array.isArray(tables) ? tables : [tables];
|
|
1287
|
+
return async (strings, ...values) => {
|
|
1288
|
+
const { strings: colStrings, values: colValues } = buildSelectCols(tableArray);
|
|
1289
|
+
const { strings: expandedStrings, values: expandedValues } = expandFragments(strings, values);
|
|
1290
|
+
const queryStrings = ["SELECT "];
|
|
1291
|
+
const queryValues = [];
|
|
1292
|
+
queryStrings[0] += colStrings[0];
|
|
1293
|
+
for (let i = 1; i < colStrings.length; i++) {
|
|
1294
|
+
queryStrings.push(colStrings[i]);
|
|
1295
|
+
}
|
|
1296
|
+
queryValues.push(...colValues);
|
|
1297
|
+
queryStrings[queryStrings.length - 1] += " FROM ";
|
|
1298
|
+
queryValues.push(ident(tableArray[0].name));
|
|
1299
|
+
queryStrings.push(" ");
|
|
1300
|
+
queryStrings[queryStrings.length - 1] += expandedStrings[0];
|
|
1301
|
+
for (let i = 1; i < expandedStrings.length; i++) {
|
|
1302
|
+
queryStrings.push(expandedStrings[i]);
|
|
1303
|
+
}
|
|
1304
|
+
queryValues.push(...expandedValues);
|
|
1305
|
+
const rows = await this.#driver.all(
|
|
1306
|
+
makeTemplate(queryStrings),
|
|
1307
|
+
queryValues
|
|
1308
|
+
);
|
|
1309
|
+
return normalize(rows, tableArray);
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
get(tables, id) {
|
|
1313
|
+
if (id !== void 0) {
|
|
1314
|
+
const table2 = tables;
|
|
1315
|
+
const pk = table2.meta.primary;
|
|
1316
|
+
if (!pk) {
|
|
1317
|
+
return Promise.reject(
|
|
1318
|
+
new Error(`Table ${table2.name} has no primary key defined`)
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
const { strings, values } = buildSelectByPkParts(table2.name, pk, id);
|
|
1322
|
+
return this.#driver.get(strings, values).then((row) => {
|
|
1323
|
+
if (!row)
|
|
1324
|
+
return null;
|
|
1325
|
+
return decodeData(table2, row);
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
const tableArray = Array.isArray(tables) ? tables : [tables];
|
|
1329
|
+
return async (strings, ...values) => {
|
|
1330
|
+
const { strings: colStrings, values: colValues } = buildSelectCols(tableArray);
|
|
1331
|
+
const { strings: expandedStrings, values: expandedValues } = expandFragments(strings, values);
|
|
1332
|
+
const queryStrings = ["SELECT "];
|
|
1333
|
+
const queryValues = [];
|
|
1334
|
+
queryStrings[0] += colStrings[0];
|
|
1335
|
+
for (let i = 1; i < colStrings.length; i++) {
|
|
1336
|
+
queryStrings.push(colStrings[i]);
|
|
1337
|
+
}
|
|
1338
|
+
queryValues.push(...colValues);
|
|
1339
|
+
queryStrings[queryStrings.length - 1] += " FROM ";
|
|
1340
|
+
queryValues.push(ident(tableArray[0].name));
|
|
1341
|
+
queryStrings.push(" ");
|
|
1342
|
+
queryStrings[queryStrings.length - 1] += expandedStrings[0];
|
|
1343
|
+
for (let i = 1; i < expandedStrings.length; i++) {
|
|
1344
|
+
queryStrings.push(expandedStrings[i]);
|
|
1345
|
+
}
|
|
1346
|
+
queryValues.push(...expandedValues);
|
|
1347
|
+
const row = await this.#driver.get(
|
|
1348
|
+
makeTemplate(queryStrings),
|
|
1349
|
+
queryValues
|
|
1350
|
+
);
|
|
1351
|
+
return normalizeOne(row, tableArray);
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
async insert(table2, data) {
|
|
1355
|
+
if (Array.isArray(data)) {
|
|
1356
|
+
if (data.length === 0) {
|
|
1357
|
+
return [];
|
|
1358
|
+
}
|
|
1359
|
+
const results = [];
|
|
1360
|
+
for (const row of data) {
|
|
1361
|
+
results.push(await this.#insertOne(table2, row));
|
|
1362
|
+
}
|
|
1363
|
+
return results;
|
|
1364
|
+
}
|
|
1365
|
+
return this.#insertOne(table2, data);
|
|
1366
|
+
}
|
|
1367
|
+
async #insertOne(table2, data) {
|
|
1368
|
+
if (table2.meta.isPartial) {
|
|
1369
|
+
throw new Error(
|
|
1370
|
+
`Cannot insert into partial table "${table2.name}". Use the full table definition instead.`
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
if (table2.meta.isDerived) {
|
|
1374
|
+
throw new Error(
|
|
1375
|
+
`Cannot insert into derived table "${table2.name}". Derived tables are SELECT-only.`
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
const dataWithSchemaExprs = injectSchemaExpressions(
|
|
1379
|
+
table2,
|
|
1380
|
+
data,
|
|
1381
|
+
"insert"
|
|
1382
|
+
);
|
|
1383
|
+
const { regularData, expressions, symbols } = extractDBExpressions(
|
|
1384
|
+
dataWithSchemaExprs,
|
|
1385
|
+
table2
|
|
1386
|
+
);
|
|
1387
|
+
let schema = table2.schema;
|
|
1388
|
+
const skipFields = { ...expressions, ...symbols };
|
|
1389
|
+
if (Object.keys(skipFields).length > 0) {
|
|
1390
|
+
const skipFieldSchemas = Object.keys(skipFields).reduce(
|
|
1391
|
+
(acc, key) => {
|
|
1392
|
+
acc[key] = table2.schema.shape[key].optional();
|
|
1393
|
+
return acc;
|
|
1394
|
+
},
|
|
1395
|
+
{}
|
|
1396
|
+
);
|
|
1397
|
+
schema = table2.schema.extend(skipFieldSchemas);
|
|
1398
|
+
}
|
|
1399
|
+
const validated = validateWithStandardSchema(
|
|
1400
|
+
schema,
|
|
1401
|
+
regularData
|
|
1402
|
+
);
|
|
1403
|
+
const encoded = encodeData(table2, validated);
|
|
1404
|
+
const insertParts = buildInsertParts(
|
|
1405
|
+
table2.name,
|
|
1406
|
+
encoded,
|
|
1407
|
+
expressions,
|
|
1408
|
+
symbols
|
|
1409
|
+
);
|
|
1410
|
+
if (this.#driver.supportsReturning) {
|
|
1411
|
+
const { strings, values } = appendReturning(insertParts);
|
|
1412
|
+
const row = await this.#driver.get(
|
|
1413
|
+
strings,
|
|
1414
|
+
values
|
|
1415
|
+
);
|
|
1416
|
+
return decodeData(table2, row);
|
|
1417
|
+
}
|
|
1418
|
+
await this.#driver.run(insertParts.strings, insertParts.values);
|
|
1419
|
+
const pk = table2.meta.primary;
|
|
1420
|
+
const pkValue = pk ? encoded[pk] ?? (expressions[pk] ? void 0 : null) : null;
|
|
1421
|
+
if (pk && pkValue !== void 0 && pkValue !== null) {
|
|
1422
|
+
const { strings, values } = buildSelectByPkParts(table2.name, pk, pkValue);
|
|
1423
|
+
const row = await this.#driver.get(
|
|
1424
|
+
strings,
|
|
1425
|
+
values
|
|
1426
|
+
);
|
|
1427
|
+
if (row) {
|
|
1428
|
+
return decodeData(table2, row);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
return validated;
|
|
1432
|
+
}
|
|
1433
|
+
update(table2, data, idOrIds) {
|
|
1434
|
+
if (idOrIds === void 0) {
|
|
1435
|
+
return async (strings, ...values) => {
|
|
1436
|
+
return this.#updateWithWhere(table2, data, strings, values);
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
if (Array.isArray(idOrIds)) {
|
|
1440
|
+
return this.#updateByIds(table2, data, idOrIds);
|
|
1441
|
+
}
|
|
1442
|
+
return this.#updateById(table2, data, idOrIds);
|
|
1443
|
+
}
|
|
1444
|
+
async #updateById(table2, data, id) {
|
|
1445
|
+
const pk = table2.meta.primary;
|
|
1446
|
+
if (!pk) {
|
|
1447
|
+
throw new Error(`Table ${table2.name} has no primary key defined`);
|
|
1448
|
+
}
|
|
1449
|
+
if (table2.meta.isDerived) {
|
|
1450
|
+
throw new Error(
|
|
1451
|
+
`Cannot update derived table "${table2.name}". Derived tables are SELECT-only.`
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
const dataWithSchemaExprs = injectSchemaExpressions(
|
|
1455
|
+
table2,
|
|
1456
|
+
data,
|
|
1457
|
+
"update"
|
|
1458
|
+
);
|
|
1459
|
+
const { regularData, expressions, symbols } = extractDBExpressions(
|
|
1460
|
+
dataWithSchemaExprs,
|
|
1461
|
+
table2
|
|
1462
|
+
);
|
|
1463
|
+
const partialSchema = table2.schema.partial();
|
|
1464
|
+
const validated = validateWithStandardSchema(
|
|
1465
|
+
partialSchema,
|
|
1466
|
+
regularData
|
|
1467
|
+
);
|
|
1468
|
+
const allColumns = [
|
|
1469
|
+
...Object.keys(validated),
|
|
1470
|
+
...Object.keys(expressions),
|
|
1471
|
+
...Object.keys(symbols)
|
|
1472
|
+
];
|
|
1473
|
+
if (allColumns.length === 0) {
|
|
1474
|
+
throw new Error("No fields to update");
|
|
1475
|
+
}
|
|
1476
|
+
const encoded = encodeData(table2, validated);
|
|
1477
|
+
const updateParts = buildUpdateByIdParts(
|
|
1478
|
+
table2.name,
|
|
1479
|
+
pk,
|
|
1480
|
+
encoded,
|
|
1481
|
+
expressions,
|
|
1482
|
+
id,
|
|
1483
|
+
symbols
|
|
1484
|
+
);
|
|
1485
|
+
if (this.#driver.supportsReturning) {
|
|
1486
|
+
const { strings, values } = appendReturning(updateParts);
|
|
1487
|
+
const row2 = await this.#driver.get(
|
|
1488
|
+
strings,
|
|
1489
|
+
values
|
|
1490
|
+
);
|
|
1491
|
+
if (!row2)
|
|
1492
|
+
return null;
|
|
1493
|
+
return decodeData(table2, row2);
|
|
1494
|
+
}
|
|
1495
|
+
await this.#driver.run(updateParts.strings, updateParts.values);
|
|
1496
|
+
const { strings: selectStrings, values: selectValues } = buildSelectByPkParts(
|
|
1497
|
+
table2.name,
|
|
1498
|
+
pk,
|
|
1499
|
+
id
|
|
1500
|
+
);
|
|
1501
|
+
const row = await this.#driver.get(
|
|
1502
|
+
selectStrings,
|
|
1503
|
+
selectValues
|
|
1504
|
+
);
|
|
1505
|
+
if (!row)
|
|
1506
|
+
return null;
|
|
1507
|
+
return decodeData(table2, row);
|
|
1508
|
+
}
|
|
1509
|
+
async #updateByIds(table2, data, ids) {
|
|
1510
|
+
if (ids.length === 0) {
|
|
1511
|
+
return [];
|
|
1512
|
+
}
|
|
1513
|
+
const pk = table2.meta.primary;
|
|
1514
|
+
if (!pk) {
|
|
1515
|
+
throw new Error(`Table ${table2.name} has no primary key defined`);
|
|
1516
|
+
}
|
|
1517
|
+
if (table2.meta.isDerived) {
|
|
1518
|
+
throw new Error(
|
|
1519
|
+
`Cannot update derived table "${table2.name}". Derived tables are SELECT-only.`
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
const dataWithSchemaExprs = injectSchemaExpressions(
|
|
1523
|
+
table2,
|
|
1524
|
+
data,
|
|
1525
|
+
"update"
|
|
1526
|
+
);
|
|
1527
|
+
const { regularData, expressions, symbols } = extractDBExpressions(
|
|
1528
|
+
dataWithSchemaExprs,
|
|
1529
|
+
table2
|
|
1530
|
+
);
|
|
1531
|
+
const partialSchema = table2.schema.partial();
|
|
1532
|
+
const validated = validateWithStandardSchema(
|
|
1533
|
+
partialSchema,
|
|
1534
|
+
regularData
|
|
1535
|
+
);
|
|
1536
|
+
const allColumns = [
|
|
1537
|
+
...Object.keys(validated),
|
|
1538
|
+
...Object.keys(expressions),
|
|
1539
|
+
...Object.keys(symbols)
|
|
1540
|
+
];
|
|
1541
|
+
if (allColumns.length === 0) {
|
|
1542
|
+
throw new Error("No fields to update");
|
|
1543
|
+
}
|
|
1544
|
+
const encoded = encodeData(table2, validated);
|
|
1545
|
+
const updateParts = buildUpdateByIdsParts(
|
|
1546
|
+
table2.name,
|
|
1547
|
+
pk,
|
|
1548
|
+
encoded,
|
|
1549
|
+
expressions,
|
|
1550
|
+
ids,
|
|
1551
|
+
symbols
|
|
1552
|
+
);
|
|
1553
|
+
if (this.#driver.supportsReturning) {
|
|
1554
|
+
const { strings, values } = appendReturning(updateParts);
|
|
1555
|
+
const rows2 = await this.#driver.all(
|
|
1556
|
+
strings,
|
|
1557
|
+
values
|
|
1558
|
+
);
|
|
1559
|
+
const resultMap2 = /* @__PURE__ */ new Map();
|
|
1560
|
+
for (const row of rows2) {
|
|
1561
|
+
const entity = decodeData(table2, row);
|
|
1562
|
+
resultMap2.set(row[pk], entity);
|
|
1563
|
+
}
|
|
1564
|
+
return ids.map((id) => resultMap2.get(id) ?? null);
|
|
1565
|
+
}
|
|
1566
|
+
await this.#driver.run(updateParts.strings, updateParts.values);
|
|
1567
|
+
const { strings: selectStrings, values: selectValues } = buildSelectByPksParts(table2.name, pk, ids);
|
|
1568
|
+
const rows = await this.#driver.all(
|
|
1569
|
+
selectStrings,
|
|
1570
|
+
selectValues
|
|
1571
|
+
);
|
|
1572
|
+
const resultMap = /* @__PURE__ */ new Map();
|
|
1573
|
+
for (const row of rows) {
|
|
1574
|
+
const entity = decodeData(table2, row);
|
|
1575
|
+
resultMap.set(row[pk], entity);
|
|
1576
|
+
}
|
|
1577
|
+
return ids.map((id) => resultMap.get(id) ?? null);
|
|
1578
|
+
}
|
|
1579
|
+
async #updateWithWhere(table2, data, strings, templateValues) {
|
|
1580
|
+
if (table2.meta.isDerived) {
|
|
1581
|
+
throw new Error(
|
|
1582
|
+
`Cannot update derived table "${table2.name}". Derived tables are SELECT-only.`
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
const pk = table2.meta.primary;
|
|
1586
|
+
if (!pk) {
|
|
1587
|
+
throw new Error(`Table ${table2.name} has no primary key defined`);
|
|
1588
|
+
}
|
|
1589
|
+
const dataWithSchemaExprs = injectSchemaExpressions(
|
|
1590
|
+
table2,
|
|
1591
|
+
data,
|
|
1592
|
+
"update"
|
|
1593
|
+
);
|
|
1594
|
+
const { regularData, expressions } = extractDBExpressions(
|
|
1595
|
+
dataWithSchemaExprs,
|
|
1596
|
+
table2
|
|
1597
|
+
);
|
|
1598
|
+
const partialSchema = table2.schema.partial();
|
|
1599
|
+
const validated = validateWithStandardSchema(
|
|
1600
|
+
partialSchema,
|
|
1601
|
+
regularData
|
|
1602
|
+
);
|
|
1603
|
+
const allColumns = [...Object.keys(validated), ...Object.keys(expressions)];
|
|
1604
|
+
if (allColumns.length === 0) {
|
|
1605
|
+
throw new Error("No fields to update");
|
|
1606
|
+
}
|
|
1607
|
+
const encoded = encodeData(table2, validated);
|
|
1608
|
+
const { strings: whereStrings, values: whereValues } = expandFragments(
|
|
1609
|
+
strings,
|
|
1610
|
+
templateValues
|
|
1611
|
+
);
|
|
1612
|
+
const setCols = Object.keys(encoded);
|
|
1613
|
+
const exprCols = Object.keys(expressions);
|
|
1614
|
+
const queryStrings = ["UPDATE "];
|
|
1615
|
+
const queryValues = [];
|
|
1616
|
+
queryValues.push(ident(table2.name));
|
|
1617
|
+
queryStrings.push(" SET ");
|
|
1618
|
+
for (let i = 0; i < setCols.length; i++) {
|
|
1619
|
+
const col = setCols[i];
|
|
1620
|
+
queryValues.push(ident(col));
|
|
1621
|
+
queryStrings.push(" = ");
|
|
1622
|
+
queryValues.push(encoded[col]);
|
|
1623
|
+
queryStrings.push(i < setCols.length - 1 ? ", " : "");
|
|
1624
|
+
}
|
|
1625
|
+
for (let i = 0; i < exprCols.length; i++) {
|
|
1626
|
+
if (setCols.length > 0 || i > 0) {
|
|
1627
|
+
queryStrings[queryStrings.length - 1] += ", ";
|
|
1628
|
+
}
|
|
1629
|
+
queryValues.push(ident(exprCols[i]));
|
|
1630
|
+
queryStrings.push(" = ");
|
|
1631
|
+
mergeExpression(queryStrings, queryValues, expressions[exprCols[i]]);
|
|
1632
|
+
}
|
|
1633
|
+
queryStrings[queryStrings.length - 1] += " " + whereStrings[0];
|
|
1634
|
+
for (let i = 1; i < whereStrings.length; i++) {
|
|
1635
|
+
queryStrings.push(whereStrings[i]);
|
|
1636
|
+
}
|
|
1637
|
+
queryValues.push(...whereValues);
|
|
1638
|
+
if (this.#driver.supportsReturning) {
|
|
1639
|
+
queryStrings[queryStrings.length - 1] += " RETURNING *";
|
|
1640
|
+
const rows2 = await this.#driver.all(
|
|
1641
|
+
makeTemplate(queryStrings),
|
|
1642
|
+
queryValues
|
|
1643
|
+
);
|
|
1644
|
+
return rows2.map((row) => decodeData(table2, row));
|
|
1645
|
+
}
|
|
1646
|
+
const selectIdStrings = ["SELECT "];
|
|
1647
|
+
const selectIdValues = [];
|
|
1648
|
+
selectIdValues.push(ident(pk));
|
|
1649
|
+
selectIdStrings.push(" FROM ");
|
|
1650
|
+
selectIdValues.push(ident(table2.name));
|
|
1651
|
+
selectIdStrings.push(" " + whereStrings[0]);
|
|
1652
|
+
for (let i = 1; i < whereStrings.length; i++) {
|
|
1653
|
+
selectIdStrings.push(whereStrings[i]);
|
|
1654
|
+
}
|
|
1655
|
+
selectIdValues.push(...whereValues);
|
|
1656
|
+
const idRows = await this.#driver.all(
|
|
1657
|
+
makeTemplate(selectIdStrings),
|
|
1658
|
+
selectIdValues
|
|
1659
|
+
);
|
|
1660
|
+
const ids = idRows.map((r) => r[pk]);
|
|
1661
|
+
if (ids.length === 0) {
|
|
1662
|
+
return [];
|
|
1663
|
+
}
|
|
1664
|
+
await this.#driver.run(makeTemplate(queryStrings), queryValues);
|
|
1665
|
+
const { strings: selectStrings, values: selectVals } = buildSelectByPksParts(
|
|
1666
|
+
table2.name,
|
|
1667
|
+
pk,
|
|
1668
|
+
ids
|
|
1669
|
+
);
|
|
1670
|
+
const rows = await this.#driver.all(
|
|
1671
|
+
selectStrings,
|
|
1672
|
+
selectVals
|
|
1673
|
+
);
|
|
1674
|
+
return rows.map((row) => decodeData(table2, row));
|
|
1675
|
+
}
|
|
1676
|
+
delete(table2, idOrIds) {
|
|
1677
|
+
if (idOrIds === void 0) {
|
|
1678
|
+
return async (strings, ...values) => {
|
|
1679
|
+
const { strings: expandedStrings, values: expandedValues } = expandFragments(strings, values);
|
|
1680
|
+
const deleteStrings = ["DELETE FROM "];
|
|
1681
|
+
const deleteValues = [ident(table2.name)];
|
|
1682
|
+
deleteStrings.push(" " + expandedStrings[0]);
|
|
1683
|
+
for (let i = 1; i < expandedStrings.length; i++) {
|
|
1684
|
+
deleteStrings.push(expandedStrings[i]);
|
|
1685
|
+
}
|
|
1686
|
+
deleteValues.push(...expandedValues);
|
|
1687
|
+
return this.#driver.run(makeTemplate(deleteStrings), deleteValues);
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
if (Array.isArray(idOrIds)) {
|
|
1691
|
+
return this.#deleteByIds(table2, idOrIds);
|
|
1692
|
+
}
|
|
1693
|
+
return this.#deleteById(table2, idOrIds);
|
|
1694
|
+
}
|
|
1695
|
+
async #deleteById(table2, id) {
|
|
1696
|
+
const pk = table2.meta.primary;
|
|
1697
|
+
if (!pk) {
|
|
1698
|
+
throw new Error(`Table ${table2.name} has no primary key defined`);
|
|
1699
|
+
}
|
|
1700
|
+
const { strings, values } = buildDeleteByPkParts(table2.name, pk, id);
|
|
1701
|
+
return this.#driver.run(strings, values);
|
|
1702
|
+
}
|
|
1703
|
+
async #deleteByIds(table2, ids) {
|
|
1704
|
+
if (ids.length === 0) {
|
|
1705
|
+
return 0;
|
|
1706
|
+
}
|
|
1707
|
+
const pk = table2.meta.primary;
|
|
1708
|
+
if (!pk) {
|
|
1709
|
+
throw new Error(`Table ${table2.name} has no primary key defined`);
|
|
1710
|
+
}
|
|
1711
|
+
const { strings, values } = buildDeleteByPksParts(table2.name, pk, ids);
|
|
1712
|
+
return this.#driver.run(strings, values);
|
|
1713
|
+
}
|
|
1714
|
+
softDelete(table2, idOrIds) {
|
|
1715
|
+
const softDeleteField = table2.meta.softDeleteField;
|
|
1716
|
+
if (!softDeleteField) {
|
|
1717
|
+
throw new Error(
|
|
1718
|
+
`Table ${table2.name} does not have a soft delete field. Use softDelete() wrapper to mark a field.`
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
if (idOrIds === void 0) {
|
|
1722
|
+
return async (strings, ...values) => {
|
|
1723
|
+
return this.#softDeleteWithWhere(table2, strings, values);
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
if (Array.isArray(idOrIds)) {
|
|
1727
|
+
return this.#softDeleteByIds(table2, idOrIds);
|
|
1728
|
+
}
|
|
1729
|
+
return this.#softDeleteById(table2, idOrIds);
|
|
1730
|
+
}
|
|
1731
|
+
async #softDeleteById(table2, id) {
|
|
1732
|
+
const pk = table2.meta.primary;
|
|
1733
|
+
if (!pk) {
|
|
1734
|
+
throw new Error(`Table ${table2.name} has no primary key defined`);
|
|
1735
|
+
}
|
|
1736
|
+
const softDeleteField = table2.meta.softDeleteField;
|
|
1737
|
+
const schemaExprs = injectSchemaExpressions(table2, {}, "update");
|
|
1738
|
+
const { expressions, symbols } = extractDBExpressions(schemaExprs);
|
|
1739
|
+
const queryStrings = ["UPDATE "];
|
|
1740
|
+
const queryValues = [];
|
|
1741
|
+
queryValues.push(ident(table2.name));
|
|
1742
|
+
queryStrings.push(" SET ");
|
|
1743
|
+
queryValues.push(ident(softDeleteField));
|
|
1744
|
+
queryStrings.push(" = CURRENT_TIMESTAMP");
|
|
1745
|
+
for (const [field, expr] of Object.entries(expressions)) {
|
|
1746
|
+
if (field !== softDeleteField) {
|
|
1747
|
+
queryStrings[queryStrings.length - 1] += ", ";
|
|
1748
|
+
queryValues.push(ident(field));
|
|
1749
|
+
queryStrings.push(" = ");
|
|
1750
|
+
mergeExpression(queryStrings, queryValues, expr);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
for (const [field, sym] of Object.entries(symbols)) {
|
|
1754
|
+
if (field !== softDeleteField) {
|
|
1755
|
+
queryStrings[queryStrings.length - 1] += ", ";
|
|
1756
|
+
queryValues.push(ident(field));
|
|
1757
|
+
queryStrings.push(` = ${resolveSQLBuiltin(sym)}`);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
queryStrings[queryStrings.length - 1] += " WHERE ";
|
|
1761
|
+
queryValues.push(ident(pk));
|
|
1762
|
+
queryStrings.push(" = ");
|
|
1763
|
+
queryValues.push(id);
|
|
1764
|
+
queryStrings.push("");
|
|
1765
|
+
return this.#driver.run(makeTemplate(queryStrings), queryValues);
|
|
1766
|
+
}
|
|
1767
|
+
async #softDeleteByIds(table2, ids) {
|
|
1768
|
+
if (ids.length === 0) {
|
|
1769
|
+
return 0;
|
|
1770
|
+
}
|
|
1771
|
+
const pk = table2.meta.primary;
|
|
1772
|
+
if (!pk) {
|
|
1773
|
+
throw new Error(`Table ${table2.name} has no primary key defined`);
|
|
1774
|
+
}
|
|
1775
|
+
const softDeleteField = table2.meta.softDeleteField;
|
|
1776
|
+
const schemaExprs = injectSchemaExpressions(table2, {}, "update");
|
|
1777
|
+
const { expressions, symbols } = extractDBExpressions(schemaExprs);
|
|
1778
|
+
const queryStrings = ["UPDATE "];
|
|
1779
|
+
const queryValues = [];
|
|
1780
|
+
queryValues.push(ident(table2.name));
|
|
1781
|
+
queryStrings.push(" SET ");
|
|
1782
|
+
queryValues.push(ident(softDeleteField));
|
|
1783
|
+
queryStrings.push(" = CURRENT_TIMESTAMP");
|
|
1784
|
+
for (const [field, expr] of Object.entries(expressions)) {
|
|
1785
|
+
if (field !== softDeleteField) {
|
|
1786
|
+
queryStrings[queryStrings.length - 1] += ", ";
|
|
1787
|
+
queryValues.push(ident(field));
|
|
1788
|
+
queryStrings.push(" = ");
|
|
1789
|
+
mergeExpression(queryStrings, queryValues, expr);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
for (const [field, sym] of Object.entries(symbols)) {
|
|
1793
|
+
if (field !== softDeleteField) {
|
|
1794
|
+
queryStrings[queryStrings.length - 1] += ", ";
|
|
1795
|
+
queryValues.push(ident(field));
|
|
1796
|
+
queryStrings.push(` = ${resolveSQLBuiltin(sym)}`);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
queryStrings[queryStrings.length - 1] += " WHERE ";
|
|
1800
|
+
queryValues.push(ident(pk));
|
|
1801
|
+
queryStrings.push(" IN (");
|
|
1802
|
+
for (let i = 0; i < ids.length; i++) {
|
|
1803
|
+
queryValues.push(ids[i]);
|
|
1804
|
+
queryStrings.push(i < ids.length - 1 ? ", " : ")");
|
|
1805
|
+
}
|
|
1806
|
+
return this.#driver.run(makeTemplate(queryStrings), queryValues);
|
|
1807
|
+
}
|
|
1808
|
+
async #softDeleteWithWhere(table2, strings, templateValues) {
|
|
1809
|
+
const softDeleteField = table2.meta.softDeleteField;
|
|
1810
|
+
const schemaExprs = injectSchemaExpressions(table2, {}, "update");
|
|
1811
|
+
const { expressions, symbols } = extractDBExpressions(schemaExprs);
|
|
1812
|
+
const { strings: expandedStrings, values: expandedValues } = expandFragments(
|
|
1813
|
+
strings,
|
|
1814
|
+
templateValues
|
|
1815
|
+
);
|
|
1816
|
+
const queryStrings = ["UPDATE "];
|
|
1817
|
+
const queryValues = [];
|
|
1818
|
+
queryValues.push(ident(table2.name));
|
|
1819
|
+
queryStrings.push(" SET ");
|
|
1820
|
+
queryValues.push(ident(softDeleteField));
|
|
1821
|
+
queryStrings.push(" = CURRENT_TIMESTAMP");
|
|
1822
|
+
for (const [field, expr] of Object.entries(expressions)) {
|
|
1823
|
+
if (field !== softDeleteField) {
|
|
1824
|
+
queryStrings[queryStrings.length - 1] += ", ";
|
|
1825
|
+
queryValues.push(ident(field));
|
|
1826
|
+
queryStrings.push(" = ");
|
|
1827
|
+
mergeExpression(queryStrings, queryValues, expr);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
for (const [field, sym] of Object.entries(symbols)) {
|
|
1831
|
+
if (field !== softDeleteField) {
|
|
1832
|
+
queryStrings[queryStrings.length - 1] += ", ";
|
|
1833
|
+
queryValues.push(ident(field));
|
|
1834
|
+
queryStrings.push(` = ${resolveSQLBuiltin(sym)}`);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
queryStrings[queryStrings.length - 1] += " " + expandedStrings[0];
|
|
1838
|
+
for (let i = 1; i < expandedStrings.length; i++) {
|
|
1839
|
+
queryStrings.push(expandedStrings[i]);
|
|
1840
|
+
}
|
|
1841
|
+
queryValues.push(...expandedValues);
|
|
1842
|
+
return this.#driver.run(makeTemplate(queryStrings), queryValues);
|
|
1843
|
+
}
|
|
1844
|
+
// ==========================================================================
|
|
1845
|
+
// Raw - No Normalization
|
|
1846
|
+
// ==========================================================================
|
|
1847
|
+
/**
|
|
1848
|
+
* Execute a raw query and return rows.
|
|
1849
|
+
*
|
|
1850
|
+
* @example
|
|
1851
|
+
* const counts = await db.query<{ count: number }>`
|
|
1852
|
+
* SELECT COUNT(*) as count FROM posts WHERE author_id = ${userId}
|
|
1853
|
+
* `;
|
|
1854
|
+
*/
|
|
1855
|
+
async query(strings, ...values) {
|
|
1856
|
+
const { strings: expandedStrings, values: expandedValues } = expandFragments(
|
|
1857
|
+
strings,
|
|
1858
|
+
values
|
|
1859
|
+
);
|
|
1860
|
+
return this.#driver.all(expandedStrings, expandedValues);
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Execute a statement (INSERT, UPDATE, DELETE, DDL).
|
|
1864
|
+
*
|
|
1865
|
+
* @example
|
|
1866
|
+
* await db.exec`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY)`;
|
|
1867
|
+
*/
|
|
1868
|
+
async exec(strings, ...values) {
|
|
1869
|
+
const { strings: expandedStrings, values: expandedValues } = expandFragments(
|
|
1870
|
+
strings,
|
|
1871
|
+
values
|
|
1872
|
+
);
|
|
1873
|
+
return this.#driver.run(expandedStrings, expandedValues);
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Execute a query and return a single value.
|
|
1877
|
+
*
|
|
1878
|
+
* @example
|
|
1879
|
+
* const count = await db.val<number>`SELECT COUNT(*) FROM posts`;
|
|
1880
|
+
*/
|
|
1881
|
+
async val(strings, ...values) {
|
|
1882
|
+
const { strings: expandedStrings, values: expandedValues } = expandFragments(
|
|
1883
|
+
strings,
|
|
1884
|
+
values
|
|
1885
|
+
);
|
|
1886
|
+
return this.#driver.val(expandedStrings, expandedValues);
|
|
1887
|
+
}
|
|
1888
|
+
// ==========================================================================
|
|
1889
|
+
// Debugging
|
|
1890
|
+
// ==========================================================================
|
|
1891
|
+
/**
|
|
1892
|
+
* Print the generated SQL and parameters without executing.
|
|
1893
|
+
* Useful for debugging query composition and fragment expansion.
|
|
1894
|
+
* Note: SQL shown uses ? placeholders; actual query may use $1, $2 etc.
|
|
1895
|
+
*/
|
|
1896
|
+
print(strings, ...values) {
|
|
1897
|
+
const { strings: expandedStrings, values: expandedValues } = expandFragments(
|
|
1898
|
+
strings,
|
|
1899
|
+
values
|
|
1900
|
+
);
|
|
1901
|
+
let sql = expandedStrings[0];
|
|
1902
|
+
for (let i = 1; i < expandedStrings.length; i++) {
|
|
1903
|
+
sql += "?" + expandedStrings[i];
|
|
1904
|
+
}
|
|
1905
|
+
return { sql, params: expandedValues };
|
|
1906
|
+
}
|
|
1907
|
+
// ==========================================================================
|
|
1908
|
+
// Schema Ensure Methods
|
|
1909
|
+
// ==========================================================================
|
|
1910
|
+
/**
|
|
1911
|
+
* Ensure a table exists with its columns and indexes.
|
|
1912
|
+
*
|
|
1913
|
+
* **For new tables**: Creates the table with full structure including
|
|
1914
|
+
* primary key, unique constraints, foreign keys, and indexes.
|
|
1915
|
+
*
|
|
1916
|
+
* **For existing tables**: Only performs safe, additive operations:
|
|
1917
|
+
* - Adds missing columns
|
|
1918
|
+
* - Adds missing non-unique indexes
|
|
1919
|
+
*
|
|
1920
|
+
* Unique constraints and foreign keys on existing tables require
|
|
1921
|
+
* explicit `ensureConstraints()` call (they can fail or lock).
|
|
1922
|
+
*
|
|
1923
|
+
* @throws {EnsureError} If DDL execution fails
|
|
1924
|
+
* @throws {SchemaDriftError} If existing table has missing constraints
|
|
1925
|
+
* (directs user to run ensureConstraints)
|
|
1926
|
+
*
|
|
1927
|
+
* @example
|
|
1928
|
+
* // In migration handler
|
|
1929
|
+
* await db.ensureTable(Users);
|
|
1930
|
+
* await db.ensureTable(Posts); // FK to Users - ensure Users first
|
|
1931
|
+
*/
|
|
1932
|
+
async ensureTable(table2) {
|
|
1933
|
+
if (!this.#driver.ensureTable) {
|
|
1934
|
+
throw new Error(
|
|
1935
|
+
"Driver does not implement ensureTable(). Schema ensure methods require a driver with schema management support."
|
|
1936
|
+
);
|
|
1937
|
+
}
|
|
1938
|
+
const doEnsure = () => this.#driver.ensureTable(table2);
|
|
1939
|
+
if (this.#driver.withMigrationLock) {
|
|
1940
|
+
return await this.#driver.withMigrationLock(doEnsure);
|
|
1941
|
+
}
|
|
1942
|
+
return await doEnsure();
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Ensure constraints (unique, foreign key) are applied to an existing table.
|
|
1946
|
+
*
|
|
1947
|
+
* **WARNING**: This operation can be expensive and cause locks on large tables.
|
|
1948
|
+
* It performs preflight checks to detect data violations before applying constraints.
|
|
1949
|
+
*
|
|
1950
|
+
* For each declared constraint:
|
|
1951
|
+
* 1. Preflight: Check for violations (duplicates for UNIQUE, orphans for FK)
|
|
1952
|
+
* 2. If violations found: Throw ConstraintPreflightError with diagnostic query
|
|
1953
|
+
* 3. If clean: Apply the constraint
|
|
1954
|
+
*
|
|
1955
|
+
* @throws {Error} If table doesn't exist
|
|
1956
|
+
* @throws {ConstraintPreflightError} If data violates a constraint
|
|
1957
|
+
* @throws {EnsureError} If DDL execution fails
|
|
1958
|
+
*
|
|
1959
|
+
* @example
|
|
1960
|
+
* // After ensuring table structure
|
|
1961
|
+
* await db.ensureTable(Users);
|
|
1962
|
+
* // Explicitly apply constraints (may lock, may fail)
|
|
1963
|
+
* await db.ensureConstraints(Users);
|
|
1964
|
+
*/
|
|
1965
|
+
async ensureConstraints(table2) {
|
|
1966
|
+
if (!this.#driver.ensureConstraints) {
|
|
1967
|
+
throw new Error(
|
|
1968
|
+
"Driver does not implement ensureConstraints(). Schema ensure methods require a driver with schema management support."
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1971
|
+
const doEnsure = () => this.#driver.ensureConstraints(table2);
|
|
1972
|
+
if (this.#driver.withMigrationLock) {
|
|
1973
|
+
return await this.#driver.withMigrationLock(doEnsure);
|
|
1974
|
+
}
|
|
1975
|
+
return await doEnsure();
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Copy column data for safe rename migrations.
|
|
1979
|
+
*
|
|
1980
|
+
* Executes: UPDATE <table> SET <toField> = <fromField> WHERE <toField> IS NULL
|
|
1981
|
+
*
|
|
1982
|
+
* This is idempotent - rows where toField already has a value are skipped.
|
|
1983
|
+
* The fromField may be a legacy column not in the current schema.
|
|
1984
|
+
*
|
|
1985
|
+
* @param table The table to update
|
|
1986
|
+
* @param fromField Source column (may be legacy/not in schema)
|
|
1987
|
+
* @param toField Destination column (must exist in schema)
|
|
1988
|
+
* @returns Number of rows updated
|
|
1989
|
+
*
|
|
1990
|
+
* @example
|
|
1991
|
+
* // Rename "email" to "emailAddress":
|
|
1992
|
+
* // 1. Add new column
|
|
1993
|
+
* await db.ensureTable(UsersWithEmailAddress);
|
|
1994
|
+
* // 2. Copy data
|
|
1995
|
+
* const updated = await db.copyColumn(Users, "email", "emailAddress");
|
|
1996
|
+
* // 3. Later: remove old column (manual migration)
|
|
1997
|
+
*/
|
|
1998
|
+
async copyColumn(table2, fromField, toField) {
|
|
1999
|
+
const fields = Object.keys(table2.meta.fields);
|
|
2000
|
+
if (!fields.includes(toField)) {
|
|
2001
|
+
throw new Error(
|
|
2002
|
+
`Destination field "${toField}" does not exist in table "${table2.name}". Available fields: ${fields.join(", ")}`
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
if (this.#driver.copyColumn) {
|
|
2006
|
+
const doCopy2 = () => this.#driver.copyColumn(table2, fromField, toField);
|
|
2007
|
+
if (this.#driver.withMigrationLock) {
|
|
2008
|
+
return await this.#driver.withMigrationLock(doCopy2);
|
|
2009
|
+
}
|
|
2010
|
+
return await doCopy2();
|
|
2011
|
+
}
|
|
2012
|
+
const doCopy = async () => {
|
|
2013
|
+
const tableName = table2.name;
|
|
2014
|
+
const columnExists = await this.#checkColumnExists(tableName, fromField);
|
|
2015
|
+
if (!columnExists) {
|
|
2016
|
+
throw new EnsureError(
|
|
2017
|
+
`Source field "${fromField}" does not exist in table "${tableName}"`,
|
|
2018
|
+
{ operation: "copyColumn", table: tableName, step: 0 }
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
try {
|
|
2022
|
+
const updateStrings = makeTemplate([
|
|
2023
|
+
"UPDATE ",
|
|
2024
|
+
" SET ",
|
|
2025
|
+
" = ",
|
|
2026
|
+
" WHERE ",
|
|
2027
|
+
" IS NULL"
|
|
2028
|
+
]);
|
|
2029
|
+
const updateValues = [
|
|
2030
|
+
ident(tableName),
|
|
2031
|
+
ident(toField),
|
|
2032
|
+
ident(fromField),
|
|
2033
|
+
ident(toField)
|
|
2034
|
+
];
|
|
2035
|
+
return await this.#driver.run(updateStrings, updateValues);
|
|
2036
|
+
} catch (error) {
|
|
2037
|
+
throw new EnsureError(
|
|
2038
|
+
`copyColumn failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2039
|
+
{ operation: "copyColumn", table: tableName, step: 0 },
|
|
2040
|
+
{ cause: error }
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
};
|
|
2044
|
+
if (this.#driver.withMigrationLock) {
|
|
2045
|
+
return await this.#driver.withMigrationLock(doCopy);
|
|
2046
|
+
}
|
|
2047
|
+
return await doCopy();
|
|
2048
|
+
}
|
|
2049
|
+
/**
|
|
2050
|
+
* Check if a column exists in the actual database table.
|
|
2051
|
+
* Queries the actual table structure to verify column existence.
|
|
2052
|
+
*/
|
|
2053
|
+
async #checkColumnExists(tableName, columnName) {
|
|
2054
|
+
if (this.#driver.getColumns) {
|
|
2055
|
+
const columns = await this.#driver.getColumns(tableName);
|
|
2056
|
+
return columns.some((col) => col.name === columnName);
|
|
2057
|
+
}
|
|
2058
|
+
try {
|
|
2059
|
+
const pragmaStrings = makeTemplate(["PRAGMA table_info(", ")"]);
|
|
2060
|
+
const pragmaValues = [ident(tableName)];
|
|
2061
|
+
const columns = await this.#driver.all(
|
|
2062
|
+
pragmaStrings,
|
|
2063
|
+
pragmaValues
|
|
2064
|
+
);
|
|
2065
|
+
if (columns.length > 0) {
|
|
2066
|
+
return columns.some((col) => col.name === columnName);
|
|
2067
|
+
}
|
|
2068
|
+
} catch {
|
|
2069
|
+
}
|
|
2070
|
+
try {
|
|
2071
|
+
const schemaStrings = makeTemplate([
|
|
2072
|
+
"SELECT column_name FROM information_schema.columns WHERE table_name = ",
|
|
2073
|
+
" AND column_name = ",
|
|
2074
|
+
" LIMIT 1"
|
|
2075
|
+
]);
|
|
2076
|
+
const schemaValues = [tableName, columnName];
|
|
2077
|
+
const result = await this.#driver.all(schemaStrings, schemaValues);
|
|
2078
|
+
return result.length > 0;
|
|
2079
|
+
} catch {
|
|
2080
|
+
return true;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
// ==========================================================================
|
|
2084
|
+
// Transactions
|
|
2085
|
+
// ==========================================================================
|
|
2086
|
+
/**
|
|
2087
|
+
* Execute a function within a database transaction.
|
|
2088
|
+
*
|
|
2089
|
+
* If the function completes successfully, the transaction is committed.
|
|
2090
|
+
* If the function throws an error, the transaction is rolled back.
|
|
2091
|
+
*
|
|
2092
|
+
* All operations within the transaction callback use the same database
|
|
2093
|
+
* connection, ensuring transactional consistency.
|
|
2094
|
+
*
|
|
2095
|
+
* @example
|
|
2096
|
+
* await db.transaction(async (tx) => {
|
|
2097
|
+
* const user = await tx.insert(users, { id: "1", name: "Alice" });
|
|
2098
|
+
* await tx.insert(posts, { id: "1", authorId: user.id, title: "Hello" });
|
|
2099
|
+
* // If any insert fails, both are rolled back
|
|
2100
|
+
* });
|
|
2101
|
+
*/
|
|
2102
|
+
async transaction(fn) {
|
|
2103
|
+
return await this.#driver.transaction(async (txDriver) => {
|
|
2104
|
+
const tx = new Transaction(txDriver);
|
|
2105
|
+
return await fn(tx);
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
|
|
2110
|
+
// src/zen.ts
|
|
2111
|
+
extendZod(zod);
|
|
2112
|
+
export {
|
|
2113
|
+
AlreadyExistsError,
|
|
2114
|
+
CURRENT_DATE,
|
|
2115
|
+
CURRENT_TIME,
|
|
2116
|
+
CURRENT_TIMESTAMP,
|
|
2117
|
+
ConnectionError,
|
|
2118
|
+
ConstraintPreflightError,
|
|
2119
|
+
ConstraintViolationError,
|
|
2120
|
+
Database,
|
|
2121
|
+
DatabaseError,
|
|
2122
|
+
DatabaseUpgradeEvent,
|
|
2123
|
+
EnsureError,
|
|
2124
|
+
MigrationError,
|
|
2125
|
+
MigrationLockError,
|
|
2126
|
+
NOW,
|
|
2127
|
+
NotFoundError,
|
|
2128
|
+
QueryError,
|
|
2129
|
+
SchemaDriftError,
|
|
2130
|
+
TODAY,
|
|
2131
|
+
TableDefinitionError,
|
|
2132
|
+
Transaction,
|
|
2133
|
+
TransactionError,
|
|
2134
|
+
ValidationError,
|
|
2135
|
+
hasErrorCode,
|
|
2136
|
+
ident,
|
|
2137
|
+
isDatabaseError,
|
|
2138
|
+
isSQLBuiltin,
|
|
2139
|
+
isSQLIdentifier,
|
|
2140
|
+
isSQLTemplate,
|
|
2141
|
+
table,
|
|
2142
|
+
zod as z
|
|
2143
|
+
};
|