assessment-model/src/app/db/schema/materials.ts
Jun-te Kim f51fdca3d8 Add partial index on material(is_active) for the catalogue read
The modelling_e2e Lambda reads the active catalogue once per invocation
(SELECT * FROM material WHERE is_active ORDER BY id). Without an index this
was a full seq scan every time (~23ms), paid by all 32 concurrent Lambdas at
startup. Partial index on id WHERE is_active returns the active rows already
id-ordered — no sort, no filter.

Verified on dev: seq scan 23.2ms -> index scan 4.2ms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 11:53:19 +00:00

137 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { InferModel, sql } from "drizzle-orm";
import {
bigserial,
text,
timestamp,
pgTable,
real,
pgEnum,
json,
boolean,
index,
} from "drizzle-orm/pg-core";
export const MaterialType: [string, ...string[]] = [
"suspended_floor_insulation",
"solid_floor_insulation",
"external_wall_insulation",
"internal_wall_insulation",
"cavity_wall_insulation",
"mechanical_ventilation",
"loft_insulation",
"exposed_floor_insulation",
"flat_roof_insulation",
"room_roof_insulation",
"cavity_wall_extraction",
// Non insulation tasks
"iwi_wall_demolition",
"iwi_vapour_barrier",
"iwi_redecoration",
"suspended_floor_demolition",
"suspended_floor_redecoration",
"suspended_floor_vapour_barrier",
"solid_floor_demolition",
"solid_floor_preparation",
"solid_floor_vapour_barrier",
"solid_floor_redecoration",
"ewi_wall_demolition",
"ewi_wall_preparation",
"ewi_wall_redecoration",
"low_energy_lighting_installation",
"flat_roof_preparation",
"flat_roof_vapour_barrier",
"flat_roof_waterproofing",
// Windows
"windows_glazing",
"secondary_glazing",
"double_glazing",
// vents
"trickle_vent",
"door_undercut",
// solar
"solar_pv",
"solar_battery",
// scaffolding
"scaffolding",
// heating systems
"high_heat_retention_storage_heaters",
"air_source_heat_pump",
// Upgrade an exiting heating system
"boiler_upgrade",
// heating controls
"roomstat_programmer_trvs",
"time_temperature_zone_control",
// other
"sealing_fireplace",
];
export const materialTypeEnum = pgEnum("type", MaterialType);
export const DepthUnit: [string, ...string[]] = ["mm"];
export const depthUnitEnum = pgEnum("depth_unit", DepthUnit);
export const SizeUnit: [string, ...string[]] = ["kWp", "kW", "watt", "storey"];
export const sizeUnitEnum = pgEnum("size_unit", SizeUnit);
export const CostUnit: [string, ...string[]] = [
"gbp_sq_meter",
"gbp_per_unit",
"gbp_per_m2",
"gbp_per_m",
];
export const costUnitEnum = pgEnum("cost_unit", CostUnit);
export const RValueUnit: [string, ...string[]] = [
"square_meter_kelvin_per_watt",
];
export const rValueUnitEnum = pgEnum("r_value_unit", RValueUnit);
export const ThermalConductivityUnit: [string, ...string[]] = [
"watt_per_meter_kelvin",
];
export const thermalConductivityUnitEnum = pgEnum(
"thermal_conductivity_unit",
ThermalConductivityUnit
);
export const material = pgTable("material", {
id: bigserial("id", { mode: "bigint" }).primaryKey(),
type: materialTypeEnum("type").notNull(),
description: text("description").notNull(),
depth: json("depth").$type<number[]>(),
depth_unit: depthUnitEnum("depth_unit"),
costUnit: costUnitEnum("cost_unit"),
rValuePerMm: real("r_value_per_mm"),
rValueUnit: rValueUnitEnum("r_value_unit"),
thermalConductivity: real("thermal_conductivity"),
thermalConductivityUnit: thermalConductivityUnitEnum(
"thermal_conductivity_unit"
),
link: text("link"),
createdAt: timestamp("created_at").notNull().defaultNow(),
isActive: boolean("is_active").notNull().default(true),
prime_material_cost: real("prime_material_cost"),
material_cost: real("material_cost"),
labour_cost: real("labour_cost"),
labour_hours_per_unit: real("labour_hours_per_unit"),
plant_cost: real("plant_cost"),
total_cost: real("total_cost"),
cost: json("cost").$type<number[]>(),
notes: text("notes"),
isInstallerQuote: boolean("is_installer_quote").default(false),
innovationRate: real("innovation_rate").default(0),
size: real("size"),
// Can be null
sizeUnit: sizeUnitEnum("size_unit"),
includesScaffolding: boolean("includes_scaffolding").default(false),
includesBattery: boolean("includes_battery").default(false),
batterySize: real("battery_size"),
}, (table) => [
// The modelling_e2e Lambda reads the active catalogue once per invocation
// (WHERE is_active ORDER BY id). Without this index that is a full seq scan
// every time (~23ms, ×32 concurrent at startup). Partial on id so the active
// rows come back already id-ordered — no sort, no filter.
index("idx_material_active").on(table.id).where(sql`${table.isActive}`),
]);
export type Material = InferModel<typeof material, "select">;