@bakery-framework/orm 2.0.0-alpha.9 → 2.0.0-rc.2
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/README.md +9 -9
- package/package.json +3 -3
- package/src/adapters/base.ts +181 -62
- package/src/adapters/mysql.ts +29 -17
- package/src/adapters/observe.ts +11 -11
- package/src/adapters/pgsql.ts +54 -39
- package/src/adapters/registry.ts +13 -13
- package/src/adapters/sqlite.ts +175 -44
- package/src/adapters.ts +4 -4
- package/src/backup.ts +1 -1
- package/src/connection.ts +1 -1
- package/src/define.ts +22 -22
- package/src/field.ts +55 -40
- package/src/globals.d.ts +1 -1
- package/src/index.ts +4 -4
- package/src/orm/index.ts +2 -2
- package/src/orm/mutation.ts +36 -28
- package/src/orm/query.ts +92 -89
- package/src/pool.ts +5 -5
- package/src/schema-registry.ts +3 -3
- package/src/schema-util.ts +48 -17
- package/src/sync/builder.ts +32 -32
- package/src/sync/diff.ts +12 -12
- package/src/sync/engine.ts +19 -19
- package/src/sync/execute.ts +16 -16
- package/src/sync/history.ts +5 -5
- package/src/sync/index.ts +12 -12
- package/src/sync/ledger.ts +23 -23
- package/src/sync/load.ts +16 -16
- package/src/sync/plan.ts +1 -2
- package/src/sync/rename.ts +1 -1
- package/src/sync/rollback.ts +3 -3
- package/src/sync/types.ts +20 -4
- package/src/sync/view-sql.ts +9 -9
- package/templates/schema.example.ts +4 -4
package/src/sync/execute.ts
CHANGED
|
@@ -37,7 +37,7 @@ async function processTableRebuild(
|
|
|
37
37
|
// Inline, or the rebuild silently drops every foreign key the table had.
|
|
38
38
|
//
|
|
39
39
|
// A constraint is part of the table definition, so recreating the table
|
|
40
|
-
// without it removes it
|
|
40
|
+
// without it removes it, and on SQLite there is no `ALTER` to put one back,
|
|
41
41
|
// which is precisely why the planner turns a foreign-key change into a
|
|
42
42
|
// rebuild. Without this, that plan could never *add* a key: the rebuild it
|
|
43
43
|
// scheduled was the thing dropping them.
|
|
@@ -209,12 +209,12 @@ async function rebuildTablesPhase(
|
|
|
209
209
|
* Drop declared views before any table is rebuilt, where the dialect needs it.
|
|
210
210
|
*
|
|
211
211
|
* A rebuild swaps the table out and back, and two of the three dialects refuse
|
|
212
|
-
* to do that while a view still names the table
|
|
212
|
+
* to do that while a view still names the table: SQLite at the rename, Postgres
|
|
213
213
|
* at the drop. `viewsBlockTableRebuild` carries which, and why; MySQL is the one
|
|
214
214
|
* that does not care and skips this entirely.
|
|
215
215
|
*
|
|
216
216
|
* Views hold no data and `syncViewsAndTablesPhase` recreates every declared one
|
|
217
|
-
* a moment later, so dropping them first costs nothing
|
|
217
|
+
* a moment later, so dropping them first costs nothing: it is the same "drop
|
|
218
218
|
* and recreate" the engine already does when a view's body changes.
|
|
219
219
|
*
|
|
220
220
|
* Only when something is actually being rebuilt: a sync with no rebuilds should
|
|
@@ -295,7 +295,7 @@ async function addIndexesPhase(
|
|
|
295
295
|
* Foreign keys on tables that already existed.
|
|
296
296
|
*
|
|
297
297
|
* Only reachable where the dialect can ALTER one in. SQLite cannot, so its
|
|
298
|
-
* missing keys are handled by scheduling a table rebuild in the planner
|
|
298
|
+
* missing keys are handled by scheduling a table rebuild in the planner: the
|
|
299
299
|
* rebuild recreates the table through `createTable`, which emits them inline.
|
|
300
300
|
*/
|
|
301
301
|
async function foreignKeysPhase(
|
|
@@ -308,7 +308,7 @@ async function foreignKeysPhase(
|
|
|
308
308
|
for (const fk of fksToDrop.values()) {
|
|
309
309
|
// Called unconditionally, like every other phase. These two were `?.` for
|
|
310
310
|
// as long as `syncMsgs` did not declare them, which read as "this message
|
|
311
|
-
// is optional" and was really "this message does not exist"
|
|
311
|
+
// is optional" and was really "this message does not exist": the optional
|
|
312
312
|
// call is what let the gap survive: it type-checked, and the proxy turned
|
|
313
313
|
// the miss into an error line at run time instead of a build failure.
|
|
314
314
|
MESSAGES.EXEC_DROP_FK({ table: fk.table, name: fk.name ?? '' })
|
|
@@ -375,7 +375,7 @@ export function hasOldWrappers(constraints: SyncTypes.DBConstraints) {
|
|
|
375
375
|
// and a table-level `_transform` (a function), so `Object.values` really
|
|
376
376
|
// does yield a mixed union and `_oldColumn` really is absent from two of
|
|
377
377
|
// its three members. Reading it off the string or the function gives
|
|
378
|
-
// `undefined`, which is what this check wants
|
|
378
|
+
// `undefined`, which is what this check wants, so the narrowing is
|
|
379
379
|
// deliberate rather than a lie, and the emitted code is unchanged.
|
|
380
380
|
const cols = Object.values(tObj) as (
|
|
381
381
|
| SyncTypes.ColumnConstraint
|
|
@@ -385,7 +385,7 @@ export function hasOldWrappers(constraints: SyncTypes.DBConstraints) {
|
|
|
385
385
|
})
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
-
/** The `foreign()` declarations,
|
|
388
|
+
/** The `foreign()` declarations, normalized into the shape the diff uses. */
|
|
389
389
|
export function collectForeignKeys(
|
|
390
390
|
tsIndexes: SyncTypes.DBIndexes,
|
|
391
391
|
): SyncTypes.DBForeignKeys {
|
|
@@ -412,7 +412,7 @@ export function collectForeignKeys(
|
|
|
412
412
|
* Which foreign keys to add and which to drop.
|
|
413
413
|
*
|
|
414
414
|
* Keyed by the tuple, so a constraint the database named itself still matches
|
|
415
|
-
* the declaration that produced it
|
|
415
|
+
* the declaration that produced it, including on SQLite, which reports no name
|
|
416
416
|
* at all.
|
|
417
417
|
*/
|
|
418
418
|
export function calculateForeignKeyDiff(
|
|
@@ -422,7 +422,7 @@ export function calculateForeignKeyDiff(
|
|
|
422
422
|
/**
|
|
423
423
|
* Tables that already exist, so a key on a table being *created* is left out.
|
|
424
424
|
*
|
|
425
|
-
* `CREATE TABLE` emits its foreign keys inline
|
|
425
|
+
* `CREATE TABLE` emits its foreign keys inline: the only spelling SQLite
|
|
426
426
|
* has. Counting those as "to add" made MySQL and Postgres ALTER in a
|
|
427
427
|
* constraint that already existed, and made SQLite schedule a rebuild of a
|
|
428
428
|
* table that did not exist yet: a fresh `db:sync` announced
|
|
@@ -430,7 +430,7 @@ export function calculateForeignKeyDiff(
|
|
|
430
430
|
*
|
|
431
431
|
* Phrased as "being created" rather than "already exists" deliberately. The
|
|
432
432
|
* inverse defaults to an *empty* set, which reads as "nothing exists" and so
|
|
433
|
-
* suppressed every key precisely when the database was new
|
|
433
|
+
* suppressed every key precisely when the database was new: the case that
|
|
434
434
|
* exposed this in the first place.
|
|
435
435
|
*/
|
|
436
436
|
tablesBeingCreated: Set<string> = new Set(),
|
|
@@ -439,7 +439,7 @@ export function calculateForeignKeyDiff(
|
|
|
439
439
|
const fksToDrop = new Map<string, SyncTypes.ForeignKeyInfo>()
|
|
440
440
|
|
|
441
441
|
// `NO ACTION` on both sides of the comparison, because that is what every
|
|
442
|
-
// dialect reports for a key declared without one
|
|
442
|
+
// dialect reports for a key declared without one, so an omitted action and
|
|
443
443
|
// an explicit `NO ACTION` must not read as a difference.
|
|
444
444
|
const act = (a?: string) => a ?? 'NO ACTION'
|
|
445
445
|
const sameActions = (
|
|
@@ -456,8 +456,8 @@ export function calculateForeignKeyDiff(
|
|
|
456
456
|
|
|
457
457
|
const existing = dbFks[id]
|
|
458
458
|
if (existing) {
|
|
459
|
-
// Same columns, different
|
|
460
|
-
// action in place, so the constraint is replaced
|
|
459
|
+
// Same columns, different behavior. No dialect alters a referential
|
|
460
|
+
// action in place, so the constraint is replaced, and the drop carries
|
|
461
461
|
// the name the *database* gave it, which is not necessarily the one we
|
|
462
462
|
// would generate for it.
|
|
463
463
|
if (!sameActions(existing, fk)) {
|
|
@@ -479,14 +479,14 @@ export function calculateForeignKeyDiff(
|
|
|
479
479
|
* Table names ordered so a parent is always created before its children.
|
|
480
480
|
*
|
|
481
481
|
* Not cosmetic: a foreign key requires the referenced table to exist, so an
|
|
482
|
-
* unordered CREATE fails outright
|
|
482
|
+
* unordered CREATE fails outright, verified against Postgres, which answers
|
|
483
483
|
* `relation "o_parent" does not exist`. Dropping runs in reverse for the
|
|
484
484
|
* mirror-image reason.
|
|
485
485
|
*
|
|
486
486
|
* A cycle cannot be ordered at all. Rather than loop forever or drop tables,
|
|
487
487
|
* the remainder is appended in declaration order: the foreign key that closes
|
|
488
|
-
* the cycle then fails loudly at the database, which is the honest outcome
|
|
489
|
-
*
|
|
488
|
+
* the cycle then fails loudly at the database, which is the honest outcome.
|
|
489
|
+
* Breaking it needs a deferred constraint, which no dialect here spells alike.
|
|
490
490
|
*/
|
|
491
491
|
export function orderTablesByDependency(
|
|
492
492
|
tables: string[],
|
package/src/sync/history.ts
CHANGED
|
@@ -9,8 +9,8 @@ const logger = new Logger('db-history')
|
|
|
9
9
|
/**
|
|
10
10
|
* What changed between two applied schemas, by name only.
|
|
11
11
|
*
|
|
12
|
-
* Names, not types
|
|
13
|
-
* reason: a type comparison here would need dialect
|
|
12
|
+
* Names, not types: the same restraint `shapesMatch` uses and for the same
|
|
13
|
+
* reason: a type comparison here would need dialect normalization, which is the
|
|
14
14
|
* thing the ledger exists to avoid depending on. A summary that says "column
|
|
15
15
|
* changed" when a Postgres default merely re-rendered itself would be worse
|
|
16
16
|
* than saying nothing.
|
|
@@ -63,7 +63,7 @@ export function isEmptyDiff(d: HistoryDiff): boolean {
|
|
|
63
63
|
)
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/** `applied_at` is stored as whole seconds
|
|
66
|
+
/** `applied_at` is stored as whole seconds. See `writeLedger`. */
|
|
67
67
|
export function formatWhen(appliedAt: number): string {
|
|
68
68
|
if (!Number.isFinite(appliedAt) || appliedAt <= 0) return 'unknown'
|
|
69
69
|
return new Date(appliedAt * 1000).toISOString().replace('T', ' ').slice(0, 19)
|
|
@@ -102,7 +102,7 @@ export function formatEntry(
|
|
|
102
102
|
// Only worth saying on rows that predate the payload carrying indexes,
|
|
103
103
|
// because those are exactly the rows `db:rollback` will refuse.
|
|
104
104
|
if (entry.indexes === undefined) {
|
|
105
|
-
lines.push(' (no index record
|
|
105
|
+
lines.push(' (no index record: written before ledger v2)')
|
|
106
106
|
}
|
|
107
107
|
return lines
|
|
108
108
|
}
|
|
@@ -115,7 +115,7 @@ export class HistoryService {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
static printHelp(): void {
|
|
118
|
-
// Program output, not a log line
|
|
118
|
+
// Program output, not a log line: the same call the other commands' usage
|
|
119
119
|
// text makes, and one of the two documented `console` exceptions.
|
|
120
120
|
console.log(`
|
|
121
121
|
Usage: bun run db:history
|
package/src/sync/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ const syncMsgs = {
|
|
|
15
15
|
'E %rforeign() is declared but not implemented%*: {names}. No adapter emits FOREIGN KEY DDL, so it would be created as a plain index and then re-diffed on every sync. Use index() on the column and enforce the reference in your application.',
|
|
16
16
|
SCHEMA_NOT_FOUND:
|
|
17
17
|
'E %rConfigured schema path not found%*: {path}. %yschema%* in server.config.ts must name a file or an orm/ folder that exists; remove it to auto-detect. Generating one from the database? Create the (empty) file first, or run %ydb:sync --migrate%*.',
|
|
18
|
-
MIGRATE_SCAFFOLDED: 'I Created %y{dir}
|
|
18
|
+
MIGRATE_SCAFFOLDED: 'I Created %y{dir}%*: the generator owns tables.ts.',
|
|
19
19
|
MIGRATE_RETIRED:
|
|
20
20
|
'I Converted to the orm/ folder. The previous %yschema.ts%* was moved to %y{to}%*, not deleted.',
|
|
21
21
|
} as const
|
|
@@ -23,7 +23,7 @@ const syncMsgs = {
|
|
|
23
23
|
const MESSAGES = messageLogger(logger, syncMsgs)
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* `orm/index.ts
|
|
26
|
+
* `orm/index.ts`: the one file in the folder layout nothing else writes.
|
|
27
27
|
*
|
|
28
28
|
* `tables.ts` belongs to the generator, and `views.ts` / `indexes.ts` are seeded
|
|
29
29
|
* by it. This is the re-export barrel plus the type registration, and without
|
|
@@ -50,8 +50,8 @@ ${hasViews ? "export * from './views'\n" : ''}${hasIndexes ? "export * from './i
|
|
|
50
50
|
type Model = ${model}
|
|
51
51
|
|
|
52
52
|
// Without this block the ORM still runs, untyped: every table and column falls
|
|
53
|
-
// back to \`any\`. The framework never imports this file at runtime
|
|
54
|
-
// values are loaded by path
|
|
53
|
+
// back to \`any\`. The framework never imports this file at runtime (schema
|
|
54
|
+
// values are loaded by path), so this is purely the type registration.
|
|
55
55
|
declare module '@bakery-framework/orm/schema-registry' {
|
|
56
56
|
interface SchemaRegistry {
|
|
57
57
|
schema: {
|
|
@@ -72,7 +72,7 @@ export class SyncService {
|
|
|
72
72
|
*
|
|
73
73
|
* Separated from `run()` so the answer is available before anything is
|
|
74
74
|
* opened. It used to be the last check in `run()`, after `initConfig`,
|
|
75
|
-
* `initDB`, `loadSchema` and both fatal guards
|
|
75
|
+
* `initDB`, `loadSchema` and both fatal guards, so the one flag whose whole
|
|
76
76
|
* job is to explain the others exited 1 on an unreachable database or a
|
|
77
77
|
* single `foreign()` declaration, and creating `bakery/server.db` as a side
|
|
78
78
|
* effect of asking for help.
|
|
@@ -82,7 +82,7 @@ export class SyncService {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
static printHelp(): void {
|
|
85
|
-
// CLI usage text goes to stdout verbatim
|
|
85
|
+
// CLI usage text goes to stdout verbatim: it is program output, not a
|
|
86
86
|
// log line, so it deliberately bypasses the structured logger.
|
|
87
87
|
console.log(`
|
|
88
88
|
Usage: bun run db:sync [--migrate] [--choose=db|ts] [--dry-run] [--force-sync] [--help]
|
|
@@ -141,7 +141,7 @@ Flags:
|
|
|
141
141
|
*
|
|
142
142
|
* Moved, never deleted: it goes to `bakery/backups/`, beside the copies the
|
|
143
143
|
* generator already keeps there. `loadSchema` prefers `orm/index.ts`, so a
|
|
144
|
-
* leftover `schema.ts` would be *ignored* rather than used
|
|
144
|
+
* leftover `schema.ts` would be *ignored* rather than used, which is the
|
|
145
145
|
* quiet kind of wrong, since it looks like the file still describes the app
|
|
146
146
|
* while nothing reads it.
|
|
147
147
|
*/
|
|
@@ -178,7 +178,7 @@ Flags:
|
|
|
178
178
|
|
|
179
179
|
// A configured path that does not exist is a config error, not an empty
|
|
180
180
|
// project. Continuing would sync against no schema and then generate a new
|
|
181
|
-
// one at the wrong location
|
|
181
|
+
// one at the wrong location, with the app's real model still sitting
|
|
182
182
|
// where the typo missed it.
|
|
183
183
|
if (loaded.missing) {
|
|
184
184
|
MESSAGES.SCHEMA_NOT_FOUND({ path: loaded.missing })
|
|
@@ -193,12 +193,12 @@ Flags:
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
// `foreign()` used to abort here, because no adapter emitted FOREIGN KEY
|
|
196
|
-
// DDL and the declaration would have become a plain index
|
|
196
|
+
// DDL and the declaration would have become a plain index: referential
|
|
197
197
|
// integrity in appearance only. All three adapters now emit and read back
|
|
198
198
|
// real foreign keys, so the guard is gone.
|
|
199
199
|
//
|
|
200
200
|
// `findUnsupportedForeignKeys` is kept and still exported *from
|
|
201
|
-
// `sync/load
|
|
201
|
+
// `sync/load`*: it is what a future adapter without support would use to
|
|
202
202
|
// refuse rather than pretend. It is no longer imported here, which is the
|
|
203
203
|
// distinction: the function has a reason to exist, the dead import did not.
|
|
204
204
|
|
|
@@ -209,14 +209,14 @@ Flags:
|
|
|
209
209
|
// `--migrate` on a project with no schema at all creates the folder layout
|
|
210
210
|
// rather than a single `schema.ts`. Adoption is exactly the case where the
|
|
211
211
|
// folder earns its keep: the generator owns `tables.ts` and regenerating it
|
|
212
|
-
// cannot touch the views, indexes and registration beside it
|
|
212
|
+
// cannot touch the views, indexes and registration beside it, which for an
|
|
213
213
|
// adopted database is the difference between re-running the command and
|
|
214
214
|
// hand-restoring what it overwrote.
|
|
215
215
|
// `--migrate` always lands on the folder layout, including from an existing
|
|
216
216
|
// single-file `schema.ts`. It used to convert only from *nothing*, which
|
|
217
217
|
// read as an arbitrary distinction: the reason to prefer the folder is that
|
|
218
218
|
// the generator owns `tables.ts` and cannot touch the views, indexes and
|
|
219
|
-
// registration beside it
|
|
219
|
+
// registration beside it, and that is worth exactly as much to a project
|
|
220
220
|
// that already has a schema as to one that does not.
|
|
221
221
|
const adopting = SyncService.migrateRequested()
|
|
222
222
|
const layout = adopting ? 'folder' : loaded.layout
|
package/src/sync/ledger.ts
CHANGED
|
@@ -16,20 +16,20 @@ import type * as SyncTypes from './types'
|
|
|
16
16
|
* the table on every single sync.
|
|
17
17
|
*
|
|
18
18
|
* Comparing against what we *wrote down* removes that whole class. Two JSON
|
|
19
|
-
* objects, no dialect spellings, no
|
|
19
|
+
* objects, no dialect spellings, no normalization, and it can carry things
|
|
20
20
|
* introspection cannot report reliably, such as a `VARCHAR` width.
|
|
21
21
|
*
|
|
22
22
|
* **Why it is not simply trusted.** Introspection cannot lie about what exists;
|
|
23
23
|
* a ledger can. If someone alters the database outside Bakery, the ledger is
|
|
24
|
-
* stale, and migrating from a stale premise is worse than a spurious rebuild
|
|
24
|
+
* stale, and migrating from a stale premise is worse than a spurious rebuild:
|
|
25
25
|
* it can drop a column somebody added. So the ledger is used only when its
|
|
26
26
|
* *shape* still matches the live database, and shape is checked by comparing
|
|
27
|
-
* table and column names, which needs no
|
|
27
|
+
* table and column names, which needs no normalization and therefore cannot
|
|
28
28
|
* suffer the bugs above. Anything else falls back to introspection.
|
|
29
29
|
*
|
|
30
30
|
* One consequence to know: **MySQL commits DDL implicitly**, so the schema
|
|
31
31
|
* change and the ledger write cannot be one atomic unit there. A crash between
|
|
32
|
-
* them leaves the ledger behind, which the shape check then catches
|
|
32
|
+
* them leaves the ledger behind, which the shape check then catches: it fails
|
|
33
33
|
* safe, toward introspection.
|
|
34
34
|
*/
|
|
35
35
|
export const LEDGER_TABLE = '__bakery_schema'
|
|
@@ -58,7 +58,7 @@ export interface LedgerEntry {
|
|
|
58
58
|
appliedAt: number
|
|
59
59
|
constraints: SyncTypes.DBConstraints
|
|
60
60
|
/**
|
|
61
|
-
* Absent on rows written before the payload carried indexes
|
|
61
|
+
* Absent on rows written before the payload carried indexes. See
|
|
62
62
|
* {@link parsePayload}. `undefined` means "not recorded", which is not the
|
|
63
63
|
* same as `{}` ("recorded, and there were none"), and `db:rollback` refuses
|
|
64
64
|
* the difference rather than guessing.
|
|
@@ -72,11 +72,11 @@ export interface LedgerEntry {
|
|
|
72
72
|
* v1 rows are a bare `DBConstraints` object; v2 wraps constraints alongside the
|
|
73
73
|
* indexes that were applied with them. The version is detected by the `v` key
|
|
74
74
|
* rather than by a column, so existing rows keep working with no migration of
|
|
75
|
-
* the migration table
|
|
75
|
+
* the migration table, which would be a fine joke and a bad idea.
|
|
76
76
|
*
|
|
77
77
|
* Indexes were missing from v1 because the ledger only ever fed the *diff*,
|
|
78
78
|
* which reads constraints. `db:rollback` replays a stored schema as a target,
|
|
79
|
-
* and a target with no indexes means "drop every index"
|
|
79
|
+
* and a target with no indexes means "drop every index", so the payload had to
|
|
80
80
|
* grow before rollback could be honest.
|
|
81
81
|
*/
|
|
82
82
|
function parsePayload(
|
|
@@ -186,7 +186,7 @@ export async function writeLedger(
|
|
|
186
186
|
* Every spelling of the ledger's name that can appear in a constraints object.
|
|
187
187
|
*
|
|
188
188
|
* `getConstraints()` camelCases table names, so the table created as
|
|
189
|
-
* `__bakery_schema` comes back as `bakerySchema
|
|
189
|
+
* `__bakery_schema` comes back as `bakerySchema`: matching only the literal
|
|
190
190
|
* name silently stripped nothing, and the ledger showed up in its own diff as
|
|
191
191
|
* a table the schema did not declare. Which then made the shape check fail
|
|
192
192
|
* forever and the ledger never get used at all.
|
|
@@ -213,28 +213,28 @@ export function stripLedger(
|
|
|
213
213
|
* live database?
|
|
214
214
|
*
|
|
215
215
|
* **Names only, deliberately.** Comparing types or defaults here would need the
|
|
216
|
-
* same
|
|
216
|
+
* same normalization the ledger exists to avoid, and a disagreement there is
|
|
217
217
|
* exactly what the ledger is more trustworthy about. Names are names in every
|
|
218
218
|
* dialect, so this check cannot itself be wrong in the way the others were.
|
|
219
219
|
*
|
|
220
220
|
* **But a name has two spellings, and the two sides do not use the same one.**
|
|
221
|
-
* The ledger stores the keys of your TypeScript schema
|
|
222
|
-
* `table()` / `view()`
|
|
221
|
+
* The ledger stores the keys of your TypeScript schema (whatever you passed to
|
|
222
|
+
* `table()` / `view()`), while `getConstraints()` camelCases everything it reads
|
|
223
223
|
* back. So a table declared `view('published_posts', …)` is `published_posts` in
|
|
224
224
|
* the ledger and `publishedPosts` from introspection, and this check called that
|
|
225
225
|
* a drifted database: *"tables differ (+publishedPosts; -published_posts)"*, on
|
|
226
|
-
* a database nothing had touched. Permanently
|
|
226
|
+
* a database nothing had touched. Permanently: the spellings never converge, so
|
|
227
227
|
* every later sync re-reported it and the ledger was never used again. Every app
|
|
228
228
|
* `bun create bakery` generated hit it on the first `db:sync`, because the
|
|
229
229
|
* generated schema declares exactly that view.
|
|
230
230
|
*
|
|
231
231
|
* The damage is quieter than the warning. Falling back to introspection is
|
|
232
|
-
* *safe*, so nothing breaks loudly
|
|
232
|
+
* *safe*, so nothing breaks loudly: it just silently withdraws the thing the
|
|
233
233
|
* ledger is for. Enum member changes, for one, only migrate when the diff runs
|
|
234
234
|
* against the ledger (`plan.ledgerSource === 'ledger'`), so on any such app that
|
|
235
235
|
* feature was inert.
|
|
236
236
|
*
|
|
237
|
-
* Comparing camel-
|
|
237
|
+
* Comparing camel-normalized names fixes it at the one place the two spellings
|
|
238
238
|
* meet. `LEDGER_ALIASES` below is the same bug, found earlier and patched for a
|
|
239
239
|
* single known name; this is the general form of it.
|
|
240
240
|
*/
|
|
@@ -243,7 +243,7 @@ export function shapesMatch(
|
|
|
243
243
|
live: SyncTypes.DBConstraints,
|
|
244
244
|
): { ok: true } | { ok: false; reason: string } {
|
|
245
245
|
const meta = (k: string) => k.startsWith('_')
|
|
246
|
-
// Compare on the
|
|
246
|
+
// Compare on the normalized spelling, report the declared one: a diff that
|
|
247
247
|
// named tables the reader cannot find in either their schema or their database
|
|
248
248
|
// would trade one confusion for another.
|
|
249
249
|
const namesOf = (o: object) => {
|
|
@@ -284,13 +284,13 @@ export function shapesMatch(
|
|
|
284
284
|
* The ledger stores the keys of your TypeScript schema verbatim; every consumer
|
|
285
285
|
* of "current state" downstream looks tables up by `Case.camel(name)`, because
|
|
286
286
|
* that is what `getConstraints()` produces. Handing the raw ledger to the
|
|
287
|
-
* planner therefore made every lookup miss
|
|
287
|
+
* planner therefore made every lookup miss: `diffViews` asked for
|
|
288
288
|
* `publishedPosts`, the ledger held `published_posts`, and a miss reads as "the
|
|
289
289
|
* database does not have this view", so the view was recreated on every single
|
|
290
290
|
* sync. Silent and harmless-looking; a view holds no data, so the only symptom
|
|
291
291
|
* is churn in the log.
|
|
292
292
|
*
|
|
293
|
-
*
|
|
293
|
+
* Normalizing on read rather than on write is deliberate: it repairs the ledgers
|
|
294
294
|
* already written by earlier versions, which a write-side fix could not.
|
|
295
295
|
*
|
|
296
296
|
* Meta keys (`_view`, `_references`, …) are values, not identifiers, and are
|
|
@@ -319,7 +319,7 @@ function normalizeLedgerKeys(
|
|
|
319
319
|
* database, introspection otherwise.
|
|
320
320
|
*
|
|
321
321
|
* Returning the *source* as well as the constraints is what lets the caller say
|
|
322
|
-
* out loud which one it used
|
|
322
|
+
* out loud which one it used: a sync that quietly changed its mind about where
|
|
323
323
|
* truth lives would be very hard to debug later.
|
|
324
324
|
*/
|
|
325
325
|
export async function resolveCurrentState(
|
|
@@ -333,8 +333,8 @@ export async function resolveCurrentState(
|
|
|
333
333
|
const live = stripLedger(await adapter.getConstraints())
|
|
334
334
|
// `--no-ledger`, and it exists because the ledger can be *wrong about types*
|
|
335
335
|
// in a way `shapesMatch` cannot see. That check compares names only, on
|
|
336
|
-
// purpose
|
|
337
|
-
// exists to avoid
|
|
336
|
+
// purpose (comparing types would need the dialect normalization the ledger
|
|
337
|
+
// exists to avoid), so a ledger claiming `VARCHAR(64)` where the column is
|
|
338
338
|
// really `TEXT` matches its shape and wins the diff. That happens whenever a
|
|
339
339
|
// sync legitimately concluded "no change needed" and the *rule* it used later
|
|
340
340
|
// got stricter, which is exactly what adding width to the diff did.
|
|
@@ -369,8 +369,8 @@ export async function resolveCurrentState(
|
|
|
369
369
|
* Has the database stopped matching what Bakery last applied?
|
|
370
370
|
*
|
|
371
371
|
* The same comparison {@link resolveCurrentState} makes, exposed on its own so
|
|
372
|
-
* a caller can *report* it. Sync already handles drift correctly
|
|
373
|
-
* falls back to introspection
|
|
372
|
+
* a caller can *report* it. Sync already handles drift correctly (it quietly
|
|
373
|
+
* falls back to introspection), but quietly is the problem: a column added by
|
|
374
374
|
* hand in production is indistinguishable, from the outside, from a normal run.
|
|
375
375
|
* This is how you find out.
|
|
376
376
|
*
|
|
@@ -378,7 +378,7 @@ export async function resolveCurrentState(
|
|
|
378
378
|
* synced is not "drifted"), or the shape still matches.
|
|
379
379
|
*
|
|
380
380
|
* Never throws. It runs at boot, and a database that cannot answer must not be
|
|
381
|
-
* the reason a server fails to start
|
|
381
|
+
* the reason a server fails to start: the sync path will raise anything that
|
|
382
382
|
* genuinely matters.
|
|
383
383
|
*/
|
|
384
384
|
export async function detectDrift(
|
package/src/sync/load.ts
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* `orm/index.ts`. A single `schema.ts` at the root still works, so nothing
|
|
7
7
|
* has to move.
|
|
8
8
|
*
|
|
9
|
-
* The folder is not only tidier
|
|
9
|
+
* The folder is not only tidier: it separates what the generator owns from
|
|
10
10
|
* what a person wrote. `--choose=db` regenerates tables; with everything in
|
|
11
11
|
* one file it has to rewrite indexes and foreign keys too, and anything
|
|
12
12
|
* hand-authored alongside them is collateral.
|
|
13
13
|
*
|
|
14
14
|
* Both of those are *defaults*, probed in that order from the app's cwd. An
|
|
15
|
-
* app that keeps its model somewhere else
|
|
16
|
-
* shared with another tool
|
|
15
|
+
* app that keeps its model somewhere else (`db/`, `src/database/`, a path
|
|
16
|
+
* shared with another tool) sets `schema` in `server.config.ts` and this
|
|
17
17
|
* stops guessing:
|
|
18
18
|
*
|
|
19
19
|
* ```ts
|
|
@@ -40,7 +40,7 @@ import type * as SyncTypes from './types'
|
|
|
40
40
|
* `folder` means `table()` values in `<dir>/schema.ts`, with `<dir>/index.ts`
|
|
41
41
|
* owning the re-exports and the schema registration. `file` and `none` mean the
|
|
42
42
|
* single-file `DBInfo` namespace, which carries its own registration block.
|
|
43
|
-
* Emitting the wrong one is not cosmetic
|
|
43
|
+
* Emitting the wrong one is not cosmetic. See `SchemaBuilder.generate`.
|
|
44
44
|
*/
|
|
45
45
|
export type SchemaLayout = 'folder' | 'file' | 'none'
|
|
46
46
|
|
|
@@ -53,7 +53,7 @@ export interface LoadedSchema {
|
|
|
53
53
|
*
|
|
54
54
|
* SQL forbids those. MySQL and Postgres refuse the CREATE outright; SQLite
|
|
55
55
|
* accepts the DDL and then fails *every insert* with `foreign key mismatch`,
|
|
56
|
-
* naming two tables and nothing else
|
|
56
|
+
* naming two tables and nothing else, so the caller aborts on this rather
|
|
57
57
|
* than letting it surface at runtime.
|
|
58
58
|
*/
|
|
59
59
|
unreferenceable?: string[]
|
|
@@ -62,7 +62,7 @@ export interface LoadedSchema {
|
|
|
62
62
|
layout: SchemaLayout
|
|
63
63
|
/**
|
|
64
64
|
* The path a configured `schema` pointed at that does not exist. Set only
|
|
65
|
-
* for that case
|
|
65
|
+
* for that case, never when nothing is configured, since absence is a
|
|
66
66
|
* supported state for the defaults. The caller must abort rather than sync.
|
|
67
67
|
*/
|
|
68
68
|
missing?: string
|
|
@@ -74,8 +74,8 @@ const CONSTRAINT_TYPES = new Set(['index', 'unique', 'foreign'])
|
|
|
74
74
|
* Foreign keys are declarable but not implemented, and the failure is nasty:
|
|
75
75
|
* no adapter emits FOREIGN KEY DDL, so a declaration is created as an ordinary
|
|
76
76
|
* index. The next diff then compares `foreign` in TypeScript against `index`
|
|
77
|
-
* in the database, decides to drop and re-add it, and
|
|
78
|
-
* count as destructive
|
|
77
|
+
* in the database, decides to drop and re-add it, and (because index drops
|
|
78
|
+
* count as destructive) aborts the sync. The dev server stops starting, with
|
|
79
79
|
* nothing pointing at the cause.
|
|
80
80
|
*
|
|
81
81
|
* Failing here converts that into one clear message. The alternative would be
|
|
@@ -110,7 +110,7 @@ function collectIndexes(module: Record<string, unknown>) {
|
|
|
110
110
|
*
|
|
111
111
|
* Takes the whole config object rather than the field so the one place that
|
|
112
112
|
* knows the config's shape is here. `unknown` because core's `AppConfig` does
|
|
113
|
-
* not declare `schema` yet
|
|
113
|
+
* not declare `schema` yet: `defaultConfig` in `core/core/config.ts` is
|
|
114
114
|
* annotated `Required<AppConfig>`, so declaring the field there forces a
|
|
115
115
|
* default value for it, and there is no single path to give: the default is
|
|
116
116
|
* the *probe*, not a location. Core spreads unknown keys from
|
|
@@ -182,7 +182,7 @@ type Resolved =
|
|
|
182
182
|
/**
|
|
183
183
|
* Turn a configured `schema` value into an entry file and a write target.
|
|
184
184
|
*
|
|
185
|
-
* Relative paths resolve against the app's cwd
|
|
185
|
+
* Relative paths resolve against the app's cwd: the framework's own location
|
|
186
186
|
* is irrelevant and, once it is an installed package, meaningless.
|
|
187
187
|
*
|
|
188
188
|
* A directory means the folder layout. So does a path ending in `index.ts`,
|
|
@@ -193,13 +193,13 @@ type Resolved =
|
|
|
193
193
|
/**
|
|
194
194
|
* Where the folder layout's table declarations live.
|
|
195
195
|
*
|
|
196
|
-
* `tables.ts`, beside `views.ts` and `indexes.ts
|
|
196
|
+
* `tables.ts`, beside `views.ts` and `indexes.ts`: one file per kind of
|
|
197
197
|
* declaration, which is the separation the folder layout exists for. It was
|
|
198
198
|
* `schema.ts`, which read oddly next to its siblings and collided with the
|
|
199
199
|
* single-file layout's `schema.ts` in conversation.
|
|
200
200
|
*
|
|
201
|
-
* The old name is still
|
|
202
|
-
* cared
|
|
201
|
+
* The old name is still honored when it is the one on disk. Loading never
|
|
202
|
+
* cared: that goes through `index.ts`'s re-exports, so any filename works,
|
|
203
203
|
* but *generation* writes here, and writing `tables.ts` beside someone's
|
|
204
204
|
* existing `schema.ts` would leave two files declaring the same tables.
|
|
205
205
|
*/
|
|
@@ -280,8 +280,8 @@ export async function loadSchema(
|
|
|
280
280
|
* cannot do on its own:
|
|
281
281
|
*
|
|
282
282
|
* 1. **Copies the referenced column's type onto the child.** MySQL rejects a
|
|
283
|
-
* foreign key whose types do not match exactly
|
|
284
|
-
* `BIGINT` parent is refused
|
|
283
|
+
* foreign key whose types do not match exactly (an `INT` child against a
|
|
284
|
+
* `BIGINT` parent is refused), and the two declarations are usually pages
|
|
285
285
|
* apart. Copying makes the mismatch unrepresentable rather than merely
|
|
286
286
|
* unlikely. `length` comes along for a `Varchar` key, for the same reason.
|
|
287
287
|
* 2. **Emits the key into the index map**, which is where `collectForeignKeys`
|
|
@@ -321,7 +321,7 @@ export function resolveColumnForeignKeys(
|
|
|
321
321
|
|
|
322
322
|
for (const [tableName, cols] of Object.entries(constraints)) {
|
|
323
323
|
// A view cannot carry a foreign key, and `view(name, sourceTable, body)`
|
|
324
|
-
// borrows the source table's columns
|
|
324
|
+
// borrows the source table's columns: `_references` included. Without this
|
|
325
325
|
// the view gets a key of its own, which no dialect will create, so every
|
|
326
326
|
// sync plans to add it again: an empty printed plan and a run that never
|
|
327
327
|
// reports a perfectly synced database.
|
package/src/sync/plan.ts
CHANGED
|
@@ -30,8 +30,7 @@ export async function buildSyncPlan(
|
|
|
30
30
|
dbConstraintsForDiff: {},
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// The one place sync decides what "currently" means. Prefers the ledger
|
|
34
|
-
// what Bakery last applied — and falls back to introspection whenever the
|
|
33
|
+
// The one place sync decides what "currently" means. Prefers the ledger ( // what Bakery last applied), and falls back to introspection whenever the
|
|
35
34
|
// ledger no longer describes the same tables and columns. See sync/ledger.ts
|
|
36
35
|
// for why that fallback is the whole safety argument.
|
|
37
36
|
const current = await resolveCurrentState(adapter, {
|
package/src/sync/rename.ts
CHANGED
|
@@ -125,7 +125,7 @@ export function handleTableRenames(
|
|
|
125
125
|
)
|
|
126
126
|
// Views are excluded, not merely absent. `initDbTablesMap` skips `_view`
|
|
127
127
|
// entries, so a view is never in `dbTables` and would look like a table that
|
|
128
|
-
// still needs creating
|
|
128
|
+
// still needs creating: on every run, forever. `evaluateChanges` counts
|
|
129
129
|
// `unmappedTsTables`, so a schema with a view could never report a perfectly
|
|
130
130
|
// synced database. The view phase creates them; this set is about tables.
|
|
131
131
|
plan.unmappedTsTables = new Set(
|
package/src/sync/rollback.ts
CHANGED
|
@@ -26,7 +26,7 @@ const rollbackMsgs = {
|
|
|
26
26
|
'W %ySchema #{id} predates the ledger recording indexes%*, so this rollback leaves indexes exactly as they are. Tables and columns roll back; index changes made since then do not.',
|
|
27
27
|
SCHEMA_REWRITTEN: 'I Rewrote %y{path}%* to match the restored schema.',
|
|
28
28
|
SCHEMA_KEPT:
|
|
29
|
-
'W %y--keep-schema: schema.ts still describes the newer schema.%* The next %ydb:sync%*
|
|
29
|
+
'W %y--keep-schema: schema.ts still describes the newer schema.%* The next %ydb:sync%* (including the one a dev boot runs) will apply it again and undo this rollback. Revert the file yourself, or re-run without the flag.',
|
|
30
30
|
DONE: 'I %gRollback complete%*.',
|
|
31
31
|
} as const
|
|
32
32
|
|
|
@@ -127,7 +127,7 @@ changes, exactly as db:sync does.
|
|
|
127
127
|
})
|
|
128
128
|
|
|
129
129
|
// A v1 row recorded no indexes, and replaying it with an empty index set
|
|
130
|
-
// would read as "drop every index"
|
|
130
|
+
// would read as "drop every index": a silent, permanent performance
|
|
131
131
|
// change dressed up as a rollback. Restoring the *live* indexes instead
|
|
132
132
|
// means the plan contains no index work at all, which is the honest
|
|
133
133
|
// degradation: it does less than asked, and says so.
|
|
@@ -163,7 +163,7 @@ changes, exactly as db:sync does.
|
|
|
163
163
|
// The whole point of the ledger: a rollback is just a sync whose target is
|
|
164
164
|
// a schema we already stored. Backups, the destructive-change prompt,
|
|
165
165
|
// `--dry-run`, `--force-sync` and the new ledger row all come from
|
|
166
|
-
// `SyncEngine.run` unchanged
|
|
166
|
+
// `SyncEngine.run` unchanged: there is no second migration path to keep
|
|
167
167
|
// correct, which is the only reason this file is as short as it is.
|
|
168
168
|
await SyncEngine.run(
|
|
169
169
|
adapter,
|
package/src/sync/types.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface ColumnConstraint {
|
|
|
15
15
|
* **Now part of the column diff**, so widening a `Varchar` migrates. This
|
|
16
16
|
* used to say the opposite, and the reason was sound at the time: it needed
|
|
17
17
|
* every adapter to report the width back exactly, and any that did not would
|
|
18
|
-
* rebuild the table on every sync. That is no longer a guess
|
|
18
|
+
* rebuild the table on every sync. That is no longer a guess: all three
|
|
19
19
|
* were measured against live servers, and the one real trap (MySQL reporting
|
|
20
20
|
* `character_maximum_length = 65535` for an unsized `TEXT`) is handled in
|
|
21
21
|
* `SQLAdapter.sizedTextLength`.
|
|
@@ -25,7 +25,7 @@ export interface ColumnConstraint {
|
|
|
25
25
|
*/
|
|
26
26
|
length?: number
|
|
27
27
|
/**
|
|
28
|
-
* The permitted values of an enum column
|
|
28
|
+
* The permitted values of an enum column: `Field.Enum([...])`.
|
|
29
29
|
*
|
|
30
30
|
* Not part of the column diff, for exactly the reason `length` is not:
|
|
31
31
|
* MySQL reports an ENUM's members back in its own spelling, Postgres reports
|
|
@@ -55,13 +55,29 @@ export interface IndexConstraint {
|
|
|
55
55
|
type: 'index' | 'unique' | 'foreign'
|
|
56
56
|
table: string
|
|
57
57
|
cols: string[]
|
|
58
|
+
/**
|
|
59
|
+
* The column names exactly as the database reported them, aligned with
|
|
60
|
+
* `cols` by position. Introspection always fills this; a TS-declared index
|
|
61
|
+
* (`Field.Unique`, `Field.Index`) has no database spelling to carry, which
|
|
62
|
+
* is why it is optional.
|
|
63
|
+
*
|
|
64
|
+
* It exists because `Case.snake` is not the inverse of `Case.camel`
|
|
65
|
+
* (`SKU_code` → `sKUCode` → `s_k_u_code`), so a consumer that needs the raw
|
|
66
|
+
* spelling (a statement, a display) cannot recover it from `cols` and had
|
|
67
|
+
* to rebuild a camel→raw map from the schema's column list. That rebuild was
|
|
68
|
+
* first-wins over collisions: a table holding both `user_id` and `userId`
|
|
69
|
+
* files them under one camel key, and an index over the *second* resolved to
|
|
70
|
+
* the first, a predicate over the wrong column. The adapter knows the real
|
|
71
|
+
* names at parse time; carrying them costs an array.
|
|
72
|
+
*/
|
|
73
|
+
rawCols?: string[]
|
|
58
74
|
/** Set only when `type` is 'foreign'. */
|
|
59
75
|
refTable?: string
|
|
60
76
|
refCols?: string[]
|
|
61
77
|
}
|
|
62
78
|
|
|
63
79
|
/**
|
|
64
|
-
* Referential actions,
|
|
80
|
+
* Referential actions, normalized to the SQL spelling.
|
|
65
81
|
*
|
|
66
82
|
* One vocabulary for three dialects: MySQL and SQLite report these words back
|
|
67
83
|
* verbatim, Postgres reports single characters (`c`, `a`, `r`, `n`, `d`) which
|
|
@@ -99,7 +115,7 @@ export interface ForeignKeyInfo {
|
|
|
99
115
|
*
|
|
100
116
|
* It lives here rather than beside `buildSyncPlan` because `sync/rename.ts` and
|
|
101
117
|
* `sync/diff.ts` both mutate a plan and are both imported *by* the planner. A
|
|
102
|
-
* home in `plan.ts` would make that pair of edges circular
|
|
118
|
+
* home in `plan.ts` would make that pair of edges circular: type-only, and so
|
|
103
119
|
* erased under `verbatimModuleSyntax`, but this repo has been bitten by import
|
|
104
120
|
* cycles often enough that not creating one is worth more than the adjacency.
|
|
105
121
|
*/
|