2022-03-16 03:39:21 +03:00
|
|
|
{-# LANGUAGE QuasiQuotes #-}
|
2022-03-10 14:18:13 +03:00
|
|
|
{-# LANGUAGE ViewPatterns #-}
|
2021-12-30 14:00:52 +03:00
|
|
|
|
2022-03-16 03:39:21 +03:00
|
|
|
{-# OPTIONS -Wno-redundant-constraints #-}
|
|
|
|
|
2021-12-30 14:00:52 +03:00
|
|
|
-- | CitusQL helpers. Pretty much the same as postgres. Could refactor
|
|
|
|
-- if we add more things here.
|
2022-01-21 10:48:27 +03:00
|
|
|
module Harness.Backend.Citus
|
2021-12-30 14:00:52 +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,
|
2022-06-08 02:24:42 +03:00
|
|
|
setupPermissions,
|
|
|
|
teardownPermissions,
|
2022-06-08 19:35:44 +03:00
|
|
|
setupTablesAction,
|
|
|
|
setupPermissionsAction,
|
2021-12-30 14:00:52 +03:00
|
|
|
)
|
|
|
|
where
|
|
|
|
|
2022-06-30 12:55:06 +03:00
|
|
|
import Control.Concurrent.Extended (sleep)
|
2021-12-30 14:00:52 +03:00
|
|
|
import Control.Monad.Reader
|
2022-01-25 19:34:29 +03:00
|
|
|
import Data.Aeson (Value)
|
2021-12-30 14:00:52 +03:00
|
|
|
import Data.ByteString.Char8 qualified as S8
|
2022-08-03 17:18:43 +03:00
|
|
|
import Data.String (fromString)
|
2022-03-01 01:47:51 +03:00
|
|
|
import Data.Text qualified as T
|
|
|
|
import Data.Text.Extended (commaSeparated)
|
2022-04-12 18:39:36 +03:00
|
|
|
import Data.Time (defaultTimeLocale, formatTime)
|
2021-12-30 14:00:52 +03:00
|
|
|
import Database.PostgreSQL.Simple qualified as Postgres
|
|
|
|
import Harness.Constants as Constants
|
2022-03-15 19:08:47 +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-08-02 21:01:34 +03:00
|
|
|
import Harness.Test.BackendType (BackendType (Citus), defaultSource)
|
2022-06-08 19:35:44 +03:00
|
|
|
import Harness.Test.Fixture (SetupAction (..))
|
2022-06-08 02:24:42 +03:00
|
|
|
import Harness.Test.Permissions qualified as Permissions
|
2022-04-19 18:39:02 +03:00
|
|
|
import Harness.Test.Schema (BackendScalarType (..), BackendScalarValue (..), ScalarValue (..))
|
2022-03-01 01:47:51 +03:00
|
|
|
import Harness.Test.Schema qualified as Schema
|
2022-04-20 20:15:42 +03:00
|
|
|
import Harness.TestEnvironment (TestEnvironment)
|
2022-08-03 17:18:43 +03:00
|
|
|
import Hasura.Prelude
|
2021-12-30 14:00:52 +03:00
|
|
|
import System.Process.Typed
|
|
|
|
|
|
|
|
-- | Check the citus server is live and ready to accept connections.
|
|
|
|
livenessCheck :: HasCallStack => IO ()
|
|
|
|
livenessCheck = loop Constants.postgresLivenessCheckAttempts
|
|
|
|
where
|
|
|
|
loop 0 = error ("Liveness check failed for Citus.")
|
|
|
|
loop attempts =
|
|
|
|
catch
|
|
|
|
( bracket
|
|
|
|
( Postgres.connectPostgreSQL
|
|
|
|
(fromString Constants.citusConnectionString)
|
|
|
|
)
|
|
|
|
Postgres.close
|
|
|
|
(const (pure ()))
|
|
|
|
)
|
|
|
|
( \(_failure :: ExitCodeException) -> do
|
2022-06-30 12:55:06 +03:00
|
|
|
sleep Constants.httpHealthCheckIntervalSeconds
|
2021-12-30 14:00:52 +03:00
|
|
|
loop (attempts - 1)
|
|
|
|
)
|
|
|
|
|
|
|
|
-- | Run a plain SQL query. On error, print something useful for
|
|
|
|
-- debugging.
|
|
|
|
run_ :: HasCallStack => String -> IO ()
|
|
|
|
run_ q =
|
|
|
|
catch
|
|
|
|
( bracket
|
|
|
|
( Postgres.connectPostgreSQL
|
|
|
|
(fromString Constants.citusConnectionString)
|
|
|
|
)
|
|
|
|
Postgres.close
|
|
|
|
(\conn -> void (Postgres.execute_ conn (fromString q)))
|
|
|
|
)
|
|
|
|
( \(e :: Postgres.SqlError) ->
|
|
|
|
error
|
|
|
|
( unlines
|
|
|
|
[ "Citus query error:",
|
|
|
|
S8.unpack (Postgres.sqlErrorMsg e),
|
|
|
|
"SQL was:",
|
|
|
|
q
|
|
|
|
]
|
|
|
|
)
|
|
|
|
)
|
2022-01-25 19:34:29 +03:00
|
|
|
|
|
|
|
-- | Metadata source information for the default Citus instance.
|
|
|
|
defaultSourceMetadata :: Value
|
|
|
|
defaultSourceMetadata =
|
|
|
|
[yaml|
|
|
|
|
name: citus
|
|
|
|
kind: citus
|
|
|
|
tables: []
|
|
|
|
configuration:
|
|
|
|
connection_info:
|
|
|
|
database_url: *citusConnectionString
|
|
|
|
pool_settings: {}
|
|
|
|
|]
|
2022-03-01 01:47:51 +03:00
|
|
|
|
|
|
|
-- | Serialize Table into a Citus-SQL statement, as needed, and execute it on the Citus backend
|
2022-03-15 19:08:47 +03:00
|
|
|
createTable :: HasCallStack => Schema.Table -> IO ()
|
2022-06-17 11:44:04 +03:00
|
|
|
createTable Schema.Table {tableName, tableColumns, tablePrimaryKey = pk, tableReferences, tableUniqueConstraints} = do
|
2022-03-01 01:47:51 +03:00
|
|
|
run_ $
|
|
|
|
T.unpack $
|
|
|
|
T.unwords
|
|
|
|
[ "CREATE TABLE",
|
2022-06-27 17:32:31 +03:00
|
|
|
T.pack Constants.citusDb <> "." <> wrapIdentifier 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
|
|
|
|
2022-06-17 11:44:04 +03:00
|
|
|
for_ tableUniqueConstraints (createUniqueConstraint tableName)
|
|
|
|
|
|
|
|
createUniqueConstraint :: Text -> Schema.UniqueConstraint -> IO ()
|
|
|
|
createUniqueConstraint tableName (Schema.UniqueConstraintColumns cols) =
|
|
|
|
run_ $ T.unpack $ T.unwords $ ["CREATE UNIQUE INDEX ON ", tableName, "("] ++ [commaSeparated cols] ++ [")"]
|
|
|
|
createUniqueConstraint tableName (Schema.UniqueConstraintExpression ex) =
|
|
|
|
run_ $ T.unpack $ T.unwords $ ["CREATE UNIQUE INDEX ON ", tableName, "((", ex, "))"]
|
|
|
|
|
2022-03-10 14:18:13 +03:00
|
|
|
scalarType :: HasCallStack => Schema.ScalarType -> Text
|
|
|
|
scalarType = \case
|
|
|
|
Schema.TInt -> "SERIAL"
|
|
|
|
Schema.TStr -> "VARCHAR"
|
|
|
|
Schema.TUTCTime -> "TIMESTAMP"
|
|
|
|
Schema.TBool -> "BOOLEAN"
|
2022-04-12 18:39:36 +03:00
|
|
|
Schema.TCustomType txt -> Schema.getBackendScalarType txt bstCitus
|
2022-03-10 14:18:13 +03:00
|
|
|
|
|
|
|
mkColumn :: Schema.Column -> Text
|
|
|
|
mkColumn Schema.Column {columnName, columnType, columnNullable, columnDefault} =
|
|
|
|
T.unwords
|
2022-03-18 13:04:52 +03:00
|
|
|
[ wrapIdentifier columnName,
|
2022-03-10 14:18:13 +03:00
|
|
|
scalarType columnType,
|
|
|
|
bool "NOT NULL" "DEFAULT NULL" columnNullable,
|
|
|
|
maybe "" ("DEFAULT " <>) columnDefault
|
|
|
|
]
|
|
|
|
|
|
|
|
mkPrimaryKey :: [Text] -> Text
|
|
|
|
mkPrimaryKey key =
|
|
|
|
T.unwords
|
|
|
|
[ "PRIMARY KEY",
|
|
|
|
"(",
|
2022-03-18 13:04:52 +03:00
|
|
|
commaSeparated $ map wrapIdentifier key,
|
2022-03-10 14:18:13 +03:00
|
|
|
")"
|
|
|
|
]
|
|
|
|
|
|
|
|
mkReference :: Schema.Reference -> Text
|
|
|
|
mkReference Schema.Reference {referenceLocalColumn, referenceTargetTable, referenceTargetColumn} =
|
|
|
|
T.unwords
|
2022-04-22 13:32:35 +03:00
|
|
|
[ "FOREIGN KEY",
|
2022-03-10 14:18:13 +03:00
|
|
|
"(",
|
2022-03-18 13:04:52 +03:00
|
|
|
wrapIdentifier referenceLocalColumn,
|
2022-03-10 14:18:13 +03:00
|
|
|
")",
|
|
|
|
"REFERENCES",
|
|
|
|
referenceTargetTable,
|
|
|
|
"(",
|
2022-03-18 13:04:52 +03:00
|
|
|
wrapIdentifier referenceTargetColumn,
|
2022-03-10 14:18:13 +03:00
|
|
|
")",
|
|
|
|
"ON DELETE CASCADE",
|
|
|
|
"ON UPDATE CASCADE"
|
|
|
|
]
|
2022-03-01 01:47:51 +03:00
|
|
|
|
|
|
|
-- | Serialize tableData into a Citus-SQL insert statement and execute it.
|
2022-03-15 19:08:47 +03:00
|
|
|
insertTable :: HasCallStack => Schema.Table -> IO ()
|
|
|
|
insertTable Schema.Table {tableName, tableColumns, tableData}
|
|
|
|
| null tableData = pure ()
|
|
|
|
| otherwise = do
|
|
|
|
run_ $
|
|
|
|
T.unpack $
|
|
|
|
T.unwords
|
|
|
|
[ "INSERT INTO",
|
2022-03-18 13:04:52 +03:00
|
|
|
T.pack Constants.citusDb <> "." <> wrapIdentifier tableName,
|
2022-03-15 19:08:47 +03:00
|
|
|
"(",
|
2022-03-18 13:04:52 +03:00
|
|
|
commaSeparated (wrapIdentifier . Schema.columnName <$> tableColumns),
|
2022-03-15 19:08:47 +03:00
|
|
|
")",
|
|
|
|
"VALUES",
|
|
|
|
commaSeparated $ mkRow <$> tableData,
|
|
|
|
";"
|
|
|
|
]
|
2022-03-10 14:18:13 +03:00
|
|
|
|
2022-03-18 13:04:52 +03:00
|
|
|
-- | Citus identifiers which may be case-sensitive needs to be wrapped in @""@.
|
|
|
|
wrapIdentifier :: Text -> Text
|
|
|
|
wrapIdentifier identifier = "\"" <> identifier <> "\""
|
|
|
|
|
2022-04-12 18:39:36 +03:00
|
|
|
-- | 'ScalarValue' serializer for Citus
|
|
|
|
serialize :: ScalarValue -> Text
|
|
|
|
serialize = \case
|
|
|
|
VInt i -> tshow i
|
2022-08-03 17:18:43 +03:00
|
|
|
VStr s -> "'" <> T.replace "'" "\'" s <> "'"
|
|
|
|
VUTCTime t -> T.pack $ formatTime defaultTimeLocale "'%F %T'" t
|
2022-08-01 18:44:49 +03:00
|
|
|
VBool b -> if b then "TRUE" else "FALSE"
|
2022-04-12 18:39:36 +03:00
|
|
|
VNull -> "NULL"
|
2022-04-19 18:39:02 +03:00
|
|
|
VCustomValue bsv -> Schema.formatBackendScalarValueType $ Schema.backendScalarValue bsv bsvCitus
|
2022-04-12 18:39:36 +03:00
|
|
|
|
2022-03-10 14:18:13 +03:00
|
|
|
mkRow :: [Schema.ScalarValue] -> Text
|
|
|
|
mkRow row =
|
|
|
|
T.unwords
|
|
|
|
[ "(",
|
2022-04-12 18:39:36 +03:00
|
|
|
commaSeparated $ serialize <$> row,
|
2022-03-10 14:18:13 +03:00
|
|
|
")"
|
|
|
|
]
|
2022-03-01 01:47:51 +03:00
|
|
|
|
|
|
|
-- | Serialize Table into a Citus-SQL DROP statement and execute it
|
2022-03-15 19:08:47 +03:00
|
|
|
dropTable :: HasCallStack => Schema.Table -> IO ()
|
2022-03-01 01:47:51 +03:00
|
|
|
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-01 01:47:51 +03:00
|
|
|
T.pack Constants.citusDb <> "." <> tableName,
|
|
|
|
";"
|
|
|
|
]
|
|
|
|
|
2022-03-15 19:08:47 +03:00
|
|
|
-- | Post an http request to start tracking the table
|
2022-04-20 20:15:42 +03:00
|
|
|
trackTable :: HasCallStack => TestEnvironment -> Schema.Table -> IO ()
|
|
|
|
trackTable testEnvironment table =
|
|
|
|
Schema.trackTable Citus (defaultSource Citus) table testEnvironment
|
2022-03-15 19:08:47 +03:00
|
|
|
|
2022-03-01 01:47:51 +03:00
|
|
|
-- | Post an http request to stop tracking the table
|
2022-04-20 20:15:42 +03:00
|
|
|
untrackTable :: HasCallStack => TestEnvironment -> Schema.Table -> IO ()
|
|
|
|
untrackTable testEnvironment table =
|
|
|
|
Schema.untrackTable Citus (defaultSource Citus) table testEnvironment
|
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.
|
2022-04-20 20:15:42 +03:00
|
|
|
setup :: HasCallStack => [Schema.Table] -> (TestEnvironment, ()) -> IO ()
|
|
|
|
setup tables (testEnvironment, _) = do
|
2022-03-01 01:47:51 +03:00
|
|
|
-- Clear and reconfigure the metadata
|
2022-05-11 09:14:25 +03:00
|
|
|
GraphqlEngine.setSource testEnvironment defaultSourceMetadata Nothing
|
2022-03-01 01:47:51 +03:00
|
|
|
-- Setup and track tables
|
|
|
|
for_ tables $ \table -> do
|
|
|
|
createTable table
|
|
|
|
insertTable table
|
2022-04-20 20:15:42 +03:00
|
|
|
trackTable testEnvironment table
|
2022-03-10 14:18:13 +03:00
|
|
|
-- Setup relationships
|
|
|
|
for_ tables $ \table -> do
|
2022-04-20 20:15:42 +03:00
|
|
|
Schema.trackObjectRelationships Citus table testEnvironment
|
|
|
|
Schema.trackArrayRelationships Citus table testEnvironment
|
2022-03-01 01:47:51 +03:00
|
|
|
|
2022-06-08 19:35:44 +03:00
|
|
|
setupTablesAction :: [Schema.Table] -> TestEnvironment -> SetupAction
|
|
|
|
setupTablesAction ts env =
|
|
|
|
SetupAction
|
|
|
|
(setup ts (env, ()))
|
|
|
|
(const $ teardown ts (env, ()))
|
|
|
|
|
|
|
|
setupPermissionsAction :: [Permissions.Permission] -> TestEnvironment -> SetupAction
|
|
|
|
setupPermissionsAction permissions env =
|
|
|
|
SetupAction
|
|
|
|
(setupPermissions permissions env)
|
|
|
|
(const $ teardownPermissions permissions env)
|
|
|
|
|
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.
|
2022-04-20 20:15:42 +03:00
|
|
|
teardown :: HasCallStack => [Schema.Table] -> (TestEnvironment, ()) -> IO ()
|
2022-06-27 17:32:31 +03:00
|
|
|
teardown (reverse -> tables) (testEnvironment, _) = do
|
|
|
|
finally
|
|
|
|
-- Teardown relationships first
|
|
|
|
( forFinally_ tables $ \table ->
|
|
|
|
Schema.untrackRelationships Citus table testEnvironment
|
|
|
|
)
|
|
|
|
-- Then teardown tables
|
|
|
|
( forFinally_ tables $ \table ->
|
|
|
|
finally
|
2022-04-20 20:15:42 +03:00
|
|
|
(untrackTable testEnvironment table)
|
2022-03-15 19:08:47 +03:00
|
|
|
(dropTable table)
|
2022-06-27 17:32:31 +03:00
|
|
|
)
|
2022-06-08 02:24:42 +03:00
|
|
|
|
|
|
|
-- | Setup the given permissions to the graphql engine in a TestEnvironment.
|
|
|
|
setupPermissions :: [Permissions.Permission] -> TestEnvironment -> IO ()
|
|
|
|
setupPermissions permissions env = Permissions.setup "citus" permissions env
|
|
|
|
|
|
|
|
-- | Remove the given permissions from the graphql engine in a TestEnvironment.
|
|
|
|
teardownPermissions :: [Permissions.Permission] -> TestEnvironment -> IO ()
|
|
|
|
teardownPermissions permissions env = Permissions.teardown "citus" permissions env
|