Merge pull request #2668 from unisonweb/21-11-13-unison-sqlite

Extract unison-sqlite library from unison-codebase-sqlite
This commit is contained in:
Mitchell Rosen 2021-12-04 11:30:12 -05:00 committed by GitHub
commit db67eac8c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1362 additions and 0 deletions

View File

@ -27,6 +27,9 @@ cradle:
- path: "lib/unison-prelude/src"
component: "unison-prelude:lib"
- path: "lib/unison-sqlite/src"
component: "unison-sqlite:lib"
- path: "lib/unison-util-relation/src"
component: "unison-util-relation:lib"

View File

@ -44,6 +44,7 @@ import Data.Text as X (Text)
import qualified Data.Text as Text
import Data.Text.Encoding as X (decodeUtf8, encodeUtf8)
import Data.Traversable as X (for)
import Data.Typeable as X (Typeable)
import Data.Word as X
import Debug.Trace as X
import GHC.Generics as X (Generic, Generic1)

View File

@ -0,0 +1,3 @@
# unison-sqlite
This package provides a interface to SQLite for Unison packages.

View File

@ -0,0 +1,51 @@
name: unison-sqlite
github: unisonweb/unison
copyright: Copyright (C) 2013-2021 Unison Computing, PBC and contributors
library:
source-dirs: src
other-modules:
- Unison.Sqlite.DataVersion
- Unison.Sqlite.Exception
- Unison.Sqlite.JournalMode
- Unison.Sqlite.Sql
dependencies:
- base
- direct-sqlite
- exceptions
- mtl
- recover-rtti
- sqlite-simple
- text
- transformers
- unison-prelude
- unliftio
- unliftio-core
ghc-options:
-Wall
default-extensions:
- BlockArguments
- ConstraintKinds
- DeriveAnyClass
- DeriveFunctor
- DerivingStrategies
- DerivingVia
- DoAndIfThenElse
- DuplicateRecordFields
- FlexibleContexts
- FlexibleInstances
- GADTs
- GeneralizedNewtypeDeriving
- KindSignatures
- LambdaCase
- MultiParamTypeClasses
- NamedFieldPuns
- NumericUnderscores
- OverloadedStrings
- ScopedTypeVariables
- TupleSections
- TypeApplications
- ViewPatterns

View File

@ -0,0 +1,131 @@
-- | The Unison monorepo interface to SQLite.
--
-- This module provides a high(-er) level interface to SQLite than the @sqlite-simple@ library, which it wraps. Code
-- that interacts with SQLite in this monorepo should use this interface, rather than @sqlite-simple@ or @direct-sqlite@
-- directly.
--
-- Three variants of the main query interface are provided:
--
-- * "Unison.Sqlite.Connection" provides an interface in @IO@, which takes the 'Connection' argument as an explicit
-- argument.
-- * "Unison.Sqlite.DB" provides a type class interface, which moves the 'Connection' to an implicit argument. This
-- interface is also re-exported by this module, for convenient backwards compatibility with the existing queries.
-- * "Unison.Sqlite.Transaction" provides a newer, yet-unused interface that executes queries in transactions, with
-- automatic retries on @SQLITE_BUSY@ due to concurrent writers.
module Unison.Sqlite
( -- * Connection management
Connection,
withConnection,
-- * Type class query interface
DB,
runDB,
-- * Executing queries
Sql (..),
-- ** Without results
-- *** With parameters
execute,
executeMany,
-- *** Without parameters
execute_,
-- ** With results
-- $query-naming-convention
-- *** With parameters
queryListRow,
queryListCol,
queryMaybeRow,
queryMaybeCol,
queryOneRow,
queryOneCol,
-- **** With checks
queryListRowCheck,
queryListColCheck,
queryMaybeRowCheck,
queryMaybeColCheck,
queryOneRowCheck,
queryOneColCheck,
-- *** Without parameters
queryListRow_,
queryListCol_,
queryMaybeRow_,
queryMaybeCol_,
queryOneRow_,
queryOneCol_,
-- **** With checks
queryListRowCheck_,
queryListColCheck_,
queryMaybeRowCheck_,
queryMaybeColCheck_,
queryOneRowCheck_,
queryOneColCheck_,
-- * Data version
DataVersion (..),
getDataVersion,
-- * Journal mode
JournalMode (..),
trySetJournalMode,
-- ** Low-level
withSavepoint,
withStatement,
-- * Exceptions
SqliteException (..),
SqliteExceptionReason,
SomeSqliteExceptionReason (..),
ExpectedAtMostOneRowException (..),
ExpectedExactlyOneRowException (..),
SetJournalModeException (..),
)
where
import Unison.Sqlite.Connection
( Connection,
ExpectedAtMostOneRowException (..),
ExpectedExactlyOneRowException (..),
withConnection,
)
import Unison.Sqlite.DB
import Unison.Sqlite.DataVersion (DataVersion (..), getDataVersion)
import Unison.Sqlite.Exception (SomeSqliteExceptionReason (..), SqliteException (..), SqliteExceptionReason)
import Unison.Sqlite.JournalMode (JournalMode (..), SetJournalModeException (..), trySetJournalMode)
import Unison.Sqlite.Sql (Sql (..))
-- $query-naming-convention
--
-- Queries that return results have many different variants.
--
-- Every function name begins with the string @__query__@.
--
-- 1. /Row count/. The caller may expect /exactly one/, /zero or one/, or /zero or more/ rows, in which case the
-- function name includes the string @__List__@, @__Maybe__@, or @__One__@, respectively.
-- Example: @query__List__Row@.
--
-- 2. /Row width/. The caller may expect the returned rows may contain /exactly one/ or /more than one/ column, in
-- which case the function name includes the string @__Col__@ or @__Row__@, respectively.
-- Example: @queryOne__Col__@.
--
-- 3. /Result checks/. The caller may want to perform additional validation on the returned rows, in which case the
-- function name includes the string @__Check__@.
-- Example: @queryMaybeCol__Check__@.
--
-- 4. /Parameter count/. The query may contain /zero/ or /one or more/ parameters. In the former case, the function
-- name includes the string @__\___@.
-- Example: @queryListRow__\___@.
--
-- All together, the full anatomy of a query function is:
--
-- @
-- query(List|Maybe|One)(Row|Col)[Check][_]
-- @

