mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
server: X -> MSSQL remote joins
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4097 GitOrigin-RevId: f39b82bac26f6ade83bd4f5e996dc26f5e048365
This commit is contained in:
parent
0b2944993f
commit
42480ee902
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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: |
|
||||
|
Loading…
Reference in New Issue
Block a user