server: X -> MSSQL remote joins

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4097
GitOrigin-RevId: f39b82bac26f6ade83bd4f5e996dc26f5e048365
This commit is contained in:
Evie Ciobanu 2022-04-06 10:18:59 +03:00 committed by hasura-bot
parent 0b2944993f
commit 42480ee902
10 changed files with 379 additions and 99 deletions

View File

@ -4,6 +4,7 @@
### Bug fixes and improvements
- server: ms sql server can now be used in remote relationships
- server: fix JSON key encoding issue for remote schemas (fixes #7543 and #8200)
- server: fix MSSQL insert mutation when relationships are used in check permissions (fix #8225)
- server: refactor GQL query static analysis and improve OpenAPI warning messages

View File

@ -3,13 +3,22 @@
-- | This module defines translation functions for queries which select data.
-- Principally this includes translating the @query@ root field, but parts are
-- also reused for serving the responses for mutations.
module Hasura.Backends.MSSQL.FromIr.Query (fromQueryRootField, fromSelect) where
module Hasura.Backends.MSSQL.FromIr.Query
( fromQueryRootField,
fromSelect,
fromSourceRelationship,
)
where
import Control.Applicative (getConst)
import Control.Monad.Validate
import Data.Aeson.Extended qualified as J
import Data.HashMap.Strict qualified as HM
import Data.List.NonEmpty qualified as NE
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as M
import Data.Proxy
import Data.Text.NonEmpty (mkNonEmptyTextUnsafe)
import Database.ODBC.SQLServer qualified as ODBC
import Hasura.Backends.MSSQL.FromIr
( Error (..),
@ -65,6 +74,124 @@ fromSelect jsonAggSelect annSimpleSel =
]
}
-- | Used in 'Hasura.Backends.MSSQL.Plan.planSourceRelationship', which is in
-- turn used by to implement `mkDBRemoteRelationship' for 'BackendExecute'.
-- For more information, see the module/documentation of 'Hasura.GraphQL.Execute.RemoteJoin.Source'.
fromSourceRelationship ::
-- | List of json objects, each of which becomes a row of the table
NE.NonEmpty J.Object ->
-- | The above objects have this schema
HM.HashMap IR.FieldName (ColumnName, ScalarType) ->
IR.FieldName ->
(IR.FieldName, IR.SourceRelationshipSelection 'MSSQL Void (Const Expression)) ->
FromIr TSQL.Select
fromSourceRelationship lhs lhsSchema argumentId relationshipField = do
(argumentIdQualified, fieldSource) <-
flip runReaderT (fromAlias selectFrom) $ do
argumentIdQualified <- fromColumn (coerceToColumn argumentId)
relationshipSource <-
fromRemoteRelationFieldsG
mempty
(fst <$> joinColumns)
relationshipField
pure (ColumnExpression argumentIdQualified, relationshipSource)
let selectProjections = [projectArgumentId argumentIdQualified, fieldSourceProjections fieldSource]
pure
Select
{ selectWith = Nothing,
selectOrderBy = Nothing,
selectTop = NoTop,
selectProjections,
selectFrom = Just selectFrom,
selectJoins = mapMaybe fieldSourceJoin $ pure fieldSource,
selectWhere = mempty,
selectFor =
JsonFor ForJson {jsonCardinality = JsonArray, jsonRoot = NoRoot},
selectOffset = Nothing
}
where
projectArgumentId column =
ExpressionProjection $
Aliased
{ aliasedThing = column,
aliasedAlias = IR.getFieldNameTxt argumentId
}
selectFrom =
FromOpenJson
Aliased
{ aliasedThing =
OpenJson
{ openJsonExpression =
ValueExpression (ODBC.TextValue $ lbsToTxt $ J.encode lhs),
openJsonWith =
Just $
toJsonFieldSpec argumentId IntegerType
NE.:| map (uncurry toJsonFieldSpec . second snd) (HM.toList lhsSchema)
},
aliasedAlias = "lhs"
}
joinColumns = mapKeys coerceToColumn lhsSchema
coerceToColumn = ColumnName . IR.getFieldNameTxt
toJsonFieldSpec (IR.FieldName lhsFieldName) scalarType =
ScalarField scalarType DataLengthMax lhsFieldName (Just $ FieldPath RootPath lhsFieldName)
-- | Build the 'FieldSource' for the relation field, depending on whether it's
-- an object, array, or aggregate relationship.
fromRemoteRelationFieldsG ::
Map TableName EntityAlias ->
HM.HashMap ColumnName ColumnName ->
(IR.FieldName, IR.SourceRelationshipSelection 'MSSQL Void (Const Expression)) ->
ReaderT EntityAlias FromIr FieldSource
fromRemoteRelationFieldsG existingJoins joinColumns (IR.FieldName name, field) =
case field of
IR.SourceRelationshipObject selectionSet ->
fmap
( \aliasedThing ->
JoinFieldSource JsonSingleton (Aliased {aliasedThing, aliasedAlias = name})
)
( fromObjectRelationSelectG
existingJoins
( withJoinColumns $
runIdentity $
traverse (Identity . getConst) selectionSet
)
)
IR.SourceRelationshipArray selectionSet ->
fmap
( \aliasedThing ->
JoinFieldSource JsonArray (Aliased {aliasedThing, aliasedAlias = name})
)
( fromArraySelectG
( IR.ASSimple $
withJoinColumns $
runIdentity $
traverse (Identity . getConst) selectionSet
)
)
IR.SourceRelationshipArrayAggregate selectionSet ->
fmap
( \aliasedThing ->
JoinFieldSource JsonArray (Aliased {aliasedThing, aliasedAlias = name})
)
( fromArraySelectG
( IR.ASAggregate $
withJoinColumns $
runIdentity $
traverse (Identity . getConst) selectionSet
)
)
where
withJoinColumns ::
s -> IR.AnnRelationSelectG 'MSSQL s
withJoinColumns annotatedRelationship =
IR.AnnRelationSelectG
(IR.RelName $ mkNonEmptyTextUnsafe name)
joinColumns
annotatedRelationship
-- | Top/root-level 'Select'. All descendent/sub-translations are collected to produce a root TSQL.Select.
fromSelectRows :: IR.AnnSelectG 'MSSQL (IR.AnnFieldG 'MSSQL Void) Expression -> FromIr TSQL.Select
fromSelectRows annSelectG = do

View File

@ -60,10 +60,6 @@ instance BackendExecute 'MSSQL where
mkDBQueryExplain = msDBQueryExplain
mkSubscriptionExplain = msDBSubscriptionExplain
-- NOTE: Currently unimplemented!.
--
-- This function is just a stub for future implementation; for now it just
-- throws a 500 error.
mkDBRemoteRelationshipPlan =
msDBRemoteRelationshipPlan
@ -201,7 +197,7 @@ multiplexRootReselect variables rootReselect =
openJsonWith =
Just $
NE.fromList
[ UuidField resultIdAlias (Just $ IndexPath RootPath 0),
[ ScalarField GuidType DataLengthUnspecified resultIdAlias (Just $ IndexPath RootPath 0),
JsonField resultVarsAlias (Just $ IndexPath RootPath 1)
]
},
@ -416,5 +412,16 @@ msDBRemoteRelationshipPlan ::
RQLTypes.FieldName ->
(RQLTypes.FieldName, SourceRelationshipSelection 'MSSQL Void UnpreparedValue) ->
m (DBStepInfo 'MSSQL)
msDBRemoteRelationshipPlan _userInfo _sourceName _sourceConfig _lhs _lhsSchema _argumentId _relationship = do
throw500 "mkDBRemoteRelationshipPlan: SQL Server (MSSQL) does not currently support generalized joins."
msDBRemoteRelationshipPlan userInfo sourceName sourceConfig lhs lhsSchema argumentId relationship = do
statement <- planSourceRelationship (_uiSession userInfo) lhs lhsSchema argumentId relationship
let printer = fromSelect statement
queryString = ODBC.renderQuery $ toQueryPretty printer
odbcQuery = runSelectQuery printer
pure $ DBStepInfo @'MSSQL sourceName sourceConfig (Just queryString) odbcQuery
where
runSelectQuery :: Printer -> ExceptT QErr IO EncJSON
runSelectQuery queryPrinter = do
let queryTx = encJFromText <$> Tx.singleRowQueryE defaultMSSQLTxErrorHandler (toQueryFlat queryPrinter)
mssqlRunReadOnly (_mscExecCtx sourceConfig) queryTx

View File

@ -5,6 +5,7 @@
module Hasura.Backends.MSSQL.Plan
( PrepareState (..),
planQuery,
planSourceRelationship,
planSubscription,
prepareValueQuery,
resultAlias,
@ -19,22 +20,25 @@ where
-- , planSubscription
-- ) where
import Control.Applicative (Const (Const))
import Data.Aeson qualified as J
import Data.ByteString.Lazy (toStrict)
import Data.HashMap.Strict qualified as HM
import Data.HashMap.Strict.InsOrd qualified as OMap
import Data.HashSet qualified as Set
import Data.List.NonEmpty qualified as NE
import Data.Text qualified as T
import Data.Text.Extended
import Database.ODBC.SQLServer qualified as ODBC
import Hasura.Backends.MSSQL.FromIr
import Hasura.Backends.MSSQL.FromIr.Query (fromQueryRootField)
import Hasura.Backends.MSSQL.FromIr.Query (fromQueryRootField, fromSourceRelationship)
import Hasura.Backends.MSSQL.Types.Internal
import Hasura.Base.Error
import Hasura.GraphQL.Parser qualified as GraphQL
import Hasura.Prelude hiding (first)
import Hasura.RQL.IR
import Hasura.RQL.Types.Column qualified as RQL
import Hasura.RQL.Types.Common qualified as RQL
import Hasura.SQL.Backend
import Hasura.SQL.Types
import Hasura.Session
@ -50,7 +54,43 @@ planQuery ::
m Select
planQuery sessionVariables queryDB = do
rootField <- traverse (prepareValueQuery sessionVariables) queryDB
runFromIr (fromQueryRootField rootField)
runIrWrappingRoot $ fromQueryRootField rootField
-- | For more information, see the module/documentation of 'Hasura.GraphQL.Execute.RemoteJoin.Source'.
planSourceRelationship ::
MonadError QErr m =>
SessionVariables ->
-- | List of json objects, each of which becomes a row of the table
NE.NonEmpty J.Object ->
-- | The above objects have this schema
HM.HashMap RQL.FieldName (ColumnName, ScalarType) ->
RQL.FieldName ->
(RQL.FieldName, SourceRelationshipSelection 'MSSQL Void GraphQL.UnpreparedValue) ->
m Select
planSourceRelationship
sessionVariables
lhs
lhsSchema
argumentId
(relationshipName, sourceRelationshipRaw) = do
sourceRelationship <-
traverseSourceRelationshipSelection
(fmap Const . prepareValueQuery sessionVariables)
sourceRelationshipRaw
runIrWrappingRoot $
fromSourceRelationship
lhs
lhsSchema
argumentId
(relationshipName, sourceRelationship)
runIrWrappingRoot ::
MonadError QErr m =>
FromIr Select ->
m Select
runIrWrappingRoot selectAction = do
runFromIr selectAction
`onLeft` (throw400 NotSupported . tshow)
-- | Prepare a value without any query planning; we just execute the
-- query with the values embedded.

View File

@ -736,10 +736,12 @@ fromOpenJson OpenJson {openJsonExpression, openJsonWith} =
fromJsonFieldSpec :: JsonFieldSpec -> Printer
fromJsonFieldSpec =
\case
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"
ScalarField fieldType fieldLength name mPath ->
fromNameText name <+> " "
<+> fromString (T.unpack $ scalarTypeDBName fieldLength fieldType)
<+> quote mPath
where
quote mPath = maybe "" ((\p -> " '" <+> p <+> "'") . go) mPath
go = \case

View File

@ -453,10 +453,9 @@ data OpenJson = OpenJson
}
data JsonFieldSpec
= IntField Text (Maybe JsonPath)
= ScalarField ScalarType DataLength Text (Maybe JsonPath)
| JsonField Text (Maybe JsonPath)
| StringField Text (Maybe JsonPath)
| UuidField Text (Maybe JsonPath)
data Aliased a = Aliased
{ aliasedThing :: a,

View File

@ -122,6 +122,7 @@ module Hasura.RQL.IR.Select
saOffset,
saOrderBy,
saWhere,
traverseSourceRelationshipSelection,
_AFArrayRelation,
_AFColumn,
_AFComputedField,
@ -511,6 +512,19 @@ mkAnnColumnFieldAsText ::
mkAnnColumnFieldAsText ci =
AFColumn (AnnColumnField (ciColumn ci) (ciType ci) True Nothing Nothing)
traverseSourceRelationshipSelection ::
(Applicative f, Backend backend) =>
(vf backend -> f (vg backend)) ->
SourceRelationshipSelection backend r vf ->
f (SourceRelationshipSelection backend r vg)
traverseSourceRelationshipSelection f = \case
SourceRelationshipObject s ->
SourceRelationshipObject <$> traverse f s
SourceRelationshipArray s ->
SourceRelationshipArray <$> traverse f s
SourceRelationshipArrayAggregate s ->
SourceRelationshipArrayAggregate <$> traverse f s
-- Aggregation fields
data TableAggregateFieldG (b :: BackendType) (r :: Type) v

View File

@ -17,7 +17,7 @@ import Control.Lens (findOf, has, only, (^?!))
import Data.Aeson (Value)
import Data.Aeson.Lens (key, values, _String)
import Data.Char (isUpper, toLower)
import Data.Foldable (for_, traverse_)
import Data.Foldable (traverse_)
import Data.Function ((&))
import Data.List (intercalate, sortBy)
import Data.List.Split (dropBlanks, keepDelimsL, split, whenElt)
@ -29,6 +29,7 @@ import Data.Text (Text)
import Data.Typeable (Typeable)
import GHC.Generics (Generic)
import Harness.Backend.Postgres qualified as Postgres
import Harness.Backend.Sqlserver qualified as SQLServer
import Harness.GraphqlEngine qualified as GraphqlEngine
import Harness.Quoter.Graphql (graphql)
import Harness.Quoter.Yaml (shouldBeYaml, shouldReturnYaml, yaml)
@ -47,7 +48,7 @@ spec :: SpecWith State
spec = Context.runWithLocalState contexts tests
where
lhsContexts = [lhsPostgres, lhsRemoteServer]
rhsContexts = [rhsPostgres]
rhsContexts = [rhsPostgres, rhsSQLServer]
contexts = combine <$> lhsContexts <*> rhsContexts
-- | Combines a lhs and a rhs.
@ -141,10 +142,22 @@ rhsPostgres =
}
in (table, context)
{-
rhsMSSQL :: (Value, Context)
rhsMSSQL = ([yaml|{"schema":"hasura", "name":"album"}|], Context "MSSQL" rhsMSSQLSetup rhsMSSQLTeardown)
-}
rhsSQLServer :: RHSContext
rhsSQLServer =
let table =
[yaml|
schema: hasura
name: album
|]
context =
Context
{ name = Context.Backend Context.SQLServer,
mkLocalState = Context.noLocalState,
setup = rhsSQLServerSetup,
teardown = rhsSQLServerTeardown,
customOptions = Nothing
}
in (table, context)
--------------------------------------------------------------------------------
-- Schema
@ -178,7 +191,8 @@ album =
[]
[ [Schema.VInt 1, Schema.VStr "album1_artist1", Schema.VInt 1],
[Schema.VInt 2, Schema.VStr "album2_artist1", Schema.VInt 1],
[Schema.VInt 3, Schema.VStr "album3_artist2", Schema.VInt 2]
[Schema.VInt 3, Schema.VStr "album3_artist1", Schema.VInt 1],
[Schema.VInt 4, Schema.VStr "album4_artist2", Schema.VInt 2]
]
--------------------------------------------------------------------------------
@ -493,13 +507,73 @@ args:
filter:
artist_id:
_eq: x-hasura-artist-id
limit: 1
limit: 2
allow_aggregations: true
|]
rhsPostgresTeardown :: (State, ()) -> IO ()
rhsPostgresTeardown _ = Postgres.dropTable album
--------------------------------------------------------------------------------
-- RHS SQLServer
rhsSQLServerSetup :: (State, ()) -> IO ()
rhsSQLServerSetup (state, _) = do
let sourceName = "target"
sourceConfig = SQLServer.defaultSourceConfiguration
schemaName = Context.defaultSchema Context.SQLServer
-- Add remote source
GraphqlEngine.postMetadata_
state
[yaml|
type: mssql_add_source
args:
name: *sourceName
configuration: *sourceConfig
|]
-- setup tables only
SQLServer.createTable album
SQLServer.insertTable album
Schema.trackTable Context.SQLServer sourceName album state
GraphqlEngine.postMetadata_
state
[yaml|
type: bulk
args:
- type: mssql_create_select_permission
args:
source: *sourceName
role: role1
table:
schema: *schemaName
name: album
permission:
columns:
- title
- artist_id
filter:
artist_id:
_eq: x-hasura-artist-id
- type: mssql_create_select_permission
args:
source: *sourceName
role: role2
table:
schema: *schemaName
name: album
permission:
columns: [id, title, artist_id]
filter:
artist_id:
_eq: x-hasura-artist-id
limit: 2
allow_aggregations: true
|]
rhsSQLServerTeardown :: (State, ()) -> IO ()
rhsSQLServerTeardown _ = SQLServer.dropTable album
--------------------------------------------------------------------------------
-- Tests
@ -547,46 +621,6 @@ schemaTests _opts =
}
}
|]
relationshipFieldArgsSchema =
[yaml|
- name: distinct_on
type:
kind: LIST
name: null
ofType:
kind: NON_NULL
name: null
ofType:
kind: ENUM
name: hasura_album_select_column
ofType: null
- name: limit
type:
kind: SCALAR
name: Int
ofType: null
- name: offset
type:
kind: SCALAR
name: Int
ofType: null
- name: order_by
type:
kind: LIST
name: null
ofType:
kind: NON_NULL
name: null
ofType:
kind: INPUT_OBJECT
name: hasura_album_order_by
ofType: null
- name: where
type:
kind: INPUT_OBJECT
name: hasura_album_bool_exp
ofType: null
|]
introspectionResult <- GraphqlEngine.postGraphql state query
let focusArtistFields =
key "data"
@ -606,13 +640,6 @@ schemaTests _opts =
(has $ key "name" . _String . only "albums_aggregate")
introspectionResult
-- the schema of args should be same for both albums and albums_aggregate field
for_
[ albumsField ^?! key "args",
albumsAggregateField ^?! key "args"
]
(`shouldBeYaml` relationshipFieldArgsSchema)
-- check the return type of albums field
shouldBeYaml
(albumsField ^?! key "type")
@ -665,6 +692,7 @@ executionTests opts = describe "execution" do
albums:
- title: album1_artist1
- title: album2_artist1
- title: album3_artist1
|]
shouldReturnYaml
opts
@ -750,6 +778,7 @@ executionTests opts = describe "execution" do
albums:
- title: album1_artist1
- title: album2_artist1
- title: album3_artist1
- name: artist_no_id
albums: null
|]
@ -791,6 +820,7 @@ permissionTests opts = describe "permission" do
albums:
- title: album1_artist1
- title: album2_artist1
- title: album3_artist1
- name: artist2
albums: []
- name: artist_no_albums
@ -921,6 +951,7 @@ permissionTests opts = describe "permission" do
- name: artist1
albums:
- title: album1_artist1
- title: album2_artist1
|]
shouldReturnYaml
opts
@ -937,7 +968,7 @@ permissionTests opts = describe "permission" do
where: {name: {_eq: "artist1"}}
) {
name
albums (order_by: {id: asc} limit: 0){
albums (order_by: {id: asc} limit: 1){
title
}
}
@ -948,7 +979,8 @@ permissionTests opts = describe "permission" do
data:
artist:
- name: artist1
albums: []
albums:
- title: album1_artist1
|]
shouldReturnYaml
opts
@ -978,6 +1010,7 @@ permissionTests opts = describe "permission" do
- name: artist1
albums:
- title: album1_artist1
- title: album2_artist1
|]
shouldReturnYaml
opts
@ -1012,9 +1045,10 @@ permissionTests opts = describe "permission" do
- name: artist1
albums_aggregate:
aggregate:
count: 2
count: 3
nodes:
- title: album1_artist1
- title: album2_artist1
|]
shouldReturnYaml
opts
@ -1031,7 +1065,7 @@ permissionTests opts = describe "permission" do
where: {name: {_eq: "artist1"}}
) {
name
albums_aggregate (limit: 1 order_by: {id: asc}){
albums_aggregate (limit: 2 order_by: {id: asc}){
aggregate {
count
}
@ -1049,9 +1083,10 @@ permissionTests opts = describe "permission" do
- name: artist1
albums_aggregate:
aggregate:
count: 1
count: 2
nodes:
- title: album1_artist1
- title: album2_artist1
|]
shouldReturnYaml
opts

View File

@ -26,6 +26,7 @@ import Data.Text (Text)
import Data.Typeable (Typeable)
import GHC.Generics
import Harness.Backend.Postgres qualified as Postgres
import Harness.Backend.Sqlserver qualified as SQLServer
import Harness.GraphqlEngine qualified as GraphqlEngine
import Harness.Quoter.Graphql (graphql)
import Harness.Quoter.Yaml (shouldReturnYaml, yaml)
@ -44,7 +45,7 @@ spec :: SpecWith State
spec = Context.runWithLocalState contexts tests
where
lhsContexts = [lhsPostgres, lhsRemoteServer]
rhsContexts = [rhsPostgres]
rhsContexts = [rhsPostgres, rhsSQLServer]
contexts = combine <$> lhsContexts <*> rhsContexts
-- | Combines a lhs and a rhs.
@ -138,10 +139,22 @@ rhsPostgres =
}
in (table, context)
{-
rhsMSSQL :: (Value, Context)
rhsMSSQL = ([yaml|{"schema":"hasura", "name":"album"}|], Context "MSSQL" rhsMSSQLSetup rhsMSSQLTeardown)
-}
rhsSQLServer :: RHSContext
rhsSQLServer =
let table =
[yaml|
schema: hasura
name: album
|]
context =
Context
{ name = Context.Backend Context.SQLServer,
mkLocalState = Context.noLocalState,
setup = rhsSQLServerSetup,
teardown = rhsSQLServerTeardown,
customOptions = Nothing
}
in (table, context)
--------------------------------------------------------------------------------
-- Schema
@ -514,6 +527,66 @@ args:
rhsPostgresTeardown :: (State, ()) -> IO ()
rhsPostgresTeardown _ = Postgres.dropTable album
--------------------------------------------------------------------------------
-- RHS SQLServer
rhsSQLServerSetup :: (State, ()) -> IO ()
rhsSQLServerSetup (state, _) = do
let sourceName = "target"
sourceConfig = SQLServer.defaultSourceConfiguration
schemaName = Context.defaultSchema Context.SQLServer
-- Add remote source
GraphqlEngine.postMetadata_
state
[yaml|
type: mssql_add_source
args:
name: *sourceName
configuration: *sourceConfig
|]
-- setup tables only
SQLServer.createTable album
SQLServer.insertTable album
Schema.trackTable Context.SQLServer sourceName album state
GraphqlEngine.postMetadata_
state
[yaml|
type: bulk
args:
- type: mssql_create_select_permission
args:
source: *sourceName
role: role1
table:
schema: *schemaName
name: album
permission:
columns:
- title
- artist_id
filter:
artist_id:
_eq: x-hasura-artist-id
- type: mssql_create_select_permission
args:
source: *sourceName
role: role2
table:
schema: *schemaName
name: album
permission:
columns: [id, title, artist_id]
filter:
artist_id:
_eq: x-hasura-artist-id
limit: 1
allow_aggregations: true
|]
rhsSQLServerTeardown :: (State, ()) -> IO ()
rhsSQLServerTeardown _ = SQLServer.dropTable album
--------------------------------------------------------------------------------
-- Tests

View File

@ -4,31 +4,13 @@ status: 200
response:
sql: "SELECT [row].[result_id] AS [result_id],\n [result].[json] AS [result]\n\
FROM OPENJSON((N''+NCHAR(91)+''+NCHAR(91)+''+NCHAR(34)+'00000000-0000-0000-0000-000000000000'+NCHAR(34)+','+NCHAR(123)+''+NCHAR(34)+'synthetic'+NCHAR(34)+''+NCHAR(58)+''+NCHAR(91)+''+NCHAR(93)+','+NCHAR(34)+'query'+NCHAR(34)+''+NCHAR(58)+''+NCHAR(123)+''+NCHAR(125)+','+NCHAR(34)+'session'+NCHAR(34)+''+NCHAR(58)+''+NCHAR(123)+''+NCHAR(125)+''+NCHAR(125)+''+NCHAR(93)+''+NCHAR(93)+''))\n\
\ WITH ([result_id] UNIQUEIDENTIFIER '$[0]',\n [result_vars] NVARCHAR(MAX)\
\ WITH ([result_id] uniqueidentifier '$[0]',\n [result_vars] NVARCHAR(MAX)\
\ '$[1]' AS JSON) AS [row]\nOUTER APPLY (SELECT (SELECT ISNULL((SELECT [t_user1].[name]\
\ AS [name]\n FROM [dbo].[user] AS [t_user1]\n \
\ FOR JSON PATH, INCLUDE_NULL_VALUES), (N''+NCHAR(91)+''+NCHAR(93)+''))\
\ AS [root]) AS [user]\n FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER)\
\ \nAS [result]([json])\nFOR JSON PATH, INCLUDE_NULL_VALUES"
plan:
- "SELECT ISNULL((SELECT [row].[result_id] AS [result_id],\n [result].[json]\
\ AS [result]\nFROM OPENJSON((N''+NCHAR(91)+''+NCHAR(91)+''+NCHAR(34)+'00000000-0000-0000-0000-000000000000'+NCHAR(34)+','+NCHAR(123)+''+NCHAR(34)+'synthetic'+NCHAR(34)+''+NCHAR(58)+''+NCHAR(91)+''+NCHAR(93)+','+NCHAR(34)+'query'+NCHAR(34)+''+NCHAR(58)+''+NCHAR(123)+''+NCHAR(125)+','+NCHAR(34)+'session'+NCHAR(34)+''+NCHAR(58)+''+NCHAR(123)+''+NCHAR(125)+''+NCHAR(125)+''+NCHAR(93)+''+NCHAR(93)+''))\n\
\ WITH ([result_id] UNIQUEIDENTIFIER '$[0]',\n [result_vars] NVARCHAR(MAX)\
\ '$[1]' AS JSON) AS [row]\nOUTER APPLY (SELECT ISNULL((SELECT (SELECT ISNULL((SELECT\
\ [t_user1].[name] AS [name]\n FROM [dbo].[user] AS [t_user1]\n\
\ FOR JSON PATH, INCLUDE_NULL_VALUES), '[]')) AS [user]\n\
\ FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER), 'null'))\
\ \nAS [result]([json])\nFOR JSON PATH, INCLUDE_NULL_VALUES), '[]')"
- " |--Compute Scalar(DEFINE:([Expr1010]=isnull([Expr1008],CONVERT_IMPLICIT(nvarchar(max),'[]',0))))"
- ' |--UDX((OPENJSON_EXPLICIT.[result_id], [Expr1007]))'
- ' |--Nested Loops(Left Outer Join)'
- ' |--Table-valued function'
- " |--Compute Scalar(DEFINE:([Expr1007]=isnull([Expr1005],CONVERT_IMPLICIT(nvarchar(max),'[]',0))))"
- ' |--UDX(([Expr1004]))'
- " |--Compute Scalar(DEFINE:([Expr1004]=isnull([Expr1001],CONVERT_IMPLICIT(nvarchar(max),'[]',0))))"
- ' |--UDX(([t_user1].[name]))'
- ' |--Clustered Index Scan(OBJECT:([master].[dbo].[user].[PK__user__3213E83F9704D3EC]
AS [t_user1]))'
query:
query:
query: |