mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 08:02:15 +03:00
mssql: support query multiplexing in subscriptions
GitOrigin-RevId: 757ceba2c1cdb1107ce0b0e41d2e70ac795d0d73
This commit is contained in:
parent
78abc5423a
commit
703928de9f
@ -14,6 +14,7 @@ only when there are enough present in the items inventory.
|
||||
|
||||
### Bug fixes and improvements
|
||||
|
||||
- server: support query multiplexing in MSSQL subscriptions
|
||||
- console: add bigquery support (#1000)
|
||||
- cli: add support for bigquery in metadata operations
|
||||
|
||||
|
@ -356,9 +356,9 @@ library
|
||||
|
||||
, Hasura.Backends.MSSQL.Connection
|
||||
, Hasura.Backends.MSSQL.DDL
|
||||
, Hasura.Backends.MSSQL.DDL.BoolExp
|
||||
, Hasura.Backends.MSSQL.DDL.RunSQL
|
||||
, Hasura.Backends.MSSQL.DDL.Source
|
||||
, Hasura.Backends.MSSQL.DDL.BoolExp
|
||||
, Hasura.Backends.MSSQL.FromIr
|
||||
, Hasura.Backends.MSSQL.Instances.Execute
|
||||
, Hasura.Backends.MSSQL.Instances.Metadata
|
||||
@ -367,6 +367,7 @@ library
|
||||
, Hasura.Backends.MSSQL.Instances.Types
|
||||
, Hasura.Backends.MSSQL.Meta
|
||||
, Hasura.Backends.MSSQL.Plan
|
||||
, Hasura.Backends.MSSQL.SQL.Value
|
||||
, Hasura.Backends.MSSQL.ToQuery
|
||||
, Hasura.Backends.MSSQL.Types
|
||||
, Hasura.Backends.MSSQL.Types.Instances
|
||||
@ -571,6 +572,7 @@ library
|
||||
, Hasura.SQL.GeoJSON
|
||||
, Hasura.SQL.Time
|
||||
, Hasura.SQL.Types
|
||||
, Hasura.SQL.Value
|
||||
, Hasura.SQL.WKT
|
||||
, Hasura.Tracing
|
||||
, Network.HTTP.Client.Extended
|
||||
|
@ -1,23 +1,27 @@
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
|
||||
module Hasura.Backends.MSSQL.Instances.Execute (NoMultiplex(..)) where
|
||||
module Hasura.Backends.MSSQL.Instances.Execute (MultiplexedQuery'(..), multiplexRootReselect) where
|
||||
|
||||
import Hasura.Prelude
|
||||
|
||||
import qualified Data.Aeson.Extended as J
|
||||
import qualified Data.Environment as Env
|
||||
import qualified Data.HashMap.Strict.InsOrd as OMap
|
||||
import qualified Data.List.NonEmpty as NE
|
||||
import qualified Data.Text.Extended as T
|
||||
import qualified Database.ODBC.SQLServer as ODBC
|
||||
import qualified Language.GraphQL.Draft.Syntax as G
|
||||
import qualified Network.HTTP.Client as HTTP
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
|
||||
import Data.Text.Extended
|
||||
|
||||
import qualified Hasura.SQL.AnyBackend as AB
|
||||
|
||||
import Hasura.Backends.MSSQL.Connection
|
||||
import Hasura.Backends.MSSQL.FromIr as TSQL
|
||||
import Hasura.Backends.MSSQL.Plan
|
||||
import Hasura.Backends.MSSQL.SQL.Value (toTxtEncodedVal)
|
||||
import Hasura.Backends.MSSQL.ToQuery
|
||||
import Hasura.Backends.MSSQL.Types as TSQL
|
||||
import Hasura.EncJSON
|
||||
import Hasura.GraphQL.Context
|
||||
import Hasura.GraphQL.Execute.Backend
|
||||
@ -29,7 +33,7 @@ import Hasura.Session
|
||||
|
||||
instance BackendExecute 'MSSQL where
|
||||
type PreparedQuery 'MSSQL = Text
|
||||
type MultiplexedQuery 'MSSQL = NoMultiplex
|
||||
type MultiplexedQuery 'MSSQL = MultiplexedQuery'
|
||||
type ExecutionMonad 'MSSQL = ExceptT QErr IO
|
||||
getRemoteJoins = const []
|
||||
|
||||
@ -39,12 +43,12 @@ instance BackendExecute 'MSSQL where
|
||||
mkDBQueryExplain = msDBQueryExplain
|
||||
mkLiveQueryExplain = msDBLiveQueryExplain
|
||||
|
||||
|
||||
-- multiplexed query
|
||||
newtype MultiplexedQuery' = MultiplexedQuery' Reselect
|
||||
|
||||
newtype NoMultiplex = NoMultiplex (G.Name, ODBC.Query)
|
||||
|
||||
instance ToTxt NoMultiplex where
|
||||
toTxt (NoMultiplex (_name, query)) = toTxt query
|
||||
instance T.ToTxt MultiplexedQuery' where
|
||||
toTxt (MultiplexedQuery' reselect) = T.toTxt $ toQueryPretty $ fromReselect reselect
|
||||
|
||||
|
||||
-- query
|
||||
@ -73,9 +77,7 @@ msDBQueryPlan _env _manager _reqHeaders userInfo _directives sourceName sourceCo
|
||||
$ DBStepInfo sourceName sourceConfig (Just queryString) odbcQuery
|
||||
|
||||
msDBQueryExplain
|
||||
:: forall m
|
||||
. ( MonadError QErr m
|
||||
)
|
||||
:: MonadError QErr m
|
||||
=> G.Name
|
||||
-> UserInfo
|
||||
-> SourceName
|
||||
@ -95,18 +97,86 @@ msDBQueryExplain fieldName userInfo sourceName sourceConfig qrf = do
|
||||
$ DBStepInfo sourceName sourceConfig Nothing odbcQuery
|
||||
|
||||
msDBLiveQueryExplain
|
||||
:: ( MonadError QErr m
|
||||
, MonadIO m
|
||||
)
|
||||
:: MonadError QErr m
|
||||
=> LiveQueryPlan 'MSSQL (MultiplexedQuery 'MSSQL) -> m LiveQueryPlanExplanation
|
||||
msDBLiveQueryExplain (LiveQueryPlan plan sourceConfig variables) = do
|
||||
let NoMultiplex (_name, query) = _plqpQuery plan
|
||||
select = withExplain $ QueryPrinter query
|
||||
pool = _mscConnectionPool sourceConfig
|
||||
-- TODO: execute `select` in separate batch
|
||||
-- https://github.com/hasura/graphql-engine-mono/issues/1024
|
||||
_explainInfo <- runJSONPathQuery pool (toQueryFlat select)
|
||||
pure $ LiveQueryPlanExplanation (toTxt query) [] variables
|
||||
msDBLiveQueryExplain (LiveQueryPlan plan _sourceConfig variables) = do
|
||||
let query = _plqpQuery plan
|
||||
-- TODO: execute `select` in separate batch
|
||||
-- https://github.com/hasura/graphql-engine-mono/issues/1024
|
||||
-- select = withExplain $ QueryPrinter query
|
||||
-- pool = _mscConnectionPool sourceConfig
|
||||
-- explainInfo <- runJSONPathQuery pool (toQueryFlat select)
|
||||
pure $ LiveQueryPlanExplanation (T.toTxt query) [] variables
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Producing the correct SQL-level list comprehension to multiplex a query
|
||||
|
||||
-- Problem description:
|
||||
--
|
||||
-- Generate a query that repeats the same query N times but with
|
||||
-- certain slots replaced:
|
||||
--
|
||||
-- [ Select x y | (x,y) <- [..] ]
|
||||
--
|
||||
|
||||
multiplexRootReselect
|
||||
:: [(CohortId, CohortVariables)]
|
||||
-> TSQL.Reselect
|
||||
-> TSQL.Select
|
||||
multiplexRootReselect variables rootReselect =
|
||||
Select
|
||||
{ selectTop = NoTop
|
||||
, selectProjections =
|
||||
[ FieldNameProjection
|
||||
Aliased
|
||||
{ aliasedThing =
|
||||
TSQL.FieldName
|
||||
{fieldNameEntity = rowAlias, fieldName = resultIdAlias}
|
||||
, aliasedAlias = resultIdAlias
|
||||
}
|
||||
, ExpressionProjection
|
||||
Aliased
|
||||
{ aliasedThing =
|
||||
ColumnExpression
|
||||
(TSQL.FieldName
|
||||
{ fieldNameEntity = resultAlias
|
||||
, fieldName = TSQL.jsonFieldName
|
||||
})
|
||||
, aliasedAlias = resultAlias
|
||||
}
|
||||
]
|
||||
, selectFrom =
|
||||
FromOpenJson
|
||||
Aliased
|
||||
{ aliasedThing =
|
||||
OpenJson
|
||||
{ openJsonExpression =
|
||||
ValueExpression (ODBC.TextValue $ lbsToTxt $ J.encode variables)
|
||||
, openJsonWith =
|
||||
NE.fromList
|
||||
[ UuidField resultIdAlias (Just $ IndexPath RootPath 0)
|
||||
, JsonField resultVarsAlias (Just $ IndexPath RootPath 1)
|
||||
]
|
||||
}
|
||||
, aliasedAlias = rowAlias
|
||||
}
|
||||
, selectJoins =
|
||||
[ Join
|
||||
{ joinSource = JoinReselect rootReselect
|
||||
, joinJoinAlias =
|
||||
JoinAlias
|
||||
{ joinAliasEntity = resultAlias
|
||||
, joinAliasField = Just TSQL.jsonFieldName
|
||||
}
|
||||
}
|
||||
]
|
||||
, selectWhere = Where mempty
|
||||
, selectFor =
|
||||
JsonFor ForJson {jsonCardinality = JsonArray, jsonRoot = NoRoot}
|
||||
, selectOrderBy = Nothing
|
||||
, selectOffset = Nothing
|
||||
}
|
||||
|
||||
|
||||
-- mutation
|
||||
|
||||
@ -138,13 +208,18 @@ msDBSubscriptionPlan
|
||||
-> SourceConfig 'MSSQL
|
||||
-> InsOrdHashMap G.Name (QueryDB 'MSSQL (UnpreparedValue 'MSSQL))
|
||||
-> m (LiveQueryPlan 'MSSQL (MultiplexedQuery 'MSSQL))
|
||||
msDBSubscriptionPlan userInfo _sourceName sourceConfig rootFields = do
|
||||
-- WARNING: only keeping the first root field for now!
|
||||
query <- traverse mkQuery $ head $ OMap.toList rootFields
|
||||
let roleName = _uiRole userInfo
|
||||
parameterizedPlan = ParameterizedLiveQueryPlan roleName $ NoMultiplex query
|
||||
msDBSubscriptionPlan UserInfo {_uiSession, _uiRole} _sourceName sourceConfig rootFields = do
|
||||
(reselect, prepareState) <- planMultiplex rootFields _uiSession
|
||||
let PrepareState{sessionVariables, namedArguments, positionalArguments} = prepareState
|
||||
-- TODO: call MSSQL validateVariables
|
||||
-- We need to ensure that the values provided for variables are correct according to MSSQL.
|
||||
-- Without this check an invalid value for a variable for one instance of the subscription will
|
||||
-- take down the entire multiplexed query.
|
||||
let cohortVariables = mkCohortVariables
|
||||
sessionVariables
|
||||
_uiSession
|
||||
(toTxtEncodedVal namedArguments)
|
||||
(toTxtEncodedVal positionalArguments)
|
||||
let parameterizedPlan = ParameterizedLiveQueryPlan _uiRole $ MultiplexedQuery' reselect
|
||||
pure
|
||||
$ LiveQueryPlan parameterizedPlan sourceConfig
|
||||
$ mkCohortVariables mempty mempty mempty mempty
|
||||
where
|
||||
mkQuery = fmap (toQueryFlat . fromSelect) . planNoPlan userInfo
|
||||
$ LiveQueryPlan parameterizedPlan sourceConfig cohortVariables
|
||||
|
@ -6,16 +6,18 @@ import Hasura.Prelude
|
||||
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Database.ODBC.SQLServer as ODBC
|
||||
import qualified Language.GraphQL.Draft.Syntax as G
|
||||
|
||||
import Data.String (fromString)
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Text.Extended
|
||||
import Hasura.RQL.Types.Error as HE
|
||||
|
||||
import qualified Hasura.Logging as L
|
||||
|
||||
import Hasura.Backends.MSSQL.Connection
|
||||
import Hasura.Backends.MSSQL.Instances.Execute
|
||||
import Hasura.Backends.MSSQL.ToQuery
|
||||
import Hasura.EncJSON
|
||||
import Hasura.GraphQL.Execute.Backend
|
||||
import Hasura.GraphQL.Execute.LiveQuery.Plan
|
||||
@ -34,6 +36,14 @@ instance BackendTransport 'MSSQL where
|
||||
runDBMutation = runMutation
|
||||
runDBSubscription = runSubscription
|
||||
|
||||
newtype CohortResult = CohortResult (CohortId, Text)
|
||||
|
||||
instance J.FromJSON CohortResult where
|
||||
parseJSON = J.withObject "CohortResult" \o -> do
|
||||
cohortId <- o J..: "result_id"
|
||||
cohortData <- o J..: "result"
|
||||
pure $ CohortResult (cohortId, cohortData)
|
||||
|
||||
runQuery
|
||||
:: ( MonadIO m
|
||||
, MonadQueryLog m
|
||||
@ -88,25 +98,29 @@ runMutation reqId query fieldName _userInfo logger _sourceConfig tx _genSql = d
|
||||
$ run tx
|
||||
|
||||
runSubscription
|
||||
:: ( MonadIO m
|
||||
)
|
||||
:: MonadIO m
|
||||
=> SourceConfig 'MSSQL
|
||||
-> MultiplexedQuery 'MSSQL
|
||||
-> [(CohortId, CohortVariables)]
|
||||
-> m (DiffTime, Either QErr [(CohortId, B.ByteString)])
|
||||
runSubscription sourceConfig (NoMultiplex (name, query)) variables = do
|
||||
runSubscription sourceConfig (MultiplexedQuery' reselect) variables = do
|
||||
let pool = _mscConnectionPool sourceConfig
|
||||
withElapsedTime $ runExceptT $ for variables $ traverse $ const $
|
||||
fmap toResult $ run $ runJSONPathQuery pool query
|
||||
where
|
||||
toResult :: Text -> B.ByteString
|
||||
toResult = encodeUtf8 . addFieldName
|
||||
|
||||
-- TODO: This should probably be generated from the database or should
|
||||
-- probably return encjson so that encJFromAssocList can be used
|
||||
addFieldName result =
|
||||
"{\"" <> G.unName name <> "\":" <> result <> "}"
|
||||
multiplexed = multiplexRootReselect variables reselect
|
||||
query = toQueryFlat $ fromSelect multiplexed
|
||||
withElapsedTime $ runExceptT $ executeMultiplexedQuery pool query
|
||||
|
||||
executeMultiplexedQuery
|
||||
:: MonadIO m
|
||||
=> MSSQLPool
|
||||
-> ODBC.Query
|
||||
-> ExceptT QErr m [(CohortId, B.ByteString)]
|
||||
executeMultiplexedQuery pool query = do
|
||||
let parseResult r = J.eitherDecodeStrict (encodeUtf8 r) `onLeft` \s -> throw400 ParseFailed (fromString s)
|
||||
convertFromJSON :: [CohortResult] -> [(CohortId, B.ByteString)]
|
||||
convertFromJSON = map \(CohortResult (cid, cresult)) -> (cid, encodeUtf8 cresult)
|
||||
textResult <- run $ runJSONPathQuery pool query
|
||||
parsedResult <- parseResult textResult
|
||||
pure $ convertFromJSON parsedResult
|
||||
|
||||
run :: (MonadIO m, MonadError QErr m) => ExceptT QErr IO a -> m a
|
||||
run action = do
|
||||
|
@ -13,7 +13,6 @@ import qualified Data.Aeson as J
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.HashMap.Strict.InsOrd as OMap
|
||||
import qualified Data.HashSet as Set
|
||||
import qualified Data.List.NonEmpty as NE
|
||||
import qualified Data.Text as T
|
||||
import qualified Database.ODBC.SQLServer as ODBC
|
||||
import qualified Language.GraphQL.Draft.Syntax as G
|
||||
@ -59,21 +58,22 @@ planNoPlan userInfo queryDB = do
|
||||
JsonFor forJson -> JsonFor forJson {jsonRoot = Root "root"}
|
||||
}
|
||||
|
||||
-- planMultiplex ::
|
||||
-- OMap.InsOrdHashMap G.Name (SubscriptionRootFieldMSSQL (GraphQL.UnpreparedValue 'MSSQL))
|
||||
-- -> Either PrepareError Select
|
||||
-- planMultiplex _unpreparedMap =
|
||||
-- let rootFieldMap =
|
||||
-- evalState
|
||||
-- (traverse
|
||||
-- (traverseQueryRootField prepareValueMultiplex)
|
||||
-- unpreparedMap)
|
||||
-- emptyPrepareState
|
||||
-- selectMap <-
|
||||
-- first
|
||||
-- FromIrError
|
||||
-- (runValidate (TSQL.runFromIr (traverse TSQL.fromRootField rootFieldMap)))
|
||||
-- pure (multiplexRootReselect (collapseMap selectMap))
|
||||
planMultiplex
|
||||
:: MonadError QErr m
|
||||
=> OMap.InsOrdHashMap G.Name (QueryDB 'MSSQL (GraphQL.UnpreparedValue 'MSSQL))
|
||||
-> SessionVariables
|
||||
-> m (Reselect, PrepareState)
|
||||
planMultiplex unpreparedMap sessionVariables = do
|
||||
let (rootFieldMap, prepareState) =
|
||||
runState
|
||||
(traverse
|
||||
(traverseQueryDB (prepareValueMultiplex (getSessionVariablesSet sessionVariables)))
|
||||
unpreparedMap)
|
||||
emptyPrepareState
|
||||
selectMap <-
|
||||
runValidate (TSQL.runFromIr (traverse TSQL.fromRootField rootFieldMap))
|
||||
`onLeft` (throw400 NotSupported . tshow)
|
||||
pure (collapseMap selectMap, prepareState)
|
||||
|
||||
-- Plan a query without prepare/exec.
|
||||
-- planNoPlanMap ::
|
||||
@ -119,10 +119,6 @@ globalSessionExpression :: TSQL.Expression
|
||||
globalSessionExpression =
|
||||
ValueExpression (ODBC.TextValue "current_setting('hasura.user')::json")
|
||||
|
||||
-- TODO: real env object.
|
||||
envObjectExpression :: TSQL.Expression
|
||||
envObjectExpression =
|
||||
ValueExpression (ODBC.TextValue "[{\"result_id\":1,\"result_vars\":{\"synthetic\":[10]}}]")
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Resolving values
|
||||
@ -131,14 +127,17 @@ data PrepareError
|
||||
= FromIrError (NonEmpty TSQL.Error)
|
||||
|
||||
data PrepareState = PrepareState
|
||||
{ positionalArguments :: !Integer
|
||||
{ positionalArguments :: ![RQL.ColumnValue 'MSSQL]
|
||||
, namedArguments :: !(HashMap G.Name (RQL.ColumnValue 'MSSQL))
|
||||
, sessionVariables :: !(Set.HashSet SessionVariable)
|
||||
}
|
||||
|
||||
emptyPrepareState :: PrepareState
|
||||
emptyPrepareState =
|
||||
PrepareState {positionalArguments = 0, namedArguments = mempty, sessionVariables = mempty}
|
||||
emptyPrepareState = PrepareState
|
||||
{ positionalArguments = mempty
|
||||
, namedArguments = mempty
|
||||
, sessionVariables = mempty
|
||||
}
|
||||
|
||||
-- | Prepare a value without any query planning; we just execute the
|
||||
-- query with the values embedded.
|
||||
@ -158,22 +157,37 @@ prepareValueNoPlan sessionVariables =
|
||||
pure $ ValueExpression $ ODBC.TextValue value
|
||||
|
||||
-- | Prepare a value for multiplexed queries.
|
||||
prepareValueMultiplex ::
|
||||
GraphQL.UnpreparedValue 'MSSQL
|
||||
prepareValueMultiplex
|
||||
:: Set.HashSet SessionVariable
|
||||
-> GraphQL.UnpreparedValue 'MSSQL
|
||||
-> State PrepareState TSQL.Expression
|
||||
prepareValueMultiplex =
|
||||
prepareValueMultiplex globalVariables =
|
||||
\case
|
||||
GraphQL.UVLiteral x -> pure x
|
||||
GraphQL.UVSession ->
|
||||
pure (JsonQueryExpression globalSessionExpression)
|
||||
GraphQL.UVSession -> do
|
||||
modify' (\s -> s {sessionVariables = sessionVariables s <> globalVariables})
|
||||
pure $ JsonValueExpression
|
||||
(ColumnExpression
|
||||
FieldName
|
||||
{ fieldNameEntity = rowAlias
|
||||
, fieldName = resultVarsAlias
|
||||
})
|
||||
(RootPath `FieldPath` "session")
|
||||
GraphQL.UVSessionVar _typ text -> do
|
||||
modify' (\s -> s {sessionVariables = text `Set.insert` sessionVariables s})
|
||||
pure $ JsonValueExpression globalSessionExpression (FieldPath RootPath (toTxt text))
|
||||
GraphQL.UVParameter mVariableInfo pgcolumnvalue ->
|
||||
pure $ JsonValueExpression
|
||||
(ColumnExpression
|
||||
FieldName
|
||||
{ fieldNameEntity = rowAlias
|
||||
, fieldName = resultVarsAlias
|
||||
})
|
||||
(RootPath `FieldPath` "session" `FieldPath` toTxt text)
|
||||
GraphQL.UVParameter mVariableInfo columnValue ->
|
||||
case fmap GraphQL.getName mVariableInfo of
|
||||
Nothing -> do
|
||||
index <- gets positionalArguments
|
||||
modify' (\s -> s {positionalArguments = index + 1})
|
||||
modify' (\s -> s {
|
||||
positionalArguments = positionalArguments s <> [columnValue] })
|
||||
index <- (toInteger . length) <$> gets positionalArguments
|
||||
pure
|
||||
(JsonValueExpression
|
||||
(ColumnExpression
|
||||
@ -187,76 +201,17 @@ prepareValueMultiplex =
|
||||
(\s ->
|
||||
s
|
||||
{ namedArguments =
|
||||
HM.insert name pgcolumnvalue (namedArguments s)
|
||||
HM.insert name columnValue (namedArguments s)
|
||||
})
|
||||
pure
|
||||
(JsonValueExpression
|
||||
envObjectExpression
|
||||
(ColumnExpression
|
||||
FieldName
|
||||
{ fieldNameEntity = rowAlias
|
||||
, fieldName = resultVarsAlias
|
||||
})
|
||||
(RootPath `FieldPath` "query" `FieldPath` G.unName name))
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Producing the correct SQL-level list comprehension to multiplex a query
|
||||
|
||||
-- Problem description:
|
||||
--
|
||||
-- Generate a query that repeats the same query N times but with
|
||||
-- certain slots replaced:
|
||||
--
|
||||
-- [ Select x y | (x,y) <- [..] ]
|
||||
--
|
||||
|
||||
multiplexRootReselect :: TSQL.Reselect -> TSQL.Select
|
||||
multiplexRootReselect rootReselect =
|
||||
Select
|
||||
{ selectTop = NoTop
|
||||
, selectProjections =
|
||||
[ FieldNameProjection
|
||||
Aliased
|
||||
{ aliasedThing =
|
||||
FieldName
|
||||
{fieldNameEntity = rowAlias, fieldName = resultIdAlias}
|
||||
, aliasedAlias = resultIdAlias
|
||||
}
|
||||
, ExpressionProjection
|
||||
Aliased
|
||||
{ aliasedThing =
|
||||
JsonQueryExpression
|
||||
(ColumnExpression
|
||||
(FieldName
|
||||
{ fieldNameEntity = resultAlias
|
||||
, fieldName = TSQL.jsonFieldName
|
||||
}))
|
||||
, aliasedAlias = resultAlias
|
||||
}
|
||||
]
|
||||
, selectFrom =
|
||||
FromOpenJson
|
||||
Aliased
|
||||
{ aliasedThing =
|
||||
OpenJson
|
||||
{ openJsonExpression = envObjectExpression
|
||||
, openJsonWith =
|
||||
NE.fromList
|
||||
[IntField resultIdAlias, JsonField resultVarsAlias]
|
||||
}
|
||||
, aliasedAlias = rowAlias
|
||||
}
|
||||
, selectJoins =
|
||||
[ Join
|
||||
{ joinSource = JoinReselect rootReselect
|
||||
, joinJoinAlias =
|
||||
JoinAlias
|
||||
{ joinAliasEntity = resultAlias
|
||||
, joinAliasField = Just TSQL.jsonFieldName
|
||||
}
|
||||
}
|
||||
]
|
||||
, selectWhere = Where mempty
|
||||
, selectFor =
|
||||
JsonFor ForJson {jsonCardinality = JsonArray, jsonRoot = NoRoot}
|
||||
, selectOrderBy = Nothing
|
||||
, selectOffset = Nothing
|
||||
}
|
||||
|
||||
resultIdAlias :: T.Text
|
||||
resultIdAlias = "result_id"
|
||||
|
19
server/src-lib/Hasura/Backends/MSSQL/SQL/Value.hs
Normal file
19
server/src-lib/Hasura/Backends/MSSQL/SQL/Value.hs
Normal file
@ -0,0 +1,19 @@
|
||||
module Hasura.Backends.MSSQL.SQL.Value where
|
||||
|
||||
import Hasura.Prelude
|
||||
|
||||
import Hasura.Backends.MSSQL.Types.Internal (Value)
|
||||
import Hasura.SQL.Value (TxtEncodedVal (..))
|
||||
|
||||
import qualified Database.ODBC.SQLServer as ODBC
|
||||
import qualified Hasura.GraphQL.Execute.LiveQuery.Plan as LQP
|
||||
import qualified Hasura.RQL.Types.Column as RQL
|
||||
|
||||
import Hasura.SQL.Backend
|
||||
|
||||
txtEncodedVal :: Value -> TxtEncodedVal
|
||||
txtEncodedVal ODBC.NullValue = TENull
|
||||
txtEncodedVal val = TELit $ tshow val
|
||||
|
||||
toTxtEncodedVal :: forall f. Functor f => f (RQL.ColumnValue 'MSSQL) -> LQP.ValidatedVariables f
|
||||
toTxtEncodedVal = LQP.ValidatedVariables . fmap (txtEncodedVal . RQL.cvValue)
|
@ -7,6 +7,7 @@ module Hasura.Backends.MSSQL.ToQuery
|
||||
( fromSelect
|
||||
, withExplain
|
||||
, fromReselect
|
||||
, toSQL
|
||||
, toQueryFlat
|
||||
, toQueryPretty
|
||||
, fromDelete
|
||||
@ -16,8 +17,6 @@ module Hasura.Backends.MSSQL.ToQuery
|
||||
import Hasura.Prelude hiding (GT, LT)
|
||||
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Lazy as L
|
||||
import qualified Data.Text.Lazy.Builder as L
|
||||
|
||||
import Data.List (intersperse)
|
||||
import Data.String
|
||||
@ -64,7 +63,7 @@ fromExpression =
|
||||
\case
|
||||
JsonQueryExpression e -> "JSON_QUERY(" <+> fromExpression e <+> ")"
|
||||
JsonValueExpression e path ->
|
||||
"JSON_VALUE(" <+> fromExpression e <+> fromPath path <+> ")"
|
||||
"JSON_VALUE(" <+> fromExpression e <+> ", " <+> fromPath path <+> ")"
|
||||
ValueExpression value -> QueryPrinter (toSql value)
|
||||
AndExpression xs ->
|
||||
SepByPrinter
|
||||
@ -120,16 +119,10 @@ fromOp =
|
||||
NLIKE -> "NOT LIKE"
|
||||
|
||||
fromPath :: JsonPath -> Printer
|
||||
fromPath path =
|
||||
", " <+> string path
|
||||
where
|
||||
string = fromExpression .
|
||||
ValueExpression . TextValue . L.toStrict . L.toLazyText . go
|
||||
go =
|
||||
\case
|
||||
RootPath -> "$"
|
||||
IndexPath r i -> go r <> "[" <> L.fromString (show i) <> "]"
|
||||
FieldPath r f -> go r <> ".\"" <> L.fromText f <> "\""
|
||||
fromPath = \case
|
||||
RootPath -> "$"
|
||||
IndexPath r i -> fromPath r <+> "[" <+> fromString (show i) <+> "]"
|
||||
FieldPath r f -> fromPath r <+> ".\"" <+> fromString (T.unpack f) <+> "\""
|
||||
|
||||
fromFieldName :: FieldName -> Printer
|
||||
fromFieldName (FieldName {..}) =
|
||||
@ -344,8 +337,12 @@ fromOpenJson OpenJson {openJsonExpression, openJsonWith} =
|
||||
fromJsonFieldSpec :: JsonFieldSpec -> Printer
|
||||
fromJsonFieldSpec =
|
||||
\case
|
||||
IntField name -> fromNameText name <+> " INT"
|
||||
JsonField name -> fromNameText name <+> " NVARCHAR(MAX) AS JSON"
|
||||
IntField name mPath -> fromNameText name <+> " INT" <+> quote mPath
|
||||
StringField name mPath -> fromNameText name <+> " NVARCHAR(MAX)" <+> quote mPath
|
||||
UuidField name mPath -> fromNameText name <+> " UNIQUEIDENTIFIER" <+> quote mPath
|
||||
JsonField name mPath -> fromJsonFieldSpec (StringField name mPath) <+> " AS JSON"
|
||||
where
|
||||
quote mPath = maybe "" ((\p -> " '" <+> p <+> "'"). fromPath) mPath
|
||||
|
||||
fromTableName :: TableName -> Printer
|
||||
fromTableName TableName {tableName, tableSchema} =
|
||||
|
@ -203,8 +203,10 @@ data OpenJson = OpenJson
|
||||
}
|
||||
|
||||
data JsonFieldSpec
|
||||
= IntField Text
|
||||
| JsonField Text
|
||||
= IntField Text (Maybe JsonPath)
|
||||
| JsonField Text (Maybe JsonPath)
|
||||
| StringField Text (Maybe JsonPath)
|
||||
| UuidField Text (Maybe JsonPath)
|
||||
|
||||
data Aliased a = Aliased
|
||||
{ aliasedThing :: !a
|
||||
|
@ -68,7 +68,7 @@ validateVariables pgExecCtx variableValues = do
|
||||
let valSel = mkValidationSel $ toList variableValues
|
||||
Q.Discard () <- runQueryTx_ $ liftTx $
|
||||
Q.rawQE dataExnErrHandler (Q.fromBuilder $ toSQL valSel) [] False
|
||||
pure . ValidatedVariables $ fmap (txtEncodedPGVal . cvValue) variableValues
|
||||
pure . ValidatedVariables $ fmap (txtEncodedVal . cvValue) variableValues
|
||||
where
|
||||
mkExtr = flip S.Extractor Nothing . toTxtValue
|
||||
mkValidationSel vars =
|
||||
|
@ -241,7 +241,7 @@ mutateAndFetchCols
|
||||
-> [ColumnInfo 'Postgres]
|
||||
-> (MutationCTE, DS.Seq Q.PrepArg)
|
||||
-> Bool
|
||||
-> Q.TxE QErr (MutateResp TxtEncodedPGVal)
|
||||
-> Q.TxE QErr (MutateResp TxtEncodedVal)
|
||||
mutateAndFetchCols qt cols (cte, p) strfyNum = do
|
||||
let mutationTx :: Q.FromRes a => Q.TxE QErr a
|
||||
mutationTx =
|
||||
|
@ -6,8 +6,8 @@ module Hasura.Backends.Postgres.SQL.Value
|
||||
, scientificToInteger
|
||||
, scientificToFloat
|
||||
|
||||
, TxtEncodedPGVal(..)
|
||||
, txtEncodedPGVal
|
||||
, TxtEncodedVal(..)
|
||||
, txtEncodedVal
|
||||
|
||||
, binEncoder
|
||||
, txtEncoder
|
||||
@ -16,6 +16,8 @@ module Hasura.Backends.Postgres.SQL.Value
|
||||
|
||||
import Hasura.Prelude
|
||||
|
||||
import Hasura.SQL.Value (TxtEncodedVal (..))
|
||||
|
||||
import qualified Data.Aeson.Text as AE
|
||||
import qualified Data.Aeson.Types as AT
|
||||
import qualified Data.ByteString as B
|
||||
@ -201,25 +203,8 @@ parsePGValue ty val = case (ty, val) of
|
||||
PGUnknown tyName ->
|
||||
fail $ "A string is expected for type: " ++ T.unpack tyName
|
||||
|
||||
data TxtEncodedPGVal
|
||||
= TENull
|
||||
| TELit !Text
|
||||
deriving (Show, Eq, Generic)
|
||||
|
||||
instance Hashable TxtEncodedPGVal
|
||||
|
||||
instance ToJSON TxtEncodedPGVal where
|
||||
toJSON = \case
|
||||
TENull -> Null
|
||||
TELit t -> String t
|
||||
|
||||
instance FromJSON TxtEncodedPGVal where
|
||||
parseJSON Null = pure TENull
|
||||
parseJSON (String t) = pure $ TELit t
|
||||
parseJSON v = AT.typeMismatch "String" v
|
||||
|
||||
txtEncodedPGVal :: PGScalarValue -> TxtEncodedPGVal
|
||||
txtEncodedPGVal = \case
|
||||
txtEncodedVal :: PGScalarValue -> TxtEncodedVal
|
||||
txtEncodedVal = \case
|
||||
PGValInteger i -> TELit $ tshow i
|
||||
PGValSmallInt i -> TELit $ tshow i
|
||||
PGValBigInt i -> TELit $ tshow i
|
||||
@ -317,7 +302,7 @@ binEncoder = \case
|
||||
PGValUnknown t -> (PTI.auto, Just (TE.encodeUtf8 t, PQ.Text))
|
||||
|
||||
txtEncoder :: PGScalarValue -> S.SQLExp
|
||||
txtEncoder colVal = case txtEncodedPGVal colVal of
|
||||
txtEncoder colVal = case txtEncodedVal colVal of
|
||||
TENull -> S.SENull
|
||||
TELit t -> S.SELit t
|
||||
|
||||
|
@ -25,7 +25,7 @@ import Hasura.SQL.Types
|
||||
-- `SELECT ("row"::table).* VALUES (1, 'Robert', 23) AS "row"`.
|
||||
mkSelectExpFromColumnValues
|
||||
:: (MonadError QErr m)
|
||||
=> QualifiedTable -> [ColumnInfo 'Postgres] -> [ColumnValues 'Postgres TxtEncodedPGVal] -> m S.Select
|
||||
=> QualifiedTable -> [ColumnInfo 'Postgres] -> [ColumnValues 'Postgres TxtEncodedVal] -> m S.Select
|
||||
mkSelectExpFromColumnValues qt allCols = \case
|
||||
[] -> return selNoRows
|
||||
colVals -> do
|
||||
|
@ -146,7 +146,7 @@ insertObject
|
||||
-> PGE.MutationRemoteJoinCtx
|
||||
-> Seq.Seq Q.PrepArg
|
||||
-> Bool
|
||||
-> m (Int, Maybe (ColumnValues 'Postgres TxtEncodedPGVal))
|
||||
-> m (Int, Maybe (ColumnValues 'Postgres TxtEncodedVal))
|
||||
insertObject env singleObjIns additionalColumns remoteJoinCtx planVars stringifyNum = Tracing.trace ("Insert " <> qualifiedObjectToText table) do
|
||||
validateInsert (map fst columns) (map IR._riRelInfo objectRels) (map fst additionalColumns)
|
||||
|
||||
@ -197,7 +197,7 @@ insertObject env singleObjIns additionalColumns remoteJoinCtx planVars stringify
|
||||
_aiDefVals
|
||||
|
||||
withArrRels
|
||||
:: Maybe (ColumnValues 'Postgres TxtEncodedPGVal)
|
||||
:: Maybe (ColumnValues 'Postgres TxtEncodedVal)
|
||||
-> m Int
|
||||
withArrRels colValM = do
|
||||
colVal <- onNothing colValM $ throw400 NotSupported cannotInsArrRelErr
|
||||
@ -207,8 +207,8 @@ insertObject env singleObjIns additionalColumns remoteJoinCtx planVars stringify
|
||||
return $ sum arrInsARows
|
||||
|
||||
asSingleObject
|
||||
:: [ColumnValues 'Postgres TxtEncodedPGVal]
|
||||
-> m (Maybe (ColumnValues 'Postgres TxtEncodedPGVal))
|
||||
:: [ColumnValues 'Postgres TxtEncodedVal]
|
||||
-> m (Maybe (ColumnValues 'Postgres TxtEncodedVal))
|
||||
asSingleObject = \case
|
||||
[] -> pure Nothing
|
||||
[r] -> pure $ Just r
|
||||
@ -325,7 +325,7 @@ mkInsertQ table onConflictM insCols defVals (insCheck, updCheck) = do
|
||||
|
||||
fetchFromColVals
|
||||
:: MonadError QErr m
|
||||
=> ColumnValues 'Postgres TxtEncodedPGVal
|
||||
=> ColumnValues 'Postgres TxtEncodedVal
|
||||
-> [ColumnInfo 'Postgres]
|
||||
-> m [(PGCol, PG.SQLExp)]
|
||||
fetchFromColVals colVal reqCols =
|
||||
|
@ -125,7 +125,7 @@ import Hasura.Session
|
||||
-- Cohort
|
||||
|
||||
newtype CohortId = CohortId { unCohortId :: UUID }
|
||||
deriving (Show, Eq, Hashable, J.ToJSON, Q.FromCol)
|
||||
deriving (Show, Eq, Hashable, J.ToJSON, J.FromJSON, Q.FromCol)
|
||||
|
||||
newCohortId :: (MonadIO m) => m CohortId
|
||||
newCohortId = CohortId <$> liftIO UUID.nextRandom
|
||||
@ -233,14 +233,14 @@ instance Q.ToPrepArg CohortVariablesArray where
|
||||
--
|
||||
-- so if any variable values are invalid, the error will be caught early.
|
||||
|
||||
newtype ValidatedVariables f = ValidatedVariables (f TxtEncodedPGVal)
|
||||
newtype ValidatedVariables f = ValidatedVariables (f TxtEncodedVal)
|
||||
|
||||
deriving instance (Show (f TxtEncodedPGVal)) => Show (ValidatedVariables f)
|
||||
deriving instance (Eq (f TxtEncodedPGVal)) => Eq (ValidatedVariables f)
|
||||
deriving instance (Hashable (f TxtEncodedPGVal)) => Hashable (ValidatedVariables f)
|
||||
deriving instance (J.ToJSON (f TxtEncodedPGVal)) => J.ToJSON (ValidatedVariables f)
|
||||
deriving instance (Semigroup (f TxtEncodedPGVal)) => Semigroup (ValidatedVariables f)
|
||||
deriving instance (Monoid (f TxtEncodedPGVal)) => Monoid (ValidatedVariables f)
|
||||
deriving instance (Show (f TxtEncodedVal)) => Show (ValidatedVariables f)
|
||||
deriving instance (Eq (f TxtEncodedVal)) => Eq (ValidatedVariables f)
|
||||
deriving instance (Hashable (f TxtEncodedVal)) => Hashable (ValidatedVariables f)
|
||||
deriving instance (J.ToJSON (f TxtEncodedVal)) => J.ToJSON (ValidatedVariables f)
|
||||
deriving instance (Semigroup (f TxtEncodedVal)) => Semigroup (ValidatedVariables f)
|
||||
deriving instance (Monoid (f TxtEncodedVal)) => Monoid (ValidatedVariables f)
|
||||
|
||||
type ValidatedQueryVariables = ValidatedVariables (Map.HashMap G.Name)
|
||||
type ValidatedSyntheticVariables = ValidatedVariables []
|
||||
|
25
server/src-lib/Hasura/SQL/Value.hs
Normal file
25
server/src-lib/Hasura/SQL/Value.hs
Normal file
@ -0,0 +1,25 @@
|
||||
module Hasura.SQL.Value where
|
||||
|
||||
import Hasura.Prelude
|
||||
|
||||
|
||||
import qualified Data.Aeson as A
|
||||
import qualified Data.Aeson.Types as AT
|
||||
import qualified Data.Text as T
|
||||
|
||||
data TxtEncodedVal
|
||||
= TENull
|
||||
| TELit !T.Text
|
||||
deriving (Show, Eq, Generic)
|
||||
|
||||
instance Hashable TxtEncodedVal
|
||||
|
||||
instance A.ToJSON TxtEncodedVal where
|
||||
toJSON = \case
|
||||
TENull -> AT.Null
|
||||
TELit t -> AT.String t
|
||||
|
||||
instance A.FromJSON TxtEncodedVal where
|
||||
parseJSON A.Null = pure TENull
|
||||
parseJSON (A.String t) = pure $ TELit t
|
||||
parseJSON v = AT.typeMismatch "String" v
|
12
server/tests-py/queries/explain/schema_setup_mssql.yaml
Normal file
12
server/tests-py/queries/explain/schema_setup_mssql.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
type: bulk
|
||||
args:
|
||||
|
||||
- type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
CREATE TABLE [user](
|
||||
id int identity NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
age INTEGER
|
||||
);
|
@ -0,0 +1,8 @@
|
||||
type: bulk
|
||||
args:
|
||||
- type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
cascade: true
|
||||
sql: |
|
||||
DROP TABLE [user];
|
18
server/tests-py/queries/explain/setup_mssql.yaml
Normal file
18
server/tests-py/queries/explain/setup_mssql.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
type: bulk
|
||||
args:
|
||||
|
||||
- type: mssql_track_table
|
||||
args:
|
||||
source: mssql
|
||||
table:
|
||||
name: user
|
||||
- type: mssql_create_select_permission
|
||||
args:
|
||||
source: mssql
|
||||
table:
|
||||
name: user
|
||||
role: user
|
||||
permission:
|
||||
columns: '*'
|
||||
filter:
|
||||
id: X-Hasura-User-Id
|
2
server/tests-py/queries/explain/teardown_mssql.yaml
Normal file
2
server/tests-py/queries/explain/teardown_mssql.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
type: bulk
|
||||
args: []
|
@ -0,0 +1,14 @@
|
||||
type: bulk
|
||||
args:
|
||||
- type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
CREATE TABLE test(id int identity NOT NULL PRIMARY KEY);
|
||||
create table articles(
|
||||
id int identity NOT NULL PRIMARY KEY,
|
||||
user_id int,
|
||||
content text,
|
||||
title text,
|
||||
is_public bit default 0
|
||||
);
|
@ -0,0 +1,8 @@
|
||||
type: bulk
|
||||
args:
|
||||
- type: mssql_run_sql
|
||||
args:
|
||||
source: mssql
|
||||
sql: |
|
||||
DROP TABLE test;
|
||||
DROP TABLE articles;
|
@ -0,0 +1,39 @@
|
||||
type: bulk
|
||||
args:
|
||||
- type: mssql_track_table
|
||||
args:
|
||||
source: mssql
|
||||
table:
|
||||
name: test
|
||||
- type: mssql_track_table
|
||||
args:
|
||||
source: mssql
|
||||
table:
|
||||
name: articles
|
||||
- type: mssql_create_select_permission
|
||||
args:
|
||||
source: mssql
|
||||
table:
|
||||
name: articles
|
||||
role: public
|
||||
permission:
|
||||
columns:
|
||||
- title
|
||||
- content
|
||||
filter:
|
||||
is_public: 1
|
||||
- type: mssql_create_select_permission
|
||||
args:
|
||||
source: mssql
|
||||
table:
|
||||
name: articles
|
||||
role: user
|
||||
permission:
|
||||
columns:
|
||||
- user_id
|
||||
- title
|
||||
- content
|
||||
- is_public
|
||||
filter:
|
||||
id:
|
||||
_eq: X-Hasura-User-Id
|
@ -0,0 +1,2 @@
|
||||
type: bulk
|
||||
args: []
|
@ -250,7 +250,8 @@ class TestSubscriptionLiveQueries:
|
||||
with pytest.raises(queue.Empty):
|
||||
ev = ws_client.get_ws_event(3)
|
||||
|
||||
@usefixtures('per_method_tests_db_state')
|
||||
@pytest.mark.parametrize("backend", ['mssql', 'postgres'])
|
||||
@usefixtures('per_class_tests_db_state', 'per_backend_tests')
|
||||
class TestSubscriptionMultiplexing:
|
||||
|
||||
@classmethod
|
||||
|
Loading…
Reference in New Issue
Block a user