View File

@ -0,0 +1,433 @@
module Unison.Sqlite.Connection
( -- * Connection management
Connection (..),
withConnection,
-- * Executing queries
-- ** Without results
-- *** With parameters
execute,
executeMany,
-- *** Without parameters
execute_,
-- ** With results
-- *** With parameters
queryListRow,
queryListCol,
queryMaybeRow,
queryMaybeCol,
queryOneRow,
queryOneCol,
-- **** With checks
queryListRowCheck,
queryListColCheck,
queryMaybeRowCheck,
queryMaybeColCheck,
queryOneRowCheck,
queryOneColCheck,
-- *** Without parameters
queryListRow_,
queryListCol_,
queryMaybeRow_,
queryMaybeCol_,
queryOneRow_,
queryOneCol_,
-- **** With checks
queryListRowCheck_,
queryListColCheck_,
queryMaybeRowCheck_,
queryMaybeColCheck_,
queryOneRowCheck_,
queryOneColCheck_,
-- * Low-level operations
withSavepoint,
withStatement,
-- * Exceptions
ExpectedAtMostOneRowException (..),
ExpectedExactlyOneRowException (..),
)
where
import qualified Database.SQLite.Simple as Sqlite
import qualified Database.SQLite.Simple.FromField as Sqlite
import qualified Database.SQLite3.Direct as Sqlite (Database (..))
import Debug.RecoverRTTI (anythingToString)
import Unison.Prelude
import Unison.Sqlite.Exception
import Unison.Sqlite.Sql
import UnliftIO (MonadUnliftIO)
import UnliftIO.Exception
-- | A /non-thread safe/ connection to a SQLite database.
data Connection = Connection
{ name :: String,
file :: FilePath,
conn :: Sqlite.Connection
}
instance Show Connection where
show (Connection name file (Sqlite.Connection (Sqlite.Database conn))) =
"Connection " ++ show name ++ " " ++ show file ++ " " ++ show conn
-- | Perform an action with a connection to a SQLite database.
--
-- Note: the connection is created with @PRAGMA foreign_keys = ON@ automatically, to work around the fact that SQLite
-- does not automatically enforce foreign key integrity, because it elected to maintain backwards compatibility with
-- code that was written before the foreign key integrity feature was implemented.
withConnection ::
MonadUnliftIO m =>
-- | Connection name, for debugging.
String ->
-- | Path to SQLite database file.
FilePath ->
(Connection -> m a) ->
m a
withConnection name file =
bracket (openConnection name file) closeConnection
-- Open a connection to a SQLite database.
openConnection ::
MonadIO m =>
-- Connection name, for debugging.
String ->
-- Path to SQLite database file.
FilePath ->
m Connection
openConnection name file = do
conn0 <- liftIO (Sqlite.open file)
let conn = Connection {conn = conn0, file, name}
liftIO (execute_ conn "PRAGMA foreign_keys = ON")
pure conn
-- Close a connection opened with 'openConnection'.
closeConnection :: MonadIO m => Connection -> m ()
closeConnection (Connection _ _ conn) =
liftIO (Sqlite.close conn)
-- Without results, with parameters
execute :: Sqlite.ToRow a => Connection -> Sql -> a -> IO ()
execute conn@(Connection _ _ conn0) s params =
Sqlite.execute conn0 (coerce s) params `catch` \(exception :: Sqlite.SQLError) ->
throwSqliteException
SqliteExceptionInfo
{ connection = conn,
exception = SomeSqliteExceptionReason exception,
params = Just params,
sql = s
}
executeMany :: Sqlite.ToRow a => Connection -> Sql -> [a] -> IO ()
executeMany conn@(Connection _ _ conn0) s params =
Sqlite.executeMany conn0 (coerce s) params `catch` \(exception :: Sqlite.SQLError) ->
throwSqliteException
SqliteExceptionInfo
{ connection = conn,
exception = SomeSqliteExceptionReason exception,
params = Just params,
sql = s
}
-- Without results, without parameters
execute_ :: Connection -> Sql -> IO ()
execute_ conn@(Connection _ _ conn0) s =
Sqlite.execute_ conn0 (coerce s) `catch` \(exception :: Sqlite.SQLError) ->
throwSqliteException
SqliteExceptionInfo
{ connection = conn,
exception = SomeSqliteExceptionReason exception,
params = Nothing,
sql = s
}
-- With results, with parameters, without checks
queryListRow :: (Sqlite.FromRow b, Sqlite.ToRow a) => Connection -> Sql -> a -> IO [b]
queryListRow conn@(Connection _ _ conn0) s params =
Sqlite.query conn0 (coerce s) params `catch` \(exception :: Sqlite.SQLError) ->
throwSqliteException
SqliteExceptionInfo
{ connection = conn,
exception = SomeSqliteExceptionReason exception,
params = Just params,
sql = s
}
queryListCol :: forall a b. (Sqlite.FromField b, Sqlite.ToRow a) => Connection -> Sql -> a -> IO [b]
queryListCol conn s params =
coerce @(IO [Sqlite.Only b]) @(IO [b]) (queryListRow conn s params)
queryMaybeRow :: (Sqlite.ToRow a, Sqlite.FromRow b) => Connection -> Sql -> a -> IO (Maybe b)
queryMaybeRow conn s params =
queryListRowCheck conn s params \case
[] -> Right Nothing
[x] -> Right (Just x)
xs -> Left (ExpectedAtMostOneRowException (anythingToString xs))
queryMaybeCol :: forall a b. (Sqlite.ToRow a, Sqlite.FromField b) => Connection -> Sql -> a -> IO (Maybe b)
queryMaybeCol conn s params =
coerce @(IO (Maybe (Sqlite.Only b))) @(IO (Maybe b)) (queryMaybeRow conn s params)
queryOneRow :: (Sqlite.FromRow b, Sqlite.ToRow a) => Connection -> Sql -> a -> IO b
queryOneRow conn s params =
queryListRowCheck conn s params \case
[x] -> Right x
xs -> Left (ExpectedExactlyOneRowException (anythingToString xs))
queryOneCol :: forall a b. (Sqlite.FromField b, Sqlite.ToRow a) => Connection -> Sql -> a -> IO b
queryOneCol conn s params = do
coerce @(IO (Sqlite.Only b)) @(IO b) (queryOneRow conn s params)
-- With results, with parameters, with checks
queryListRowCheck ::
(Sqlite.FromRow b, Sqlite.ToRow a, SqliteExceptionReason e) =>
Connection ->
Sql ->
a ->
([b] -> Either e r) ->
IO r
queryListRowCheck conn s params check =
gqueryListCheck conn s params (mapLeft SomeSqliteExceptionReason . check)
gqueryListCheck ::
(Sqlite.FromRow b, Sqlite.ToRow a) =>
Connection ->
Sql ->
a ->
([b] -> Either SomeSqliteExceptionReason r) ->
IO r
gqueryListCheck conn s params check = do
xs <- queryListRow conn s params
case check xs of
Left exception ->
throwSqliteException
SqliteExceptionInfo
{ connection = conn,
exception,
params = Just params,
sql = s
}
Right result -> pure result
queryListColCheck ::
forall a b e r.
(Sqlite.FromField b, Sqlite.ToRow a, SqliteExceptionReason e) =>
Connection ->
Sql ->
a ->
([b] -> Either e r) ->
IO r
queryListColCheck conn s params check =
queryListRowCheck conn s params (coerce @([b] -> Either e r) @([Sqlite.Only b] -> Either e r) check)
queryMaybeRowCheck ::
(Sqlite.FromRow b, Sqlite.ToRow a, SqliteExceptionReason e) =>
Connection ->
Sql ->
a ->
(Maybe b -> Either e r) ->
IO r
queryMaybeRowCheck conn s params check =
gqueryListCheck conn s params \case
[] -> mapLeft SomeSqliteExceptionReason (check Nothing)
[x] -> mapLeft SomeSqliteExceptionReason (check (Just x))
xs -> Left (SomeSqliteExceptionReason (ExpectedAtMostOneRowException (anythingToString xs)))
queryMaybeColCheck ::
forall a b e r.
(Sqlite.FromField b, Sqlite.ToRow a, SqliteExceptionReason e) =>
Connection ->
Sql ->
a ->
(Maybe b -> Either e r) ->
IO r
queryMaybeColCheck conn s params check =
queryMaybeRowCheck conn s params (coerce @(Maybe b -> Either e r) @(Maybe (Sqlite.Only b) -> Either e r) check)
queryOneRowCheck ::
(Sqlite.FromRow b, Sqlite.ToRow a, SqliteExceptionReason e) =>
Connection ->
Sql ->
a ->
(b -> Either e r) ->
IO r
queryOneRowCheck conn s params check =
gqueryListCheck conn s params \case
[x] -> mapLeft SomeSqliteExceptionReason (check x)
xs -> Left (SomeSqliteExceptionReason (ExpectedExactlyOneRowException (anythingToString xs)))
queryOneColCheck ::
forall a b e r.
(Sqlite.FromField b, Sqlite.ToRow a, SqliteExceptionReason e) =>
Connection ->
Sql ->
a ->
(b -> Either e r) ->
IO r
queryOneColCheck conn s params check =
queryOneRowCheck conn s params (coerce @(b -> Either e r) @(Sqlite.Only b -> Either e r) check)
-- With results, without parameters, without checks
queryListRow_ :: Sqlite.FromRow a => Connection -> Sql -> IO [a]
queryListRow_ conn@(Connection _ _ conn0) s =
Sqlite.query_ conn0 (coerce s) `catch` \(exception :: Sqlite.SQLError) ->
throwSqliteException
SqliteExceptionInfo
{ connection = conn,
exception = SomeSqliteExceptionReason exception,
params = Nothing,
sql = s
}
queryListCol_ :: forall a. Sqlite.FromField a => Connection -> Sql -> IO [a]
queryListCol_ conn s =
coerce @(IO [Sqlite.Only a]) @(IO [a]) (queryListRow_ conn s)
queryMaybeRow_ :: Sqlite.FromRow a => Connection -> Sql -> IO (Maybe a)
queryMaybeRow_ conn s =
queryListRowCheck_ conn s \case
[] -> Right Nothing
[x] -> Right (Just x)
xs -> Left (SomeSqliteExceptionReason (ExpectedAtMostOneRowException (anythingToString xs)))
queryMaybeCol_ :: forall a. Sqlite.FromField a => Connection -> Sql -> IO (Maybe a)
queryMaybeCol_ conn s =
coerce @(IO (Maybe (Sqlite.Only a))) @(IO (Maybe a)) (queryMaybeRow_ conn s)
queryOneRow_ :: Sqlite.FromRow a => Connection -> Sql -> IO a
queryOneRow_ conn s =
queryListRowCheck_ conn s \case
[x] -> Right x
xs -> Left (SomeSqliteExceptionReason (ExpectedExactlyOneRowException (anythingToString xs)))
queryOneCol_ :: forall a. Sqlite.FromField a => Connection -> Sql -> IO a
queryOneCol_ conn s =
coerce @(IO (Sqlite.Only a)) @(IO a) (queryOneRow_ conn s)
-- With results, without parameters, with checks
queryListRowCheck_ :: (Sqlite.FromRow a, SqliteExceptionReason e) => Connection -> Sql -> ([a] -> Either e r) -> IO r
queryListRowCheck_ conn s check =
gqueryListCheck_ conn s (mapLeft SomeSqliteExceptionReason . check)
gqueryListCheck_ :: Sqlite.FromRow a => Connection -> Sql -> ([a] -> Either SomeSqliteExceptionReason r) -> IO r
gqueryListCheck_ conn s check = do
xs <- queryListRow_ conn s
case check xs of
Left exception ->
throwSqliteException
SqliteExceptionInfo
{ connection = conn,
exception,
params = Nothing,
sql = s
}
Right result -> pure result
queryListColCheck_ ::
forall a e r.
(Sqlite.FromField a, SqliteExceptionReason e) =>
Connection ->
Sql ->
([a] -> Either e r) ->
IO r
queryListColCheck_ conn s check =
queryListRowCheck_ conn s (coerce @([a] -> Either e r) @([Sqlite.Only a] -> Either e r) check)
queryMaybeRowCheck_ :: (Sqlite.FromRow a, SqliteExceptionReason e) => Connection -> Sql -> (Maybe a -> Either e r) -> IO r
queryMaybeRowCheck_ conn s check =
gqueryListCheck_ conn s \case
[] -> mapLeft SomeSqliteExceptionReason (check Nothing)
[x] -> mapLeft SomeSqliteExceptionReason (check (Just x))
xs -> Left (SomeSqliteExceptionReason (ExpectedAtMostOneRowException (anythingToString xs)))
queryMaybeColCheck_ ::
forall a e r.
(Sqlite.FromField a, SqliteExceptionReason e) =>
Connection ->
Sql ->
(Maybe a -> Either e r) ->
IO r
queryMaybeColCheck_ conn s check =
queryMaybeRowCheck_ conn s (coerce @(Maybe a -> Either e r) @(Maybe (Sqlite.Only a) -> Either e r) check)
queryOneRowCheck_ :: (Sqlite.FromRow a, SqliteExceptionReason e) => Connection -> Sql -> (a -> Either e r) -> IO r
queryOneRowCheck_ conn s check =
gqueryListCheck_ conn s \case
[x] -> mapLeft SomeSqliteExceptionReason (check x)
xs -> Left (SomeSqliteExceptionReason (ExpectedExactlyOneRowException (anythingToString xs)))
queryOneColCheck_ ::
forall a e r.
(Sqlite.FromField a, SqliteExceptionReason e) =>
Connection ->
Sql ->
(a -> Either e r) ->
IO r
queryOneColCheck_ conn s check =
queryOneRowCheck_ conn s (coerce @(a -> Either e r) @(Sqlite.Only a -> Either e r) check)
-- Low-level
-- | Perform an action within a named savepoint. The action is provided a rollback action.
withSavepoint :: Connection -> Text -> (IO () -> IO a) -> IO a
withSavepoint conn name action = do
uninterruptibleMask \restore -> do
execute_ conn (Sql ("SAVEPOINT " <> name))
result <-
restore (action rollback) `onException` do
rollback
release
release
pure result
where
rollback = execute_ conn (Sql ("ROLLBACK TO " <> name))
release = execute_ conn (Sql ("RELEASE " <> name))
withStatement :: (Sqlite.FromRow a, Sqlite.ToRow b) => Connection -> Sql -> b -> (IO (Maybe a) -> IO c) -> IO c
withStatement conn@(Connection _ _ conn0) s params callback =
thing `catch` \(exception :: Sqlite.SQLError) ->
throwSqliteException
SqliteExceptionInfo
{ connection = conn,
exception = SomeSqliteExceptionReason exception,
params = Just params,
sql = s
}
where
thing =
bracket (Sqlite.openStatement conn0 (coerce s)) Sqlite.closeStatement \statement -> do
Sqlite.bind statement params
callback (Sqlite.nextRow statement)
------------------------------------------------------------------------------------------------------------------------
-- Exceptions
-- | A query was expected to return exactly one row, but it did not. The exception carries a string representation of
-- the rows that were actually returned.
newtype ExpectedExactlyOneRowException = ExpectedExactlyOneRowException
{ rows :: String
}
deriving stock (Show)
deriving anyclass (SqliteExceptionReason)
-- | A query was expected to return exactly one row, but it did not. The exception carries a string representation of
-- the rows that were actually returned.
newtype ExpectedAtMostOneRowException = ExpectedAtMostOneRowException
{ rows :: String
}
deriving stock (Show)
deriving anyclass (SqliteExceptionReason)

