From 6061c6380990bdab64f397fdb9848fb8dd67a0ac Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 2 Aug 2026 12:43:21 +1200 Subject: [PATCH 1/3] Add wiring_list_view: from-to wiring list over the conductor tables Slice 3 of discussion #503, on top of slice 2 (#628). One row per conductor, each endpoint resolved to its element label and terminal name -- the `F1:4 -> M200:U1` shape from the original prototype. The view deviates from the SQL sketched in the discussion in two ways, both because the sketched version silently loses wires: - **No join to the `element` table.** A terminal row already carries its `element_uuid`, so joining `element` back just to read the same uuid adds nothing. Worse, it filters: `populateElementTable()` only inserts elements matching `Simple|Terminal|Master|Thumbnail`, so `Slave` elements (relay contacts and the like -- extremely common at the end of a wire) and report elements are simply absent from that table after a project load, and an inner join through it drops their conductors. - **`element_info` is LEFT joined** for the same reason. A wire whose endpoint element has no info row still belongs in a wiring list; it comes back with an empty label rather than vanishing. Losing a wire from a wiring list is a worse failure than showing one with a blank end. Note this only bites after a save/reload. The incremental `addElement()` path does not apply the type filter, so a slave element placed live is present in `element`/`element_info` and an inner join looks fine -- it is the bulk repopulate on project load that drops it. Testing only the live-editing path would have missed this entirely. Measured, comparing this view against an inner-join-through-element variant built from the same tables in the same session: | project | conductors | wiring_list_view | inner-join variant | |---|---|---|---| | Polonez MR'89 wiring diagram | 280 | 280 | 280 | | two slave contacts, after save+reload | 1 | **1** | **0** | Polonez happens to have no slave elements at conductor ends, so both agree there and the problem is invisible. The second case is the minimal reproduction: place two "Simple contact" elements (`link_type="slave"`) so autoconnect wires them, save, reload -- the sketched view returns zero rows for a project that plainly has a wire in it. Acceptance criterion held throughout: `wiring_list_view` row count equals `conductor` row count, i.e. the view itself drops nothing. Conductors already excluded upstream (legacy terminals without uuids, see #628) stay excluded; that remains the only thing missing from the list, and is what slice 4 should surface a count for. --- sources/dataBase/projectdatabase.cpp | 51 ++++++++++++++++++++++++++++ sources/dataBase/projectdatabase.h | 1 + 2 files changed, 52 insertions(+) diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index 37bb3d2e6..ff48bfd80 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -505,6 +505,7 @@ bool projectDataBase::createDataBase() createElementNomenclatureView(); createSummaryView(); + createWiringListView(); prepareQuery(); updateDB(); return true; @@ -622,6 +623,56 @@ void projectDataBase::createSummaryView() } } +/** + @brief projectDataBase::createWiringListView + A from-to wiring list: one row per conductor, each endpoint resolved to + its element label and terminal name. + + Two deliberate differences from an ordinary inner-join view like + element_nomenclature_view: + + - No join to the element table. A terminal row already carries its + element_uuid, so joining element back just to read the same uuid adds + nothing -- and would actively drop rows, because populateElementTable() + only inserts elements matching Simple|Terminal|Master|Thumbnail. Slave + elements (relay contacts and the like, extremely common at the end of a + wire) and report elements are absent from that table after a project + load, so an inner join through it silently loses their conductors. + - element_info is LEFT joined for the same reason. A wire whose endpoint + element carries no info row still belongs in a wiring list; it comes + back with an empty label rather than vanishing. Losing a wire from a + wiring list is a worse failure than showing one with a blank end. + + The result is that this view returns exactly as many rows as the + conductor table holds -- what is already excluded upstream (conductors + on legacy terminals without uuids) stays excluded, and nothing new is + dropped here. +*/ +void projectDataBase::createWiringListView() +{ + QString create_view ("CREATE VIEW wiring_list_view AS SELECT " + "c.uuid AS conductor_uuid," + "c.text AS wire_number," + "t1.element_uuid AS from_element_uuid," + "ei1.label AS from_element_label," + "t1.name AS from_terminal," + "t2.element_uuid AS to_element_uuid," + "ei2.label AS to_element_label," + "t2.name AS to_terminal," + "d.pos AS diagram_position" + " FROM conductor c" + " JOIN terminal t1 ON c.terminal1_uuid = t1.uuid AND c.terminal1_element_uuid = t1.element_uuid" + " JOIN terminal t2 ON c.terminal2_uuid = t2.uuid AND c.terminal2_element_uuid = t2.element_uuid" + " LEFT JOIN element_info ei1 ON t1.element_uuid = ei1.element_uuid" + " LEFT JOIN element_info ei2 ON t2.element_uuid = ei2.element_uuid" + " JOIN diagram d ON c.diagram_uuid = d.uuid"); + + QSqlQuery query(m_data_base); + if (!query.exec(create_view)) { + qDebug() << query.lastError(); + } +} + void projectDataBase::populateDiagramTable() { QSqlQuery query_(m_data_base); diff --git a/sources/dataBase/projectdatabase.h b/sources/dataBase/projectdatabase.h index cddf53468..65ca17950 100644 --- a/sources/dataBase/projectdatabase.h +++ b/sources/dataBase/projectdatabase.h @@ -77,6 +77,7 @@ class projectDataBase : public QObject bool createDataBase(); void createElementNomenclatureView(); void createSummaryView(); + void createWiringListView(); void populateDiagramTable(); void populateElementTable(); void populateElementInfoTable(); From 44ed01ff5dab2c3822079c3190ade48495416dac Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 21 Aug 2026 19:16:35 +1200 Subject: [PATCH 2/3] Don't let the wiring list lose a wire to the diagram join The comment above this view promised that it "returns exactly as many rows as the conductor table holds", and argued carefully for the two joins that could have broken that -- no inner join to element, and element_info LEFT joined. Then it ended with an inner join to diagram that it never mentioned, which can drop rows just as easily. Feeding the real schema a conductor whose diagram_uuid has no diagram row returned 2 view rows for 3 conductors. With the join made LEFT it returns 3, with a null folio instead of a missing wire. In practice this should never fire: QETProject::diagramAdded is connected to addDiagram(), so the folio exists before anything can be drawn on it. But an inner join turns that into an assumption the view enforces silently, and of all the things this view can get wrong, dropping a wire from a wiring list is the one that matters most. The comment now says which joins are inner and why those two are safe. Co-Authored-By: Claude Opus 5 --- sources/dataBase/projectdatabase.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index ff48bfd80..f2b3df670 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -643,10 +643,18 @@ void projectDataBase::createSummaryView() back with an empty label rather than vanishing. Losing a wire from a wiring list is a worse failure than showing one with a blank end. + - diagram is LEFT joined for the same reason. It should + always match, since QETProject::diagramAdded is wired to addDiagram() + and a conductor cannot exist before its folio -- but an inner join here + would make that an assumption the view silently enforces, and a wire + missing from a wiring list is the one failure this view must not have. + The result is that this view returns exactly as many rows as the conductor table holds -- what is already excluded upstream (conductors on legacy terminals without uuids) stays excluded, and nothing new is - dropped here. + dropped here. Only the terminal joins are inner, and both are guaranteed + by insertTerminal() running for each endpoint before the conductor row + is written. */ void projectDataBase::createWiringListView() { @@ -665,7 +673,7 @@ void projectDataBase::createWiringListView() " JOIN terminal t2 ON c.terminal2_uuid = t2.uuid AND c.terminal2_element_uuid = t2.element_uuid" " LEFT JOIN element_info ei1 ON t1.element_uuid = ei1.element_uuid" " LEFT JOIN element_info ei2 ON t2.element_uuid = ei2.element_uuid" - " JOIN diagram d ON c.diagram_uuid = d.uuid"); + " LEFT JOIN diagram d ON c.diagram_uuid = d.uuid"); QSqlQuery query(m_data_base); if (!query.exec(create_view)) { From b034b1a634da009a0a2dbb2405d8d2a185d175b1 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Fri, 21 Aug 2026 21:06:41 +1200 Subject: [PATCH 3/3] Add --export-wiring: the wiring list, headless The wiring_list_view added by this slice was only reachable through the GUI, which meant the one thing worth proving about it -- that it still describes the project -- could not be checked without a person clicking. This is the same shape as the existing --export-bom, which reads element_nomenclature_view, and it makes the view verifiable in CI. It also makes this slice useful on its own: a from-to wiring list is a thing people want as a CSV, and it no longer waits on the dialog in the next slice. There is deliberately an overlap with --export-cables, which produces the same logical list from the document XML rather than the database. Keeping both is the point: running them and diffing them is a direct check that the cache and the document still agree, which nothing else in the codebase can do. Measured on the example corpus, the two also differ in what they can actually fill in. Rows carrying any endpoint data: --export-cables --export-wiring industrial.qet 0 / 671 541 / 671 m_000.qet 0 / 457 362 / 457 affuteuse_250h.qet 0 / 263 197 / 263 tremie_vibrante.qet 0 / 77 61 / 77 tableau_domestique.qet 58 / 130 104 / 130 Both return a row per conductor; the XML-derived one leaves the component and terminal columns empty on the older projects, and emits an unresolved "%id" in its folio column. That is not an argument for removing it -- it carries columns the view does not, and it is the independent second opinion -- but it does mean the database path is the one with the data on the projects people actually have. The terminal-name columns come back empty on most projects. That is absent source data, not a loss in transit: tableau_domestique.qet has no terminal name on 457 of 457 terminals, and industrial.qet stores the "_" placeholder on 1421 of 1790. Co-Authored-By: Claude Opus 5 --- sources/cli_export.cpp | 51 ++++++++++++++++++++++++++++++++++++++++++ sources/cli_export.h | 6 +++++ 2 files changed, 57 insertions(+) diff --git a/sources/cli_export.cpp b/sources/cli_export.cpp index add222935..526207971 100644 --- a/sources/cli_export.cpp +++ b/sources/cli_export.cpp @@ -70,6 +70,7 @@ const QHash &exportFlags() {"--export-cables", "cables"}, {"--export-wires", "wires"}, {"--export-bom", "bom"}, + {"--export-wiring", "wiring"}, {"--export-nets", "nets"}, {"--export-links", "links"}, {"--info", "info"}, @@ -525,6 +526,54 @@ QHash folioIndex(QETProject &project) return folio; } +/// From-to wiring list: one row per conductor, each endpoint resolved to its +/// element label and terminal name. +/// +/// Reads wiring_list_view out of the project database. --export-cables produces +/// the same logical list from the document XML instead, and the two are meant +/// to agree: running both and diffing them is a direct check that the database +/// still describes the project, which is otherwise only observable through the +/// GUI. +int exportWiring(QETProject &project, const QString &output) +{ + // The project database is built lazily; force a (re)build before querying. + project.dataBase()->updateDB(); + + static const QStringList columns { + "wire_number", "from_element_label", "from_terminal", + "to_element_label", "to_terminal", "diagram_position", "conductor_uuid" + }; + + QSqlQuery query = project.dataBase()->newQuery( + "SELECT " % columns.join(", ") % + " FROM wiring_list_view ORDER BY diagram_position, wire_number"); + if (!query.exec()) { + err << "Wiring list query failed: " << query.lastError().text() << "\n"; + return 1; + } + + QString csv = columns.join(";") % "\n"; + int rows = 0; + while (query.next()) { + QStringList values; + for (int i = 0; i < columns.size(); ++i) + values << csvField(query.value(i).toString()); + csv += values.join(";") % "\n"; + ++rows; + } + + QFile file(output); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + err << "Cannot open '" << output << "' for writing.\n"; + return 1; + } + QTextStream fout(&file); + fout << csv; + file.close(); + out << "Exported " << rows << " conductor(s) -> " << output << "\n"; + return 0; +} + /// Electrical nets: groups of terminals joined into one potential. /// Walks QET's own potential graph, so each net is a connected component /// of terminals across all folios. The ground truth for connectivity. @@ -823,6 +872,8 @@ int run(const QStringList &args) return exportCsv(project, format, output); if (format == "bom") return exportBom(project, output); + if (format == "wiring") + return exportWiring(project, output); if (format == "nets") return exportNets(project, output); if (format == "links") diff --git a/sources/cli_export.h b/sources/cli_export.h index 2354fbfc1..81380f43a 100644 --- a/sources/cli_export.h +++ b/sources/cli_export.h @@ -48,6 +48,7 @@ namespace CLIExport { qelectrotech --export-cables qelectrotech --export-wires qelectrotech --export-bom + qelectrotech --export-wiring qelectrotech --export-nets qelectrotech --export-links qelectrotech --info [output.json] @@ -60,6 +61,11 @@ namespace CLIExport { cables: wiring list (one row per conductor) as CSV. wires: list of distinct wire numbers as CSV. bom: bill of materials (one row per element) as CSV. + wiring: from-to wiring list (one row per conductor) as CSV, read from + the project database. Same logical list as `cables`, which + reads the document XML instead; the two are meant to agree, + so diffing them checks that the database still describes the + project. nets: electrical nets (connected-terminal groups) as JSON. links: element cross-references (coil/contact) as CSV, with unresolved links flagged.