evikit 2.9.2 → 2.10.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.
Files changed (6) hide show
  1. package/README.md +11 -19
  2. package/db.d.ts +255 -255
  3. package/index.d.ts +19 -1
  4. package/index.js +41 -7
  5. package/package.json +1 -1
  6. package/sqlite.js +576 -576
package/sqlite.js CHANGED
@@ -1,576 +1,576 @@
1
- import envPaths from "env-paths";
2
- import { fail, ok } from "node:assert/strict";
3
- import { mkdirSync } from "node:fs";
4
- import { DatabaseSync } from "node:sqlite";
5
- import { getMetadata, parse } from "valibot";
6
-
7
- import { flatten, relations } from "./db.js";
8
-
9
- /**
10
- * @typedef Column
11
- * @property {string} name
12
- * @property {0 | 1} notnull
13
- * @property {0 | 1} pk
14
- * @property {"INTEGER" | "TEXT"} type
15
- */
16
-
17
- /** @type {typeof import("./sqlite.d.ts").sqlite} */
18
- export function sqlite({ pkg, schema }) {
19
- const data = pkg
20
- ? (() => {
21
- const { data } = envPaths(pkg.name);
22
- mkdirSync(data, { recursive: true });
23
- return data;
24
- })()
25
- : undefined;
26
- const database = new DatabaseSync(data ? `${data}/db.sqlite3` : ":memory:", {
27
- timeout: 1000,
28
- });
29
- database.prepare("pragma journal_mode=wal").run();
30
- const flat = flatten(relations(schema));
31
-
32
- const haveTables = /** @type {{ name: string }[]} */ (
33
- database
34
- .prepare(
35
- "select * from sqlite_schema where type='table' and name not like 'sqlite_%'",
36
- )
37
- .all()
38
- ).map((table) => {
39
- const columns = /** @type {Column[]} */ (
40
- database.prepare(`pragma table_info(${table.name})`).all()
41
- );
42
- const foreignKeys =
43
- /** @type {{ from: string, on_delete: "CASCADE" | "SET NULL", on_update: "CASCADE" | "SET NULL", table: string, to: string, }[]} */ (
44
- database.prepare(`pragma foreign_key_list(${table.name})`).all()
45
- );
46
- return {
47
- columns,
48
- foreignKeys,
49
- indices: /** @type {{ name: string }[]} */ (
50
- database.prepare(`pragma index_list(${table.name})`).all()
51
- )
52
- .filter(({ name }) => !/^sqlite_/.test(name))
53
- .map((index) => ({
54
- ...index,
55
- info: /** @type {{ name: string; }} */ (
56
- database.prepare(`pragma index_info(${index.name})`).get()
57
- ),
58
- })),
59
- table,
60
- };
61
- });
62
-
63
- const wantTables = Object.entries(schema).map(([name, { entries }]) => ({
64
- columns: Object.entries(entries).map(([name, maybeEntry]) => {
65
- const entry =
66
- /** @type {import("valibot").BaseSchema<undefined, undefined, any>} */ (
67
- maybeEntry
68
- );
69
- const pk = /** @type {{ primary?: true }} */ (getMetadata(entry)).primary
70
- ? 1
71
- : 0;
72
- return {
73
- name,
74
- notnull: !pk && entry.expects.includes("undefined") ? 0 : 1,
75
- pk,
76
- type:
77
- entry.expects == "string" || entry.expects == "(string | undefined)"
78
- ? "TEXT"
79
- : entry.expects == "number" ||
80
- entry.expects == "(number | undefined)"
81
- ? "INTEGER"
82
- : entry.expects == "Uint8Array" ||
83
- entry.expects == "(Uint8Array | undefined)"
84
- ? "BLOB"
85
- : fail(name),
86
- };
87
- }),
88
- foreignKeys: Object.entries(entries)
89
- .map(([name, maybeEntry]) => {
90
- const entry =
91
- /** @type {import("valibot").BaseSchema<undefined, undefined, any>} */ (
92
- maybeEntry
93
- );
94
-
95
- const metadata =
96
- /** @type {{ key?: string, references?: any } | undefined} */ (
97
- getMetadata(entry)
98
- );
99
- const table = metadata
100
- ? Object.entries(schema)
101
- .filter(([, value]) => value == metadata.references)
102
- .find(Boolean)
103
- : undefined;
104
- if (!metadata?.references) {
105
- return;
106
- }
107
- ok(table);
108
- const to = Object.entries(table[1].entries).find(
109
- ([, value]) => value == metadata.key,
110
- );
111
- ok(to);
112
- return {
113
- from: name,
114
- on_delete: /undefined/.test(entry.expects) ? "SET NULL" : "CASCADE",
115
- on_update: /undefined/.test(entry.expects) ? "SET NULL" : "CASCADE",
116
- table: table[0],
117
- to: to[0],
118
- };
119
- })
120
- .filter(Boolean)
121
- .map((x) => {
122
- ok(x);
123
- return x;
124
- }),
125
- table: { name },
126
- }));
127
-
128
- const migration = [];
129
-
130
- for (const wantTable of wantTables) {
131
- const haveTable = haveTables.find(
132
- ({ table }) => table.name == wantTable.table.name,
133
- );
134
- if (haveTable) {
135
- for (const { info, name } of haveTable.indices) {
136
- if (!wantTable.foreignKeys.find(({ from }) => from == info.name)) {
137
- migration.push(`drop index ${name}`);
138
- }
139
- }
140
- const haveColumns = haveTable.columns;
141
- const columnsNowMissing = haveColumns.filter(
142
- (haveColumn) =>
143
- !wantTable.columns.find(({ name }) => name == haveColumn.name),
144
- );
145
- for (const { name, pk } of columnsNowMissing) {
146
- if (pk) {
147
- fail(`${haveTable.table.name}.${name}: Can't drop primary key`);
148
- }
149
- }
150
- for (const wantColumn of wantTable.columns) {
151
- const haveColumn = haveColumns.find(
152
- ({ name }) => name == wantColumn.name,
153
- );
154
-
155
- /** @param {Column} column */
156
- function checkConstraint(column) {
157
- ok(column);
158
- ok(haveTable);
159
- if (column.pk != wantColumn.pk) {
160
- fail(
161
- `${haveTable.table.name}.${column.name}: Can't toggle primary key`,
162
- );
163
- }
164
- if (
165
- !wantColumn.pk && // Without this condition I get an error with real database created by drizzle-kit.
166
- column.notnull != wantColumn.notnull
167
- ) {
168
- fail(
169
- `${haveTable.table.name}.${column.name}: Can't toggle optional`,
170
- );
171
- }
172
- }
173
-
174
- if (haveColumn) {
175
- checkConstraint(haveColumn);
176
- } else {
177
- const renamedColumn = columnsNowMissing.find(
178
- ({ notnull, type }) =>
179
- wantColumn.type == type && wantColumn.notnull == notnull,
180
- );
181
- if (renamedColumn) {
182
- checkConstraint(renamedColumn);
183
- columnsNowMissing.splice(
184
- columnsNowMissing.indexOf(renamedColumn),
185
- 1,
186
- );
187
- migration.push(
188
- `alter table ${wantTable.table.name} rename column ${renamedColumn.name} to ${wantColumn.name}`,
189
- );
190
- } else {
191
- if (wantColumn.pk) {
192
- fail(
193
- `${wantTable.table.name}.${wantColumn.name}: Can't add primary key`,
194
- );
195
- }
196
- migration.push(
197
- [
198
- `alter table ${wantTable.table.name} add column`,
199
- wantColumn.name,
200
- wantColumn.type,
201
- wantColumn.notnull ? "not null" : "",
202
- ...wantTable.foreignKeys
203
- .map(({ from, on_delete, on_update, table, to }) =>
204
- from == wantColumn.name
205
- ? `references ${table}(${to}) on delete ${on_delete} on update ${on_update}`
206
- : "",
207
- )
208
- .filter(Boolean),
209
- ]
210
- .filter(Boolean)
211
- .join(" "),
212
- );
213
- }
214
- }
215
-
216
- for (const { from } of wantTable.foreignKeys) {
217
- if (
218
- from == wantColumn.name &&
219
- !haveTable.indices.find(({ info }) => info.name == from)
220
- ) {
221
- migration.push(
222
- `create index ${wantTable.table.name}_${from} on ${wantTable.table.name}(${from})`,
223
- );
224
- }
225
- }
226
- }
227
- for (const missingColumn of columnsNowMissing) {
228
- migration.push(
229
- `alter table ${wantTable.table.name} drop column ${missingColumn.name}`,
230
- );
231
- }
232
- } else {
233
- if (!wantTable.columns.some(({ pk }) => pk)) {
234
- fail(`${wantTable.table.name} has no primary key`);
235
- }
236
- migration.push(
237
- `create table ${wantTable.table.name} (\n` +
238
- wantTable.columns
239
- .map(({ name, notnull, pk, type }) =>
240
- [
241
- name,
242
- type,
243
- pk ? "primary key" : "",
244
- !pk && notnull ? "not null" : "",
245
- ...wantTable.foreignKeys
246
- .map(({ from, on_delete, on_update, table, to }) =>
247
- from == name
248
- ? `references ${table}(${to}) on delete ${on_delete} on update ${on_update}`
249
- : "",
250
- )
251
- .filter(Boolean),
252
- ]
253
- .filter(Boolean)
254
- .join(" "),
255
- )
256
- .join(",\n") +
257
- `)`,
258
- );
259
- for (const { from } of wantTable.foreignKeys) {
260
- migration.push(
261
- `create index ${wantTable.table.name}_${from} on ${wantTable.table.name}(${from})`,
262
- );
263
- }
264
- }
265
- }
266
-
267
- for (const haveTable of haveTables) {
268
- const wantTable = wantTables.find(
269
- ({ table }) => table.name == haveTable.table.name,
270
- );
271
- if (!wantTable) {
272
- migration.push(`drop table ${haveTable.table.name}`);
273
- }
274
- }
275
-
276
- for (const sql of migration) {
277
- database.exec(sql);
278
- }
279
-
280
- /** @type {(({ table }: { table: string }) => void)[]} */
281
- const listeners = [];
282
- if (process.env["i"]) {
283
- process.on("message", (maybeMessage) => {
284
- for (const onMessage of listeners) {
285
- onMessage(/** @type {any} */ (maybeMessage));
286
- }
287
- });
288
- }
289
- /** @param {{ table: string }} message */
290
- function publish(message) {
291
- Object.assign(message, { i: process.env["i"] });
292
- process.send?.(message, undefined, { keepOpen: true });
293
- for (const onMessage of listeners) {
294
- onMessage(message);
295
- }
296
- }
297
-
298
- return {
299
- .../** @type {any} */ (
300
- Object.fromEntries(
301
- Object.entries(schema).map(([maybeTable]) => {
302
- const table = /** @type {keyof typeof schema & string} */ (
303
- maybeTable
304
- );
305
- ok(schema[table]);
306
- const columns = Object.entries(schema[table].entries);
307
- const primaryKey = columns.find(
308
- ([, column]) =>
309
- /** @type {{ primary?: true }} */ (getMetadata(column)).primary,
310
- );
311
- ok(primaryKey);
312
- const upsert = database.prepare(
313
- `insert into ${table} (${columns.map(([name]) => name).join(", ")}) values\n` +
314
- `(${columns.map(() => "?").join(", ")})\n` +
315
- `on conflict (${primaryKey[0]}) do update set\n` +
316
- columns.map(([name]) => `${name} = ?`).join(",\n"),
317
- );
318
-
319
- return [
320
- table,
321
- {
322
- /** @param {import("./db.d.ts").Where<typeof schema, typeof table>} where */
323
- delete(where) {
324
- if (
325
- database
326
- .prepare(`delete from ${table}` + whereQuery({ where }))
327
- .run(...whereValues(where)).changes
328
- ) {
329
- publish({ table });
330
- }
331
- },
332
-
333
- /** @param {any} join */
334
- select(join) {
335
- return select({ database, flat, join, schema, table });
336
- },
337
-
338
- /** @param {{ set: Partial<import("valibot").InferOutput<(typeof schema)[typeof table]>>, where: import("./db.d.ts").Where<typeof schema, typeof table> }} props */
339
- update({ set, where }) {
340
- if (
341
- database
342
- .prepare(
343
- `update ${table} set\n` +
344
- Object.keys(set)
345
- .map((name) => `${name} = ?`)
346
- .join(",\n") +
347
- whereQuery({ where }),
348
- )
349
- .run(
350
- ...[
351
- ...Object.values(set).map((value) => value ?? null),
352
- ...whereValues(where),
353
- ],
354
- ).changes
355
- ) {
356
- publish({ table });
357
- }
358
- },
359
-
360
- /** @param {any[]} values */
361
- upsert(...values) {
362
- ok(schema[table]);
363
- const outputs = [];
364
- /** @type {boolean} */
365
- let changes = false;
366
- for (const row of values) {
367
- const output = parse(schema[table], row);
368
- const values = columns.map(([name]) => output[name] ?? null);
369
- if (upsert.run(...values, ...values).changes) {
370
- changes = true;
371
- }
372
- outputs.push(output);
373
- }
374
- if (changes) {
375
- publish({ table });
376
- }
377
- return outputs;
378
- },
379
- },
380
- ];
381
- }),
382
- )
383
- ),
384
- schema,
385
- subscribe(onMessage) {
386
- listeners.push(onMessage);
387
- return () => {
388
- listeners.splice(listeners.indexOf(onMessage), 1);
389
- };
390
- },
391
- };
392
- }
393
-
394
- /** @type {<Schema extends Record<string, import("valibot").ObjectSchema<any, any>>, R extends import("./db.d.ts").Relations<Schema> = import("./db.d.ts").Relations<Schema>, F extends import("./db.d.ts").Flatten<Schema> = import("./db.d.ts").Flatten<Schema>, Table extends keyof F & keyof Schema = keyof F & keyof Schema >(props: { database: DatabaseSync; flat: F; join?: import("./db.d.ts").Join<R, Table> & { where?: import("./db.d.ts").Where<Schema, Table> }; schema: Schema; table: Table; }) => any[]} */
395
- function select(props) {
396
- ok(typeof props.table == "string");
397
- let alias = `${props.table}_0`;
398
- let sql = `from ${props.table} ${alias}`;
399
-
400
- let columns = Object.keys(
401
- /** @type {any} */ (props.schema)[props.table].entries,
402
- );
403
- if (/** @type {any} */ (props.join)?.with) {
404
- columns = columns.map((name) => `${alias}.${name} ${alias}_${name}`);
405
- }
406
-
407
- /** @type {Record<string, { entities: any[]; table: string }>} */
408
- const aliases = {};
409
- /**
410
- * @param {number} unique
411
- * @param {string} table
412
- * @param {string} alias
413
- * @param {{ with: any }} ownJoin // eslint-disable-line @typescript-eslint/no-explicit-any
414
- */
415
- function join(unique, table, alias, ownJoin) {
416
- if (!aliases[alias]) {
417
- aliases[alias] = { entities: [], table };
418
- }
419
- for (const [name, itsJoin] of ownJoin?.with
420
- ? Object.entries(ownJoin.with)
421
- : []) {
422
- const otherFlat = props.flat[table];
423
- ok(otherFlat);
424
- const relation = otherFlat[/** @type {keyof typeof otherFlat} */ (name)];
425
- ok(relation);
426
-
427
- const { Its, Other, Own } =
428
- /** @type {{ Its: string, Other: string, Own: string }} */ (
429
- /** @type {any} */ (relation)
430
- );
431
- const otherAlias = `${name}_${unique + 1}`;
432
- if (!aliases[otherAlias]) {
433
- aliases[otherAlias] = { entities: [], table: Other };
434
- }
435
- columns = [
436
- ...columns,
437
-
438
- ...Object.keys(/** @type {any} */ (props.schema)[Other].entries).map(
439
- (name) => `${otherAlias}.${name} ${otherAlias}_${name}`,
440
- ),
441
- ];
442
- sql += `\n left join ${Other} ${otherAlias} on ${otherAlias}.${Its} = ${alias}.${Own}`;
443
- if (typeof itsJoin == "object") {
444
- ok(itsJoin);
445
- join(unique + 1, Other, otherAlias, itsJoin);
446
- }
447
- }
448
- }
449
-
450
- join(0, props.table, alias, /** @type {any} */ (props.join));
451
- sql =
452
- `select ${columns.join(",\n")} ${sql}` +
453
- whereQuery({ alias, where: props.join?.where });
454
-
455
- const rows = props.database
456
- .prepare(sql)
457
- .all(...whereValues(props.join?.where));
458
- if (!(/** @type {any} */ (props.join)?.with)) {
459
- for (const row of rows) {
460
- for (const name of Object.keys(row)) {
461
- Object.assign(row, { [name]: row[name] ?? undefined });
462
- }
463
- }
464
- return rows;
465
- }
466
- for (const [alias, state] of Object.entries(aliases)) {
467
- /** @type {Record<string, (typeof state.entities)[0]>} */
468
- const unique = {};
469
- for (const row of rows) {
470
- /** @type {Record<string, any>} */
471
- const unwrapped = {};
472
-
473
- let empty = true;
474
- for (const name of Object.keys(
475
- /** @type {any} */ (props.schema)[state.table].entries,
476
- )) {
477
- unwrapped[name] = row[`${alias}_${name}`] ?? undefined;
478
- if (unwrapped[name] !== undefined) {
479
- empty = false;
480
- }
481
- }
482
- if (!empty) {
483
- unique[JSON.stringify(unwrapped)] = unwrapped;
484
- }
485
- }
486
- state.entities = Object.values(unique);
487
- }
488
-
489
- alias = `${props.table}_0`;
490
- /**
491
- * @param {number} unique
492
- * @param {string} table
493
- * @param {string} alias
494
- * @param {{ with: any }} ownJoin // eslint-disable-line @typescript-eslint/no-explicit-any
495
- */
496
- function merge(unique, table, alias, ownJoin) {
497
- ok(aliases[alias]);
498
- for (const [name, itsJoin] of ownJoin?.with
499
- ? Object.entries(ownJoin.with)
500
- : []) {
501
- const otherFlat = props.flat[table];
502
- ok(otherFlat);
503
- const relation = otherFlat[/** @type {keyof typeof otherFlat} */ (name)];
504
- ok(relation);
505
-
506
- const { Its, Other, Own, type } =
507
- /** @type {{ Its: string, Other: string, Own: string, type: "referencedBy" | "references" }} */ (
508
- /** @type {any} */ (relation)
509
- );
510
- const otherAlias = `${name}_${unique + 1}`;
511
- ok(aliases[otherAlias]?.entities);
512
- for (const self of aliases[alias].entities) {
513
- const matchingOthers = aliases[otherAlias].entities.filter(
514
- (other) => other[Its] == self[Own],
515
- );
516
- self[name] = type == "references" ? matchingOthers[0] : matchingOthers;
517
- }
518
- if (typeof itsJoin == "object") {
519
- ok(itsJoin);
520
- merge(unique + 1, Other, otherAlias, itsJoin);
521
- }
522
- }
523
- }
524
-
525
- merge(0, props.table, alias, /** @type {any} */ (props.join));
526
- const state = aliases[alias];
527
- ok(state);
528
- return state.entities;
529
- }
530
-
531
- /** @param {{ alias?: string, where: import("./db.js").Where<any, string> | undefined }} props */
532
- function whereQuery({ alias, where }) {
533
- return (
534
- "\nwhere true\n" +
535
- Object.entries(where || {})
536
- .map(([name, values]) => {
537
- if (alias) {
538
- name = `${alias}.${name}`;
539
- }
540
- return Array.isArray(values)
541
- ? `and ${name} in (${values.map(() => "?").join(", ")})`
542
- : typeof values == "number" || typeof values == "string"
543
- ? `and ${name} = ?`
544
- : Object.keys(values)
545
- .map(
546
- (operator) =>
547
- `and ${name} ` +
548
- (operator == "gt"
549
- ? ">"
550
- : operator == "gte"
551
- ? ">="
552
- : operator == "lt"
553
- ? "<"
554
- : operator == "lte"
555
- ? "<="
556
- : "!=") +
557
- " ?",
558
- )
559
- .join("");
560
- })
561
- .join("\n")
562
- );
563
- }
564
-
565
- /** @param {import("./db.js").Where<any, string> | undefined} where */
566
- function whereValues(where) {
567
- return Object.values(where || {})
568
- .flatMap((values) =>
569
- Array.isArray(values)
570
- ? values
571
- : typeof values == "number" || typeof values == "string"
572
- ? values
573
- : Object.values(values),
574
- )
575
- .map((value) => value ?? null);
576
- }
1
+ import envPaths from "env-paths";
2
+ import { fail, ok } from "node:assert/strict";
3
+ import { mkdirSync } from "node:fs";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import { getMetadata, parse } from "valibot";
6
+
7
+ import { flatten, relations } from "./db.js";
8
+
9
+ /**
10
+ * @typedef Column
11
+ * @property {string} name
12
+ * @property {0 | 1} notnull
13
+ * @property {0 | 1} pk
14
+ * @property {"INTEGER" | "TEXT"} type
15
+ */
16
+
17
+ /** @type {typeof import("./sqlite.d.ts").sqlite} */
18
+ export function sqlite({ pkg, schema }) {
19
+ const data = pkg
20
+ ? (() => {
21
+ const { data } = envPaths(pkg.name);
22
+ mkdirSync(data, { recursive: true });
23
+ return data;
24
+ })()
25
+ : undefined;
26
+ const database = new DatabaseSync(data ? `${data}/db.sqlite3` : ":memory:", {
27
+ timeout: 1000,
28
+ });
29
+ database.prepare("pragma journal_mode=wal").run();
30
+ const flat = flatten(relations(schema));
31
+
32
+ const haveTables = /** @type {{ name: string }[]} */ (
33
+ database
34
+ .prepare(
35
+ "select * from sqlite_schema where type='table' and name not like 'sqlite_%'",
36
+ )
37
+ .all()
38
+ ).map((table) => {
39
+ const columns = /** @type {Column[]} */ (
40
+ database.prepare(`pragma table_info(${table.name})`).all()
41
+ );
42
+ const foreignKeys =
43
+ /** @type {{ from: string, on_delete: "CASCADE" | "SET NULL", on_update: "CASCADE" | "SET NULL", table: string, to: string, }[]} */ (
44
+ database.prepare(`pragma foreign_key_list(${table.name})`).all()
45
+ );
46
+ return {
47
+ columns,
48
+ foreignKeys,
49
+ indices: /** @type {{ name: string }[]} */ (
50
+ database.prepare(`pragma index_list(${table.name})`).all()
51
+ )
52
+ .filter(({ name }) => !/^sqlite_/.test(name))
53
+ .map((index) => ({
54
+ ...index,
55
+ info: /** @type {{ name: string; }} */ (
56
+ database.prepare(`pragma index_info(${index.name})`).get()
57
+ ),
58
+ })),
59
+ table,
60
+ };
61
+ });
62
+
63
+ const wantTables = Object.entries(schema).map(([name, { entries }]) => ({
64
+ columns: Object.entries(entries).map(([name, maybeEntry]) => {
65
+ const entry =
66
+ /** @type {import("valibot").BaseSchema<undefined, undefined, any>} */ (
67
+ maybeEntry
68
+ );
69
+ const pk = /** @type {{ primary?: true }} */ (getMetadata(entry)).primary
70
+ ? 1
71
+ : 0;
72
+ return {
73
+ name,
74
+ notnull: !pk && entry.expects.includes("undefined") ? 0 : 1,
75
+ pk,
76
+ type:
77
+ entry.expects == "string" || entry.expects == "(string | undefined)"
78
+ ? "TEXT"
79
+ : entry.expects == "number" ||
80
+ entry.expects == "(number | undefined)"
81
+ ? "INTEGER"
82
+ : entry.expects == "Uint8Array" ||
83
+ entry.expects == "(Uint8Array | undefined)"
84
+ ? "BLOB"
85
+ : fail(name),
86
+ };
87
+ }),
88
+ foreignKeys: Object.entries(entries)
89
+ .map(([name, maybeEntry]) => {
90
+ const entry =
91
+ /** @type {import("valibot").BaseSchema<undefined, undefined, any>} */ (
92
+ maybeEntry
93
+ );
94
+
95
+ const metadata =
96
+ /** @type {{ key?: string, references?: any } | undefined} */ (
97
+ getMetadata(entry)
98
+ );
99
+ const table = metadata
100
+ ? Object.entries(schema)
101
+ .filter(([, value]) => value == metadata.references)
102
+ .find(Boolean)
103
+ : undefined;
104
+ if (!metadata?.references) {
105
+ return;
106
+ }
107
+ ok(table);
108
+ const to = Object.entries(table[1].entries).find(
109
+ ([, value]) => value == metadata.key,
110
+ );
111
+ ok(to);
112
+ return {
113
+ from: name,
114
+ on_delete: /undefined/.test(entry.expects) ? "SET NULL" : "CASCADE",
115
+ on_update: /undefined/.test(entry.expects) ? "SET NULL" : "CASCADE",
116
+ table: table[0],
117
+ to: to[0],
118
+ };
119
+ })
120
+ .filter(Boolean)
121
+ .map((x) => {
122
+ ok(x);
123
+ return x;
124
+ }),
125
+ table: { name },
126
+ }));
127
+
128
+ const migration = [];
129
+
130
+ for (const wantTable of wantTables) {
131
+ const haveTable = haveTables.find(
132
+ ({ table }) => table.name == wantTable.table.name,
133
+ );
134
+ if (haveTable) {
135
+ for (const { info, name } of haveTable.indices) {
136
+ if (!wantTable.foreignKeys.find(({ from }) => from == info.name)) {
137
+ migration.push(`drop index ${name}`);
138
+ }
139
+ }
140
+ const haveColumns = haveTable.columns;
141
+ const columnsNowMissing = haveColumns.filter(
142
+ (haveColumn) =>
143
+ !wantTable.columns.find(({ name }) => name == haveColumn.name),
144
+ );
145
+ for (const { name, pk } of columnsNowMissing) {
146
+ if (pk) {
147
+ fail(`${haveTable.table.name}.${name}: Can't drop primary key`);
148
+ }
149
+ }
150
+ for (const wantColumn of wantTable.columns) {
151
+ const haveColumn = haveColumns.find(
152
+ ({ name }) => name == wantColumn.name,
153
+ );
154
+
155
+ /** @param {Column} column */
156
+ function checkConstraint(column) {
157
+ ok(column);
158
+ ok(haveTable);
159
+ if (column.pk != wantColumn.pk) {
160
+ fail(
161
+ `${haveTable.table.name}.${column.name}: Can't toggle primary key`,
162
+ );
163
+ }
164
+ if (
165
+ !wantColumn.pk && // Without this condition I get an error with real database created by drizzle-kit.
166
+ column.notnull != wantColumn.notnull
167
+ ) {
168
+ fail(
169
+ `${haveTable.table.name}.${column.name}: Can't toggle optional`,
170
+ );
171
+ }
172
+ }
173
+
174
+ if (haveColumn) {
175
+ checkConstraint(haveColumn);
176
+ } else {
177
+ const renamedColumn = columnsNowMissing.find(
178
+ ({ notnull, type }) =>
179
+ wantColumn.type == type && wantColumn.notnull == notnull,
180
+ );
181
+ if (renamedColumn) {
182
+ checkConstraint(renamedColumn);
183
+ columnsNowMissing.splice(
184
+ columnsNowMissing.indexOf(renamedColumn),
185
+ 1,
186
+ );
187
+ migration.push(
188
+ `alter table ${wantTable.table.name} rename column ${renamedColumn.name} to ${wantColumn.name}`,
189
+ );
190
+ } else {
191
+ if (wantColumn.pk) {
192
+ fail(
193
+ `${wantTable.table.name}.${wantColumn.name}: Can't add primary key`,
194
+ );
195
+ }
196
+ migration.push(
197
+ [
198
+ `alter table ${wantTable.table.name} add column`,
199
+ wantColumn.name,
200
+ wantColumn.type,
201
+ wantColumn.notnull ? "not null" : "",
202
+ ...wantTable.foreignKeys
203
+ .map(({ from, on_delete, on_update, table, to }) =>
204
+ from == wantColumn.name
205
+ ? `references ${table}(${to}) on delete ${on_delete} on update ${on_update}`
206
+ : "",
207
+ )
208
+ .filter(Boolean),
209
+ ]
210
+ .filter(Boolean)
211
+ .join(" "),
212
+ );
213
+ }
214
+ }
215
+
216
+ for (const { from } of wantTable.foreignKeys) {
217
+ if (
218
+ from == wantColumn.name &&
219
+ !haveTable.indices.find(({ info }) => info.name == from)
220
+ ) {
221
+ migration.push(
222
+ `create index ${wantTable.table.name}_${from} on ${wantTable.table.name}(${from})`,
223
+ );
224
+ }
225
+ }
226
+ }
227
+ for (const missingColumn of columnsNowMissing) {
228
+ migration.push(
229
+ `alter table ${wantTable.table.name} drop column ${missingColumn.name}`,
230
+ );
231
+ }
232
+ } else {
233
+ if (!wantTable.columns.some(({ pk }) => pk)) {
234
+ fail(`${wantTable.table.name} has no primary key`);
235
+ }
236
+ migration.push(
237
+ `create table ${wantTable.table.name} (\n` +
238
+ wantTable.columns
239
+ .map(({ name, notnull, pk, type }) =>
240
+ [
241
+ name,
242
+ type,
243
+ pk ? "primary key" : "",
244
+ !pk && notnull ? "not null" : "",
245
+ ...wantTable.foreignKeys
246
+ .map(({ from, on_delete, on_update, table, to }) =>
247
+ from == name
248
+ ? `references ${table}(${to}) on delete ${on_delete} on update ${on_update}`
249
+ : "",
250
+ )
251
+ .filter(Boolean),
252
+ ]
253
+ .filter(Boolean)
254
+ .join(" "),
255
+ )
256
+ .join(",\n") +
257
+ `)`,
258
+ );
259
+ for (const { from } of wantTable.foreignKeys) {
260
+ migration.push(
261
+ `create index ${wantTable.table.name}_${from} on ${wantTable.table.name}(${from})`,
262
+ );
263
+ }
264
+ }
265
+ }
266
+
267
+ for (const haveTable of haveTables) {
268
+ const wantTable = wantTables.find(
269
+ ({ table }) => table.name == haveTable.table.name,
270
+ );
271
+ if (!wantTable) {
272
+ migration.push(`drop table ${haveTable.table.name}`);
273
+ }
274
+ }
275
+
276
+ for (const sql of migration) {
277
+ database.exec(sql);
278
+ }
279
+
280
+ /** @type {(({ table }: { table: string }) => void)[]} */
281
+ const listeners = [];
282
+ if (process.env["i"]) {
283
+ process.on("message", (maybeMessage) => {
284
+ for (const onMessage of listeners) {
285
+ onMessage(/** @type {any} */ (maybeMessage));
286
+ }
287
+ });
288
+ }
289
+ /** @param {{ table: string }} message */
290
+ function publish(message) {
291
+ Object.assign(message, { i: process.env["i"] });
292
+ process.send?.(message, undefined, { keepOpen: true });
293
+ for (const onMessage of listeners) {
294
+ onMessage(message);
295
+ }
296
+ }
297
+
298
+ return {
299
+ .../** @type {any} */ (
300
+ Object.fromEntries(
301
+ Object.entries(schema).map(([maybeTable]) => {
302
+ const table = /** @type {keyof typeof schema & string} */ (
303
+ maybeTable
304
+ );
305
+ ok(schema[table]);
306
+ const columns = Object.entries(schema[table].entries);
307
+ const primaryKey = columns.find(
308
+ ([, column]) =>
309
+ /** @type {{ primary?: true }} */ (getMetadata(column)).primary,
310
+ );
311
+ ok(primaryKey);
312
+ const upsert = database.prepare(
313
+ `insert into ${table} (${columns.map(([name]) => name).join(", ")}) values\n` +
314
+ `(${columns.map(() => "?").join(", ")})\n` +
315
+ `on conflict (${primaryKey[0]}) do update set\n` +
316
+ columns.map(([name]) => `${name} = ?`).join(",\n"),
317
+ );
318
+
319
+ return [
320
+ table,
321
+ {
322
+ /** @param {import("./db.d.ts").Where<typeof schema, typeof table>} where */
323
+ delete(where) {
324
+ if (
325
+ database
326
+ .prepare(`delete from ${table}` + whereQuery({ where }))
327
+ .run(...whereValues(where)).changes
328
+ ) {
329
+ publish({ table });
330
+ }
331
+ },
332
+
333
+ /** @param {any} join */
334
+ select(join) {
335
+ return select({ database, flat, join, schema, table });
336
+ },
337
+
338
+ /** @param {{ set: Partial<import("valibot").InferOutput<(typeof schema)[typeof table]>>, where: import("./db.d.ts").Where<typeof schema, typeof table> }} props */
339
+ update({ set, where }) {
340
+ if (
341
+ database
342
+ .prepare(
343
+ `update ${table} set\n` +
344
+ Object.keys(set)
345
+ .map((name) => `${name} = ?`)
346
+ .join(",\n") +
347
+ whereQuery({ where }),
348
+ )
349
+ .run(
350
+ ...[
351
+ ...Object.values(set).map((value) => value ?? null),
352
+ ...whereValues(where),
353
+ ],
354
+ ).changes
355
+ ) {
356
+ publish({ table });
357
+ }
358
+ },
359
+
360
+ /** @param {any[]} values */
361
+ upsert(...values) {
362
+ ok(schema[table]);
363
+ const outputs = [];
364
+ /** @type {boolean} */
365
+ let changes = false;
366
+ for (const row of values) {
367
+ const output = parse(schema[table], row);
368
+ const values = columns.map(([name]) => output[name] ?? null);
369
+ if (upsert.run(...values, ...values).changes) {
370
+ changes = true;
371
+ }
372
+ outputs.push(output);
373
+ }
374
+ if (changes) {
375
+ publish({ table });
376
+ }
377
+ return outputs;
378
+ },
379
+ },
380
+ ];
381
+ }),
382
+ )
383
+ ),
384
+ schema,
385
+ subscribe(onMessage) {
386
+ listeners.push(onMessage);
387
+ return () => {
388
+ listeners.splice(listeners.indexOf(onMessage), 1);
389
+ };
390
+ },
391
+ };
392
+ }
393
+
394
+ /** @type {<Schema extends Record<string, import("valibot").ObjectSchema<any, any>>, R extends import("./db.d.ts").Relations<Schema> = import("./db.d.ts").Relations<Schema>, F extends import("./db.d.ts").Flatten<Schema> = import("./db.d.ts").Flatten<Schema>, Table extends keyof F & keyof Schema = keyof F & keyof Schema >(props: { database: DatabaseSync; flat: F; join?: import("./db.d.ts").Join<R, Table> & { where?: import("./db.d.ts").Where<Schema, Table> }; schema: Schema; table: Table; }) => any[]} */
395
+ function select(props) {
396
+ ok(typeof props.table == "string");
397
+ let alias = `${props.table}_0`;
398
+ let sql = `from ${props.table} ${alias}`;
399
+
400
+ let columns = Object.keys(
401
+ /** @type {any} */ (props.schema)[props.table].entries,
402
+ );
403
+ if (/** @type {any} */ (props.join)?.with) {
404
+ columns = columns.map((name) => `${alias}.${name} ${alias}_${name}`);
405
+ }
406
+
407
+ /** @type {Record<string, { entities: any[]; table: string }>} */
408
+ const aliases = {};
409
+ /**
410
+ * @param {number} unique
411
+ * @param {string} table
412
+ * @param {string} alias
413
+ * @param {{ with: any }} ownJoin // eslint-disable-line @typescript-eslint/no-explicit-any
414
+ */
415
+ function join(unique, table, alias, ownJoin) {
416
+ if (!aliases[alias]) {
417
+ aliases[alias] = { entities: [], table };
418
+ }
419
+ for (const [name, itsJoin] of ownJoin?.with
420
+ ? Object.entries(ownJoin.with)
421
+ : []) {
422
+ const otherFlat = props.flat[table];
423
+ ok(otherFlat);
424
+ const relation = otherFlat[/** @type {keyof typeof otherFlat} */ (name)];
425
+ ok(relation);
426
+
427
+ const { Its, Other, Own } =
428
+ /** @type {{ Its: string, Other: string, Own: string }} */ (
429
+ /** @type {any} */ (relation)
430
+ );
431
+ const otherAlias = `${name}_${unique + 1}`;
432
+ if (!aliases[otherAlias]) {
433
+ aliases[otherAlias] = { entities: [], table: Other };
434
+ }
435
+ columns = [
436
+ ...columns,
437
+
438
+ ...Object.keys(/** @type {any} */ (props.schema)[Other].entries).map(
439
+ (name) => `${otherAlias}.${name} ${otherAlias}_${name}`,
440
+ ),
441
+ ];
442
+ sql += `\n left join ${Other} ${otherAlias} on ${otherAlias}.${Its} = ${alias}.${Own}`;
443
+ if (typeof itsJoin == "object") {
444
+ ok(itsJoin);
445
+ join(unique + 1, Other, otherAlias, itsJoin);
446
+ }
447
+ }
448
+ }
449
+
450
+ join(0, props.table, alias, /** @type {any} */ (props.join));
451
+ sql =
452
+ `select ${columns.join(",\n")} ${sql}` +
453
+ whereQuery({ alias, where: props.join?.where });
454
+
455
+ const rows = props.database
456
+ .prepare(sql)
457
+ .all(...whereValues(props.join?.where));
458
+ if (!(/** @type {any} */ (props.join)?.with)) {
459
+ for (const row of rows) {
460
+ for (const name of Object.keys(row)) {
461
+ Object.assign(row, { [name]: row[name] ?? undefined });
462
+ }
463
+ }
464
+ return rows;
465
+ }
466
+ for (const [alias, state] of Object.entries(aliases)) {
467
+ /** @type {Record<string, (typeof state.entities)[0]>} */
468
+ const unique = {};
469
+ for (const row of rows) {
470
+ /** @type {Record<string, any>} */
471
+ const unwrapped = {};
472
+
473
+ let empty = true;
474
+ for (const name of Object.keys(
475
+ /** @type {any} */ (props.schema)[state.table].entries,
476
+ )) {
477
+ unwrapped[name] = row[`${alias}_${name}`] ?? undefined;
478
+ if (unwrapped[name] !== undefined) {
479
+ empty = false;
480
+ }
481
+ }
482
+ if (!empty) {
483
+ unique[JSON.stringify(unwrapped)] = unwrapped;
484
+ }
485
+ }
486
+ state.entities = Object.values(unique);
487
+ }
488
+
489
+ alias = `${props.table}_0`;
490
+ /**
491
+ * @param {number} unique
492
+ * @param {string} table
493
+ * @param {string} alias
494
+ * @param {{ with: any }} ownJoin // eslint-disable-line @typescript-eslint/no-explicit-any
495
+ */
496
+ function merge(unique, table, alias, ownJoin) {
497
+ ok(aliases[alias]);
498
+ for (const [name, itsJoin] of ownJoin?.with
499
+ ? Object.entries(ownJoin.with)
500
+ : []) {
501
+ const otherFlat = props.flat[table];
502
+ ok(otherFlat);
503
+ const relation = otherFlat[/** @type {keyof typeof otherFlat} */ (name)];
504
+ ok(relation);
505
+
506
+ const { Its, Other, Own, type } =
507
+ /** @type {{ Its: string, Other: string, Own: string, type: "referencedBy" | "references" }} */ (
508
+ /** @type {any} */ (relation)
509
+ );
510
+ const otherAlias = `${name}_${unique + 1}`;
511
+ ok(aliases[otherAlias]?.entities);
512
+ for (const self of aliases[alias].entities) {
513
+ const matchingOthers = aliases[otherAlias].entities.filter(
514
+ (other) => other[Its] == self[Own],
515
+ );
516
+ self[name] = type == "references" ? matchingOthers[0] : matchingOthers;
517
+ }
518
+ if (typeof itsJoin == "object") {
519
+ ok(itsJoin);
520
+ merge(unique + 1, Other, otherAlias, itsJoin);
521
+ }
522
+ }
523
+ }
524
+
525
+ merge(0, props.table, alias, /** @type {any} */ (props.join));
526
+ const state = aliases[alias];
527
+ ok(state);
528
+ return state.entities;
529
+ }
530
+
531
+ /** @param {{ alias?: string, where: import("./db.js").Where<any, string> | undefined }} props */
532
+ function whereQuery({ alias, where }) {
533
+ return (
534
+ "\nwhere true\n" +
535
+ Object.entries(where || {})
536
+ .map(([name, values]) => {
537
+ if (alias) {
538
+ name = `${alias}.${name}`;
539
+ }
540
+ return Array.isArray(values)
541
+ ? `and ${name} in (${values.map(() => "?").join(", ")})`
542
+ : typeof values == "number" || typeof values == "string"
543
+ ? `and ${name} = ?`
544
+ : Object.keys(values)
545
+ .map(
546
+ (operator) =>
547
+ `and ${name} ` +
548
+ (operator == "gt"
549
+ ? ">"
550
+ : operator == "gte"
551
+ ? ">="
552
+ : operator == "lt"
553
+ ? "<"
554
+ : operator == "lte"
555
+ ? "<="
556
+ : "!=") +
557
+ " ?",
558
+ )
559
+ .join("");
560
+ })
561
+ .join("\n")
562
+ );
563
+ }
564
+
565
+ /** @param {import("./db.js").Where<any, string> | undefined} where */
566
+ function whereValues(where) {
567
+ return Object.values(where || {})
568
+ .flatMap((values) =>
569
+ Array.isArray(values)
570
+ ? values
571
+ : typeof values == "number" || typeof values == "string"
572
+ ? values
573
+ : Object.values(values),
574
+ )
575
+ .map((value) => value ?? null);
576
+ }