spacr.qt.screens.db_browser¶
Workflow inputs and outputs¶
Database Browser¶
Browse selected tables and export rows; table inspection does not rerun image analysis.
Open: the application’s Help/tools menus.
Inputs and outputs below include conditional alternatives. The guidance and handoff notes say which route applies.
Inputs
Measured objects — measurements/measurements.db; object tables depend on the enabled cell, nucleus, pathogen and organelle masks. Relevant tables, depending on the route:
cell,nucleus,pathogen,cytoplasm. Relevant columns, depending on the route:plateID,rowID,columnID,fieldID.
Outputs
Figures and table exports — The output location chosen by the tool; exports describe the selected data and filters.
Before this module
Measure: Inspect actual tables before exporting.
Database Browser — a read-only query panel for a spaCR measurements.db.
Answering “how many cells are in plate1?” or “what does the png_list
table actually contain?” used to mean dropping to a terminal and typing
sqlite3 .../measurements/measurements.db. This screen puts the same
questions one click away, and — unless you deliberately arm edit mode —
without ever giving the GUI a way to write to the file.
Layout:
┌───────────────────────────────────────────────────────────────────┐
│ /data/plate1/measurements/measurements.db [DB…] [Run folder…] │
│ ☐ Edit mode Read-only — enable editing in Preferences first. │
├──────────────┬────────────────────────────────────────────────────┤
│ Tables │ Columns [search…] (120 of 512 columns) │
│ cell 40 │ ┌────────────────────────────────────────────────┐ │
│ nucleus │ │ prc cell_area cell_channel_1_mean… │ │
│ png_list │ │ plate1_A01_1 1204.5 3311.2 │ │
│ │ └────────────────────────────────────────────────┘ │
│ │ showing 100 of ≈412 003 rows (estimate) [Load more]│
├──────────────┴────────────────────────────────────────────────────┤
│ Filter: [column ▾] [op ▾] [value] ☐ raw SQL [Apply] [Clear] │
│ [Export filtered CSV]│
└───────────────────────────────────────────────────────────────────┘
Design notes that matter for real spaCR databases:
Never
SELECT *the whole table. Measurement tables run to hundreds of thousands of rows and hundreds of feature columns. The first chunk (100 rows) is fetched and painted on its own; the rest arrives as the user scrolls, throughcanFetchMore/fetchMore.Keyset paging, not OFFSET. Each chunk asks for
rowid > <last rowid seen>andORDER BY rowid.LIMIT ? OFFSET ?makes SQLite walk (and throw away) every skipped row, so chunk 500 of a 400 k-row table would cost 500× chunk 1 — the “fast” version would get slower the further you scrolled. Only tables with no usable key (views, composite-primary-keyWITHOUT ROWIDtables) fall back toOFFSET, and those are small by construction.The count never blocks the first paint.
SELECT COUNT(*)is a full scan. The first chunk is painted againstmax(rowid), which is O(1), and that number is always rendered as “≈… (estimate)”. The exactCOUNT(*)follows on its own job and replaces it.Off the GUI thread. Every chunk, count and export goes through
spacr.qt.bridge.make_thread(), the same helper the pipeline screens use, and each worker opens (and closes) its own sqlite connection —sqlite3objects are not shareable across threads. Jobs queue and run one at a time; seeDbBrowserScreen._run_job()for why twoPipelineWorkers must not overlap.Cancellable. Switching table or database bumps a load token. Results tagged with a stale token are dropped, so an in-flight load can never paint the previous table’s rows into the new view, and a job that has not started yet is dropped without spending a thread. Nothing is ever killed mid-flight.
Read-only by default, structurally. Browsing connections are opened with the
file:…?mode=roURI andPRAGMA query_only = ON. A write is rejected by SQLite itself, not by a check we could forget. Editing is a separate, opt-in path (see below) with its own connection.No string-formatted values. Everything the user types is bound as a
?parameter. Identifiers (table + column names) never come from free text — they are matched against the live schema and only then double-quoted.No modal dialogs on any error path. Problems land in an inline status label; a headless run can never block on a message box. The single exception is the edit-mode confirmation, which is injectable (
DbBrowserScreen.confirm_edit_mode).
Editing (opt-in, and guarded five ways)¶
An UPDATE against measurements.db is unrecoverable, so every guard
below has to fail open to read-only:
spacr.qt.preferences.get_db_browser_editable()must be on — it is off by default and lives in Preferences, not on this screen.The database must have been chosen explicitly in this session (
set_database(..., explicit=True)).The user must tick “Edit mode” and confirm; ticking alone does nothing.
The row must be addressable by
rowidor a primary key. Without one, the edit is refused — an UPDATE matching on column values can hit many rows, which on a measurements table is silent mass corruption. The write also probesCOUNT(*)for the row address first and rolls back unlessrowcount == 1.The typed text must be coercible to the column’s declared type. SQLite will cheerfully store
'abc'in an INTEGER column;coerce_for_column()refuses instead.
Loading a different database always resets edit mode to off.
Exceptions¶
An edit was rejected before anything was written. |
Classes¶
Browser for a spaCR measurements database — read-only by default. |
|
Holds the rows fetched so far plus the column-visibility mask. |
|
A read-only handle on a sqlite database. |
|
A read-write handle used only by an armed edit mode. |
Functions¶
|
Return the one statement this screen is ever willing to run. |
|
Build a |
|
Return the value to bind for |
|
Return SQLite's type affinity for a declared column type. |
|
Put db_browser's fold strip on |
|
Double-quote a SQL identifier, escaping embedded quotes. |
|
Return the absolute path of the sqlite file |
|
Return |
Module Contents¶
- exception spacr.qt.screens.db_browser.EditRefused[source]¶
Bases:
ExceptionAn edit was rejected before anything was written.
Distinct from
sqlite3.Errorso the screen can tell “we refused this” (a guard fired, nothing happened) from “SQLite refused this” (a constraint, a locked file).Initialize self. See help(type(self)) for accurate signature.
- class spacr.qt.screens.db_browser.DbBrowserScreen(parent=None, threaded: bool = True)[source]¶
Bases:
spacr.qt.linked_selection.LinkedView,PySide6.QtWidgets.QWidgetBrowser for a spaCR measurements database — read-only by default.
Joined to the shared selection as
"db_browser", in both directions:selecting rows publishes them, so a row picked out here lights up on the plate heatmap and in the UMAP;
a selection published elsewhere selects and scrolls to the same rows;
a filter published elsewhere HIDES rows, which a selection never does.
All three act on the rows already fetched. Nothing here re-queries: the shared filter is a lens over the page in memory, and the row-count label keeps saying how much of the table that page is.
- Parameters:
parent – optional Qt owner responsible for the screen’s lifetime.
threaded – run queries on a worker thread (the default). Tests pass
Falseto get deterministic, synchronous behaviour.
- Variables:
last_error – text of the most recent failure,
""when the last operation succeeded. Errors are only ever reported here and in the inline status label — never in a modal dialog.confirm_edit_mode – callable taking the confirmation text and returning a bool. Replace it to arm edit mode without a dialog (every test does). The default opens the one QMessageBox this screen owns.
auto_count – when True (the default) the exact
COUNT(*)follows the first chunk on its own job. Set False to browse a very large table on themax(rowid)estimate alone.
Build the browser: the table list, the preview and the controls.
- Parameters:
parent – parent widget.
- apply_filter() bool[source]¶
Read the filter row, validate it, and reload from the first chunk.
Returns False (and reports inline) when the filter is malformed.
- apply_seed(seed: Dict[str, Any]) None[source]¶
Open a database (and optionally a table) another screen sent here.
The generic hand-off seam
MainWindow._on_train_requestedlooks for. Everything is optional and anything unusable is ignored rather than raised: this is a convenience jump, and a screen that refuses to open because a seed was stale is worse than one that opens on the wrong table.- Parameters:
seed –
db_path, and optionallytableandcolumn.
- closeEvent(event)[source]¶
Let every in-flight query thread finish before the widget dies.
Destroying a QThread that is still running aborts the process, so we wait (briefly) rather than hope. Jobs that have not started yet are dropped — nothing has been spawned for them.
The shared link outlives this screen, so let go of it too.
- Parameters:
event – Qt close event forwarded after worker cleanup.
- current_table() str[source]¶
Which table the browser is showing.
- Returns:
the table’s name, or
""when none is open.
- disable_edit_mode(quiet: bool = False) None[source]¶
Drop back to read-only. Safe to call when already read-only.
- Parameters:
quiet – suppress the read-only status message when true.
- edit_cell(row: int, column: str, text: Any) bool[source]¶
Write one cell of one row. Returns False, with a reason, if refused.
This is the only path in the screen that can write, and it is also what
PreviewModel.setData()calls when a cell editor closes.- Parameters:
row – zero-based loaded model row to address.
column – schema column whose value the user edited.
text – raw editor value to coerce against the declared type.
- Returns:
whether exactly one database row was updated or already held the requested value.
- editing_allowed_by_preference() bool[source]¶
Whether Preferences permits edit mode at all (read fresh).
- enable_edit_mode() bool[source]¶
Arm edit mode for the open database, if every guard allows it.
Requires the Preferences opt-in, a database the user chose in this session, and an explicit confirmation. Returns False and explains inline otherwise; never raises.
- export_csv(out_path: str) bool[source]¶
Write the current table + filter + visible columns to
out_path.Runs off the GUI thread and streams the result, so a 400 000-row export neither freezes the window nor loads the table into memory. Reports inline on failure and returns
False.- Parameters:
out_path – destination CSV path selected by the user.
- Returns:
whether the export job was accepted for execution.
- fetch_more() bool[source]¶
Fetch the next chunk. Called by the view when it scrolls to the end.
Returns False when there is nothing to fetch or a chunk is already in flight — a scroll must never queue ten of them.
Model rows the shared filter is hiding, ascending.
- on_linked_filter_changed(data_filter: spacr.selection.DataFilter) None[source]¶
Apply a newly published shared filter to the loaded rows.
- Parameters:
data_filter – current process-wide filter; the linked-view state already owns it, so this callback only refreshes row visibility.
- on_linked_selection_changed(selection: spacr.selection.Selection) None[source]¶
Select and scroll to the rows somebody else picked.
Nothing is hidden: rows the selection does not name stay exactly where they are. Only the shared filter removes rows from view.
- Parameters:
selection – process-wide object selection published elsewhere.
- row_count() int[source]¶
Best known total for the current table + filter.
Exact once
COUNT(*)has landed or the table has been read to the end; otherwise themax(rowid)estimate, or the number of rows loaded so far. Askrow_count_is_estimate()before quoting it as a fact.
- row_count_is_estimate() bool[source]¶
True while
row_count()is not known to be exact.
- rows_for_selection(selection: spacr.selection.Selection) List[int][source]¶
The loaded rows
selectionnames, ascending.Empty — not an error — for a table with no object identity in it (
png_listkeyed on a path, a summary, a view). A shared selection does not identify those rows.- Parameters:
selection – shared object identity selection to match.
- select_rows(rows: Sequence[int]) List[int][source]¶
Select
rowsas a user would, publishing them to every view.- Parameters:
rows – zero-based model row indices requested by the caller.
- Returns:
the rows that were in range and got selected.
- select_table(name: str) bool[source]¶
Make
namethe previewed table and reset load and filter state.- Parameters:
name – live table or view name from the open database.
- Returns:
whether the schema was read and the table load was started.
- set_column_filter(text: str) None[source]¶
Narrow the displayed columns to those containing
text.Purely a view operation — no re-query — so it stays instant on a table with hundreds of feature columns. An empty string restores every column.
- Parameters:
text – case-insensitive substring required in visible names.
- set_database(path: str, explicit: bool = True) bool[source]¶
Open
pathread-only and list its tables.Accepts the database file or a run
srcfolder. Any problem (missing file, not a sqlite database, unreadable) is reported in the status label and returnsFalse— this never raises.Always resets edit mode to off: a database the user armed for editing is that database, never the next one.
- Parameters:
path – SQLite database file or run
srcdirectory containing one; paths are expanded and resolved byReadOnlyDb.explicit – True when the user chose this database themselves. Pass False when spaCR opens one on their behalf (a remembered path, a folder handed over by another screen); such a database can be browsed but never edited.
- Returns:
True when a database was opened.
- set_filter(column: str, op: str, value: Any = '') bool[source]¶
Programmatic equivalent of filling in the filter row and applying.
- Parameters:
column – live schema column to filter.
op – structured operator label from
OPERATORS.value – raw filter value; operators such as
is nullignore it.
- Returns:
whether validation succeeded and the reload was started.
- class spacr.qt.screens.db_browser.PreviewModel(parent=None)[source]¶
Bases:
PySide6.QtCore.QAbstractTableModelHolds the rows fetched so far plus the column-visibility mask.
Two jobs beyond the obvious one:
Incremental fetch.
canFetchMore/fetchMoreare the Qt way to say “there is more where that came from”; the view calls them when the user scrolls to the bottom and the model asks the screen for another chunk. While a chunk is in flightcanFetchMoreis False, so a scroll cannot queue ten of them.Row identity. Every row carries the key tuple it was fetched with (
rowid, or the primary key). That is what makes an edit addressable — and its absence is what makes an edit refusable.
Chunks are fetched with every column, and the column search only changes which of them are mapped into the view. Typing in the search box therefore never re-queries the database — which is what makes it usable on a table with 500 feature columns.
- Parameters:
parent – optional Qt owner responsible for the model’s lifetime.
Build an empty preview.
- all_columns() List[str][source]¶
Every column the table has, filtered or not.
- Returns:
the column names.
- append_rows(rows: Sequence[Sequence[Any]], keys: Sequence[tuple | None] | None = None) int[source]¶
Add a fetched chunk to the end and return how many rows landed.
- Parameters:
rows – complete schema-order rows to append.
keys – stable row addresses paired with
rows;Nonesupplies an uneditableNonekey for every appended row.
- canFetchMore(parent=QModelIndex()) bool[source]¶
Return whether the root model can request another chunk.
- Parameters:
parent – Qt parent index; valid child indexes cannot fetch.
- columnCount(parent=QModelIndex()) int[source]¶
Return visible columns for the root model and zero for children.
- Parameters:
parent – Qt parent index whose child-column count is requested.
- data(index, role=Qt.DisplayRole)[source]¶
Return the display, tooltip, or exact editor value for a cell.
- Parameters:
index – model index identifying the requested cell.
role – Qt data role; unsupported roles return
None.
- fetchMore(parent=QModelIndex()) None[source]¶
Request the next chunk through the configured fetch hook.
- Parameters:
parent – Qt parent index; valid child indexes are ignored.
- flags(index)[source]¶
Return Qt item flags for
index, including armed editability.- Parameters:
index – model index whose interaction flags are requested.
- headerData(section, orientation, role=Qt.DisplayRole)[source]¶
Return a column name or one-based absolute row number.
- Parameters:
section – zero-based visible column or row-header section.
orientation – horizontal for names, vertical for row numbers.
role – Qt data role; only
Qt.DisplayRoleis served.
- is_editable() bool[source]¶
Whether this preview allows edits.
OFF UNLESS CHOSEN. The browser reads a project’s real measurements, and an accidental edit there is a silent change to data a run already produced.
- Returns:
True when editing is enabled.
- rowCount(parent=QModelIndex()) int[source]¶
Return loaded rows for the root model and zero for children.
- Parameters:
parent – Qt parent index whose child-row count is requested.
- row_key(row: int) tuple | None[source]¶
Return the key tuple addressing
row, orNone.- Parameters:
row – zero-based model row index.
- setData(index, value, role=Qt.EditRole) bool[source]¶
Commit an editor value through the guarded screen callback.
- Parameters:
index – valid editable model index.
value – raw editor value passed to the commit hook.
role – Qt role, which must be
Qt.EditRole.
- Returns:
whether the commit hook accepted and wrote the change.
- set_column_filter(text: str) None[source]¶
Show only columns whose name contains
text(case-insensitive).An empty string restores every column.
- Parameters:
text – case-insensitive substring required in visible names.
- set_commit_hook(hook: Callable[[int, str, Any], bool] | None) None[source]¶
Set what
setData()calls to actually write a cell.- Parameters:
hook –
(row, column, value)writer, orNoneto disable it.
- set_editable(editable: bool) None[source]¶
Turn cell editing on or off and repaint the item flags.
- Parameters:
editable – whether valid cells advertise Qt’s editable flag.
- set_fetch_hook(hook: Callable[[], Any] | None) None[source]¶
Set what
fetchMore()calls to ask for the next chunk.- Parameters:
hook – zero-argument fetch request, or
Noneto disconnect it.
- set_more(more: bool) None[source]¶
Say whether another chunk can be fetched right now.
- Parameters:
more – true only while an unfetched chunk remains available.
- set_page(columns: Sequence[str], rows: Sequence[Sequence[Any]], row_offset: int = 0, keys: Sequence[tuple | None] | None = None) None[source]¶
Replace the model contents, keeping the current column search.
- Parameters:
columns – schema-order column names represented by every row.
rows – complete values for the newly loaded page.
row_offset – zero-based database offset used for row headings.
keys – stable row addresses, or
Nonefor uneditable rows.
- set_value(row: int, column: str, value: Any) bool[source]¶
Write a value into the in-memory page after a real UPDATE.
- Parameters:
row – zero-based model row index.
column – schema column whose displayed value changed.
value – already-coerced value written to the database.
- Returns:
whether the addressed cell exists and was updated.
- sort(column: int, order=Qt.AscendingOrder) None[source]¶
Sort the loaded rows in memory.
The screen only enables view sorting once the whole table is loaded, so this never sorts a partial result and calls it the table’s order. Keys travel with their rows, so an edit after a sort still addresses the row it looks like it addresses.
- Parameters:
column – zero-based visible column to sort by.
order – Qt sort order, defaulting to
Qt.AscendingOrder.
- class spacr.qt.screens.db_browser.ReadOnlyDb(path: str)[source]¶
A read-only handle on a sqlite database.
Every method opens (and closes) its own connection. That costs a few microseconds and buys thread-safety for free: a chunk query runs on a worker thread while the GUI thread may still be listing tables, and
sqlite3objects are not shareable across threads.- Parameters:
path – database file, or a run folder — see
resolve_db_path().- Raises:
sqlite3.DatabaseError – when the file is not a sqlite database.
Open a database for reading only.
- Parameters:
path – the database file.
- check_columns(table: str, columns: Sequence[str] | None) List[str][source]¶
Return the requested columns, validated against the schema.
Noneor an empty sequence means “all of them”.- Parameters:
table – validated user-table or view name.
columns – requested names, or
None/an empty sequence for the complete schema.
- Returns:
validated names in requested or declaration order.
- Raises:
ValueError – when any requested name is absent.
- check_table(table: str) str[source]¶
Return
tableif the schema really has it, else raise.This is the gate that keeps identifiers out of the “user input” category: a name that isn’t in
sqlite_masternever reaches SQL.- Parameters:
table – candidate user-table or view name.
- Returns:
tableunchanged after validation.- Raises:
ValueError – when the live schema has no such table or view.
- chunk(table: str, columns: Sequence[str] | None = None, where: str | None = None, params: Sequence = (), limit: int = DEFAULT_PAGE_SIZE, after: tuple | None = None, loaded: int = 0, order_by: Tuple[str, bool] | None = None) Tuple[List[str], List[tuple], List[tuple | None]][source]¶
Return
(columns, rows, keys)for the nextlimitrows.afteris the key tuple of the last row already loaded;Noneasks for the first chunk.keys[i]addressesrows[i]uniquely (or isNonewhen the table has no key, in which case the caller must refuse to edit it).- Parameters:
table – validated user-table name to page through. Its schema determines both the returned columns and each row’s stable key.
columns – requested schema columns, or
None/an empty sequence for every column.where – optional validated predicate without
WHERE.params – bound values consumed by predicate placeholders.
limit – maximum number of rows requested for this chunk.
after – stable key tuple of the last row already loaded, or
Nonefor the first chunk.loaded – only used by the
OFFSETfallback for tables with no single-column key.order_by – optional
(column, descending)whole-table order.
- chunk_sql(table: str, columns: Sequence[str], key_columns: Sequence[str], where: str | None = None, after: bool = False, use_offset: bool = False, order_by: Tuple[str, bool] | None = None) str[source]¶
Build the paged chunk SELECT. Exposed so tests can read it.
aftersays whether akey > ?clause is wanted (i.e. this is not the first chunk).use_offsetis the fallback for tables that have no single-column key.order_byis(column, descending)when the user has clicked a column header. It sorts in SQL, over the whole table, which is the only way to sort a table the view has only partly loaded – sorting the rows fetched so far and presenting that as the table’s order is the one option worse than not sorting at all.Two details that are not optional:
the key column is appended as a tiebreak. Without it, rows sharing a value come back in whatever order SQLite happens to produce, and that order is free to differ between two chunks of the same scroll – so a row could be shown twice and another not at all.
paging falls back to
OFFSET. Keyset paging needs the ordering column to be the one being compared, and(value, rowid) > (?, ?)over an arbitrary user-chosen column is a different and much more delicate query. OFFSET makes deep scrolling of a sorted view progressively slower, which is a real cost and the reason it is not the default; it is bounded here because a sort is an explicit act on a table the user is looking at.
- Parameters:
table – schema-validated source table or view.
columns – visible data columns to select after the key columns.
key_columns – stable row-address columns prepended to each row.
where – optional validated predicate without
WHERE.after – whether to add the next-page key comparison.
use_offset – use OFFSET paging when no single key is available.
order_by – optional
(column, descending)whole-table order.
- Returns:
parameterized SELECT containing a bounded
LIMITand, when required,OFFSET.
- column_types(table: str) Dict[str, str][source]¶
Return
{column: declared type}—''for untyped columns.Cached: the edit path asks for this on every keystroke-committed cell, and the schema cannot change under a read-only connection.
- Parameters:
table – validated user-table or view name.
- columns(table: str) List[str][source]¶
Return the column names of
tablein declaration order.- Parameters:
table – validated user-table or view name.
- connect() sqlite3.Connection[source]¶
Return a fresh read-only connection. The caller closes it.
- count(table: str, where: str | None = None, params: Sequence = ()) int[source]¶
Return the exact
COUNT(*)under the current filter.This one is a full scan — never call it on the path that paints the first chunk.
- Parameters:
table – schema-validated table or view to count.
where – optional validated predicate without
WHERE.params – bound values consumed by the predicate placeholders.
- estimate_count(table: str) int | None[source]¶
Return an O(1) estimate of the row count, or
None.max(rowid)is answered from the right-hand edge of the table’s b-tree without scanning, which is the whole point: a realCOUNT(*)on a 400 k-row measurement table takes long enough to be felt. It is only an estimate — deleted rows leave gaps — so every caller must label it as one.Nonewhen the table has no rowid, or is empty.- Parameters:
table – validated table or view to estimate.
- export_csv(out_path: str, table: str, columns: Sequence[str] | None = None, where: str | None = None, params: Sequence = (), chunk: int = 5000) int[source]¶
Stream the filtered result to
out_pathas CSV.Rows are pulled in
chunk-sized batches and written straight out, so exporting a 400 k-row table costs a constant amount of memory.- Parameters:
out_path – destination CSV path; missing parent folders are made.
table – schema-validated table or view to export.
columns – selected columns, or
Nonefor every column.where – optional validated predicate without
WHERE.params – bound values consumed by predicate placeholders.
chunk – cursor batch size used while streaming rows.
- Returns:
number of data rows written (header excluded).
- row_key(table: str) Tuple[str, List[str]][source]¶
Return how one row of
tablecan be addressed uniquely.("rowid", ["rowid"])for an ordinary table,("pk", [...])for aWITHOUT ROWIDtable (its declared primary key), and("", [])for anything with neither — a view, or a table createdWITHOUT ROWIDwith no primary key.Two things hang off this: paging (keyset needs an ordered key) and editing (an UPDATE without a unique address is refused).
- Parameters:
table – validated table or view whose identity is inspected.
- select_sql(table: str, columns: Sequence[str], where: str | None = None) str[source]¶
Build the unbounded SELECT used by the CSV export.
- Parameters:
table – schema-validated source table or view.
columns – schema-validated columns to export.
where – optional validated predicate without the
WHEREword.
- Returns:
SELECT statement containing no user-formatted values.
- table_info(table: str) List[tuple][source]¶
Return raw
PRAGMA table_inforows fortable.- Parameters:
table – validated user-table or view name.
- tables(refresh: bool = False) List[str][source]¶
Return the user tables and views alphabetically.
- Parameters:
refresh – bypass the cached schema inventory when true.
- validate_where(table: str, where: str, params: Sequence = ()) None[source]¶
Let SQLite parse
whereand raise if it is malformed.Prepared with
LIMIT 0, which SQLite short-circuits before it touches a single row — so a typo costs a parse, not a full scan of a 400 000-row measurement table.- Parameters:
table – schema-validated table or view used for name resolution.
where – predicate fragment for SQLite to parse.
params – bound values consumed by the predicate placeholders.
- Raises:
sqlite3.Error – with SQLite’s own message (
no such column: cell_are,near ">": syntax error, …).
- class spacr.qt.screens.db_browser.WritableDb(path: str)[source]¶
A read-write handle used only by an armed edit mode.
Deliberately tiny: it knows how to write one cell of one row and nothing else. There is no
execute(), no DDL, no multi-row update — the class has no method that could touch more than a single row.- Parameters:
path – database file, or a run folder — see
resolve_db_path().
Open a database for reading and writing.
- Parameters:
path – the database file.
- connect() sqlite3.Connection[source]¶
Return a fresh read-write connection in autocommit mode.
isolation_level = Nonehands transaction control back to us, so the BEGIN/COMMIT/ROLLBACK inupdate_cell()are the real ones and do not fight Python’s implicit transaction handling (which differs between 3.11 and 3.12+).
- update_cell(table: str, column: str, value: Any, key_columns: Sequence[str], key_values: Sequence[Any]) str[source]¶
Set one column of one row. Returns the SQL that ran.
The guards, in order:
tablemust be a real table insqlite_master— a view has no row to update.columnand every key column must exist in its schema.the row address must match exactly one row (probed with
COUNT(*)before anything is written);the UPDATE itself must report
rowcount == 1, or the transaction is rolled back.
- Parameters:
table – real schema table receiving the change.
column – existing table column whose value will be replaced.
value – already-coerced value to bind to the UPDATE.
key_columns – rowid alias or primary-key columns addressing it.
key_values – bound address values paired with
key_columns.
- Returns:
exact parameterized SQL statement that ran.
- Raises:
EditRefused – when any guard fires — nothing was written.
sqlite3.Error – when SQLite refuses the write itself (constraint violation, read-only file, locked database).
- spacr.qt.screens.db_browser.build_update(table: str, column: str, key_columns: Sequence[str]) str[source]¶
Return the one statement this screen is ever willing to run.
UPDATE "t" SET "c" = ? WHERE "rowid" = ?— one column, one row address, everything bound. Exposed so the screen can show the user the exact SQL before it runs, and so tests can assert on it without a database.- Parameters:
table – schema-validated table to update.
column – schema-validated column whose value will be replaced.
key_columns – rowid alias or primary-key columns identifying one row.
- Returns:
parameterized single-cell UPDATE statement.
- Raises:
EditRefused – when there is no row address at all. An UPDATE without a unique key would match on values and could rewrite thousands of rows.
- spacr.qt.screens.db_browser.build_where(column: str, op: str, value: Any, columns: Sequence[str]) Tuple[str, tuple][source]¶
Build a
(where_sql, params)pair from the structured filter row.- Parameters:
column – column name; must appear in
columns.op – one of
OPERATORS.value – raw text from the value field — bound, never formatted.
columns – the table’s real column list, from the schema.
- Raises:
ValueError – for an unknown column or operator.
- spacr.qt.screens.db_browser.coerce_for_column(text: Any, decl_type: str | None, column: str = 'value') Any[source]¶
Return the value to bind for
textin a column ofdecl_type.SQLite has no static typing: binding the string
"abc"to an INTEGER column stores the text “abc” there, and every downstreampandas.read_sqlthen gets an object column where it expected a number. This function refuses instead.empty text →
None(SQL NULL). A NOT NULL column then raises from SQLite itself and the write is rolled back.INTEGER affinity →
int, orValueError.REAL affinity →
float, orValueError.TEXT affinity →
str, always.NUMERIC affinity / no declared type →
int, elsefloat, elsestr(which is exactly what SQLite itself would store).anything declared BLOB → refused; binary is not editable as text.
- Parameters:
text – editor value to convert before binding it to SQLite.
decl_type – declared type of the destination column.
column – destination column name used in actionable error messages.
- Raises:
ValueError – when
textcannot be represented in the column’s type.
- spacr.qt.screens.db_browser.column_affinity(decl_type: str | None) str[source]¶
Return SQLite’s type affinity for a declared column type.
The five rules are the ones in the SQLite file-format spec, in order:
INT→ INTEGER,CHAR/CLOB/TEXT→ TEXT,BLOBor no type at all → BLOB,REAL/FLOA/DOUB→ REAL, otherwise NUMERIC. Knowing the affinity is what letscoerce_for_column()refuse a value SQLite would otherwise store with the wrong type.- Parameters:
decl_type – declared SQLite column type, or
None/an empty string when untyped.- Returns:
INTEGER,TEXT,BLOB,REAL, orNUMERIC.
- spacr.qt.screens.db_browser.install_folds(screen: PySide6.QtWidgets.QWidget) spacr.qt.widgets.fold_strip.FoldStrip | None[source]¶
Put db_browser’s fold strip on
screen’s masthead.Reached by the one pass over the stack that serves every host – see
spacr.qt.screens.map_barcodes.FOLD_HOST_MODULES.
- spacr.qt.screens.db_browser.quote_ident(name: str) str[source]¶
Double-quote a SQL identifier, escaping embedded quotes.
Only ever called with a name that has already been matched against the live schema; the quoting is belt-and-braces for identifiers that are legal but awkward (
cell_channel_1 (raw)).- Parameters:
name – schema-validated table or column identifier.
- Returns:
identifier surrounded by SQL double quotes.
- spacr.qt.screens.db_browser.resolve_db_path(path: str) str[source]¶
Return the absolute path of the sqlite file
pathrefers to.Accepts either the database file itself or a run
srcfolder, in which case<src>/measurements/measurements.dbis used — the same layout every other spaCR module assumes (seespacr.measureandspacr.ml).<src>/measurements.dbis accepted as a fallback so pointing at themeasurementsfolder itself also works.- Parameters:
path – file or folder chosen by the user.
- Returns:
absolute path to an existing file.
- Raises:
ValueError – when
pathis empty.FileNotFoundError – when nothing resolvable exists there.
- spacr.qt.screens.db_browser.validate_raw_predicate(text: str) str[source]¶
Return
textstripped, or raise if it is not a lone predicate.The raw box is an explicit power-user escape hatch (
cell_area > 1000 AND well LIKE 'A%'). We refuse statement separators, comments and DDL/DML keywords so it stays a predicate; the read-only connection plusPRAGMA query_onlyguarantee the rest.- Parameters:
text – raw WHERE-clause fragment entered by the user.
- Returns:
stripped predicate after the safety checks.
- Raises:
ValueError – when the fragment is empty or contains statement syntax, comments, or a forbidden SQL keyword.
Nested helpers¶
- DbBrowserScreen._fetch_chunk._job() Dict[str, Any]¶
Read one chunk of the table. Off the GUI thread.
spacr/qt/screens/db_browser.py:2066
- DbBrowserScreen._start_count._job() Dict[str, Any]¶
Count the matching rows, carrying the token that ordered it.
The token comes back with the answer so a slow count from a query the user has since changed can be recognised and dropped.
spacr/qt/screens/db_browser.py:2128