From e7607c90c489c3bea437038690e6bef2feab75e5 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Thu, 30 Jul 2026 19:15:00 +0200 Subject: [PATCH] Add component persistence foundation --- docs/current-architecture.md | 9 + docs/spec/08-current-product-backlog.md | 5 +- ...-board-components-and-protection-groups.md | 22 +- package.json | 4 +- scripts/db-verify-circuit-schema.js | 75 +- .../migrations/0024_damp_mister_sinister.sql | 150 ++ src/db/migrations/meta/0024_snapshot.json | 2242 +++++++++++++++++ src/db/migrations/meta/_journal.json | 7 + ...rd-structure-project-command.repository.ts | 51 +- .../circuit-list-equipment-identifiers.ts | 26 + src/db/schema/circuit-protection-devices.ts | 27 + src/db/schema/circuit-sections.ts | 8 + ...tion-board-component-protection-devices.ts | 29 + .../schema/distribution-board-components.ts | 37 + ...n-board-component-schema-migration.test.ts | 466 ++++ 15 files changed, 3146 insertions(+), 12 deletions(-) create mode 100644 src/db/migrations/0024_damp_mister_sinister.sql create mode 100644 src/db/migrations/meta/0024_snapshot.json create mode 100644 src/db/schema/circuit-list-equipment-identifiers.ts create mode 100644 src/db/schema/circuit-protection-devices.ts create mode 100644 src/db/schema/distribution-board-component-protection-devices.ts create mode 100644 src/db/schema/distribution-board-components.ts create mode 100644 tests/distribution-board-component-schema-migration.test.ts diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 60cca86..6a90e0f 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -335,6 +335,15 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät. - Die vollständige Verteilungs-Testfixture liegt unter `tests/support/distribution-board-fixture.ts` und ist kein exportierter Produktions-Schreibweg. +- Migration `0024` bildet die additive relationale Grundlage für geschützte + Stromkreisgruppen und Verteilerkomponenten. Die drei bestehenden + Stromkreisabschnitte erhalten Kategorie und Gruppennummer 1. Separate + 1:1-Tabellen halten künftig Stromkreis- und Komponenten-Schutzgeräte; die + bisherigen flachen Schutzfelder bleiben in dieser Übergangsphase unverändert. + Ein triggergeführtes Register erzwingt bereits eine normalisierte, + stromkreislistenweite BMK-Eindeutigkeit über Stromkreise und + Verteilerkomponenten. Snapshot-, Transfer-, Command- und UI-Integration + folgen in den nächsten abgegrenzten Arbeitspaketen. PostgreSQL ist bewusst nicht implementiert. Die Domainregeln und Transaktionsgrenzen sollen portabel bleiben; Schema und Betriebsmodell benötigen diff --git a/docs/spec/08-current-product-backlog.md b/docs/spec/08-current-product-backlog.md index ad76708..2426c2f 100644 --- a/docs/spec/08-current-product-backlog.md +++ b/docs/spec/08-current-product-backlog.md @@ -14,7 +14,10 @@ requirements and intended sequencing, not proof of implementation. component model in phased work packages. - [x] Phase A: central protection catalog, validation, defaults and pure grouped BMK rules. -- [ ] Phase B: persistence and snapshot/transfer compatibility model. +- [x] Phase B1: additive relational schema, group backfill, component/protection + tables and shared BMK uniqueness. +- [ ] Phase B2: snapshot/transfer schema upgrade and deterministic compatibility + mapping. - [ ] Phase C: persistent commands and new-board defaults. - [ ] Phase D: group numbering, moves and destructive operations. - [ ] Phase E: editor projection and editing. diff --git a/docs/spec/09-distribution-board-components-and-protection-groups.md b/docs/spec/09-distribution-board-components-and-protection-groups.md index b883375..ed19690 100644 --- a/docs/spec/09-distribution-board-components-and-protection-groups.md +++ b/docs/spec/09-distribution-board-components-and-protection-groups.md @@ -3,8 +3,9 @@ ## Status This document defines the agreed target behavior for distribution-board -components, protected circuit groups and circuit protection devices. It is a -future specification, not proof of implementation. +components, protected circuit groups and circuit protection devices. Phase A +and the relational Phase-B1 foundation are implemented; later sections remain +requirements and are not proof of runtime or editor support. Full rule-based protection and cable sizing is deliberately deferred. This phase establishes the structure and manually selected technical values that a @@ -544,6 +545,9 @@ Acceptance: ### B. Persistence and Compatibility Model +Status: In progress. Relational foundation B1 is complete; logical +snapshot/transfer integration B2 is pending. + - add group category and group number - add distribution-board component persistence - add one-to-one circuit protection persistence @@ -552,6 +556,20 @@ Acceptance: - add snapshot/transfer schema upgrade - preserve existing identifiers and protection values +Implemented in B1: + +- additive migration `0024` adds group identity to circuit sections and maps + the three established sections to group 1 without changing UUIDs +- separate component and circuit/component protection tables preserve the + one-to-one ownership boundaries +- a trigger-maintained circuit-list registry rejects normalized BMK collisions + across circuits and distribution-board components +- the existing flat circuit protection fields remain untouched until their + deterministic snapshot/runtime transition is implemented +- clean and populated pre-migration databases are covered by migration tests +- newly created legacy-compatible board structures receive the same initial + group identity while their persisted command format remains unchanged + Acceptance: - a current database migrates without data loss diff --git a/package.json b/package.json index ec6b584..4216834 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "build:web": "next build", "typecheck:scripts": "tsc --noEmit -p tsconfig.scripts.json", "start": "node dist/server/index.js", - "test": "tsx --test tests/protection-device.test.ts tests/distribution-board-component.test.ts tests/circuit-group-numbering.test.ts tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.service.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.persistence.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-overview-import.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts", - "test:watch": "tsx --watch --test tests/protection-device.test.ts tests/distribution-board-component.test.ts tests/circuit-group-numbering.test.ts tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.service.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.persistence.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-overview-import.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts", + "test": "tsx --test tests/protection-device.test.ts tests/distribution-board-component.test.ts tests/distribution-board-component-schema-migration.test.ts tests/circuit-group-numbering.test.ts tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.service.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.persistence.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-overview-import.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts", + "test:watch": "tsx --watch --test tests/protection-device.test.ts tests/distribution-board-component.test.ts tests/distribution-board-component-schema-migration.test.ts tests/circuit-group-numbering.test.ts tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.service.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.persistence.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-overview-import.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:backup": "tsx scripts/db-backup.ts", diff --git a/scripts/db-verify-circuit-schema.js b/scripts/db-verify-circuit-schema.js index 0a811c2..0fff995 100644 --- a/scripts/db-verify-circuit-schema.js +++ b/scripts/db-verify-circuit-schema.js @@ -8,12 +8,21 @@ const requiredTables = [ "circuit_sections", "circuits", "circuit_device_rows", + "circuit_list_equipment_identifiers", + "circuit_protection_devices", + "distribution_board_components", + "distribution_board_component_protection_devices", "legacy_consumer_circuit_migrations", "legacy_consumer_migration_reports", ]; +const tablePlaceholders = requiredTables.map(() => "?").join(", "); const rows = db - .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name IN (?, ?, ?, ?, ?)") + .prepare( + `SELECT name + FROM sqlite_master + WHERE type = 'table' AND name IN (${tablePlaceholders})` + ) .all(...requiredTables); const existing = new Set(rows.map((row) => row.name)); @@ -48,6 +57,16 @@ const circuitColumns = new Set( db.prepare("PRAGMA table_info(circuits)").all().map((column) => column.name) ); const missingCircuitColumns = requiredCircuitColumns.filter((name) => !circuitColumns.has(name)); +const requiredCircuitSectionColumns = ["category", "group_number"]; +const circuitSectionColumns = new Set( + db + .prepare("PRAGMA table_info(circuit_sections)") + .all() + .map((column) => column.name) +); +const missingCircuitSectionColumns = requiredCircuitSectionColumns.filter( + (name) => !circuitSectionColumns.has(name) +); const requiredProjectColumns = [ "internal_project_number", "external_project_number", @@ -61,7 +80,11 @@ const projectColumns = new Set( const missingProjectColumns = requiredProjectColumns.filter( (name) => !projectColumns.has(name) ); -const requiredDistributionBoardColumns = ["floor_id", "supply_type"]; +const requiredDistributionBoardColumns = [ + "floor_id", + "supply_type", + "simultaneity_factor", +]; const distributionBoardColumns = new Set( db .prepare("PRAGMA table_info(distribution_boards)") @@ -72,6 +95,28 @@ const missingDistributionBoardColumns = requiredDistributionBoardColumns.filter( (name) => !distributionBoardColumns.has(name) ); +const requiredEquipmentIdentifierTriggers = [ + "circuits_equipment_identifier_insert", + "circuits_equipment_identifier_update", + "circuits_equipment_identifier_delete", + "distribution_board_components_equipment_identifier_insert", + "distribution_board_components_equipment_identifier_update", + "distribution_board_components_equipment_identifier_delete", +]; +const equipmentIdentifierTriggers = new Set( + db + .prepare( + `SELECT name + FROM sqlite_master + WHERE type = 'trigger'` + ) + .all() + .map((trigger) => trigger.name) +); +const missingEquipmentIdentifierTriggers = + requiredEquipmentIdentifierTriggers.filter( + (name) => !equipmentIdentifierTriggers.has(name) + ); const integrityCheck = db.pragma("integrity_check", { simple: true }); const foreignKeyViolations = db.pragma("foreign_key_check"); @@ -80,11 +125,19 @@ console.log("Required tables:", requiredTables.join(", ")); console.log("Existing tables:", [...existing].join(", ") || "(none)"); console.log("Required project-device columns:", requiredProjectDeviceColumns.join(", ")); console.log("Required circuit columns:", requiredCircuitColumns.join(", ")); +console.log( + "Required circuit-section columns:", + requiredCircuitSectionColumns.join(", ") +); console.log("Required project columns:", requiredProjectColumns.join(", ")); console.log( "Required distribution-board columns:", requiredDistributionBoardColumns.join(", ") ); +console.log( + "Required BMK synchronization triggers:", + requiredEquipmentIdentifierTriggers.join(", ") +); console.log("Integrity:", integrityCheck); console.log("Foreign-key violations:", foreignKeyViolations.length); @@ -111,6 +164,14 @@ if (missingCircuitColumns.length > 0) { process.exit(1); } +if (missingCircuitSectionColumns.length > 0) { + console.error( + "Missing circuit-section columns:", + missingCircuitSectionColumns.join(", ") + ); + process.exit(1); +} + if (missingProjectColumns.length > 0) { console.error("Missing project columns:", missingProjectColumns.join(", ")); process.exit(1); @@ -124,9 +185,17 @@ if (missingDistributionBoardColumns.length > 0) { process.exit(1); } +if (missingEquipmentIdentifierTriggers.length > 0) { + console.error( + "Missing BMK synchronization triggers:", + missingEquipmentIdentifierTriggers.join(", ") + ); + process.exit(1); +} + if (integrityCheck !== "ok" || foreignKeyViolations.length > 0) { console.error("Database integrity or foreign-key verification failed."); process.exit(1); } -console.log("Circuit-first schema verification passed."); +console.log("Circuit-first and distribution-component schema verification passed."); diff --git a/src/db/migrations/0024_damp_mister_sinister.sql b/src/db/migrations/0024_damp_mister_sinister.sql new file mode 100644 index 0000000..3e054c3 --- /dev/null +++ b/src/db/migrations/0024_damp_mister_sinister.sql @@ -0,0 +1,150 @@ +CREATE TABLE `circuit_list_equipment_identifiers` ( + `owner_type` text NOT NULL, + `owner_id` text NOT NULL, + `circuit_list_id` text NOT NULL, + `equipment_identifier` text NOT NULL, + FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `circuit_list_equipment_identifiers_owner_unique` ON `circuit_list_equipment_identifiers` (`owner_type`,`owner_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `circuit_list_equipment_identifiers_list_identifier_unique` ON `circuit_list_equipment_identifiers` (`circuit_list_id`,`equipment_identifier`);--> statement-breakpoint +CREATE TABLE `circuit_protection_devices` ( + `circuit_id` text PRIMARY KEY NOT NULL, + `type` text NOT NULL, + `rated_current_a` real NOT NULL, + `fuse_utilization_category` text, + `trip_characteristic` text, + `rcd_type` text, + `rated_residual_current_ma` real, + FOREIGN KEY (`circuit_id`) REFERENCES `circuits`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `distribution_board_component_protection_devices` ( + `component_id` text PRIMARY KEY NOT NULL, + `type` text NOT NULL, + `rated_current_a` real NOT NULL, + `fuse_utilization_category` text, + `trip_characteristic` text, + `rcd_type` text, + `rated_residual_current_ma` real, + FOREIGN KEY (`component_id`) REFERENCES `distribution_board_components`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `distribution_board_components` ( + `id` text PRIMARY KEY NOT NULL, + `circuit_list_id` text NOT NULL, + `section_id` text, + `equipment_identifier` text NOT NULL, + `name` text NOT NULL, + `role` text NOT NULL, + `placement` text NOT NULL, + `sort_order` integer DEFAULT 0 NOT NULL, + FOREIGN KEY (`circuit_list_id`) REFERENCES `circuit_lists`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`section_id`) REFERENCES `circuit_sections`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `distribution_board_components_list_identifier_unique` ON `distribution_board_components` (`circuit_list_id`,`equipment_identifier`);--> statement-breakpoint +CREATE UNIQUE INDEX `distribution_board_components_section_role_unique` ON `distribution_board_components` (`section_id`,`role`);--> statement-breakpoint +ALTER TABLE `circuit_sections` ADD `category` text;--> statement-breakpoint +ALTER TABLE `circuit_sections` ADD `group_number` integer;--> statement-breakpoint +CREATE UNIQUE INDEX `circuit_sections_list_category_group_unique` ON `circuit_sections` (`circuit_list_id`,`category`,`group_number`);--> statement-breakpoint +UPDATE `circuit_sections` +SET + `category` = CASE `key` + WHEN 'lighting' THEN 'lighting' + WHEN 'single_phase' THEN 'single_phase' + WHEN 'three_phase' THEN 'three_phase' + END, + `group_number` = 1 +WHERE `key` IN ('lighting', 'single_phase', 'three_phase');--> statement-breakpoint +INSERT INTO `circuit_list_equipment_identifiers` ( + `owner_type`, + `owner_id`, + `circuit_list_id`, + `equipment_identifier` +) +SELECT + 'circuit', + `id`, + `circuit_list_id`, + `equipment_identifier` +FROM `circuits`;--> statement-breakpoint +CREATE UNIQUE INDEX `circuit_list_equipment_identifiers_normalized_unique` +ON `circuit_list_equipment_identifiers` ( + `circuit_list_id`, + lower(trim(`equipment_identifier`)) +);--> statement-breakpoint +CREATE UNIQUE INDEX `distribution_board_components_fixed_role_unique` +ON `distribution_board_components` (`circuit_list_id`, `role`) +WHERE `role` IN ('main_switch', 'surge_protective_device');--> statement-breakpoint +CREATE TRIGGER `circuits_equipment_identifier_insert` +AFTER INSERT ON `circuits` +BEGIN + INSERT INTO `circuit_list_equipment_identifiers` ( + `owner_type`, + `owner_id`, + `circuit_list_id`, + `equipment_identifier` + ) + VALUES ( + 'circuit', + NEW.`id`, + NEW.`circuit_list_id`, + NEW.`equipment_identifier` + ); +END;--> statement-breakpoint +CREATE TRIGGER `circuits_equipment_identifier_update` +AFTER UPDATE OF `circuit_list_id`, `equipment_identifier` ON `circuits` +BEGIN + UPDATE `circuit_list_equipment_identifiers` + SET + `circuit_list_id` = NEW.`circuit_list_id`, + `equipment_identifier` = NEW.`equipment_identifier` + WHERE + `owner_type` = 'circuit' + AND `owner_id` = OLD.`id`; +END;--> statement-breakpoint +CREATE TRIGGER `circuits_equipment_identifier_delete` +AFTER DELETE ON `circuits` +BEGIN + DELETE FROM `circuit_list_equipment_identifiers` + WHERE + `owner_type` = 'circuit' + AND `owner_id` = OLD.`id`; +END;--> statement-breakpoint +CREATE TRIGGER `distribution_board_components_equipment_identifier_insert` +AFTER INSERT ON `distribution_board_components` +BEGIN + INSERT INTO `circuit_list_equipment_identifiers` ( + `owner_type`, + `owner_id`, + `circuit_list_id`, + `equipment_identifier` + ) + VALUES ( + 'distribution_board_component', + NEW.`id`, + NEW.`circuit_list_id`, + NEW.`equipment_identifier` + ); +END;--> statement-breakpoint +CREATE TRIGGER `distribution_board_components_equipment_identifier_update` +AFTER UPDATE OF `circuit_list_id`, `equipment_identifier` +ON `distribution_board_components` +BEGIN + UPDATE `circuit_list_equipment_identifiers` + SET + `circuit_list_id` = NEW.`circuit_list_id`, + `equipment_identifier` = NEW.`equipment_identifier` + WHERE + `owner_type` = 'distribution_board_component' + AND `owner_id` = OLD.`id`; +END;--> statement-breakpoint +CREATE TRIGGER `distribution_board_components_equipment_identifier_delete` +AFTER DELETE ON `distribution_board_components` +BEGIN + DELETE FROM `circuit_list_equipment_identifiers` + WHERE + `owner_type` = 'distribution_board_component' + AND `owner_id` = OLD.`id`; +END; diff --git a/src/db/migrations/meta/0024_snapshot.json b/src/db/migrations/meta/0024_snapshot.json new file mode 100644 index 0000000..bd411ce --- /dev/null +++ b/src/db/migrations/meta/0024_snapshot.json @@ -0,0 +1,2242 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "31166943-989c-4199-a8a3-a896dab14995", + "prevId": "f99355d0-b023-424b-870e-3ea8423497eb", + "tables": { + "circuit_device_rows": { + "name": "circuit_device_rows", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_id": { + "name": "circuit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "linked_project_device_id": { + "name": "linked_project_device_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "legacy_consumer_id": { + "name": "legacy_consumer_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase_type": { + "name": "phase_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_group": { + "name": "cost_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_number_snapshot": { + "name": "room_number_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_name_snapshot": { + "name": "room_name_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "power_per_unit": { + "name": "power_per_unit", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cos_phi": { + "name": "cos_phi", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "overridden_fields": { + "name": "overridden_fields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "circuit_device_rows_circuit_id_circuits_id_fk": { + "name": "circuit_device_rows_circuit_id_circuits_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "circuits", + "columnsFrom": [ + "circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { + "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "project_devices", + "columnsFrom": [ + "linked_project_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "circuit_device_rows_room_id_rooms_id_fk": { + "name": "circuit_device_rows_room_id_rooms_id_fk", + "tableFrom": "circuit_device_rows", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_list_equipment_identifiers": { + "name": "circuit_list_equipment_identifiers", + "columns": { + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "circuit_list_equipment_identifiers_owner_unique": { + "name": "circuit_list_equipment_identifiers_owner_unique", + "columns": [ + "owner_type", + "owner_id" + ], + "isUnique": true + }, + "circuit_list_equipment_identifiers_list_identifier_unique": { + "name": "circuit_list_equipment_identifiers_list_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk": { + "name": "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuit_list_equipment_identifiers", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_lists": { + "name": "circuit_lists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "distribution_board_id": { + "name": "distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "circuit_lists_distribution_board_id_unique": { + "name": "circuit_lists_distribution_board_id_unique", + "columns": [ + "distribution_board_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_lists_project_id_projects_id_fk": { + "name": "circuit_lists_project_id_projects_id_fk", + "tableFrom": "circuit_lists", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuit_lists_distribution_board_id_distribution_boards_id_fk": { + "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "circuit_lists", + "tableTo": "distribution_boards", + "columnsFrom": [ + "distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_protection_devices": { + "name": "circuit_protection_devices", + "columns": { + "circuit_id": { + "name": "circuit_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rated_current_a": { + "name": "rated_current_a", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fuse_utilization_category": { + "name": "fuse_utilization_category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trip_characteristic": { + "name": "trip_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_type": { + "name": "rcd_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rated_residual_current_ma": { + "name": "rated_residual_current_ma", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "circuit_protection_devices_circuit_id_circuits_id_fk": { + "name": "circuit_protection_devices_circuit_id_circuits_id_fk", + "tableFrom": "circuit_protection_devices", + "tableTo": "circuits", + "columnsFrom": [ + "circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuit_sections": { + "name": "circuit_sections", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_number": { + "name": "group_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "circuit_sections_list_key_unique": { + "name": "circuit_sections_list_key_unique", + "columns": [ + "circuit_list_id", + "key" + ], + "isUnique": true + }, + "circuit_sections_list_prefix_unique": { + "name": "circuit_sections_list_prefix_unique", + "columns": [ + "circuit_list_id", + "prefix" + ], + "isUnique": true + }, + "circuit_sections_list_category_group_unique": { + "name": "circuit_sections_list_category_group_unique", + "columns": [ + "circuit_list_id", + "category", + "group_number" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuit_sections_circuit_list_id_circuit_lists_id_fk": { + "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuit_sections", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "circuits": { + "name": "circuits", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "protection_type": { + "name": "protection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protection_rated_current": { + "name": "protection_rated_current", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protection_characteristic": { + "name": "protection_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cable_type": { + "name": "cable_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cable_cross_section": { + "name": "cable_cross_section", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cable_length": { + "name": "cable_length", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_assignment": { + "name": "rcd_assignment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_designation": { + "name": "terminal_designation", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voltage": { + "name": "voltage", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "control_requirement": { + "name": "control_requirement", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_reserve": { + "name": "is_reserve", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "circuits_list_equipment_identifier_unique": { + "name": "circuits_list_equipment_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + } + }, + "foreignKeys": { + "circuits_circuit_list_id_circuit_lists_id_fk": { + "name": "circuits_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "circuits", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "circuits_section_id_circuit_sections_id_fk": { + "name": "circuits_section_id_circuit_sections_id_fk", + "tableFrom": "circuits", + "tableTo": "circuit_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "consumers": { + "name": "consumers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "distribution_board_id": { + "name": "distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_device_id": { + "name": "project_device_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_linked_to_device": { + "name": "is_linked_to_device", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "circuit_number": { + "name": "circuit_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phase_type": { + "name": "phase_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trade_or_cost_group": { + "name": "trade_or_cost_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protection_type": { + "name": "protection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protection_rated_current": { + "name": "protection_rated_current", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "protection_characteristic": { + "name": "protection_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cable_type": { + "name": "cable_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cable_cross_section": { + "name": "cable_cross_section", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "installed_power_per_unit_kw": { + "name": "installed_power_per_unit_kw", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "demand_factor": { + "name": "demand_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "voltage_v": { + "name": "voltage_v", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phase_count": { + "name": "phase_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "power_factor": { + "name": "power_factor", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "consumers_project_id_projects_id_fk": { + "name": "consumers_project_id_projects_id_fk", + "tableFrom": "consumers", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "consumers_distribution_board_id_distribution_boards_id_fk": { + "name": "consumers_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "consumers", + "tableTo": "distribution_boards", + "columnsFrom": [ + "distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "consumers_circuit_list_id_circuit_lists_id_fk": { + "name": "consumers_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "consumers", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "consumers_project_device_id_project_devices_id_fk": { + "name": "consumers_project_device_id_project_devices_id_fk", + "tableFrom": "consumers", + "tableTo": "project_devices", + "columnsFrom": [ + "project_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "consumers_room_id_rooms_id_fk": { + "name": "consumers_room_id_rooms_id_fk", + "tableFrom": "consumers", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_board_component_protection_devices": { + "name": "distribution_board_component_protection_devices", + "columns": { + "component_id": { + "name": "component_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rated_current_a": { + "name": "rated_current_a", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fuse_utilization_category": { + "name": "fuse_utilization_category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trip_characteristic": { + "name": "trip_characteristic", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rcd_type": { + "name": "rcd_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rated_residual_current_ma": { + "name": "rated_residual_current_ma", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk": { + "name": "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk", + "tableFrom": "distribution_board_component_protection_devices", + "tableTo": "distribution_board_components", + "columnsFrom": [ + "component_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_board_components": { + "name": "distribution_board_components", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "equipment_identifier": { + "name": "equipment_identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "placement": { + "name": "placement", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "distribution_board_components_list_identifier_unique": { + "name": "distribution_board_components_list_identifier_unique", + "columns": [ + "circuit_list_id", + "equipment_identifier" + ], + "isUnique": true + }, + "distribution_board_components_section_role_unique": { + "name": "distribution_board_components_section_role_unique", + "columns": [ + "section_id", + "role" + ], + "isUnique": true + } + }, + "foreignKeys": { + "distribution_board_components_circuit_list_id_circuit_lists_id_fk": { + "name": "distribution_board_components_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "distribution_board_components", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "distribution_board_components_section_id_circuit_sections_id_fk": { + "name": "distribution_board_components_section_id_circuit_sections_id_fk", + "tableFrom": "distribution_board_components", + "tableTo": "circuit_sections", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distribution_boards": { + "name": "distribution_boards", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "floor_id": { + "name": "floor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supply_type": { + "name": "supply_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "distribution_boards_project_id_projects_id_fk": { + "name": "distribution_boards_project_id_projects_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "distribution_boards_floor_id_floors_id_fk": { + "name": "distribution_boards_floor_id_floors_id_fk", + "tableFrom": "distribution_boards", + "tableTo": "floors", + "columnsFrom": [ + "floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "floors": { + "name": "floors", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "floors_project_id_projects_id_fk": { + "name": "floors_project_id_projects_id_fk", + "tableFrom": "floors", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "global_devices": { + "name": "global_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "installed_power_per_unit_kw": { + "name": "installed_power_per_unit_kw", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "demand_factor": { + "name": "demand_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "voltage_v": { + "name": "voltage_v", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "phase_count": { + "name": "phase_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "power_factor": { + "name": "power_factor", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "legacy_consumer_circuit_migrations": { + "name": "legacy_consumer_circuit_migrations", + "columns": { + "consumer_id": { + "name": "consumer_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_id": { + "name": "circuit_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "circuit_device_row_id": { + "name": "circuit_device_row_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk": { + "name": "legacy_consumer_circuit_migrations_circuit_id_circuits_id_fk", + "tableFrom": "legacy_consumer_circuit_migrations", + "tableTo": "circuits", + "columnsFrom": [ + "circuit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk": { + "name": "legacy_consumer_circuit_migrations_circuit_device_row_id_circuit_device_rows_id_fk", + "tableFrom": "legacy_consumer_circuit_migrations", + "tableTo": "circuit_device_rows", + "columnsFrom": [ + "circuit_device_row_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk": { + "name": "legacy_consumer_circuit_migrations_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "legacy_consumer_circuit_migrations", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "legacy_consumer_migration_reports": { + "name": "legacy_consumer_migration_reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "circuit_list_id": { + "name": "circuit_list_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "legacy_consumer_count": { + "name": "legacy_consumer_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_circuit_count": { + "name": "created_circuit_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_device_row_count": { + "name": "created_device_row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duplicate_grouped_count": { + "name": "duplicate_grouped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "generated_identifier_count": { + "name": "generated_identifier_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unassigned_row_count": { + "name": "unassigned_row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "warnings_json": { + "name": "warnings_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "generated_identifiers_json": { + "name": "generated_identifiers_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duplicate_groups_json": { + "name": "duplicate_groups_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "legacy_consumer_migration_reports_list_unique": { + "name": "legacy_consumer_migration_reports_list_unique", + "columns": [ + "circuit_list_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk": { + "name": "legacy_consumer_migration_reports_circuit_list_id_circuit_lists_id_fk", + "tableFrom": "legacy_consumer_migration_reports", + "tableTo": "circuit_lists", + "columnsFrom": [ + "circuit_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_change_sets": { + "name": "project_change_sets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_revision_id": { + "name": "project_revision_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_type": { + "name": "command_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_schema_version": { + "name": "payload_schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forward_payload_json": { + "name": "forward_payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inverse_payload_json": { + "name": "inverse_payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_change_sets_revision_unique": { + "name": "project_change_sets_revision_unique", + "columns": [ + "project_revision_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_change_sets_project_revision_id_project_revisions_id_fk": { + "name": "project_change_sets_project_revision_id_project_revisions_id_fk", + "tableFrom": "project_change_sets", + "tableTo": "project_revisions", + "columnsFrom": [ + "project_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_devices": { + "name": "project_devices", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phase_type": { + "name": "phase_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'single_phase'" + }, + "connection_kind": { + "name": "connection_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost_group": { + "name": "cost_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "power_per_unit": { + "name": "power_per_unit", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "simultaneity_factor": { + "name": "simultaneity_factor", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "cos_phi": { + "name": "cos_phi", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "remark": { + "name": "remark", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voltage_v": { + "name": "voltage_v", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "project_devices_project_id_projects_id_fk": { + "name": "project_devices_project_id_projects_id_fk", + "tableFrom": "project_devices", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_history_stack_entries": { + "name": "project_history_stack_entries", + "columns": { + "change_set_id": { + "name": "change_set_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stack": { + "name": "stack", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "project_history_stack_entries_position_unique": { + "name": "project_history_stack_entries_position_unique", + "columns": [ + "project_id", + "stack", + "position" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { + "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", + "tableFrom": "project_history_stack_entries", + "tableTo": "project_change_sets", + "columnsFrom": [ + "change_set_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_history_stack_entries_project_id_projects_id_fk": { + "name": "project_history_stack_entries_project_id_projects_id_fk", + "tableFrom": "project_history_stack_entries", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "project_history_stack_entries_stack_check": { + "name": "project_history_stack_entries_stack_check", + "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" + } + } + }, + "project_revisions": { + "name": "project_revisions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "project_revisions_project_number_unique": { + "name": "project_revisions_project_number_unique", + "columns": [ + "project_id", + "revision_number" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_revisions_project_id_projects_id_fk": { + "name": "project_revisions_project_id_projects_id_fk", + "tableFrom": "project_revisions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_snapshots": { + "name": "project_snapshots", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_revision": { + "name": "source_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'named'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload_json": { + "name": "payload_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload_sha256": { + "name": "payload_sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at_iso": { + "name": "created_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "project_snapshots_project_created_idx": { + "name": "project_snapshots_project_created_idx", + "columns": [ + "project_id", + "created_at_iso" + ], + "isUnique": false + }, + "project_snapshots_project_kind_revision_idx": { + "name": "project_snapshots_project_kind_revision_idx", + "columns": [ + "project_id", + "kind", + "source_revision" + ], + "isUnique": false + }, + "project_snapshots_project_name_unique": { + "name": "project_snapshots_project_name_unique", + "columns": [ + "project_id", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_snapshots_project_id_projects_id_fk": { + "name": "project_snapshots_project_id_projects_id_fk", + "tableFrom": "project_snapshots", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_project_number": { + "name": "internal_project_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_project_number": { + "name": "external_project_number", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "building_owner": { + "name": "building_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "single_phase_voltage_v": { + "name": "single_phase_voltage_v", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 230 + }, + "three_phase_voltage_v": { + "name": "three_phase_voltage_v", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 400 + }, + "enabled_distribution_board_supply_types": { + "name": "enabled_distribution_board_supply_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[\"AV\",\"SV\",\"EV\",\"USV\",\"MSR\",\"SiBe\"]'" + }, + "current_revision": { + "name": "current_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rooms": { + "name": "rooms", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "floor_id": { + "name": "floor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "room_number": { + "name": "room_number", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "room_name": { + "name": "room_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "rooms_project_id_projects_id_fk": { + "name": "rooms_project_id_projects_id_fk", + "tableFrom": "rooms", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rooms_floor_id_floors_id_fk": { + "name": "rooms_floor_id_floors_id_fk", + "tableFrom": "rooms", + "tableTo": "floors", + "columnsFrom": [ + "floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 6825610..afa07cf 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -169,6 +169,13 @@ "when": 1785343645615, "tag": "0023_cheerful_night_thrasher", "breakpoints": true + }, + { + "idx": 24, + "version": "6", + "when": 1785431250141, + "tag": "0024_damp_mister_sinister", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/repositories/distribution-board-structure-project-command.repository.ts b/src/db/repositories/distribution-board-structure-project-command.repository.ts index 7aab73e..8cded53 100644 --- a/src/db/repositories/distribution-board-structure-project-command.repository.ts +++ b/src/db/repositories/distribution-board-structure-project-command.repository.ts @@ -24,6 +24,7 @@ import type { DistributionBoardStructureProjectCommandStore, ExecuteDistributionBoardStructureCommandInput, } from "../../domain/ports/distribution-board-structure-project-command.store.js"; +import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js"; import type { AppDatabase } from "../database-context.js"; import { circuitLists } from "../schema/circuit-lists.js"; import { circuitSections } from "../schema/circuit-sections.js"; @@ -168,7 +169,10 @@ export class DistributionBoardStructureProjectCommandRepository .values(structure.distributionBoard) .run(); database.insert(circuitLists).values(structure.circuitList).run(); - database.insert(circuitSections).values(structure.sections).run(); + database + .insert(circuitSections) + .values(structure.sections.map(withInitialGroupIdentity)) + .run(); return createDistributionBoardDeleteProjectCommand(structure); } @@ -409,9 +413,48 @@ function structureMatches( left.sortOrder - right.sortOrder || left.id.localeCompare(right.id) ); - return expectedSections.every((section, index) => - sameRecord(section, actual.sections[index]) - ); + return expectedSections.every((section, index) => { + const actualSection = actual.sections[index]; + if ( + actualSection.category !== initialCategoryBySectionKey(section.key) || + actualSection.groupNumber !== initialGroupNumberBySectionKey(section.key) + ) { + return false; + } + const { + category: _category, + groupNumber: _groupNumber, + ...comparableActualSection + } = actualSection; + return sameRecord(section, comparableActualSection); + }); +} + +function withInitialGroupIdentity( + section: DistributionBoardStructureSnapshot["sections"][number] +): typeof circuitSections.$inferInsert { + return { + ...section, + category: initialCategoryBySectionKey(section.key), + groupNumber: initialGroupNumberBySectionKey(section.key), + }; +} + +function initialCategoryBySectionKey( + key: string +): CircuitGroupCategory | null { + if ( + key === "lighting" || + key === "single_phase" || + key === "three_phase" + ) { + return key; + } + return null; +} + +function initialGroupNumberBySectionKey(key: string): number | null { + return initialCategoryBySectionKey(key) === null ? null : 1; } function sameRecord( diff --git a/src/db/schema/circuit-list-equipment-identifiers.ts b/src/db/schema/circuit-list-equipment-identifiers.ts new file mode 100644 index 0000000..ec4721e --- /dev/null +++ b/src/db/schema/circuit-list-equipment-identifiers.ts @@ -0,0 +1,26 @@ +import { sqliteTable, text, unique } from "drizzle-orm/sqlite-core"; +import { circuitLists } from "./circuit-lists.js"; + +export const circuitListEquipmentIdentifiers = sqliteTable( + "circuit_list_equipment_identifiers", + { + ownerType: text("owner_type") + .$type<"circuit" | "distribution_board_component">() + .notNull(), + ownerId: text("owner_id").notNull(), + circuitListId: text("circuit_list_id") + .notNull() + .references(() => circuitLists.id, { onDelete: "cascade" }), + equipmentIdentifier: text("equipment_identifier").notNull(), + }, + (table) => [ + unique("circuit_list_equipment_identifiers_owner_unique").on( + table.ownerType, + table.ownerId + ), + unique("circuit_list_equipment_identifiers_list_identifier_unique").on( + table.circuitListId, + table.equipmentIdentifier + ), + ] +); diff --git a/src/db/schema/circuit-protection-devices.ts b/src/db/schema/circuit-protection-devices.ts new file mode 100644 index 0000000..e8d721e --- /dev/null +++ b/src/db/schema/circuit-protection-devices.ts @@ -0,0 +1,27 @@ +import { real, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import type { + BreakerTripCharacteristic, + FuseUtilizationCategory, + ProtectionDeviceType, + RcdType, +} from "../../shared/constants/protection-device.js"; +import { circuits } from "./circuits.js"; + +export const circuitProtectionDevices = sqliteTable( + "circuit_protection_devices", + { + circuitId: text("circuit_id") + .primaryKey() + .references(() => circuits.id, { onDelete: "cascade" }), + type: text("type").$type().notNull(), + ratedCurrentA: real("rated_current_a").notNull(), + fuseUtilizationCategory: text( + "fuse_utilization_category" + ).$type(), + tripCharacteristic: text( + "trip_characteristic" + ).$type(), + rcdType: text("rcd_type").$type(), + ratedResidualCurrentMa: real("rated_residual_current_ma"), + } +); diff --git a/src/db/schema/circuit-sections.ts b/src/db/schema/circuit-sections.ts index cb11231..ce806e1 100644 --- a/src/db/schema/circuit-sections.ts +++ b/src/db/schema/circuit-sections.ts @@ -1,5 +1,6 @@ import { integer, sqliteTable, text, unique } from "drizzle-orm/sqlite-core"; import { circuitLists } from "./circuit-lists.js"; +import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js"; export const circuitSections = sqliteTable( "circuit_sections", @@ -12,10 +13,17 @@ export const circuitSections = sqliteTable( displayName: text("display_name").notNull(), prefix: text("prefix").notNull(), sortOrder: integer("sort_order").notNull().default(0), + category: text("category").$type(), + groupNumber: integer("group_number"), }, (table) => [ unique("circuit_sections_list_key_unique").on(table.circuitListId, table.key), unique("circuit_sections_list_prefix_unique").on(table.circuitListId, table.prefix), + unique("circuit_sections_list_category_group_unique").on( + table.circuitListId, + table.category, + table.groupNumber + ), ] ); diff --git a/src/db/schema/distribution-board-component-protection-devices.ts b/src/db/schema/distribution-board-component-protection-devices.ts new file mode 100644 index 0000000..5078180 --- /dev/null +++ b/src/db/schema/distribution-board-component-protection-devices.ts @@ -0,0 +1,29 @@ +import { real, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import type { + BreakerTripCharacteristic, + FuseUtilizationCategory, + ProtectionDeviceType, + RcdType, +} from "../../shared/constants/protection-device.js"; +import { distributionBoardComponents } from "./distribution-board-components.js"; + +export const distributionBoardComponentProtectionDevices = sqliteTable( + "distribution_board_component_protection_devices", + { + componentId: text("component_id") + .primaryKey() + .references(() => distributionBoardComponents.id, { + onDelete: "cascade", + }), + type: text("type").$type().notNull(), + ratedCurrentA: real("rated_current_a").notNull(), + fuseUtilizationCategory: text( + "fuse_utilization_category" + ).$type(), + tripCharacteristic: text( + "trip_characteristic" + ).$type(), + rcdType: text("rcd_type").$type(), + ratedResidualCurrentMa: real("rated_residual_current_ma"), + } +); diff --git a/src/db/schema/distribution-board-components.ts b/src/db/schema/distribution-board-components.ts new file mode 100644 index 0000000..9aad96f --- /dev/null +++ b/src/db/schema/distribution-board-components.ts @@ -0,0 +1,37 @@ +import { integer, sqliteTable, text, unique } from "drizzle-orm/sqlite-core"; +import type { + DistributionBoardComponentPlacement, + DistributionBoardComponentRole, +} from "../../shared/constants/distribution-board-component.js"; +import { circuitLists } from "./circuit-lists.js"; +import { circuitSections } from "./circuit-sections.js"; + +export const distributionBoardComponents = sqliteTable( + "distribution_board_components", + { + id: text("id").primaryKey(), + circuitListId: text("circuit_list_id") + .notNull() + .references(() => circuitLists.id, { onDelete: "cascade" }), + sectionId: text("section_id").references(() => circuitSections.id, { + onDelete: "cascade", + }), + equipmentIdentifier: text("equipment_identifier").notNull(), + name: text("name").notNull(), + role: text("role").$type().notNull(), + placement: text("placement") + .$type() + .notNull(), + sortOrder: integer("sort_order").notNull().default(0), + }, + (table) => [ + unique("distribution_board_components_list_identifier_unique").on( + table.circuitListId, + table.equipmentIdentifier + ), + unique("distribution_board_components_section_role_unique").on( + table.sectionId, + table.role + ), + ] +); diff --git a/tests/distribution-board-component-schema-migration.test.ts b/tests/distribution-board-component-schema-migration.test.ts new file mode 100644 index 0000000..a8132f9 --- /dev/null +++ b/tests/distribution-board-component-schema-migration.test.ts @@ -0,0 +1,466 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { describe, it } from "node:test"; +import Database from "better-sqlite3"; +import { migrate } from "drizzle-orm/better-sqlite3/migrator"; +import { createDatabaseContext } from "../src/db/database-context.js"; + +const migrationPath = path.resolve( + "src", + "db", + "migrations", + "0024_damp_mister_sinister.sql" +); + +function applyComponentSchemaMigration(sqlite: Database.Database): void { + const migrationSql = fs.readFileSync(migrationPath, "utf8"); + for (const statement of migrationSql.split("--> statement-breakpoint")) { + if (statement.trim()) { + sqlite.exec(statement); + } + } +} + +function createLegacySchema(sqlite: Database.Database): void { + sqlite.pragma("foreign_keys = ON"); + sqlite.exec(` + CREATE TABLE circuit_lists ( + id text PRIMARY KEY NOT NULL + ); + CREATE TABLE circuit_sections ( + id text PRIMARY KEY NOT NULL, + circuit_list_id text NOT NULL REFERENCES circuit_lists(id) ON DELETE CASCADE, + key text NOT NULL, + display_name text NOT NULL, + prefix text NOT NULL, + sort_order integer DEFAULT 0 NOT NULL + ); + CREATE TABLE circuits ( + id text PRIMARY KEY NOT NULL, + circuit_list_id text NOT NULL REFERENCES circuit_lists(id) ON DELETE CASCADE, + equipment_identifier text NOT NULL, + protection_type text, + protection_rated_current real, + protection_characteristic text + ); + `); +} + +describe("distribution board component schema migration", () => { + it("creates the additive schema on an empty database", () => { + const context = createDatabaseContext(":memory:"); + try { + migrate(context.db, { + migrationsFolder: path.resolve("src", "db", "migrations"), + }); + + const tables = ( + context.sqlite + .prepare( + `SELECT name + FROM sqlite_master + WHERE type = 'table' + ORDER BY name` + ) + .all() as Array<{ name: string }> + ).map((table) => table.name); + + assert.equal(tables.includes("distribution_board_components"), true); + assert.equal(tables.includes("circuit_protection_devices"), true); + assert.deepEqual( + tables.includes("distribution_board_component_protection_devices"), + true + ); + assert.deepEqual( + tables.includes("circuit_list_equipment_identifiers"), + true + ); + + const sectionColumns = ( + context.sqlite + .prepare("PRAGMA table_info(circuit_sections)") + .all() as Array<{ name: string }> + ).map((column) => column.name); + assert.equal(sectionColumns.includes("category"), true); + assert.equal(sectionColumns.includes("group_number"), true); + } finally { + context.close(); + } + }); + + it("backfills groups and the BMK registry without rewriting existing planning data", () => { + const sqlite = new Database(":memory:"); + try { + createLegacySchema(sqlite); + sqlite.exec(` + INSERT INTO circuit_lists (id) VALUES ('list-1'); + INSERT INTO circuit_sections ( + id, + circuit_list_id, + key, + display_name, + prefix, + sort_order + ) VALUES + ('lighting', 'list-1', 'lighting', 'Beleuchtung', '-1F', 10), + ('single', 'list-1', 'single_phase', '1-phasig', '-2F', 20), + ('three', 'list-1', 'three_phase', '3-phasig', '-3F', 30), + ('unassigned', 'list-1', 'unassigned', 'Nicht zugeordnet', '-9F', 40); + INSERT INTO circuits ( + id, + circuit_list_id, + equipment_identifier, + protection_type, + protection_rated_current, + protection_characteristic + ) VALUES + ('circuit-1', 'list-1', '-1F1', 'legacy fuse', 13, 'legacy'), + ('circuit-2', 'list-1', '-2F4', NULL, NULL, NULL); + `); + + applyComponentSchemaMigration(sqlite); + + assert.deepEqual( + sqlite + .prepare( + `SELECT + id, + category, + group_number AS groupNumber + FROM circuit_sections + ORDER BY sort_order` + ) + .all(), + [ + { id: "lighting", category: "lighting", groupNumber: 1 }, + { id: "single", category: "single_phase", groupNumber: 1 }, + { id: "three", category: "three_phase", groupNumber: 1 }, + { id: "unassigned", category: null, groupNumber: null }, + ] + ); + assert.deepEqual( + sqlite + .prepare( + `SELECT + id, + equipment_identifier AS equipmentIdentifier, + protection_type AS protectionType, + protection_rated_current AS protectionRatedCurrent, + protection_characteristic AS protectionCharacteristic + FROM circuits + ORDER BY id` + ) + .all(), + [ + { + id: "circuit-1", + equipmentIdentifier: "-1F1", + protectionType: "legacy fuse", + protectionRatedCurrent: 13, + protectionCharacteristic: "legacy", + }, + { + id: "circuit-2", + equipmentIdentifier: "-2F4", + protectionType: null, + protectionRatedCurrent: null, + protectionCharacteristic: null, + }, + ] + ); + assert.deepEqual( + sqlite + .prepare( + `SELECT + owner_type AS ownerType, + owner_id AS ownerId, + equipment_identifier AS equipmentIdentifier + FROM circuit_list_equipment_identifiers + ORDER BY owner_id` + ) + .all(), + [ + { + ownerType: "circuit", + ownerId: "circuit-1", + equipmentIdentifier: "-1F1", + }, + { + ownerType: "circuit", + ownerId: "circuit-2", + equipmentIdentifier: "-2F4", + }, + ] + ); + assert.deepEqual( + sqlite + .prepare("SELECT count(*) AS count FROM circuit_protection_devices") + .get(), + { count: 0 } + ); + } finally { + sqlite.close(); + } + }); + + it("keeps the shared BMK registry synchronized and rejects normalized collisions", () => { + const sqlite = new Database(":memory:"); + try { + createLegacySchema(sqlite); + sqlite.exec(` + INSERT INTO circuit_lists (id) VALUES ('list-1'); + INSERT INTO circuit_sections ( + id, + circuit_list_id, + key, + display_name, + prefix + ) VALUES ('lighting', 'list-1', 'lighting', 'Beleuchtung', '-1F'); + INSERT INTO circuits ( + id, + circuit_list_id, + equipment_identifier + ) VALUES ('circuit-1', 'list-1', '-1F1'); + `); + applyComponentSchemaMigration(sqlite); + + sqlite + .prepare( + `INSERT INTO distribution_board_components ( + id, + circuit_list_id, + equipment_identifier, + name, + role, + placement, + sort_order + ) VALUES (?, ?, ?, ?, ?, ?, ?)` + ) + .run( + "main-switch", + "list-1", + "-Q0", + "Hauptschalter", + "main_switch", + "header", + 10 + ); + + assert.throws(() => + sqlite + .prepare( + `INSERT INTO distribution_board_components ( + id, + circuit_list_id, + equipment_identifier, + name, + role, + placement + ) VALUES (?, ?, ?, ?, ?, ?)` + ) + .run( + "duplicate", + "list-1", + " -q0 ", + "Duplikat", + "auxiliary", + "footer" + ) + ); + assert.throws(() => + sqlite + .prepare( + `INSERT INTO distribution_board_components ( + id, + circuit_list_id, + equipment_identifier, + name, + role, + placement + ) VALUES (?, ?, ?, ?, ?, ?)` + ) + .run( + "circuit-duplicate", + "list-1", + "-1f1", + "Duplikat", + "auxiliary", + "footer" + ) + ); + + sqlite + .prepare( + `INSERT INTO circuits ( + id, + circuit_list_id, + equipment_identifier + ) VALUES (?, ?, ?)` + ) + .run("circuit-2", "list-1", "-1F2"); + sqlite + .prepare( + `UPDATE circuits + SET equipment_identifier = ? + WHERE id = ?` + ) + .run("-1F3", "circuit-2"); + + assert.deepEqual( + sqlite + .prepare( + `SELECT equipment_identifier AS equipmentIdentifier + FROM circuit_list_equipment_identifiers + WHERE owner_type = 'circuit' AND owner_id = 'circuit-2'` + ) + .get(), + { equipmentIdentifier: "-1F3" } + ); + + assert.throws(() => + sqlite + .prepare( + `UPDATE circuits + SET equipment_identifier = ? + WHERE id = ?` + ) + .run("-q0", "circuit-2") + ); + assert.deepEqual( + sqlite + .prepare( + `SELECT equipment_identifier AS equipmentIdentifier + FROM circuits + WHERE id = 'circuit-2'` + ) + .get(), + { equipmentIdentifier: "-1F3" } + ); + + sqlite + .prepare("DELETE FROM distribution_board_components WHERE id = ?") + .run("main-switch"); + assert.deepEqual( + sqlite + .prepare( + `SELECT count(*) AS count + FROM circuit_list_equipment_identifiers + WHERE owner_type = 'distribution_board_component'` + ) + .get(), + { count: 0 } + ); + } finally { + sqlite.close(); + } + }); + + it("stores independent one-to-one circuit and component protection state", () => { + const sqlite = new Database(":memory:"); + try { + createLegacySchema(sqlite); + sqlite.exec(` + INSERT INTO circuit_lists (id) VALUES ('list-1'); + INSERT INTO circuit_sections ( + id, + circuit_list_id, + key, + display_name, + prefix + ) VALUES ('lighting', 'list-1', 'lighting', 'Beleuchtung', '-1F'); + INSERT INTO circuits ( + id, + circuit_list_id, + equipment_identifier + ) VALUES ('circuit-1', 'list-1', '-1F1'); + `); + applyComponentSchemaMigration(sqlite); + sqlite.exec(` + INSERT INTO circuit_protection_devices ( + circuit_id, + type, + rated_current_a, + trip_characteristic + ) VALUES ('circuit-1', 'LS', 10, 'B'); + INSERT INTO distribution_board_components ( + id, + circuit_list_id, + section_id, + equipment_identifier, + name, + role, + placement + ) VALUES ( + 'group-rcd', + 'list-1', + 'lighting', + '-1Q1.0', + 'Gruppen-FI', + 'group_residual_current_protection', + 'group' + ); + INSERT INTO distribution_board_component_protection_devices ( + component_id, + type, + rated_current_a, + rcd_type, + rated_residual_current_ma + ) VALUES ('group-rcd', 'FI', 40, 'A', 30); + `); + + assert.deepEqual( + sqlite + .prepare("SELECT * FROM circuit_protection_devices") + .get(), + { + circuit_id: "circuit-1", + type: "LS", + rated_current_a: 10, + fuse_utilization_category: null, + trip_characteristic: "B", + rcd_type: null, + rated_residual_current_ma: null, + } + ); + assert.deepEqual( + sqlite + .prepare( + "SELECT * FROM distribution_board_component_protection_devices" + ) + .get(), + { + component_id: "group-rcd", + type: "FI", + rated_current_a: 40, + fuse_utilization_category: null, + trip_characteristic: null, + rcd_type: "A", + rated_residual_current_ma: 30, + } + ); + + sqlite.prepare("DELETE FROM circuits WHERE id = ?").run("circuit-1"); + sqlite + .prepare("DELETE FROM distribution_board_components WHERE id = ?") + .run("group-rcd"); + assert.deepEqual( + sqlite + .prepare("SELECT count(*) AS count FROM circuit_protection_devices") + .get(), + { count: 0 } + ); + assert.deepEqual( + sqlite + .prepare( + `SELECT count(*) AS count + FROM distribution_board_component_protection_devices` + ) + .get(), + { count: 0 } + ); + } finally { + sqlite.close(); + } + }); +});