Let a script query the project database

Every structural question this API could answer, it answered by walking
live objects. The project builds a SQLite database that already knows
most of them, and nothing outside the application could reach it.

  tables()      what is queryable, tables and views
  query(sql)    rows, one object per row
  queryError()  why the last one returned nothing

This is not a new door. QElectroTech already ships a "Requête SQL
personnalisée" box in the element-query dialog where a user types
arbitrary SQL, guarded by projectDataBase::isReadOnlySelect(); query()
goes through projectDataBase::newQuery(), which applies that same rule
and returns the same rejection message. A script gets what a user
already has, and neither can write: DELETE, UPDATE and a chained
"SELECT 1; DROP TABLE" are all refused before reaching SQLite.

An empty result and a failure are told apart. query() returns no rows
for both, so queryError() carries the reason -- a refusal, or SQLite's
own message for a bad column -- and is empty when the query simply
matched nothing. Conflating those is how a silent typo in a column name
becomes "there are no such elements".

No updateDB() before querying, and that is a measured decision rather
than an omission. A script that has just edited something is the
expected caller, so a stale cache was the obvious hazard; but
projectDataBase maintains itself incrementally through addElement(),
elementInfoChanged(), addConductor() and the rest, which the undo
commands behind every edit already call. Tested both ways on the cases
most likely to go stale -- an element added and labelled, a conductor
property changed -- each queried immediately afterwards through both the
table and the view. Identical counts with the rebuild and without it,
and updateDB() repopulates every table, so calling it per query would
have been real cost for no benefit. The comment says so, so it is not
added back on the assumption it must be needed.

The views are the surface to depend on: element_nomenclature_view,
project_summary_view and wiring_list_view exist to be queried. The
tables are how the cache is arranged today and a column may move --
which is why tables() lists both and the header says which is which.

Verified against examples/industrial.qet, the largest shipped project:
618 elements counted, the busiest wire numbers ranked (0VDC 93 times,
24V2 64), and duplicate element labels found by GROUP BY ... HAVING --
V6 seven times, V5 six -- which is a design-rule question no tool here
could previously ask. Qt 6.10.2, ctest matches master.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-09-21 19:14:53 +12:00
parent 2adc58c1c0
commit 46c35d2297
2 changed files with 116 additions and 0 deletions
+23
View File
@@ -21,6 +21,7 @@
#include <QObject>
#include <QString>
#include <QStringList>
#include <QVariantList>
class QETProject;
class DiagramView;
@@ -133,6 +134,22 @@ class QetShapeItem;
deleting one: indexes after the affected position shift, the way a
list's do. Call texts() or shapes() again rather than holding an
index across an edit that adds or removes one.
- @b Querying the project database: run a read-only SELECT against the
SQLite database QElectroTech builds from the project, and get rows
back as objects. This is not a new door. QET already ships a
"Requête SQL personnalisée" box in the element-query dialog where a
user types arbitrary SQL, and it is guarded by the same
projectDataBase::isReadOnlySelect() this calls through
projectDataBase::newQuery(). A script gets what a user already has,
under the same rule, and neither can write.
What is worth knowing is what the database @b is: a cache, rebuilt
from the XML on every load and never written to disk. The three
views -- element_nomenclature_view, project_summary_view and
wiring_list_view -- exist to be queried and are the surface to
depend on. The underlying tables are how the cache happens to be
arranged today, and a column may move. tables() lists both so a
script can see what it is querying rather than guess.
- @b Navigating and @b messaging: select an element, zoom the active
view, and show the user a message. Deliberately narrow: selection and
messaging work with no view at all (headless `--run`); zoom is a no-op
@@ -241,6 +258,11 @@ class QetScriptApi : public QObject
double x1, double y1, double x2, double y2);
Q_INVOKABLE bool deleteShape(int folioIndex, int shapeIndex);
// -- query the project database --
Q_INVOKABLE QStringList tables() const;
Q_INVOKABLE QVariantList query(const QString &sql);
Q_INVOKABLE QString queryError() const;
// -- folios --
Q_INVOKABLE int addFolio();
Q_INVOKABLE bool setFolioTitle(int folioIndex, const QString &title);
@@ -276,6 +298,7 @@ class QetScriptApi : public QObject
QETProject *m_project;
DiagramView *m_view;
QString m_query_error;
};
#endif // QET_SCRIPT_API_H