{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
{-# LANGUAGE TupleSections #-}
module Database.Beam.Sqlite.Migrate
(
migrationBackend
, migrateScript, writeMigrationScript
, sqlitePredConverter, sqliteTypeToHs
, getDbConstraints
, sqliteText, sqliteBlob, sqliteBigInt
) where
import qualified Database.Beam.Migrate as Db
import qualified Database.Beam.Migrate.Backend as Tool
import qualified Database.Beam.Migrate.Serialization as Db
import Database.Beam.Backend.SQL
import Database.Beam.Haskell.Syntax
import Database.Beam.Sqlite.Connection
import Database.Beam.Sqlite.Syntax
import Control.Applicative
import Control.Exception
import Control.Monad.Reader
import Database.SQLite.Simple
( Connection, open, close, query_ )
import Data.Aeson
import Data.Attoparsec.Text (asciiCI, skipSpace)
import qualified Data.Attoparsec.Text as A
import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy.Char8 as BL
import Data.Char (isSpace)
import Data.Int (Int64)
import Data.List (sortBy)
import Data.Maybe (mapMaybe, isJust)
import Data.Monoid (Endo(..), (<>))
import Data.Ord (comparing)
import Data.String (fromString)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
migrationBackend :: Tool.BeamMigrationBackend SqliteCommandSyntax Sqlite Connection SqliteM
migrationBackend = Tool.BeamMigrationBackend
"sqlite"
"For beam-sqlite, this is the path to a sqlite3 file"
(BL.concat . migrateScript)
getDbConstraints
(Db.sql92Deserializers <> sqliteDataTypeDeserializers <>
Db.beamCheckDeserializers)
(BL.unpack . (<> ";") . sqliteRenderSyntaxScript . fromSqliteCommand)
"sqlite.sql"
sqlitePredConverter Db.defaultActionProvider
(\fp action ->
bracket (open fp) close $ \conn ->
catch (Right <$> runReaderT (runSqliteM action)
(\_ -> pure (), conn))
(\e -> pure (Left (show (e :: SomeException)))))
sqliteDataTypeDeserializers :: Db.BeamDeserializers SqliteCommandSyntax
sqliteDataTypeDeserializers =
Db.beamDeserializer $ \_ v ->
fmap (id @SqliteDataTypeSyntax) $
case v of
"blob" -> pure sqliteBlobType
"clob" -> pure sqliteTextType
"bigint" -> pure sqliteBigIntType
Object o ->
(fmap (\(_ :: Maybe Word) -> sqliteBlobType) (o .: "binary")) <|>
(fmap (\(_ :: Maybe Word) -> sqliteBlobType) (o .: "varbinary"))
_ -> fail "Could not parse sqlite-specific data type"
migrateScript :: Db.MigrationSteps SqliteCommandSyntax () a -> [BL.ByteString]
migrateScript steps =
"-- Generated by beam-sqlite beam-migrate backend\n" :
"\n" :
appEndo (Db.migrateScript renderHeader renderCommand steps) []
where
renderHeader nm =
Endo (("-- " <> BL.fromStrict (TE.encodeUtf8 nm) <> "\n"):)
renderCommand cmd =
Endo ((sqliteRenderSyntaxScript (fromSqliteCommand cmd) <> ";\n"):)
writeMigrationScript :: FilePath -> Db.MigrationSteps SqliteCommandSyntax () a -> IO ()
writeMigrationScript fp steps =
let stepBs = migrateScript steps
in BL.writeFile fp (BL.concat stepBs)
sqlitePredConverter :: Tool.HaskellPredicateConverter
sqlitePredConverter = Tool.sql92HsPredicateConverters @SqliteColumnSchemaSyntax sqliteTypeToHs <>
Tool.hsPredicateConverter sqliteHasColumnConstraint
where
sqliteHasColumnConstraint (Db.TableColumnHasConstraint tblNm colNm c ::
Db.TableColumnHasConstraint SqliteColumnSchemaSyntax)
| c == Db.constraintDefinitionSyntax Nothing Db.notNullConstraintSyntax Nothing =
Just (Db.SomeDatabasePredicate (Db.TableColumnHasConstraint tblNm colNm (Db.constraintDefinitionSyntax Nothing Db.notNullConstraintSyntax Nothing) ::
Db.TableColumnHasConstraint HsColumnSchema))
| otherwise = Nothing
sqliteTypeToHs :: SqliteDataTypeSyntax
-> Maybe HsDataType
sqliteTypeToHs = Just . sqliteDataTypeToHs
parseSqliteDataType :: T.Text -> SqliteDataTypeSyntax
parseSqliteDataType txt =
case A.parseOnly dtParser txt of
Left {} -> SqliteDataTypeSyntax (emit (TE.encodeUtf8 txt))
(hsErrorType ("Unknown SQLite datatype '" ++ T.unpack txt ++ "'"))
(Db.BeamSerializedDataType $
Db.beamSerializeJSON "sqlite"
(toJSON txt))
False
Right x -> x
where
dtParser = charP <|> varcharP <|>
ncharP <|> nvarcharP <|>
bitP <|> varbitP <|> numericP <|>
doubleP <|> integerP <|>
smallIntP <|> bigIntP <|> floatP <|>
doubleP <|> realP <|> dateP <|>
timestampP <|> timeP <|> textP <|>
blobP <|> booleanP
ws = A.many1 A.space
characterP = asciiCI "CHARACTER" <|> asciiCI "CHAR"
characterVaryingP = characterP >> ws >> asciiCI "VARYING"
charP = do
characterP
charType <$> precP <*> charSetP
varcharP = do
asciiCI "VARCHAR" <|> characterVaryingP
varCharType <$> precP <*> charSetP
ncharP = do
asciiCI "NATIONAL"
ws
characterP
nationalCharType <$> precP
nvarcharP = do
asciiCI "NVARCHAR" <|> (asciiCI "NATIONAL" >> ws >> characterVaryingP)
nationalVarCharType <$> precP
bitP = do
asciiCI "BIT"
bitType <$> precP
varbitP = do
asciiCI "VARBIT" <|> (asciiCI "BIT" >> ws >> asciiCI "VARYING")
varBitType <$> precP
numericP = do
asciiCI "NUMERIC"
numericType <$> numericPrecP
doubleP = do
asciiCI "DOUBLE" <|> asciiCI "DECIMAL"
decimalType <$> numericPrecP
floatP = do
asciiCI "FLOAT"
floatType <$> precP
realP = realType <$ asciiCI "REAL"
intTypeP =
asciiCI "INT" <|> asciiCI "INTEGER"
integerP = do
intTypeP
pure intType
smallIntP = do
asciiCI "INT2" <|> (asciiCI "SMALL" >> optional ws >> intTypeP)
pure smallIntType
bigIntP = do
asciiCI "INT8" <|> (asciiCI "BIG" >> optional ws >> intTypeP)
pure sqliteBigIntType
dateP = dateType <$ asciiCI "DATE"
timeP = do
asciiCI "TIME"
timeType <$> precP <*> timezoneP
timestampP = do
asciiCI "TIMESTAMP"
timestampType <$> precP <*> timezoneP
textP = sqliteTextType <$ asciiCI "TEXT"
blobP = sqliteBlobType <$ asciiCI "BLOB"
booleanP = booleanType <$ (asciiCI "BOOL" <|> asciiCI "BOOLEAN")
timezoneP = (skipSpace *>
asciiCI "WITH" *> ws *>
(asciiCI "TIMEZONE" <|>
(asciiCI "TIME" >> ws >>
asciiCI "ZONE")) *>
pure True) <|>
pure False
precP = optional (skipSpace *> A.char '(' *>
A.decimal <* A.char ')')
numericPrecP = optional ((,) <$> (skipSpace *> A.char '(' *>
A.decimal)
<*> (skipSpace *>
optional (A.char ',' *> skipSpace *>
A.decimal) <*
skipSpace <* A.char ')'))
charSetP = optional (skipSpace *>
asciiCI "CHARACTER" *> ws *>
asciiCI "SET" *> ws *>
A.takeWhile (not . isSpace))
getDbConstraints :: SqliteM [Db.SomeDatabasePredicate]
getDbConstraints =
SqliteM . ReaderT $ \(_, conn) -> do
tblNames <- query_ conn "SELECT name, sql from sqlite_master where type='table'"
tblPreds <-
fmap mconcat . forM tblNames $ \(tblName, sql) -> do
columns <- fmap (sortBy (comparing (\(cid, _, _, _, _, _) -> cid :: Int))) $
query_ conn (fromString ("PRAGMA table_info('" <> T.unpack tblName <> "')"))
let columnPreds =
foldMap
(\(_ ::Int, nm, typStr, notNull, _, _) ->
let dtType = if isAutoincrement then sqliteSerialType else parseSqliteDataType typStr
isAutoincrement = isJust (A.maybeResult (A.parse autoincrementParser sql))
autoincrementParser = do
A.manyTill A.anyChar $ do
hadQuote <- optional (A.char '"')
A.string nm
maybe (pure ()) (\_ -> void $ A.char '"') hadQuote
A.many1 A.space
asciiCI "INTEGER"
A.many1 A.space
asciiCI "PRIMARY"
A.many1 A.space
asciiCI "KEY"
A.many1 A.space
asciiCI "AUTOINCREMENT"
notNullPred =
if notNull
then [ Db.SomeDatabasePredicate
(Db.TableColumnHasConstraint tblName nm
(Db.constraintDefinitionSyntax Nothing Db.notNullConstraintSyntax Nothing)
:: Db.TableColumnHasConstraint SqliteColumnSchemaSyntax) ]
else []
in [ Db.SomeDatabasePredicate
(Db.TableHasColumn tblName nm dtType ::
Db.TableHasColumn SqliteColumnSchemaSyntax) ] ++
notNullPred
)
columns
pkColumns = map fst $ sortBy (comparing snd) $
mapMaybe (\(_, nm, _, _, _ :: Maybe T.Text, pk) ->
(nm,) <$> (pk <$ guard (pk > (0 :: Int)))) columns
pkPred = case pkColumns of
[] -> []
_ -> [ Db.SomeDatabasePredicate (Db.TableHasPrimaryKey tblName pkColumns) ]
pure ( [ Db.SomeDatabasePredicate (Db.TableExistsPredicate tblName) ]
++ pkPred ++ columnPreds )
pure tblPreds
sqliteText :: Db.DataType SqliteDataTypeSyntax T.Text
sqliteText = Db.DataType sqliteTextType
sqliteBlob :: Db.DataType SqliteDataTypeSyntax ByteString
sqliteBlob = Db.DataType sqliteBlobType
sqliteBigInt :: Db.DataType SqliteDataTypeSyntax Int64
sqliteBigInt = Db.DataType sqliteBigIntType