Source document

scripts/loaders/miami/atomic-load.ts

Served verbatim from the project repository. Internal working document conventions apply: documents may reference file paths, branch names, and findings-ledger anchors from the repo.

// Keep candidate loads off the served tables until all their writes can commit.
// This module has no connection/env side effects so failure paths can be tested.
import crypto from "node:crypto";
import type { MiamiSubgroup, Prisma, PrismaClient } from "@prisma/client";

export type Fact = {
  schoolNumber: string;
  year: string;
  metricKey: string;
  subgroup?: MiamiSubgroup;
  value: number | null;
  suppressed?: boolean;
  denominator?: number | null;
};
export type LoadOptions = {
  sourceId: string;
  sourceUrl?: string;
  yearCovered?: string;
  scriptPath: string;
  sha256?: string;
  bytes?: number;
  notes?: string;
  gitSha?: string | null;
};
type Counts = { inserted: number; updated: number; suppressed: number };
type Operation = (tx: Prisma.TransactionClient) => Promise<unknown>;
type Pending = { opts: LoadOptions; previousId: string | null; facts: Fact[]; operations: Operation[]; hasFacts: boolean };

export function createDataLoader(db: Pick<PrismaClient, "$transaction" | "miamiDataLoad">) {
  const pending = new Map<string, Pending>();
  const latest = (client: Pick<Prisma.TransactionClient, "miamiDataLoad">, sourceId: string) => client.miamiDataLoad.findFirst({
    where: { sourceId, OR: [{ rowsInserted: { gt: 0 } }, { rowsUpdated: { gt: 0 } }] },
    orderBy: [{ loadedAt: "desc" }, { id: "desc" }], select: { id: true },
  });
  const get = (id: string) => {
    const load = pending.get(id);
    if (!load) throw new Error(`[load] unknown or already finished candidate ${id}`);
    return load;
  };

  async function startDataLoad(opts: LoadOptions) {
    const id = crypto.randomUUID();
    const previous = await latest(db, opts.sourceId);
    pending.set(id, { opts: { ...opts }, previousId: previous?.id ?? null, facts: [], operations: [], hasFacts: false });
    return { id };
  }

  async function upsertFacts(facts: Fact[], id: string, _concurrency = 8) {
    const load = get(id);
    for (const f of facts) {
      if (!f.schoolNumber || !f.year || !f.metricKey || (f.value !== null && !Number.isFinite(f.value)) ||
        (f.denominator != null && !Number.isFinite(f.denominator))) {
        throw new Error(`[load] invalid candidate fact ${JSON.stringify(f)}`);
      }
    }
    load.hasFacts = true;
    load.facts.push(...facts.map((f) => ({ ...f })));
    // Counts describe staged candidates. They become served only at finishDataLoad.
    return { written: facts.length, suppressed: facts.filter((f) => f.suppressed).length, fkFailures: new Set<string>() };
  }

  function stageLoadOperation(id: string, operation: Operation) {
    get(id).operations.push(operation);
  }

  async function finishDataLoad(id: string, counts: Counts, notes?: string) {
    const load = get(id);
    try {
      if (counts.inserted + counts.updated === 0 || (load.hasFacts && load.facts.length === 0) ||
        (!load.hasFacts && load.operations.length === 0)) {
        throw new Error(`[load] ${load.opts.sourceId}: no candidate writes; previous served load preserved`);
      }
      if (load.hasFacts && counts.inserted !== load.facts.length) {
        throw new Error(`[load] ${load.opts.sourceId}: staged fact count disagrees with completion count`);
      }
      return await db.$transaction(async (tx) => {
        // The lock prevents interleaved replacements. Rechecking the generation
        // also rejects a candidate staged against a source that changed meanwhile.
        await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${'miami-load:' + load.opts.sourceId}))`;
        const previous = await latest(tx, load.opts.sourceId);
        if ((previous?.id ?? null) !== load.previousId) {
          throw new Error(`[load] ${load.opts.sourceId}: another load committed while this candidate was staged; rerun from current inputs`);
        }
        const keys = [...new Set(load.facts.map((f) => f.metricKey))];
        if (keys.length) {
          const registered = new Set((await tx.miamiMetricDefinition.findMany({ where: { key: { in: keys } }, select: { key: true } })).map((r) => r.key));
          const absent = keys.filter((k) => !registered.has(k));
          if (absent.length) throw new Error(`[facts] unregistered metric keys (run metric_definitions.ts first): ${absent.join(", ")}`);
        }
        await tx.miamiDataLoad.create({ data: { id, ...load.opts } });
        for (const f of load.facts) {
          const subgroup = f.subgroup ?? "ALL";
          const data = { value: f.value, suppressed: f.suppressed ?? false, denominator: f.denominator ?? null, sourceLoadId: id };
          await tx.miamiSchoolYearMetric.upsert({
            where: { schoolNumber_year_metricKey_subgroup: { schoolNumber: f.schoolNumber, year: f.year, metricKey: f.metricKey, subgroup } },
            update: data, create: { schoolNumber: f.schoolNumber, year: f.year, metricKey: f.metricKey, subgroup, ...data },
          });
        }
        for (const operation of load.operations) await operation(tx);
        const stale = load.hasFacts ? await tx.$executeRaw`
          DELETE FROM miami_school_year_metrics
           WHERE source_load_id IN (SELECT id FROM miami_data_loads WHERE id <> ${id} AND source_id = ${load.opts.sourceId})` : 0;
        // Timestamp precision is milliseconds in this schema. Give committed
        // generations a strict order even if two short transactions finish in
        // the same millisecond, or this transaction began while waiting for a lock.
        await tx.$executeRaw`
          UPDATE miami_data_loads
             SET loaded_at = GREATEST(clock_timestamp(),
               (SELECT MAX(loaded_at) + INTERVAL '1 millisecond' FROM miami_data_loads
                 WHERE source_id = ${load.opts.sourceId} AND id <> ${id}))
           WHERE id = ${id}`;
        return tx.miamiDataLoad.update({
          where: { id }, data: { rowsInserted: counts.inserted, rowsUpdated: counts.updated, rowsSuppressed: counts.suppressed,
            notes: [load.opts.notes, notes, load.hasFacts ? `pruned ${stale} stale cells from earlier loads` : null].filter(Boolean).join("; ") || null },
        });
      }, { maxWait: 60_000, timeout: 300_000 });
    } finally {
      pending.delete(id);
    }
  }

  return { startDataLoad, upsertFacts, stageLoadOperation, finishDataLoad };
}