Repository navigation
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}.
Answering "list every component in this project, with manufacturer references,
grouped by sheet" 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 sheet |
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-wiring (CLI) |
wiring_list_view |
| Sheet summary tables | project_summary_view |
Scripting's qet.query()
|
any table or view |
None of these are storage. Every one of them is a report about data that already exists in the XML.
QETProject constructed
βββ projectDataBase constructed β createDataBase()
βββ open an anonymous SQLite connection
βββ CREATE TABLE Γ 9, CREATE VIEW Γ 4
βββ updateDB()
project XML read
βββ updateDB() β full repopulate, once, after everything is loaded
(read from the .qet document since PR #1142, see below)
user edits the diagram
βββ addElement / removeElement / elementInfoChanged
addDiagram / removeDiagram / diagramInfoChanged / diagramOrderChanged
addConductor/ removeConductor / updateConductor
drawingItemChanged / drawingItemDestroyed β 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.
Since PR #1142 (merged 2026-09-29), the full fill at load reads the
.qet document instead of walking the sheets QElectroTech has built. The
tables hold the same rows either way β a test fills them both ways on all 24
shipped examples and requires identical results β but the database no longer
needs every sheet built before it can say what is on them. That is the first
step towards opening a big project without building every sheet up front
(discussion #1141).
-
Labels from formulas are computed from the document too. The file keeps
the last computed value, which can be stale (
K%total-%idsaved asK1-1, shown asK3-1), so the formula is evaluated again rather than the saved text trusted. - A file that lacks something the fill needs β older files without saved uuids, or wires with a frozen formula text β falls back to the old way and says why in the log.
-
QET_DATABASE_FROM_FOLIOS=1in the environment forces the old way. - Not moved yet: shapes, free texts and pictures still come from the built sheets (text sizes need fonts). Opening is not faster yet.
The incremental updates while you edit are unchanged.
Nine tables:
| Table | Key | Notes |
|---|---|---|
diagram |
uuid |
plus pos, the sheet 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 |
shape |
uuid |
lines, rectangles, ellipses, polygons: type, color, fill
|
independent_text |
uuid |
free texts on a sheet: text, rotation
|
image |
uuid |
pictures: pixel_width, pixel_height of the source image |
The last three share their first columns: uuid, diagram_uuid, pos (the
sheet cell of the item's top-left corner, as for element), and x, y,
width, height, the item's bounding box on the sheet. They follow edits as
they happen, like the element and conductor tables.
Four views: element_nomenclature_view, project_summary_view,
wiring_list_view, and drawing_item_view, which lists shapes, texts and
pictures together with a kind column (shape, text, image) and the
sheet's position in the project as folio:
SELECT kind, folio, pos, description FROM drawing_item_view ORDER BY folio, pos;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.
element holds every element type, slaves and sheet 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.
The SQL box of a nomenclature table, queries saved in a project and
scripting's qet.query() run with SQLite's query_only setting on, so a
statement that would write (INSERT, UPDATE, DROPβ¦) is refused.
Development builds from 26β27 September 2026, between
PR #1046
and its fix in
PR #1066,
had a side effect: a query whose rows came back unsorted β a UNION ALL
without ORDER BY, as drawing_item_view is β returned only its first
row, with no error. If such a build gives you one row where you expect many,
add an ORDER BY.
Rows need stable keys. Elements and diagrams have real UUIDs, so they are
fine. So do conductors drawn or saved by a recent QElectroTech. Items saved
by an older version, without a uuid, get one worked out when the project
opens, the same every time, and saved from then on:
- A symbol gets one from its type, its position on the sheet and its rotation (PR #1105).
- A wire gets one from what it connects β the symbol and terminal at each end β never from its place in the file or its sheet's number, so inserting or moving a sheet does not change it. Once saved, reconnecting the wire keeps it (PR #1107). A worked-out uuid is never one the file already carries.
- Shapes, free texts and pictures on a sheet, and the parts drawn inside a symbol, get one from their sheet, their kind and their order in the file. Paste and sheet duplication give the copies new ids. Older versions of QElectroTech open such files and drop the attribute when they save.
A terminal's uuid belongs to the symbol's 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. So 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.
Besides its name, each row has a terminal_index: the terminal's number in
its symbol, counted from 0, top to bottom and then left to right β the number
a script's addConductor() takes. It is empty when two
terminals of one symbol sit at the same point, where that order is not
defined. wiring_list_view carries the index and uuid of both ends
(from_terminal_index, from_terminal_uuid, to_terminal_index,
to_terminal_uuid), and so does --export-wiring
(PR #1248):
most shipped symbols leave their terminals unnamed, and these say which
terminal each end of a wire is on all the same.
The collection's symbols have carried terminal uuids since 2024, but each project keeps its own copy of every symbol it uses, and older projects keep the copy they were drawn with: in the 24 example projects, 706 of the 900 stored symbols had no terminal uuid. How that is handled:
-
Terminal::stableUuid()gives a terminal without a uuid an identity derived as a UUID v5 from its position and orientation inside its symbol β the same basis the project format uses to match a conductor back to a terminal. Names are deliberately left out: QElectroTech rewrites a terminal named_as unnamed, and the identity would change on the first resave. - Since PR #1118, opening a project gives every terminal of its stored symbols that has no uuid that same value, and saving writes it. Nothing keyed on terminals changes: the terminal table and the wire uuids above were already using it. A second terminal at the same point of a symbol gets its own value instead of sharing the first one's. The first save of an older project rewrites its wires in the form that names terminals by uuid, which QElectroTech has read since 0.8.0.
- A wire saved against a terminal uuid is reattached on opening to a terminal with that uuid. Replacing a project's copy of a symbol ("Γcraser l'Γ©lΓ©ment dΓ©jΓ intΓ©gΓ©" β sic β when placing a changed version) keeps the old terminal uuids for terminals at the same place (PR #1116), and a wire that still cannot be reattached is listed in a warning when the project opens, instead of disappearing silently.
- Since PR #1122, the symbol editor gives the terminals of an old symbol file the same worked-out uuids when it opens it, instead of random ones, so every copy of that symbol agrees with the projects that use it. The collection's last three symbols without terminal uuids got these values too (elements #83).
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 was the constraint on any future persistence work. While the database is derived, a terminal whose identity is guessed from geometry costs a cache miss; kept on disk, a guess moves whenever the terminal does. Writing the identity into the project (#1118) turns the guess into a fact the file keeps, which is what persisting anything keyed on terminals needs.
| 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 |
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;"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
Windows without admin rights β the portable archive, no installer
Guides
Conductors β wire properties, what feeds which export, cables, and hops where wires cross
Wires per terminal β limit the wires on a terminal, chain wiring instead of stars
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
Grid size and element size β why symbols aren't all the same scale, and scaling one without leaving the grid
Preferences reference β what each settings page does
Saving and loading settings β your whole setup in one file, to copy or keep
Keyboard-only control β mouseless QET, and what still needs a mouse
Mouse modifiers β what Shift, Ctrl and Alt change while you drag
3D mouse β SpaceMouse pan, zoom and buttons
Aligning items β snap symbols back to the grid, or line them up
Pictures on a sheet β labels, crop, transparency, what they cost in the file
Arcs and curved wires β the Arc tool, pulling an arc in or out, rounding a corner with a fillet, dashed arcs for lighting layouts
Grouping items β select, move and copy several items as one
Finding your place on a sheet β go to a cell like B13 or 4-B7, keep the headers in sight, show the cell limits, zoom and pan
Showing and hiding kinds of items β hide texts, wire numbers, shapes, pictures, tables or cross-references on every sheet
Drawing faster β place without dragging, the S shortcut bar, command search, gestures
Customising QElectroTech β keys, toolbar size and contents, the gesture ring (partly pending)
Managing collections β folders, writability, building your own shortlist
Templates β reusable multi-element blocks, placed by double-click or drag
Search & Replace β bulk property changes
Building a nomenclature query β the BOM/summary table builder
Linking wires across pages β sheet 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; command-line export and layers
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
MCP server β let an AI assistant read, verify and edit projects
Connecting an AI assistant β setup for Claude, Copilot, Gemini, Codex, Cursor, LM Studio
Script buttons β stored scripts with an icon, by hand or by an assistant
Live mode β an assistant working in the open project while you watch
Macro recorder β record a task by hand, for an assistant to script
Vision β proposal, under discussion
