feat: add raw SQL execution helpers (execute, executeWith, query_, queryWith)
Build and Test / build-and-test (push) Successful in 4m46s

This commit is contained in:
2026-05-30 07:51:31 -04:00
parent dfc231604b
commit b6622c0a01
8 changed files with 158 additions and 21 deletions
+3
View File
@@ -19,7 +19,9 @@ library
Orville.SQLite.AutoMigration
Orville.SQLite.Execution
Orville.SQLite.FieldDefinition
Orville.SQLite.Internal
Orville.SQLite.Monad
Orville.SQLite.Raw
Orville.SQLite.RawSql
Orville.SQLite.SqlMarshaller
Orville.SQLite.SqlType
@@ -42,6 +44,7 @@ test-suite spec
Test.AutoMigration
Test.EntityOperations
Test.FieldDefinition
Test.Raw
Test.SqlMarshaller
hs-source-dirs: test
build-depends:
+3
View File
@@ -13,12 +13,15 @@ module Orville.SQLite
, module Orville.SQLite.AutoMigration
-- * Execution
, module Orville.SQLite.Execution
-- * Raw
, module Orville.SQLite.Raw
) where
import Orville.SQLite.AutoMigration
import Orville.SQLite.Execution
import Orville.SQLite.FieldDefinition
import Orville.SQLite.Monad
import Orville.SQLite.Raw
import Orville.SQLite.SqlMarshaller
import Orville.SQLite.SqlType
import Orville.SQLite.TableDefinition
+2 -20
View File
@@ -15,8 +15,8 @@ import Control.Monad.Reader (ask)
import Data.List (intercalate)
import qualified Data.Text as T
import qualified Database.SQLite3 as SQLite3
import Database.SQLite3.Direct (columnCount)
import Orville.SQLite.FieldDefinition (fieldColumnName, fieldToSqlValue)
import Orville.SQLite.Internal (getRowData)
import Orville.SQLite.Monad (OrvilleM)
import Orville.SQLite.SqlMarshaller (
marshallerDecodeRow,
@@ -148,22 +148,4 @@ deleteEntity tableDef key = do
_ <- SQLite3.step 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
+28
View File
@@ -0,0 +1,28 @@
{-# LANGUAGE ScopedTypeVariables #-}
module Orville.SQLite.Internal (
getRowData,
) where
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 ..]
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
+71
View File
@@ -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 []
+2
View File
@@ -7,6 +7,7 @@ import Test.Hspec
import qualified Test.AutoMigration as AutoMigration
import qualified Test.EntityOperations as EntityOperations
import qualified Test.FieldDefinition as FieldDefinition
import qualified Test.Raw as Raw
import qualified Test.SqlMarshaller as SqlMarshaller
main :: IO ()
@@ -15,3 +16,4 @@ main = hspec $ do
describe "SqlMarshaller" SqlMarshaller.sqlMarshallerTests
describe "EntityOperations" EntityOperations.entityOperationsTests
describe "AutoMigration" AutoMigration.autoMigrationTests
describe "Raw" Raw.rawTests
+1 -1
View File
@@ -23,7 +23,7 @@ autoMigrationTests = do
pure ()
closeConnection db
it "is idempotent running twice does not error" $ do
it "is idempotent -- running twice does not error" $ do
db <- openConnection ":memory:"
withConnection db $ do
autoMigrateSchema defaultOptions [schemaTable personTable []]
+48
View File
@@ -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