View File

@ -0,0 +1,273 @@
-- | A type class interface to SQLite.
module Unison.Sqlite.DB
( -- * Type-class
DB,
runDB,
runTransaction,
-- * Executing queries
-- ** Without results
-- *** With parameters
execute,
executeMany,
-- *** Without parameters
execute_,
-- ** With results
-- *** With parameters
queryListRow,
queryListCol,
queryMaybeRow,
queryMaybeCol,
queryOneRow,
queryOneCol,
-- **** With checks
queryListRowCheck,
queryListColCheck,
queryMaybeRowCheck,
queryMaybeColCheck,
queryOneRowCheck,
queryOneColCheck,
-- *** Without parameters
queryListRow_,
queryListCol_,
queryMaybeRow_,
queryMaybeCol_,
queryOneRow_,
queryOneCol_,
-- **** With checks
queryListRowCheck_,
queryListColCheck_,
queryMaybeRowCheck_,
queryMaybeColCheck_,
queryOneRowCheck_,
queryOneColCheck_,
-- * Low-level operations
withSavepoint,
withStatement,
)
where
import Control.Monad.IO.Unlift (MonadUnliftIO, withRunInIO)
import Control.Monad.Reader (MonadReader, ReaderT, ask, runReaderT)
import qualified Database.SQLite.Simple as Sqlite
import qualified Database.SQLite.Simple.FromField as Sqlite
import Unison.Prelude
import Unison.Sqlite.Connection (Connection)
import qualified Unison.Sqlite.Connection as Connection
import Unison.Sqlite.Exception (SqliteExceptionReason)
import Unison.Sqlite.Sql (Sql (..))
import Unison.Sqlite.Transaction (Transaction)
import qualified Unison.Sqlite.Transaction as Transaction
type DB m =
(MonadIO m, MonadReader Connection m)
runDB :: MonadIO m => Connection -> ReaderT Connection m a -> m a
runDB conn action =
runReaderT action conn
runTransaction :: DB m => Transaction a -> m a
runTransaction transaction = do
conn <- ask
Transaction.runTransaction conn transaction
-- Without results, with parameters
execute :: (DB m, Sqlite.ToRow a) => Sql -> a -> m ()
execute s params = do
conn <- ask
liftIO (Connection.execute conn s params)
executeMany :: (DB m, Sqlite.ToRow a) => Sql -> [a] -> m ()
executeMany s params = do
conn <- ask
liftIO (Connection.executeMany conn s params)
-- Without results, without parameters
execute_ :: DB m => Sql -> m ()
execute_ s = do
conn <- ask
liftIO (Connection.execute_ conn s)
-- With results, with parameters, without checks
queryListRow :: (DB m, Sqlite.FromRow a, Sqlite.ToRow b) => Sql -> b -> m [a]
queryListRow s params = do
conn <- ask
liftIO (Connection.queryListRow conn s params)
queryListCol :: (DB m, Sqlite.FromField a, Sqlite.ToRow b) => Sql -> b -> m [a]
queryListCol s params = do
conn <- ask
liftIO (Connection.queryListCol conn s params)
queryMaybeRow :: (DB m, Sqlite.FromRow a, Sqlite.ToRow b) => Sql -> b -> m (Maybe a)
queryMaybeRow s params = do
conn <- ask
liftIO (Connection.queryMaybeRow conn s params)
queryMaybeCol :: (DB m, Sqlite.FromField a, Sqlite.ToRow b) => Sql -> b -> m (Maybe a)
queryMaybeCol s params = do
conn <- ask
liftIO (Connection.queryMaybeCol conn s params)
queryOneRow :: (DB m, Sqlite.FromRow b, Sqlite.ToRow a) => Sql -> a -> m b
queryOneRow s params = do
conn <- ask
liftIO (Connection.queryOneRow conn s params)
queryOneCol :: (DB m, Sqlite.FromField b, Sqlite.ToRow a) => Sql -> a -> m b
queryOneCol s params = do
conn <- ask
liftIO (Connection.queryOneCol conn s params)
-- With results, with parameters, with checks
queryListRowCheck ::
(DB m, Sqlite.FromRow b, Sqlite.ToRow a, SqliteExceptionReason e) =>
Sql ->
a ->
([b] -> Either e r) ->
m r
queryListRowCheck s params check = do
conn <- ask
liftIO (Connection.queryListRowCheck conn s params check)
queryListColCheck ::
(DB m, Sqlite.FromField b, Sqlite.ToRow a, SqliteExceptionReason e) =>
Sql ->
a ->
([b] -> Either e r) ->
m r
queryListColCheck s params check = do
conn <- ask
liftIO (Connection.queryListColCheck conn s params check)
queryMaybeRowCheck ::
(DB m, Sqlite.FromRow b, Sqlite.ToRow a, SqliteExceptionReason e) =>
Sql ->
a ->
(Maybe b -> Either e r) ->
m r
queryMaybeRowCheck s params check = do
conn <- ask
liftIO (Connection.queryMaybeRowCheck conn s params check)
queryMaybeColCheck ::
(DB m, Sqlite.FromField b, Sqlite.ToRow a, SqliteExceptionReason e) =>
Sql ->
a ->
(Maybe b -> Either e r) ->
m r
queryMaybeColCheck s params check = do
conn <- ask
liftIO (Connection.queryMaybeColCheck conn s params check)
queryOneRowCheck ::
(DB m, Sqlite.FromRow b, Sqlite.ToRow a, SqliteExceptionReason e) =>
Sql ->
a ->
(b -> Either e r) ->
m r
queryOneRowCheck s params check = do
conn <- ask
liftIO (Connection.queryOneRowCheck conn s params check)
queryOneColCheck ::
(DB m, Sqlite.FromField b, Sqlite.ToRow a, SqliteExceptionReason e) =>
Sql ->
a ->
(b -> Either e r) ->
m r
queryOneColCheck s params check = do
conn <- ask
liftIO (Connection.queryOneColCheck conn s params check)
-- With results, without parameters, without checks
queryListRow_ :: (DB m, Sqlite.FromRow a) => Sql -> m [a]
queryListRow_ s = do
conn <- ask
liftIO (Connection.queryListRow_ conn s)
queryListCol_ :: (DB m, Sqlite.FromField a) => Sql -> m [a]
queryListCol_ s = do
conn <- ask
liftIO (Connection.queryListCol_ conn s)
queryMaybeRow_ :: (DB m, Sqlite.FromRow a) => Sql -> m (Maybe a)
queryMaybeRow_ s = do
conn <- ask
liftIO (Connection.queryMaybeRow_ conn s)
queryMaybeCol_ :: (DB m, Sqlite.FromField a) => Sql -> m (Maybe a)
queryMaybeCol_ s = do
conn <- ask
liftIO (Connection.queryMaybeCol_ conn s)
queryOneRow_ :: (DB m, Sqlite.FromRow a) => Sql -> m a
queryOneRow_ s = do
conn <- ask
liftIO (Connection.queryOneRow_ conn s)
queryOneCol_ :: (DB m, Sqlite.FromField a) => Sql -> m a
queryOneCol_ s = do
conn <- ask
liftIO (Connection.queryOneCol_ conn s)
-- With results, without parameters, with checks
queryListRowCheck_ :: (DB m, Sqlite.FromRow a, SqliteExceptionReason e) => Sql -> ([a] -> Either e r) -> m r
queryListRowCheck_ s check = do
conn <- ask
liftIO (Connection.queryListRowCheck_ conn s check)
queryListColCheck_ :: (DB m, Sqlite.FromField a, SqliteExceptionReason e) => Sql -> ([a] -> Either e r) -> m r
queryListColCheck_ s check = do
conn <- ask
liftIO (Connection.queryListColCheck_ conn s check)
queryMaybeRowCheck_ :: (DB m, Sqlite.FromRow a, SqliteExceptionReason e) => Sql -> (Maybe a -> Either e r) -> m r
queryMaybeRowCheck_ s check = do
conn <- ask
liftIO (Connection.queryMaybeRowCheck_ conn s check)
queryMaybeColCheck_ :: (DB m, Sqlite.FromField a, SqliteExceptionReason e) => Sql -> (Maybe a -> Either e r) -> m r
queryMaybeColCheck_ s check = do
conn <- ask
liftIO (Connection.queryMaybeColCheck_ conn s check)
queryOneRowCheck_ :: (DB m, Sqlite.FromRow a, SqliteExceptionReason e) => Sql -> (a -> Either e r) -> m r
queryOneRowCheck_ s check = do
conn <- ask
liftIO (Connection.queryOneRowCheck_ conn s check)
queryOneColCheck_ :: (DB m, Sqlite.FromField a, SqliteExceptionReason e) => Sql -> (a -> Either e r) -> m r
queryOneColCheck_ s check = do
conn <- ask
liftIO (Connection.queryOneColCheck_ conn s check)
-- Low-level
-- | Perform an action within a named savepoint. The action is provided a rollback action.
withSavepoint :: (DB m, MonadUnliftIO m) => Text -> (m () -> m a) -> m a
withSavepoint name action = do
conn <- ask
withRunInIO \unlift ->
liftIO (Connection.withSavepoint conn name (unlift . action . liftIO))
withStatement :: (DB m, MonadUnliftIO m, Sqlite.FromRow a, Sqlite.ToRow b) => Sql -> b -> (m (Maybe a) -> m c) -> m c
withStatement s params callback = do
conn <- ask
withRunInIO \unlift ->
Connection.withStatement conn s params (unlift . callback . liftIO)

