Architecture¶
Sqliteman 2 is built the way Slonisko is: a few headless libraries, each with its own tests, and the GUI on top. Everything except the widgets can be tested without a display.
Directory |
What belongs there |
|---|---|
|
The SQLite C API: connections on a thread of their own, statements, cancelling, values, the row store, quoting |
|
Preferences and the recent files |
|
Lexing SQLite’s SQL, keywords, statement splitting, comments, text search |
|
The schema snapshot, and everything derived from a database: table definitions and altering them, editing rows, export, import, the populator, the query builder, dumps, pragmas, completion, query plans |
|
The GUI |
Only src/app may depend on Qt Widgets. A query, a parser or a format
belongs in a library where a test can reach it: altering a table, writing
edited rows back, the CSV import, the export formats, the completion
matcher and the plan parser all live outside the widgets and have tests of
their own. Each library keeps its tests in its own tests/ subdirectory.
The SQLite layer¶
Sqliteman 1 used Qt’s SQL module. Version 2 talks to SQLite directly: the
Qt driver hides what a client like this needs — a statement’s error
offset, the table and column a result column comes from, the storage class
of each value, sqlite3_interrupt(), extensions — and it bundles a SQLite
of its own choosing.
sqlite::Connection owns one sqlite3 * and a worker thread. Everything
that touches the handle runs on that thread; the UI thread only queues work
and receives results, as queued calls, so nothing ever blocks the window.
Cancelling is sqlite3_interrupt(), the one call SQLite allows from
another thread.
What is queued is either SQL or a task. SQL is prepared and stepped
statement by statement; its rows come back in chunks of a
sqlite::RowStore — values kept with their storage class, NULL and
blobs included — so a big result shows its first rows while the rest are
still being read. A task is a function given the sqlite3 * on the worker
thread, for work that is more than a statement: reading the schema,
altering a table, writing edits, dumping. sqlite::QueryRunner puts a
queue and callbacks in front of a connection, so a page can ask for work
and be told when it is done, or be forgotten if it closes first.
An authorizer watches what each statement does, so a result can say that it changed the schema; the session then reads the schema again.
One connection per database¶
Here Sqliteman deliberately differs from Slonisko. Slonisko gives each editor a connection of its own, because a PostgreSQL server handles many connections, each with its own transaction, well. SQLite does not work like that:
A database file has one writer at a time. Two connections in the same program would lock each other out; a transaction opened in one editor would make every other editor’s writes fail with database is locked.
ATTACH,temptables, loaded extensions, user functions and most pragmas belong to a connection. An editor that attaches a database expects the browser, and the other editors, to see it.
So Session — one open database — owns a single sqlite::Connection and
its QueryRunner, and every page on that database shares them: editors,
object pages with their rows, pragmas and dialogs. Their work is queued and runs one
item after another, and the transaction state is the session’s.
The schema¶
catalog::Snapshot is the whole schema of a connection — every database
in it, their tables, views, indexes and triggers, with columns, keys and
foreign keys — read in one task by catalog::loadSnapshot(). A virtual
table whose module is not loaded is kept, with what can be known of it.
The browser, completion, the editing of rows, the diagrams and the dialogs
all read the same immutable snapshot, shared by pointer, so none of them
queries the schema on its own. catalog::erdGraph() makes a diagram’s
tables and edges from it, and the layouts that place them are as headless
as the rest, so that the tests can check them; both come from Slonisko,
where they are built from the server’s catalogue instead.
The session reads it again when a statement changed the schema, when a
transaction ends (a rollback can undo a CREATE), and after the tasks that
alter it; a change that comes in while one is being read makes it read
again once that is done.
How a statement gets run¶
sql::splitStatements()cuts the script into statements, the waysqlite3_complete()does — trigger bodies with their semicolons included — and marks thesqlite3shell’s dot commands.The editor queues each statement on the session’s runner, which prepares and steps it on the worker thread.
Rows arrive in chunks; the grid shows them as they come, up to the row limit.
With the columns’ origin from SQLite’s column metadata, and the snapshot,
catalog::editTarget()decides whether the rows can be edited, and by which key.An error comes back with SQLite’s offset into the statement, which the editor turns into a mark in the script.
Writing back¶
Edited rows become parameterised UPDATE, INSERT and DELETE
statements (catalog::dmlStatements()), each finding its row by the
rowid or the primary key. catalog::applyEdits() runs them as one task
in a savepoint, and fails all of them unless each changed exactly one row.
Altering a table is catalog::planAlterTable(): from the table’s old and
new definitions, either native ALTER TABLE statements or the rebuild
SQLite’s documentation describes, with legacy_alter_table on so that
renaming the new table does not rewrite references. catalog::alterTable()
runs the plan as one task, with foreign keys off and checked afterwards.
The dialog shows the plan’s SQL before anything runs.