selda-sqlite-0.1.6.1: src/Database/Selda/SQLite.hs
{-# LANGUAGE GADTs, CPP, OverloadedStrings #-}
-- | SQLite3 backend for Selda.
module Database.Selda.SQLite (withSQLite, sqliteOpen, seldaClose) where
import Database.Selda
import Database.Selda.Backend
import Data.Dynamic
import Data.Text as Text (pack, toLower, take, isSuffixOf)
import Control.Monad (void, when, unless)
import Control.Monad.Catch
#ifndef __HASTE__
import Database.SQLite3
import System.Directory (makeAbsolute)
#endif
-- | Open a new connection to an SQLite database.
-- The connection is reusable across calls to `runSeldaT`, and must be
-- explicitly closed using 'seldaClose' when no longer needed.
sqliteOpen :: (MonadIO m, MonadMask m) => FilePath -> m SeldaConnection
sqliteOpen file = do
#ifdef __HASTE__
error "sqliteOpen called in JS context"
#else
mask $ \restore -> do
edb <- try $ liftIO $ open (pack file)
case edb of
Left e@(SQLError{}) -> do
throwM (DbError (show e))
Right db -> flip onException (liftIO (close db)) . restore $ do
absFile <- liftIO $ pack <$> makeAbsolute file
let backend = sqliteBackend db
void . liftIO $ runStmt backend "PRAGMA foreign_keys = ON;" []
newConnection backend absFile
#endif
-- | Perform the given computation over an SQLite database.
-- The database is guaranteed to be closed when the computation terminates.
withSQLite :: (MonadIO m, MonadMask m) => FilePath -> SeldaT m a -> m a
#ifdef __HASTE__
withSQLite _ _ = return $ error "withSQLite called in JS context"
#else
withSQLite file m = bracket (sqliteOpen file) seldaClose (runSeldaT m)
sqliteBackend :: Database -> SeldaBackend
sqliteBackend db = SeldaBackend
{ runStmt = \q ps -> snd <$> sqliteQueryRunner db q ps
, runStmtWithPK = \q ps -> fst <$> sqliteQueryRunner db q ps
, prepareStmt = \_ _ -> sqlitePrepare db
, runPrepared = sqliteRunPrepared db
, getTableInfo = sqliteGetTableInfo db . fromTableName
, ppConfig = defPPConfig {ppMaxInsertParams = Just 999}
, backendId = SQLite
, closeConnection = \conn -> do
stmts <- allStmts conn
flip mapM_ stmts $ \(_, stm) -> do
finalize $ fromDyn stm (error "BUG: non-statement SQLite statement")
close db
, disableForeignKeys = disableFKs db
}
sqliteGetTableInfo :: Database -> Text -> IO [ColumnInfo]
sqliteGetTableInfo db tbl = do
cols <- (snd . snd) <$> sqliteQueryRunner db tblinfo []
indexes <- (snd . snd) <$> sqliteQueryRunner db indexes []
fks <- (snd . snd) <$> sqliteQueryRunner db fklist []
indexes' <- mapM indexInfo indexes
mapM (describe fks indexes') cols
where
tblinfo = mconcat ["PRAGMA table_info(", tbl, ");"]
indexes = mconcat ["PRAGMA index_list(", tbl, ");"]
fklist = mconcat ["PRAGMA foreign_key_list(", tbl, ");"]
ixinfo name = mconcat ["PRAGMA index_info(", name, ");"]
toTypeRep _ "text" = Right TText
toTypeRep _ "double" = Right TFloat
toTypeRep _ "boolean" = Right TBool
toTypeRep _ "datetime" = Right TDateTime
toTypeRep _ "date" = Right TDate
toTypeRep _ "time" = Right TTime
toTypeRep _ "blob" = Right TBlob
toTypeRep True "integer" = Right TRowID
toTypeRep pk s | Text.take 3 s == "int" = Right $ if pk then TRowID else TInt
toTypeRep _ typ = Left typ
indexInfo [_, SqlString ixname, _, SqlString itype, _] = do
let q = (ixinfo ixname)
[[_, _, SqlString name]] <- (snd . snd) <$> sqliteQueryRunner db q []
return (name, itype)
indexInfo _ = do
error "unreachable"
describe fks ixs [_, SqlString name, SqlString ty, SqlInt nonnull, _, SqlInt pk] = do
return $ ColumnInfo
{ colName = mkColName name
, colType = toTypeRep (pk == 1) (toLower ty)
, colIsPK = pk == 1
, colIsAutoIncrement = "auto_increment" `isSuffixOf` ty
, colIsUnique = any (== (name, "u")) ixs || pk == 1
, colHasIndex = any (== (name, "c")) ixs
, colIsNullable = nonnull == 0
, colFKs =
[ (mkTableName reftbl, mkColName refkey)
| (_:_:SqlString reftbl:SqlString key:SqlString refkey:_) <- fks
, key == name
]
}
describe _ _ result = do
throwM $ SqlError $ "bad result from PRAGMA table_info: " ++ show result
disableFKs :: Database -> Bool -> IO ()
disableFKs db disable = do
unless disable $ void $ sqliteQueryRunner db "COMMIT;" []
void $ sqliteQueryRunner db q []
when disable $ void $ sqliteQueryRunner db "BEGIN TRANSACTION;" []
where
q | disable = "PRAGMA foreign_keys = OFF;"
| otherwise = "PRAGMA foreign_keys = ON;"
sqlitePrepare :: Database -> Text -> IO Dynamic
sqlitePrepare db qry = do
eres <- try $ prepare db qry
case eres of
Left e@(SQLError{}) -> throwM (SqlError (show e))
Right r -> return $ toDyn r
sqliteRunPrepared :: Database -> Dynamic -> [Param] -> IO (Int, [[SqlValue]])
sqliteRunPrepared db hdl params = do
eres <- try $ do
let Just stm = fromDynamic hdl
sqliteRunStmt db stm params `finally` do
clearBindings stm
reset stm
case eres of
Left e@(SQLError{}) -> throwM (SqlError (show e))
Right res -> return (snd res)
sqliteQueryRunner :: Database -> QueryRunner (Int, (Int, [[SqlValue]]))
sqliteQueryRunner db qry params = do
eres <- try $ do
stm <- prepare db qry
sqliteRunStmt db stm params `finally` do
finalize stm
case eres of
Left e@(SQLError{}) -> throwM (SqlError (show e))
Right res -> return res
sqliteRunStmt :: Database -> Statement -> [Param] -> IO (Int, (Int, [[SqlValue]]))
sqliteRunStmt db stm params = do
bind stm [toSqlData p | Param p <- params]
rows <- getRows stm []
rid <- lastInsertRowId db
cs <- changes db
return (fromIntegral rid, (cs, [map fromSqlData r | r <- rows]))
getRows :: Statement -> [[SQLData]] -> IO [[SQLData]]
getRows s acc = do
res <- step s
case res of
Row -> do
cs <- columns s
getRows s (cs : acc)
_ -> do
return $ reverse acc
toSqlData :: Lit a -> SQLData
toSqlData (LInt i) = SQLInteger $ fromIntegral i
toSqlData (LDouble d) = SQLFloat d
toSqlData (LText s) = SQLText s
toSqlData (LDateTime s) = SQLText s
toSqlData (LDate s) = SQLText s
toSqlData (LTime s) = SQLText s
toSqlData (LBool b) = SQLInteger $ if b then 1 else 0
toSqlData (LBlob b) = SQLBlob b
toSqlData (LNull) = SQLNull
toSqlData (LJust x) = toSqlData x
toSqlData (LCustom l) = toSqlData l
fromSqlData :: SQLData -> SqlValue
fromSqlData (SQLInteger i) = SqlInt $ fromIntegral i
fromSqlData (SQLFloat f) = SqlFloat f
fromSqlData (SQLText s) = SqlString s
fromSqlData (SQLBlob b) = SqlBlob b
fromSqlData SQLNull = SqlNull
#endif