Add property_overrides table + override_component enum (0221)

The per-Property fact layer deferred by ADR-0004: one row per
(property, building_part, override_component) holding the resolved
landlord-override enum as a denormalised text snapshot, plus the raw
spreadsheet description it resolved from.

Schema only — no writer yet. The bulk_upload_finaliser application will
populate it (recalculate-on-rerun via upsert on the unique key). Design
and rationale (snapshot-not-FK, drop source, recalculate semantics) in
docs/design/bulk-upload-finaliser.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jun-te Kim 2026-06-04 09:52:37 +00:00
parent e923cf0ff6
commit fa9bf538da
4 changed files with 10774 additions and 0 deletions

View file

@ -0,0 +1,16 @@
CREATE TYPE "public"."override_component" AS ENUM('wall_type', 'roof_type', 'property_type', 'built_form_type');--> statement-breakpoint
CREATE TABLE "property_overrides" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"property_id" bigint NOT NULL,
"portfolio_id" bigint NOT NULL,
"building_part" smallint NOT NULL,
"override_component" "override_component" NOT NULL,
"override_value" text NOT NULL,
"original_spreadsheet_description" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "property_overrides_property_component_part_unique" UNIQUE("property_id","override_component","building_part")
);
--> statement-breakpoint
ALTER TABLE "property_overrides" ADD CONSTRAINT "property_overrides_property_id_property_id_fk" FOREIGN KEY ("property_id") REFERENCES "public"."property"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "property_overrides" ADD CONSTRAINT "property_overrides_portfolio_id_portfolio_id_fk" FOREIGN KEY ("portfolio_id") REFERENCES "public"."portfolio"("id") ON DELETE cascade ON UPDATE no action;

File diff suppressed because it is too large Load diff

View file

@ -1541,6 +1541,13 @@
"when": 1780491109956,
"tag": "0220_round_retro_girl",
"breakpoints": true
},
{
"idx": 221,
"version": "7",
"when": 1780566543108,
"tag": "0221_nice_sumo",
"breakpoints": true
}
]
}

View file

@ -0,0 +1,68 @@
import {
bigint,
pgEnum,
pgTable,
smallint,
text,
timestamp,
unique,
uuid,
} from "drizzle-orm/pg-core";
import { portfolio } from "./portfolio";
import { property } from "./property";
// The per-Property fact layer deferred by ADR-0004: one row per
// (property, building_part, override_component) carrying the resolved enum value
// as a denormalised snapshot. Design + rationale (Q6Q9, snapshot-not-FK,
// recalculate-on-rerun) live in docs/design/bulk-upload-finaliser.md.
//
// `override_component` values mirror the classifier category keys used in BOTH the
// frontend (src/lib/bulkUpload/columnFields.ts) and the Model backend
// (ClassifiableColumn.name), so the finaliser maps category → component with no
// translation. This is the only DB-level typing left on a row — `override_value`
// is a free-text snapshot of the resolved enum from `landlord_*_overrides`.
export const OverrideComponentValues: [string, ...string[]] = [
"wall_type",
"roof_type",
"property_type",
"built_form_type",
];
export const overrideComponentEnum = pgEnum(
"override_component",
OverrideComponentValues,
);
export const propertyOverrides = pgTable(
"property_overrides",
{
id: uuid("id").defaultRandom().primaryKey(),
propertyId: bigint("property_id", { mode: "bigint" })
.notNull()
.references(() => property.id, { onDelete: "cascade" }),
portfolioId: bigint("portfolio_id", { mode: "bigint" })
.notNull()
.references(() => portfolio.id, { onDelete: "cascade" }),
// 0 = main building, 1 = extension 1, 2 = extension 2, … (ADR-0004 ordering).
buildingPart: smallint("building_part").notNull(),
overrideComponent: overrideComponentEnum("override_component").notNull(),
// Denormalised snapshot copy of the resolved enum from landlord_*_overrides.
overrideValue: text("override_value").notNull(),
// Raw spreadsheet cell text this snapshot resolved from (provenance + re-sync key).
originalSpreadsheetDescription: text(
"original_spreadsheet_description",
).notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
},
(table) => ({
propertyComponentPartUnique: unique(
"property_overrides_property_component_part_unique",
).on(table.propertyId, table.overrideComponent, table.buildingPart),
}),
);