@sigitex/outlaw 1.0.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 (58) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +9 -0
  3. package/package.json +52 -0
  4. package/src/api/DatabaseTable.ts +51 -0
  5. package/src/api/DatabaseView.ts +22 -0
  6. package/src/api/api.types.ts +200 -0
  7. package/src/api/createDatabase.ts +24 -0
  8. package/src/api/index.ts +4 -0
  9. package/src/bun/bun.ts +27 -0
  10. package/src/bun/index.ts +1 -0
  11. package/src/cloudflare/cloudflare.ts +18 -0
  12. package/src/cloudflare/index.ts +1 -0
  13. package/src/console.ts +14 -0
  14. package/src/cowboyMigration/Compare.ts +97 -0
  15. package/src/cowboyMigration/CowboyConnection.ts +57 -0
  16. package/src/cowboyMigration/CowboyMigrator.ts +437 -0
  17. package/src/cowboyMigration/CowboySeeder.ts +245 -0
  18. package/src/cowboyMigration/cowboyMigration.types.ts +29 -0
  19. package/src/cowboyMigration/createSchemaHacker.ts +28 -0
  20. package/src/cowboyMigration/index.ts +3 -0
  21. package/src/crypto.d.ts +9 -0
  22. package/src/framework/Format.ts +31 -0
  23. package/src/framework/definitions.d.ts +4 -0
  24. package/src/framework/index.ts +1 -0
  25. package/src/index.ts +10 -0
  26. package/src/queryBuilder/DeleteBuilder.ts +40 -0
  27. package/src/queryBuilder/InsertBuilder.ts +31 -0
  28. package/src/queryBuilder/Mappings.ts +55 -0
  29. package/src/queryBuilder/SelectBuilder.ts +39 -0
  30. package/src/queryBuilder/SelectQueryBuilder.ts +93 -0
  31. package/src/queryBuilder/UpdateBuilder.ts +44 -0
  32. package/src/queryBuilder/addWhereClause.ts +35 -0
  33. package/src/queryBuilder/index.ts +7 -0
  34. package/src/queryBuilder/operators.ts +35 -0
  35. package/src/queryBuilder/queryBuilders.types.ts +105 -0
  36. package/src/queryGenerator/Clause.ts +79 -0
  37. package/src/queryGenerator/generateDelete.ts +18 -0
  38. package/src/queryGenerator/generateInsert.ts +29 -0
  39. package/src/queryGenerator/generateSelect.ts +89 -0
  40. package/src/queryGenerator/generateUpdate.ts +29 -0
  41. package/src/queryGenerator/index.ts +4 -0
  42. package/src/reflection/Reflector.ts +47 -0
  43. package/src/reflection/index.ts +2 -0
  44. package/src/reflection/reflection.types.ts +12 -0
  45. package/src/schemaBuilder/Mapping.ts +44 -0
  46. package/src/schemaBuilder/columnBuilders.ts +67 -0
  47. package/src/schemaBuilder/createFixture.ts +112 -0
  48. package/src/schemaBuilder/createIndex.ts +33 -0
  49. package/src/schemaBuilder/createSchema.ts +23 -0
  50. package/src/schemaBuilder/createTable.ts +80 -0
  51. package/src/schemaBuilder/createView.ts +21 -0
  52. package/src/schemaBuilder/index.ts +9 -0
  53. package/src/schemaBuilder/metadata.ts +73 -0
  54. package/src/schemaBuilder/schemaBuilder.types.ts +139 -0
  55. package/src/schemaGenerator/generateCreateIndex.ts +18 -0
  56. package/src/schemaGenerator/generateCreateTable.ts +68 -0
  57. package/src/schemaGenerator/generateCreateView.ts +13 -0
  58. package/src/schemaGenerator/index.ts +3 -0
