removeDiagram() only ever deleted the diagram's own row. No foreign key in
this schema is declared ON DELETE CASCADE (and SQLite foreign-key
enforcement is never turned on for this connection anyway), so removing a
diagram left every element, element_info, terminal and conductor row that
belonged to it behind in the database -- silently, since nothing reads them
until the next full updateDB() rebuild papers over it.
Traced why this had never crashed anything: Diagram::~Diagram() explicitly
walks and deletes its top-level items through removeItem() (which does call
dataBase()->removeElement() correctly), but deliberately skips conductors --
because a conductor's destructor touches both of its terminals
(terminal1->removeConductor(this)), and those terminals may belong to an
element already destroyed earlier in the same sweep. Conductors are instead
destroyed as a side effect of Terminal::~Terminal()'s qDeleteAll() on its own
conductor list, which is a plain C++ delete that never goes through
Diagram::removeItem() and therefore never calls dataBase()->removeConductor()
at all. So the object graph is torn down safely, but the database is never
told about the conductors or their terminals.
Fixed by adding the missing bulk deletes to projectDataBase::removeDiagram()
itself, run while the diagram (and its live scene) still exist -- verified
that QETProject::detachDiagram() emits diagramRemoved() (which this class's
constructor connects to this slot) synchronously, before the Diagram object
is scheduled for destruction via deleteLater(), so nothing here races the
C++ teardown described above. Order matters: element_info and terminal have
no diagram_uuid column of their own, so both are scoped through a subquery
on element and must run before element itself is deleted.
Verified against examples/industrial.qet (50 diagrams) by calling
projectDataBase::removeDiagram() directly and comparing table counts before
and after, with no intervening updateDB() call to mask a gap:
element=354->335 element_info=354->335 terminal=1087->1033 conductor=671->626 diagram=50->49
Every delta matches a direct SQL count for that diagram's own rows exactly
(19 elements, 54 terminals), and both "orphan rows still referencing the
removed diagram" checks read 0 afterward -- so the cascade is complete and,
just as importantly, scoped: nothing belonging to the other 49 diagrams
moved.
Separate finding, not fixed here: QETProject::removeDiagram(Diagram*) (the
synchronous, non-undoable variant, not the usual GUI
ProjectView::removeDiagram() path) segfaults if the enclosing QETProject is
destroyed before an event loop iteration lets its pending deleteLater() run
-- reproduces identically on unmodified master, so it predates and is
unrelated to this change. Worth its own report; a headless caller is the
only realistic way to hit it, which is how this surfaced.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three fixes to the tables added by this slice.
A conductor's text was written once at insert and never again. Renaming a
wire left the database holding the old number, so the wiring list showed a
stale value until the next full repopulate -- elements have
elementInfoChanged() for exactly this and conductors had nothing.
Conductor::setProperties() has around a dozen call sites (auto-numbering,
the properties dialog, element moves, the delete command's re-links), so
rather than adding a call to each and missing the ones added later, listen
to the propertiesChange() signal it already emits. Qt::UniqueConnection
means a repeated insert or a full repopulate cannot double-subscribe, and
the connection is established on both insert paths because conductors read
from a file never pass through addConductor().
addConductor() and populateConductorTable() each carried their own copy of
the same seven bindValue() lines. They had not drifted yet, but that is the
same duplication the element paths had before bindElementValues(), where
they had drifted -- one binding kindInformations()["type"] and the other
masterTypeToString(). One bindConductorValues() for both.
Finally, index the conductor columns that get looked up per element rather
than per conductor. element_nomenclature_view counts the wires touching each
element with a correlated subquery, so without an index every element row
full-scans the conductor table and the cost grows as elements x conductors.
Measured on a standalone SQLite harness at 2000 elements x 5000 conductors:
2134 ms unindexed, 10 ms indexed. diagram_uuid is indexed too, since the
wiring list view joins on it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Slice 2 of discussion #503 (from-to wiring list built on projectDataBase),
building on the conductor uuid from slice 1 (#625). Pure plumbing: two
new additive tables plus their populate/add/remove hooks. No view, no UI,
no visible behavior change yet -- the wiring-list view is slice 3.
Follows the existing shape of the class throughout: same table/column
naming, same prepared-statement idiom in prepareQuery(), same
bind/exec/qDebug-lastError error handling, same DELETE-then-loop
populate pattern.
- `terminal (uuid, element_uuid, name)` and
`conductor (uuid, diagram_uuid, terminal1_uuid, terminal1_element_uuid,
terminal2_uuid, terminal2_element_uuid, text)` created alongside the
existing tables in createDataBase().
- populateConductorTable() added as a fifth populate* call in updateDB().
Terminal population is folded into it, since a terminal only matters
here in the context of a conductor referencing it.
- addConductor()/removeConductor() hooked into the already-existing
Conductor::Type branch of Diagram::addItem()/removeItem(), mirroring
the Element::Type branch directly above.
Two things the original schema sketch in the discussion got wrong, found
by testing rather than inspection:
1. Terminal::uuid() is NOT unique per placed terminal. It is the
terminal-position id baked into the catalog .elmt definition ("the
top terminal"), so every placed instance of the same catalog element
shares it. A terminal instance is only uniquely identified by
(uuid, element_uuid) together, so that pair is the terminal table's
primary key and the conductor table carries both halves for each
endpoint. With uuid alone as PK, the second placed instance of any
element silently lost its terminals to the INSERT OR IGNORE.
2. Conductors whose terminals predate terminal uuids are omitted rather
than given a fabricated identity, as agreed in the discussion. This
turns out to matter far more than expected in practice -- see below.
Testing (all live, in the running app):
- Incremental add: fresh project, two vertically aligned contacts placed
so autoconnect creates a conductor -> 2 terminals, 1 conductor.
- Incremental remove: deleting that conductor -> conductor count 1 -> 0.
- Undo: ctrl+Z after the delete -> back to 1, no duplicate-primary-key
error (the same Conductor object keeps its uuid).
- Bulk populate: examples/weneedpolonez-Polonez_MR89_wiring_diagram.qet
(366 conductors) -> 478 terminals, 280 conductors; the 86 conductors
touching legacy terminals correctly omitted.
- Join correctness: conductor -> terminal (composite key) -> element_info
resolves real from-to rows with real element labels.
- Legacy-only project: examples/industrial.qet has 1794 terminals and
*zero* terminal uuids, so all 671 of its conductors are omitted. Loads
and renders fine, no crash, no spurious rows -- but worth stating
plainly that a from-to wiring list for that project would be empty
today. This is a property of the element catalog definitions, not of
the project file, and is the strongest argument for surfacing an
"N conductors excluded" count to the user when the view lands.
- No SQL errors logged in any of the above.
Known limitation, consistent with existing behavior: removeDiagram()
does not cascade-delete the conductor rows of that diagram, exactly as
it already does not cascade to element/element_info. A full updateDB()
rebuild clears them, and the future wiring-list view INNER JOINs from
conductor, so orphan terminal rows never surface.
The export of internal database to file is now do by sqlite3 native
function instead of a dirty home made function.
For the moment, the export to file function is unavailable on macOSX (we
need some help to add sqlite3 lib on macOSX)