{-# LANGUAGE GADTs, CPP, OverloadedStrings #-}
module Database.Selda.SQLite
( SQLite
, withSQLite
, sqliteOpen, seldaClose
, sqliteBackend
) where
import Database.Selda
import Database.Selda.Backend
import Database.Selda.SQLite.Parser
import Data.Maybe (fromJust)
#ifndef __HASTE__
import Control.Monad (void, when, unless)
import Control.Monad.Catch
import Data.ByteString.Lazy (toStrict)
import Data.Dynamic
import Data.Text as Text (pack, toLower, take)
import Data.Time (FormatTime, formatTime, defaultTimeLocale)
import Data.UUID.Types (toByteString)
import Database.SQLite3
import System.Directory (makeAbsolute)
#endif
data SQLite
sqliteOpen :: (MonadIO m, MonadMask m) => FilePath -> m (SeldaConnection SQLite)
#ifdef __HASTE__
sqliteOpen _ = error "sqliteOpen called in JS context"
#else
sqliteOpen file = do
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
withSQLite :: (MonadIO m, MonadMask m) => FilePath -> SeldaT SQLite m a -> m a
#ifdef __HASTE__
withSQLite _ _ = return $ error "withSQLite called in JS context"
sqliteBackend :: a -> SeldaBackend
sqliteBackend _ = error "sqliteBackend called in JS context"
#else
withSQLite file m = bracket (sqliteOpen file) seldaClose (runSeldaT m)
sqliteBackend :: Database -> SeldaBackend SQLite
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 TableInfo
sqliteGetTableInfo db tbl = do
cols <- (snd . snd) <$> sqliteQueryRunner db tblinfo []
fks <- (snd . snd) <$> sqliteQueryRunner db fklist []
createQuery <- (snd . snd) <$> sqliteQueryRunner db autos []
let cs = case createQuery of
[[SqlString q]] -> colsFromQuery q
_ -> []
ixs <- mapM indexInfo . snd . snd =<< sqliteQueryRunner db indexes []
colInfos <- mapM (describe fks ixs cs) cols
return $ TableInfo
{ tableColumnInfos = colInfos
, tableUniqueGroups =
[ map mkColName names
| (names, "u") <- ixs
]
, tablePrimaryKey = concat
[ concat
[ map mkColName names
| (names, "pk") <- ixs
]
, [ colName ci
| ci <- colInfos
, colIsAutoPrimary ci
]
]
}
where
tblinfo = mconcat ["PRAGMA table_info(", tbl, ");"]
indexes = mconcat ["PRAGMA index_list(", tbl, ");"]
fklist = mconcat ["PRAGMA foreign_key_list(", tbl, ");"]
autos = mconcat ["SELECT sql FROM sqlite_master WHERE name = ", 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
info <- (snd . snd) <$> sqliteQueryRunner db q []
return $ (map (\[_,_,SqlString name] -> name) info, itype)
indexInfo _ = do
error "unreachable"
describe fks ixs cs [_, SqlString name, SqlString ty, SqlInt nonnull, _, SqlInt pk] = do
let ty' = toLower ty
return $ ColumnInfo
{ colName = mkColName name
, colType = toTypeRep (pk == 1) ty'
, colIsAutoPrimary = snd $ fromJust $ lookup name cs
, 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 t) = SQLText $ pack $ fmtTime sqlDateTimeFormat t
toSqlData (LDate d) = SQLText $ pack $ fmtTime sqlDateFormat d
toSqlData (LTime t) = SQLText $ pack $ fmtTime sqlTimeFormat t
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
toSqlData (LUUID x) = SQLBlob (toStrict $ toByteString x)
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
fmtTime :: FormatTime t => String -> t -> String
fmtTime = formatTime defaultTimeLocale
#endif