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.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
@@ -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:
+3
View File
@@ -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
+2 -20
View File
@@ -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
+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.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
+1 -1
View File
@@ -23,7 +23,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 []]
+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