Skip to content

← All posts

Three ways a panel can say nothing, and why they differ

Two panels refuse with their reason, one stays empty in the engine's own words, a third reads not measured, and each of those is a different claim about the database.

Open the monitoring dashboard on an embedded LibreDB file and several panels have nothing in them. That is not a defect, and it is not one condition either. In database monitoring, missing metrics have a meaning, and the meaning is different in each panel: one was measured and came back at zero, one was never measurable, one was asked and the engine has no record, and two were not asked at all because there is nothing there to ask.

A blank rectangle can stand for any of them, and rendering all four the same way leaves the reader to guess which one they are looking at. On a LibreDB connection all four occur at once on the same screen, which makes it a workable place to pull them apart.

Refused, empty, N/A and zero

Here is the taxonomy, stated as four distinct claims about the world.

Rendering The claim it makes
A number, including 0 This was measured. The reading is this.
N/A over Not measured There is no counter here to read.
An empty list plus a sentence The engine was asked and keeps no such record.
Absent, with its reason There is no such object, so the question does not apply.

An empty array is the dangerous one, because it looks like humility and is not. [] in a Sessions panel says the store was asked who was connected and answered nobody. That is a measurement. If nobody can be asked, the array is a fabricated reading, and the panel has no way to tell it apart from a real one.

This is why refusing carries more information than answering empty. A refusal names the reason at the point where the reason applies. An empty list hands the reader a shape they already know how to misread.

The cost of getting it wrong was measured before the change. On a file holding a 25-row relational table, a 7-document collection and 4 raw key-value keys, getMonitoringData() answered "tableCount": 5 in the Overview and "tables": [] in the panel directly beside it. The dashboard reported zero tables on a database with tables. LibreDB is the embedded engine of the zero-config first run, so that is the first dashboard many people ever open.

Why there is no session to list here

getActiveSessions() throws LIBREDB_ACTIVE_SESSIONS_REFUSAL. The panel is absent and its reason is printed where it would have been.

The reason is structural. A .libredb file has no server and no wire protocol; it is opened in-process by the @libredb/libredb package, inside the Studio server that is drawing the dashboard. There is no session, connection or client call anywhere in the shipped surface of that package to read.

The nearest thing to a registry is the exclusive lock sidecar open() takes, and it was checked rather than assumed. Measured, <path>.lock holds four lines:

libredb-lock
<pid>
<hostname>
<nonce>

That pid is this very server’s own. There is no user, no statement and no start time in it, so there is no session row to build even by inference. A single line reading “this process” would be a claim about concurrency that the file cannot support.

Note what is not refused: getHealth() still answers, with activeConnections: 1 and activeSessions: []. That is deliberate rather than inconsistent. The connection dialog’s Save is gated on POST /api/db/test-connection, which calls health, so a throwing health check would lock the embedded engine out of the product entirely. A health check exists to answer whether the thing is reachable. A monitoring panel has the opposite obligation - it is the surface whose job is to say what it could not read.

Why there is no index object to describe

getIndexStats() throws LIBREDB_INDEX_STATS_REFUSAL, for a different reason than sessions and worth separating.

The kernel is a single ordered key-value keyspace. A key’s own byte order is the only index there is, and it is not an object: nothing declares it, nothing names it, nothing can be rebuilt or dropped. The persisted catalog records which lens a namespace belongs to and, for a relational namespace, its declared columns - and nothing that indexes them.

So an empty Indexes panel would read as this database has no indexes yet, which is an invitation. It suggests the next step is to create one. On this engine there is no next step; the state is permanent, not initial.

One number survives that refusal. getOverview().indexCount stays 0, because that genuinely is a measurement: the count of index objects is zero and always will be. Zero and absent are the two claims easiest to confuse and the two it matters most to keep apart. The same split governs the Performance tab, where getPerformanceMetrics() returns an empty object and the Cache Hit, Buffer and Deadlocks cards read N/A beside Not measured rather than a percentage or a 0 badged healthy. There is no buffer pool here with hits and misses, so there is no counter to read and nothing a ratio would be a ratio of.

The scan cap that makes a count untrustworthy

The Tables panel does answer, and it answers from the same bounded scan the schema tree uses, so the tree and the panel cannot disagree. One row per namespace, key count as the row count, the namespace’s lens (relational, document or kv) in the schema column. The byte fields stay absent and totalSize reads N/A, because the file format keeps no per-namespace size and a 0 would be summed by the Storage tab as a measurement.

That scan stops at LIBREDB_MAX_KEY_SCAN, which is 10,000 keys, reached via kv.range('', '\u{10FFFF}') over the whole keyspace. Past that point every count is short by an unknown amount - not approximately right, but wrong by a quantity nobody can state. So above the cap the panel refuses with LIBREDB_TABLE_STATS_TRUNCATED instead of publishing the low number.

Here is the limit, plainly. On this engine, active sessions and index statistics are refused with their reason rather than answered empty, and table statistics refuse above the ten thousand key scan cap rather than publishing counts short by an unknown amount. If your file is larger than that, this panel will tell you it cannot count rather than count badly.

The schema tree still lists the namespaces it reached, and that is not a contradiction: a list of namespaces is one reading’s reach, while a row count is a quantity. The first can be partial and still true. The second cannot.

Reading the dashboard

Applied to a LibreDB connection, the monitoring panels sort like this. Overview, Storage and Tables answer. Performance and Pool read N/A over Not measured. Queries is empty and carries the engine’s own sentence - LibreDB keeps no statistics about finished statements in this version - which is the one panel allowed to stay empty, precisely because the provider declares that sentence. Sessions and the index statistics are absent with their reasons. The Vacuum summary card reads N/A over Not supported, since runMaintenance() throws here and a bloat count over no rows once produced a 0 badged green, which is a clean bill of health for an operation that does not exist.

This is the rule the rest of the product runs on: what each panel can show is bounded by what the engine reports, and every engine’s deliberate absences are published on its own page rather than discovered at runtime. The dashboard is just where that rule is hardest to follow, because blankness is cheap and a reason costs someone a paragraph.