View File

@ -0,0 +1,17 @@
module Unison.Sqlite.DataVersion
( DataVersion (..),
getDataVersion,
)
where
import Unison.Prelude
import Unison.Sqlite.Transaction
newtype DataVersion
= DataVersion Int64
deriving stock (Eq)
deriving newtype (Show)
getDataVersion :: Transaction DataVersion
getDataVersion =
coerce @(Transaction Int64) (queryOneCol_ "PRAGMA data_version")

View File

@ -0,0 +1,75 @@
-- | Sqlite exception utils
module Unison.Sqlite.Exception
( SqliteException (..),
SqliteExceptionReason,
SomeSqliteExceptionReason (..),
SqliteExceptionInfo (..),
throwSqliteException,
)
where
import Control.Concurrent (ThreadId, myThreadId)
import qualified Database.SQLite.Simple as Sqlite
import Debug.RecoverRTTI (anythingToString)
import Unison.Prelude
import Unison.Sqlite.Sql
import UnliftIO.Exception
data SqliteExceptionInfo params connection = SqliteExceptionInfo
{ sql :: Sql,
params :: Maybe params,
exception :: SomeSqliteExceptionReason,
connection :: connection
}
throwSqliteException :: Show connection => SqliteExceptionInfo params connection -> IO a
throwSqliteException SqliteExceptionInfo {connection, exception, params, sql} = do
threadId <- myThreadId
throwIO
SqliteException
{ sql,
params = maybe "" anythingToString params,
exception,
connection = show connection,
threadId
}
-- | A type that is intended to be used as additional context for a sqlite-related exception.
class (Show e, Typeable e) => SqliteExceptionReason e
instance SqliteExceptionReason Sqlite.SQLError
data SomeSqliteExceptionReason
= forall e. SqliteExceptionReason e => SomeSqliteExceptionReason e
deriving anyclass (SqliteExceptionReason)
instance Show SomeSqliteExceptionReason where
show (SomeSqliteExceptionReason x) = show x
-- | A @SqliteException@ represents an exception paired with some context that resulted in the exception.
--
-- A @SqliteException@ may result from a number of different conditions:
--
-- * The underlying sqlite library threw an exception, as when establishing a connection to a non-existent database.
-- * A postcondition violation of a function like 'queryMaybe', which asserts that the resulting relation will have
-- certain number of rows,
-- * A postcondition violation of a function like 'queryListCheck', which takes a user-defined check as an argument.
--
-- A @SqliteException@ should not be inspected or used for control flow when run in a trusted environment, where the
-- database can be assumed to be uncorrupt. Rather, wherever possible, the user of this library should write code that
-- is guaranteed not to throw exceptions, by checking the necessary preconditions first. If that is not possible, it
-- should be considered a bug in this library.
--
-- When actions are run on an untrusted codebase, e.g. one downloaded from a remote server, it is sufficient to catch
-- just one exception type, @SqliteException@.
data SqliteException = SqliteException
{ sql :: Sql,
params :: String,
-- | The inner exception. It is intentionally not 'SomeException', so that calling code cannot accidentally
-- 'throwIO' domain-specific exception types, but must instead use a @*Check@ query variant.
exception :: SomeSqliteExceptionReason,
connection :: String,
threadId :: ThreadId
}
deriving stock (Show)
deriving anyclass (Exception)

