Skip to content

feat: SQLite DB adapter (sqlite3: ^3.5.1) - #148

Merged
gmpassos merged 2 commits into
masterfrom
feat/db-adapter-sqlite
Aug 11, 2026
Merged

feat: SQLite DB adapter (sqlite3: ^3.5.1)#148
gmpassos merged 2 commits into
masterfrom
feat/db-adapter-sqlite

Conversation

@gmpassos

Copy link
Copy Markdown
Contributor

Adds DBSQLiteAdapter, a fourth DBSQLAdapter alongside 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: the sqlite3 package bundles SQLite 3.53.4 via Dart's build hooks.

import 'package:bones_api/bones_api_db_sqlite.dart';

var adapter = DBSQLiteAdapter('/var/lib/myapp/db.sqlite', generateTables: true);
var memory  = DBSQLiteAdapter(':memory:', generateTables: true);
db:
  sqlite:
    path: /var/lib/myapp/db.sqlite
    generateTables: true

Registered as sqlite, sqlite3, sql.sqlite, sql.sqlite3.

Reviewing this PR

Read the two commits separately.

  • 0004437 chore: bump minimum Dart SDK to 3.10.0 — mostly mechanical, ~3k lines. sqlite3 ^3.5.1 requires SDK >= 3.10.0, and dart format picks its style from the resolved language version, so raising the floor reflows most of the package. master is format-clean under the old constraint and CI runs dart format --set-exit-if-changed, so the reflow has to land with the bump. Regenerated *.reflection.g.dart for the same reason. No behavior changes.
  • 112c47d feat: 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 AUTOINCREMENT for auto-assigning IDs. SQLite has no SERIAL, and the generic SERIAL PRIMARY KEY is silently accepted (unknown type names get NUMERIC affinity) while leaving every ID NULL. AUTOINCREMENT is what matches PostgreSQL/MySQL semantics — a plain INTEGER PRIMARY KEY reuses the ID of a deleted row (measured: [1, 2] vs [1, 3]).
  • ENUM 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 FK index; constraintSupport: false (no ALTER TABLE ... ADD CONSTRAINT); PRAGMA foreign_keys = ON (off by default, per connection); WAL + busy_timeout for file databases.
  • Introspection via pragma_table_info / pragma_foreign_key_list instead of an information_schema.
  • sqlite3 binds only int/double/String/List<int>/null, so bool, DateTime, Time, BigInt, DynamicInt, Decimal and enums are normalized at a single choke point.
  • Constraint errors map to EntityFieldInvalid. SQLite's message names only table.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

sqlite3 is 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 full busy_timeout and then failed with database is locked. And there is no I/O wait to overlap.

Capping the pool at 1 would not have sufficed: Pool.catchFromPool force-creates elements beyond maxConnections under contention. Transactions opened while one is already active nest as a SAVEPOINT.

(Shared-cache in-memory URIs are not an option either — the bundled SQLite is compiled with OMIT_SHARED_CACHE.)

Changes outside the adapter

  • New SQLDialect.returningAcceptsTableWildcard, defaulting to true so PostgreSQL/MySQL/memory 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 isn't usable here — its CREATE TEMPORARY TABLE ... AS ( SELECT ... ) is invalid SQLite.
  • Shared create-table assertion now also accepts AUTOINCREMENT after PRIMARY KEY. Strict generalization; no other adapter's expectation changes.
  • README: documents the adapter, and corrects the class names in the SQLAdapter list, which still used the pre-DB* names (PostgreSQLAdapterDBPostgreSQLAdapter, etc.).

⚠️ Breaking

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. New APITestConfigSQLite in package:bones_api/bones_api_test_sqlite.dart; new sqlite tag in dart_test.yaml.

Gate Result
dart test test/bones_api_entity_db_sqlite_test.dart 91 passed
dart test --exclude-tags docker 778 passed
dart test --platform chrome 519 passed
dart analyze --fatal-infos --fatal-warnings . clean
dart format -o none --set-exit-if-changed . clean
dart pub publish --dry-run 0 warnings
dependency_validator no issues

Not 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 the SQLDialect defaults test asserts that for the generic/PostgreSQL/MySQL dialects.

🤖 Generated with Claude Code

gmpassos and others added 2 commits August 11, 2026 19:04
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
gmpassos force-pushed the feat/db-adapter-sqlite branch from 112c47d to ca70200 Compare August 11, 2026 22:06
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.75227% with 561 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.12%. Comparing base (e92bc3e) to head (ca70200).

Files with missing lines Patch % Lines
lib/src/bones_api_entity.dart 54.09% 101 Missing ⚠️
lib/src/bones_api_entity_db_sql.dart 62.22% 85 Missing ⚠️
lib/src/bones_api_entity_db_sqlite.dart 81.05% 83 Missing ⚠️
lib/src/bones_api_server.dart 38.61% 62 Missing ⚠️
lib/src/bones_api_db_module.dart 10.00% 27 Missing ⚠️
lib/src/bones_api_entity_db_relational.dart 67.18% 21 Missing ⚠️
lib/src/bones_api_condition.dart 60.78% 20 Missing ⚠️
lib/src/bones_api_entity_db_object_directory.dart 40.74% 16 Missing ⚠️
lib/src/bones_api_entity_reference.dart 65.21% 16 Missing ⚠️
lib/src/bones_api_sql_builder.dart 82.66% 13 Missing ⚠️
... and 24 more
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     
Flag Coverage Δ
unittests 68.12% <71.75%> (+0.35%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@gmpassos
gmpassos merged commit 74c0dfb into master Aug 11, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant