checkStoredRows
uses node:fs
When a table's row type changes, look at what's already stored, now rather than in production: rows that still
fit are fine as is, rows that don't need the table's migrate.
import type { Codebase } from "#codebase/tcmts/cli/codebase"; import { decode } from "#codebase/tcmts/cli/codec"; import { storeFile } from "#codebase/tcmts/cli/main"; import { dim, red, yellow } from "#codebase/tcmts/cli/style"; import { checkValue } from "#codebase/tcmts/core/schema"; import type { DefRecord } from "#codebase/tcmts/core/types"; import fs from "node:fs"; import { DatabaseSync } from "node:sqlite"; export function checkStoredRows(cb: Codebase, defs: DefRecord[]) { if (!fs.existsSync(storeFile(cb))) return; const db = new DatabaseSync(storeFile(cb), { readOnly: true }); try { for (const d of defs) { const t = d.table; if (!t?.name) continue; const label = ` table "${t.name}"`; // a renamed table's rows are still under the old name until it's first used const tables = [t.name, ...(t.renamedFrom ? [t.renamedFrom] : [])]; const inTables = `tbl IN (${tables.map(() => "?").join(", ")})`; if (t.renamedFrom) { const moving = ( db.prepare("SELECT count(*) AS n FROM rows WHERE tbl = ?").get(t.renamedFrom) as { n: number; } ).n; if (moving) console.log( dim( `${label}: renamed from "${t.renamedFrom}"; its ${moving} row(s) move over when the table is first used`, ), ); } const stale = ( db .prepare(`SELECT id, data FROM rows WHERE ${inTables} AND schema_hash != ?`) .all(...tables, t.hash) as { id: string; data: string }[] ).map((r) => ({ id: r.id, value: decode(r.data) })); const misfits = stale .map((r) => ({ ...r, why: checkValue(t.s, r.value, "row", t.defs) })) .filter((m) => m.why); if (stale.length && !misfits.length) console.log( dim( `${label}: ${stale.length} stored row(s) were written as an earlier type and still fit the new one`, ), ); else if (misfits.length && t.steps?.length) { // a migrate step takes a row if the row fits its parameter type const untaken = misfits.filter( (m) => !t.steps!.some((st) => checkValue(st.s, m.value, "row", st.defs) === undefined), ); if (!untaken.length) console.log( yellow( `${label}: ${misfits.length} stored row(s) don't fit the new type; migrate takes them as they're read (or all now: tcm data migrate ${t.name})`, ), ); else { const e = untaken[0]!; console.log( red( `${label}: ${untaken.length} stored row(s) fit neither the new type nor any migrate step's parameter type, e.g. ${e.id}: ${e.why}`, ), ); console.log(red(` reading them will fail; add a step that takes them`)); } } else if (misfits.length && t.hasOptions) console.log( yellow( `${label}: ${misfits.length} stored row(s) don't fit the new type; they'll go through migrate() when read`, ), ); else if (misfits.length) { console.log( red( `${label}: ${misfits.length} stored row(s) don't fit the new type, e.g. ${misfits[0]!.id}: ${misfits[0]!.why}`, ), ); console.log( red( ` reading them will fail until table() gets a migrate function: table<Row>("${t.name}", { migrate: (old) => ... })`, ), ); } for (const f of t.unique ?? []) { const dup = db .prepare( `SELECT json_extract(data, '$."${f}"') AS v, count(*) AS n FROM rows WHERE ${inTables} GROUP BY v HAVING n > 1 AND v IS NOT NULL LIMIT 1`, ) .get(...tables) as { v: unknown; n: number } | undefined; if (dup) console.log( red( `${label}: ${dup.n} stored rows have ${f} = ${JSON.stringify(dup.v)}, so ${f} can't be unique: using the table will fail until they differ`, ), ); } } } finally { db.close(); } }
uses
- tcmts.cli.codebase.Codebase
- tcmts.cli.codec.decode
- tcmts.cli.main.storeFile
- tcmts.cli.style.dim
- tcmts.cli.style.red
- tcmts.cli.style.yellow
- tcmts.core.schema.checkValue
- tcmts.core.types.DefRecord