# 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` / `SqlTransaction` API 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.db`
- `sqlite:/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.:

```lyng
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 transaction
- `SqlTransaction.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 should stay usable after the owning
  transaction ends once they were materialized, e.g. with `toList()`
- iteration closes underlying resources when finished or canceled
- `isEmpty()` should be cheap where possible
- `size()` 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 -> `Decimal` when
  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` / `BOOLEAN` columns 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) ` -> `NUMERIC`
- `timestamp   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` -> SQL `NULL`
- `Bool` -> integer `0` / `1`
- `Int` -> SQLite integer
- `Double` -> SQLite real
- `Decimal` -> canonical decimal text representation using the existing Lyng
  Decimal formatter
- `String` -> text
- `Buffer` -> blob
- `Date` -> ISO text `YYYY-MM-DD`
- `DateTime` -> ISO text without timezone
- `Instant` -> ISO text in UTC with explicit timezone marker

Reading:
- storage class `NULL` -> `null`
- normalized declared/native type `BOOLEAN` or `BOOL` -> parse as `Bool` with
  this ordered rule:
  - integer `0` / `1` first
  - then legacy text forms, case-insensitively: `true`, `false`, `t`, `f`
  - other stored values are conversion errors
- normalized declared/native type `DATE` -> parse as `Date`
- normalized declared/native type `DATETIME` or `TIMESTAMP` -> parse as `DateTime`
- normalized declared/native type `TIMESTAMP WITH TIME ZONE`,
  `TIMESTAMPTZ`, or `DATETIME WITH TIME ZONE` -> parse as `Instant`
- normalized declared/native type `TIME`, `TIME WITHOUT TIME ZONE`, or
  `TIME WITH TIME ZONE` -> keep as `String` in v1
- normalized declared/native type `DECIMAL` or `NUMERIC` -> parse as `Decimal`
- 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 `DECIMAL` or `NUMERIC` -> parse as `Decimal`
- 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`, or `NULL`
- binding Lyng `Decimal` as text is only the provider's chosen encoding, not a
  native SQLite decimal representation
- SQLite v1 should therefore store Lyng `Decimal` values 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` / `NUMERIC` affinity so the provider knows to expose `Decimal`
- exact round-tripping therefore cannot be guaranteed for generic
  `DECIMAL` / `NUMERIC` columns, because SQLite affinity rules may coerce stored
  values before they are read back
- SQLite values already stored as `REAL` in `DECIMAL` / `NUMERIC` columns 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
