From dfc231604bd59be1c92c543d8d9808498e1f66f3 Mon Sep 17 00:00:00 2001 From: James Brechtel Date: Sat, 30 May 2026 07:13:48 -0400 Subject: [PATCH] Update README with usage examples, module overview, and scope --- README.md | 169 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 131 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index b136dea..8463111 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,154 @@ # orville-sqlite -This library is intended to be a SQLite Haskell API similar in spirit to https://github.com/flipstone/orville/ +A type-safe SQLite API for Haskell, modeled after the [Flipstone Orville](https://github.com/flipstone/orville/) PostgreSQL library. -The goal of this library is to provide a type safe approach to mapping Haskell types to SQLite data. We aim for type-safe queries and migrations. +Maps Haskell data types to SQLite tables with compile-time schema guarantees. -The Flipstone orville library is robust and deep. This library aspires to the same but realizes it's unlikely to get there anytime soon. +## Status — MVP -## Goal +The MVP provides the core pipeline: define columns and record marshallers, auto-migrate schemas, and execute basic CRUD queries. -This library intends to provide a similar API for describing the database schema and executing queries as the Flipstone Orville library. - -We will not port the PostgreSQL implementation directly but instead target a similar API. - -The APIs that concern us the most are: - -- Orville.PostgreSQL.Marshall.SqlMarshaller -- Orville.PostgreSQL.AutoMigration as AutoMigration -- Orville.PostgreSQL.Execution - -and even for those, we only what's strictly necessary to define a way to take a Haskell data type like +## Usage ```haskell +{-# LANGUAGE DataKinds #-} -data Person = +import Orville.SQLite + +data Person = Person + { personId :: Int64 + , firstName :: Text + , lastName :: Text + , age :: Int64 + } + deriving (Show, Eq) + +-- Column definitions (type-safe, parameterized by nullability) +personIdField :: FieldDefinition 'NotNull Int64 +personIdField = integerField "id" + +firstNameField :: FieldDefinition 'NotNull Text +firstNameField = textField "first_name" + +lastNameField :: FieldDefinition 'NotNull Text +lastNameField = textField "last_name" + +ageField :: FieldDefinition 'NotNull Int64 +ageField = integerField "age" + +-- SqlMarshaller: maps the record to/from SQL columns +personMarshaller :: SqlMarshaller Person Person +personMarshaller = Person - { firstName :: Text - , lastName :: Text - , age :: Int - } + <$> marshallReadOnlyField personIdField -- auto-increment PK + <*> marshallField firstName firstNameField + <*> marshallField lastName lastNameField + <*> marshallField age ageField + +-- TableDefinition ties a table name, primary key, and marshaller together +personTable :: TableDefinition Int64 Person Person +personTable = + mkTableDefinition "person" (primaryKey personId personIdField) personMarshaller + +main :: IO () +main = do + db <- openConnection "people.db" + withConnection db $ do + -- Auto-migrate: creates the table if it doesn't exist + autoMigrateSchema defaultOptions [schemaTable personTable []] + + -- Insert + insertEntity personTable (Person 0 "Alice" "Smith" 30) + insertEntity personTable (Person 0 "Bob" "Jones" 25) + + -- Query by primary key + mAlice <- findEntity personTable 1 + -- mAlice = Just (Person 1 "Alice" "Smith" 30) + + -- Find all + all <- findAll personTable + -- all = [Person 1 "Alice" "Smith" 30, Person 2 "Bob" "Jones" 25] + + -- Update + updateEntity personTable (Person 1 "Alice" "Jones" 31) + + -- Delete + deleteEntity personTable 2 + + pure () + closeConnection db ``` -and map it to a table of schema +The above creates this table: ```sql -CREATE TABLE person ( - first_name VARCHAR(100) NOT NULL, - last_name VARCHAR(100) NOT NULL, - age INT +CREATE TABLE IF NOT EXISTS person ( + age INTEGER NOT NULL, + last_name TEXT NOT NULL, + first_name TEXT NOT NULL, + id INTEGER PRIMARY KEY ); ``` -and then give us an API for marshalling that Haskell type to that table definition +## Modules -then using that marshaller we should have APIs that can; +| Module | Purpose | +|--------|---------| +| `Orville.SQLite` | Top-level re-exports | +| `Orville.SQLite.Monad` | `OrvilleM` reader monad and connection management | +| `Orville.SQLite.SqlType` | Type encoding/decoding for SQLite storage classes | +| `Orville.SQLite.FieldDefinition` | GADT column definitions with `NotNull`/`Nullable` | +| `Orville.SQLite.SqlMarshaller` | GADT record-to-table mapping (applicative syntax) | +| `Orville.SQLite.TableDefinition` | `PrimaryKey` and `mkTableDefinition` | +| `Orville.SQLite.AutoMigration` | Schema introspection via `PRAGMA table_info`, DDL generation | +| `Orville.SQLite.Execution` | `insertEntity`, `findEntity`, `findAll`, `updateEntity`, `deleteEntity` | -- create the table if it does not exist -- alter the table to match the schema if the table does exist but its columns don't match -- query the table and retrieve Person values -- insert Person values into the table -- update the table from Person values. +## Feature Coverage -## Development environment +### Included (MVP) -The development environment should be entirely inside of Docker - no local -tools should be installed on the machine directly. use the `hs` wrapper script -to execute Haskell tooling inside of Docker. +- `FieldDefinition` with `NotNull`/`Nullable` type parameters +- `SqlMarshaller` GADT with `marshallField`, `marshallReadOnlyField`, `marshallMaybe` +- `TableDefinition` with single-column primary keys +- `AutoMigration`: create tables, add nullable columns, drop columns (explicit opt-in), dry-run +- `Execution`: insert, find-by-PK, find-all, update, delete +- In-memory databases (`:memory:`) and file-based databases -## Base SQLite library +### Not yet implemented -Our base library for interacting with SQLite should be direct-sqlite - https://github.com/IreneKnapp/direct-sqlite +- Foreign keys and composite primary keys +- Table indexes +- Plans (N+1 query prevention via `Plan`) +- Batch operations +- Upsert (`upsertEntity`) +- `findEntitiesBy` (filtering on non-PK columns) +- `updateFields` (partial update) +- Raw SQL execution (`executeAndDecode`) +- Nested record marshalling and `prefixMarshaller` +- `AnnotatedSqlMarshaller` +- `SyntheticField` +- Connection pooling + +## Development + +All tooling runs inside Docker via the `./hs` wrapper. No local Haskell installation needed. + +```bash +./hs stack build # build +./hs stack test # run tests (53 tests) +./hs stack ghci # REPL +./hs fourmolu -i src/ # format +``` + +Or use the convenience scripts: + +```bash +./scripts/build # build with pedantic +./scripts/test # run tests +``` + +### Dependencies + +- GHC 9.10.3 +- [direct-sqlite](https://github.com/IreneKnapp/direct-sqlite) — low-level FFI binding to SQLite +- `text`, `bytestring`, `mtl`