mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-18 13:02:11 +03:00
d91029ad51
### Description This PR removes all `fmapX` and `traverseX` functions from RQL.IR, favouring instead `Functor` and `Traversable` instances throughout the code. This was a relatively straightforward change, except for two small pain points: `AnnSelectG` and `AnnInsert`. Both were parametric over two types `a` and `v`, making it impossible to make them traversable functors... But it turns out that in every single use case, `a ~ f v`. By changing those types to take such an `f :: Type -> Type` as an argument instead of `a :: Type` makes it possible to make them functors. The only small difference is for `AnnIns`, I had to introduce one `Identity` transformation for one of the `f` parameters. This is relatively straightforward. ### Notes This PR fixes the most verbose BigQuery hint (`let` instead of `<- pure`). https://github.com/hasura/graphql-engine-mono/pull/1668 GitOrigin-RevId: e632263a8c559aa04aeae10dcaec915b4a81ad1a
233 lines
7.8 KiB
Haskell
233 lines
7.8 KiB
Haskell
-- | Planning T-SQL queries and subscriptions.
|
|
|
|
module Hasura.Backends.MSSQL.Plan where
|
|
-- TODO: Re-add the export list after cleaning up the module
|
|
-- ( planQuery
|
|
-- , planSubscription
|
|
-- ) where
|
|
|
|
import Hasura.Prelude hiding (first)
|
|
|
|
import qualified Data.Aeson as J
|
|
import Data.ByteString.Lazy (toStrict)
|
|
import qualified Data.HashMap.Strict as HM
|
|
import qualified Data.HashMap.Strict.InsOrd as OMap
|
|
import qualified Data.HashSet as Set
|
|
import qualified Data.Text as T
|
|
import qualified Database.ODBC.SQLServer as ODBC
|
|
import qualified Language.GraphQL.Draft.Syntax as G
|
|
|
|
import Control.Monad.Validate
|
|
import Data.Text.Extended
|
|
|
|
import qualified Hasura.GraphQL.Parser as GraphQL
|
|
import qualified Hasura.RQL.Types.Column as RQL
|
|
|
|
import Hasura.Backends.MSSQL.FromIr
|
|
import Hasura.Backends.MSSQL.Types
|
|
import Hasura.Base.Error
|
|
import Hasura.RQL.IR
|
|
import Hasura.SQL.Backend
|
|
import Hasura.Session
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Top-level planner
|
|
|
|
planQuery
|
|
:: MonadError QErr m
|
|
=> SessionVariables
|
|
-> QueryDB 'MSSQL (Const Void) (GraphQL.UnpreparedValue 'MSSQL)
|
|
-> m Select
|
|
planQuery sessionVariables queryDB = do
|
|
rootField <- traverse (prepareValueQuery sessionVariables) queryDB
|
|
sel <-
|
|
runValidate (runFromIr (fromRootField rootField))
|
|
`onLeft` (throw400 NotSupported . tshow)
|
|
pure $
|
|
sel
|
|
{ selectFor =
|
|
case selectFor sel of
|
|
NoFor -> NoFor
|
|
JsonFor forJson ->
|
|
JsonFor forJson {jsonRoot =
|
|
case jsonRoot forJson of
|
|
NoRoot -> Root "root"
|
|
-- Keep whatever's there if already
|
|
-- specified. In the case of an
|
|
-- aggregate query, the root will
|
|
-- be specified "aggregate", for
|
|
-- example.
|
|
keep -> keep
|
|
}
|
|
}
|
|
|
|
-- | Prepare a value without any query planning; we just execute the
|
|
-- query with the values embedded.
|
|
prepareValueQuery
|
|
:: MonadError QErr m
|
|
=> SessionVariables
|
|
-> GraphQL.UnpreparedValue 'MSSQL
|
|
-> m Expression
|
|
prepareValueQuery sessionVariables =
|
|
{- History note:
|
|
This function used to be called 'planNoPlan', and was used for building sql
|
|
expressions for queries. That evolved differently, but this function is now
|
|
left as a *suggestion* for implementing support for mutations.
|
|
-}
|
|
\case
|
|
GraphQL.UVLiteral x -> pure x
|
|
GraphQL.UVSession -> pure $ ValueExpression $ ODBC.ByteStringValue $ toStrict $ J.encode sessionVariables
|
|
GraphQL.UVParameter _ RQL.ColumnValue{..} -> pure $ ValueExpression cvValue
|
|
GraphQL.UVSessionVar _typ sessionVariable -> do
|
|
value <- getSessionVariableValue sessionVariable sessionVariables
|
|
`onNothing` throw400 NotFound ("missing session variable: " <>> sessionVariable)
|
|
pure $ ValueExpression $ ODBC.TextValue value
|
|
|
|
planSubscription
|
|
:: MonadError QErr m
|
|
=> OMap.InsOrdHashMap G.Name (QueryDB 'MSSQL (Const Void) (GraphQL.UnpreparedValue 'MSSQL))
|
|
-> SessionVariables
|
|
-> m (Reselect, PrepareState)
|
|
planSubscription unpreparedMap sessionVariables = do
|
|
let (rootFieldMap, prepareState) =
|
|
runState
|
|
(traverse
|
|
(traverse (prepareValueSubscription (getSessionVariablesSet sessionVariables)))
|
|
unpreparedMap)
|
|
emptyPrepareState
|
|
selectMap <-
|
|
runValidate (runFromIr (traverse fromRootField rootFieldMap))
|
|
`onLeft` (throw400 NotSupported . tshow)
|
|
pure (collapseMap selectMap, prepareState)
|
|
|
|
-- Plan a query without prepare/exec.
|
|
-- planNoPlanMap ::
|
|
-- OMap.InsOrdHashMap G.Name (SubscriptionRootFieldMSSQL (GraphQL.UnpreparedValue 'MSSQL))
|
|
-- -> Either PrepareError Reselect
|
|
-- planNoPlanMap _unpreparedMap =
|
|
-- let rootFieldMap = runIdentity $
|
|
-- traverse (traverseQueryRootField (pure . prepareValueNoPlan)) unpreparedMap
|
|
-- selectMap <-
|
|
-- first
|
|
-- FromIrError
|
|
-- (runValidate (runFromIr (traverse fromRootField rootFieldMap)))
|
|
-- pure (collapseMap selectMap)
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Converting a root field into a T-SQL select statement
|
|
|
|
-- | Collapse a set of selects into a single select that projects
|
|
-- these as subselects.
|
|
collapseMap :: OMap.InsOrdHashMap G.Name Select
|
|
-> Reselect
|
|
collapseMap selects =
|
|
Reselect
|
|
{ reselectFor =
|
|
JsonFor ForJson {jsonCardinality = JsonSingleton, jsonRoot = NoRoot}
|
|
, reselectWhere = Where mempty
|
|
, reselectProjections =
|
|
map projectSelect (OMap.toList selects)
|
|
}
|
|
where
|
|
projectSelect :: (G.Name, Select) -> Projection
|
|
projectSelect (name, sel) =
|
|
ExpressionProjection
|
|
(Aliased
|
|
{ aliasedThing = SelectExpression sel
|
|
, aliasedAlias = G.unName name
|
|
})
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Session variables
|
|
|
|
globalSessionExpression :: Expression
|
|
globalSessionExpression =
|
|
ValueExpression (ODBC.TextValue "current_setting('hasura.user')::json")
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Resolving values
|
|
|
|
data PrepareError
|
|
= FromIrError (NonEmpty Error)
|
|
|
|
data PrepareState = PrepareState
|
|
{ positionalArguments :: ![RQL.ColumnValue 'MSSQL]
|
|
, namedArguments :: !(HashMap G.Name (RQL.ColumnValue 'MSSQL))
|
|
, sessionVariables :: !(Set.HashSet SessionVariable)
|
|
}
|
|
|
|
emptyPrepareState :: PrepareState
|
|
emptyPrepareState = PrepareState
|
|
{ positionalArguments = mempty
|
|
, namedArguments = mempty
|
|
, sessionVariables = mempty
|
|
}
|
|
|
|
|
|
-- | Prepare a value for multiplexed queries.
|
|
prepareValueSubscription
|
|
:: Set.HashSet SessionVariable
|
|
-> GraphQL.UnpreparedValue 'MSSQL
|
|
-> State PrepareState Expression
|
|
prepareValueSubscription globalVariables =
|
|
\case
|
|
GraphQL.UVLiteral x -> pure x
|
|
|
|
GraphQL.UVSession -> do
|
|
modify' (\s -> s {sessionVariables = sessionVariables s <> globalVariables})
|
|
pure $ resultVarExp (RootPath `FieldPath` "session")
|
|
|
|
GraphQL.UVSessionVar _typ text -> do
|
|
modify' (\s -> s {sessionVariables = text `Set.insert` sessionVariables s})
|
|
pure $ resultVarExp (sessionDot $ toTxt text)
|
|
|
|
GraphQL.UVParameter mVariableInfo columnValue ->
|
|
case fmap GraphQL.getName mVariableInfo of
|
|
Nothing -> do
|
|
currentIndex <- (toInteger . length) <$> gets positionalArguments
|
|
modify' (\s -> s {
|
|
positionalArguments = positionalArguments s <> [columnValue] })
|
|
pure (resultVarExp (syntheticIx currentIndex))
|
|
Just name -> do
|
|
modify
|
|
(\s ->
|
|
s
|
|
{ namedArguments =
|
|
HM.insert name columnValue (namedArguments s)
|
|
})
|
|
pure $ resultVarExp (queryDot $ G.unName name)
|
|
|
|
where
|
|
resultVarExp :: JsonPath -> Expression
|
|
resultVarExp =
|
|
JsonValueExpression $
|
|
ColumnExpression $
|
|
FieldName
|
|
{ fieldNameEntity = rowAlias
|
|
, fieldName = resultVarsAlias
|
|
}
|
|
|
|
queryDot :: Text -> JsonPath
|
|
queryDot name = RootPath `FieldPath` "query" `FieldPath` name
|
|
|
|
syntheticIx :: Integer -> JsonPath
|
|
syntheticIx i = (RootPath `FieldPath` "synthetic" `IndexPath` i)
|
|
|
|
sessionDot :: Text -> JsonPath
|
|
sessionDot name = RootPath `FieldPath` "session" `FieldPath` name
|
|
|
|
|
|
resultIdAlias :: T.Text
|
|
resultIdAlias = "result_id"
|
|
|
|
resultVarsAlias :: T.Text
|
|
resultVarsAlias = "result_vars"
|
|
|
|
resultAlias :: T.Text
|
|
resultAlias = "result"
|
|
|
|
rowAlias :: T.Text
|
|
rowAlias = "row"
|