mysql: Metadata awareness

https://github.com/hasura/graphql-engine-mono/pull/1599

Co-authored-by: Chris Done <11019+chrisdone@users.noreply.github.com>
Co-authored-by: Aniket Deshpande <922486+aniketd@users.noreply.github.com>
Co-authored-by: Abby Sassel <3883855+sassela@users.noreply.github.com>
GitOrigin-RevId: 4df4a8ff00fa8ef311a85199d66abe4cc10adc8c
This commit is contained in:
Sibi Prabakaran 2021-07-15 18:14:26 +05:30 committed by hasura-bot
parent 241a116e8e
commit 385d27449e
24 changed files with 1026 additions and 19 deletions

View File

@ -395,6 +395,13 @@ library
, Hasura.Backends.MySQL.Types
, Hasura.Backends.MySQL.Connection
, Hasura.Backends.MySQL.Meta
, Hasura.Backends.MySQL.Instances.Types
, Hasura.Backends.MySQL.Instances.Metadata
, Hasura.Backends.MySQL.Instances.Schema
, Hasura.Backends.MySQL.Instances.Execute
, Hasura.Backends.MySQL.Instances.Transport
, Hasura.Backends.MySQL.Instances.API
-- Exposed for benchmark:
, Hasura.Cache.Bounded

View File

