10 KiB
SQLite provider for lyng.io.db
First concrete provider candidate for the DB module.
Module name:
lyng.io.db.sqlite
Responsibilities:
- register SQLite URL schemes on first import
- provide the generic
openDatabase(...)entry point for SQLite URLs - provide typed SQLite-specific helpers for ergonomic opening
- implement the core
Database/SqlTransactionAPI on both JVM and Native
Registration and URL schemes
On first import, the module should register at least:
sqlite
Possible accepted URL forms:
sqlite::memory:sqlite:./local.dbsqlite:/absolute/path/data.db
The exact accepted path grammar can be tightened during implementation, but it should stay simple and configuration-friendly.
Typed helper
The provider should also expose a typed helper, e.g.:
fun openSqlite(
path: String,
readOnly: Bool = false,
createIfMissing: Bool = true,
foreignKeys: Bool = true,
busyTimeoutMillis: Int = 5000
): Database
Possible special values:
":memory:"for in-memory DB
The helper is provider-specific sugar with explicit typed arguments. The generic
openDatabase(...) path must stay fully supported for configuration-driven
usage.
Implementation strategy
SQLite should be the first real provider because it is available on both JVM and Native and exercises almost the whole core API surface.
JVM
Preferred implementation:
- JDBC-backed SQLite provider for the JVM-specific backend implementation
This is acceptable because SQLite itself is local and the JDBC bridge is much simpler here than for network databases.
Native
Preferred implementation:
- direct SQLite C library binding
The provider should present the same Lyng-facing semantics on both backends.
Transactions
Required behavior:
Database.transaction {}starts a real SQLite transactionSqlTransaction.transaction {}uses SQLite savepoints- nested transactions must be supported
- failures in nested transactions roll back only to the nested savepoint unless the exception escapes further
- error precedence follows the core DB contract:
- user exception + successful rollback -> user exception escapes unchanged
- user exception + rollback failure -> user exception stays primary
- intentional
RollbackException+ rollback failure -> rollback failure is primary - commit failure after normal completion -> commit failure is primary
SQLite is a good fit here because savepoints are well-supported.
Connection/handle semantics:
- one outer
Database.transaction {}must use exactly one physical SQLite connection/handle for its whole lifetime - nested transactions must stay on that same connection/handle
- a transaction must never hop across connections
This is required because SQLite transaction state, savepoints, and generated row-id behavior are all connection-local.
Result sets
The provider may stream rows or buffer them, but it must preserve the core contract:
- result sets are valid only while the owning transaction is active
- rows obtained from a result set are also invalid after the owning transaction ends, even if they were already buffered
- iteration closes underlying resources when finished or canceled
isEmpty()should be cheap where possiblesize()may consume or buffer the full result
SQLite-specific type mapping notes
SQLite uses dynamic typing and affinity rules, so the provider must normalize returned values into the portable Lyng types.
Recommended mapping strategy:
- integer values ->
Int - floating-point values ->
Double - text values ->
String - blob values ->
Buffer - declared/native
BOOLEAN/BOOL->Bool - numeric values that are explicitly read/declared as decimal ->
Decimalwhen the provider can determine this reliably - date/time values should be parsed only when the declared/native column type indicates temporal intent
The provider should not heuristically parse arbitrary TEXT, INTEGER, or
REAL values into temporal or decimal types just because the stored value looks
like one.
If a column is exposed with a stronger portable SqlType such as Bool,
Decimal, Date, DateTime, or Instant, then the produced row value should
either be that Lyng value or null. Invalid stored representations should fail
with SqlExecutionException.
Boolean policy exception:
- unlike temporal values, legacy boolean encodings are cheap to recognize and have low ambiguity
- therefore SQLite
BOOL/BOOLEANcolumns may accept a small tolerant set of legacy boolean encodings on read - writes should still always use integer
0/1 - temporal and decimal conversions remain strict/schema-driven
Declared type-name normalization for SQLite v1:
- trim surrounding whitespace
- uppercase
- collapse internal whitespace runs to a single space
- strip a trailing
( ... )size/precision suffix before matching
Examples:
numeric(10,2)->NUMERICtimestamp with time zone->TIMESTAMP WITH TIME ZONE
SQLite v1 declared type-name whitelist:
| normalized declared/native type | portable SqlType |
|---|---|
BOOLEAN |
Bool |
BOOL |
Bool |
DECIMAL |
Decimal |
NUMERIC |
Decimal |
DATE |
Date |
DATETIME |
DateTime |
TIMESTAMP |
DateTime |
TIMESTAMP WITH TIME ZONE |
Instant |
TIMESTAMPTZ |
Instant |
DATETIME WITH TIME ZONE |
Instant |
TIME |
String |
TIME WITHOUT TIME ZONE |
String |
TIME WITH TIME ZONE |
String |
Anything not in this table should not be promoted to a stronger portable type just from its declared name.
SQLite temporal policy
SQLite has no strong built-in temporal storage types, so the provider should use a strict schema-driven conversion policy.
Binding:
null-> SQLNULLBool-> integer0/1Int-> SQLite integerDouble-> SQLite realDecimal-> canonical decimal text representation using the existing Lyng Decimal formatterString-> textBuffer-> blobDate-> ISO textYYYY-MM-DDDateTime-> ISO text without timezoneInstant-> ISO text in UTC with explicit timezone marker
Reading:
- storage class
NULL->null - normalized declared/native type
BOOLEANorBOOL-> parse asBoolwith this ordered rule:- integer
0/1first - then legacy text forms, case-insensitively:
true,false,t,f - other stored values are conversion errors
- integer
- normalized declared/native type
DATE-> parse asDate - normalized declared/native type
DATETIMEorTIMESTAMP-> parse asDateTime - normalized declared/native type
TIMESTAMP WITH TIME ZONE,TIMESTAMPTZ, orDATETIME WITH TIME ZONE-> parse asInstant - normalized declared/native type
TIME,TIME WITHOUT TIME ZONE, orTIME WITH TIME ZONE-> keep asStringin v1 - normalized declared/native type
DECIMALorNUMERIC-> parse asDecimal - otherwise integer storage ->
Int - otherwise real storage ->
Double - otherwise text storage ->
String - otherwise blob storage ->
Buffer - otherwise do not guess, and return the raw normalized SQLite value type
For v1, the provider should not automatically interpret numeric epoch values or Julian date encodings unless this is later added as an explicit provider option.
SQLite decimal policy
Decimal conversion should also be schema-driven:
- normalized declared/native type
DECIMALorNUMERIC-> parse asDecimal - otherwise do not guess from text or floating-point storage alone
Decimal exactness note:
- SQLite has no native decimal storage class; values are stored as
INTEGER,REAL,TEXT,BLOB, orNULL - binding Lyng
Decimalas text is only the provider's chosen encoding, not a native SQLite decimal representation - SQLite v1 should therefore store Lyng
Decimalvalues as canonical text and parse them back with the existing Lyng Decimal parser/formatter stack - schemas that care about decimal semantics should still declare
DECIMAL/NUMERICaffinity so the provider knows to exposeDecimal - exact round-tripping therefore cannot be guaranteed for generic
DECIMAL/NUMERICcolumns, because SQLite affinity rules may coerce stored values before they are read back - SQLite values already stored as
REALinDECIMAL/NUMERICcolumns may already reflect floating-point precision loss before Lyng sees them - if exact decimal preservation is required, the schema and provider policy must intentionally store decimal values in an exact representation, most simply as canonical text
This area will need careful implementation rules because SQLite itself does not have a strong native temporal type system.
Generated keys
ExecutionResult.getGeneratedKeys() for SQLite should return implementation-
supported generated values for execute(...).
Typical example:
- row id generated by an insert into a table with integer primary key
Statements that explicitly return rows should still go through select(...),
for example if the provider eventually supports SQLite RETURNING.
Options
Likely provider-specific options:
- read-only mode
- create-if-missing
- busy timeout
- foreign keys on/off
These options can be accepted both through SQLite helper functions and through
openDatabase(..., extraParams) when the URL scheme is sqlite.
Recommended defaults:
- foreign keys enabled by default
- busy timeout may be configurable, but should have a sensible default
- read-only and create-if-missing should be explicit options rather than hidden URL magic
Non-goals for v1
Not required for the first SQLite provider:
- schema metadata beyond result-column metadata
- prepared statement API in public surface
- batch execution API
- provider capability flags
- JS/browser support