Verify legacy migration completion

This commit is contained in:
2026-07-23 19:16:41 +02:00
parent 030f18aeb0
commit d7ce135ac8
4 changed files with 94 additions and 3 deletions
+6 -1
View File
@@ -43,6 +43,11 @@ Run in this order for local database workflows:
5. Migrate legacy consumers:
- `npm run db:migrate:legacy-consumers`
The migration command finishes with a cutover verification across the complete
database. It exits with an error while any legacy consumer lacks a migration
mapping, including consumers that cannot be migrated because they have no circuit
list assignment. Do not remove or disable the legacy path until this check passes.
## Validation Checks
After migration, verify:
@@ -72,6 +77,6 @@ Actions:
Do not delete legacy consumers yet.
- legacy endpoints/views may still depend on them
- legacy endpoints/views remain enabled only until the cutover verification passes
- migration trace tables reference old/new mapping
- keeping legacy rows allows comparison and rollback validation during transition
+15 -1
View File
@@ -6,8 +6,9 @@ import { LegacyConsumerMigrationService } from "../src/domain/services/legacy-co
const projectRepository = new ProjectRepository();
const circuitListRepository = new CircuitListRepository();
const migrationRepository = new LegacyConsumerMigrationRepository(db);
const migrationService = new LegacyConsumerMigrationService(
new LegacyConsumerMigrationRepository(db)
migrationRepository
);
async function run() {
@@ -30,8 +31,21 @@ async function run() {
}
}
const unmigratedConsumers =
await migrationRepository.listUnmigratedConsumers();
if (unmigratedConsumers.length > 0) {
const withoutCircuitList = unmigratedConsumers.filter(
(consumer) => !consumer.circuitListId
).length;
throw new Error(
`Cutover verification failed: ${unmigratedConsumers.length} legacy consumer(s) remain unmigrated` +
` (${withoutCircuitList} without a circuit list).`
);
}
console.log("Legacy consumer migration summary:");
console.table(reports);
console.log("Cutover verification passed: all legacy consumers are mapped.");
}
run().catch((error) => {
@@ -1,8 +1,9 @@
import crypto from "node:crypto";
import { eq, inArray } from "drizzle-orm";
import { eq, inArray, isNull } from "drizzle-orm";
import type { AppDatabase } from "../database-context.js";
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
import { circuits } from "../schema/circuits.js";
import { consumers } from "../schema/consumers.js";
import { legacyConsumerCircuitMigrations } from "../schema/legacy-consumer-circuit-migrations.js";
import { legacyConsumerMigrationReports } from "../schema/legacy-consumer-migration-report.js";
import {
@@ -36,6 +37,21 @@ export interface LegacyMigrationReportPersistenceInput {
export class LegacyConsumerMigrationRepository {
constructor(private readonly database: AppDatabase) {}
async listUnmigratedConsumers() {
return this.database
.select({
id: consumers.id,
projectId: consumers.projectId,
circuitListId: consumers.circuitListId,
})
.from(consumers)
.leftJoin(
legacyConsumerCircuitMigrations,
eq(legacyConsumerCircuitMigrations.consumerId, consumers.id)
)
.where(isNull(legacyConsumerCircuitMigrations.consumerId));
}
async listMigratedConsumerIds(circuitListId: string) {
const rows = await this.database
.select({ consumerId: legacyConsumerCircuitMigrations.consumerId })
@@ -12,6 +12,7 @@ import { LegacyConsumerMigrationRepository } from "../src/db/repositories/legacy
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
import { circuitSections } from "../src/db/schema/circuit-sections.js";
import { circuits } from "../src/db/schema/circuits.js";
import { consumers } from "../src/db/schema/consumers.js";
import { legacyConsumerCircuitMigrations } from "../src/db/schema/legacy-consumer-circuit-migrations.js";
import { legacyConsumerMigrationReports } from "../src/db/schema/legacy-consumer-migration-report.js";
import { projects } from "../src/db/schema/projects.js";
@@ -97,6 +98,61 @@ function assertNoMigrationWrites(context: DatabaseContext) {
}
describe("legacy consumer migration repository", () => {
it("reports every consumer without a completed migration mapping", async () => {
const { context, circuitListId, sectionId } = createTestDatabase();
try {
context.db
.insert(consumers)
.values([
{
id: "consumer-1",
projectId: "project-1",
circuitListId,
name: "Light 1",
quantity: 1,
installedPowerPerUnitKw: 0.1,
demandFactor: 1,
},
{
id: "consumer-2",
projectId: "project-1",
circuitListId: null,
name: "Light 2",
quantity: 1,
installedPowerPerUnitKw: 0.2,
demandFactor: 1,
},
])
.run();
const repository = new LegacyConsumerMigrationRepository(context.db);
assert.deepEqual(
(await repository.listUnmigratedConsumers()).sort((left, right) =>
left.id.localeCompare(right.id)
),
[
{
id: "consumer-1",
projectId: "project-1",
circuitListId,
},
{
id: "consumer-2",
projectId: "project-1",
circuitListId: null,
},
]
);
repository.persistCircuitListMigration(
migrationInput(circuitListId, sectionId)
);
assert.deepEqual(await repository.listUnmigratedConsumers(), []);
} finally {
context.close();
}
});
it("commits circuits, rows, mappings and report together", () => {
const { context, circuitListId, sectionId } = createTestDatabase();
try {