View File

@ -0,0 +1,68 @@
module Unison.Sqlite.JournalMode
( JournalMode (..),
trySetJournalMode,
SetJournalModeException (..),
)
where
import qualified Data.Text as Text
import qualified Database.SQLite.Simple as Sqlite
import Unison.Prelude
import Unison.Sqlite.Exception (SqliteExceptionReason)
import Unison.Sqlite.Sql
import Unison.Sqlite.Transaction
-- | https://www.sqlite.org/pragma.html#pragma_journal_mode
data JournalMode
= JournalMode'DELETE
| JournalMode'TRUNCATE
| JournalMode'PERSIST
| JournalMode'MEMORY
| JournalMode'WAL
| JournalMode'OFF
deriving stock (Eq, Show)
journalModeFromText :: Text -> Maybe JournalMode
journalModeFromText = \case
"DELETE" -> Just JournalMode'DELETE
"TRUNCATE" -> Just JournalMode'TRUNCATE
"PERSIST" -> Just JournalMode'PERSIST
"MEMORY" -> Just JournalMode'MEMORY
"WAL" -> Just JournalMode'WAL
"OFF" -> Just JournalMode'OFF
_ -> Nothing
unsafeJournalModeFromText :: HasCallStack => Text -> JournalMode
unsafeJournalModeFromText s =
fromMaybe (error ("Unknown journal mode: " ++ Text.unpack s)) (journalModeFromText s)
journalModeToText :: JournalMode -> Text
journalModeToText = \case
JournalMode'DELETE -> "DELETE"
JournalMode'TRUNCATE -> "TRUNCATE"
JournalMode'PERSIST -> "PERSIST"
JournalMode'MEMORY -> "MEMORY"
JournalMode'WAL -> "WAL"
JournalMode'OFF -> "OFF"
trySetJournalMode :: JournalMode -> Transaction ()
trySetJournalMode mode0 = do
queryOneRowCheck_
(Sql ("PRAGMA journal_mode = " <> journalModeToText mode0))
\(Sqlite.Only mode1s) ->
let mode1 = unsafeJournalModeFromText mode1s
in if mode0 /= mode1
then
Left
SetJournalModeException
{ currentJournalMode = mode1,
couldntSetTo = mode0
}
else Right ()
data SetJournalModeException = SetJournalModeException
{ currentJournalMode :: JournalMode,
couldntSetTo :: JournalMode
}
deriving stock (Show)
deriving anyclass (SqliteExceptionReason)

