mikser-io 9.57.0 → 9.58.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/CLAUDE.md +5 -4
- package/docs/decisions/0009-database-engine-substrate.md +7 -1
- package/index.js +2 -1
- package/package.json +1 -1
- package/src/database/durable.js +1 -127
package/CLAUDE.md
CHANGED
|
@@ -142,10 +142,11 @@ brevity.
|
|
|
142
142
|
schema_version). Nothing durable lives here any more.
|
|
143
143
|
- `database/durable.js` — the durable store. `registerMigrations()`,
|
|
144
144
|
`useDurableDatabase()` (a knex instance), `runMigrations()`,
|
|
145
|
-
`closeDurableDatabase()`. `
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
`closeDurableDatabase()`. `ensureIgnored` adds the file to `.gitignore`
|
|
146
|
+
(it holds credentials and the working folder is usually a repo).
|
|
147
|
+
**No upgrade path from the pre-split layout** — per the posture above,
|
|
148
|
+
a working folder from before 9.56 loses its grants and change-set log
|
|
149
|
+
to the ordinary cache wipe, and everyone signs in again once.
|
|
149
150
|
`sift-to-sql.js` translates sift filters to SQL WHERE clauses
|
|
150
151
|
against `INDEXED_COLUMNS`; un-pushed clauses fall through to
|
|
151
152
|
JS-side sift. `query-context.js` is the AsyncLocalStorage that
|
|
@@ -101,7 +101,13 @@ boot can never add a column, rename one, or backfill a value; that is invisible
|
|
|
101
101
|
for a cache table and the permanent condition of a durable one.
|
|
102
102
|
|
|
103
103
|
`registerSchema(name, sql, { durable: true })` now THROWS, pointing at
|
|
104
|
-
`registerMigrations`.
|
|
104
|
+
`registerMigrations`.
|
|
105
|
+
|
|
106
|
+
No upgrade path from the single-file layout, deliberately. Until v10 mikser
|
|
107
|
+
carries no back-compat, and a one-shot migration for a shape that will never
|
|
108
|
+
exist again is exactly the code that shape does not earn. A working folder
|
|
109
|
+
written before 9.56 loses its grants and change-set log to the ordinary cache
|
|
110
|
+
wipe: everyone signs in once more, and the change-set history starts empty. Accepting it would put the table in the file that gets
|
|
105
111
|
deleted, and the first sign would be an operator asked to sign in again.
|
|
106
112
|
|
|
107
113
|
For the cache: WAL mode + `synchronous=NORMAL` +
|
package/index.js
CHANGED
|
@@ -10,12 +10,13 @@ export * from './src/report.js'
|
|
|
10
10
|
// CLI formats, rather than each one reimplementing the question.
|
|
11
11
|
export * from './src/explain.js'
|
|
12
12
|
export * from './src/lifecycle.js'
|
|
13
|
-
export * from './src/database/index.js'
|
|
14
13
|
// The durable store — registerMigrations / useDurableDatabase. A separate
|
|
15
14
|
// module from the cache because it is a separate database with none of the
|
|
16
15
|
// cache's constraints: main-thread only, so async, so knex, so portable to
|
|
17
16
|
// another engine.
|
|
18
17
|
export * from './src/database/durable.js'
|
|
18
|
+
|
|
19
|
+
export * from './src/database/index.js'
|
|
19
20
|
export * from './src/journal.js'
|
|
20
21
|
export * from './src/catalog.js'
|
|
21
22
|
export * from './src/search.js'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikser-io",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.58.0",
|
|
4
4
|
"description": "A mixer for content: entities in, configurable render pipelines, outputs of any kind. Static sites are the canonical recipe, not the definition — the same engine renders PDFs, emails and whatever a renderer plugin produces. Files are the source of truth, every lifecycle phase is observable, and the build graph is queryable by an agent.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"exports": {
|
package/src/database/durable.js
CHANGED
|
@@ -28,8 +28,7 @@
|
|
|
28
28
|
// ordered list of migrations instead, applied once and recorded.
|
|
29
29
|
|
|
30
30
|
import path from 'node:path'
|
|
31
|
-
import {
|
|
32
|
-
import Database from 'better-sqlite3'
|
|
31
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
33
32
|
import knexFactory from 'knex'
|
|
34
33
|
|
|
35
34
|
import runtime from '../runtime.js'
|
|
@@ -190,106 +189,6 @@ export async function runMigrations(knex, logger = useLogger()) {
|
|
|
190
189
|
return applied
|
|
191
190
|
}
|
|
192
191
|
|
|
193
|
-
// Carry a durable table out of a cache file written before the split.
|
|
194
|
-
//
|
|
195
|
-
// UPGRADE PATH, and the one piece of this module that knows it is talking
|
|
196
|
-
// to sqlite. It has to: moving rows between two files is ATTACH, and there
|
|
197
|
-
// is no portable spelling of it. Gated on the durable store actually being
|
|
198
|
-
// a local sqlite file, skipped otherwise, and deletable once no deployment
|
|
199
|
-
// predates 9.56.
|
|
200
|
-
//
|
|
201
|
-
// Which tables those are is not guessed. The previous design recorded the
|
|
202
|
-
// names in `mikser_meta.durable_tables` — it had to, so that a config which
|
|
203
|
-
// never loaded the owning plugin would not unlink them — and that record is
|
|
204
|
-
// exactly the upgrade instruction needed here. Matching on names alone would
|
|
205
|
-
// be worse than useless: a cache table that happens to share a name with some
|
|
206
|
-
// plugin's durable table would be moved and dropped, which is destroying data
|
|
207
|
-
// on the strength of a coincidence.
|
|
208
|
-
//
|
|
209
|
-
// Runs on the DURABLE connection with the cache attached, rather than the
|
|
210
|
-
// other way round, so a table the schemas did not create can be rebuilt by
|
|
211
|
-
// replaying the CREATE sqlite itself stored — unqualified, landing in durable
|
|
212
|
-
// because that is this connection's main. The alternative was rewriting the
|
|
213
|
-
// stored DDL to insert a schema prefix, and parsing DDL is what produced
|
|
214
|
-
// columns named `and`, `a` and `which` on a live deployment.
|
|
215
|
-
//
|
|
216
|
-
// The record is cleared once the move succeeds, so this is a one-way door
|
|
217
|
-
// that runs once per working folder.
|
|
218
|
-
export function adoptFromCache(durable, cachePath, logger) {
|
|
219
|
-
if (!cachePath || cachePath === ':memory:' || !existsSync(cachePath)) return
|
|
220
|
-
|
|
221
|
-
let recorded = []
|
|
222
|
-
try {
|
|
223
|
-
durable.exec(`ATTACH DATABASE '${cachePath.replace(/'/g, "''")}' AS cache`)
|
|
224
|
-
const row = durable.prepare('SELECT value FROM cache.mikser_meta WHERE key = ?').get('durable_tables')
|
|
225
|
-
const parsed = row?.value ? JSON.parse(row.value) : []
|
|
226
|
-
recorded = Array.isArray(parsed) ? parsed.filter(name => typeof name === 'string') : []
|
|
227
|
-
} catch {
|
|
228
|
-
try { durable.exec('DETACH DATABASE cache') } catch { /* never attached */ }
|
|
229
|
-
return // no cache, or one that predates the record — nothing to carry
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
try {
|
|
233
|
-
const present = new Set(durable.prepare(
|
|
234
|
-
"SELECT name FROM cache.sqlite_master WHERE type='table'").all().map(r => r.name))
|
|
235
|
-
const mine = new Set(durable.prepare(
|
|
236
|
-
"SELECT name FROM main.sqlite_master WHERE type='table'").all().map(r => r.name))
|
|
237
|
-
|
|
238
|
-
for (const table of recorded) {
|
|
239
|
-
if (table === 'mikser_meta' || !present.has(table)) continue
|
|
240
|
-
try {
|
|
241
|
-
// A table no loaded plugin declares still has to come across —
|
|
242
|
-
// that is the case the record exists for. Rebuilt from the
|
|
243
|
-
// DDL sqlite kept, so no schema has to be reconstructed.
|
|
244
|
-
if (!mine.has(table)) {
|
|
245
|
-
const { sql } = durable.prepare(
|
|
246
|
-
"SELECT sql FROM cache.sqlite_master WHERE type='table' AND name = ?").get(table) ?? {}
|
|
247
|
-
if (!sql) continue
|
|
248
|
-
durable.exec(sql)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const columnsOf = (schema) =>
|
|
252
|
-
durable.prepare(`PRAGMA ${schema}.table_info("${table}")`).all().map(c => c.name)
|
|
253
|
-
// Intersected rather than trusting `SELECT *`: the shape in
|
|
254
|
-
// the cache is by definition the OLD one, and lining the two
|
|
255
|
-
// up wrong would put values in the wrong columns.
|
|
256
|
-
const source = new Set(columnsOf('cache'))
|
|
257
|
-
const shared = columnsOf('main').filter(c => source.has(c))
|
|
258
|
-
const list = shared.map(c => `"${c}"`).join(', ')
|
|
259
|
-
|
|
260
|
-
const moved = durable.transaction(() => {
|
|
261
|
-
const { n } = durable.prepare(`SELECT COUNT(*) AS n FROM cache."${table}"`).get()
|
|
262
|
-
if (n && shared.length) {
|
|
263
|
-
durable.exec(
|
|
264
|
-
`INSERT OR IGNORE INTO main."${table}" (${list}) SELECT ${list} FROM cache."${table}"`)
|
|
265
|
-
}
|
|
266
|
-
durable.exec(`DROP TABLE cache."${table}"`)
|
|
267
|
-
return n
|
|
268
|
-
})()
|
|
269
|
-
|
|
270
|
-
logger?.notice('Moved %s out of the cache and into %s (%d row%s)',
|
|
271
|
-
table, DEFAULT_DURABLE_FILENAME, moved, moved === 1 ? '' : 's')
|
|
272
|
-
} catch (err) {
|
|
273
|
-
// Loud, and specifically not fatal: the rows are still in the
|
|
274
|
-
// cache, which means the next wipe takes them. That is a
|
|
275
|
-
// warning an operator can act on; silence is not.
|
|
276
|
-
logger?.error({ code: 'durable-adopt' },
|
|
277
|
-
'Could not move %s into the durable database: %s. Its rows are still in the cache, so the '
|
|
278
|
-
+ 'next wipe will delete them.', table, err.message)
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Cleared last, and only here. While it is present this runs again,
|
|
283
|
-
// which is what makes a partial move recoverable on the next start.
|
|
284
|
-
durable.prepare('DELETE FROM cache.mikser_meta WHERE key = ?').run('durable_tables')
|
|
285
|
-
} catch (err) {
|
|
286
|
-
logger?.error({ code: 'durable-adopt' },
|
|
287
|
-
'Could not read the durable-table record out of the cache: %s', err.message)
|
|
288
|
-
} finally {
|
|
289
|
-
try { durable.exec('DETACH DATABASE cache') } catch { /* already detached */ }
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
192
|
// Keep the durable database out of the repository.
|
|
294
193
|
//
|
|
295
194
|
// It sits at the working-folder root — deliberately outside runtime/, which
|
|
@@ -324,26 +223,6 @@ export function ensureIgnored(folder, filename, logger) {
|
|
|
324
223
|
}
|
|
325
224
|
}
|
|
326
225
|
|
|
327
|
-
// Carry pre-split tables across, on a connection of its own.
|
|
328
|
-
//
|
|
329
|
-
// Raw better-sqlite3 rather than the knex instance because the move is ATTACH,
|
|
330
|
-
// which knex has no portable spelling for — and because this is an upgrade
|
|
331
|
-
// path with an end date, not part of how the store works. After migrations, so
|
|
332
|
-
// the tables it copies into exist with their current shape.
|
|
333
|
-
function adoptPreSplitTables(durablePath, cachePath, logger) {
|
|
334
|
-
if (!existsSync(cachePath)) return
|
|
335
|
-
let handle = null
|
|
336
|
-
try {
|
|
337
|
-
handle = new Database(durablePath)
|
|
338
|
-
adoptFromCache(handle, cachePath, logger)
|
|
339
|
-
} catch (err) {
|
|
340
|
-
logger?.error({ code: 'durable-adopt' },
|
|
341
|
-
'Could not carry pre-split tables out of the cache: %s', err.message)
|
|
342
|
-
} finally {
|
|
343
|
-
try { handle?.close() } catch { /* already closed */ }
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
226
|
onLoaded(async () => {
|
|
348
227
|
if (db) return // watch mode keeps the connection across cycles
|
|
349
228
|
if (!registry.size) return // nothing durable is registered; open nothing
|
|
@@ -370,11 +249,6 @@ onLoaded(async () => {
|
|
|
370
249
|
return
|
|
371
250
|
}
|
|
372
251
|
|
|
373
|
-
// Only a local sqlite target can have a pre-split cache beside it.
|
|
374
|
-
if (filename && runtime.options.runtimeFolder) {
|
|
375
|
-
adoptPreSplitTables(filename, path.join(runtime.options.runtimeFolder, 'mikser.sqlite'), logger)
|
|
376
|
-
}
|
|
377
|
-
|
|
378
252
|
runtime.durable = db
|
|
379
253
|
logger?.info('Durable store ready: %s (%s)', filename ?? 'remote', config.client)
|
|
380
254
|
})
|