diff --git a/00-overview/decisions/0005-sqlite-wal.md b/00-overview/decisions/0005-sqlite-wal.md index c530824..c099340 100644 --- a/00-overview/decisions/0005-sqlite-wal.md +++ b/00-overview/decisions/0005-sqlite-wal.md @@ -1,6 +1,6 @@ # ADR-0005: SQLite with WAL journal mode as the canonical store -**Status:** Accepted +**Status:** Accepted; the portability claim below is superseded by [ADR-0022](0022-sqlite-only-schema.md) **Date:** 2026-05-27 **Graduated from:** product-repo Phase 17, decision D-32 @@ -49,7 +49,7 @@ proceed while a writer is active. | Option | Why it lost | |--------|-------------| -| **PostgreSQL 16** | Every operational cost above, for capability the product does not need. The migration path is preserved: no SQLite-only schema feature is used. | +| **PostgreSQL 16** | Every operational cost above, for capability the product does not need. *(This row claimed the migration path was preserved. It is not — thirty-two migrations use SQLite-only triggers and search is built on FTS5. See [ADR-0022](0022-sqlite-only-schema.md).)* | | **MySQL / MariaDB** | Same rejection, plus a less clean framework-default story. | | **SQLite with the default rollback journal** | The scheduler-plus-worker-plus-request concurrency produced "database is locked" errors during background jobs. WAL is the fix. | | **A separate database file per subsystem** | The cross-file consistency story was more complex than the value it added, and the framework's database queue driver is happy sharing the application database. | diff --git a/00-overview/decisions/0022-sqlite-only-schema.md b/00-overview/decisions/0022-sqlite-only-schema.md new file mode 100644 index 0000000..0b966df --- /dev/null +++ b/00-overview/decisions/0022-sqlite-only-schema.md @@ -0,0 +1,91 @@ +# ADR-0022: The schema is SQLite-only, including for self-hosting + +**Status:** Accepted +**Date:** 2026-07-27 +**Supersedes:** the portability claim in +[ADR-0005](0005-sqlite-wal.md#alternatives-considered) — "the migration path is +preserved: no SQLite-only schema feature is used". Its decision, SQLite in +write-ahead journal mode, stands and is reinforced. + +## Context + +ADR-0005 chose SQLite and, in rejecting PostgreSQL, recorded that the door was +left open: no SQLite-only schema feature was in use, so a move to a server +database would remain a Laravel-level concern. + +That stopped being true, and nobody noticed because nothing tested it. Walking +the self-hosting instructions on a clean machine surfaced it: the shipped +`deploy/server/` stack runs `postgres:16-alpine`, and `artisan migrate` against +it fails on the first substantive table. + +```text +SQLSTATE[42601]: syntax error at or near "NEW" +SQL: CREATE TRIGGER transactions_type_check_insert BEFORE INSERT ON transactions + FOR EACH ROW WHEN NEW.type NOT IN (...) BEGIN SELECT RAISE(ABORT, '...'); END +``` + +A survey of the migrations found the coupling is not incidental: + +| Feature | Migrations | +|---------|-----------| +| `RAISE(ABORT, …)` enum-guard triggers | **32** | +| An FTS5 virtual table for full-text search | 1 | + +The triggers are a mechanical port — PostgreSQL wants a trigger function rather +than an inline body. **FTS5 is not.** Full-text search would have to be rebuilt +on `tsvector`, with its own indexing strategy, ranking behaviour and migration +of the existing index. That is a project, not a compatibility shim. + +## Decision + +**SQLite is the only supported database, in every deployment shape — desktop +bundle, mobile shell, and self-hosted server.** The schema may use SQLite +features freely, and does. + +The self-hosting recipe ships SQLite. The PostgreSQL and MySQL options in the +deployment guide are withdrawn: they described something that could not work. + +## Why this rather than making the schema portable + +Portability was never a requirement of this product; it was a door left open in +case one appeared, and the door had already closed without anyone checking. +Re-opening it would mean rewriting search on a second engine to serve a use case +nobody has asked for — a single-person or two-person household running their own +copy is not a workload SQLite struggles with. + +The honest position is the one the code already takes. + +## Consequences + +### Positive + +- The deployment guide describes something that works. Anyone following it now + reaches a running instance rather than a syntax error on the first migration. +- One database engine across every shape, so a bug reproduces everywhere and the + test suite covers what ships. +- The schema can keep using the features it already relies on without a portability + claim hanging over them. + +### Negative + +- **A server database is no longer a documented escape hatch.** An installation + that outgrows SQLite has no supported migration path, and creating one would be + the project described above. +- SQLite concurrency limits now apply to the self-hosted shape too. Write-ahead + journal mode plus a busy timeout is the mitigation, and the workload — one + household — is well inside it. + +### Neutral + +- Nothing changes for the desktop or mobile bundles, which were already SQLite. + +## Revisit if + +- A self-hosted deployment appears with genuine multi-writer concurrency, which + would make the search rewrite worth costing rather than assuming. + +## Related + +- [ADR-0005](0005-sqlite-wal.md) — the store decision this reinforces +- [ADR-0004](0004-local-only-hosting.md) · [ADR-0007](0007-database-queue-driver.md) +- [20-architecture/data-model.md](../../20-architecture/data-model.md)