@@ -0,0 +1,437 @@
1
+ import { newline, print } from "@sigitex/print"
2
+ import type { Connection } from "../api"
3
+ import { Format } from "../framework"
4
+ import { Reflector } from "../reflection"
5
+ import type {
6
+ BuildTable,
7
+ BuildView,
8
+ IndexData,
9
+ SchemaMembers,
10
+ Schema,
11
+ TableData,
12
+ ViewData,
13
+ } from "../schemaBuilder"
14
+ import { generateCreateTable } from "../schemaGenerator"
15
+ import { generateCreateIndex } from "../schemaGenerator/generateCreateIndex"
16
+ import { generateCreateView } from "../schemaGenerator/generateCreateView"
17
+ import { Compare } from "./Compare"
18
+ import type { SchemaHack } from "./cowboyMigration.types"
19
+
20
+ type Migratable = TableData | ViewData | IndexData
21
+ type MigratableType = "table" | "view" | "index"
22
+
23
+ const MIGRATION_TABLE = "cowboy_migration"
24
+
25
+ export class CowboyMigrator {
26
+ private readonly connection: Connection
27
+ private readonly schema: Schema<SchemaMembers>
28
+ private readonly hacks: SchemaHack[]
29
+ private readonly reflect: Reflector
30
+
31
+ constructor(
32
+ connection: Connection,
33
+ schema: Schema<SchemaMembers>,
34
+ hacks: SchemaHack[],
35
+ ) {
36
+ this.connection = connection
37
+ this.schema = schema
38
+ this.hacks = hacks
39
+ this.reflect = new Reflector(connection)
40
+ }
41
+
42
+ async migrate() {
43
+ console.log("🤠 checking for migrations")
44
+
45
+ const hasMigrationTable = await this.reflect.hasTable(MIGRATION_TABLE)
46
+ if (!hasMigrationTable) {
47
+ await this.connection.script([
48
+ "PRAGMA defer_foreign_keys = on",
49
+ createMigrationTable(),
50
+ ])
51
+ this.reflect.invalidate()
52
+ }
53
+
54
+ // phase 1: execute hacks
55
+ await this.executeHacks()
56
+
57
+ // phase 2: schema convergence
58
+ const statements: string[] = ["PRAGMA defer_foreign_keys = on"]
59
+ for (const { $meta: table } of Object.values<BuildTable<unknown>>(
60
+ this.schema.tables,
61
+ )) {
62
+ const isMissingTable = await this.reflect.isMissingTable(table.name)
63
+ if (isMissingTable) {
64
+ console.log(`🤠 creating table: ${table.name}`)
65
+ statements.push(generateCreateTable(table).trim())
66
+ statements.push(insertMigration("table", table))
67
+ } else {
68
+ const oldTable = await this.getTableData(table.name)
69
+ const equal = Compare.table(oldTable, table)
70
+ if (!equal) {
71
+ const migration = migrateTable(oldTable, table, this.hacks)
72
+ if (migration.length > 0) {
73
+ console.log(`🤠 migrating table: ${table.name}`)
74
+ statements.push(...migration)
75
+ statements.push(updateMigration("table", table))
76
+ }
77
+ }
78
+ }
79
+ }
80
+ if (statements.length > 1) {
81
+ await this.connection.script(statements)
82
+ this.reflect.invalidate()
83
+ }
84
+
85
+ // phase 3: views and indexes (after tables are converged)
86
+ await this.convergeViewsAndIndexes()
87
+ }
88
+
89
+ private async convergeViewsAndIndexes() {
90
+ const statements: string[] = []
91
+
92
+ // Collect current schema view/index names for stale detection
93
+ const schemaViewNames = new Set<string>()
94
+ const schemaIndexNames = new Set<string>()
95
+
96
+ // Views
97
+ for (const member of Object.values(this.schema.views)) {
98
+ const view = (member as BuildView).$meta
99
+ schemaViewNames.add(view.name)
100
+ const exists = await this.hasMigration(view.name)
101
+ if (!exists) {
102
+ console.log(`🤠 creating view: ${view.name}`)
103
+ statements.push(generateCreateView(view).trim())
104
+ statements.push(insertMigration("view", view))
105
+ } else {
106
+ const oldData = await this.getMigrationData(view.name)
107
+ if (oldData !== JSON.stringify(view)) {
108
+ console.log(`🤠 recreating view: ${view.name}`)
109
+ statements.push(
110
+ print(["drop view if exists ", Format.name(view.name)]),
111
+ )
112
+ statements.push(generateCreateView(view).trim())
113
+ statements.push(updateMigration("view", view))
114
+ }
115
+ }
116
+ }
117
+
118
+ // Indexes
119
+ for (const member of Object.values(this.schema.indexes)) {
120
+ const index = (member as { $meta: IndexData }).$meta
121
+ schemaIndexNames.add(index.name)
122
+ const exists = await this.hasMigration(index.name)
123
+ if (!exists) {
124
+ console.log(`🤠 creating index: ${index.name}`)
125
+ statements.push(generateCreateIndex(index).trim())
126
+ statements.push(insertMigration("index", index))
127
+ } else {
128
+ const oldData = await this.getMigrationData(index.name)
129
+ if (oldData !== JSON.stringify(index)) {
130
+ console.log(`🤠 recreating index: ${index.name}`)
131
+ statements.push(
132
+ print(["drop index if exists ", Format.name(index.name)]),
133
+ )
134
+ statements.push(generateCreateIndex(index).trim())
135
+ statements.push(updateMigration("index", index))
136
+ }
137
+ }
138
+ }
139
+
140
+ // Drop stale views/indexes that are tracked but no longer in schema
141
+ const tracked = await this.getTrackedNonTables()
142
+ for (const { name, type } of tracked) {
143
+ if (type === "view" && !schemaViewNames.has(name)) {
144
+ console.log(`🤠 dropping stale view: ${name}`)
145
+ statements.push(print(["drop view if exists ", Format.name(name)]))
146
+ statements.push(deleteMigration(name))
147
+ }
148
+ if (type === "index" && !schemaIndexNames.has(name)) {
149
+ console.log(`🤠 dropping stale index: ${name}`)
150
+ statements.push(print(["drop index if exists ", Format.name(name)]))
151
+ statements.push(deleteMigration(name))
152
+ }
153
+ }
154
+
155
+ if (statements.length > 0) {
156
+ await this.connection.script(statements)
157
+ }
158
+ }
159
+
160
+ private async executeHacks() {
161
+ if (this.hacks.length === 0) {
162
+ return
163
+ }
164
+
165
+ const statements: string[] = ["PRAGMA defer_foreign_keys = on"]
166
+ let dirty = false
167
+
168
+ for (const hack of this.hacks) {
169
+ switch (hack.type) {
170
+ case "droppedTable": {
171
+ if (await this.reflect.hasTable(hack.tableName)) {
172
+ console.log(`🤠 dropping table: ${hack.tableName}`)
173
+ statements.push(print(["drop table ", Format.name(hack.tableName)]))
174
+ statements.push(deleteMigration(hack.tableName))
175
+ dirty = true
176
+ }
177
+ break
178
+ }
179
+ case "renamedTable": {
180
+ if (await this.reflect.hasTable(hack.fromTable)) {
181
+ if (await this.reflect.hasTable(hack.toTable)) {
182
+ // auto-recovery: previous hack-less migration created an empty
183
+ // table under the new name — drop it so we can rename the old one
184
+ console.log(`🤠 dropping stale table: ${hack.toTable}`)
185
+ statements.push(print(["drop table ", Format.name(hack.toTable)]))
186
+ statements.push(deleteMigration(hack.toTable))
187
+ }
188
+ console.log(
189
+ `🤠 renaming table: ${hack.fromTable} → ${hack.toTable}`,
190
+ )
191
+ statements.push(
192
+ print([
193
+ "alter table ",
194
+ Format.name(hack.fromTable),
195
+ " rename to ",
196
+ Format.name(hack.toTable),
197
+ ]),
198
+ )
199
+ statements.push(renameMigration(hack.fromTable, hack.toTable))
200
+ dirty = true
201
+ }
202
+ break
203
+ }
204
+ case "droppedColumn": {
205
+ if (await this.reflect.hasTable(hack.tableName)) {
206
+ const tableData = await this.getTableData(hack.tableName)
207
+ const columnExists = tableData.columns.some(
208
+ ({ name }) => name === hack.columnName,
209
+ )
210
+ if (columnExists) {
211
+ console.log(
212
+ `🤠 dropping column: ${hack.tableName}.${hack.columnName}`,
213
+ )
214
+ statements.push(
215
+ print([
216
+ "alter table ",
217
+ Format.name(hack.tableName),
218
+ " drop column ",
219
+ Format.name(hack.columnName),
220
+ ]),
221
+ )
222
+ const updated: TableData = {
223
+ ...tableData,
224
+ columns: tableData.columns.filter(
225
+ ({ name }) => name !== hack.columnName,
226
+ ),
227
+ }
228
+ statements.push(updateMigration("table", updated))
229
+ dirty = true
230
+ }
231
+ }
232
+ break
233
+ }
234
+ case "renamedColumn": {
235
+ if (await this.reflect.hasTable(hack.fromTable)) {
236
+ const tableData = await this.getTableData(hack.fromTable)
237
+ const columnExists = tableData.columns.some(
238
+ ({ name }) => name === hack.fromColumn,
239
+ )
240
+ if (columnExists) {
241
+ console.log(
242
+ `🤠 renaming column: ${hack.fromTable}.${hack.fromColumn} → ${hack.toColumn}`,
243
+ )
244
+ statements.push(
245
+ print([
246
+ "alter table ",
247
+ Format.name(hack.fromTable),
248
+ " rename column ",
249
+ Format.name(hack.fromColumn),
250
+ " to ",
251
+ Format.name(hack.toColumn),
252
+ ]),
253
+ )
254
+ const updated: TableData = {
255
+ ...tableData,
256
+ columns: tableData.columns.map((col) =>
257
+ col.name === hack.fromColumn
258
+ ? { ...col, name: hack.toColumn }
259
+ : col,
260
+ ),
261
+ }
262
+ statements.push(updateMigration("table", updated))
263
+ dirty = true
264
+ }
265
+ }
266
+ break
267
+ }
268
+ }
269
+ }
270
+
271
+ if (dirty) {
272
+ await this.connection.script(statements)
273
+ this.reflect.invalidate()
274
+ }
275
+ }
276
+
277
+ private async getTableData(tableName: string) {
278
+ const results = await this.connection.query<{ data: string }>(`
279
+ select data from ${MIGRATION_TABLE}
280
+ where name = ${Format.text(tableName)}
281
+ `)
282
+ return JSON.parse(results[0].data) as TableData
283
+ }
284
+
285
+ private async hasMigration(name: string) {
286
+ const results = await this.connection.query<{ name: string }>(`
287
+ select name from ${MIGRATION_TABLE}
288
+ where name = ${Format.text(name)}
289
+ `)
290
+ return results.length > 0
291
+ }
292
+
293
+ private async getMigrationData(name: string) {
294
+ const results = await this.connection.query<{ data: string }>(`
295
+ select data from ${MIGRATION_TABLE}
296
+ where name = ${Format.text(name)}
297
+ `)
298
+ return results[0]?.data
299
+ }
300
+
301
+ private async getTrackedNonTables() {
302
+ return this.connection.query<{ name: string; type: string }>(`
303
+ select name, type from ${MIGRATION_TABLE}
304
+ where type != 'table'
305
+ `)
306
+ }
307
+ }
308
+
309
+ function createMigrationTable() {
310
+ return `
311
+ create table ${MIGRATION_TABLE} (
312
+ id integer primary key autoincrement,
313
+ timestamp integer not null,
314
+ type string not null,
315
+ name string not null unique,
316
+ data string not null
317
+ )
318
+ `
319
+ }
320
+
321
+ function insertMigration(type: MigratableType, data: Migratable) {
322
+ const json = JSON.stringify(data)
323
+ return `
324
+ insert into ${MIGRATION_TABLE} (
325
+ timestamp,
326
+ type,
327
+ name,
328
+ data
329
+ )
330
+ values (
331
+ ${Format.NOW},
332
+ '${type}',
333
+ ${Format.text(data.name)},
334
+ ${Format.text(json)}
335
+ )
336
+ `
337
+ }
338
+
339
+ function updateMigration(_type: MigratableType, data: Migratable) {
340
+ const json = JSON.stringify(data)
341
+ return `
342
+ update ${MIGRATION_TABLE}
343
+ set timestamp = ${Format.NOW},
344
+ data = ${Format.text(json)}
345
+ where name = ${Format.text(data.name)}
346
+ `
347
+ }
348
+
349
+ function deleteMigration(tableName: string) {
350
+ return `
351
+ delete from ${MIGRATION_TABLE}
352
+ where name = ${Format.text(tableName)}
353
+ `
354
+ }
355
+
356
+ function renameMigration(fromName: string, toName: string) {
357
+ return `
358
+ update ${MIGRATION_TABLE}
359
+ set timestamp = ${Format.NOW},
360
+ name = ${Format.text(toName)}
361
+ where name = ${Format.text(fromName)}
362
+ `
363
+ }
364
+
365
+ function migrateTable(
366
+ oldTable: TableData,
367
+ newTable: TableData,
368
+ hacks: SchemaHack[],
369
+ ): string[] {
370
+ const interimName = `__interim_${newTable.name}`
371
+ const renames = hacks
372
+ .filter((hack) => hack.type === "renamedColumn")
373
+ .filter(({ fromTable }) => fromTable === newTable.name)
374
+ // oxlint-disable-next-line unicorn/no-array-reduce
375
+ const mappings = newTable.columns.reduce<{ from: string; to: string }[]>(
376
+ (mappings, column) => {
377
+ const oldColumnExists = oldTable.columns.some(
378
+ ({ name }) => name === column.name,
379
+ )
380
+ const rename = renames.find((rename) => rename.toColumn === column.name)
381
+ const renamesOldColumn =
382
+ !!rename &&
383
+ oldTable.columns.some(({ name }) => name === rename.fromColumn)
384
+ if (!oldColumnExists && !renamesOldColumn) {
385
+ return mappings
386
+ }
387
+ return [
388
+ // biome-ignore lint/performance/noAccumulatingSpread: small n
389
+ ...mappings,
390
+ {
391
+ from: renamesOldColumn ? rename.fromColumn : column.name,
392
+ to: column.name,
393
+ },
394
+ ]
395
+ },
396
+ [],
397
+ )
398
+ const orphaned = oldTable.columns.filter((col) => {
399
+ const isMapped = mappings.some((m) => m.from === col.name)
400
+ const isDropped = hacks.some(
401
+ (h) =>
402
+ h.type === "droppedColumn" &&
403
+ h.tableName === newTable.name &&
404
+ h.columnName === col.name,
405
+ )
406
+ return !isMapped && !isDropped
407
+ })
408
+ if (orphaned.length > 0) {
409
+ const names = orphaned.map((c) => c.name).join(", ")
410
+ console.error(
411
+ `🤠 refusing to migrate ${newTable.name}: columns [${names}] would be lost — add renamed.column() or dropped.column() hacks`,
412
+ )
413
+ return []
414
+ }
415
+ return [
416
+ generateCreateTable({ ...newTable, name: interimName }).trim(),
417
+ print([
418
+ "insert into ",
419
+ Format.name(interimName),
420
+ " (",
421
+ mappings.map((mapping, m) => [m > 0 && ", ", Format.name(mapping.to)]),
422
+ ")",
423
+ newline,
424
+ "select ",
425
+ mappings.map((mapping, m) => [m > 0 && ", ", Format.name(mapping.from)]),
426
+ " from ",
427
+ Format.name(oldTable.name),
428
+ ]),
429
+ print(["drop table ", Format.name(oldTable.name)]),
430
+ print([
431
+ "alter table ",
432
+ Format.name(interimName),
433
+ " rename to ",
434
+ Format.name(newTable.name),
435
+ ]),
436
+ ]
437
+ }
@@ -0,0 +1,245 @@
1
+ // oxlint-disable typescript/no-explicit-any
2
+ import { join, print } from "@sigitex/print"
3
+ import stableStringify from "fast-json-stable-stringify"
4
+ import { createHash } from "node:crypto"
5
+ import type { Connection } from "../api"
6
+ import { Format } from "../framework"
7
+ import { Mappings } from "../queryBuilder/Mappings"
8
+ import { generateDelete, generateInsert } from "../queryGenerator"
9
+ import { Reflector } from "../reflection"
10
+ import { text } from "../schemaBuilder/columnBuilders"
11
+ import { Fixture, type Fixtures } from "../schemaBuilder/createFixture"
12
+ import { createTable } from "../schemaBuilder/createTable"
13
+ import type { RefBy } from "../schemaBuilder/schemaBuilder.types"
14
+ import { generateCreateTable } from "../schemaGenerator"
15
+
16
+ const SEED_TABLE = "cowboy_seed"
17
+
18
+ const seedTable = createTable(SEED_TABLE, {
19
+ name: text.primaryKey,
20
+ hash: text.notNull,
21
+ })
22
+
23
+ export class CowboySeeder {
24
+ private readonly connection: Connection
25
+ private readonly seeds: Fixtures
26
+ private readonly fixtures: Fixtures
27
+ private readonly runFixtures: boolean
28
+ private readonly reflect: Reflector
29
+
30
+ constructor(
31
+ connection: Connection,
32
+ seeds: Fixtures,
33
+ fixtures: Fixtures,
34
+ runFixtures: boolean,
35
+ ) {
36
+ this.connection = connection
37
+ this.seeds = seeds
38
+ this.fixtures = fixtures
39
+ this.runFixtures = runFixtures
40
+ this.reflect = new Reflector(connection)
41
+ }
42
+
43
+ async seed() {
44
+ console.log("🌱 checking for seed changes")
45
+ const statements: string[] = []
46
+
47
+ const hasSeedTable = await this.reflect.hasTable(SEED_TABLE)
48
+ if (!hasSeedTable) {
49
+ statements.push(generateCreateTable(seedTable.$meta).trim())
50
+ }
51
+
52
+ const existingHashes = hasSeedTable
53
+ ? await this.connection.query<{ name: string; hash: string }>(
54
+ `select name, hash from ${SEED_TABLE}`,
55
+ )
56
+ : []
57
+
58
+ const hashMap = new Map(existingHashes.map((r) => [r.name, r.hash]))
59
+ const combined = this.combineByTable()
60
+
61
+ for (const [tableName, { table, rows }] of combined) {
62
+ const hash = computeHash(rows)
63
+ const existing = hashMap.get(tableName)
64
+ if (existing === hash) {
65
+ continue
66
+ }
67
+
68
+ console.log(`🌱 reseeding: ${tableName}`)
69
+ statements.push(...generateReseed(tableName, table, rows))
70
+ statements.push(upsertHash(tableName, hash))
71
+ }
72
+
73
+ if (statements.length > 0) {
74
+ statements.unshift("PRAGMA defer_foreign_keys = on")
75
+ await this.connection.script(statements)
76
+ }
77
+ }
78
+
79
+ private combineByTable() {
80
+ const combined = new Map<
81
+ string,
82
+ { table: Fixture<any>["table"]; rows: Record<string, unknown>[] }
83
+ >()
84
+ const allFixtures = {
85
+ ...this.seeds,
86
+ ...(this.runFixtures ? this.fixtures : {}),
87
+ }
88
+
89
+ for (const fixture of Object.values(allFixtures)) {
90
+ const tableName: string = fixture.table.$meta.name
91
+ const existing = combined.get(tableName)
92
+ if (existing) {
93
+ existing.rows.push(...fixture.rows)
94
+ } else {
95
+ combined.set(tableName, {
96
+ table: fixture.table,
97
+ rows: [...fixture.rows],
98
+ })
99
+ }
100
+ }
101
+ return combined
102
+ }
103
+ }
104
+
105
+ function computeHash(rows: Record<string, unknown>[]) {
106
+ const sanitized = rows.map((row) => {
107
+ const out: Record<string, unknown> = {}
108
+ for (const key in row) {
109
+ const value = row[key]
110
+ if (Fixture.isRefBy(value)) {
111
+ out[key] =
112
+ `ref:${value.table.$meta.name}.${String(value.column)}=${value.value}`
113
+ } else {
114
+ out[key] = value
115
+ }
116
+ }
117
+ return out
118
+ })
119
+ const serialized = stableStringify(sanitized)
120
+ return createHash("sha256").update(serialized).digest("hex")
121
+ }
122
+
123
+ function generateReseed(
124
+ tableName: string,
125
+ table: Fixture<any>["table"],
126
+ rows: Record<string, unknown>[],
127
+ ): string[] {
128
+ const tableData = table.$meta
129
+ const autoincrementCol = tableData.columns.find(
130
+ (c: any) => c.primaryKey?.autoincrement,
131
+ )?.name
132
+
133
+ const deleteSQL = generateDelete({ table: tableName }).trim()
134
+ if (rows.length === 0) {
135
+ return [deleteSQL]
136
+ }
137
+
138
+ const strippedRows = autoincrementCol
139
+ ? rows.map((row) => stripAutoincrement(row, autoincrementCol))
140
+ : rows
141
+
142
+ const processedRows = strippedRows.map((row) => {
143
+ const mapped: Record<string, unknown> = {}
144
+ for (const key in row) {
145
+ const value = row[key]
146
+ if (Fixture.isRefBy(value)) {
147
+ continue
148
+ }
149
+ mapped[key] = value
150
+ }
151
+ return Mappings.row(tableData, mapped)
152
+ })
153
+
154
+ const hasRefBy = strippedRows.some((row) =>
155
+ Object.values(row).some(Fixture.isRefBy),
156
+ )
157
+
158
+ if (!hasRefBy) {
159
+ const columns = Object.keys(strippedRows[0])
160
+ const insertSQL = generateInsert({
161
+ table: tableName,
162
+ columns,
163
+ rows: processedRows,
164
+ }).trim()
165
+ return [deleteSQL, insertSQL]
166
+ }
167
+
168
+ return [
169
+ deleteSQL,
170
+ ...generateRefByInserts(tableName, tableData, strippedRows),
171
+ ]
172
+ }
173
+
174
+ function generateRefByInserts(
175
+ tableName: string,
176
+ tableData: Fixture<any>["table"]["$meta"],
177
+ rows: Record<string, unknown>[],
178
+ ): string[] {
179
+ const statements: string[] = []
180
+ for (const row of rows) {
181
+ const columns: string[] = []
182
+ const values: string[] = []
183
+
184
+ for (const key in row) {
185
+ columns.push(key)
186
+ const value = row[key]
187
+ if (Fixture.isRefBy(value)) {
188
+ values.push(refBySubquery(value))
189
+ } else {
190
+ const mapped = Mappings.row(tableData, { [key]: value })
191
+ values.push(Format.value(mapped[key]))
192
+ }
193
+ }
194
+
195
+ statements.push(
196
+ print([
197
+ "insert into ",
198
+ Format.name(tableName),
199
+ " (",
200
+ join(", ", columns, Format.name),
201
+ ") values (",
202
+ values.join(", "),
203
+ ")",
204
+ ]),
205
+ )
206
+ }
207
+ return statements
208
+ }
209
+
210
+ function refBySubquery(ref: RefBy) {
211
+ const refTableName = ref.table.$meta.name
212
+ const pk = ref.table.$meta.columns.find((c: any) => c.primaryKey)
213
+ const pkColumn = pk ? pk.name : "id"
214
+ return print([
215
+ "(select ",
216
+ Format.name(pkColumn),
217
+ " from ",
218
+ Format.name(refTableName),
219
+ " where ",
220
+ Format.name(ref.column as string),
221
+ " = ",
222
+ Format.value(ref.value),
223
+ ")",
224
+ ])
225
+ }
226
+
227
+ function stripAutoincrement(row: Record<string, unknown>, col: string) {
228
+ if (row[col] !== 0) {
229
+ return row
230
+ }
231
+ const { [col]: _, ...rest } = row
232
+ return rest
233
+ }
234
+
235
+ function upsertHash(tableName: string, hash: string) {
236
+ return print([
237
+ "insert or replace into ",
238
+ SEED_TABLE,
239
+ " (name, hash) values (",
240
+ Format.text(tableName),
241
+ ", ",
242
+ Format.text(hash),
243
+ ")",
244
+ ])
245
+ }
@@ -0,0 +1,29 @@
1
+ export type SchemaHack =
2
+ | RenamedTable
3
+ | RenamedColumn
4
+ | DroppedTable
5
+ | DroppedColumn
6
+
7
+ export type RenamedTable = {
8
+ readonly type: "renamedTable"
9
+ readonly fromTable: string
10
+ readonly toTable: string
11
+ }
12
+
13
+ export type RenamedColumn = {
14
+ readonly type: "renamedColumn"
15
+ readonly fromTable: string
16
+ readonly fromColumn: string
17
+ readonly toColumn: string
18
+ }
19
+
20
+ export type DroppedTable = {
21
+ readonly type: "droppedTable"
22
+ readonly tableName: string
23
+ }
24
+
25
+ export type DroppedColumn = {
26
+ readonly type: "droppedColumn"
27
+ readonly tableName: string
28
+ readonly columnName: string
29
+ }