2022-03-16 03:39:21 +03:00
|
|
|
{-# LANGUAGE QuasiQuotes #-}
|
|
|
|
{-# LANGUAGE ViewPatterns #-}
|
|
|
|
|
2021-11-17 22:50:39 +03:00
|
|
|
{-# OPTIONS -Wno-redundant-constraints #-}
|
|
|
|
|
|
|
|
-- | MySQL helpers.
|
2022-01-21 10:48:27 +03:00
|
|
|
module Harness.Backend.Mysql
|
2021-11-17 22:50:39 +03:00
|
|
|
( livenessCheck,
|
|
|
|
run_,
|
2022-01-25 19:34:29 +03:00
|
|
|
defaultSourceMetadata,
|
2022-03-01 01:47:51 +03:00
|
|
|
createTable,
|
|
|
|
insertTable,
|
|
|
|
trackTable,
|
|
|
|
dropTable,
|
|
|
|
untrackTable,
|
|
|
|
setup,
|
|
|
|
teardown,
|
2021-11-17 22:50:39 +03:00
|
|
|
)
|
|
|
|
where
|
|
|
|
|
|
|
|
import Control.Concurrent
|
2021-11-26 21:21:01 +03:00
|
|
|
import Control.Monad.Reader
|
2022-01-25 19:34:29 +03:00
|
|
|
import Data.Aeson (Value)
|
2022-03-01 01:47:51 +03:00
|
|
|
import Data.Bool (bool)
|
|
|
|
import Data.Foldable (for_)
|
2021-11-17 22:50:39 +03:00
|
|
|
import Data.String
|
2022-03-01 01:47:51 +03:00
|
|
|
import Data.Text (Text)
|
|
|
|
import Data.Text qualified as T
|
|
|
|
import Data.Text.Extended (commaSeparated)
|
2021-11-17 22:50:39 +03:00
|
|
|
import Database.MySQL.Simple as Mysql
|
2021-11-26 21:21:01 +03:00
|
|
|
import Harness.Constants as Constants
|
2022-04-04 17:45:12 +03:00
|
|
|
import Harness.Exceptions
|
2022-03-01 01:47:51 +03:00
|
|
|
import Harness.GraphqlEngine qualified as GraphqlEngine
|
2022-01-25 19:34:29 +03:00
|
|
|
import Harness.Quoter.Yaml (yaml)
|
2022-03-01 01:47:51 +03:00
|
|
|
import Harness.State (State)
|
2022-03-15 19:08:47 +03:00
|
|
|
import Harness.Test.Context (BackendType (MySQL), defaultBackendTypeString, defaultSource)
|
2022-03-01 01:47:51 +03:00
|
|
|
import Harness.Test.Schema qualified as Schema
|
2021-11-17 22:50:39 +03:00
|
|
|
import System.Process.Typed
|
|
|
|
import Prelude
|
|
|
|
|
|
|
|
-- | Check that the MySQL service is live and ready to accept connections.
|
|
|
|
livenessCheck :: HasCallStack => IO ()
|
|
|
|
livenessCheck = loop Constants.mysqlLivenessCheckAttempts
|
|
|
|
where
|
|
|
|
loop 0 = error ("Liveness check failed for MySQL.")
|
|
|
|
loop attempts =
|
|
|
|
catch
|
|
|
|
( bracket
|
|
|
|
(Mysql.connect Constants.mysqlConnectInfo)
|
|
|
|
Mysql.close
|
|
|
|
(const (pure ()))
|
|
|
|
)
|
|
|
|
( \(_failure :: ExitCodeException) -> do
|
|
|
|
threadDelay
|
|
|
|
Constants.mysqlLivenessCheckIntervalMicroseconds
|
|
|
|
loop (attempts - 1)
|
|
|
|
)
|
|
|
|
|
|
|
|
-- | Run a plain SQL string against the server, ignore the
|
|
|
|
-- result. Just checks for errors.
|
|
|
|
run_ :: HasCallStack => String -> IO ()
|
|
|
|
run_ query' =
|
|
|
|
catch
|
|
|
|
( bracket
|
|
|
|
(Mysql.connect Constants.mysqlConnectInfo)
|
|
|
|
Mysql.close
|
|
|
|
(\conn -> void (Mysql.execute_ conn (fromString query')))
|
|
|
|
)
|
|
|
|
( \(e :: SomeException) ->
|
|
|
|
error
|
|
|
|
( unlines
|
|
|
|
[ "MySQL query error:",
|
|
|
|
show e,
|
|
|
|
"SQL was:",
|
|
|
|
query'
|
|
|
|
]
|
|
|
|
)
|
|
|
|
)
|
2022-01-25 19:34:29 +03:00
|
|
|
|
|
|
|
-- | Metadata source information for the default Mysql instance.
|
|
|
|
defaultSourceMetadata :: Value
|
|
|
|
defaultSourceMetadata =
|
2022-03-15 19:08:47 +03:00
|
|
|
let source = defaultSource MySQL
|
|
|
|
backendType = defaultBackendTypeString MySQL
|
|
|
|
in [yaml|
|
|
|
|
name: *source
|
|
|
|
kind: *backendType
|
2022-01-25 19:34:29 +03:00
|
|
|
tables: []
|
|
|
|
configuration:
|
2022-03-15 19:08:47 +03:00
|
|
|
database: *mysqlDb
|
2022-01-25 19:34:29 +03:00
|
|
|
user: *mysqlUser
|
|
|
|
password: *mysqlPassword
|
|
|
|
host: *mysqlHost
|
|
|
|
port: *mysqlPort
|
|
|
|
pool_settings: {}
|
|
|
|
|]
|
2022-03-01 01:47:51 +03:00
|
|
|
|
|
|
|
-- | Serialize Table into a SQL statement, as needed, and execute it on the MySQL backend
|
|
|
|
createTable :: Schema.Table -> IO ()
|
|
|
|
createTable Schema.Table {tableName, tableColumns, tablePrimaryKey = pk, tableReferences} = do
|
|
|
|
run_ $
|
|
|
|
T.unpack $
|
|
|
|
T.unwords
|
|
|
|
[ "CREATE TABLE",
|
2022-03-15 19:08:47 +03:00
|
|
|
T.pack Constants.mysqlDb <> "." <> tableName,
|
2022-03-01 01:47:51 +03:00
|
|
|
"(",
|
|
|
|
commaSeparated $
|
|
|
|
(mkColumn <$> tableColumns)
|
|
|
|
<> (bool [mkPrimaryKey pk] [] (null pk))
|
|
|
|
<> (mkReference <$> tableReferences),
|
|
|
|
");"
|
|
|
|
]
|
2022-03-10 14:18:13 +03:00
|
|
|
|
|
|
|
scalarType :: HasCallStack => Schema.ScalarType -> Text
|
|
|
|
scalarType = \case
|
|
|
|
Schema.TInt -> "INT UNSIGNED"
|
|
|
|
Schema.TStr -> "TEXT"
|
|
|
|
Schema.TUTCTime -> "DATETIME"
|
|
|
|
Schema.TBool -> "BIT"
|
|
|
|
t -> error $ "Unexpected scalar type used for MySQL: " <> show t
|
|
|
|
|
|
|
|
mkColumn :: Schema.Column -> Text
|
|
|
|
mkColumn Schema.Column {columnName, columnType, columnNullable, columnDefault} =
|
|
|
|
T.unwords
|
|
|
|
[ columnName,
|
|
|
|
scalarType columnType,
|
|
|
|
bool "NOT NULL" "DEFAULT NULL" columnNullable,
|
|
|
|
maybe "" ("DEFAULT " <>) columnDefault
|
|
|
|
]
|
|
|
|
|
|
|
|
mkPrimaryKey :: [Text] -> Text
|
|
|
|
mkPrimaryKey key =
|
|
|
|
T.unwords
|
|
|
|
[ "PRIMARY KEY",
|
|
|
|
"(",
|
|
|
|
commaSeparated key,
|
|
|
|
")"
|
|
|
|
]
|
|
|
|
|
|
|
|
mkReference :: Schema.Reference -> Text
|
|
|
|
mkReference Schema.Reference {referenceLocalColumn, referenceTargetTable, referenceTargetColumn} =
|
|
|
|
T.unwords
|
|
|
|
[ "FOREIGN KEY",
|
|
|
|
"(",
|
|
|
|
referenceLocalColumn,
|
|
|
|
")",
|
|
|
|
"REFERENCES",
|
|
|
|
referenceTargetTable,
|
|
|
|
"(",
|
|
|
|
referenceTargetColumn,
|
|
|
|
")",
|
|
|
|
"ON DELETE CASCADE",
|
|
|
|
"ON UPDATE CASCADE"
|
|
|
|
]
|
2022-03-01 01:47:51 +03:00
|
|
|
|
|
|
|
-- | Serialize tableData into an SQL insert statement and execute it.
|
|
|
|
insertTable :: Schema.Table -> IO ()
|
2022-03-15 19:08:47 +03:00
|
|
|
insertTable Schema.Table {tableName, tableColumns, tableData}
|
|
|
|
| null tableData = pure ()
|
|
|
|
| otherwise = do
|
|
|
|
run_ $
|
|
|
|
T.unpack $
|
|
|
|
T.unwords
|
|
|
|
[ "INSERT INTO",
|
|
|
|
T.pack Constants.mysqlDb <> "." <> tableName,
|
|
|
|
"(",
|
|
|
|
commaSeparated (Schema.columnName <$> tableColumns),
|
|
|
|
")",
|
|
|
|
"VALUES",
|
|
|
|
commaSeparated $ mkRow <$> tableData,
|
|
|
|
";"
|
|
|
|
]
|
2022-03-10 14:18:13 +03:00
|
|
|
|
|
|
|
mkRow :: [Schema.ScalarValue] -> Text
|
|
|
|
mkRow row =
|
|
|
|
T.unwords
|
|
|
|
[ "(",
|
|
|
|
commaSeparated $ Schema.serialize <$> row,
|
|
|
|
")"
|
|
|
|
]
|
2022-03-01 01:47:51 +03:00
|
|
|
|
|
|
|
-- | Serialize Table into an SQL DROP statement and execute it
|
|
|
|
dropTable :: Schema.Table -> IO ()
|
|
|
|
dropTable Schema.Table {tableName} = do
|
|
|
|
run_ $
|
|
|
|
T.unpack $
|
|
|
|
T.unwords
|
2022-03-10 14:18:13 +03:00
|
|
|
[ "DROP TABLE", -- we don't want @IF EXISTS@ here, because we don't want this to fail silently
|
2022-03-15 19:08:47 +03:00
|
|
|
T.pack Constants.mysqlDb <> "." <> tableName,
|
2022-03-01 01:47:51 +03:00
|
|
|
";"
|
|
|
|
]
|
|
|
|
|
2022-03-15 19:08:47 +03:00
|
|
|
-- | Post an http request to start tracking the table
|
|
|
|
trackTable :: State -> Schema.Table -> IO ()
|
|
|
|
trackTable state table =
|
|
|
|
Schema.trackTable MySQL (defaultSource MySQL) table state
|
|
|
|
|
2022-03-01 01:47:51 +03:00
|
|
|
-- | Post an http request to stop tracking the table
|
|
|
|
untrackTable :: State -> Schema.Table -> IO ()
|
2022-03-15 19:08:47 +03:00
|
|
|
untrackTable state table =
|
|
|
|
Schema.untrackTable MySQL (defaultSource MySQL) table state
|
2022-03-01 01:47:51 +03:00
|
|
|
|
|
|
|
-- | Setup the schema in the most expected way.
|
|
|
|
-- NOTE: Certain test modules may warrant having their own local version.
|
|
|
|
setup :: [Schema.Table] -> (State, ()) -> IO ()
|
|
|
|
setup tables (state, _) = do
|
|
|
|
-- Clear and reconfigure the metadata
|
|
|
|
GraphqlEngine.setSource state defaultSourceMetadata
|
|
|
|
-- Setup and track tables
|
|
|
|
for_ tables $ \table -> do
|
|
|
|
createTable table
|
|
|
|
insertTable table
|
|
|
|
trackTable state table
|
2022-03-10 14:18:13 +03:00
|
|
|
-- Setup relationships
|
|
|
|
for_ tables $ \table -> do
|
2022-03-15 19:08:47 +03:00
|
|
|
Schema.trackObjectRelationships MySQL table state
|
|
|
|
Schema.trackArrayRelationships MySQL table state
|
2022-03-01 01:47:51 +03:00
|
|
|
|
|
|
|
-- | Teardown the schema and tracking in the most expected way.
|
|
|
|
-- NOTE: Certain test modules may warrant having their own version.
|
|
|
|
teardown :: [Schema.Table] -> (State, ()) -> IO ()
|
2022-03-15 19:08:47 +03:00
|
|
|
teardown tables (state, _) = do
|
2022-04-04 17:45:12 +03:00
|
|
|
forFinally_ (reverse tables) $ \table ->
|
2022-03-15 19:08:47 +03:00
|
|
|
finally
|
|
|
|
(Schema.untrackRelationships MySQL table state)
|
|
|
|
( finally
|
|
|
|
(untrackTable state table)
|
|
|
|
(dropTable table)
|
|
|
|
)
|