Compare commits
5 Commits
42b45a1c20
...
lumen-ci-1
| Author | SHA1 | Date | |
|---|---|---|---|
| e995844bc5 | |||
| 469527f03c | |||
| b6622c0a01 | |||
| dfc231604b | |||
| b9d43a63bd |
@@ -0,0 +1,41 @@
|
|||||||
|
name: Build and Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq libsqlite3-dev
|
||||||
|
|
||||||
|
- name: Install Stack
|
||||||
|
run: |
|
||||||
|
curl -sSL https://get.haskellstack.org/ | sh
|
||||||
|
|
||||||
|
- name: Cache Stack artifacts
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.stack
|
||||||
|
.stack-work
|
||||||
|
key: stack-${{ runner.os }}-${{ hashFiles('stack.yaml', 'orville-sqlite.cabal') }}
|
||||||
|
restore-keys: |
|
||||||
|
stack-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: stack build --pedantic
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: stack test
|
||||||
@@ -1,61 +1,154 @@
|
|||||||
# orville-sqlite
|
# 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.
|
## Usage
|
||||||
|
|
||||||
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
|
```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
|
Person
|
||||||
{ firstName :: Text
|
<$> marshallReadOnlyField personIdField -- auto-increment PK
|
||||||
, lastName :: Text
|
<*> marshallField firstName firstNameField
|
||||||
, age :: Int
|
<*> 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
|
```sql
|
||||||
CREATE TABLE person (
|
CREATE TABLE IF NOT EXISTS person (
|
||||||
first_name VARCHAR(100) NOT NULL,
|
age INTEGER NOT NULL,
|
||||||
last_name VARCHAR(100) NOT NULL,
|
last_name TEXT NOT NULL,
|
||||||
age INT
|
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
|
## Feature Coverage
|
||||||
- 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
|
### Included (MVP)
|
||||||
|
|
||||||
The development environment should be entirely inside of Docker - no local
|
- `FieldDefinition` with `NotNull`/`Nullable` type parameters
|
||||||
tools should be installed on the machine directly. use the `hs` wrapper script
|
- `SqlMarshaller` GADT with `marshallField`, `marshallReadOnlyField`, `marshallMaybe`
|
||||||
to execute Haskell tooling inside of Docker.
|
- `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`
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ library
|
|||||||
Orville.SQLite.AutoMigration
|
Orville.SQLite.AutoMigration
|
||||||
Orville.SQLite.Execution
|
Orville.SQLite.Execution
|
||||||
Orville.SQLite.FieldDefinition
|
Orville.SQLite.FieldDefinition
|
||||||
|
Orville.SQLite.Internal
|
||||||
Orville.SQLite.Monad
|
Orville.SQLite.Monad
|
||||||
|
Orville.SQLite.Raw
|
||||||
Orville.SQLite.RawSql
|
Orville.SQLite.RawSql
|
||||||
Orville.SQLite.SqlMarshaller
|
Orville.SQLite.SqlMarshaller
|
||||||
Orville.SQLite.SqlType
|
Orville.SQLite.SqlType
|
||||||
@@ -32,7 +34,7 @@ library
|
|||||||
, text
|
, text
|
||||||
, bytestring
|
, bytestring
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
ghc-options: -Wall
|
ghc-options: -Wall -Werror
|
||||||
|
|
||||||
test-suite spec
|
test-suite spec
|
||||||
type: exitcode-stdio-1.0
|
type: exitcode-stdio-1.0
|
||||||
@@ -42,6 +44,7 @@ test-suite spec
|
|||||||
Test.AutoMigration
|
Test.AutoMigration
|
||||||
Test.EntityOperations
|
Test.EntityOperations
|
||||||
Test.FieldDefinition
|
Test.FieldDefinition
|
||||||
|
Test.Raw
|
||||||
Test.SqlMarshaller
|
Test.SqlMarshaller
|
||||||
hs-source-dirs: test
|
hs-source-dirs: test
|
||||||
build-depends:
|
build-depends:
|
||||||
@@ -53,3 +56,4 @@ test-suite spec
|
|||||||
, hspec
|
, hspec
|
||||||
, text
|
, text
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
|
ghc-options: -Wall -Werror
|
||||||
|
|||||||
@@ -13,12 +13,15 @@ module Orville.SQLite
|
|||||||
, module Orville.SQLite.AutoMigration
|
, module Orville.SQLite.AutoMigration
|
||||||
-- * Execution
|
-- * Execution
|
||||||
, module Orville.SQLite.Execution
|
, module Orville.SQLite.Execution
|
||||||
|
-- * Raw
|
||||||
|
, module Orville.SQLite.Raw
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Orville.SQLite.AutoMigration
|
import Orville.SQLite.AutoMigration
|
||||||
import Orville.SQLite.Execution
|
import Orville.SQLite.Execution
|
||||||
import Orville.SQLite.FieldDefinition
|
import Orville.SQLite.FieldDefinition
|
||||||
import Orville.SQLite.Monad
|
import Orville.SQLite.Monad
|
||||||
|
import Orville.SQLite.Raw
|
||||||
import Orville.SQLite.SqlMarshaller
|
import Orville.SQLite.SqlMarshaller
|
||||||
import Orville.SQLite.SqlType
|
import Orville.SQLite.SqlType
|
||||||
import Orville.SQLite.TableDefinition
|
import Orville.SQLite.TableDefinition
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import Control.Monad.Reader (ask)
|
|||||||
import Data.List (intercalate)
|
import Data.List (intercalate)
|
||||||
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 Database.SQLite3.Direct (columnCount)
|
|
||||||
import Orville.SQLite.FieldDefinition (fieldColumnName, fieldToSqlValue)
|
import Orville.SQLite.FieldDefinition (fieldColumnName, fieldToSqlValue)
|
||||||
|
import Orville.SQLite.Internal (getRowData)
|
||||||
import Orville.SQLite.Monad (OrvilleM)
|
import Orville.SQLite.Monad (OrvilleM)
|
||||||
import Orville.SQLite.SqlMarshaller (
|
import Orville.SQLite.SqlMarshaller (
|
||||||
marshallerDecodeRow,
|
marshallerDecodeRow,
|
||||||
@@ -148,22 +148,4 @@ deleteEntity tableDef key = do
|
|||||||
_ <- SQLite3.step stmt
|
_ <- SQLite3.step stmt
|
||||||
SQLite3.finalize stmt
|
SQLite3.finalize stmt
|
||||||
|
|
||||||
getRowData ::
|
|
||||||
SQLite3.Statement ->
|
|
||||||
[String] ->
|
|
||||||
IO [(String, SQLite3.SQLData)]
|
|
||||||
getRowData stmt cols = do
|
|
||||||
colCount <- columnCount stmt
|
|
||||||
let count :: Int = fromIntegral colCount
|
|
||||||
indexes = take count [0 :: SQLite3.ColumnIndex ..]
|
|
||||||
mapM
|
|
||||||
( \i -> do
|
|
||||||
let idx :: Int = fromIntegral i
|
|
||||||
colName =
|
|
||||||
if idx < length cols
|
|
||||||
then cols !! idx
|
|
||||||
else ""
|
|
||||||
sqlVal <- SQLite3.column stmt i
|
|
||||||
pure (colName, sqlVal)
|
|
||||||
)
|
|
||||||
indexes
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
|
|
||||||
|
module Orville.SQLite.Internal (
|
||||||
|
getRowData,
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Control.Monad (forM)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Database.SQLite3 as SQLite3
|
||||||
|
import Database.SQLite3.Direct (columnCount)
|
||||||
|
|
||||||
|
getRowData ::
|
||||||
|
SQLite3.Statement ->
|
||||||
|
[String] ->
|
||||||
|
IO [(String, SQLite3.SQLData)]
|
||||||
|
getRowData stmt cols = do
|
||||||
|
colCount <- columnCount stmt
|
||||||
|
let count :: Int = fromIntegral colCount
|
||||||
|
indexes = take count [0 :: SQLite3.ColumnIndex ..]
|
||||||
|
forM indexes $ \i -> do
|
||||||
|
let idx :: Int = fromIntegral i
|
||||||
|
-- Try column name from statement metadata first, then fall back to provided names, then empty
|
||||||
|
mName <- SQLite3.columnName stmt i
|
||||||
|
let colName = case mName of
|
||||||
|
Just n | not (T.null n) -> T.unpack n
|
||||||
|
_ -> if idx < length cols then cols !! idx else ""
|
||||||
|
sqlVal <- SQLite3.column stmt i
|
||||||
|
pure (colName, sqlVal)
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
module Orville.SQLite.Raw (
|
||||||
|
execute,
|
||||||
|
executeWith,
|
||||||
|
query_,
|
||||||
|
queryWith,
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Control.Monad.IO.Class (liftIO)
|
||||||
|
import Control.Monad.Reader (ask)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Database.SQLite3 as SQLite3
|
||||||
|
|
||||||
|
import Orville.SQLite.Internal (getRowData)
|
||||||
|
import Orville.SQLite.Monad (OrvilleM)
|
||||||
|
|
||||||
|
-- | Execute a raw SQL statement with no parameters.
|
||||||
|
execute :: T.Text -> OrvilleM ()
|
||||||
|
execute sql = do
|
||||||
|
db <- ask
|
||||||
|
liftIO $ do
|
||||||
|
stmt <- SQLite3.prepare db sql
|
||||||
|
_ <- SQLite3.step stmt
|
||||||
|
SQLite3.finalize stmt
|
||||||
|
|
||||||
|
-- | Execute a raw SQL statement with bound parameters.
|
||||||
|
executeWith :: T.Text -> [SQLite3.SQLData] -> OrvilleM ()
|
||||||
|
executeWith sql params = do
|
||||||
|
db <- ask
|
||||||
|
liftIO $ do
|
||||||
|
stmt <- SQLite3.prepare db sql
|
||||||
|
SQLite3.bind stmt params
|
||||||
|
_ <- SQLite3.step stmt
|
||||||
|
SQLite3.finalize stmt
|
||||||
|
|
||||||
|
-- | Execute a raw SQL query with no parameters and collect all rows.
|
||||||
|
query_ :: T.Text -> OrvilleM [[(String, SQLite3.SQLData)]]
|
||||||
|
query_ sql = do
|
||||||
|
db <- ask
|
||||||
|
liftIO $ do
|
||||||
|
stmt <- SQLite3.prepare db sql
|
||||||
|
let loop acc = do
|
||||||
|
stepResult <- SQLite3.step stmt
|
||||||
|
case stepResult of
|
||||||
|
SQLite3.Done -> do
|
||||||
|
SQLite3.finalize stmt
|
||||||
|
pure (reverse acc)
|
||||||
|
SQLite3.Row -> do
|
||||||
|
rowData <- getRowData stmt []
|
||||||
|
loop (rowData : acc)
|
||||||
|
loop []
|
||||||
|
|
||||||
|
-- | Execute a raw SQL query with bound parameters and collect all rows.
|
||||||
|
queryWith :: T.Text -> [SQLite3.SQLData] -> OrvilleM [[(String, SQLite3.SQLData)]]
|
||||||
|
queryWith sql params = do
|
||||||
|
db <- ask
|
||||||
|
liftIO $ do
|
||||||
|
stmt <- SQLite3.prepare db sql
|
||||||
|
SQLite3.bind stmt params
|
||||||
|
let loop acc = do
|
||||||
|
stepResult <- SQLite3.step stmt
|
||||||
|
case stepResult of
|
||||||
|
SQLite3.Done -> do
|
||||||
|
SQLite3.finalize stmt
|
||||||
|
pure (reverse acc)
|
||||||
|
SQLite3.Row -> do
|
||||||
|
rowData <- getRowData stmt []
|
||||||
|
loop (rowData : acc)
|
||||||
|
loop []
|
||||||
@@ -7,6 +7,7 @@ import Test.Hspec
|
|||||||
import qualified Test.AutoMigration as AutoMigration
|
import qualified Test.AutoMigration as AutoMigration
|
||||||
import qualified Test.EntityOperations as EntityOperations
|
import qualified Test.EntityOperations as EntityOperations
|
||||||
import qualified Test.FieldDefinition as FieldDefinition
|
import qualified Test.FieldDefinition as FieldDefinition
|
||||||
|
import qualified Test.Raw as Raw
|
||||||
import qualified Test.SqlMarshaller as SqlMarshaller
|
import qualified Test.SqlMarshaller as SqlMarshaller
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
@@ -15,3 +16,4 @@ main = hspec $ do
|
|||||||
describe "SqlMarshaller" SqlMarshaller.sqlMarshallerTests
|
describe "SqlMarshaller" SqlMarshaller.sqlMarshallerTests
|
||||||
describe "EntityOperations" EntityOperations.entityOperationsTests
|
describe "EntityOperations" EntityOperations.entityOperationsTests
|
||||||
describe "AutoMigration" AutoMigration.autoMigrationTests
|
describe "AutoMigration" AutoMigration.autoMigrationTests
|
||||||
|
describe "Raw" Raw.rawTests
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ module Test.AutoMigration where
|
|||||||
|
|
||||||
import Control.Exception (SomeException, try)
|
import Control.Exception (SomeException, try)
|
||||||
import Control.Monad.IO.Class (liftIO)
|
import Control.Monad.IO.Class (liftIO)
|
||||||
import Data.Int (Int64)
|
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
|
|
||||||
import Orville.SQLite
|
import Orville.SQLite
|
||||||
@@ -23,7 +22,7 @@ autoMigrationTests = do
|
|||||||
pure ()
|
pure ()
|
||||||
closeConnection db
|
closeConnection db
|
||||||
|
|
||||||
it "is idempotent — running twice does not error" $ do
|
it "is idempotent -- running twice does not error" $ do
|
||||||
db <- openConnection ":memory:"
|
db <- openConnection ":memory:"
|
||||||
withConnection db $ do
|
withConnection db $ do
|
||||||
autoMigrateSchema defaultOptions [schemaTable personTable []]
|
autoMigrateSchema defaultOptions [schemaTable personTable []]
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
module Test.EntityOperations where
|
module Test.EntityOperations where
|
||||||
|
|
||||||
import Control.Monad.IO.Class (liftIO)
|
import Control.Monad.IO.Class (liftIO)
|
||||||
import Data.Int (Int64)
|
|
||||||
import Data.Text (Text)
|
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
|
|
||||||
import Orville.SQLite
|
import Orville.SQLite
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
module Test.Raw where
|
||||||
|
|
||||||
|
import qualified Database.SQLite3 as SQLite3
|
||||||
|
import Test.Hspec
|
||||||
|
|
||||||
|
import Orville.SQLite
|
||||||
|
|
||||||
|
rawTests :: Spec
|
||||||
|
rawTests = do
|
||||||
|
describe "execute" $ do
|
||||||
|
it "runs a CREATE TABLE and INSERT statement" $ do
|
||||||
|
db <- openConnection ":memory:"
|
||||||
|
runOrvilleM db $ execute "CREATE TABLE raw_test (id INTEGER, name TEXT)"
|
||||||
|
runOrvilleM db $ execute "INSERT INTO raw_test VALUES (1, 'hello')"
|
||||||
|
rows <- runOrvilleM db $ query_ "SELECT * FROM raw_test"
|
||||||
|
length rows `shouldBe` 1
|
||||||
|
closeConnection db
|
||||||
|
|
||||||
|
describe "executeWith" $ do
|
||||||
|
it "runs parameterized INSERT" $ do
|
||||||
|
db <- openConnection ":memory:"
|
||||||
|
runOrvilleM db $ execute "CREATE TABLE raw_test2 (id INTEGER, name TEXT)"
|
||||||
|
runOrvilleM db $ executeWith "INSERT INTO raw_test2 VALUES (?, ?)" [SQLite3.SQLInteger 42, SQLite3.SQLText "world"]
|
||||||
|
rows <- runOrvilleM db $ query_ "SELECT * FROM raw_test2"
|
||||||
|
length rows `shouldBe` 1
|
||||||
|
closeConnection db
|
||||||
|
|
||||||
|
describe "query_" $ do
|
||||||
|
it "returns rows from a SELECT" $ do
|
||||||
|
db <- openConnection ":memory:"
|
||||||
|
runOrvilleM db $ execute "CREATE TABLE raw_test3 (a INTEGER)"
|
||||||
|
runOrvilleM db $ execute "INSERT INTO raw_test3 VALUES (1)"
|
||||||
|
runOrvilleM db $ execute "INSERT INTO raw_test3 VALUES (2)"
|
||||||
|
rows <- runOrvilleM db $ query_ "SELECT * FROM raw_test3 ORDER BY a"
|
||||||
|
length rows `shouldBe` 2
|
||||||
|
closeConnection db
|
||||||
|
|
||||||
|
describe "queryWith" $ do
|
||||||
|
it "returns rows matching parameters" $ do
|
||||||
|
db <- openConnection ":memory:"
|
||||||
|
runOrvilleM db $ execute "CREATE TABLE raw_test4 (name TEXT)"
|
||||||
|
runOrvilleM db $ executeWith "INSERT INTO raw_test4 VALUES (?)" [SQLite3.SQLText "alpha"]
|
||||||
|
runOrvilleM db $ executeWith "INSERT INTO raw_test4 VALUES (?)" [SQLite3.SQLText "beta"]
|
||||||
|
rows <- runOrvilleM db $ queryWith "SELECT * FROM raw_test4 WHERE name = ?" [SQLite3.SQLText "beta"]
|
||||||
|
length rows `shouldBe` 1
|
||||||
|
closeConnection db
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
module Test.SqlMarshaller where
|
module Test.SqlMarshaller where
|
||||||
|
|
||||||
import Data.Int (Int64)
|
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user