Files
simplexmq/src/Simplex/Messaging/Agent/Store/SQLite.hs
2024-12-12 17:42:58 +04:00

168 lines
6.1 KiB
Haskell

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Simplex.Messaging.Agent.Store.SQLite
( createDBStore,
connectSQLiteStore,
closeDBStore,
openSQLiteStore,
reopenSQLiteStore,
sqlString,
keyString,
storeKey,
execSQL,
)
where
import Control.Monad
import Data.ByteArray (ScrubbedBytes)
import qualified Data.ByteArray as BA
import Data.Functor (($>))
import Data.IORef
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Database.SQLite.Simple (Query (..))
import qualified Database.SQLite.Simple as SQL
import Database.SQLite.Simple.QQ (sql)
import qualified Database.SQLite3 as SQLite3
import Simplex.Messaging.Agent.Store.Migrations (migrateSchema)
import Simplex.Messaging.Agent.Store.SQLite.Common
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import Simplex.Messaging.Agent.Store.Shared (Migration (..), MigrationConfirmation (..), MigrationError (..))
import Simplex.Messaging.Util (ifM, safeDecodeUtf8)
import System.Directory (createDirectoryIfMissing, doesFileExist)
import System.FilePath (takeDirectory)
import UnliftIO.Exception (bracketOnError, onException)
import UnliftIO.MVar
import UnliftIO.STM
-- * SQLite Store implementation
createDBStore :: FilePath -> ScrubbedBytes -> Bool -> [Migration] -> MigrationConfirmation -> IO (Either MigrationError DBStore)
createDBStore dbFilePath dbKey keepKey migrations confirmMigrations = do
let dbDir = takeDirectory dbFilePath
createDirectoryIfMissing True dbDir
st <- connectSQLiteStore dbFilePath dbKey keepKey
r <- migrateSchema st migrations confirmMigrations `onException` closeDBStore st
case r of
Right () -> pure $ Right st
Left e -> closeDBStore st $> Left e
connectSQLiteStore :: FilePath -> ScrubbedBytes -> Bool -> IO DBStore
connectSQLiteStore dbFilePath key keepKey = do
dbNew <- not <$> doesFileExist dbFilePath
dbConn <- dbBusyLoop (connectDB dbFilePath key)
dbConnection <- newMVar dbConn
dbKey <- newTVarIO $! storeKey key keepKey
dbClosed <- newTVarIO False
dbSem <- newTVarIO 0
pure DBStore {dbFilePath, dbKey, dbSem, dbConnection, dbNew, dbClosed}
connectDB :: FilePath -> ScrubbedBytes -> IO DB.Connection
connectDB path key = do
db <- DB.open path
prepare db `onException` DB.close db
-- _printPragmas db path
pure db
where
prepare db = do
let exec = SQLite3.exec $ SQL.connectionHandle $ DB.conn db
unless (BA.null key) . exec $ "PRAGMA key = " <> keyString key <> ";"
exec . fromQuery $
[sql|
PRAGMA busy_timeout = 100;
PRAGMA foreign_keys = ON;
-- PRAGMA trusted_schema = OFF;
PRAGMA secure_delete = ON;
PRAGMA auto_vacuum = FULL;
|]
closeDBStore :: DBStore -> IO ()
closeDBStore st@DBStore {dbClosed} =
ifM (readTVarIO dbClosed) (putStrLn "closeDBStore: already closed") $
withConnection st $ \conn -> do
DB.close conn
atomically $ writeTVar dbClosed True
openSQLiteStore :: DBStore -> ScrubbedBytes -> Bool -> IO ()
openSQLiteStore st@DBStore {dbClosed} key keepKey =
ifM (readTVarIO dbClosed) (openSQLiteStore_ st key keepKey) (putStrLn "openSQLiteStore: already opened")
openSQLiteStore_ :: DBStore -> ScrubbedBytes -> Bool -> IO ()
openSQLiteStore_ DBStore {dbConnection, dbFilePath, dbKey, dbClosed} key keepKey =
bracketOnError
(takeMVar dbConnection)
(tryPutMVar dbConnection)
$ \DB.Connection {slow} -> do
DB.Connection {conn} <- connectDB dbFilePath key
atomically $ do
writeTVar dbClosed False
writeTVar dbKey $! storeKey key keepKey
putMVar dbConnection DB.Connection {conn, slow}
reopenSQLiteStore :: DBStore -> IO ()
reopenSQLiteStore st@DBStore {dbKey, dbClosed} =
ifM (readTVarIO dbClosed) open (putStrLn "reopenSQLiteStore: already opened")
where
open =
readTVarIO dbKey >>= \case
Just key -> openSQLiteStore_ st key True
Nothing -> fail "reopenSQLiteStore: no key"
keyString :: ScrubbedBytes -> Text
keyString = sqlString . safeDecodeUtf8 . BA.convert
sqlString :: Text -> Text
sqlString s = quote <> T.replace quote "''" s <> quote
where
quote = "'"
-- _printPragmas :: DB.Connection -> FilePath -> IO ()
-- _printPragmas db path = do
-- foreign_keys <- DB.query_ db "PRAGMA foreign_keys;" :: IO [[Int]]
-- print $ path <> " foreign_keys: " <> show foreign_keys
-- -- when run via sqlite-simple query for trusted_schema seems to return empty list
-- trusted_schema <- DB.query_ db "PRAGMA trusted_schema;" :: IO [[Int]]
-- print $ path <> " trusted_schema: " <> show trusted_schema
-- secure_delete <- DB.query_ db "PRAGMA secure_delete;" :: IO [[Int]]
-- print $ path <> " secure_delete: " <> show secure_delete
-- auto_vacuum <- DB.query_ db "PRAGMA auto_vacuum;" :: IO [[Int]]
-- print $ path <> " auto_vacuum: " <> show auto_vacuum
execSQL :: DB.Connection -> Text -> IO [Text]
execSQL db query = do
rs <- newIORef []
SQLite3.execWithCallback (SQL.connectionHandle $ DB.conn db) query (addSQLResultRow rs)
reverse <$> readIORef rs
addSQLResultRow :: IORef [Text] -> SQLite3.ColumnIndex -> [Text] -> [Maybe Text] -> IO ()
addSQLResultRow rs _count names values = modifyIORef' rs $ \case
[] -> [showValues values, T.intercalate "|" names]
rs' -> showValues values : rs'
where
showValues = T.intercalate "|" . map (fromMaybe "")