feat(13-01): add TenderSource model + Tender.fingerprint, backfill 2851 rows
Additive schema change (SCHEMA-03/D-03/D-04): new model TenderSource (1:n Tender, @@unique[sourcePortal, sourceNoticeId], onDelete Cascade) and a nullable Tender.fingerprint column + index. dedupKey stays unchanged as the SCHEMA-02 upsert target. Migration 20260723120000_add_tender_source applies in strict order (Pitfall 5): table+column create, then one TenderSource row per pre-existing Tender via SQL INSERT/SELECT, then the unique constraint. Applied locally against the tessera dev DB (container IP, no host port) — verified via psql: TenderSource count == Tender count == 2851. backfill-tender-source.ts is a one-time script that computes Tender.fingerprint via the Task-1 tenderFingerprint() function (Decimal->number conversion for estimatedValue, T-13-01-03) — run via the compiled dist/ output (source uses standard extensionless TS imports for tsc compatibility). Confirmed: 2851/2851 rows backfilled, idempotent re-run verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* One-time backfill script (SCHEMA-03, D-05) — computes and persists
|
||||
* `Tender.fingerprint` for every pre-existing row.
|
||||
*
|
||||
* The TenderSource row backfill itself lives in the migration SQL
|
||||
* (20260723120000_add_tender_source/migration.sql) because it's a pure
|
||||
* column copy. The fingerprint backfill does NOT belong in SQL: it needs
|
||||
* the umlaut/CPV-normalization logic in tender-fingerprint.ts, which is
|
||||
* code, not SQL (13-RESEARCH.md "Backfill-Migration").
|
||||
*
|
||||
* Run once, manually, after the migration has been applied (compiled first
|
||||
* — this file uses standard extensionless TS imports, so it must run as
|
||||
* built CommonJS, not as a raw .ts file):
|
||||
* pnpm --filter @tessera/api build
|
||||
* DATABASE_URL=... node dist/tenders/backfill-tender-source.js
|
||||
*
|
||||
* Idempotent: recomputes and overwrites fingerprint for every row it reads,
|
||||
* so re-running is safe (same deterministic output for unchanged inputs).
|
||||
*/
|
||||
import { PrismaClient, Prisma } from '@prisma/client';
|
||||
import { tenderFingerprint } from './tender-fingerprint';
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const prisma = new PrismaClient();
|
||||
try {
|
||||
const tenders = await prisma.tender.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
buyerName: true,
|
||||
cpvDivisions: true,
|
||||
deadlineAt: true,
|
||||
estimatedValue: true,
|
||||
},
|
||||
});
|
||||
|
||||
let updated = 0;
|
||||
for (const t of tenders) {
|
||||
const fingerprint = tenderFingerprint({
|
||||
buyerName: t.buyerName,
|
||||
title: t.title,
|
||||
cpvDivisions: t.cpvDivisions,
|
||||
deadlineAt: t.deadlineAt,
|
||||
// estimatedValue is Decimal? — convert to number|null before bucketing (T-13-01-03).
|
||||
estimatedValue: decimalToNumberOrNull(t.estimatedValue),
|
||||
});
|
||||
await prisma.tender.update({
|
||||
where: { id: t.id },
|
||||
data: { fingerprint },
|
||||
});
|
||||
updated += 1;
|
||||
}
|
||||
|
||||
console.log(`backfill-tender-source: fingerprint set on ${updated}/${tenders.length} tenders`);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
function decimalToNumberOrNull(v: Prisma.Decimal | null): number | null {
|
||||
return v === null ? null : Number(v);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('backfill-tender-source failed:', err);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user