@ -1,18 +1,21 @@
module Hasura.Backends.MySQL.Connection where
import Data.Pool (createPool)
import Data.Pool (createPool, withResource)
import qualified Data.Text as T
import Database.MySQL.Base
import Hasura.Backends.MySQL.Meta (getMetadata)
import Hasura.Backends.MySQL.Types
import Hasura.Base.Error
import Hasura.Prelude
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.Source
import Hasura.SQL.Backend
resolveSourceConfig :: (MonadIO m) =>
SourceName -> ConnSourceConfig -> m (Either QErr SourceConfig)
resolveSourceConfig _name csc@ConnSourceConfig{..} =
resolveSourceConfig _name csc@ConnSourceConfig{_cscPoolSettings = ConnPoolSettings{..}, ..} =
let connectInfo =
defaultConnectInfo
{ connectHost = T.unpack _cscHost
@ -23,4 +26,19 @@ resolveSourceConfig _name csc@ConnSourceConfig{..} =
}
in runExceptT $
SourceConfig csc <$>
liftIO (createPool (connect connectInfo) close 1 (60 {-seconds-} * 60 {-minutes-}) 1)
liftIO
(createPool
(connect connectInfo)
close
1
(fromIntegral _cscIdleTimeout)
(fromIntegral _cscMaxConnections))
resolveDatabaseMetadata :: (MonadIO m) =>
SourceConfig ->
m (Either QErr (ResolvedSource 'MySQL))
resolveDatabaseMetadata sc@SourceConfig{..} =
runExceptT $ do
metadata <- liftIO $ withResource scConnectionPool (getMetadata scConfig)
pure $ ResolvedSource sc metadata mempty mempty

View File

@ -0,0 +1,17 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Hasura.Backends.MySQL.Instances.API where
import Hasura.Prelude
import Hasura.SQL.Backend
import Hasura.Server.API.Backend
instance BackendAPI 'MySQL where
metadataV1CommandParsers = concat
[ sourceCommands @'MySQL
, tableCommands @'MySQL
, tablePermissionsCommands @'MySQL
, relationshipCommands @'MySQL
]

View File

@ -0,0 +1,21 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Hasura.Backends.MySQL.Instances.Execute where
import Hasura.Base.Error
import Hasura.GraphQL.Execute.Backend
import Hasura.Prelude
import Hasura.RQL.Types
import qualified Hasura.Tracing as Tracing
instance BackendExecute 'MySQL where
type PreparedQuery 'MySQL = Text
type MultiplexedQuery 'MySQL = Void
type ExecutionMonad 'MySQL = Tracing.TraceT (ExceptT QErr IO)
mkDBQueryPlan = error "MySQL backend does not support this operation yet."
mkDBMutationPlan = error "MySQL backend does not support this operation yet."
mkDBSubscriptionPlan _ _ _ _ = error "MySQL backend does not support this operation yet."
mkDBQueryExplain = error "MySQL backend does not support this operation yet."
mkLiveQueryExplain _ = error "MySQL backend does not support this operation yet."

View File

@ -0,0 +1,21 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Hasura.Backends.MySQL.Instances.Metadata where
import qualified Hasura.Backends.MySQL.Connection as MySQL
import Hasura.Prelude
import Hasura.RQL.Types.Metadata.Backend
import Hasura.SQL.Backend
instance BackendMetadata 'MySQL where
buildComputedFieldInfo = error "MySQL backend does not support this operation yet."
fetchAndValidateEnumValues = error "MySQL backend does not support this operation yet."
resolveSourceConfig = MySQL.resolveSourceConfig
resolveDatabaseMetadata = MySQL.resolveDatabaseMetadata
createTableEventTrigger = error "MySQL backend does not support this operation yet."
buildEventTriggerInfo = error "MySQL backend does not support this operation yet."
parseBoolExpOperations = error "MySQL backend does not support this operation yet."
buildFunctionInfo = error "MySQL backend does not support this operation yet."
updateColumnInEventTrigger = error "MySQL backend does not support this operation yet."
parseCollectableType = error "MySQL backend does not support this operation yet."
postDropSourceHook = error "MySQL backend does not support this operation yet."

View File

@ -0,0 +1,295 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Hasura.Backends.MySQL.Instances.Schema where
import qualified Data.Aeson as J
import qualified Data.HashMap.Strict as HM
import qualified Data.List.NonEmpty as NE
import Data.Text.Extended
import qualified Database.MySQL.Base.Types as MySQL
import qualified Hasura.Backends.MySQL.Types as MySQL
import Hasura.Base.Error
import Hasura.GraphQL.Parser hiding (EnumValueInfo, field)
import qualified Hasura.GraphQL.Parser as P
import Hasura.GraphQL.Parser.Internal.Parser hiding (field)
import Hasura.GraphQL.Parser.Internal.TypeChecking
import Hasura.GraphQL.Schema.Backend
import qualified Hasura.GraphQL.Schema.Build as GSB
import Hasura.GraphQL.Schema.Select
import Hasura.Prelude
import Hasura.RQL.IR
import qualified Hasura.RQL.IR.Select as IR
import qualified Hasura.RQL.IR.Update as IR
import Hasura.RQL.Types as RQL
import qualified Language.GraphQL.Draft.Syntax as G
instance BackendSchema 'MySQL where
buildTableQueryFields = GSB.buildTableQueryFields
buildTableRelayQueryFields = buildTableRelayQueryFields'
buildTableInsertMutationFields = buildTableInsertMutationFields'
buildTableUpdateMutationFields = buildTableUpdateMutationFields'
buildTableDeleteMutationFields = buildTableDeleteMutationFields'
buildFunctionQueryFields = buildFunctionQueryFields'
buildFunctionRelayQueryFields = buildFunctionRelayQueryFields'
buildFunctionMutationFields = buildFunctionMutationFields'
relayExtension = Nothing
tableArguments = mysqlTableArgs
nodesAggExtension = Just ()
columnParser = columnParser'
jsonPathArg = jsonPathArg'
orderByOperators = orderByOperators'
comparisonExps = comparisonExps'
updateOperators = updateOperators'
mkCountType = error "MySQL backend does not support this operation yet."
aggregateOrderByCountType = error "MySQL backend does not support this operation yet."
computedField = error "MySQL backend does not support this operation yet."
node = error "MySQL backend does not support this operation yet."
columnDefaultValue = error "MySQL backend does not support this operation yet."
mysqlTableArgs
:: forall r m n
. MonadBuildSchema 'MySQL r m n
=> SourceName
-> TableInfo 'MySQL
-> SelPermInfo 'MySQL
-> m (InputFieldsParser n (IR.SelectArgsG 'MySQL (UnpreparedValue 'MySQL)))
mysqlTableArgs sourceName tableInfo selectPermissions = do
whereParser <- tableWhereArg sourceName tableInfo selectPermissions
orderByParser <- tableOrderByArg sourceName tableInfo selectPermissions
pure do
whereArg <- whereParser
orderByArg <- orderByParser
limitArg <- tableLimitArg
offsetArg <- tableOffsetArg
pure $ IR.SelectArgs
{ IR._saWhere = whereArg
, IR._saOrderBy = orderByArg
, IR._saLimit = limitArg
, IR._saOffset = offsetArg
, IR._saDistinct = Nothing
}
buildTableRelayQueryFields' ::
MonadBuildSchema 'MySQL r m n =>
SourceName ->
RQL.SourceConfig 'MySQL ->
RQL.TableName 'MySQL ->
TableInfo 'MySQL ->
G.Name ->
NESeq (ColumnInfo 'MySQL) ->
SelPermInfo 'MySQL ->
m [FieldParser n (QueryRootField UnpreparedValue UnpreparedValue)]
buildTableRelayQueryFields' _sourceName _sourceInfo _tableName _tableInfo _gqlName _pkeyColumns _selPerms =
pure []
buildTableInsertMutationFields' ::
MonadBuildSchema 'MySQL r m n =>
SourceName ->
RQL.SourceConfig 'MySQL ->
RQL.TableName 'MySQL ->
TableInfo 'MySQL ->
G.Name ->
InsPermInfo 'MySQL ->
Maybe (SelPermInfo 'MySQL) ->
Maybe (UpdPermInfo 'MySQL) ->
m [FieldParser n (MutationRootField UnpreparedValue UnpreparedValue)]
buildTableInsertMutationFields' _sourceName _sourceInfo _tableName _tableInfo _gqlName _insPerms _selPerms _updPerms =
pure []
buildTableUpdateMutationFields' ::
MonadBuildSchema 'MySQL r m n =>
SourceName ->
RQL.SourceConfig 'MySQL ->
RQL.TableName 'MySQL ->
TableInfo 'MySQL ->
G.Name ->
UpdPermInfo 'MySQL ->
Maybe (SelPermInfo 'MySQL) ->
m [FieldParser n (MutationRootField UnpreparedValue UnpreparedValue)]
buildTableUpdateMutationFields' _sourceName _sourceInfo _tableName _tableInfo _gqlName _updPerns _selPerms =
pure []
buildTableDeleteMutationFields' ::
MonadBuildSchema 'MySQL r m n =>
SourceName ->
RQL.SourceConfig 'MySQL ->
RQL.TableName 'MySQL ->
TableInfo 'MySQL ->
G.Name ->
DelPermInfo 'MySQL ->
Maybe (SelPermInfo 'MySQL) ->
m [FieldParser n (MutationRootField UnpreparedValue UnpreparedValue)]
buildTableDeleteMutationFields' _sourceName _sourceInfo _tableName _tableInfo _gqlName _delPerns _selPerms =
pure []
buildFunctionQueryFields' ::
MonadBuildSchema 'MySQL r m n =>
SourceName ->
RQL.SourceConfig 'MySQL ->
FunctionName 'MySQL ->
FunctionInfo 'MySQL ->
RQL.TableName 'MySQL ->
SelPermInfo 'MySQL ->
m [FieldParser n (QueryRootField UnpreparedValue UnpreparedValue)]
buildFunctionQueryFields' _ _ _ _ _ _ =
pure []
buildFunctionRelayQueryFields' ::
MonadBuildSchema 'MySQL r m n =>
SourceName ->
RQL.SourceConfig 'MySQL ->
FunctionName 'MySQL ->
FunctionInfo 'MySQL ->
RQL.TableName 'MySQL ->
NESeq (ColumnInfo 'MySQL) ->
SelPermInfo 'MySQL ->
m [(FieldParser n (QueryRootField UnpreparedValue UnpreparedValue))]
buildFunctionRelayQueryFields' _sourceName _sourceInfo _functionName _functionInfo _tableName _pkeyColumns _selPerms =
pure []
buildFunctionMutationFields' ::
MonadBuildSchema 'MySQL r m n =>
SourceName ->
RQL.SourceConfig 'MySQL ->
FunctionName 'MySQL ->
FunctionInfo 'MySQL ->
RQL.TableName 'MySQL ->
SelPermInfo 'MySQL ->
m [FieldParser n (MutationRootField UnpreparedValue UnpreparedValue)]
buildFunctionMutationFields' _ _ _ _ _ _ =
pure []
columnParser' :: (MonadSchema n m, MonadError QErr m) =>
ColumnType 'MySQL ->
G.Nullability ->
m (Parser 'Both n (Opaque (ColumnValue 'MySQL)))
columnParser' columnType (G.Nullability isNullable) =
opaque . fmap (ColumnValue columnType) <$> case columnType of
ColumnScalar scalarType -> case scalarType of
MySQL.Bit -> pure $ possiblyNullable scalarType $ MySQL.BitValue <$> P.boolean
MySQL.String -> pure $ possiblyNullable scalarType $ MySQL.VarcharValue <$> P.string
MySQL.Decimal -> pure $ possiblyNullable scalarType $ MySQL.DecimalValue <$> P.float
MySQL.Double -> pure $ possiblyNullable scalarType $ MySQL.DoubleValue <$> P.float
MySQL.Float -> pure $ possiblyNullable scalarType $ MySQL.FloatValue <$> P.float
MySQL.Int24 -> pure $ possiblyNullable scalarType $ MySQL.MediumValue <$> P.int
MySQL.LongLong -> pure $ possiblyNullable scalarType $ MySQL.BigValue <$> P.int
MySQL.Long -> pure $ possiblyNullable scalarType $ MySQL.IntValue <$> P.int
MySQL.Short -> pure $ possiblyNullable scalarType $ MySQL.SmallValue <$> P.int
MySQL.Tiny -> pure $ possiblyNullable scalarType $ MySQL.TinyValue <$> P.int
_ -> pure $ possiblyNullable scalarType $ MySQL.NullValue <$ P.string -- TODO: Complete this
ColumnEnumReference (EnumReference tableName enumValues) ->
case nonEmpty (HM.toList enumValues) of
Just enumValuesList -> do
tableGQLName <- tableGraphQLName @'MySQL tableName `onLeft` throwError
let enumName = tableGQLName <> $$(G.litName "_enum")
pure $ possiblyNullable MySQL.VarChar $ P.enum enumName Nothing (mkEnumValue <$> enumValuesList)
Nothing -> throw400 ValidationFailed "empty enum values"
where
opaque :: MonadParse m => Parser 'Both m a -> Parser 'Both m (Opaque a)
opaque parser = parser
{ pParser = \case
P.GraphQLValue (G.VVariable var@Variable{ vInfo, vValue }) -> do
typeCheck False (P.toGraphQLType $ pType parser) var
P.mkOpaque (Just vInfo) <$> pParser parser (absurd <$> vValue)
value -> P.mkOpaque Nothing <$> pParser parser value
}
possiblyNullable :: (MonadParse m) => MySQL.Type -> Parser 'Both m MySQL.ScalarValue -> Parser 'Both m MySQL.ScalarValue
possiblyNullable _scalarType
| isNullable = fmap (fromMaybe MySQL.NullValue) . P.nullable
| otherwise = id
mkEnumValue :: (EnumValue, EnumValueInfo) -> (P.Definition P.EnumValueInfo, RQL.ScalarValue 'MySQL)
mkEnumValue (RQL.EnumValue value, EnumValueInfo description) =
( P.mkDefinition value (G.Description <$> description) P.EnumValueInfo
, MySQL.VarcharValue $ G.unName value
)
throughJSON scalarName =
let schemaType = P.NonNullable $ P.TNamed $ P.mkDefinition scalarName Nothing P.TIScalar
in Parser
{ pType = schemaType
, pParser =
valueToJSON (P.toGraphQLType schemaType) >=>
either (parseErrorWith ParseFailed . qeError) pure . runAesonParser J.parseJSON
}
jsonPathArg' ::
MonadParse n =>
ColumnType 'MySQL ->
InputFieldsParser n (Maybe (IR.ColumnOp 'MySQL))
jsonPathArg' _columnType = pure Nothing
orderByOperators' :: NonEmpty (Definition P.EnumValueInfo, (BasicOrderType 'MySQL, NullsOrderType 'MySQL))
orderByOperators' =
NE.fromList
[ ( define $$(G.litName "asc") "in ascending order, nulls first"
, (MySQL.Asc, MySQL.NullsFirst)
)
, ( define $$(G.litName "asc_nulls_first") "in ascending order, nulls first"
, (MySQL.Asc, MySQL.NullsFirst)
)
, ( define $$(G.litName "asc_nulls_last") "in ascending order, nulls last"
, (MySQL.Asc, MySQL.NullsLast)
)
, ( define $$(G.litName "desc") "in descending order, nulls last"
, (MySQL.Desc, MySQL.NullsLast)
)
, ( define $$(G.litName "desc_nulls_first") "in descending order, nulls first"
, (MySQL.Desc, MySQL.NullsFirst)
)
, ( define $$(G.litName "desc_nulls_last") "in descending order, nulls last"
, (MySQL.Desc, MySQL.NullsLast)
)
]
where
define name desc = P.mkDefinition name (Just desc) P.EnumValueInfo
-- | TODO: Make this as thorough as the one for MSSQL/PostgreSQL
comparisonExps' ::
forall m n. (BackendSchema 'MySQL, MonadSchema n m, MonadError QErr m) =>
ColumnType 'MySQL ->
m (Parser 'Input n [ComparisonExp 'MySQL])
comparisonExps' = P.memoize 'comparisonExps $ \columnType -> do
-- see Note [Columns in comparison expression are never nullable]
typedParser <- columnParser columnType (G.Nullability False)
nullableTextParser <- columnParser (ColumnScalar @'MySQL MySQL.VarChar) (G.Nullability True)
textParser <- columnParser (ColumnScalar @'MySQL MySQL.VarChar) (G.Nullability False)
let name = P.getName typedParser <> $$(G.litName "_MySQL_comparison_exp")
desc = G.Description $ "Boolean expression to compare columns of type "
<> P.getName typedParser
<<> ". All fields are combined with logical 'AND'."
textListParser = P.list textParser `P.bind` traverse P.openOpaque
columnListParser = P.list typedParser `P.bind` traverse P.openOpaque
pure $ P.object name (Just desc) $ catMaybes <$> sequenceA
[ P.fieldOptional $$(G.litName "_is_null") Nothing (bool ANISNOTNULL ANISNULL <$> P.boolean)
, P.fieldOptional $$(G.litName "_eq") Nothing (AEQ True . mkParameter <$> typedParser)
, P.fieldOptional $$(G.litName "_neq") Nothing (ANE True . mkParameter <$> typedParser)
, P.fieldOptional $$(G.litName "_gt") Nothing (AGT . mkParameter <$> typedParser)
, P.fieldOptional $$(G.litName "_lt") Nothing (ALT . mkParameter <$> typedParser)
, P.fieldOptional $$(G.litName "_gte") Nothing (AGTE . mkParameter <$> typedParser)
, P.fieldOptional $$(G.litName "_lte") Nothing (ALTE . mkParameter <$> typedParser)
]
offsetParser' :: MonadParse n => Parser 'Both n (SQLExpression 'MySQL)
offsetParser' =
MySQL.ValueExpression . MySQL.BigValue . fromIntegral <$> P.int
-- | Various update operators
updateOperators' ::
Applicative m =>
-- | qualified name of the table
TableInfo 'MySQL ->
-- | update permissions of the table
UpdPermInfo 'MySQL ->
m (Maybe (InputFieldsParser n [(RQL.Column 'MySQL, IR.UpdOpExpG (UnpreparedValue 'MySQL))]))
updateOperators' _table _updatePermissions = pure Nothing

View File

@ -0,0 +1,17 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Hasura.Backends.MySQL.Instances.Transport where
import Hasura.Backends.MySQL.Instances.Execute ()
import Hasura.GraphQL.Transport.Backend
import Hasura.Prelude
import Hasura.RQL.Types
instance BackendTransport 'MySQL where
runDBQuery = error "MySQL backend does not support this operation yet."
runDBQueryExplain = error "MySQL backend does not support this operation yet."
runDBMutation = error "MySQL backend does not support this operation yet."
runDBSubscription = error "MySQL backend does not support this operation yet."

View File

@ -0,0 +1,110 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Hasura.Backends.MySQL.Instances.Types where
import qualified Data.Aeson as J
import qualified Database.MySQL.Base.Types as MySQL
import qualified Hasura.Backends.MySQL.Types as MySQL
import Hasura.Base.Error
import Hasura.Prelude
import Hasura.RQL.DDL.Headers ()
import Hasura.RQL.Types.Backend
import Hasura.SQL.Backend
import qualified Language.GraphQL.Draft.Syntax as G
instance Arbitrary Void where
arbitrary = error "MySQL backend does not support this operation yet."
instance Backend 'MySQL where
type SourceConfig 'MySQL = MySQL.SourceConfig
type SourceConnConfiguration 'MySQL = MySQL.ConnSourceConfig
type Identifier 'MySQL = Void
type TableName 'MySQL = MySQL.TableName
type FunctionName 'MySQL = Void -- MySQL.FunctionName
type FunctionArgType 'MySQL = Void
type ConstraintName 'MySQL = MySQL.ConstraintName
type BasicOrderType 'MySQL = MySQL.Order
type NullsOrderType 'MySQL = MySQL.NullsOrder
type CountType 'MySQL = Void -- MySQL.Countable MySQL.ColumnName
type Column 'MySQL = MySQL.Column
type ScalarValue 'MySQL = MySQL.ScalarValue
type ScalarType 'MySQL = MySQL.ScalarType -- DB.Type
type SQLExpression 'MySQL = MySQL.Expression
type SQLOperator 'MySQL = Void -- MySQL.Op
type BooleanOperators 'MySQL = Const Void
type XComputedField 'MySQL = Void
type XRelay 'MySQL = Void
type XNodesAgg 'MySQL = XEnable
type ExtraTableMetadata 'MySQL = ()
functionArgScalarType :: FunctionArgType 'MySQL -> ScalarType 'MySQL
functionArgScalarType = error "functionArgScalarType: not implemented yet"
isComparableType :: ScalarType 'MySQL -> Bool
isComparableType = isNumType @'MySQL -- TODO: For now we only allow comparisons for numeric types
isNumType :: ScalarType 'MySQL -> Bool
isNumType = \case
MySQL.Decimal -> True
MySQL.Tiny -> True
MySQL.Short -> True
MySQL.Long -> True
MySQL.Float -> True
MySQL.Double -> True
MySQL.Null -> False
MySQL.Timestamp -> False
MySQL.LongLong -> True
MySQL.Int24 -> True
MySQL.Date -> False
MySQL.Time -> False
MySQL.DateTime -> False
MySQL.Year -> False
MySQL.NewDate -> False
MySQL.VarChar -> False
MySQL.Bit -> False
MySQL.NewDecimal -> True
MySQL.Enum -> False
MySQL.Set -> False
MySQL.TinyBlob -> False
MySQL.MediumBlob -> False
MySQL.LongBlob -> False
MySQL.Blob -> False
MySQL.VarString -> False
MySQL.String -> False
MySQL.Geometry -> False
MySQL.Json -> False
textToScalarValue :: Maybe Text -> ScalarValue 'MySQL
textToScalarValue = error "MySQL backend does not support this operation yet."
parseScalarValue :: ScalarType 'MySQL -> J.Value -> Either QErr (ScalarValue 'MySQL)
parseScalarValue = error "MySQL backend does not support this operation yet."
scalarValueToJSON :: ScalarValue 'MySQL -> J.Value
scalarValueToJSON = error "MySQL backend does not support this operation yet."
functionToTable :: FunctionName 'MySQL -> TableName 'MySQL
functionToTable = error "MySQL backend does not support this operation yet."
tableToFunction :: TableName 'MySQL -> FunctionName 'MySQL
tableToFunction = error "MySQL backend does not support this operation yet."
tableGraphQLName :: TableName 'MySQL -> Either QErr G.Name
tableGraphQLName MySQL.TableName{..} =
let gName = schema <> "_" <> name
in (G.mkName gName)
`onNothing`
throw400 ValidationFailed ("TableName " <> gName <> " is not a valid GraphQL identifier")
functionGraphQLName :: FunctionName 'MySQL -> Either QErr G.Name
functionGraphQLName = error "MySQL backend does not support this operation yet."
scalarTypeGraphQLName :: ScalarType 'MySQL -> Either QErr G.Name
scalarTypeGraphQLName = error "MySQL backend does not support this operation yet."
snakeCaseTableName :: TableName 'MySQL -> Text
snakeCaseTableName = error "MySQL backend does not support this operation yet."

View File

@ -0,0 +1,209 @@
module Hasura.Backends.MySQL.Meta where
import Control.Exception (throw)
import qualified Data.ByteString.Char8 as B8
import Data.FileEmbed (embedFile, makeRelativeToProject)
import qualified Data.HashMap.Strict as HM
import qualified Data.HashSet as HS
import qualified Data.Sequence.NonEmpty as SNE
import Data.String (fromString)
import Database.MySQL.Base (Connection)
import Database.MySQL.Base.Types (Field (..))
import Database.MySQL.Simple (Only (Only), query)
import Database.MySQL.Simple.QueryResults (QueryResults (..), convertError)
import Database.MySQL.Simple.Result (Result, ResultError (..), convert)
import Hasura.Backends.MySQL.Instances.Types ()
import Hasura.Backends.MySQL.Types
import Hasura.Prelude
import Hasura.RQL.Types.Column
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.Table
import Hasura.SQL.Backend
import qualified Language.GraphQL.Draft.Syntax as G
getMetadata :: ConnSourceConfig -> Connection -> IO (DBTablesMetadata 'MySQL)
getMetadata ConnSourceConfig{_cscDatabase} scConnection = do
let sql = $(makeRelativeToProject "src-rsr/mysql_table_metadata.sql" >>= embedFile)
results :: [InformationSchema] <- query scConnection (fromString . B8.unpack $ sql) (Only _cscDatabase)
pure (mkMetadata results)
mkMetadata :: [InformationSchema] -> DBTablesMetadata 'MySQL
mkMetadata = foldr mergeMetadata HM.empty
mergeMetadata :: InformationSchema -> DBTablesMetadata 'MySQL -> DBTablesMetadata 'MySQL
mergeMetadata InformationSchema{..} =
HM.insertWith
mergeDBTableMetadata
(TableName isTableName isTableSchema) $
DBTableMetadata
{ _ptmiOid = OID 0
, _ptmiColumns =
[ RawColumnInfo
{ prciName = Column $ fromMaybe "" isColumnName
, prciPosition = fromIntegral isOrdinalPosition
, prciType = parseMySQLScalarType isColumnType -- TODO: This needs to become more precise by considering Field length and character-set
, prciIsNullable = isIsNullable == "YES" -- ref: https://dev.mysql.com/doc/refman/8.0/en/information-schema-columns-table.html
, prciDescription = Just $ G.Description isColumnComment
}
]
, _ptmiPrimaryKey = if isColumnKey == PRI
then Just $
PrimaryKey
(Constraint
(ConstraintName $ fromMaybe "" isConstraintName)
(OID $ fromIntegral $ fromMaybe 0 isConstraintOrdinalPosition))
(SNE.singleton (Column $ fromMaybe "" isColumnName))
else Nothing
, _ptmiUniqueConstraints = if isColumnKey == UNI
then HS.singleton
(Constraint
(ConstraintName $ fromMaybe "" isConstraintName)
(OID $ fromIntegral $ fromMaybe 0 isConstraintOrdinalPosition))
else HS.empty
, _ptmiForeignKeys = if isColumnKey == MUL
then HS.singleton
(ForeignKeyMetadata
(ForeignKey
(Constraint
(ConstraintName $ fromMaybe "" isConstraintName)
(OID $ fromIntegral $ fromMaybe 0 isConstraintOrdinalPosition))
(TableName
(fromMaybe "" isReferencedTableName)
(fromMaybe "" isReferencedTableSchema))
(HM.singleton
(Column $ fromMaybe "" isColumnName)
(Column $ fromMaybe "" isReferencedColumnName))
)
)
else HS.empty
, _ptmiViewInfo = Nothing
, _ptmiDescription = Nothing
, _ptmiExtraTableMetadata = ()
}
mergeDBTableMetadata :: DBTableMetadata 'MySQL -> DBTableMetadata 'MySQL -> DBTableMetadata 'MySQL
mergeDBTableMetadata new existing =
DBTableMetadata
{ _ptmiOid = OID 0
, _ptmiColumns = _ptmiColumns existing <> _ptmiColumns new
, _ptmiPrimaryKey = _ptmiPrimaryKey existing <|> _ptmiPrimaryKey new -- Only one column can be a PRIMARY KEY, so this is just a courtesy choice.
, _ptmiUniqueConstraints = _ptmiUniqueConstraints existing <> _ptmiUniqueConstraints new -- union
, _ptmiForeignKeys = _ptmiForeignKeys existing <> _ptmiForeignKeys new -- union
, _ptmiViewInfo = _ptmiViewInfo existing <|> _ptmiViewInfo new
, _ptmiDescription = _ptmiDescription existing <|> _ptmiDescription new
, _ptmiExtraTableMetadata = ()
}
data InformationSchema
= InformationSchema
{ isTableSchema :: !Text
, isTableName :: !Text
, isColumnName :: !(Maybe Text)
, isOrdinalPosition :: !Word
, isColumnDefault :: !(Maybe Text)
, isIsNullable :: !Text
, isDataType :: !(Maybe Text)
, isColumnType :: !Text
, isColumnKey :: !InformationSchemaColumnKey
, isColumnComment :: !Text
, isConstraintName :: !(Maybe Text)
, isConstraintOrdinalPosition :: !(Maybe Word)
, isPositionInUniqueConstraint :: !(Maybe Word)
, isReferencedTableSchema :: !(Maybe Text)
, isReferencedTableName :: !(Maybe Text)
, isReferencedColumnName :: !(Maybe Text)
} deriving (Show, Eq, Generic)
instance QueryResults InformationSchema where
convertResults
[ fisTableSchema
, fisTableName
, fisColumnName
, fisOrdinalPosition
, fisColumnDefault
, fisIsNullable
, fisDataType
, fisColumnType
, fisColumnKey
, fisColumnComment
, fisConstraintName
, fisConstraintOrdinalPosition
, fisPositionInUniqueConstraint
, fisReferencedTableSchema
, fisReferencedTableName
, fisReferencedColumnName
]
[ visTableSchema
, visTableName
, visColumnName
, visOrdinalPosition
, visColumnDefault
, visIsNullable
, visDataType
, visColumnType
, visColumnKey
, visColumnComment
, visConstraintName
, visConstraintOrdinalPosition
, visPositionInUniqueConstraint
, visReferencedTableSchema
, visReferencedTableName
, visReferencedColumnName
]
= InformationSchema
(convert fisTableSchema visTableSchema )
(convert fisTableName visTableName )
(convert fisColumnName visColumnName )
(convert fisOrdinalPosition visOrdinalPosition )
(convert fisColumnDefault visColumnDefault )
(convert fisIsNullable visIsNullable )
(convert fisDataType visDataType )
(convert fisColumnType visColumnType )
(convert fisColumnKey visColumnKey )
(convert fisColumnComment visColumnComment )
(convert fisConstraintName visConstraintName )
(convert fisConstraintOrdinalPosition visConstraintOrdinalPosition )
(convert fisPositionInUniqueConstraint visPositionInUniqueConstraint)
(convert fisReferencedTableSchema visReferencedTableSchema )
(convert fisReferencedTableName visReferencedTableName )
(convert fisReferencedColumnName visReferencedColumnName )
convertResults fs vs = convertError fs vs 16
-- ^ 'convertError' takes the number of expected columns for conversion as its third argument
data InformationSchemaColumnKey
= PRI
| UNI
| MUL
| BLANK -- ^ This field isn't NULLable and uses empty strings, by the looks of it.
deriving (Show, Read, Eq, Generic)
instance Result InformationSchemaColumnKey where
-- | ref: https://hackage.haskell.org/package/mysql-simple-0.4.5/docs/Database-MySQL-Simple-Result.html#v:convert
-- specifies that the function is expected to throw a 'Database.MySQL.Simple.Result.ResultError'
convert f mbs =
case mbs of
Nothing ->
throw $
UnexpectedNull
(show $ fieldType f)
"InformationSchemaColumnKey"
(B8.unpack $ fieldName f)
"COLUMN_KEY in INFORMATION_SCHEMA cannot be NULL"
Just bs -> case bs of
-- Could have used 'readMaybe' here, but we need the specific errors.
"PRI" -> PRI -- ^ primay key
"UNI" -> UNI -- ^ unique key
"MUL" -> MUL -- ^ foreign key (`MUL`tiple allowed, non-unique key)
"" -> BLANK
x ->
throw $
ConversionFailed
(show $ fieldType f)
"InformationSchemaColumnKey"
(B8.unpack $ fieldName f)
("COLUMN_KEY in INFORMATION_SCHEMA has value extraneous to the expected ENUM: " <> B8.unpack x)

View File

@ -8,23 +8,61 @@ module Hasura.Backends.MySQL.Types where
import qualified Data.Aeson as J
import qualified Data.Aeson.Casing as J
import qualified Data.Aeson.TH as J
import Data.ByteString
import Data.Data
import Data.Hashable
import Data.Int
import Data.Pool
import Data.Set
import Data.Text.Encoding (decodeUtf8With, encodeUtf8)
import Data.Text.Encoding.Error (lenientDecode)
import Data.Text.Extended (ToTxt (..))
import Data.Time.Calendar
import Data.Time.Clock
import Data.Time.LocalTime
import Data.Word (Word16)
import Database.MySQL.Base
import qualified Database.MySQL.Base.Types as MySQLTypes (Type (..))
import Hasura.Base.Error (QErr)
import Hasura.Incremental.Internal.Dependency (Cacheable (..))
import Hasura.Prelude
import Hasura.SQL.Types (ToSQL (..))
import System.IO.Unsafe (unsafePerformIO)
import qualified Text.Builder as TB
data ConnPoolSettings
= ConnPoolSettings
{ _cscIdleTimeout :: !Word
, _cscMaxConnections :: !Word
} deriving (Eq, Show, NFData, Generic, Hashable, Cacheable)
instance Arbitrary ConnPoolSettings where
arbitrary = genericArbitrary
instance Cacheable Word where
unchanged _ = (==)
defaultConnPoolSettings :: ConnPoolSettings
defaultConnPoolSettings =
ConnPoolSettings
{ _cscIdleTimeout = 5
, _cscMaxConnections = 50
}
instance J.FromJSON ConnPoolSettings where
parseJSON = J.withObject "MySQL pool settings" $ \o ->
ConnPoolSettings
<$> o J..:? "max_connections" J..!= _cscMaxConnections defaultConnPoolSettings
<*> o J..:? "idle_timeout" J..!= _cscIdleTimeout defaultConnPoolSettings
$(J.deriveToJSON hasuraJSON ''ConnPoolSettings)
-- | Partial of Database.MySQL.Simple.ConnectInfo
data ConnSourceConfig
= ConnSourceConfig
{ _cscHost :: !Text -- ^ Works with @127.0.0.1@ but not with @localhost@ for some reason
, _cscPort :: !Word16
, _cscUser :: !Text
, _cscPassword :: !Text
, _cscDatabase :: !Text
{ _cscHost :: !Text -- ^ Works with @127.0.0.1@ but not with @localhost@ for some reason
, _cscPort :: !Word16
, _cscUser :: !Text
, _cscPassword :: !Text
, _cscDatabase :: !Text
, _cscPoolSettings :: !ConnPoolSettings
} deriving (Eq, Show, NFData, Generic, Hashable)
$(J.deriveJSON (J.aesonDrop 4 J.snakeCase) {J.omitNothingFields = False} ''ConnSourceConfig)
instance Arbitrary ConnSourceConfig where
@ -52,3 +90,179 @@ instance Cacheable SourceConfig where
unchanged _ = (==)
data TableName
= TableName
{ name :: !Text
, schema :: !Text
} deriving (Show, Eq, Ord, Generic, J.ToJSONKey, J.ToJSON, J.FromJSON, Data, Hashable, Cacheable, NFData)
instance Arbitrary TableName where
arbitrary = genericArbitrary
instance ToTxt TableName where
toTxt TableName{..} = name
data FieldName
= FieldName
{ fName :: !Text
, fNameEntity :: !Text
}
newtype ConstraintName
= ConstraintName
{ unConstraintName :: Text }
deriving newtype (Show, Eq, ToTxt, J.FromJSON, J.ToJSON, Hashable, Cacheable, NFData)
newtype Column
= Column
{ unColumn :: Text }
deriving newtype (Show, Eq, Ord, ToTxt, J.FromJSONKey, J.ToJSONKey, J.FromJSON, J.ToJSON, Hashable, Cacheable, NFData)
deriving (Generic)
instance Arbitrary Column where
arbitrary = genericArbitrary
type ScalarType = MySQLTypes.Type
deriving instance Ord MySQLTypes.Type
deriving instance Generic MySQLTypes.Type
deriving instance J.ToJSON MySQLTypes.Type
deriving instance J.ToJSONKey MySQLTypes.Type
deriving instance J.FromJSON MySQLTypes.Type
deriving instance J.FromJSONKey MySQLTypes.Type
deriving instance Data MySQLTypes.Type
deriving instance NFData MySQLTypes.Type
deriving instance Hashable MySQLTypes.Type
deriving instance Cacheable MySQLTypes.Type
instance ToTxt MySQLTypes.Type where
toTxt = tshow
-- | ref: https://dev.mysql.com/doc/c-api/8.0/en/c-api-data-structures.html
--
-- DB has CHAR, BINARY, VARCHAR and VARBINARY
-- C API only has STRING and VARSTRING
-- Database.MySQL.Base.Types.Type has String, VarString and VarChar for some reason
--
parseMySQLScalarType :: Text -> ScalarType
parseMySQLScalarType = \case
"BIGINT" -> MySQLTypes.LongLong
"BINARY" -> MySQLTypes.String
"BIT" -> MySQLTypes.Bit
"BLOB" -> MySQLTypes.Blob -- TinyBlob, MediumBlob, LongBlob
"CHAR" -> MySQLTypes.String
"DATE" -> MySQLTypes.Date
-- ^ 'NewDate' is obsolete as per: https://dev.mysql.com/doc/dev/connector-net/6.10/html/T_MySql_Data_MySqlClient_MySqlDbType.htm
"DATETIME" -> MySQLTypes.DateTime
"DECIMAL" -> MySQLTypes.Decimal
-- ^ Sticking with 'Decimal' here, until we unearth what 'NewDecimal' is.
"DOUBLE" -> MySQLTypes.Double
"ENUM" -> MySQLTypes.Enum
"FLOAT" -> MySQLTypes.Float
"GEOMETRYCOLLECTION" -> MySQLTypes.Geometry
"GEOMETRY" -> MySQLTypes.Geometry -- For all Geometry types. TODO: Check how to distinguish between these types when it becomes necessary
"INT" -> MySQLTypes.Long
"JSON" -> MySQLTypes.Json
"LINESTRING" -> MySQLTypes.Geometry -- For now Geometry could be considered as Text
"MEDIUMINT" -> MySQLTypes.Int24
"MULTILINESTRING" -> MySQLTypes.Geometry
"MULTIPOINT" -> MySQLTypes.Geometry
"MULTIPOLYGON" -> MySQLTypes.Geometry
"NULL" -> MySQLTypes.Null -- Not a column type, but we retain it as part of this definition to enumerate all possible types
"NUMERIC" -> MySQLTypes.Decimal -- Or NewDecimal
"POINT" -> MySQLTypes.Geometry
"POLYGON" -> MySQLTypes.Geometry
"SET" -> MySQLTypes.Set
"SMALLINT" -> MySQLTypes.Short
"TEXT" -> MySQLTypes.Blob
"TIME" -> MySQLTypes.Time
"TIMESTAMP" -> MySQLTypes.Timestamp
"TINYINT" -> MySQLTypes.Tiny
"VARBINARY" -> MySQLTypes.VarString
"VARCHAR" -> MySQLTypes.VarChar
"YEAR" -> MySQLTypes.Year
_ -> MySQLTypes.Null
data ScalarValue
= BigValue !Int32 -- Not (!Int64) due to scalar-representation
| BinaryValue !ByteString
| BitValue !Bool
| BlobValue !ByteString
| CharValue !Text
| DatetimeValue !UTCTime
| DateValue !Day
| DecimalValue !Double -- Not (!Decimal) due to scalar-representation
| DoubleValue !Double
| EnumValue !Text
| FloatValue !Double -- Not (!Float) due to scalar-representation
| GeometrycollectionValue !Text -- TODO
| GeometryValue !Text -- TODO
| IntValue !Int32
| JsonValue !J.Value
| LinestringValue !Text -- TODO
| MediumValue !Int32 -- (actually, 3-bytes)
| MultilinestringValue !Text -- TODO
| MultipointValue !Text -- TODO
| MultipolygonValue !Text -- TODO
| NullValue
| NumericValue !Double -- Not (!Decimal) due to scalar-representation -- TODO: Double check
| PointValue !Text -- TODO
| PolygonValue !Text -- TODO
| SetValue !(Set Text)
| SmallValue !Int32 -- Not (!Int16) due to scalar-representation
| TextValue !Text
| TimestampValue !UTCTime
| TimeValue !TimeOfDay
| TinyValue !Int32 -- Not (!Int8) due to scalar-representation
| UnknownValue !Text
| VarbinaryValue !ByteString
| VarcharValue !Text
| YearValue !Word16 -- (4-digit year)
deriving (Show, Read, Eq, Ord, Generic, J.ToJSON, J.ToJSONKey, J.FromJSON, Data, NFData, Cacheable)
instance Hashable ScalarValue where
hashWithSalt i = hashWithSalt i . tshow
instance ToTxt ScalarValue where
toTxt = tshow
instance J.ToJSON ByteString where
toJSON = J.String . decodeUtf8With lenientDecode
instance J.FromJSON ByteString where
parseJSON = J.withText "ByteString" (pure . encodeUtf8)
parseScalarValue :: ScalarType -> Text -> Either QErr (ScalarValue)
parseScalarValue = error "parseScalarValue is yet to be implemented."
data Order
= Asc
| Desc
deriving (Show, Eq, Ord, Generic, J.FromJSON, J.ToJSON, Hashable, Cacheable, NFData)
data NullsOrder
= NullsFirst
| NullsLast
| NullsAnyOrder
deriving (Show, Eq, Ord, Generic, J.FromJSON, J.ToJSON, Hashable, Cacheable, NFData)
data OrderBy = OrderBy
{ orderByFieldName :: FieldName
, orderByOrder :: Order
, orderByNullsOrder :: NullsOrder
, orderByType :: Maybe ScalarType
}
data Expression
= ValueExpression ScalarValue
deriving (Show, Eq, Generic, Data, Hashable, Cacheable, NFData)
instance J.ToJSON Expression where
toJSON (ValueExpression scalarValue) = J.toJSON scalarValue
instance J.FromJSON Expression where
parseJSON value = ValueExpression <$> J.parseJSON value
instance ToSQL Expression where
toSQL = TB.text . tshow

View File

@ -2,6 +2,7 @@
module Hasura.GraphQL.Execute.Instances (module B) where
import Hasura.Backends.MSSQL.Instances.Execute as B ()
import Hasura.Backends.Postgres.Instances.Execute as B ()
import Hasura.Backends.BigQuery.Instances.Execute as B ()
import Hasura.Backends.MSSQL.Instances.Execute as B ()
import Hasura.Backends.MySQL.Instances.Execute as B ()
import Hasura.Backends.Postgres.Instances.Execute as B ()

View File

@ -2,6 +2,7 @@
module Hasura.GraphQL.Schema.Instances (module B) where
import Hasura.Backends.MSSQL.Instances.Schema as B ()
import Hasura.Backends.Postgres.Instances.Schema as B ()
import Hasura.Backends.BigQuery.Instances.Schema as B ()
import Hasura.Backends.MSSQL.Instances.Schema as B ()
import Hasura.Backends.MySQL.Instances.Schema as B ()
import Hasura.Backends.Postgres.Instances.Schema as B ()

View File

@ -2,6 +2,7 @@
module Hasura.GraphQL.Transport.Instances (module B) where
import Hasura.Backends.MSSQL.Instances.Transport as B ()
import Hasura.Backends.Postgres.Instances.Transport as B ()
import Hasura.Backends.BigQuery.Instances.Transport as B ()
import Hasura.Backends.MSSQL.Instances.Transport as B ()
import Hasura.Backends.MySQL.Instances.Transport as B ()
import Hasura.Backends.Postgres.Instances.Transport as B ()

View File

@ -2,6 +2,7 @@
module Hasura.RQL.Types.Instances (module B) where
import Hasura.Backends.MSSQL.Instances.Types as B ()
import Hasura.Backends.Postgres.Instances.Types as B ()
import Hasura.Backends.BigQuery.Instances.Types as B ()
import Hasura.Backends.MSSQL.Instances.Types as B ()
import Hasura.Backends.MySQL.Instances.Types as B ()
import Hasura.Backends.Postgres.Instances.Types as B ()

View File

@ -2,6 +2,7 @@
module Hasura.RQL.Types.Metadata.Instances (module B) where
import Hasura.Backends.MSSQL.Instances.Metadata as B ()
import Hasura.Backends.Postgres.Instances.Metadata as B ()
import Hasura.Backends.BigQuery.Instances.Metadata as B ()
import Hasura.Backends.MSSQL.Instances.Metadata as B ()
import Hasura.Backends.MySQL.Instances.Metadata as B ()
import Hasura.Backends.Postgres.Instances.Metadata as B ()

View File

@ -28,15 +28,16 @@ data BackendType
= Postgres PostgresKind
| MSSQL
| BigQuery
| MySQL
deriving (Show, Eq, Ord)
-- | The name of the backend, as we expect it to appear in our metadata and API.
instance ToTxt BackendType where
toTxt (Postgres Vanilla) = "postgres"
toTxt (Postgres Citus) = "citus"
toTxt MSSQL = "mssql"
toTxt BigQuery = "bigquery"
toTxt MySQL = "mysql"
-- | The FromJSON instance uses this lookup mechanism to avoid having to duplicate and hardcode the
-- backend string. We accept both the short form and the long form of the backend's name.
@ -69,4 +70,5 @@ supportedBackends =
, Postgres Citus
, MSSQL
, BigQuery
, MySQL
]

View File

@ -4,4 +4,5 @@ module Hasura.Server.API.Instances (module B) where
import Hasura.Backends.BigQuery.Instances.API as B ()
import Hasura.Backends.MSSQL.Instances.API as B ()
import Hasura.Backends.MySQL.Instances.API as B ()
import Hasura.Backends.Postgres.Instances.API as B ()

View File

@ -0,0 +1,15 @@
-- This could also be done with making MySQL itself return a non-redundant structure
-- in JSON to simplify the processing within the engine.
SELECT c.TABLE_SCHEMA, c.TABLE_NAME, c.COLUMN_NAME, c.ORDINAL_POSITION, c.COLUMN_DEFAULT,
c.IS_NULLABLE, c.DATA_TYPE, c.COLUMN_TYPE, c.COLUMN_KEY, c.COLUMN_COMMENT,
k.CONSTRAINT_NAME, k.ORDINAL_POSITION, k.POSITION_IN_UNIQUE_CONSTRAINT,
k.REFERENCED_TABLE_SCHEMA, k.REFERENCED_TABLE_NAME, k.REFERENCED_COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS c
LEFT OUTER JOIN
INFORMATION_SCHEMA.KEY_COLUMN_USAGE k
ON c.TABLE_NAME = k.TABLE_NAME AND
c.TABLE_SCHEMA = k.TABLE_SCHEMA AND
c.COLUMN_NAME = k.COLUMN_NAME
WHERE c.TABLE_SCHEMA = ?
ORDER BY c.TABLE_NAME ASC, c.ORDINAL_POSITION ASC
;

View File

@ -0,0 +1,27 @@
description: Replace schema cache (metadata)
url: /v1/metadata
status: 200
response:
message: success
query:
type: replace_metadata
args:
version: 3
sources:
- name: mysql
kind: mysql
configuration:
host: '127.0.0.1'
port: 3306
user: hasura
password: password
database: hasura
pool_settings: {}
tables: []
# - table:
# name: author
# schema: hasura
# - table:
# name: article
# schema: hasura

View File

@ -0,0 +1,3 @@
type: bulk
args: []

View File

@ -0,0 +1,3 @@
type: bulk
args: []

View File

@ -0,0 +1,3 @@
type: bulk
args: []

View File

@ -0,0 +1,3 @@
type: bulk
args: []

View File

@ -8,6 +8,22 @@ pytestmark = pytest.mark.allow_server_upgrade_test
usefixtures = pytest.mark.usefixtures
@pytest.mark.parametrize("transport", ['http', 'websocket'])
@pytest.mark.parametrize("backend", ['mysql'])
@usefixtures('per_class_tests_db_state')
class TestGraphQLQueryBasicMySQL:
# initialize the metadata
def test_replace_metadata(self, hge_ctx, transport):
if transport == 'http':
check_query_f(hge_ctx, self.dir() + '/replace_metadata.yaml')
@classmethod
def dir(cls):
return 'queries/graphql_query/mysql'
@pytest.mark.parametrize("transport", ['http', 'websocket'])
@pytest.mark.parametrize("backend", ['bigquery'])
@usefixtures('per_class_tests_db_state')