@bakery-framework/orm 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/sync/helpers.ts +26 -0
- package/src/sync/ledger.ts +80 -19
package/package.json
CHANGED
package/src/sync/helpers.ts
CHANGED
|
@@ -260,10 +260,36 @@ function diffColumnMismatch(
|
|
|
260
260
|
const lengthDiffers =
|
|
261
261
|
typeof tsCol.length === 'number' && tsCol.length !== dbCol.length
|
|
262
262
|
|
|
263
|
+
// Enum members join the diff, so changing them migrates instead of silently
|
|
264
|
+
// doing nothing — but **only when the current state came from the ledger**.
|
|
265
|
+
//
|
|
266
|
+
// `_enum` is emitted as an inline `CHECK (col IN (...))` by all three
|
|
267
|
+
// dialects, and all three *will* report that constraint back — in three
|
|
268
|
+
// incompatible shapes. Measured:
|
|
269
|
+
//
|
|
270
|
+
// sqlite CHECK (status IN ('draft','live')) in the table DDL
|
|
271
|
+
// mysql (`status` in (_utf8mb4'draft',_utf8mb4'live')) charset prefixes
|
|
272
|
+
// pgsql CHECK (((status)::text = ANY ((ARRAY[...]))) re-rendered
|
|
273
|
+
//
|
|
274
|
+
// Postgres does not store the text it was given, it re-renders a parsed
|
|
275
|
+
// expression — the same trap that turned `EXTRACT` into `date_part` and
|
|
276
|
+
// rebuilt a table on every sync forever. Three parsers, each an opportunity
|
|
277
|
+
// for that bug, is the wrong trade when the ledger already holds the members
|
|
278
|
+
// exactly as declared.
|
|
279
|
+
//
|
|
280
|
+
// So under introspection this stays out of the diff. A schema-side-only
|
|
281
|
+
// comparison would find `_enum` on one side and nothing on the other, differ
|
|
282
|
+
// every time, and rebuild the table on every sync — which is precisely what
|
|
283
|
+
// the `length` note above says it waited to rule out before shipping.
|
|
284
|
+
const enumDiffers =
|
|
285
|
+
plan.ledgerSource === 'ledger' &&
|
|
286
|
+
!Bun.deepEquals(tsCol._enum ?? null, dbCol._enum ?? null)
|
|
287
|
+
|
|
263
288
|
if (
|
|
264
289
|
!isTypeMatch ||
|
|
265
290
|
tsNullable !== dbNullable ||
|
|
266
291
|
lengthDiffers ||
|
|
292
|
+
enumDiffers ||
|
|
267
293
|
norm(tsDefault) !== norm(dbDefault)
|
|
268
294
|
) {
|
|
269
295
|
MESSAGES.COL_MISMATCH({ table: dbName, column: camelCol })
|
package/src/sync/ledger.ts
CHANGED
|
@@ -216,26 +216,50 @@ export function stripLedger(
|
|
|
216
216
|
* same normalisation 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
|
+
*
|
|
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 — whatever you passed to
|
|
222
|
+
* `table()` / `view()` — while `getConstraints()` camelCases everything it reads
|
|
223
|
+
* back. So a table declared `view('published_posts', …)` is `published_posts` in
|
|
224
|
+
* the ledger and `publishedPosts` from introspection, and this check called that
|
|
225
|
+
* a drifted database: *"tables differ (+publishedPosts; -published_posts)"*, on
|
|
226
|
+
* a database nothing had touched. Permanently — the spellings never converge, so
|
|
227
|
+
* every later sync re-reported it and the ledger was never used again. Every app
|
|
228
|
+
* `bun create bakery` generated hit it on the first `db:sync`, because the
|
|
229
|
+
* generated schema declares exactly that view.
|
|
230
|
+
*
|
|
231
|
+
* The damage is quieter than the warning. Falling back to introspection is
|
|
232
|
+
* *safe*, so nothing breaks loudly — it just silently withdraws the thing the
|
|
233
|
+
* ledger is for. Enum member changes, for one, only migrate when the diff runs
|
|
234
|
+
* against the ledger (`plan.ledgerSource === 'ledger'`), so on any such app that
|
|
235
|
+
* feature was inert.
|
|
236
|
+
*
|
|
237
|
+
* Comparing camel-normalised names fixes it at the one place the two spellings
|
|
238
|
+
* meet. `LEDGER_ALIASES` below is the same bug, found earlier and patched for a
|
|
239
|
+
* single known name; this is the general form of it.
|
|
219
240
|
*/
|
|
220
241
|
export function shapesMatch(
|
|
221
242
|
ledger: SyncTypes.DBConstraints,
|
|
222
243
|
live: SyncTypes.DBConstraints,
|
|
223
244
|
): { ok: true } | { ok: false; reason: string } {
|
|
224
245
|
const meta = (k: string) => k.startsWith('_')
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
Object.keys(
|
|
231
|
-
|
|
232
|
-
|
|
246
|
+
// Compare on the normalised spelling, report the declared one — a diff that
|
|
247
|
+
// named tables the reader cannot find in either their schema or their database
|
|
248
|
+
// would trade one confusion for another.
|
|
249
|
+
const namesOf = (o: object) => {
|
|
250
|
+
const out = new Map<string, string>()
|
|
251
|
+
for (const k of Object.keys(o ?? {})) {
|
|
252
|
+
if (!meta(k)) out.set(Case.camel(k), k)
|
|
253
|
+
}
|
|
254
|
+
return out
|
|
255
|
+
}
|
|
256
|
+
const keysOf = (o: object) => [...namesOf(o).keys()].sort()
|
|
233
257
|
|
|
234
|
-
const a =
|
|
235
|
-
const b =
|
|
236
|
-
if (
|
|
237
|
-
const added = b.filter(
|
|
238
|
-
const gone = a.filter(
|
|
258
|
+
const a = namesOf(ledger)
|
|
259
|
+
const b = namesOf(live)
|
|
260
|
+
if (keysOf(ledger).join() !== keysOf(live).join()) {
|
|
261
|
+
const added = [...b].filter(([k]) => !a.has(k)).map(([, name]) => name)
|
|
262
|
+
const gone = [...a].filter(([k]) => !b.has(k)).map(([, name]) => name)
|
|
239
263
|
return {
|
|
240
264
|
ok: false,
|
|
241
265
|
reason: `tables differ (${added.length ? `+${added.join(', ')}` : ''}${
|
|
@@ -244,16 +268,52 @@ export function shapesMatch(
|
|
|
244
268
|
}
|
|
245
269
|
}
|
|
246
270
|
|
|
247
|
-
for (const
|
|
248
|
-
const lc =
|
|
249
|
-
const dc =
|
|
271
|
+
for (const [key, name] of a) {
|
|
272
|
+
const lc = keysOf((ledger as any)[name])
|
|
273
|
+
const dc = keysOf((live as any)[b.get(key) as string])
|
|
250
274
|
if (lc.join() !== dc.join()) {
|
|
251
|
-
return { ok: false, reason: `columns of ${
|
|
275
|
+
return { ok: false, reason: `columns of ${name} differ` }
|
|
252
276
|
}
|
|
253
277
|
}
|
|
254
278
|
return { ok: true }
|
|
255
279
|
}
|
|
256
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Re-key a ledger payload the way introspection keys its own.
|
|
283
|
+
*
|
|
284
|
+
* The ledger stores the keys of your TypeScript schema verbatim; every consumer
|
|
285
|
+
* of "current state" downstream looks tables up by `Case.camel(name)`, because
|
|
286
|
+
* that is what `getConstraints()` produces. Handing the raw ledger to the
|
|
287
|
+
* planner therefore made every lookup miss — `diffViews` asked for
|
|
288
|
+
* `publishedPosts`, the ledger held `published_posts`, and a miss reads as "the
|
|
289
|
+
* database does not have this view", so the view was recreated on every single
|
|
290
|
+
* sync. Silent and harmless-looking; a view holds no data, so the only symptom
|
|
291
|
+
* is churn in the log.
|
|
292
|
+
*
|
|
293
|
+
* Normalising on read rather than on write is deliberate: it repairs the ledgers
|
|
294
|
+
* already written by earlier versions, which a write-side fix could not.
|
|
295
|
+
*
|
|
296
|
+
* Meta keys (`_view`, `_references`, …) are values, not identifiers, and are
|
|
297
|
+
* copied through untouched.
|
|
298
|
+
*/
|
|
299
|
+
function normalizeLedgerKeys(
|
|
300
|
+
constraints: SyncTypes.DBConstraints,
|
|
301
|
+
): SyncTypes.DBConstraints {
|
|
302
|
+
const out: any = {}
|
|
303
|
+
for (const [table, cols] of Object.entries(constraints)) {
|
|
304
|
+
if (table.startsWith('_') || !cols || typeof cols !== 'object') {
|
|
305
|
+
out[table] = cols
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
308
|
+
const next: any = {}
|
|
309
|
+
for (const [col, def] of Object.entries(cols)) {
|
|
310
|
+
next[col.startsWith('_') ? col : Case.camel(col)] = def
|
|
311
|
+
}
|
|
312
|
+
out[Case.camel(table)] = next
|
|
313
|
+
}
|
|
314
|
+
return out
|
|
315
|
+
}
|
|
316
|
+
|
|
257
317
|
/**
|
|
258
318
|
* The state sync should diff against: the ledger when it is still true of the
|
|
259
319
|
* database, introspection otherwise.
|
|
@@ -289,14 +349,15 @@ export async function resolveCurrentState(
|
|
|
289
349
|
reason: 'ledger ignored (--no-ledger)',
|
|
290
350
|
}
|
|
291
351
|
}
|
|
292
|
-
const
|
|
293
|
-
if (!
|
|
352
|
+
const raw = await readLedger(adapter)
|
|
353
|
+
if (!raw)
|
|
294
354
|
return {
|
|
295
355
|
constraints: live,
|
|
296
356
|
source: 'introspection',
|
|
297
357
|
reason: 'no ledger yet',
|
|
298
358
|
}
|
|
299
359
|
|
|
360
|
+
const ledger = normalizeLedgerKeys(raw)
|
|
300
361
|
const match = shapesMatch(ledger, live)
|
|
301
362
|
if (!match.ok) {
|
|
302
363
|
return { constraints: live, source: 'introspection', reason: match.reason }
|