checkStoredRows

tcmts.cli.main.checkStoredRows · #g7ac5ex3 · term · runs on the server

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

used by