feat: SQLite DB adapter (sqlite3: ^3.5.1) - #148
Merged
Conversation
Required by `sqlite3: ^3.5.1` (and its build hooks), which is added in the following commit. The rest of this commit is mechanical. `dart format` selects its style from the language version resolved from the SDK constraint, so raising the floor from 3.7.0 to 3.10.0 reflows most of the package. `master` is format-clean under the old constraint, and CI runs `dart format -o none --set-exit-if-changed .`, so the reflow has to land with the bump. Regenerated `*.reflection.g.dart` for the same reason (their emitted `languageVersion` moves to 3.10.0). No behavior changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds `DBSQLiteAdapter`, a fourth `DBSQLAdapter` alongside PostgreSQL, MySQL and
the in-memory one. Supports a database file or an in-memory database, needs no
server, and needs no native library to be installed: the `sqlite3` package
bundles SQLite (3.53.4) through Dart's build hooks.
Registered as `sqlite`, `sqlite3`, `sql.sqlite` and `sql.sqlite3`.
Entry points `package:bones_api/bones_api_db_sqlite.dart` and, for tests,
`package:bones_api/bones_api_test_sqlite.dart` (`APITestConfigSQLite`).
Runs the same shared entity suite as the PostgreSQL and MySQL adapters
(`runAdapterTests`), over the same reflection/generateTables/checkTables/
populateSource matrix, plus an in-memory pass — and needs no Docker to do it.
Dialect specifics, each verified against the bundled engine:
- `INTEGER PRIMARY KEY AUTOINCREMENT` for auto-assigning IDs. SQLite has no
`SERIAL`, and the generic `SERIAL PRIMARY KEY` is *silently accepted* while
leaving every ID `NULL`. `AUTOINCREMENT` is what matches PostgreSQL/MySQL
semantics: a plain `INTEGER PRIMARY KEY` reuses the ID of a deleted row.
- `ENUM` is emulated with `VARCHAR CHECK (col IN (...))`.
- `ON CONFLICT DO NOTHING` rather than MySQL's `INSERT IGNORE`.
- `LIMIT -1 OFFSET n`: a bare `OFFSET` is a syntax error, and the default
`offsetMaxLimitValue` (2^64-1) overflows SQLite's int64.
- No implicit index for foreign keys, so they are emitted explicitly.
- `constraintSupport: false`: SQLite has no `ALTER TABLE ... ADD CONSTRAINT`.
- `PRAGMA foreign_keys = ON` (off by default, and per connection); WAL and
`busy_timeout` for file databases.
- Scheme introspection via `pragma_table_info` / `pragma_foreign_key_list`
rather than an `information_schema`.
- Bound values are normalized at a single choke point: `sqlite3` binds only
int/double/String/List<int>/null, so bool, `DateTime`, `Time`, `BigInt`,
`DynamicInt`, `Decimal` and enums are converted there.
- Constraint errors map to `EntityFieldInvalid`. SQLite's message names only
`table.column`, so the rejected *value* is recovered from the statement's
bound parameters.
Concurrency: `sqlite3` is a synchronous driver and SQLite allows a single
writer, so the adapter shares one native handle across every pooled connection
wrapper. A second handle would be a hazard rather than a benefit — one blocking
on a lock stalls the isolate holding it, so the holder could never commit
(measured: a second writer on a WAL file blocked for the full `busy_timeout`
and then failed) — and there is no I/O wait to overlap. Capping the pool at 1
would not have sufficed, since `Pool.catchFromPool` force-creates elements
beyond `maxConnections` under contention. Transactions opened while one is
already active nest as a `SAVEPOINT`.
Also:
- New `SQLDialect.returningAcceptsTableWildcard`, defaulting to `true` so the
PostgreSQL/MySQL/memory dialects emit exactly what they did before. SQLite
rejects the table-qualified wildcard in `DELETE ... RETURNING`
("RETURNING may not use TABLE.* wildcards") and needs a bare `RETURNING *`.
The MySQL temporary-table fallback is not an option here: its
`CREATE TEMPORARY TABLE ... AS ( SELECT ... )` is invalid SQLite.
- The shared create-table assertion now also accepts `AUTOINCREMENT` after
`PRIMARY KEY`. Strict generalization: no other adapter's expectation changes.
- README: document the adapter, and correct the adapter class names in the
`SQLAdapter` list, which still used the pre-`DB*` names.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gmpassos
force-pushed
the
feat/db-adapter-sqlite
branch
from
August 11, 2026 22:06
112c47d to
ca70200
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #148 +/- ##
==========================================
+ Coverage 67.77% 68.12% +0.35%
==========================================
Files 64 66 +2
Lines 21607 22114 +507
==========================================
+ Hits 14644 15066 +422
- Misses 6963 7048 +85
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
DBSQLiteAdapter, a fourthDBSQLAdapteralongside PostgreSQL, MySQL and the in-memory one — an embedded, file-backed (or in-memory) SQL backend that needs no server and no native library to install: thesqlite3package bundles SQLite 3.53.4 via Dart's build hooks.Registered as
sqlite,sqlite3,sql.sqlite,sql.sqlite3.Reviewing this PR
Read the two commits separately.
0004437chore: bump minimum Dart SDK to 3.10.0 — mostly mechanical, ~3k lines.sqlite3 ^3.5.1requires SDK >= 3.10.0, anddart formatpicks its style from the resolved language version, so raising the floor reflows most of the package.masteris format-clean under the old constraint and CI runsdart format --set-exit-if-changed, so the reflow has to land with the bump. Regenerated*.reflection.g.dartfor the same reason. No behavior changes.112c47dfeat: SQLite DB adapter — the actual change.Verified against the real engine
Each dialect decision below was checked by executing the generated SQL, not inferred:
INTEGER PRIMARY KEY AUTOINCREMENTfor auto-assigning IDs. SQLite has noSERIAL, and the genericSERIAL PRIMARY KEYis silently accepted (unknown type names get NUMERIC affinity) while leaving every IDNULL.AUTOINCREMENTis what matches PostgreSQL/MySQL semantics — a plainINTEGER PRIMARY KEYreuses the ID of a deleted row (measured:[1, 2]vs[1, 3]).ENUMemulated withVARCHAR CHECK (col IN (...)).ON CONFLICT DO NOTHINGrather than MySQL'sINSERT IGNORE.LIMIT -1 OFFSET n— a bareOFFSETis a syntax error, and the defaultoffsetMaxLimitValue(2^64-1) overflows SQLite's int64.constraintSupport: false(noALTER TABLE ... ADD CONSTRAINT);PRAGMA foreign_keys = ON(off by default, per connection); WAL +busy_timeoutfor file databases.pragma_table_info/pragma_foreign_key_listinstead of aninformation_schema.sqlite3binds only int/double/String/List<int>/null, so bool,DateTime,Time,BigInt,DynamicInt,Decimaland enums are normalized at a single choke point.EntityFieldInvalid. SQLite's message names onlytable.column, so the rejected value is recovered from the statement's bound parameters (PostgreSQL and MySQL get it from the driver message).One shared native handle
sqlite3is a synchronous driver and SQLite allows a single writer, so the adapter shares one handle across every pooled connection wrapper. A second handle is a hazard rather than a benefit: one blocking on a lock stalls the isolate holding it, so the holder can never commit — measured, a second writer on a WAL file blocked for the fullbusy_timeoutand then failed withdatabase is locked. And there is no I/O wait to overlap.Capping the pool at 1 would not have sufficed:
Pool.catchFromPoolforce-creates elements beyondmaxConnectionsunder contention. Transactions opened while one is already active nest as aSAVEPOINT.(Shared-cache in-memory URIs are not an option either — the bundled SQLite is compiled with
OMIT_SHARED_CACHE.)Changes outside the adapter
SQLDialect.returningAcceptsTableWildcard, defaulting totrueso PostgreSQL/MySQL/memory emit exactly what they did before. SQLite rejects the table-qualified wildcard inDELETE ... RETURNING("RETURNING may not use TABLE.* wildcards") and needs a bareRETURNING *. The MySQL temporary-table fallback isn't usable here — itsCREATE TEMPORARY TABLE ... AS ( SELECT ... )is invalid SQLite.AUTOINCREMENTafterPRIMARY KEY. Strict generalization; no other adapter's expectation changes.SQLAdapterlist, which still used the pre-DB*names (PostgreSQLAdapter→DBPostgreSQLAdapter, etc.).Minimum Dart SDK is now 3.10.0 (was 3.7.0).
Testing
The adapter runs the same shared entity suite as PostgreSQL and MySQL (
runAdapterTests) over the same reflection/generateTables/checkTables/populateSource matrix, plus an in-memory pass — and needs no Docker. NewAPITestConfigSQLiteinpackage:bones_api/bones_api_test_sqlite.dart; newsqlitetag indart_test.yaml.dart test test/bones_api_entity_db_sqlite_test.dartdart test --exclude-tags dockerdart test --platform chromedart analyze --fatal-infos --fatal-warnings .dart format -o none --set-exit-if-changed .dart pub publish --dry-rundependency_validatorNot run locally: the Docker-tagged PostgreSQL and MySQL suites — no Docker daemon on this machine. They exercise
generateDeleteSQL, the one shared function this PR touches, so they are worth confirming in CI. The new dialect flag defaults to the existing behavior, and theSQLDialect defaultstest asserts that for the generic/PostgreSQL/MySQL dialects.🤖 Generated with Claude Code