mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
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:
parent
241a116e8e
commit
385d27449e
@ -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
|
||||
|
@ -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
|
||||
|
17
server/src-lib/Hasura/Backends/MySQL/Instances/API.hs
Normal file
17
server/src-lib/Hasura/Backends/MySQL/Instances/API.hs
Normal 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
|
||||
]
|
21
server/src-lib/Hasura/Backends/MySQL/Instances/Execute.hs
Normal file
21
server/src-lib/Hasura/Backends/MySQL/Instances/Execute.hs
Normal 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."
|
21
server/src-lib/Hasura/Backends/MySQL/Instances/Metadata.hs
Normal file
21
server/src-lib/Hasura/Backends/MySQL/Instances/Metadata.hs
Normal 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."
|
295
server/src-lib/Hasura/Backends/MySQL/Instances/Schema.hs
Normal file
295
server/src-lib/Hasura/Backends/MySQL/Instances/Schema.hs
Normal 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
|
17
server/src-lib/Hasura/Backends/MySQL/Instances/Transport.hs
Normal file
17
server/src-lib/Hasura/Backends/MySQL/Instances/Transport.hs
Normal 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."
|
110
server/src-lib/Hasura/Backends/MySQL/Instances/Types.hs
Normal file
110
server/src-lib/Hasura/Backends/MySQL/Instances/Types.hs
Normal 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."
|
209
server/src-lib/Hasura/Backends/MySQL/Meta.hs
Normal file
209
server/src-lib/Hasura/Backends/MySQL/Meta.hs
Normal 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)
|
@ -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
|
||||
|
@ -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 ()
|
||||
|
@ -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 ()
|
||||
|
@ -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 ()
|
||||
|
@ -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 ()
|
||||
|
@ -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 ()
|
||||
|
@ -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
|
||||
]
|
||||
|
@ -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 ()
|
||||
|
15
server/src-rsr/mysql_table_metadata.sql
Normal file
15
server/src-rsr/mysql_table_metadata.sql
Normal 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
|
||||
;
|
@ -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
|
||||
|
@ -0,0 +1,3 @@
|
||||
type: bulk
|
||||
args: []
|
||||
|
@ -0,0 +1,3 @@
|
||||
type: bulk
|
||||
args: []
|
||||
|
@ -0,0 +1,3 @@
|
||||
type: bulk
|
||||
args: []
|
||||
|
@ -0,0 +1,3 @@
|
||||
type: bulk
|
||||
args: []
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user