8.3 KiB
8.3 KiB
SQLite provider implementation plan
Implementation checklist for the first concrete DB provider:
- module:
lyng.io.db.sqlite - targets: JVM and Native
- role: reference implementation for the core
lyng.io.dbcontract
Scope
In scope for the first implementation:
- provider registration on module import
sqlite:URL dispatch throughopenDatabase(...)- typed
openSqlite(...)helper DatabaseandSqlTransactionimplementation- result-set implementation
- row and column metadata conversion
- SQLite savepoint-based nested transactions
- generated keys for
execute(...) - JVM and Native test coverage for the core portable contract
Out of scope for the first implementation:
- schema inspection APIs beyond result-set column metadata
- public prepared statement API
- batch API
- JS support
- heuristic temporal parsing
- heuristic decimal parsing
Proposed module layout
Core/public declarations:
.lyngdeclaration source forlyng.io.db.lyngdeclaration source forlyng.io.db.sqlite
Backend/runtime implementation:
- common transaction/result-set abstractions in
commonMainwhere possible - JVM SQLite implementation in
jvmMain - Native SQLite implementation in
nativeMain
Milestone 1: core DB module skeleton
- Move or copy the current DB declarations from
notes/db/lyngdb.lynginto the real module declaration source location. - Add the provider registry runtime for:
- normalized lowercase scheme lookup
- duplicate registration failure
- unknown-scheme failure
- Implement generic
openDatabase(connectionUrl, extraParams)dispatch. - Add basic tests for:
- successful provider registration
- duplicate scheme registration failure
- unknown scheme failure
- malformed URL failure
Milestone 2: SQLite provider skeleton
- Create
lyng.io.db.sqlitedeclaration source with:- typed
openSqlite(...) - provider-specific documentation
- typed
- Register
sqlitescheme at module initialization time. - Parse supported URL forms:
sqlite::memory:sqlite:relative/path.dbsqlite:/absolute/path.db
- Convert typed helper arguments into the same normalized open options used by
the generic
openDatabase(...)path. - Add tests for:
- import-time registration
- typed helper open
- generic URL-based open
- invalid URL handling
Milestone 3: JVM backend
Implementation strategy:
- use SQLite JDBC under the hood
- keep JDBC fully internal to the provider
- preserve the Lyng-facing transaction/result contracts rather than exposing JDBC semantics directly
Steps:
- Create JVM
Databaseimplementation that stores normalized SQLite config. - For each outer
Database.transaction {}:- open/acquire one JDBC connection
- configure connection-level options
- begin transaction
- commit on normal exit
- rollback on uncaught exception
- close/release connection
- preserve error precedence:
- original user exception stays primary on rollback failure
- rollback failure becomes primary for intentional
RollbackException - commit failure is primary on normal-exit commit failure
- For nested
SqlTransaction.transaction {}:- create savepoint
- release savepoint on success
- rollback to savepoint on failure
- preserve the same primary/secondary exception rules for savepoint rollback failures
- Implement
select(...):- bind positional
?parameters - expose result rows through
ResultSet - preserve transaction-scoped lifetime
- invalidate both the result set and any rows obtained from it when the owning transaction ends
- bind positional
- Implement
execute(...):- bind positional parameters
- collect affected row count
- expose generated keys when supported
- Implement column metadata normalization:
- output column label
- nullable flag where available
- portable
SqlType - backend native type name
- Add JVM tests for:
- transaction commit
- transaction rollback
- nested transaction savepoint behavior
- rollback failure precedence
- commit failure precedence
- row lookup by index and name
- ambiguous name failure
- result-set use-after-transaction failure
- row use-after-transaction failure
- generated keys
RETURNINGviaselect(...)if the backend supports it
Milestone 4: Native backend
Implementation strategy:
- use direct SQLite C bindings
- keep semantics aligned with the JVM backend
Steps:
- Create Native
Databaseimplementation that stores normalized SQLite config. - For each outer transaction:
- open one SQLite handle if needed
- configure pragmas/options
- begin transaction
- commit or rollback
- Implement nested transactions with SQLite savepoints.
- Implement prepared statement lifecycle internally for
select(...)andexecute(...). - Implement result-set iteration and statement finalization.
- Implement the same value-conversion policy as JVM:
- exact integer mapping
DoubleStringBuffer- schema-driven
Bool - schema-driven
Decimal - schema-driven temporal parsing
- Add Native tests matching the JVM behavioral suite as closely as possible.
Milestone 5: conversion policy
- Normalize integer reads:
- return
Int
- return
- Normalize floating-point reads to
Double. - Normalize text reads to
String. - Normalize blob reads to
Buffer. - Parse
Decimalonly for declared/nativeDECIMAL/NUMERICcolumns.- bind Lyng
Decimalas canonical text using existing Decimal formatting - read back with the existing Decimal parser
- bind Lyng
- Parse
Boolonly for declared/nativeBOOLEAN/BOOLcolumns:- prefer integer
0/1 - also accept legacy text
true,false,t,fcase-insensitively - always write
Boolas integer0/1
- prefer integer
- Parse temporal values only from an explicit normalized type-name whitelist:
DATEDATETIMETIMESTAMPTIMESTAMP WITH TIME ZONETIMESTAMPTZDATETIME WITH TIME ZONETIMETIME WITHOUT TIME ZONETIME WITH TIME ZONE
- Never heuristically parse arbitrary string or numeric values into temporal or decimal values.
- If a declared strong type (
Bool,Decimal,Date,DateTime,Instant) cannot be converted from the stored value, fail withSqlExecutionException. - Add tests for each conversion rule.
Milestone 6: result-set contract
- Ensure
ResultSet.columnsis available before iteration. - Implement name lookup using result-column labels.
- Throw
SqlUsageExceptionfor:- invalid row index
- missing column name
- ambiguous column name
- use after transaction end
- Implement cheap
isEmpty()where practical. - Implement
size()separately, allowing buffering/consumption when required. - Verify resource cleanup on:
- full iteration
- canceled iteration
- transaction rollback
Milestone 7: provider options
Support these typed helper options first:
readOnlycreateIfMissingforeignKeysbusyTimeoutMillis
Expected behavior:
foreignKeysdefaults totruebusyTimeoutMillishas a non-zero sensible defaultreadOnlyis explicitcreateIfMissingis explicit
Add tests for option handling where backend behavior is observable.
Milestone 8: documentation and examples
- Add user docs for:
- importing
lyng.io.db.sqlite - generic
openDatabase(...) - typed
openSqlite(...) - transaction usage
- nested transactions
- importing
- Add small sample snippets for:
- in-memory DB
- file-backed DB
- schema creation
- insert/select/update
- rollback on exception
Testing priorities
Highest priority behavioral tests:
- provider registration and scheme dispatch
- outer transaction commit/rollback
- nested savepoint semantics
- row metadata and name lookup behavior
- generated keys for inserts
- result-set lifetime rules
- value conversion rules
- helper vs generic-open parity
Risks
Main implementation risks:
- keeping JVM and Native value-conversion behavior identical
- correctly enforcing result-set lifetime across both backends
- SQLite temporal conversion ambiguity
- JDBC metadata differences on JVM
- native statement/finalizer lifecycle bugs
Suggested implementation order
- core registry runtime
- SQLite
.lyngprovider declarations - JVM SQLite backend
- JVM test suite
- Native SQLite backend
- shared behavioral test suite refinement
- documentation/examples