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
.qetXML 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, notuuidalone, 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.
Getting Started
🌐 Languages — English · Français · Deutsch
Guides
Conductors — wire properties, what feeds which export, and cables
Wire & cable catalogue — cable types, IEC 60757 core colours, assigning a core to a conductor
Printing and exporting — paper, PDF, images, and what each path does differently
Linking elements — master, slave, terminal
PLC modules — I/O tables and linking a wire to a specific point
Using the element editor — drawing tools, saving, checks
Preferences reference — what each settings page does
Keyboard-only control — mouseless QET, and the one real gap
Mouse modifiers — what Shift, Ctrl and Alt change while you drag
Managing collections — folders, writability, building your own shortlist
Templates — reusable multi-element blocks, and why clicking one does nothing
Search & Replace — bulk property changes
Building a nomenclature query — the BOM/summary table builder
Linking wires across pages — folio reports
Variables & formulas — %f, %{label}, sequences
Auto-numbering — schemes, sequences, freezing
Terminal strips — strips, levels, bridges
Title block templates — the .titleblock format
Importing EPLAN parts (.edz) — EPLAN Data Portal
DXF import & export — two unrelated features, one format
The project database — the in-memory SQLite cache
Development
Automating QET — CLI, XML formats, external tools
CLI Reference — command line usage
JavaScript Scripting — --run, geometry editing, undo
Vision — proposal, under discussion
