From dac19b093c53cebced242b55d3dfa426c00eeda9 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 2 Aug 2026 18:26:22 +0200 Subject: [PATCH] Add external row quantity foundation --- AGENTS.md | 13 +- docs/current-architecture.md | 19 +- .../spec/revit-csv-phase-14-audit-and-plan.md | 5 + scripts/db-verify-circuit-schema.js | 16 + src/db/migrations/0005_stale_gorilla_man.sql | 3 + src/db/migrations/meta/0005_snapshot.json | 2404 +++++++++++++++++ src/db/migrations/meta/_journal.json | 7 + ...t-device-row-project-command.repository.ts | 8 + ...ow-structure-project-command.repository.ts | 6 +- ...ircuit-device-row-structure.persistence.ts | 8 + .../circuit-device-row.persistence.ts | 4 + .../circuit-device-row.repository.ts | 1 + ...roup-subtree-project-command.repository.ts | 6 +- ...it-structure-project-command.repository.ts | 6 +- ...oard-subtree-project-command.repository.ts | 6 +- .../project-state-snapshot.persistence.ts | 1 + src/db/schema/circuit-device-rows.ts | 1 + .../circuit-device-row-quantity.ts | 43 + ...ircuit-device-row-project-command.model.ts | 16 +- ...ice-row-structure-project-command.model.ts | 20 +- src/domain/models/circuit-device-row.model.ts | 1 + src/domain/models/circuit-tree.model.ts | 1 + .../models/project-state-snapshot.model.ts | 43 +- src/frontend/types.ts | 1 + .../controllers/circuit-tree.controller.ts | 1 + ...ice-row-project-command.repository.test.ts | 24 + tests/circuit-device-row-quantity.test.ts | 62 + tests/external-model-foundation.test.ts | 3 +- tests/project-command.model.test.ts | 10 +- tests/project-state-snapshot.test.ts | 33 + 30 files changed, 2750 insertions(+), 22 deletions(-) create mode 100644 src/db/migrations/0005_stale_gorilla_man.sql create mode 100644 src/db/migrations/meta/0005_snapshot.json create mode 100644 src/domain/calculations/circuit-device-row-quantity.ts create mode 100644 tests/circuit-device-row-quantity.test.ts diff --git a/AGENTS.md b/AGENTS.md index e15bf4c..054b9c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -250,7 +250,9 @@ not change the project revision or undo/redo stacks. Restoring a server-stored snapshot verifies its checksum and the current-state hash, replaces supported project data atomically and records a new `restore` revision with a complete inverse command. Restore can therefore be undone and redone after a restart. -Current snapshot schema version 4 additionally stores the complete confirmed +Current snapshot schema version 5 additionally stores the manual quantity share +of every CircuitDeviceRow. Version 4 snapshots are upgraded with +`manualQuantity = quantity`; they already contain the complete confirmed external-model source, import batches, room mappings and objects. Version 3 snapshots are upgraded with an empty external-model state; version 2 snapshots are additionally upgraded with an empty external CSV configuration. Supported @@ -263,7 +265,7 @@ project graph. Project-wide Revit CSV configuration uses `external-csv-configuration.update`. Its complete expected/target snapshot is persisted through the shared project-command transaction, so create, update, -delete, Undo and Redo are revision-safe. Snapshot schema 4, restore and portable +delete, Undo and Redo are revision-safe. Snapshot schema 5, restore and portable project duplication preserve the configuration; duplication remaps its internal id and project link. The project page exposes the Revit CSV modal. It edits the complete @@ -277,12 +279,17 @@ immutable import batches with original bytes plus the classified cell matrix, normalized source-room mappings and external objects unique by `(sourceId, ifcGuid)`. Source values, planning values and overrides are separate. The general external-model repository is read-only; runtime writes belong only -to typed command adapters. Snapshot/restore and portable project transfer schema version 4 +to typed command adapters. Snapshot/restore and portable project transfer schema version 5 capture the complete external state and remap every internal UUID and link while preserving IFCGUIDs, source values, original bytes and the classified matrix. Import batches additionally store the monotonic project CSV configuration version introduced by additive migration `0004`; it is distinct from the CSV configuration JSON schema version. +Phase 14.3 migration `0005` adds `CircuitDeviceRow.manualQuantity`, backfills +existing rows from `quantity` and keeps `quantity` as the materialized total. +Normal manual row inserts and quantity edits update both values together. Future +external-object assignment commands must set the total to `manualQuantity` plus +the sum of linked objects' effective quantities in the same transaction. Confirmed initial state is written only through `external-import.apply-initial`. The command rechecks configuration version, original-byte SHA-256, parsed matrix, complete IFCGUID/source values, explicit diff --git a/docs/current-architecture.md b/docs/current-architecture.md index cb46030..f55baa7 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -416,7 +416,9 @@ wieder her. verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und ist für bestehende sowie neu angelegte Verteilungen standardmäßig `1`. -Snapshot-Schema 4 enthält zusätzlich den vollständigen externen Modellzustand +Snapshot-Schema 5 enthält zusätzlich den manuellen Mengenanteil jeder +Gerätezeile. Version 4 wird mit `manualQuantity = quantity` hochgestuft und +enthält bereits den vollständigen externen Modellzustand aus Quelle, Batches, Raum-Mappings und Objekten. Es enthält außerdem die projektweite Revit-CSV-Konfiguration, die Gebäudekennzeichnung sowie den Gleichzeitigkeitsfaktor, Gruppenkategorie und -nummer, Verteilerkomponenten sowie die getrennten @@ -477,7 +479,7 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät. Ein triggergeführtes Register erzwingt bereits eine normalisierte, stromkreislistenweite BMK-Eindeutigkeit über Stromkreise und Verteilerkomponenten. Snapshot- und Transfer-Integration verwenden aktuell - Snapshot-Schema 4. Persistente Insert/Delete/Update-Commands für + Snapshot-Schema 5. Persistente Insert/Delete/Update-Commands für veränderliche Verteilerkomponenten, Gruppen einschließlich befüllter Unterbäume sowie vollständige Gruppensortierung sind integriert. Der Editor zeigt die geschützte Struktur an und bearbeitet veränderliche Gruppen- und @@ -500,7 +502,7 @@ einschließlich BOM, CRLF und Quotierung bytegleich zurück. Der lokale Befehl `external_csv_configurations` enthält höchstens eine versionierte Konfiguration je Projekt. `external-csv-configuration.update` schreibt Anlage, Änderung oder -Entfernung zusammen mit Revision und persistentem Undo/Redo. Snapshot-Schema 4, +Entfernung zusammen mit Revision und persistentem Undo/Redo. Snapshot-Schema 5, Restore und portabler Projekttransfer enthalten diesen Zustand; beim Duplizieren werden Konfigurations-UUID und Projektlink remapped. @@ -516,7 +518,9 @@ und CircuitDeviceRow bleiben nullable. `ExternalModelStateRepository` liest den vollständigen Zustand deterministisch und unterscheidet unbekannte von noch nicht importierten Projekten. Der letzte bestätigte Batch wird aus der Batchreihenfolge abgeleitet statt redundant an der Quelle gespeichert. -Snapshot/Transfer v4 erfasst Originalbytes, Matrix und alle internen Links, +Snapshot/Transfer v5 erfasst zusätzlich `manualQuantity`; Version 4 wird mit +dem bisherigen Gesamtwert als manuellem Anteil hochgestuft. Originalbytes, +Matrix und alle internen Links werden weiterhin vollständig erfasst, stellt sie in Fremdschlüsselreihenfolge wieder her und remappt beim Projektduplikat jede interne UUID; IFCGUID und Quelltransport bleiben unverändert. `external-import.apply-initial` ist die atomare Schreibgrenze für @@ -529,6 +533,13 @@ Vorschlag entsprechen. Der Command erzeugt und verknüpft ausdrücklich keine CircuitDeviceRow; Undo entfernt den vollständigen unveränderten Importzustand, Redo verwendet dieselben UUIDs und Bytes. Bloße Vorschau und Importplanung erzeugen weiterhin keine dieser Zeilen. + +Migration `0005` trennt bei `CircuitDeviceRow` den manuellen Mengenanteil vom +weiterhin materialisierten Gesamtwert. Bestehende Daten werden mit +`manualQuantity = quantity` übernommen. Die Fachregel für spätere externe +Zuordnungs-Commands lautet `quantity = manualQuantity + Summe der +effectiveQuantity aller verknüpften ExternalModelObjects`; Link und beide +Mengenwerte müssen innerhalb desselben Projekt-Commands wechseln. `GET` und `PUT /api/projects/:projectId/external-csv/configuration` lesen oder ändern die Konfiguration; der PUT plant Identität und nächsten Konfigurationsstand serverseitig und verwendet den typisierten Command. diff --git a/docs/spec/revit-csv-phase-14-audit-and-plan.md b/docs/spec/revit-csv-phase-14-audit-and-plan.md index f959dc4..7fb4f88 100644 --- a/docs/spec/revit-csv-phase-14-audit-and-plan.md +++ b/docs/spec/revit-csv-phase-14-audit-and-plan.md @@ -313,6 +313,11 @@ Kombination aus Originalbytes und Matrix als Entscheidungen bestätigt. ### 14.3 – Editor und explizite Zuordnung 1. `manualQuantity` und Mengeninvarianten migrieren und testen. + **Erledigt:** Migration `0005` übernimmt für jede bestehende Gerätezeile + `manualQuantity = quantity`. Neue normale Zeilen und Mengenänderungen führen + beide Werte gemeinsam; Snapshot/Transfer v5 erhält den Anteil und stuft v4 + verlustfrei hoch. Die reine Mengenregel validiert den materialisierten Wert + als manuellen Anteil plus Summe der unteilbaren externen Objektmengen. 2. Commands für vorhandene Row, neue Row, neuen Circuit und Lösen ergänzen. 3. Verteilungsbezogenen Drawer, Filter und Vorschau der Mengenwirkung ergänzen. 4. Einzel- und Mehrfach-Drag-and-drop samt Warnungen, Undo/Redo und Reload diff --git a/scripts/db-verify-circuit-schema.js b/scripts/db-verify-circuit-schema.js index 318e549..8680f59 100644 --- a/scripts/db-verify-circuit-schema.js +++ b/scripts/db-verify-circuit-schema.js @@ -40,6 +40,13 @@ const projectDeviceColumns = new Set( const missingProjectDeviceColumns = requiredProjectDeviceColumns.filter( (name) => !projectDeviceColumns.has(name) ); +const requiredCircuitDeviceRowColumns = ["manual_quantity"]; +const circuitDeviceRowColumns = new Set( + db.prepare("PRAGMA table_info(circuit_device_rows)").all().map((column) => column.name) +); +const missingCircuitDeviceRowColumns = requiredCircuitDeviceRowColumns.filter( + (name) => !circuitDeviceRowColumns.has(name) +); const removedProjectDeviceColumns = [ "installed_power_per_unit_kw", "demand_factor", @@ -131,6 +138,7 @@ console.log("Database:", dbPath); 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-device-row columns:", requiredCircuitDeviceRowColumns.join(", ")); console.log("Required circuit columns:", requiredCircuitColumns.join(", ")); console.log( "Required circuit-section columns:", @@ -171,6 +179,14 @@ if (missingCircuitColumns.length > 0) { process.exit(1); } +if (missingCircuitDeviceRowColumns.length > 0) { + console.error( + "Missing circuit-device-row columns:", + missingCircuitDeviceRowColumns.join(", ") + ); + process.exit(1); +} + if (remainingRemovedCircuitColumns.length > 0) { console.error( "Transitional circuit protection columns still present:", diff --git a/src/db/migrations/0005_stale_gorilla_man.sql b/src/db/migrations/0005_stale_gorilla_man.sql new file mode 100644 index 0000000..765d7bc --- /dev/null +++ b/src/db/migrations/0005_stale_gorilla_man.sql @@ -0,0 +1,3 @@ +ALTER TABLE `circuit_device_rows` ADD `manual_quantity` integer DEFAULT 0 NOT NULL; +--> statement-breakpoint +UPDATE `circuit_device_rows` SET `manual_quantity` = `quantity`; diff --git a/src/db/migrations/meta/0005_snapshot.json b/src/db/migrations/meta/0005_snapshot.json new file mode 100644 index 0000000..acae98c --- /dev/null +++ b/src/db/migrations/meta/0005_snapshot.json @@ -0,0 +1,2404 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "b79934fe-5d05-49da-b7c4-95c954e5fec2", + "prevId": "da7eb583-b86a-440c-a71e-babb5ee24cd1", + "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 + }, + "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 + }, + "manual_quantity": { + "name": "manual_quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "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 + }, + "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": {} + }, + "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": {} + }, + "external_csv_configurations": { + "name": "external_csv_configurations", + "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 + }, + "configuration_version": { + "name": "configuration_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration": { + "name": "configuration", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_csv_configurations_project_id_unique": { + "name": "external_csv_configurations_project_id_unique", + "columns": [ + "project_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "external_csv_configurations_project_id_projects_id_fk": { + "name": "external_csv_configurations_project_id_projects_id_fk", + "tableFrom": "external_csv_configurations", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_import_batches": { + "name": "external_import_batches", + "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_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "import_kind": { + "name": "import_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imported_at_iso": { + "name": "imported_at_iso", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applied_project_revision": { + "name": "applied_project_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration_version": { + "name": "configuration_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "configuration_snapshot": { + "name": "configuration_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "original_bytes": { + "name": "original_bytes", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "document": { + "name": "document", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "external_import_batches_project_revision_idx": { + "name": "external_import_batches_project_revision_idx", + "columns": [ + "project_id", + "applied_project_revision" + ], + "isUnique": false + }, + "external_import_batches_source_imported_idx": { + "name": "external_import_batches_source_imported_idx", + "columns": [ + "source_id", + "imported_at_iso" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_import_batches_project_id_projects_id_fk": { + "name": "external_import_batches_project_id_projects_id_fk", + "tableFrom": "external_import_batches", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_import_batches_source_id_external_model_sources_id_fk": { + "name": "external_import_batches_source_id_external_model_sources_id_fk", + "tableFrom": "external_import_batches", + "tableTo": "external_model_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_model_objects": { + "name": "external_model_objects", + "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_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ifc_guid": { + "name": "ifc_guid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_seen_import_batch_id": { + "name": "last_seen_import_batch_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_accepted_import_batch_id": { + "name": "last_accepted_import_batch_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accepted_source_values": { + "name": "accepted_source_values", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "planning_values": { + "name": "planning_values", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "overridden_fields": { + "name": "overridden_fields", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_room_mapping_id": { + "name": "external_room_mapping_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "distribution_board_id": { + "name": "distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "linked_project_device_id": { + "name": "linked_project_device_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "circuit_device_row_id": { + "name": "circuit_device_row_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "presence_status": { + "name": "presence_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'present'" + } + }, + "indexes": { + "external_model_objects_source_ifc_guid_unique": { + "name": "external_model_objects_source_ifc_guid_unique", + "columns": [ + "source_id", + "ifc_guid" + ], + "isUnique": true + }, + "external_model_objects_project_presence_idx": { + "name": "external_model_objects_project_presence_idx", + "columns": [ + "project_id", + "presence_status" + ], + "isUnique": false + }, + "external_model_objects_distribution_board_idx": { + "name": "external_model_objects_distribution_board_idx", + "columns": [ + "distribution_board_id" + ], + "isUnique": false + }, + "external_model_objects_circuit_device_row_idx": { + "name": "external_model_objects_circuit_device_row_idx", + "columns": [ + "circuit_device_row_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_model_objects_project_id_projects_id_fk": { + "name": "external_model_objects_project_id_projects_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_model_objects_source_id_external_model_sources_id_fk": { + "name": "external_model_objects_source_id_external_model_sources_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_model_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_model_objects_last_seen_import_batch_id_external_import_batches_id_fk": { + "name": "external_model_objects_last_seen_import_batch_id_external_import_batches_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_import_batches", + "columnsFrom": [ + "last_seen_import_batch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "external_model_objects_last_accepted_import_batch_id_external_import_batches_id_fk": { + "name": "external_model_objects_last_accepted_import_batch_id_external_import_batches_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_import_batches", + "columnsFrom": [ + "last_accepted_import_batch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "external_model_objects_external_room_mapping_id_external_room_mappings_id_fk": { + "name": "external_model_objects_external_room_mapping_id_external_room_mappings_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "external_room_mappings", + "columnsFrom": [ + "external_room_mapping_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_model_objects_distribution_board_id_distribution_boards_id_fk": { + "name": "external_model_objects_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "distribution_boards", + "columnsFrom": [ + "distribution_board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_model_objects_linked_project_device_id_project_devices_id_fk": { + "name": "external_model_objects_linked_project_device_id_project_devices_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "project_devices", + "columnsFrom": [ + "linked_project_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_model_objects_circuit_device_row_id_circuit_device_rows_id_fk": { + "name": "external_model_objects_circuit_device_row_id_circuit_device_rows_id_fk", + "tableFrom": "external_model_objects", + "tableTo": "circuit_device_rows", + "columnsFrom": [ + "circuit_device_row_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_model_sources": { + "name": "external_model_sources", + "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 + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'revit_csv'" + } + }, + "indexes": { + "external_model_sources_project_type_unique": { + "name": "external_model_sources_project_type_unique", + "columns": [ + "project_id", + "source_type" + ], + "isUnique": true + } + }, + "foreignKeys": { + "external_model_sources_project_id_projects_id_fk": { + "name": "external_model_sources_project_id_projects_id_fk", + "tableFrom": "external_model_sources", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "external_room_mappings": { + "name": "external_room_mappings", + "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_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "normalized_source_room_key": { + "name": "normalized_source_room_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_floor_name": { + "name": "source_floor_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_room_number": { + "name": "source_room_number", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_room_name": { + "name": "source_room_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "room_id": { + "name": "room_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_distribution_board_id": { + "name": "default_distribution_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "external_room_mappings_source_key_unique": { + "name": "external_room_mappings_source_key_unique", + "columns": [ + "source_id", + "normalized_source_room_key" + ], + "isUnique": true + }, + "external_room_mappings_project_room_idx": { + "name": "external_room_mappings_project_room_idx", + "columns": [ + "project_id", + "room_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "external_room_mappings_project_id_projects_id_fk": { + "name": "external_room_mappings_project_id_projects_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_room_mappings_source_id_external_model_sources_id_fk": { + "name": "external_room_mappings_source_id_external_model_sources_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "external_model_sources", + "columnsFrom": [ + "source_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "external_room_mappings_room_id_rooms_id_fk": { + "name": "external_room_mappings_room_id_rooms_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "external_room_mappings_default_distribution_board_id_distribution_boards_id_fk": { + "name": "external_room_mappings_default_distribution_board_id_distribution_boards_id_fk", + "tableFrom": "external_room_mappings", + "tableTo": "distribution_boards", + "columnsFrom": [ + "default_distribution_board_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": {} + }, + "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 + }, + "is_public_building": { + "name": "is_public_building", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 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 e3f76b7..5a65548 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1785684457208, "tag": "0004_illegal_the_stranger", "breakpoints": true + }, + { + "idx": 5, + "version": "6", + "when": 1785687503453, + "tag": "0005_stale_gorilla_man", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/repositories/circuit-device-row-project-command.repository.ts b/src/db/repositories/circuit-device-row-project-command.repository.ts index 1eba241..ecfa646 100644 --- a/src/db/repositories/circuit-device-row-project-command.repository.ts +++ b/src/db/repositories/circuit-device-row-project-command.repository.ts @@ -82,6 +82,14 @@ export class CircuitDeviceRowProjectCommandRepository change.value, ]) ) as CircuitDeviceRowPatchInput; + const targetQuantity = patch.quantity ?? current.quantity; + const targetManualQuantity = + patch.manualQuantity ?? current.manualQuantity; + if (targetManualQuantity > targetQuantity) { + throw new Error( + "Device-row manual quantity must not exceed total quantity." + ); + } if ( input.source === "user" && patch.phaseType !== undefined && diff --git a/src/db/repositories/circuit-device-row-structure-project-command.repository.ts b/src/db/repositories/circuit-device-row-structure-project-command.repository.ts index 4d9532d..6919d20 100644 --- a/src/db/repositories/circuit-device-row-structure-project-command.repository.ts +++ b/src/db/repositories/circuit-device-row-structure-project-command.repository.ts @@ -20,6 +20,7 @@ import { circuitLists } from "../schema/circuit-lists.js"; import { circuits } from "../schema/circuits.js"; import { assertCircuitDeviceRowReferencesInProject, + toCircuitDeviceRowInsertValues, toCircuitDeviceRowSnapshot, } from "./circuit-device-row-structure.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; @@ -92,7 +93,10 @@ export class CircuitDeviceRowStructureProjectCommandRepository throw new Error("Circuit device-row id already exists."); } - database.insert(circuitDeviceRows).values(row).run(); + database + .insert(circuitDeviceRows) + .values(toCircuitDeviceRowInsertValues(row)) + .run(); const circuitUpdate = database .update(circuits) .set({ isReserve: 0 }) diff --git a/src/db/repositories/circuit-device-row-structure.persistence.ts b/src/db/repositories/circuit-device-row-structure.persistence.ts index fffaff4..6e567d4 100644 --- a/src/db/repositories/circuit-device-row-structure.persistence.ts +++ b/src/db/repositories/circuit-device-row-structure.persistence.ts @@ -61,6 +61,7 @@ export function toCircuitDeviceRowSnapshot( roomNumberSnapshot: row.roomNumberSnapshot, roomNameSnapshot: row.roomNameSnapshot, quantity: row.quantity, + manualQuantity: row.manualQuantity, powerPerUnit: row.powerPerUnit, simultaneityFactor: row.simultaneityFactor, cosPhi: row.cosPhi, @@ -68,3 +69,10 @@ export function toCircuitDeviceRowSnapshot( overriddenFields: row.overriddenFields, }; } + +export function toCircuitDeviceRowInsertValues(row: CircuitDeviceRowSnapshot) { + return { + ...row, + manualQuantity: row.manualQuantity ?? row.quantity, + }; +} diff --git a/src/db/repositories/circuit-device-row.persistence.ts b/src/db/repositories/circuit-device-row.persistence.ts index e1f9222..c075f61 100644 --- a/src/db/repositories/circuit-device-row.persistence.ts +++ b/src/db/repositories/circuit-device-row.persistence.ts @@ -13,6 +13,7 @@ export interface CircuitDeviceRowUpdateInput { roomNumberSnapshot?: string; roomNameSnapshot?: string; quantity: number; + manualQuantity?: number; powerPerUnit: number; simultaneityFactor: number; cosPhi?: number; @@ -34,6 +35,7 @@ export interface CircuitDeviceRowPatchInput { roomNumberSnapshot?: string | null; roomNameSnapshot?: string | null; quantity?: number; + manualQuantity?: number; powerPerUnit?: number; simultaneityFactor?: number; cosPhi?: number | null; @@ -61,6 +63,7 @@ export function toCircuitDeviceRowUpdateValues(input: CircuitDeviceRowUpdateInpu roomNumberSnapshot: input.roomNumberSnapshot ?? null, roomNameSnapshot: input.roomNameSnapshot ?? null, quantity: input.quantity, + manualQuantity: input.manualQuantity ?? input.quantity, powerPerUnit: input.powerPerUnit, simultaneityFactor: input.simultaneityFactor, cosPhi: input.cosPhi ?? null, @@ -90,6 +93,7 @@ export function toCircuitDeviceRowPatchValues(input: CircuitDeviceRowPatchInput) } if (has("roomNameSnapshot")) values.roomNameSnapshot = input.roomNameSnapshot ?? null; if (input.quantity !== undefined) values.quantity = input.quantity; + if (input.manualQuantity !== undefined) values.manualQuantity = input.manualQuantity; if (input.powerPerUnit !== undefined) values.powerPerUnit = input.powerPerUnit; if (input.simultaneityFactor !== undefined) { values.simultaneityFactor = input.simultaneityFactor; diff --git a/src/db/repositories/circuit-device-row.repository.ts b/src/db/repositories/circuit-device-row.repository.ts index 6bdb550..99c0860 100644 --- a/src/db/repositories/circuit-device-row.repository.ts +++ b/src/db/repositories/circuit-device-row.repository.ts @@ -37,6 +37,7 @@ export class CircuitDeviceRowRepository { roomNumberSnapshot: circuitDeviceRows.roomNumberSnapshot, roomNameSnapshot: circuitDeviceRows.roomNameSnapshot, quantity: circuitDeviceRows.quantity, + manualQuantity: circuitDeviceRows.manualQuantity, powerPerUnit: circuitDeviceRows.powerPerUnit, simultaneityFactor: circuitDeviceRows.simultaneityFactor, cosPhi: circuitDeviceRows.cosPhi, diff --git a/src/db/repositories/circuit-group-subtree-project-command.repository.ts b/src/db/repositories/circuit-group-subtree-project-command.repository.ts index 605146d..448cf99 100644 --- a/src/db/repositories/circuit-group-subtree-project-command.repository.ts +++ b/src/db/repositories/circuit-group-subtree-project-command.repository.ts @@ -24,6 +24,7 @@ import { circuitSections } from "../schema/circuit-sections.js"; import { circuits } from "../schema/circuits.js"; import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js"; import { distributionBoardComponents } from "../schema/distribution-board-components.js"; +import { toCircuitDeviceRowInsertValues } from "./circuit-device-row-structure.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; export class CircuitGroupSubtreeProjectCommandRepository @@ -212,7 +213,10 @@ export class CircuitGroupSubtreeProjectCommandRepository .run(); } if (deviceRows.length > 0) { - database.insert(circuitDeviceRows).values(deviceRows).run(); + database + .insert(circuitDeviceRows) + .values(deviceRows.map(toCircuitDeviceRowInsertValues)) + .run(); } } } diff --git a/src/db/repositories/circuit-structure-project-command.repository.ts b/src/db/repositories/circuit-structure-project-command.repository.ts index 8e76e99..454028e 100644 --- a/src/db/repositories/circuit-structure-project-command.repository.ts +++ b/src/db/repositories/circuit-structure-project-command.repository.ts @@ -22,6 +22,7 @@ import { circuitSections } from "../schema/circuit-sections.js"; import { circuits } from "../schema/circuits.js"; import { assertCircuitDeviceRowReferencesInProject, + toCircuitDeviceRowInsertValues, toCircuitDeviceRowSnapshot, } from "./circuit-device-row-structure.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; @@ -166,7 +167,10 @@ export class CircuitStructureProjectCommandRepository }) .run(); if (snapshot.deviceRows.length > 0) { - database.insert(circuitDeviceRows).values(snapshot.deviceRows).run(); + database + .insert(circuitDeviceRows) + .values(snapshot.deviceRows.map(toCircuitDeviceRowInsertValues)) + .run(); } if (snapshot.protectionDevice) { database diff --git a/src/db/repositories/distribution-board-subtree-project-command.repository.ts b/src/db/repositories/distribution-board-subtree-project-command.repository.ts index 0fab26d..63038eb 100644 --- a/src/db/repositories/distribution-board-subtree-project-command.repository.ts +++ b/src/db/repositories/distribution-board-subtree-project-command.repository.ts @@ -29,6 +29,7 @@ import { floors } from "../schema/floors.js"; import { projectDevices } from "../schema/project-devices.js"; import { projects } from "../schema/projects.js"; import { rooms } from "../schema/rooms.js"; +import { toCircuitDeviceRowInsertValues } from "./circuit-device-row-structure.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; export class DistributionBoardSubtreeProjectCommandRepository @@ -253,7 +254,10 @@ export class DistributionBoardSubtreeProjectCommandRepository .run(); } if (deviceRows.length > 0) { - database.insert(circuitDeviceRows).values(deviceRows).run(); + database + .insert(circuitDeviceRows) + .values(deviceRows.map(toCircuitDeviceRowInsertValues)) + .run(); } } } diff --git a/src/db/repositories/project-state-snapshot.persistence.ts b/src/db/repositories/project-state-snapshot.persistence.ts index 1fb96da..4dbca90 100644 --- a/src/db/repositories/project-state-snapshot.persistence.ts +++ b/src/db/repositories/project-state-snapshot.persistence.ts @@ -192,6 +192,7 @@ export function readProjectStateSnapshot( roomNumberSnapshot: circuitDeviceRows.roomNumberSnapshot, roomNameSnapshot: circuitDeviceRows.roomNameSnapshot, quantity: circuitDeviceRows.quantity, + manualQuantity: circuitDeviceRows.manualQuantity, powerPerUnit: circuitDeviceRows.powerPerUnit, simultaneityFactor: circuitDeviceRows.simultaneityFactor, cosPhi: circuitDeviceRows.cosPhi, diff --git a/src/db/schema/circuit-device-rows.ts b/src/db/schema/circuit-device-rows.ts index 805b590..a5da01e 100644 --- a/src/db/schema/circuit-device-rows.ts +++ b/src/db/schema/circuit-device-rows.ts @@ -25,6 +25,7 @@ export const circuitDeviceRows = sqliteTable("circuit_device_rows", { roomNumberSnapshot: text("room_number_snapshot"), roomNameSnapshot: text("room_name_snapshot"), quantity: integer("quantity").notNull(), + manualQuantity: integer("manual_quantity").notNull().default(0), powerPerUnit: real("power_per_unit").notNull(), simultaneityFactor: real("simultaneity_factor").notNull(), cosPhi: real("cos_phi"), diff --git a/src/domain/calculations/circuit-device-row-quantity.ts b/src/domain/calculations/circuit-device-row-quantity.ts new file mode 100644 index 0000000..3833e57 --- /dev/null +++ b/src/domain/calculations/circuit-device-row-quantity.ts @@ -0,0 +1,43 @@ +export interface ExternalQuantityContribution { + effectiveQuantity: number; +} + +export function calculateCircuitDeviceRowQuantity( + manualQuantity: number, + externalObjects: ExternalQuantityContribution[] +) { + assertNonNegativeFinite(manualQuantity, "Manual quantity"); + return externalObjects.reduce((quantity, object) => { + assertPositiveFinite(object.effectiveQuantity, "External effective quantity"); + return quantity + object.effectiveQuantity; + }, manualQuantity); +} + +export function assertCircuitDeviceRowQuantity(input: { + quantity: number; + manualQuantity: number; + externalObjects: ExternalQuantityContribution[]; +}) { + assertNonNegativeFinite(input.quantity, "Materialized quantity"); + const expected = calculateCircuitDeviceRowQuantity( + input.manualQuantity, + input.externalObjects + ); + if (input.quantity !== expected) { + throw new Error( + `Materialized quantity ${input.quantity} does not match manual and external quantity ${expected}.` + ); + } +} + +function assertNonNegativeFinite(value: number, label: string) { + if (!Number.isFinite(value) || value < 0) { + throw new Error(`${label} must be a non-negative finite number.`); + } +} + +function assertPositiveFinite(value: number, label: string) { + if (!Number.isFinite(value) || value <= 0) { + throw new Error(`${label} must be a positive finite number.`); + } +} diff --git a/src/domain/models/circuit-device-row-project-command.model.ts b/src/domain/models/circuit-device-row-project-command.model.ts index 5e79d04..825701f 100644 --- a/src/domain/models/circuit-device-row-project-command.model.ts +++ b/src/domain/models/circuit-device-row-project-command.model.ts @@ -18,6 +18,7 @@ export interface CircuitDeviceRowUpdateValues { roomNumberSnapshot: string | null; roomNameSnapshot: string | null; quantity: number; + manualQuantity: number; powerPerUnit: number; simultaneityFactor: number; cosPhi: number | null; @@ -51,7 +52,18 @@ export function createCircuitDeviceRowUpdateProjectCommand( rowId: string, patch: CircuitDeviceRowUpdatePatch ): CircuitDeviceRowUpdateProjectCommand { - const changes = Object.entries(patch).map(([field, value]) => ({ + const normalizedPatch = + patch.quantity !== undefined && patch.manualQuantity === undefined + ? { ...patch, manualQuantity: patch.quantity } + : patch; + if ( + normalizedPatch.quantity !== undefined && + normalizedPatch.manualQuantity !== undefined && + normalizedPatch.manualQuantity > normalizedPatch.quantity + ) { + throw new Error("manualQuantity must not exceed quantity."); + } + const changes = Object.entries(normalizedPatch).map(([field, value]) => ({ field: field as CircuitDeviceRowUpdateField, value, })) as CircuitDeviceRowUpdateFieldChange[]; @@ -125,6 +137,7 @@ function assertCircuitDeviceRowUpdateFieldValue( } if ( field === "quantity" || + field === "manualQuantity" || field === "powerPerUnit" || field === "simultaneityFactor" ) { @@ -164,6 +177,7 @@ function isCircuitDeviceRowUpdateField( "roomNumberSnapshot", "roomNameSnapshot", "quantity", + "manualQuantity", "powerPerUnit", "simultaneityFactor", "cosPhi", diff --git a/src/domain/models/circuit-device-row-structure-project-command.model.ts b/src/domain/models/circuit-device-row-structure-project-command.model.ts index a897b6b..d6e3cf6 100644 --- a/src/domain/models/circuit-device-row-structure-project-command.model.ts +++ b/src/domain/models/circuit-device-row-structure-project-command.model.ts @@ -22,6 +22,7 @@ export interface CircuitDeviceRowSnapshot { roomNumberSnapshot: string | null; roomNameSnapshot: string | null; quantity: number; + manualQuantity?: number; powerPerUnit: number; simultaneityFactor: number; cosPhi: number | null; @@ -57,10 +58,14 @@ export type CircuitDeviceRowStructureProjectCommand = export function createCircuitDeviceRowInsertProjectCommand( row: CircuitDeviceRowSnapshot ): CircuitDeviceRowInsertProjectCommand { + const normalizedRow = { + ...row, + manualQuantity: row.manualQuantity ?? row.quantity, + }; const command: CircuitDeviceRowInsertProjectCommand = { schemaVersion: circuitDeviceRowStructureCommandSchemaVersion, type: circuitDeviceRowInsertCommandType, - payload: { row }, + payload: { row: normalizedRow }, }; assertCircuitDeviceRowInsertProjectCommand(command); return command; @@ -117,7 +122,13 @@ export function assertCircuitDeviceRowInsertProjectCommand( ] as const) { assertNullableString(row[field], `row.${field}`); } - assertNonNegativeNumber(row.quantity, "row.quantity"); + const quantity = row.quantity; + assertNonNegativeNumber(quantity, "row.quantity"); + const manualQuantity = row.manualQuantity ?? quantity; + assertNonNegativeNumber(manualQuantity, "row.manualQuantity"); + if (manualQuantity > quantity) { + throw new Error("row.manualQuantity must not exceed row.quantity."); + } assertNonNegativeNumber(row.powerPerUnit, "row.powerPerUnit"); assertNonNegativeNumber( row.simultaneityFactor, @@ -172,7 +183,10 @@ function assertFiniteNumber( } } -function assertNonNegativeNumber(value: unknown, field: string) { +function assertNonNegativeNumber( + value: unknown, + field: string +): asserts value is number { assertFiniteNumber(value, field); if (value < 0) { throw new Error(`${field} must not be negative.`); diff --git a/src/domain/models/circuit-device-row.model.ts b/src/domain/models/circuit-device-row.model.ts index 2b6c11c..f18c63c 100644 --- a/src/domain/models/circuit-device-row.model.ts +++ b/src/domain/models/circuit-device-row.model.ts @@ -14,6 +14,7 @@ export interface CircuitDeviceRow { roomNumberSnapshot?: string; roomNameSnapshot?: string; quantity: number; + manualQuantity: number; powerPerUnit: number; simultaneityFactor: number; cosPhi?: number; diff --git a/src/domain/models/circuit-tree.model.ts b/src/domain/models/circuit-tree.model.ts index 37d68ca..71fb743 100644 --- a/src/domain/models/circuit-tree.model.ts +++ b/src/domain/models/circuit-tree.model.ts @@ -13,6 +13,7 @@ export interface CircuitTreeDeviceRow { roomNumberSnapshot?: string; roomNameSnapshot?: string; quantity: number; + manualQuantity: number; powerPerUnit: number; simultaneityFactor: number; cosPhi?: number; diff --git a/src/domain/models/project-state-snapshot.model.ts b/src/domain/models/project-state-snapshot.model.ts index 77ad47f..060c795 100644 --- a/src/domain/models/project-state-snapshot.model.ts +++ b/src/domain/models/project-state-snapshot.model.ts @@ -27,8 +27,9 @@ import { type ExternalModelStateSnapshot, } from "../../external-model/domain/external-model-contracts.js"; -export const projectStateSnapshotSchemaVersion = 4 as const; -const previousProjectStateSnapshotSchemaVersion = 3 as const; +export const projectStateSnapshotSchemaVersion = 5 as const; +const previousProjectStateSnapshotSchemaVersion = 4 as const; +const externalModelProjectStateSnapshotSchemaVersion = 3 as const; const legacyProjectStateSnapshotSchemaVersion = 2 as const; const baselineProjectStateSnapshotSchemaVersion = 1 as const; @@ -103,8 +104,15 @@ const circuitSectionSchema = z } }); -const circuitDeviceRowSchema = z - .object({ +const circuitDeviceRowSchema = z.preprocess( + (value) => { + if (value === null || typeof value !== "object" || Array.isArray(value)) return value; + const row = value as Record; + return Object.prototype.hasOwnProperty.call(row, "manualQuantity") + ? row + : { ...row, manualQuantity: row.quantity }; + }, + z.object({ id: idSchema, circuitId: idSchema, linkedProjectDeviceId: idSchema.nullable(), @@ -120,13 +128,15 @@ const circuitDeviceRowSchema = z roomNumberSnapshot: nullableStringSchema, roomNameSnapshot: nullableStringSchema, quantity: finiteNumberSchema.nonnegative(), + manualQuantity: finiteNumberSchema.nonnegative(), powerPerUnit: finiteNumberSchema.nonnegative(), simultaneityFactor: finiteNumberSchema.nonnegative(), cosPhi: finiteNumberSchema.positive().nullable(), remark: nullableStringSchema, overriddenFields: nullableStringSchema, }) - .strict(); + .strict() +); const circuitSchema = z .object({ @@ -453,6 +463,17 @@ const previousProjectStateSnapshotSchema = z project: projectSchema, distributionBoards: z.array(distributionBoardSchema), externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(), + externalModel: externalModelStateSchema, + ...projectStateSnapshotContents, + }) + .strict(); + +const externalModelProjectStateSnapshotSchema = z + .object({ + schemaVersion: z.literal(externalModelProjectStateSnapshotSchemaVersion), + project: projectSchema, + distributionBoards: z.array(distributionBoardSchema), + externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(), ...projectStateSnapshotContents, }) .strict(); @@ -502,6 +523,13 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown { const schemaVersion = (value as { schemaVersion: number }).schemaVersion; if (schemaVersion === previousProjectStateSnapshotSchemaVersion) { const previous = previousProjectStateSnapshotSchema.parse(value); + return { + ...previous, + schemaVersion: projectStateSnapshotSchemaVersion, + }; + } + if (schemaVersion === externalModelProjectStateSnapshotSchemaVersion) { + const previous = externalModelProjectStateSnapshotSchema.parse(value); return { ...previous, schemaVersion: projectStateSnapshotSchemaVersion, @@ -765,6 +793,11 @@ function assertProjectStateSnapshotRelations( if (row.roomId !== null) { assertReference(roomIds, row.roomId, "device row room"); } + if (row.manualQuantity > row.quantity) { + throw new Error( + "Snapshot device row manual quantity must not exceed total quantity." + ); + } } } diff --git a/src/frontend/types.ts b/src/frontend/types.ts index feea899..6effc53 100644 --- a/src/frontend/types.ts +++ b/src/frontend/types.ts @@ -350,6 +350,7 @@ export interface CircuitTreeDeviceRowDto { roomNumberSnapshot?: string; roomNameSnapshot?: string; quantity: number; + manualQuantity: number; powerPerUnit: number; simultaneityFactor: number; cosPhi?: number; diff --git a/src/server/controllers/circuit-tree.controller.ts b/src/server/controllers/circuit-tree.controller.ts index b7f0e0b..b7c86bf 100644 --- a/src/server/controllers/circuit-tree.controller.ts +++ b/src/server/controllers/circuit-tree.controller.ts @@ -160,6 +160,7 @@ export async function getCircuitTree(req: Request, res: Response) { roomNumberSnapshot: row.roomNumberSnapshot ?? undefined, roomNameSnapshot: row.roomNameSnapshot ?? undefined, quantity: row.quantity, + manualQuantity: row.manualQuantity, powerPerUnit: row.powerPerUnit, simultaneityFactor: row.simultaneityFactor, cosPhi: row.cosPhi ?? undefined, diff --git a/tests/circuit-device-row-project-command.repository.test.ts b/tests/circuit-device-row-project-command.repository.test.ts index 1bcf1b3..5ab1e3a 100644 --- a/tests/circuit-device-row-project-command.repository.test.ts +++ b/tests/circuit-device-row-project-command.repository.test.ts @@ -128,6 +128,7 @@ function createTestDatabase(): DatabaseContext { name: "Leuchte", displayName: "Leuchte lokal", quantity: 1, + manualQuantity: 1, powerPerUnit: 0.1, simultaneityFactor: 1, }, @@ -138,6 +139,7 @@ function createTestDatabase(): DatabaseContext { name: "Fremdzeile", displayName: "Fremdzeile", quantity: 1, + manualQuantity: 1, powerPerUnit: 0.2, simultaneityFactor: 1, }, @@ -175,6 +177,7 @@ describe("circuit device-row project-command repository", () => { const row = getRow(context); assert.equal(row.quantity, 2); + assert.equal(row.manualQuantity, 2); assert.equal(row.roomId, "room-1"); assert.equal(row.remark, null); assert.equal(row.overriddenFields, "[\"quantity\"]"); @@ -185,6 +188,7 @@ describe("circuit device-row project-command repository", () => { { field: "quantity", value: 1 }, { field: "roomId", value: null }, { field: "remark", value: null }, + { field: "manualQuantity", value: 1 }, { field: "overriddenFields", value: null }, ], }); @@ -203,6 +207,7 @@ describe("circuit device-row project-command repository", () => { { field: "quantity", value: 2 }, { field: "roomId", value: "room-1" }, { field: "remark", value: null }, + { field: "manualQuantity", value: 2 }, { field: "overriddenFields", value: "[\"quantity\"]" }, ], }, @@ -234,6 +239,7 @@ describe("circuit device-row project-command repository", () => { const row = getRow(context); assert.equal(row.quantity, 1); + assert.equal(row.manualQuantity, 1); assert.equal(row.overriddenFields, null); assert.equal(undone.revision.revisionNumber, 2); assert.deepEqual( @@ -377,4 +383,22 @@ describe("circuit device-row project-command repository", () => { context.close(); } }); + + it("rejects a manual share above the materialized total", () => { + const context = createTestDatabase(); + try { + const store = new CircuitDeviceRowProjectCommandRepository(context.db); + assert.throws(() => store.executeUpdate({ + projectId: "project-1", + expectedRevision: 0, + source: "user", + command: createCircuitDeviceRowUpdateProjectCommand("row-1", { + manualQuantity: 2, + }), + }), /manual quantity/); + assert.equal(getRow(context).manualQuantity, 1); + } finally { + context.close(); + } + }); }); diff --git a/tests/circuit-device-row-quantity.test.ts b/tests/circuit-device-row-quantity.test.ts new file mode 100644 index 0000000..c725714 --- /dev/null +++ b/tests/circuit-device-row-quantity.test.ts @@ -0,0 +1,62 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import { describe, it } from "node:test"; +import Database from "better-sqlite3"; +import { + assertCircuitDeviceRowQuantity, + calculateCircuitDeviceRowQuantity, +} from "../src/domain/calculations/circuit-device-row-quantity.js"; + +describe("circuit device-row quantity", () => { + it("adds the manual share and every indivisible external quantity", () => { + assert.equal( + calculateCircuitDeviceRowQuantity(2, [ + { effectiveQuantity: 2 }, + { effectiveQuantity: 1 }, + ]), + 5 + ); + assert.doesNotThrow(() => assertCircuitDeviceRowQuantity({ + quantity: 5, + manualQuantity: 2, + externalObjects: [{ effectiveQuantity: 2 }, { effectiveQuantity: 1 }], + })); + }); + + it("rejects invalid shares and a stale materialized total", () => { + assert.throws( + () => calculateCircuitDeviceRowQuantity(-1, []), + /Manual quantity/ + ); + assert.throws( + () => calculateCircuitDeviceRowQuantity(0, [{ effectiveQuantity: 0 }]), + /External effective quantity/ + ); + assert.throws( + () => assertCircuitDeviceRowQuantity({ + quantity: 2, + manualQuantity: 1, + externalObjects: [{ effectiveQuantity: 2 }], + }), + /does not match/ + ); + }); + + it("preserves existing row quantities in migration 0005", () => { + const database = new Database(":memory:"); + try { + database.exec("CREATE TABLE circuit_device_rows (id text PRIMARY KEY, quantity integer NOT NULL)"); + database.exec("INSERT INTO circuit_device_rows (id, quantity) VALUES ('row-1', 7)"); + const migration = fs + .readFileSync("src/db/migrations/0005_stale_gorilla_man.sql", "utf8") + .replaceAll("--> statement-breakpoint", ""); + database.exec(migration); + assert.deepEqual( + database.prepare("SELECT quantity, manual_quantity FROM circuit_device_rows").get(), + { quantity: 7, manual_quantity: 7 } + ); + } finally { + database.close(); + } + }); +}); diff --git a/tests/external-model-foundation.test.ts b/tests/external-model-foundation.test.ts index 46eed56..08319ec 100644 --- a/tests/external-model-foundation.test.ts +++ b/tests/external-model-foundation.test.ts @@ -21,6 +21,7 @@ import { projectDevices } from "../src/db/schema/project-devices.js"; import { projects } from "../src/db/schema/projects.js"; import { rooms } from "../src/db/schema/rooms.js"; import { createProjectStateRestoreCommand } from "../src/domain/models/project-state-restore-command.model.js"; +import { projectStateSnapshotSchemaVersion } from "../src/domain/models/project-state-snapshot.model.js"; import { createExternalCsvPreview } from "../src/external-model/application/external-csv-preview.js"; import { parseExternalCsv } from "../src/external-model/csv/external-csv-transport.js"; import { @@ -211,7 +212,7 @@ describe("external model snapshot and transfer", () => { insertLinkedExternalFixture(context.db); const populated = readProjectStateSnapshot(context.db, "project-1"); assert.ok(populated); - assert.equal(populated.state.schemaVersion, 4); + assert.equal(populated.state.schemaVersion, projectStateSnapshotSchemaVersion); assert.equal(populated.state.externalModel.objects[0].circuitDeviceRowId, "row-1"); context.db.delete(externalModelSources).run(); diff --git a/tests/project-command.model.test.ts b/tests/project-command.model.test.ts index 9284f47..c13ed74 100644 --- a/tests/project-command.model.test.ts +++ b/tests/project-command.model.test.ts @@ -179,6 +179,7 @@ describe("circuit device-row update project commands", () => { { field: "roomId", value: null }, { field: "quantity", value: 2 }, { field: "cosPhi", value: 0.9 }, + { field: "manualQuantity", value: 2 }, ]); }); @@ -187,6 +188,13 @@ describe("circuit device-row update project commands", () => { () => createCircuitDeviceRowUpdateProjectCommand("row-1", {}), /at least one change/ ); + assert.throws( + () => createCircuitDeviceRowUpdateProjectCommand("row-1", { + quantity: 1, + manualQuantity: 2, + }), + /must not exceed/ + ); assert.throws( () => createCircuitDeviceRowUpdateProjectCommand("row-1", { @@ -470,7 +478,7 @@ describe("circuit device-row structure project commands", () => { row.circuitId ); - assert.deepEqual(insert.payload.row, row); + assert.deepEqual(insert.payload.row, { ...row, manualQuantity: 1 }); assert.deepEqual(remove.payload, { rowId: "row-1", expectedCircuitId: "circuit-1", diff --git a/tests/project-state-snapshot.test.ts b/tests/project-state-snapshot.test.ts index a786814..878eeca 100644 --- a/tests/project-state-snapshot.test.ts +++ b/tests/project-state-snapshot.test.ts @@ -199,6 +199,39 @@ describe("project state snapshot model", () => { }); }); + it("upgrades snapshot version 4 with the existing quantity as manual share", () => { + const previous = protectedSnapshot(); + previous.schemaVersion = 4 as typeof previous.schemaVersion; + previous.circuits[0]!.isReserve = false; + previous.circuits[0]!.deviceRows = [{ + id: "row-1", + circuitId: "circuit-1", + linkedProjectDeviceId: null, + sortOrder: 10, + name: "Leuchte", + displayName: "Leuchte", + phaseType: "single_phase", + connectionKind: null, + costGroup: null, + category: "lighting", + level: null, + roomId: null, + roomNumberSnapshot: null, + roomNameSnapshot: null, + quantity: 3, + powerPerUnit: 0.1, + simultaneityFactor: 1, + cosPhi: 1, + remark: null, + overriddenFields: null, + }]; + + const upgraded = parseProjectStateSnapshot(previous); + + assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion); + assert.equal(upgraded.circuits[0]?.deviceRows[0]?.manualQuantity, 3); + }); + it("rejects malformed baseline and unknown snapshot shapes", () => { const outdated = structuredClone(minimalSnapshot()); delete (outdated.project as Record).description;