1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
|
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | A sqlite backend for persistent.
module Database.Persist.Sqlite
( withSqlitePool
, withSqliteConn
, createSqlitePool
, module Database.Persist.Sql
, SqliteConf (..)
, runSqlite
, wrapConnection
) where
import Database.Persist.Sql
import qualified Database.Sqlite as Sqlite
import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.Logger (NoLoggingT, runNoLoggingT)
import Data.IORef
import qualified Data.Map as Map
import Control.Monad.Trans.Control (control)
import qualified Control.Exception as E
import Data.Text (Text)
import Control.Monad (mzero)
import Data.Aeson
import qualified Data.Text as T
import Data.Conduit
import qualified Data.Conduit.List as CL
import Control.Applicative
import Data.Int (Int64)
import Control.Monad ((>=>))
import Data.Monoid ((<>))
import Control.Monad.Trans.Control (MonadBaseControl)
import Control.Monad.Trans.Resource (ResourceT, MonadResource, runResourceT)
createSqlitePool :: MonadIO m => Text -> Int -> m ConnectionPool
createSqlitePool s = createSqlPool $ open' s
withSqlitePool :: (MonadBaseControl IO m, MonadIO m)
=> Text
-> Int -- ^ number of connections to open
-> (ConnectionPool -> m a) -> m a
withSqlitePool s = withSqlPool $ open' s
withSqliteConn :: (MonadBaseControl IO m, MonadIO m)
=> Text -> (Connection -> m a) -> m a
withSqliteConn = withSqlConn . open'
open' :: Text -> IO Connection
open' = Sqlite.open >=> wrapConnection
-- | Wrap up a raw 'Sqlite.Connection' as a Persistent SQL 'Connection'.
--
-- Since 1.1.5
wrapConnection :: Sqlite.Connection -> IO Connection
wrapConnection conn = do
smap <- newIORef $ Map.empty
return Connection
{ connPrepare = prepare' conn
, connStmtMap = smap
, connInsertSql = insertSql'
, connClose = Sqlite.close conn
, connMigrateSql = migrate'
, connBegin = helper "BEGIN"
, connCommit = helper "COMMIT"
, connRollback = ignoreExceptions . helper "ROLLBACK"
, connEscapeName = escape
, connNoLimit = "LIMIT -1"
, connRDBMS = "sqlite"
, connLimitOffset = decorateSQLWithLimitOffset "LIMIT -1"
}
where
helper t getter = do
stmt <- getter t
_ <- stmtExecute stmt []
stmtReset stmt
ignoreExceptions = E.handle (\(_ :: E.SomeException) -> return ())
-- | A convenience helper which creates a new database connection and runs the
-- given block, handling @MonadResource@ and @MonadLogger@ requirements. Note
-- that all log messages are discarded.
--
-- Since 1.1.4
runSqlite :: (MonadBaseControl IO m, MonadIO m)
=> Text -- ^ connection string
-> SqlPersistT (NoLoggingT (ResourceT m)) a -- ^ database action
-> m a
runSqlite connstr = runResourceT
. runNoLoggingT
. withSqliteConn connstr
. runSqlConn
prepare' :: Sqlite.Connection -> Text -> IO Statement
prepare' conn sql = do
stmt <- Sqlite.prepare conn sql
return Statement
{ stmtFinalize = Sqlite.finalize stmt
, stmtReset = Sqlite.reset conn stmt
, stmtExecute = execute' conn stmt
, stmtQuery = withStmt' conn stmt
}
insertSql' :: EntityDef SqlType -> [PersistValue] -> InsertSqlResult
insertSql' ent vals =
case entityPrimary ent of
Just _ ->
ISRManyKeys sql vals
where sql = T.concat
[ "INSERT INTO "
, escape $ entityDB ent
, "("
, T.intercalate "," $ map (escape . fieldDB) $ entityFields ent
, ") VALUES("
, T.intercalate "," (map (const "?") $ entityFields ent)
, ")"
]
Nothing ->
ISRInsertGet ins sel
where
sel = "SELECT last_insert_rowid()"
ins = T.concat
[ "INSERT INTO "
, escape $ entityDB ent
, if null (entityFields ent)
then " VALUES(null)"
else T.concat
[ "("
, T.intercalate "," $ map (escape . fieldDB) $ entityFields ent
, ") VALUES("
, T.intercalate "," (map (const "?") $ entityFields ent)
, ")"
]
]
execute' :: Sqlite.Connection -> Sqlite.Statement -> [PersistValue] -> IO Int64
execute' conn stmt vals = flip finally (liftIO $ Sqlite.reset conn stmt) $ do
Sqlite.bind stmt vals
_ <- Sqlite.step stmt
Sqlite.changes conn
withStmt'
:: MonadResource m
=> Sqlite.Connection
-> Sqlite.Statement
-> [PersistValue]
-> Source m [PersistValue]
withStmt' conn stmt vals = bracketP
(Sqlite.bind stmt vals >> return stmt)
(Sqlite.reset conn)
(const pull)
where
pull = do
x <- liftIO $ Sqlite.step stmt
case x of
Sqlite.Done -> return ()
Sqlite.Row -> do
cols <- liftIO $ Sqlite.columns stmt
yield cols
pull
showSqlType :: SqlType -> Text
showSqlType SqlString = "VARCHAR"
showSqlType SqlInt32 = "INTEGER"
showSqlType SqlInt64 = "INTEGER"
showSqlType SqlReal = "REAL"
showSqlType (SqlNumeric precision scale) = T.concat [ "NUMERIC(", T.pack (show precision), ",", T.pack (show scale), ")" ]
showSqlType SqlDay = "DATE"
showSqlType SqlTime = "TIME"
showSqlType SqlDayTimeZoned = "TIMESTAMP"
showSqlType SqlDayTime = "TIMESTAMP"
showSqlType SqlBlob = "BLOB"
showSqlType SqlBool = "BOOLEAN"
showSqlType (SqlOther t) = t
migrate' :: [EntityDef a]
-> (Text -> IO Statement)
-> EntityDef SqlType
-> IO (Either [Text] [(Bool, Text)])
migrate' allDefs getter val = do
let (cols, uniqs, _) = mkColumns allDefs val
let newSql = mkCreateTable False def (filter (not . safeToRemove val . cName) cols, uniqs)
stmt <- getter "SELECT sql FROM sqlite_master WHERE type='table' AND name=?"
oldSql' <- runResourceT
$ stmtQuery stmt [PersistText $ unDBName table] $$ go
case oldSql' of
Nothing -> return $ Right [(False, newSql)]
Just oldSql -> do
if oldSql == newSql
then return $ Right []
else do
sql <- getCopyTable allDefs getter val
return $ Right sql
where
def = val
table = entityDB def
go = do
x <- CL.head
case x of
Nothing -> return Nothing
Just [PersistText y] -> return $ Just y
Just y -> error $ "Unexpected result from sqlite_master: " ++ show y
-- | Check if a column name is listed as the "safe to remove" in the entity
-- list.
safeToRemove :: EntityDef a -> DBName -> Bool
safeToRemove def (DBName colName)
= any (elem "SafeToRemove" . fieldAttrs)
$ filter ((== (DBName colName)) . fieldDB)
$ entityFields def
getCopyTable :: [EntityDef a]
-> (Text -> IO Statement)
-> EntityDef SqlType
-> IO [(Bool, Text)]
getCopyTable allDefs getter val = do
stmt <- getter $ T.concat [ "PRAGMA table_info(", escape table, ")" ]
oldCols' <- runResourceT $ stmtQuery stmt [] $$ getCols
let oldCols = map DBName $ filter (/= "id") oldCols' -- need to update for table id attribute ?
let newCols = filter (not . safeToRemove def) $ map cName cols
let common = filter (`elem` oldCols) newCols
let id_ = entityID val
return [ (False, tmpSql)
, (False, copyToTemp $ id_ : common)
, (common /= filter (not . safeToRemove def) oldCols, dropOld)
, (False, newSql)
, (False, copyToFinal $ id_ : newCols)
, (False, dropTmp)
]
where
def = val
getCols = do
x <- CL.head
case x of
Nothing -> return []
Just (_:PersistText name:_) -> do
names <- getCols
return $ name : names
Just y -> error $ "Invalid result from PRAGMA table_info: " ++ show y
table = entityDB def
tableTmp = DBName $ unDBName table <> "_backup"
(cols, uniqs, _) = mkColumns allDefs val
cols' = filter (not . safeToRemove def . cName) cols
newSql = mkCreateTable False def (cols', uniqs)
tmpSql = mkCreateTable True def { entityDB = tableTmp } (cols', uniqs)
dropTmp = "DROP TABLE " <> escape tableTmp
dropOld = "DROP TABLE " <> escape table
copyToTemp common = T.concat
[ "INSERT INTO "
, escape tableTmp
, "("
, T.intercalate "," $ map escape common
, ") SELECT "
, T.intercalate "," $ map escape common
, " FROM "
, escape table
]
copyToFinal newCols = T.concat
[ "INSERT INTO "
, escape table
, " SELECT "
, T.intercalate "," $ map escape newCols
, " FROM "
, escape tableTmp
]
mkCreateTable :: Bool -> EntityDef a -> ([Column], [UniqueDef]) -> Text
mkCreateTable isTemp entity (cols, uniqs) =
case entityPrimary entity of
Just _ ->
T.concat
[ "CREATE"
, if isTemp then " TEMP" else ""
, " TABLE "
, escape $ entityDB entity
, "("
, T.drop 1 $ T.concat $ map sqlColumn cols
, ", PRIMARY KEY "
, "("
, T.intercalate "," $ map (escape . fieldDB) $ entityFields entity
, ")"
, ")"
]
Nothing -> T.concat
[ "CREATE"
, if isTemp then " TEMP" else ""
, " TABLE "
, escape $ entityDB entity
, "("
, escape $ entityID entity
, " INTEGER PRIMARY KEY"
, T.concat $ map sqlColumn cols
, T.concat $ map sqlUnique uniqs
, ")"
]
sqlColumn :: Column -> Text
sqlColumn (Column name isNull typ def _cn _maxLen ref) = T.concat
[ ","
, escape name
, " "
, showSqlType typ
, if isNull then " NULL" else " NOT NULL"
, case def of
Nothing -> ""
Just d -> " DEFAULT " <> d
, case ref of
Nothing -> ""
Just (table, _) -> " REFERENCES " <> escape table
]
sqlUnique :: UniqueDef -> Text
sqlUnique (UniqueDef _ cname cols _) = T.concat
[ ",CONSTRAINT "
, escape cname
, " UNIQUE ("
, T.intercalate "," $ map (escape . snd) cols
, ")"
]
escape :: DBName -> Text
escape (DBName s) =
T.concat [q, T.concatMap go s, q]
where
q = T.singleton '"'
go '"' = "\"\""
go c = T.singleton c
-- | Information required to connect to a sqlite database
data SqliteConf = SqliteConf
{ sqlDatabase :: Text
, sqlPoolSize :: Int
}
instance PersistConfig SqliteConf where
type PersistConfigBackend SqliteConf = SqlPersistT
type PersistConfigPool SqliteConf = ConnectionPool
createPoolConfig (SqliteConf cs size) = createSqlitePool cs size
runPool _ = runSqlPool
loadConfig (Object o) =
SqliteConf <$> o .: "database"
<*> o .: "poolsize"
loadConfig _ = mzero
finally :: MonadBaseControl IO m
=> m a -- ^ computation to run first
-> m b -- ^ computation to run afterward (even if an exception was raised)
-> m a
finally a sequel = control $ \runInIO ->
E.finally (runInIO a)
(runInIO sequel)
{-# INLINABLE finally #-}
|