Format code with fourmolu

This commit is contained in:
2026-05-29 23:24:26 -04:00
parent c2cd26694b
commit 9d0e9ebbe5
13 changed files with 813 additions and 619 deletions
+76
View File
@@ -0,0 +1,76 @@
# AGENTS.md
## Project Overview
`orville-sqlite` is a Haskell library providing a type-safe SQLite API modeled after the [Flipstone Orville](https://github.com/flipstone/orville/) PostgreSQL library. It maps Haskell data types to SQLite tables with compile-time schema guarantees.
## Development Environment
**All development happens inside Docker.** No local Haskell tools should be installed directly on the host machine.
Use the `./hs` wrapper script for all Haskell tooling:
```bash
./hs stack build
./hs stack test
./hs stack ghci
./hs fourmolu -i src/
./hs hlint src/
```
The script builds a Docker image based on `ghcr.io/flipstone/haskell-tools:debian-ghc-9.10.3-5d6640d` with `libsqlite3-dev` installed. A named Docker volume (`orville-sqlite-stack-root`) caches GHC and dependencies across worktrees.
## Build System
- **GHC**: 9.10.3
- **Build tool**: Stack (via the `hs` wrapper)
- **Formatter**: fourmolu
- **Linter**: hlint
## Dependencies
- **Base SQLite library**: [direct-sqlite](https://github.com/IreneKnapp/direct-sqlite) — the low-level FFI binding to SQLite
- This library builds type-safe marshalling, auto-migration, and query execution on top of `direct-sqlite`
## Target API
The library should provide APIs analogous to three key modules from `orville-postgresql`:
1. **`Orville.PostgreSQL.Marshall.SqlMarshaller`** — Type-safe mapping between Haskell records and SQL table columns
2. **`Orville.PostgreSQL.AutoMigration`** — Schema migration: create tables if they don't exist, alter tables to match expected schema
3. **`Orville.PostgreSQL.Execution`** — Query execution: SELECT, INSERT, UPDATE against typed tables
### Example Usage (Target API)
Given a Haskell type:
```haskell
data Person = Person
{ firstName :: Text
, lastName :: Text
, age :: Int
}
```
It should map to a table:
```sql
CREATE TABLE person (
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
age INT
);
```
And support:
- Creating the table if it doesn't exist
- Altering the table to match the schema when columns differ
- Querying rows as `Person` values
- Inserting `Person` values
- Updating rows from `Person` values
## Design Principles
- Haskell types to SQLite schema — not a direct port of the PostgreSQL implementation
- Follow the spirit and API shape of Orville, adapted to SQLite's type system and SQL dialect
- Favor type safety and compile-time guarantees over runtime flexibility
+61
View File
@@ -0,0 +1,61 @@
# orville-sqlite
This library is intended to be a SQLite Haskell API similar in spirit to https://github.com/flipstone/orville/
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.
The Flipstone orville library is robust and deep. This library aspires to the same but realizes it's unlikely to get there anytime soon.
## Goal
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
```haskell
data Person =
Person
{ firstName :: Text
, lastName :: Text
, age :: Int
}
```
and map it to a table of schema
```sql
CREATE TABLE person (
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
age INT
);
```
and then give us an API for marshalling that Haskell type to that table definition
then using that marshaller we should have APIs that can;
- 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.
## Development environment
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.
## Base SQLite library
Our base library for interacting with SQLite should be direct-sqlite - https://github.com/IreneKnapp/direct-sqlite
Executable
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# Thin wrapper to run Haskell tooling (stack, hpack, fourmolu, hlint, ...)
# inside a Docker image with our system dependencies pre-installed.
# Usage: ./hs <cmd> [args]
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Build a custom image on top of flipstone/haskell-tools that includes
# the C libraries our Haskell packages need (e.g. libsqlite3-dev).
IMAGE="orville-sqlite-haskell-tools"
docker build \
--quiet \
-t "${IMAGE}" \
-f - \
"${PROJECT_DIR}" << DOCKERFILE
FROM ghcr.io/flipstone/haskell-tools:debian-ghc-9.10.3-5d6640d
RUN apt-get update -qq && apt-get install -y -qq libsqlite3-dev
DOCKERFILE
# Named Docker volume shared across all worktrees so cached
# GHC/dependencies don't need rebuilding per worktree.
STACK_ROOT_VOLUME="orville-sqlite-stack-root"
docker volume inspect "${STACK_ROOT_VOLUME}" > /dev/null 2>&1 || \
docker volume create "${STACK_ROOT_VOLUME}" > /dev/null
exec docker run --rm -i $([ -t 0 ] && printf -- -t) \
-v "${PROJECT_DIR}:/work" \
-v "${STACK_ROOT_VOLUME}:/stack-root" \
-e STACK_ROOT=/stack-root \
-w /work/orville-sqlite \
"${IMAGE}" \
"$@"
+17 -16
View File
@@ -3,16 +3,16 @@
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Orville.SQLite.AutoMigration module Orville.SQLite.AutoMigration (
( MigrationOptions (..) MigrationOptions (..),
, defaultOptions defaultOptions,
, SchemaItem (..) SchemaItem (..),
, schemaTable schemaTable,
, dropColumns dropColumns,
, autoMigrateSchema autoMigrateSchema,
, MigrationStep (..) MigrationStep (..),
, generateMigrationPlan generateMigrationPlan,
, executeMigrationPlan executeMigrationPlan,
) where ) where
import Control.Monad (when) import Control.Monad (when)
@@ -23,12 +23,12 @@ import qualified Data.Text as T
import qualified Database.SQLite3 as SQLite3 import qualified Database.SQLite3 as SQLite3
import Orville.SQLite.FieldDefinition (fieldColumnName) import Orville.SQLite.FieldDefinition (fieldColumnName)
import Orville.SQLite.Monad (OrvilleM) import Orville.SQLite.Monad (OrvilleM)
import Orville.SQLite.SqlMarshaller import Orville.SQLite.SqlMarshaller (
( FieldInfo (..) FieldInfo (..),
, SqlMarshaller SqlMarshaller,
, marshallerFieldInfo marshallerFieldInfo,
) )
import Orville.SQLite.TableDefinition (TableDefinition (..), PrimaryKey (..)) import Orville.SQLite.TableDefinition (PrimaryKey (..), TableDefinition (..))
data MigrationOptions = MigrationOptions data MigrationOptions = MigrationOptions
{ runSchemaChanges :: Bool { runSchemaChanges :: Bool
@@ -47,7 +47,8 @@ data SchemaItemRep where
, schemaItemMarshaller :: SqlMarshaller w r , schemaItemMarshaller :: SqlMarshaller w r
, schemaItemPkName :: String , schemaItemPkName :: String
, schemaItemDropColumns :: [String] , schemaItemDropColumns :: [String]
} -> SchemaItemRep } ->
SchemaItemRep
schemaTable :: schemaTable ::
TableDefinition key writeEntity readEntity -> TableDefinition key writeEntity readEntity ->
+11 -11
View File
@@ -2,12 +2,12 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
module Orville.SQLite.Execution module Orville.SQLite.Execution (
( insertEntity insertEntity,
, findEntity findEntity,
, findAll findAll,
, updateEntity updateEntity,
, deleteEntity deleteEntity,
) where ) where
import Control.Monad.IO.Class (liftIO) import Control.Monad.IO.Class (liftIO)
@@ -18,12 +18,12 @@ import qualified Database.SQLite3 as SQLite3
import Database.SQLite3.Direct (columnCount) import Database.SQLite3.Direct (columnCount)
import Orville.SQLite.FieldDefinition (fieldColumnName, fieldToSqlValue) import Orville.SQLite.FieldDefinition (fieldColumnName, fieldToSqlValue)
import Orville.SQLite.Monad (OrvilleM) import Orville.SQLite.Monad (OrvilleM)
import Orville.SQLite.SqlMarshaller import Orville.SQLite.SqlMarshaller (
( marshallerDerivedColumns marshallerDecodeRow,
, marshallerEncodeWrite marshallerDerivedColumns,
, marshallerDecodeRow marshallerEncodeWrite,
) )
import Orville.SQLite.TableDefinition (TableDefinition (..), PrimaryKey (..)) import Orville.SQLite.TableDefinition (PrimaryKey (..), TableDefinition (..))
insertEntity :: insertEntity ::
TableDefinition key writeEntity readEntity -> TableDefinition key writeEntity readEntity ->
+29 -27
View File
@@ -3,37 +3,37 @@
{-# LANGUAGE KindSignatures #-} {-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
module Orville.SQLite.FieldDefinition module Orville.SQLite.FieldDefinition (
( Nullability (..) Nullability (..),
, FieldDefinition (..) FieldDefinition (..),
, integerField integerField,
, textField textField,
, realField realField,
, blobField blobField,
, nullableField nullableField,
, convertField convertField,
, fieldToSqlValue fieldToSqlValue,
, fieldFromSqlValue fieldFromSqlValue,
, fieldColumnName fieldColumnName,
, fieldSqlTypeName fieldSqlTypeName,
, fieldIsNullable fieldIsNullable,
) where ) where
import Data.Kind (Type)
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import Data.Int (Int64) import Data.Int (Int64)
import Data.Kind (Type)
import qualified Data.Text as T import qualified Data.Text as T
import qualified Database.SQLite3 as SQLite3 import qualified Database.SQLite3 as SQLite3
import Orville.SQLite.SqlType import Orville.SQLite.SqlType (
( SqlType SqlType,
, convertSqlType blobType,
, integerType convertSqlType,
, realType integerType,
, blobType realType,
, sqlTypeFromSql sqlTypeFromSql,
, sqlTypeName sqlTypeName,
, sqlTypeToSql sqlTypeToSql,
, textType textType,
) )
data Nullability = NotNull | Nullable data Nullability = NotNull | Nullable
@@ -42,11 +42,13 @@ data FieldDefinition (nullability :: Nullability) :: Type -> Type where
NotNullField :: NotNullField ::
{ notNullFieldName :: String { notNullFieldName :: String
, notNullFieldSqlType :: SqlType a , notNullFieldSqlType :: SqlType a
} -> FieldDefinition 'NotNull a } ->
FieldDefinition 'NotNull a
NullableField :: NullableField ::
{ nullableFieldName :: String { nullableFieldName :: String
, nullableFieldSqlType :: SqlType a , nullableFieldSqlType :: SqlType a
} -> FieldDefinition 'Nullable a } ->
FieldDefinition 'Nullable a
fieldColumnName :: FieldDefinition null a -> String fieldColumnName :: FieldDefinition null a -> String
fieldColumnName = \case fieldColumnName = \case
+8 -8
View File
@@ -1,17 +1,17 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Orville.SQLite.Monad module Orville.SQLite.Monad (
( OrvilleM OrvilleM,
, withConnection withConnection,
, openConnection openConnection,
, closeConnection closeConnection,
, runOrvilleM runOrvilleM,
, withTransaction withTransaction,
) where ) where
import Control.Monad.IO.Class (MonadIO (liftIO)) import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.Reader (ReaderT, runReaderT, ask, MonadReader) import Control.Monad.Reader (MonadReader, ReaderT, ask, runReaderT)
import qualified Data.Text as T import qualified Data.Text as T
import qualified Database.SQLite3 as SQLite3 import qualified Database.SQLite3 as SQLite3
+11 -11
View File
@@ -1,16 +1,16 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Orville.SQLite.RawSql module Orville.SQLite.RawSql (
( RawSql (..) RawSql (..),
, fromString fromString,
, toRawSql toRawSql,
, intercalate intercalate,
, fromText fromText,
, space space,
, comma comma,
, leftParen leftParen,
, rightParen rightParen,
, equals equals,
) where ) where
import qualified Data.Text as T import qualified Data.Text as T
+18 -18
View File
@@ -3,27 +3,27 @@
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
module Orville.SQLite.SqlMarshaller module Orville.SQLite.SqlMarshaller (
( SqlMarshaller SqlMarshaller,
, FieldInfo (..) FieldInfo (..),
, marshallField marshallField,
, marshallReadOnlyField marshallReadOnlyField,
, marshallMaybe marshallMaybe,
, marshallerFieldInfo marshallerFieldInfo,
, marshallerDerivedColumns marshallerDerivedColumns,
, marshallerEncodeWrite marshallerEncodeWrite,
, marshallerDecodeRow marshallerDecodeRow,
) where ) where
import qualified Database.SQLite3 as SQLite3 import qualified Database.SQLite3 as SQLite3
import Orville.SQLite.FieldDefinition import Orville.SQLite.FieldDefinition (
( FieldDefinition (..) FieldDefinition (..),
, Nullability (..) Nullability (..),
, fieldColumnName fieldColumnName,
, fieldToSqlValue fieldFromSqlValue,
, fieldFromSqlValue fieldIsNullable,
, fieldIsNullable fieldSqlTypeName,
, fieldSqlTypeName fieldToSqlValue,
) )
data FieldInfo = FieldInfo data FieldInfo = FieldInfo
+7 -7
View File
@@ -1,12 +1,12 @@
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
module Orville.SQLite.SqlType module Orville.SQLite.SqlType (
( SqlType (..) SqlType (..),
, integerType integerType,
, textType textType,
, realType realType,
, blobType blobType,
, convertSqlType convertSqlType,
) where ) where
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
+11 -11
View File
@@ -2,19 +2,19 @@
{-# LANGUAGE GADTs #-} {-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
module Orville.SQLite.TableDefinition module Orville.SQLite.TableDefinition (
( PrimaryKey (..) PrimaryKey (..),
, primaryKey primaryKey,
, TableDefinition (..) TableDefinition (..),
, mkTableDefinition mkTableDefinition,
, mkTableDefinitionWithoutKey mkTableDefinitionWithoutKey,
) where ) where
import Orville.SQLite.FieldDefinition import Orville.SQLite.FieldDefinition (
( FieldDefinition FieldDefinition,
, Nullability (..) Nullability (..),
, integerField convertField,
, convertField integerField,
) )
import Orville.SQLite.SqlMarshaller (SqlMarshaller) import Orville.SQLite.SqlMarshaller (SqlMarshaller)
+19
View File
@@ -0,0 +1,19 @@
# This file was autogenerated by Stack.
# You should not edit this file by hand.
# For more information, please see the documentation at:
# https://docs.haskellstack.org/en/stable/topics/lock_files
packages:
- completed:
hackage: direct-sqlite-2.3.29@sha256:6ff3969a6eae383c8a9ab093abfee7f7b0ed76dab045c984a1497b7e1d71279d,4180
pantry-tree:
sha256: 794455a2e32dc749ff32d8907bc51d810f79b963d5e21c42bc8555bc34bbd625
size: 770
original:
hackage: direct-sqlite-2.3.29
snapshots:
- completed:
sha256: 3c412a7c13dba6d3d808455a458e0776c58b6cf99b8a7961a2f5e55589d6f1d6
size: 729011
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/24/43.yaml
original: lts-24.43
+1 -1
View File
@@ -6,8 +6,8 @@ module Main where
import Control.Monad.IO.Class (liftIO) import Control.Monad.IO.Class (liftIO)
import Data.Int (Int64) import Data.Int (Int64)
import Data.Text (Text) import Data.Text (Text)
import Test.Hspec
import Orville.SQLite import Orville.SQLite
import Test.Hspec
data Person = Person data Person = Person
{ personId :: Int64 { personId :: Int64