graphql-engine/server/tests-hspec/Harness/Backend/Sqlserver.hs
Philip Lykke Carlsen 12c3eddef7 Amendments to the hspec testsuite
This PR proposes some changes to the hspec testsuite:

* It amends the framework to make it easier to test from the ghci REPL
* It introduces a new module `Fixture`, distinguished from `Context` by:
   * using a new concept of `SetupAction`s which bundle setup and teardown actions into one abstraction, making test system state setup more concise, modularized and safe (because the fixture know knows about the ordering of setup actions and can do partial rollbacks)
   * somewhat opinionated, elides the `Options` of `Context`, preferring instead that tests that care about stringification of json numbers manage that themselves.

(Note that this PR builds on #4390, so contains some spurious commits which will become irrelevant once that PR is merged)

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4630
GitOrigin-RevId: 619c8d985aed0aa42de31d6f16891d0782f4b4b5
2022-06-08 16:36:50 +00:00

287 lines
9.1 KiB
Haskell

{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS -Wno-redundant-constraints #-}
-- | SQLServer helpers.
module Harness.Backend.Sqlserver
( livenessCheck,
run_,
defaultSourceMetadata,
defaultSourceConfiguration,
createTable,
insertTable,
trackTable,
dropTable,
untrackTable,
setup,
teardown,
setupTablesAction,
setupPermissionsAction,
)
where
import Control.Concurrent
import Control.Monad.Reader
import Data.Aeson (Value)
import Data.Bool (bool)
import Data.Foldable (for_)
import Data.String
import Data.Text (Text, pack, replace)
import Data.Text qualified as T (pack, unpack, unwords)
import Data.Text.Extended (commaSeparated)
import Data.Time (defaultTimeLocale, formatTime)
import Database.ODBC.SQLServer qualified as Sqlserver
import Harness.Constants qualified as Constants
import Harness.Exceptions
import Harness.GraphqlEngine qualified as GraphqlEngine
import Harness.Quoter.Yaml (yaml)
import Harness.Test.Context (BackendType (SQLServer))
import Harness.Test.Fixture
import Harness.Test.Permissions qualified as Permissions
import Harness.Test.Schema (BackendScalarType (..), BackendScalarValue (..), ScalarValue (..))
import Harness.Test.Schema qualified as Schema
import Harness.TestEnvironment (TestEnvironment)
import Hasura.Prelude (tshow)
import System.Process.Typed
import Prelude
-- | Check that the SQLServer service is live and ready to accept connections.
livenessCheck :: HasCallStack => IO ()
livenessCheck = loop Constants.sqlserverLivenessCheckAttempts
where
loop 0 = error ("Liveness check failed for SQLServer.")
loop attempts =
catch
( bracket
(Sqlserver.connect Constants.sqlserverConnectInfo)
Sqlserver.close
(const (pure ()))
)
( \(_failure :: ExitCodeException) -> do
threadDelay
Constants.sqlserverLivenessCheckIntervalMicroseconds
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
(Sqlserver.connect Constants.sqlserverConnectInfo)
Sqlserver.close
(\conn -> void (Sqlserver.exec conn (fromString query')))
)
( \(e :: SomeException) ->
error
( unlines
[ "SQLServer query error:",
show e,
"SQL was:",
query'
]
)
)
-- | Metadata source information for the default MSSQL instance.
defaultSourceMetadata :: Value
defaultSourceMetadata =
let source = defaultSource SQLServer
backendType = defaultBackendTypeString SQLServer
in [yaml|
name: *source
kind: *backendType
tables: []
configuration: *defaultSourceConfiguration
|]
defaultSourceConfiguration :: Value
defaultSourceConfiguration =
[yaml|
connection_info:
database_url: *sqlserverConnectInfo
pool_settings: {}
|]
where
sqlserverConnectInfo = Constants.sqlserverConnectInfo
-- | Serialize Table into a T-SQL statement, as needed, and execute it on the Sqlserver backend
createTable :: Schema.Table -> IO ()
createTable Schema.Table {tableName, tableColumns, tablePrimaryKey = pk, tableReferences} = do
run_ $
T.unpack $
T.unwords
[ "CREATE TABLE",
T.pack Constants.sqlserverDb <> "." <> tableName,
"(",
commaSeparated $
(mkColumn <$> tableColumns)
<> (bool [mkPrimaryKey pk] [] (null pk))
<> (mkReference <$> tableReferences),
");"
]
scalarType :: HasCallStack => Schema.ScalarType -> Text
scalarType = \case
Schema.TInt -> "INT"
Schema.TStr -> "NVARCHAR(127)"
Schema.TUTCTime -> "DATETIME"
Schema.TBool -> "BOOLEAN"
Schema.TCustomType txt -> Schema.getBackendScalarType txt bstMssql
mkColumn :: Schema.Column -> Text
mkColumn Schema.Column {columnName, columnType, columnNullable, columnDefault} =
T.unwords
[ wrapIdentifier columnName,
scalarType columnType,
bool "NOT NULL" "DEFAULT NULL" columnNullable,
maybe "" ("DEFAULT " <>) columnDefault
]
mkPrimaryKey :: [Text] -> Text
mkPrimaryKey key =
T.unwords
[ "PRIMARY KEY",
"(",
commaSeparated $ map wrapIdentifier key,
")"
]
mkReference :: Schema.Reference -> Text
mkReference Schema.Reference {referenceLocalColumn, referenceTargetTable, referenceTargetColumn} =
T.unwords
[ "CONSTRAINT ",
constraintName,
"FOREIGN KEY ",
"(",
wrapIdentifier referenceLocalColumn,
")",
"REFERENCES",
T.pack Constants.sqlserverDb <> "." <> referenceTargetTable,
"(",
wrapIdentifier referenceTargetColumn,
")",
"ON DELETE CASCADE",
"ON UPDATE CASCADE"
]
where
constraintName :: Text
constraintName =
"FK_" <> referenceTargetTable <> "_" <> referenceTargetColumn
<> "_"
<> referenceLocalColumn
-- | Serialize tableData into a T-SQL insert statement and execute it.
insertTable :: HasCallStack => Schema.Table -> IO ()
insertTable Schema.Table {tableName, tableColumns, tableData}
| null tableData = pure ()
| otherwise = do
run_ $
T.unpack $
T.unwords
[ "INSERT INTO",
T.pack Constants.sqlserverDb <> "." <> wrapIdentifier tableName,
"(",
commaSeparated (wrapIdentifier . Schema.columnName <$> tableColumns),
")",
"VALUES",
commaSeparated $ mkRow <$> tableData,
";"
]
-- | MSSQL identifiers which may contain spaces or be case-sensitive needs to be wrapped in @[]@.
--
-- More information can be found in the mssql docs:
-- https://docs.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers
wrapIdentifier :: Text -> Text
wrapIdentifier identifier = "[" <> identifier <> "]"
-- | 'ScalarValue' serializer for Mssql
serialize :: ScalarValue -> Text
serialize = \case
VInt i -> tshow i
VStr s -> "'" <> replace "'" "\'" s <> "'"
VUTCTime t -> pack $ formatTime defaultTimeLocale "'%F %T'" t
VBool b -> tshow @Int $ if b then 1 else 0
VNull -> "NULL"
VCustomValue bsv -> Schema.formatBackendScalarValueType $ Schema.backendScalarValue bsv bsvMssql
mkRow :: [Schema.ScalarValue] -> Text
mkRow row =
T.unwords
[ "(",
commaSeparated $ serialize <$> row,
")"
]
-- | Serialize Table into a T-SQL DROP statement and execute it
dropTable :: HasCallStack => Schema.Table -> IO ()
dropTable Schema.Table {tableName} = do
run_ $
T.unpack $
T.unwords
[ "DROP TABLE", -- we don't want @IF EXISTS@ here, because we don't want this to fail silently
T.pack Constants.sqlserverDb <> "." <> tableName,
";"
]
-- | Post an http request to start tracking the table
trackTable :: HasCallStack => TestEnvironment -> Schema.Table -> IO ()
trackTable testEnvironment table =
Schema.trackTable SQLServer (defaultSource SQLServer) table testEnvironment
-- | Post an http request to stop tracking the table
untrackTable :: HasCallStack => TestEnvironment -> Schema.Table -> IO ()
untrackTable testEnvironment table =
Schema.untrackTable SQLServer (defaultSource SQLServer) table testEnvironment
-- | Setup the schema in the most expected way.
-- NOTE: Certain test modules may warrant having their own local version.
setup :: HasCallStack => [Schema.Table] -> (TestEnvironment, ()) -> IO ()
setup tables (testEnvironment, _) = do
-- Clear and reconfigure the metadata
GraphqlEngine.setSource testEnvironment defaultSourceMetadata Nothing
-- Setup and track tables
for_ tables $ \table -> do
createTable table
insertTable table
trackTable testEnvironment table
-- Setup relationships
for_ tables $ \table -> do
Schema.trackObjectRelationships SQLServer table testEnvironment
Schema.trackArrayRelationships SQLServer table testEnvironment
-- | Teardown the schema and tracking in the most expected way.
-- NOTE: Certain test modules may warrant having their own version.
teardown :: HasCallStack => [Schema.Table] -> (TestEnvironment, ()) -> IO ()
teardown tables (testEnvironment, _) = do
forFinally_ (reverse tables) $ \table ->
finally
(Schema.untrackRelationships SQLServer table testEnvironment)
( finally
(untrackTable testEnvironment table)
(dropTable table)
)
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)
-- | Setup the given permissions to the graphql engine in a TestEnvironment.
setupPermissions :: [Permissions.Permission] -> TestEnvironment -> IO ()
setupPermissions permissions env = Permissions.setup "mssql" permissions env
-- | Remove the given permissions from the graphql engine in a TestEnvironment.
teardownPermissions :: [Permissions.Permission] -> TestEnvironment -> IO ()
teardownPermissions permissions env = Permissions.teardown "mssql" permissions env