mikser-io 10.2.0 → 10.3.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/database/index.js +74 -10
package/package.json
CHANGED
package/src/database/index.js
CHANGED
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
import path from 'node:path'
|
|
46
46
|
import { mkdirSync, unlinkSync, existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
47
|
+
import { createHash } from 'node:crypto'
|
|
47
48
|
import Database from 'better-sqlite3'
|
|
48
49
|
import runtime from '../runtime.js'
|
|
49
50
|
import { isReportOnlyRun } from '../tools.js'
|
|
@@ -232,6 +233,43 @@ export function useDatabase() {
|
|
|
232
233
|
return db
|
|
233
234
|
}
|
|
234
235
|
|
|
236
|
+
// What the cache's SHAPE is, as a value that changes only when the cache
|
|
237
|
+
// stops being reusable.
|
|
238
|
+
//
|
|
239
|
+
// It used to be the package version, so every release wiped every
|
|
240
|
+
// deployment's cache and rebuilt from cold — a patch that touched a README
|
|
241
|
+
// discarded 14k entities and re-rendered a site. The version was standing in
|
|
242
|
+
// for "something might have changed", which is true of every release and
|
|
243
|
+
// therefore says nothing.
|
|
244
|
+
//
|
|
245
|
+
// Two things actually invalidate a cache, and both are in here:
|
|
246
|
+
//
|
|
247
|
+
// the SQL — a column, index or table that moved. Hashed from the
|
|
248
|
+
// registered scripts themselves, so it cannot drift from what
|
|
249
|
+
// is applied: the fingerprint and the schema come from one
|
|
250
|
+
// string.
|
|
251
|
+
// the SHAPE — what the rows MEAN, when the SQL is untouched. A change to
|
|
252
|
+
// how inputHash is computed, to the edge kinds in a
|
|
253
|
+
// refClosure, to what a destination is relative to. No parser
|
|
254
|
+
// can see these, so they are a number someone bumps.
|
|
255
|
+
//
|
|
256
|
+
// BUMP DERIVED_SHAPE when a release changes the meaning of anything already
|
|
257
|
+
// stored. Getting that wrong is a silent stale cache, which is the failure
|
|
258
|
+
// this whole subsystem keeps producing — so when in doubt, bump. A needless
|
|
259
|
+
// wipe costs one cold rebuild; a missed one costs a site serving wrong
|
|
260
|
+
// output with every signal green.
|
|
261
|
+
const DERIVED_SHAPE = 1
|
|
262
|
+
|
|
263
|
+
// `<shape>:<sql-hash>` rather than a bare hash: the stored value is read by
|
|
264
|
+
// a human when a wipe happens, and the two halves say WHICH moved.
|
|
265
|
+
function cacheFingerprint(registered) {
|
|
266
|
+
const sql = [...registered.entries()]
|
|
267
|
+
.map(([name, value]) => `${name}\n${schemaEntry(value).sql}`)
|
|
268
|
+
.sort()
|
|
269
|
+
.join('\n')
|
|
270
|
+
return `${DERIVED_SHAPE}:${createHash('sha1').update(sql).digest('hex').slice(0, 12)}`
|
|
271
|
+
}
|
|
272
|
+
|
|
235
273
|
// Build a sqlite-backed database handle. Exported so tests can exercise
|
|
236
274
|
// the lifecycle and transaction semantics in isolation (without driving
|
|
237
275
|
// the full onLoaded chain). The runtime path uses this internally from
|
|
@@ -296,6 +334,11 @@ export function createSqliteDatabase({
|
|
|
296
334
|
|
|
297
335
|
const stmtMeta = handle.prepare('SELECT value FROM mikser_meta WHERE key = ?')
|
|
298
336
|
const recorded = stmtMeta.get('schema_version')?.value
|
|
337
|
+
// The identity of the cache's shape, not of the release that wrote
|
|
338
|
+
// it. `version` is still carried — into `built_by_version` at stamp
|
|
339
|
+
// time — because "which mikser built this" is worth knowing when
|
|
340
|
+
// reading a cache; it is simply not a reason to throw one away.
|
|
341
|
+
const fingerprint = cacheFingerprint(schemas)
|
|
299
342
|
|
|
300
343
|
// A config change invalidates the cache for the same reason a version
|
|
301
344
|
// change does: the derived state was computed under different rules.
|
|
@@ -327,13 +370,13 @@ export function createSqliteDatabase({
|
|
|
327
370
|
const reportOnly = isReportOnlyRun()
|
|
328
371
|
|
|
329
372
|
let upgradedFromVersion = null
|
|
330
|
-
if (reportOnly && !forceWipe && ((recorded && recorded !==
|
|
373
|
+
if (reportOnly && !forceWipe && ((recorded && recorded !== fingerprint) || configChanged)) {
|
|
331
374
|
logger?.warn(
|
|
332
375
|
'The cache is stale (%s changed since it was written) and this is a read-only run, '
|
|
333
376
|
+ 'so it was NOT wiped — the answer below describes the last build, which may not '
|
|
334
377
|
+ 'match your sources. Run a build to refresh it.',
|
|
335
378
|
configChanged ? 'config' : 'schema version')
|
|
336
|
-
} else if (configChanged && !(recorded && recorded !==
|
|
379
|
+
} else if (configChanged && !(recorded && recorded !== fingerprint)) {
|
|
337
380
|
logger?.warn(
|
|
338
381
|
'Config changed since the last run. Wiping the cache and rebuilding from sources '
|
|
339
382
|
+ '(files are the source of truth — no source data is affected). The stamp covers %s and '
|
|
@@ -341,7 +384,7 @@ export function createSqliteDatabase({
|
|
|
341
384
|
runtime.options.config,
|
|
342
385
|
)
|
|
343
386
|
}
|
|
344
|
-
if (!reportOnly && (forceWipe || (recorded && recorded !==
|
|
387
|
+
if (!reportOnly && (forceWipe || (recorded && recorded !== fingerprint) || configChanged)) {
|
|
345
388
|
// Schema mismatch on upgrade or downgrade. Per ADR-0002 the
|
|
346
389
|
// files on disk are the source of truth and this database
|
|
347
390
|
// is a derived cache, so the right behavior is to wipe the
|
|
@@ -352,10 +395,27 @@ export function createSqliteDatabase({
|
|
|
352
395
|
// expect a cold-start rebuild on this run. No data loss
|
|
353
396
|
// beyond the cache itself; everything in mikser.sqlite is
|
|
354
397
|
// recoverable from the working folder.
|
|
355
|
-
if (recorded && recorded !==
|
|
398
|
+
if (recorded && recorded !== fingerprint) {
|
|
399
|
+
// Names WHICH half moved, because the two mean different
|
|
400
|
+
// things to whoever is reading. A shape bump is a deliberate
|
|
401
|
+
// decision someone made in this release; a SQL change is a
|
|
402
|
+
// table that moved. "stored=10.0.1, current=10.1.0" said
|
|
403
|
+
// neither, and could not — it only ever meant "a release
|
|
404
|
+
// happened".
|
|
405
|
+
const [storedShape, storedSql] = String(recorded).split(':')
|
|
406
|
+
const [shape, sql] = fingerprint.split(':')
|
|
407
|
+
const cause = storedSql === undefined
|
|
408
|
+
? 'the cache predates schema fingerprints'
|
|
409
|
+
: storedShape !== shape && storedSql !== sql
|
|
410
|
+
? 'the schema tables AND the meaning of what is stored both changed'
|
|
411
|
+
: storedShape !== shape
|
|
412
|
+
? 'this release changed the meaning of what is already stored'
|
|
413
|
+
: 'the schema tables changed'
|
|
356
414
|
logger?.warn(
|
|
357
|
-
'
|
|
358
|
-
|
|
415
|
+
'Cache shape changed — %s (stored=%s, current=%s, this is mikser %s). Wiping the cache '
|
|
416
|
+
+ 'and rebuilding from sources (files are the source of truth — no source data is '
|
|
417
|
+
+ 'affected). A release that changes neither reuses the cache.',
|
|
418
|
+
cause, recorded, fingerprint, version,
|
|
359
419
|
)
|
|
360
420
|
} else if (forceWipe) {
|
|
361
421
|
logger?.info('Clearing the cache and rebuilding from sources.')
|
|
@@ -365,8 +425,8 @@ export function createSqliteDatabase({
|
|
|
365
425
|
// the same output whether the version moved, the config moved, or
|
|
366
426
|
// someone passed --clear.
|
|
367
427
|
reportWipe(
|
|
368
|
-
recorded && recorded !==
|
|
369
|
-
recorded && recorded !==
|
|
428
|
+
recorded && recorded !== fingerprint ? 'version' : forceWipe ? 'clear' : 'config',
|
|
429
|
+
recorded && recorded !== fingerprint ? { from: recorded, to: fingerprint } : {},
|
|
370
430
|
)
|
|
371
431
|
|
|
372
432
|
// Unlink, rather than dropping table by table.
|
|
@@ -412,7 +472,7 @@ export function createSqliteDatabase({
|
|
|
412
472
|
// no stamp, so the next start sees a mismatch and rebuilds properly.
|
|
413
473
|
// config_checksum goes with it for the same reason — a config change
|
|
414
474
|
// that half-applied must not look applied.
|
|
415
|
-
pendingStamp = { version, config: currentConfig }
|
|
475
|
+
pendingStamp = { fingerprint, version, config: currentConfig }
|
|
416
476
|
|
|
417
477
|
// Build provisioning context. firstRun is true when the file
|
|
418
478
|
// didn't exist before this open OR when the schema mismatch
|
|
@@ -528,7 +588,11 @@ export function createSqliteDatabase({
|
|
|
528
588
|
commitStamp() {
|
|
529
589
|
if (!pendingStamp || !handle) return false
|
|
530
590
|
const stamp = handle.prepare('INSERT OR REPLACE INTO mikser_meta (key, value) VALUES (?, ?)')
|
|
531
|
-
stamp.run('schema_version', pendingStamp.
|
|
591
|
+
stamp.run('schema_version', pendingStamp.fingerprint)
|
|
592
|
+
// Which release wrote this cache. Recorded because it is the
|
|
593
|
+
// first thing a person wants when reading one, and deliberately
|
|
594
|
+
// NOT compared against anything — that was the bug.
|
|
595
|
+
stamp.run('built_by_version', pendingStamp.version)
|
|
532
596
|
if (pendingStamp.config) stamp.run('config_checksum', pendingStamp.config)
|
|
533
597
|
pendingStamp = null
|
|
534
598
|
runtime.options.cacheRebuildInterrupted = false
|