View File

@ -0,0 +1,11 @@
module Unison.Sqlite.Sql
( Sql (..),
)
where
import Unison.Prelude
-- | A SQL snippet.
newtype Sql
= Sql Text
deriving newtype (IsString, Show)

View File

@ -0,0 +1,226 @@
module Unison.Sqlite.Transaction
( -- * Transaction management
Transaction,
runTransaction,
-- * Executing queries
-- ** Without results
-- *** With parameters
execute,
executeMany,
-- *** Without parameters
execute_,
-- ** With results
-- *** With parameters
queryListRow,
queryListCol,
queryMaybeRow,
queryMaybeCol,
queryOneRow,
queryOneCol,
-- **** With checks
queryListRowCheck,
queryListColCheck,
queryMaybeRowCheck,
queryMaybeColCheck,
queryOneRowCheck,
queryOneColCheck,
-- *** Without parameters
queryListRow_,
queryListCol_,
queryMaybeRow_,
queryMaybeCol_,
queryOneRow_,
queryOneCol_,
-- **** With checks
queryListRowCheck_,
queryListColCheck_,
queryMaybeRowCheck_,
queryMaybeColCheck_,
queryOneRowCheck_,
queryOneColCheck_,
)
where
import Control.Monad.Trans.Reader (ReaderT (..))
import qualified Database.SQLite.Simple as Sqlite
import qualified Database.SQLite.Simple.FromField as Sqlite
import Unison.Prelude
import Unison.Sqlite.Connection (Connection (..))
import qualified Unison.Sqlite.Connection as Connection
import Unison.Sqlite.Exception (SqliteExceptionReason)
import Unison.Sqlite.Sql
newtype Transaction a
= Transaction (Connection -> IO a)
-- Omit MonadIO instance because transactions may be retried
-- Omit MonadThrow instance so we always throw SqliteException (via *Check) with lots of context
deriving (Applicative, Functor, Monad) via (ReaderT Connection IO)
-- | Run a transaction on the given connection.
runTransaction :: MonadIO m => Connection -> Transaction a -> m a
runTransaction conn@(Connection _ _ conn0) (Transaction f) =
-- TODO some sensible retry logic
liftIO do
Sqlite.execute_ conn0 "BEGIN"
result <- f conn
Sqlite.execute_ conn0 "COMMIT"
pure result
-- Without results, with parameters
execute :: Sqlite.ToRow a => Sql -> a -> Transaction ()
execute s params = do
Transaction \conn -> Connection.execute conn s params
executeMany :: Sqlite.ToRow a => Sql -> [a] -> Transaction ()
executeMany s params =
Transaction \conn -> Connection.executeMany conn s params
-- Without results, without parameters
execute_ :: Sql -> Transaction ()
execute_ s =
Transaction \conn -> Connection.execute_ conn s
-- With results, with parameters, without checks
queryListRow :: (Sqlite.FromRow a, Sqlite.ToRow b) => Sql -> b -> Transaction [a]
queryListRow s params =
Transaction \conn -> Connection.queryListRow conn s params
queryListCol :: (Sqlite.FromField a, Sqlite.ToRow b) => Sql -> b -> Transaction [a]
queryListCol s params =
Transaction \conn -> Connection.queryListCol conn s params
queryMaybeRow :: (Sqlite.FromRow a, Sqlite.ToRow b) => Sql -> b -> Transaction (Maybe a)
queryMaybeRow s params =
Transaction \conn -> Connection.queryMaybeRow conn s params
queryMaybeCol :: (Sqlite.FromField a, Sqlite.ToRow b) => Sql -> b -> Transaction (Maybe a)
queryMaybeCol s params =
Transaction \conn -> Connection.queryMaybeCol conn s params
queryOneRow :: (Sqlite.FromRow b, Sqlite.ToRow a) => Sql -> a -> Transaction b
queryOneRow s params =
Transaction \conn -> Connection.queryOneRow conn s params
queryOneCol :: (Sqlite.FromField b, Sqlite.ToRow a) => Sql -> a -> Transaction b
queryOneCol s params =
Transaction \conn -> Connection.queryOneCol conn s params
-- With results, with parameters, with checks
queryListRowCheck ::
(Sqlite.FromRow b, Sqlite.ToRow a, SqliteExceptionReason e) =>
Sql ->
a ->
([b] -> Either e r) ->
Transaction r
queryListRowCheck s params check =
Transaction \conn -> Connection.queryListRowCheck conn s params check
queryListColCheck ::
(Sqlite.FromField b, Sqlite.ToRow a, SqliteExceptionReason e) =>
Sql ->
a ->
([b] -> Either e r) ->
Transaction r
queryListColCheck s params check =
Transaction \conn -> Connection.queryListColCheck conn s params check
queryMaybeRowCheck ::
(Sqlite.FromRow b, Sqlite.ToRow a, SqliteExceptionReason e) =>
Sql ->
a ->
(Maybe b -> Either e r) ->
Transaction r
queryMaybeRowCheck s params check =
Transaction \conn -> Connection.queryMaybeRowCheck conn s params check
queryMaybeColCheck ::
(Sqlite.FromField b, Sqlite.ToRow a, SqliteExceptionReason e) =>
Sql ->
a ->
(Maybe b -> Either e r) ->
Transaction r
queryMaybeColCheck s params check =
Transaction \conn -> Connection.queryMaybeColCheck conn s params check
queryOneRowCheck ::
(Sqlite.FromRow b, Sqlite.ToRow a, SqliteExceptionReason e) =>
Sql ->
a ->
(b -> Either e r) ->
Transaction r
queryOneRowCheck s params check =
Transaction \conn -> Connection.queryOneRowCheck conn s params check
queryOneColCheck ::
(Sqlite.FromField b, Sqlite.ToRow a, SqliteExceptionReason e) =>
Sql ->
a ->
(b -> Either e r) ->
Transaction r
queryOneColCheck s params check =
Transaction \conn -> Connection.queryOneColCheck conn s params check
-- With results, without parameters, without checks
queryListRow_ :: Sqlite.FromRow a => Sql -> Transaction [a]
queryListRow_ s =
Transaction \conn -> Connection.queryListRow_ conn s
queryListCol_ :: Sqlite.FromField a => Sql -> Transaction [a]
queryListCol_ s =
Transaction \conn -> Connection.queryListCol_ conn s
queryMaybeRow_ :: Sqlite.FromRow a => Sql -> Transaction (Maybe a)
queryMaybeRow_ s =
Transaction \conn -> Connection.queryMaybeRow_ conn s
queryMaybeCol_ :: Sqlite.FromField a => Sql -> Transaction (Maybe a)
queryMaybeCol_ s =
Transaction \conn -> Connection.queryMaybeCol_ conn s
queryOneRow_ :: Sqlite.FromRow a => Sql -> Transaction a
queryOneRow_ s =
Transaction \conn -> Connection.queryOneRow_ conn s
queryOneCol_ :: Sqlite.FromField a => Sql -> Transaction a
queryOneCol_ s =
Transaction \conn -> Connection.queryOneCol_ conn s
-- With results, without parameters, with checks
queryListRowCheck_ :: (Sqlite.FromRow a, SqliteExceptionReason e) => Sql -> ([a] -> Either e r) -> Transaction r
queryListRowCheck_ s check =
Transaction \conn -> Connection.queryListRowCheck_ conn s check
queryListColCheck_ :: (Sqlite.FromField a, SqliteExceptionReason e) => Sql -> ([a] -> Either e r) -> Transaction r
queryListColCheck_ s check =
Transaction \conn -> Connection.queryListColCheck_ conn s check
queryMaybeRowCheck_ :: (Sqlite.FromRow a, SqliteExceptionReason e) => Sql -> (Maybe a -> Either e r) -> Transaction r
queryMaybeRowCheck_ s check =
Transaction \conn -> Connection.queryMaybeRowCheck_ conn s check
queryMaybeColCheck_ :: (Sqlite.FromField a, SqliteExceptionReason e) => Sql -> (Maybe a -> Either e r) -> Transaction r
queryMaybeColCheck_ s check =
Transaction \conn -> Connection.queryMaybeColCheck_ conn s check
queryOneRowCheck_ :: (Sqlite.FromRow a, SqliteExceptionReason e) => Sql -> (a -> Either e r) -> Transaction r
queryOneRowCheck_ s check =
Transaction \conn -> Connection.queryOneRowCheck_ conn s check
queryOneColCheck_ :: (Sqlite.FromField a, SqliteExceptionReason e) => Sql -> (a -> Either e r) -> Transaction r
queryOneColCheck_ s check =
Transaction \conn -> Connection.queryOneColCheck_ conn s check

