Clone
1
project_database
ispyisail edited this page 2026-09-11 21:24:08 +12:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

The project database

QElectroTech builds an SQLite database of every project you open. It is worth being precise about what that database is, because the name invites an assumption that is not true:

The project database is a derived, in-memory cache. It is rebuilt from the .qet XML every time the project is opened, and it is never written back to the project file. The XML is the only thing that persists.

Everything else on this page follows from that one sentence.

Source: sources/dataBase/projectdatabase.{h,cpp}.


1. Why it exists

Answering "list every component in this project, with manufacturer references, grouped by folio" from a graphics scene means walking thousands of QGraphicsItems and comparing strings. Answering it from a table is a SELECT.

So QET keeps the same information in a second shape, one that is good at queries, and uses it for the things that are naturally queries:

Consumer What it reads
Nomenclature / BOM tables placed on a folio element_nomenclature_view, via ProjectDBModel
The nomenclature query builder any of the views, assembled in ElementQueryWidget
Wiring list dialog wiring_list_view
--export-bom (CLI) element_nomenclature_view
--export-wires, --export-cables (CLI) wiring_list_view
Folio summary tables project_summary_view

None of these are storage. Every one of them is a report about data that already exists in the XML.


2. Lifecycle

QETProject constructed
    └── projectDataBase constructed  →  createDataBase()
                                         ├── open an anonymous SQLite connection
                                         ├── CREATE TABLE × 6, CREATE VIEW × 3
                                         └── updateDB()
project XML read
    └── updateDB()   ← full repopulate, once, after everything is loaded
user edits the diagram
    └── addElement / removeElement / elementInfoChanged
        addDiagram  / removeDiagram / diagramInfoChanged / diagramOrderChanged
        addConductor/ removeConductor / updateConductor      ← incremental
project closed
    └── database discarded

QSqlDatabase::addDatabase("QSQLITE", …) is called without setDatabaseName(), so there is no file on disk for the connection to point at. During load the database's signals are blocked and a single updateDB() runs at the end, rather than one insert per object as the scene is built.

Three PRAGMAs are set immediately after opening — temp_store = MEMORY, journal_mode = MEMORY, synchronous = OFF. Those settings would be reckless for a database you cared about keeping. They are correct here precisely because losing the whole thing costs nothing: it is rebuilt on the next open.

QETProject::readProjectXml() logs how long each load phase took, the database rebuild among them, so the cost on a given project can be read straight from the console output rather than guessed at.


3. Schema

Six tables:

Table Key Notes
diagram uuid plus pos, the folio order
element uuid diagram_uuid, pos, type, sub_type
diagram_info diagram_uuid one column per QETInformation::diagramInfoKeys() — 9 today
element_info element_uuid one column per QETInformation::elementInfoKeys() — 57 today
terminal (uuid, element_uuid) see §4
conductor uuid both endpoints as (terminal uuid, element uuid) pairs

Three views: element_nomenclature_view, project_summary_view, wiring_list_view.

Note what the column lists mean: the schema is generated from elementInfoKeys() at runtime. Adding an element information field adds a column automatically, with no migration and no schema version — because there is no existing database to migrate. This is the single biggest practical consequence of the cache being derived.

The filter lives in the view, not the table

element holds every element type, slaves and folio reports included. Restricting to "things a parts list should mention" (type IN ('simple','terminal','master','thumbnail')) happens inside element_nomenclature_view.

This was not always so, and the reason it changed is instructive: with the filter in the table, a slave element (a relay contact) was absent from the database entirely, so anything else reading the table — the wiring list, for instance — silently lost every conductor that ended on a relay contact. A nomenclature's opinion about what counts as a line item does not belong in the project's model of itself.


4. Identity, and why terminals are hard

Rows need stable keys. Elements and diagrams have real UUIDs, so they are fine. Terminals are not.

Terminal::uuid() comes from the catalogue .elmt definition. It identifies a terminal position in a symbol — "the top terminal of a contactor" — and is therefore identical across every placed instance of that symbol. It is also empty for every element authored before the field existed, which is most of the installed collection.

Two consequences, both handled:

  • A terminal instance is only unique as the pair (uuid, element_uuid), which is why that pair, not uuid alone, is the terminal table's primary key and what the conductor table's foreign keys reference.
  • Terminal::stableUuid() supplies an identity when the definition gives none, derived as a UUID v5 from the terminal's local position and orientation inside its element — the same basis the project format already uses to match a conductor back to a terminal. Names are deliberately excluded from the derivation, because QET rewrites a terminal named _ as unnamed and the identity would change on the first resave.

projectDataBase::excludedConductorCount() reports how many conductors could not be keyed at all, counted from the live scene rather than the database — "precisely because the database is where these conductors are not". That is what lets a wiring list say "N wires are missing, and here is why" instead of presenting a short list as if it were complete.

This is the constraint to keep in mind for any future persistence work. While the database is derived, a terminal whose identity is guessed from geometry costs a cache miss. As a file format, the same guess becomes a permanent, one-shot migration of everyone's projects.


5. What follows from "derived"

Because it is derived… …this is true
Rebuilt on every open No schema version, no migrations, ever
Never written to .qet A wrong row costs nothing — reopen and it is gone
XML is authoritative The database cannot disagree with the drawing; if it does, the database is wrong
Discarded on close synchronous = OFF and friends are safe
Lives in one process It is not shared, not concurrent, and not a multi-user store

And the flip side, equally true:

Because it is derived… …this is also true
Nothing survives close Anything the database alone knows is lost
Rebuilt in full on open Opening cost grows with project size
Not in the file Two people cannot query the same project database

6. Seeing it for yourself

Builds compiled with QET_EXPORT_PROJECT_DB get a menu entry, Export the project's internal database, which copies the live database to a .sqlite file using SQLite's backup API. The CMake option defaults to OFF, but the official Windows, macOS, Flatpak and Snap packaging all turn it ON — so on a released build the entry is normally there.

The exported file is a snapshot for inspection. Editing it changes nothing: nothing ever reads it back.

sqlite3 myproject.sqlite ".schema"
sqlite3 myproject.sqlite "SELECT label, designation FROM element_nomenclature_view LIMIT 20;"

7. What it is not

It is not the project file, not a shared database a team connects to, and not the component-database-plus-drawing-view architecture of tools like EPLAN. A project is still one XML file; the database is a query index over it that lives as long as the window is open.

Whether it should stay that way is a live question — see the Vision page and the Development Roadmap. Any move toward persisting it is a change to the file format, with the terminal-identity problem in §4 as its first real obstacle.

Until such a decision is taken, there is a rule of thumb worth following when adding features: do not create state that only the XML knows about, and do not create state that only the database knows about. The first makes the cache incomplete; the second cannot survive a close.