Support query explain for MSSQL (fixes #1024)

Co-authored-by: Abby Sassel <3883855+sassela@users.noreply.github.com>
Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com>
GitOrigin-RevId: 5a57d7570884a5469a947742e4ab9290a0cff55f
This commit is contained in:
Chris Done 2021-05-11 11:04:38 +01:00 committed by hasura-bot
parent 62f69a428a
commit 56c094b299
7 changed files with 103 additions and 33 deletions

View File

@ -8,12 +8,13 @@
- server: fix query execution of custom function containing a composite argument type
- server: fix a bug in query validation that would cause some queries using default variable values to be rejected (fix #6867)
- server: REST endpoint bugfix for UUID url params
- server: custom URI schemes are now supported in CORS config (fix #5818) (#5940)
- server: explaining/analyzing a query now works for mssql sources
- server: fix MSSQL multiplexed subscriptions (fix #6887)
- console: read-only modify page for mssql
- console: filter out partitions from track table list and display partition info
- console: fixes an issue where no schemas are listed on an MSSQL source
- server: REST endpoint bugfix for UUID url params
## v2.0.0-alpha.10

View File

@ -76,6 +76,16 @@ msDBQueryPlan _env _manager _reqHeaders userInfo _directives sourceName sourceCo
. AB.mkAnyBackend
$ DBStepInfo @'MSSQL sourceName sourceConfig (Just queryString) odbcQuery
runShowplan
:: ODBC.Query -> ODBC.Connection -> IO [Text]
runShowplan query conn = do
ODBC.exec conn "SET SHOWPLAN_TEXT ON"
texts <- ODBC.query conn query
ODBC.exec conn "SET SHOWPLAN_TEXT OFF"
-- we don't need to use 'finally' here - if an exception occurs,
-- the connection is removed from the resource pool in 'withResource'.
pure texts
msDBQueryExplain
:: MonadError QErr m
=> G.Name
@ -85,28 +95,33 @@ msDBQueryExplain
-> QueryDB 'MSSQL (UnpreparedValue 'MSSQL)
-> m (AB.AnyBackend DBStepInfo)
msDBQueryExplain fieldName userInfo sourceName sourceConfig qrf = do
select <- withExplain . fromSelect <$> planNoPlan userInfo qrf
let queryString = ODBC.renderQuery $ toQueryPretty select
select <- fromSelect <$> planNoPlan userInfo qrf
let query = toQueryPretty select
queryString = ODBC.renderQuery $ query
pool = _mscConnectionPool sourceConfig
-- TODO: execute `select` in separate batch
-- https://github.com/hasura/graphql-engine-mono/issues/1024
odbcQuery = runJSONPathQuery pool (toQueryFlat select) <&> \explainInfo ->
encJFromJValue $ ExplainPlan fieldName (Just queryString) (Just [explainInfo])
odbcQuery =
withMSSQLPool
pool
(\conn -> do
showplan <- runShowplan query conn
pure (encJFromJValue $
ExplainPlan
fieldName
(Just queryString)
(Just showplan)))
pure
$ AB.mkAnyBackend
$ DBStepInfo @'MSSQL 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 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
msDBLiveQueryExplain (LiveQueryPlan plan sourceConfig variables) = do
let (MultiplexedQuery' reselect) = _plqpQuery plan
query = toQueryPretty $ fromSelect $ multiplexRootReselect [(dummyCohortId, variables)] reselect
pool = _mscConnectionPool sourceConfig
explainInfo <- withMSSQLPool pool (runShowplan query)
pure $ LiveQueryPlanExplanation (T.toTxt query) explainInfo variables
--------------------------------------------------------------------------------
-- Producing the correct SQL-level list comprehension to multiplex a query

View File

@ -5,7 +5,6 @@
module Hasura.Backends.MSSQL.ToQuery
( fromSelect
, withExplain
, fromReselect
, toSQL
, toQueryFlat
@ -178,15 +177,6 @@ fromSelect Select {..} = wrapFor selectFor result
, fromFor selectFor
]
withExplain :: Printer -> Printer
withExplain p =
SepByPrinter
NewlinePrinter
[ "SET SHOWPLAN_TEXT ON"
, p
, "SET SHOWPLAN_TEXT OFF"
]
fromJoinSource :: JoinSource -> Printer
fromJoinSource =
\case

View File

@ -89,6 +89,7 @@ Additional details are provided by the documentation for individual bindings.
-}
module Hasura.GraphQL.Execute.LiveQuery.Plan
( CohortId
, dummyCohortId
, newCohortId
, CohortIdArray(..)
, CohortVariablesArray(..)
@ -108,6 +109,7 @@ import qualified Data.Aeson.Extended as J
import qualified Data.Aeson.TH as J
import qualified Data.HashMap.Strict as Map
import qualified Data.HashSet as Set
import qualified Data.UUID as UUID
import qualified Data.UUID.V4 as UUID
import qualified Database.PG.Query as Q
import qualified Database.PG.Query.PTI as PTI
@ -130,6 +132,9 @@ newtype CohortId = CohortId { unCohortId :: UUID }
newCohortId :: (MonadIO m) => m CohortId
newCohortId = CohortId <$> liftIO UUID.nextRandom
dummyCohortId :: CohortId
dummyCohortId = CohortId UUID.nil
data CohortVariables
= CohortVariables
{ _cvSessionVariables :: !SessionVariables

View File

@ -0,0 +1,32 @@
description: Explain query with permissions
url: /v1/graphql/explain
status: 200
response:
- field: user
plan:
- "SELECT ISNULL((SELECT [t_user1].[id] AS [id],\n [t_user1].[name] AS [name],\n\
\ [t_user1].[age] AS [age]\nFROM [dbo].[user] AS [t_user1]\nWHERE ((((([t_user1].[id])\
\ = ((N'1')))\n OR ((([t_user1].[id]) IS NULL)\n AND (((N'1')) IS NULL)))))\n\
FOR JSON PATH), '[]')"
- " |--Compute Scalar(DEFINE:([Expr1003]=isnull([Expr1001],CONVERT_IMPLICIT(nvarchar(max),'[]',0))))"
- " |--UDX(([t_user1].[id], [t_user1].[name], [t_user1].[age]))"
- " |--Clustered Index Seek(OBJECT:([master].[dbo].[user].[PK__user__3213E83F2F718733]
AS [t_user1]), SEEK:([t_user1].[id]=(1)) ORDERED FORWARD)"
sql:
"SELECT ISNULL((SELECT [t_user1].[id] AS [id],\n [t_user1].[name] AS\
\ [name],\n [t_user1].[age] AS [age]\nFROM [dbo].[user] AS [t_user1]\nWHERE\
\ ((((([t_user1].[id]) = ((N'1')))\n OR ((([t_user1].[id]) IS NULL)\n \
\ AND (((N'1')) IS NULL)))))\nFOR JSON PATH), '[]')"
query:
user:
X-Hasura-Role: user
X-Hasura-User-Id: "1"
query:
query: |
query {
user{
id
name
age
}
}

View File

@ -0,0 +1,27 @@
description: Explain query
url: /v1/graphql/explain
status: 200
response:
- field: user
plan:
- "SELECT ISNULL((SELECT [t_user1].[id] AS [id],\n [t_user1].[name] AS [name],\n\
\ [t_user1].[age] AS [age]\nFROM [dbo].[user] AS [t_user1]\nFOR JSON PATH),\
\ '[]')"
- " |--Compute Scalar(DEFINE:([Expr1003]=isnull([Expr1001],CONVERT_IMPLICIT(nvarchar(max),'[]',0))))"
- " |--UDX(([t_user1].[id], [t_user1].[name], [t_user1].[age]))"
- " |--Clustered Index Scan(OBJECT:([master].[dbo].[user].[PK__user__3213E83F2F718733]
AS [t_user1]))"
sql:
"SELECT ISNULL((SELECT [t_user1].[id] AS [id],\n [t_user1].[name] AS\
\ [name],\n [t_user1].[age] AS [age]\nFROM [dbo].[user] AS [t_user1]\nFOR\
\ JSON PATH), '[]')"
query:
query:
query: |
query {
user{
id
name
age
}
}

View File

@ -875,17 +875,18 @@ class TestUnauthorizedRolePermission:
def test_unauth_role(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/unauthorized_role.yaml', transport, False)
@usefixtures('per_class_tests_db_state')
@pytest.mark.parametrize("backend", ['postgres', 'mssql'])
@usefixtures('per_class_tests_db_state', 'per_backend_tests')
class TestGraphQLExplain:
@classmethod
def dir(cls):
return 'queries/explain'
def test_simple_query(self, hge_ctx):
self.with_admin_secret(hge_ctx, self.dir() + '/simple_query.yaml')
def test_simple_query(self, hge_ctx, backend):
self.with_admin_secret(hge_ctx, self.dir() + hge_ctx.backend_suffix('/simple_query') + ".yaml")
def test_permissions_query(self, hge_ctx):
self.with_admin_secret(hge_ctx, self.dir() + '/permissions_query.yaml')
def test_permissions_query(self, hge_ctx, backend):
self.with_admin_secret(hge_ctx, self.dir() + hge_ctx.backend_suffix('/permissions_query') + ".yaml")
def with_admin_secret(self, hge_ctx, f):
conf = get_conf_f(f)
@ -895,8 +896,7 @@ class TestGraphQLExplain:
headers['X-Hasura-Admin-Secret'] = hge_ctx.hge_key
status_code, resp_json, _ = hge_ctx.anyq(conf['url'], conf['query'], headers)
assert status_code == 200, resp_json
# Comparing only with generated 'sql' since the 'plan' is not consistent
# across all Postgres versions
# Comparing only with generated 'sql' since the 'plan' may differ
resp_sql = resp_json[0]['sql']
exp_sql = conf['response'][0]['sql']
assert resp_sql == exp_sql, resp_json