View File

@ -0,0 +1,67 @@
cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.34.4.
--
-- see: https://github.com/sol/hpack
name: unison-sqlite
version: 0.0.0
homepage: https://github.com/unisonweb/unison#readme
bug-reports: https://github.com/unisonweb/unison/issues
copyright: Copyright (C) 2013-2021 Unison Computing, PBC and contributors
build-type: Simple
source-repository head
type: git
location: https://github.com/unisonweb/unison
library
exposed-modules:
Unison.Sqlite
Unison.Sqlite.Connection
Unison.Sqlite.DB
Unison.Sqlite.Transaction
other-modules:
Unison.Sqlite.DataVersion
Unison.Sqlite.Exception
Unison.Sqlite.JournalMode
Unison.Sqlite.Sql
hs-source-dirs:
src
default-extensions:
BlockArguments
ConstraintKinds
DeriveAnyClass
DeriveFunctor
DerivingStrategies
DerivingVia
DoAndIfThenElse
DuplicateRecordFields
FlexibleContexts
FlexibleInstances
GADTs
GeneralizedNewtypeDeriving
KindSignatures
LambdaCase
MultiParamTypeClasses
NamedFieldPuns
NumericUnderscores
OverloadedStrings
ScopedTypeVariables
TupleSections
TypeApplications
ViewPatterns
ghc-options: -Wall
build-depends:
base
, direct-sqlite
, exceptions
, mtl
, recover-rtti
, sqlite-simple
, text
, transformers
, unison-prelude
, unliftio
, unliftio-core
default-language: Haskell2010

View File

@ -22,6 +22,7 @@ packages:
- codebase2/util-serialization
- codebase2/util-term
- lib/unison-prelude
- lib/unison-sqlite
- lib/unison-util-relation
#compiler-check: match-exact
@ -41,6 +42,8 @@ extra-deps:
- fuzzyfind-3.0.0
- monad-validate-1.2.0.0@sha256:9850f408431098b28806dd464b6825a88a0b56c84f380d7fe0454c1df9d6f881,3505
- NanoID-3.1.0@sha256:9118ab00e8650b5a56a10c90295d357eb77a8057a598b7e56dfedc9c6d53c77d,1524
# not in lts-18.13
- recover-rtti-0.4.0.0@sha256:2ce1e031ec0e34d736fa45f0149bbd55026f614939dc90ffd14a9c5d24093ff4,4423
ghc-options:
# All packages