mikser-io 9.49.0 → 9.49.1
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/changeset.js +30 -3
- package/src/database/index.js +79 -1
package/package.json
CHANGED
package/src/changeset.js
CHANGED
|
@@ -185,6 +185,20 @@ function prune(handle) {
|
|
|
185
185
|
`).run(KEEP_SETS)
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
// Said once per process, not once per write: a broken log is one condition,
|
|
189
|
+
// and repeating it per write buries the builds that follow it.
|
|
190
|
+
let failureReported = false
|
|
191
|
+
function reportChangeSetFailure(err) {
|
|
192
|
+
if (failureReported) return
|
|
193
|
+
failureReported = true
|
|
194
|
+
const message = 'The change-set log could not be written (%s). Writes still land on disk, but they cannot be '
|
|
195
|
+
+ 'listed or undone until this is fixed.'
|
|
196
|
+
try {
|
|
197
|
+
runtime.engine?.logger?.error(message, err.message)
|
|
198
|
+
} catch { /* no logger yet — the console is what is left */ }
|
|
199
|
+
if (!runtime.engine?.logger) console.error(message.replace('%s', err.message))
|
|
200
|
+
}
|
|
201
|
+
|
|
188
202
|
function rowsToSets(handle, rows) {
|
|
189
203
|
const stmt = handle.prepare(
|
|
190
204
|
'SELECT path, operation FROM mikser_change_set_paths WHERE change_set = ? ORDER BY path')
|
|
@@ -283,7 +297,16 @@ export function recordChangeSetWrite({
|
|
|
283
297
|
|
|
284
298
|
const handle = db()
|
|
285
299
|
if (handle) {
|
|
286
|
-
try {
|
|
300
|
+
try {
|
|
301
|
+
persist(handle, set, rel, operation, entityId)
|
|
302
|
+
} catch (err) {
|
|
303
|
+
// Loud. A swallowed failure here is invisible in exactly the way
|
|
304
|
+
// that matters: the write succeeds, an id comes back, and the log
|
|
305
|
+
// it points at silently never gains a row — which is how a stale
|
|
306
|
+
// column shape turned the whole feature into a no-op that looked
|
|
307
|
+
// like it was working.
|
|
308
|
+
reportChangeSetFailure(err)
|
|
309
|
+
}
|
|
287
310
|
}
|
|
288
311
|
return set.id
|
|
289
312
|
}
|
|
@@ -298,7 +321,9 @@ export function pendingChangeSets() {
|
|
|
298
321
|
SELECT * FROM mikser_change_sets WHERE recorded_at IS NULL ORDER BY created_at ASC
|
|
299
322
|
`).all()
|
|
300
323
|
return rowsToSets(handle, rows).filter(set => set.paths.length)
|
|
301
|
-
} catch {
|
|
324
|
+
} catch (err) {
|
|
325
|
+
reportChangeSetFailure(err)
|
|
326
|
+
}
|
|
302
327
|
}
|
|
303
328
|
return memorySets(set => !set.recordedAt).sort((a, b) => a.startedAt - b.startedAt)
|
|
304
329
|
}
|
|
@@ -313,7 +338,9 @@ export function listChangeSets({ limit = 20 } = {}) {
|
|
|
313
338
|
SELECT * FROM mikser_change_sets ORDER BY created_at DESC LIMIT ?
|
|
314
339
|
`).all(Math.max(1, Math.min(limit, 200)))
|
|
315
340
|
return rowsToSets(handle, rows)
|
|
316
|
-
} catch {
|
|
341
|
+
} catch (err) {
|
|
342
|
+
reportChangeSetFailure(err)
|
|
343
|
+
}
|
|
317
344
|
}
|
|
318
345
|
return memorySets().sort((a, b) => b.startedAt - a.startedAt).slice(0, limit)
|
|
319
346
|
}
|
package/src/database/index.js
CHANGED
|
@@ -104,6 +104,78 @@ let db = null
|
|
|
104
104
|
// duplicate detection. Same name twice = the later registration wins
|
|
105
105
|
// (with a warning). Convention: `<owner>` matching the table prefix
|
|
106
106
|
// (`catalog`, `manifest`, `vector`, etc.).
|
|
107
|
+
// Column names a CREATE TABLE body declares, in order.
|
|
108
|
+
//
|
|
109
|
+
// Only the leading identifier of each top-level comma-separated clause, and
|
|
110
|
+
// only when it is not a table constraint. Good enough for the schemas this
|
|
111
|
+
// engine registers, and deliberately not a SQL parser.
|
|
112
|
+
function columnsFrom(sqlScript, table) {
|
|
113
|
+
const re = new RegExp(
|
|
114
|
+
`CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?[\`"'\\[]?${table}[\`"'\\]]?\\s*\\(`, 'i')
|
|
115
|
+
const m = re.exec(sqlScript)
|
|
116
|
+
if (!m) return []
|
|
117
|
+
// Walk to the matching close paren so nested types and CHECK(...) do not
|
|
118
|
+
// end the body early.
|
|
119
|
+
let depth = 1
|
|
120
|
+
let i = m.index + m[0].length
|
|
121
|
+
const start = i
|
|
122
|
+
for (; i < sqlScript.length && depth > 0; i++) {
|
|
123
|
+
if (sqlScript[i] === '(') depth++
|
|
124
|
+
else if (sqlScript[i] === ')') depth--
|
|
125
|
+
}
|
|
126
|
+
const body = sqlScript.slice(start, i - 1)
|
|
127
|
+
|
|
128
|
+
const clauses = []
|
|
129
|
+
let current = ''
|
|
130
|
+
depth = 0
|
|
131
|
+
for (const ch of body) {
|
|
132
|
+
if (ch === '(') depth++
|
|
133
|
+
else if (ch === ')') depth--
|
|
134
|
+
if (ch === ',' && depth === 0) { clauses.push(current); current = '' } else current += ch
|
|
135
|
+
}
|
|
136
|
+
clauses.push(current)
|
|
137
|
+
|
|
138
|
+
const CONSTRAINTS = new Set(['primary', 'unique', 'foreign', 'check', 'constraint'])
|
|
139
|
+
return clauses
|
|
140
|
+
.map(clause => clause.replace(/--[^\n]*/g, '').trim())
|
|
141
|
+
.filter(Boolean)
|
|
142
|
+
.map(clause => clause.split(/\s+/)[0].replace(/["`\[\]]/g, ''))
|
|
143
|
+
.filter(name => name && !CONSTRAINTS.has(name.toLowerCase()))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Add columns a DURABLE table has grown since it was created.
|
|
147
|
+
//
|
|
148
|
+
// `CREATE TABLE IF NOT EXISTS` does nothing to a table that already exists, so
|
|
149
|
+
// re-applying a schema never adds a column. For a cache table that is
|
|
150
|
+
// invisible: the version bump wipes and recreates it. A durable table is
|
|
151
|
+
// exactly the one that SURVIVES the wipe, so it is the only kind that can go
|
|
152
|
+
// stale — and it goes stale silently, with every insert naming the new column
|
|
153
|
+
// failing against a table that still has the old shape.
|
|
154
|
+
function migrateDurableColumns(handle, schemas, logger) {
|
|
155
|
+
for (const value of schemas?.values?.() ?? []) {
|
|
156
|
+
const { sql, durable } = schemaEntry(value)
|
|
157
|
+
if (!durable) continue
|
|
158
|
+
for (const table of tableNamesFrom(sql)) {
|
|
159
|
+
let existing
|
|
160
|
+
try {
|
|
161
|
+
existing = new Set(handle.prepare(`PRAGMA table_info("${table}")`).all().map(c => c.name))
|
|
162
|
+
} catch { continue }
|
|
163
|
+
if (!existing.size) continue
|
|
164
|
+
for (const column of columnsFrom(sql, table)) {
|
|
165
|
+
if (existing.has(column)) continue
|
|
166
|
+
// Only ever ADD. Dropping or retyping a column in a durable
|
|
167
|
+
// table would discard data the whole flag exists to keep.
|
|
168
|
+
try {
|
|
169
|
+
handle.exec(`ALTER TABLE "${table}" ADD COLUMN "${column}"`)
|
|
170
|
+
logger?.info('Durable table %s gained column %s', table, column)
|
|
171
|
+
} catch (err) {
|
|
172
|
+
logger?.warn('Could not add column %s to %s: %s', column, table, err.message)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
107
179
|
// Table names a schema script creates. Used to decide what a cache wipe
|
|
108
180
|
// must leave alone — the registry knows schema NAMES, and the wipe works in
|
|
109
181
|
// tables.
|
|
@@ -498,7 +570,8 @@ export function createSqliteDatabase({
|
|
|
498
570
|
}
|
|
499
571
|
|
|
500
572
|
// Apply each subsystem's registered schema script. Idempotent
|
|
501
|
-
// CREATE statements mean replay-safe across opens
|
|
573
|
+
// CREATE statements mean replay-safe across opens — but only for
|
|
574
|
+
// tables they can recreate; see migrateDurableColumns below.
|
|
502
575
|
for (const [name, value] of schemas) {
|
|
503
576
|
const { sql: sqlScript } = schemaEntry(value)
|
|
504
577
|
try {
|
|
@@ -511,6 +584,11 @@ export function createSqliteDatabase({
|
|
|
511
584
|
throw new Error(`Schema "${name}" failed to apply: ${err.message}`)
|
|
512
585
|
}
|
|
513
586
|
}
|
|
587
|
+
|
|
588
|
+
// A durable table survives the wipe, so re-applying its CREATE is not
|
|
589
|
+
// enough to give it a column the schema has grown. Reconciled here,
|
|
590
|
+
// additively.
|
|
591
|
+
migrateDurableColumns(handle, schemas, logger)
|
|
514
592
|
}
|
|
515
593
|
|
|
516
594
|
function close() {
|