tests/data lacked twelve migrations, and every test app ran them before
its test began: about four seconds an app, which put core and apis past
go test's ten minutes on the runner, and past thirty once the timeout
was raised. The fixture now holds them (go run ./tests/fixture writes
it), and TestTheFixtureIsMigrated fails, naming the migration, while
one is missing. On one host core went from 267 s to 26 s, apis from
432 s to 23 s, and the whole suite runs in 51 s.
The delete tests gave a background goroutine 100 ms to remove a
record's files, which a slow runner did not always meet. BaseApp
counts those deletions and WaitFileDeletes waits for them: the tests
wait on the count instead of the clock, and ResetBootstrapState waits
too, so a Base that stops no longer drops a deletion in flight.
TestSQLRun expected a CREATE TABLE to report one affected row. SQLite
answers a statement that writes no rows with the connection's last
count, and that row was the last migration's _migrations insert; the
handler now reads 0, the number it changed.
go test runs with its default timeout again, and crm_test.go is gofmt'd.
Adding a field the collection did not have ran it through the single to
multiple conversion, which read the fresh JSON column as text. SQLite let
that pass; PostgreSQL refused it (`COALESCE(jsonb, '')`), so no select,
relation or file field with several values could be added to an existing
collection there, and the CRM module could not install. A new field has
no old shape to convert, so it is skipped.
A composite value is read as text like a JSON field, so comparing a whole
value with null is IS NULL and a LIKE compares text on both engines.
The engine corpus installs the CRM and filters its values on both.
The admin had no storage surface at all: nothing in ui-react named an engine, a
file or a size, so the one question a growing Base raises — how much is in here
and where is it — could only be answered by finding the process's data
directory.
`/v1/database` answers it, and Settings -> Data renders it. core.DescribeDatabase
measures the engine, the files while the app holds them, and a count per
collection through CountRecords, so both engines answer the same questions and
the answer has one shape. A collection the engine refuses to count reads as
unknown rather than as empty, and is logged, so one broken view leaves the rest
of the report standing rather than taking the page down.
core.ReclaimDatabase is the one thing the page can DO, and it does something
real: it rewrites both databases without the pages deleted records left behind,
folds the log back into them, refreshes the statistics, and returns the size
either side so the page reports what was freed rather than claiming something
was. Driven in a browser it took a database from 9.0 MB to 1.1 MB with the
record counts unchanged.
The rewrite goes BEFORE the fold. A VACUUM lands through the log like any other
write, so folding first leaves the whole rewrite sitting in the log and the app
holding more than it started with — 2,068,480 bytes to 2,665,912 that way, and
to 593,920 this way. Both orders shrink something, which is why
TestDatabaseReclaim measures each database separately: an earlier version was
satisfied by 8 KB off the auxiliary log and went on passing with the rewrite
removed entirely.
There is no switch, because whether an instance belongs on a server is the
host's question and Base stopped having an opinion about it. The page states
which of the two it is on and what the other would mean, and it does not name a
backend Base cannot open — a disabled control for one would be the datastore
tier constant again, in a place a customer can see.
core.LocalDatabase is now the single place that asks whether the app holds the
database; backup asked it inline and now asks it there. tests/engine gains the
cross-engine cases, which CI already runs against PostgreSQL 18 — the LLM.md
line saying it does not was two releases stale. The third copy of a byte
formatter went into ui-react/src/lib/format.ts and the two existing pages now
read from it.
Base serves its data plane on SQLite and on a server, and only one of them was
ever run. So a corpus builds every fixture through Base's own API, carries no
engine in it, and runs on both: matching without regard to case, truth values,
numbers, numbers reached through a JSON document, paging, sorting, a collection
created down to its columns and declared index, a field added to a live
collection and queried at once, eight concurrent writers, settings that survive
a restart without being stored in the clear, and a backup that refuses where it
cannot hold the database.
A test app is handed a schema carved out of the named server, which is a
millisecond against eighty-five for a database, and the catalog is read through
the current schema, so the isolation is the same. The server is named to the
tests and to nothing else — where a Base lives is still the host's question,
answered in code. Unreachable is a failure rather than a fall back to a file,
because a gate that tests one engine twice reports the health of a gate.
Ordering by text is left out on purpose: bytes on one engine, a collation the
operator chose on the other, so it is not Base's answer to assert.
A release tag is refused at or below what is published, and the free number is
named. A version publishes verbatim and as a bare alias, while a build between
releases derives the next free patch — so the two climb the same series, and a
release below the mark would have republished a build under a name someone had
already pinned. The gate runs before the push, so the collision cannot happen
rather than being reported after it has.
Co-authored-by: Hanzo Dev <[email protected]>