NaQI - validate against the database - infra and check syntax

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7778
GitOrigin-RevId: c8366ad7261d9c120dc0612bedb96fa5ae59eda3
This commit is contained in:
Gil Mizrahi 2023-02-03 18:27:44 +02:00 committed by hasura-bot
parent 4c37b6838c
commit f90373b1ca
15 changed files with 307 additions and 50 deletions

View File

@ -602,6 +602,7 @@ library
, Hasura.Backends.Postgres.Instances.API
, Hasura.Backends.Postgres.Instances.Execute
, Hasura.Backends.Postgres.Instances.Metadata
, Hasura.Backends.Postgres.Instances.NativeQueries
, Hasura.Backends.Postgres.Instances.PingSource
, Hasura.Backends.Postgres.Instances.Schema
, Hasura.Backends.Postgres.Instances.SchemaCache
@ -807,6 +808,7 @@ library
, Hasura.RQL.Types.SchemaCache.Instances
, Hasura.RQL.Types.SchemaCacheTypes
, Hasura.RQL.Types.Source
, Hasura.RQL.Types.SourceConfiguration
, Hasura.RQL.Types.SourceCustomization
, Hasura.RQL.Types.Subscription
, Hasura.RQL.Types.Table

View File

@ -47,6 +47,12 @@ schema =
{ tableColumns =
[ Schema.column "divided" Schema.TInt
]
},
(table "stuff")
{ tableColumns =
[ Schema.column "thing" Schema.TInt,
Schema.column "date" Schema.TUTCTime
]
}
]
@ -314,3 +320,86 @@ tests opts = do
[yaml|
[]
|]
describe "Validation fails on track a native query when query" do
it "has a syntax error" $
\testEnv -> do
let schemaName = Schema.getSchemaName testEnv
let spicyQuery :: Text
spicyQuery = "query bad"
shouldReturnYaml
opts
( GraphqlEngine.postMetadataWithStatus
400
testEnv
[yaml|
type: pg_track_native_query
args:
type: query
source: postgres
root_field_name: divided_stuff
code: *spicyQuery
arguments:
denominator: int
target_date: date
returns:
name: already_tracked_return_type
schema: *schemaName
|]
)
[yaml|
code: validation-failed
error: Failed to validate query
internal:
arguments: []
error:
description: null
exec_status: "FatalError"
hint: null
message: "syntax error at or near \"query\""
status_code: "42601"
prepared: false
statement: "PREPARE _naqi_vali_divided_stuff AS query bad"
path: "$.args"
|]
it "refers to non existing table" $
\testEnv -> do
let schemaName = Schema.getSchemaName testEnv
let spicyQuery :: Text
spicyQuery = "SELECT thing / {{denominator}} AS divided FROM does_not_exist WHERE date = {{target_date}}"
shouldReturnYaml
opts
( GraphqlEngine.postMetadataWithStatus
400
testEnv
[yaml|
type: pg_track_native_query
args:
type: query
source: postgres
root_field_name: divided_stuff
code: *spicyQuery
arguments:
denominator: int
target_date: date
returns:
name: already_tracked_return_type
schema: *schemaName
|]
)
[yaml|
code: validation-failed
error: Failed to validate query
internal:
arguments: []
error:
description: null
exec_status: "FatalError"
hint: null
message: "relation \"does_not_exist\" does not exist"
status_code: "42P01"
prepared: false
statement: "PREPARE _naqi_vali_divided_stuff AS SELECT thing / $1 AS divided FROM does_not_exist WHERE date = $2"
path: "$.args"
|]

View File

@ -183,6 +183,59 @@ tests opts = do
actual `shouldBe` expected
it "Runs a simple query that takes no parameters but ends with a comment" $ \testEnvironment -> do
let spicyQuery :: Text
spicyQuery = "SELECT * FROM (VALUES ('hello', 'world'), ('welcome', 'friend')) as t(\"one\", \"two\") -- my query"
let backendTypeMetadata = fromMaybe (error "Unknown backend") $ getBackendTypeConfig testEnvironment
source = BackendType.backendSourceName backendTypeMetadata
schemaName = Schema.getSchemaName testEnvironment
shouldReturnYaml
opts
( GraphqlEngine.postMetadata
testEnvironment
[yaml|
type: pg_track_native_query
args:
type: query
source: *source
root_field_name: hello_comment_function
code: *spicyQuery
returns:
name: hello_world_table
schema: *schemaName
|]
)
[yaml|
message: success
|]
let expected =
[yaml|
data:
hello_comment_function:
- one: "hello"
two: "world"
- one: "welcome"
two: "friend"
|]
actual :: IO Value
actual =
GraphqlEngine.postGraphql
testEnvironment
[graphql|
query {
hello_comment_function {
one
two
}
}
|]
actual `shouldBe` expected
it "Runs a simple query that takes one parameter and uses it multiple times" $ \testEnvironment -> do
let backendTypeMetadata = fromMaybe (error "Unknown backend") $ getBackendTypeConfig testEnvironment
source = BackendType.backendSourceName backendTypeMetadata
@ -387,7 +440,7 @@ tests opts = do
expected =
[yaml|
data:
first:
first:
- excerpt: "I like to eat dog food I am a dogs..."
second:
- excerpt: "I like to eat..."

View File

@ -20,8 +20,6 @@ import Language.GraphQL.Draft.Syntax qualified as G
instance Backend 'BigQuery where
type BackendConfig 'BigQuery = ()
type BackendInfo 'BigQuery = ()
type SourceConfig 'BigQuery = BigQuery.BigQuerySourceConfig
type SourceConnConfiguration 'BigQuery = BigQuery.BigQueryConnSourceConfig
type TableName 'BigQuery = BigQuery.TableName
type FunctionName 'BigQuery = BigQuery.FunctionName
type RawFunctionInfo 'BigQuery = BigQuery.RestRoutine
@ -117,4 +115,8 @@ instance Backend 'BigQuery where
defaultTriggerOnReplication = Nothing
instance HasSourceConfiguration 'BigQuery where
type SourceConfig 'BigQuery = BigQuery.BigQuerySourceConfig
type SourceConnConfiguration 'BigQuery = BigQuery.BigQueryConnSourceConfig
instance NativeQueryMetadata 'BigQuery

View File

@ -24,7 +24,7 @@ import Hasura.Base.Error (Code (ValidationFailed), QErr, runAesonParser, throw40
import Hasura.NativeQuery.Types (NativeQueryMetadata)
import Hasura.Prelude
import Hasura.RQL.IR.BoolExp
import Hasura.RQL.Types.Backend (Backend (..), ComputedFieldReturnType, SupportedNamingCase (..), XDisable, XEnable)
import Hasura.RQL.Types.Backend (Backend (..), ComputedFieldReturnType, HasSourceConfiguration (..), SupportedNamingCase (..), XDisable, XEnable)
import Hasura.RQL.Types.Column (ColumnType (..))
import Hasura.RQL.Types.ResizePool (ServerReplicas)
import Hasura.SQL.Backend (BackendType (DataConnector))
@ -43,8 +43,6 @@ type Unimplemented = ()
instance Backend 'DataConnector where
type BackendConfig 'DataConnector = InsOrdHashMap DC.DataConnectorName DC.DataConnectorOptions
type BackendInfo 'DataConnector = HashMap DC.DataConnectorName DC.DataConnectorInfo
type SourceConfig 'DataConnector = DC.SourceConfig
type SourceConnConfiguration 'DataConnector = DC.ConnSourceConfig
type TableName 'DataConnector = DC.TableName
type FunctionName 'DataConnector = DC.FunctionName
@ -159,6 +157,10 @@ instance Backend 'DataConnector where
defaultTriggerOnReplication = Nothing
instance HasSourceConfiguration 'DataConnector where
type SourceConfig 'DataConnector = DC.SourceConfig
type SourceConnConfiguration 'DataConnector = DC.ConnSourceConfig
instance NativeQueryMetadata 'DataConnector
data CustomBooleanOperator a = CustomBooleanOperator

View File

@ -29,8 +29,6 @@ import Language.GraphQL.Draft.Syntax qualified as G
instance Backend 'MSSQL where
type BackendConfig 'MSSQL = ()
type BackendInfo 'MSSQL = ()
type SourceConfig 'MSSQL = MSSQL.MSSQLSourceConfig
type SourceConnConfiguration 'MSSQL = MSSQL.MSSQLConnConfiguration
type TableName 'MSSQL = MSSQL.TableName
type RawFunctionInfo 'MSSQL = Void
@ -125,4 +123,8 @@ instance Backend 'MSSQL where
defaultTriggerOnReplication = Just ((), TOREnableTrigger)
instance HasSourceConfiguration 'MSSQL where
type SourceConfig 'MSSQL = MSSQL.MSSQLSourceConfig
type SourceConnConfiguration 'MSSQL = MSSQL.MSSQLConnConfiguration
instance NativeQueryMetadata 'MSSQL

View File

@ -19,8 +19,6 @@ import Language.GraphQL.Draft.Syntax qualified as G
instance Backend 'MySQL where
type BackendConfig 'MySQL = ()
type BackendInfo 'MySQL = ()
type SourceConfig 'MySQL = MySQL.SourceConfig
type SourceConnConfiguration 'MySQL = MySQL.ConnSourceConfig
type TableName 'MySQL = MySQL.TableName
type FunctionName 'MySQL = MySQL.FunctionName
type RawFunctionInfo 'MySQL = Void -- MySQL.FunctionName
@ -149,4 +147,8 @@ instance Backend 'MySQL where
defaultTriggerOnReplication = Nothing
instance HasSourceConfiguration 'MySQL where
type SourceConfig 'MySQL = MySQL.SourceConfig
type SourceConnConfiguration 'MySQL = MySQL.ConnSourceConfig
instance NativeQueryMetadata 'MySQL

View File

@ -0,0 +1,46 @@
-- | Validate native queries against postgres-like flavors.
module Hasura.Backends.Postgres.Instances.NativeQueries
( validateNativeQuery,
)
where
import Data.Aeson (toJSON)
import Database.PG.Query qualified as PG
import Hasura.Backends.Postgres.Connection qualified as PG
import Hasura.Backends.Postgres.Connection.Connect (withPostgresDB)
import Hasura.Base.Error
import Hasura.NativeQuery.Metadata
import Hasura.Prelude
import Hasura.SQL.Backend
-- | Prepare a native query against a postgres-like database to validate it.
validateNativeQuery :: (MonadIO m, MonadError NativeQueryError m) => PG.PostgresConnConfiguration -> NativeQueryInfoImpl ('Postgres pgKind) -> m ()
validateNativeQuery connConf nativeQuery = do
let name = getNativeQueryNameImpl $ nqiiRootFieldName nativeQuery
let code :: Text
code = fold $ flip evalState (1 :: Int) do
for (getInterpolatedQuery $ nqiiCode nativeQuery) \case
IIText t -> pure t
IIVariable _v -> do
i <- get
modify (+ 1)
pure $ "$" <> tshow i
result <-
liftIO $
withPostgresDB connConf $
PG.rawQE
( \e ->
(err400 ValidationFailed "Failed to validate query")
{ qeInternal = Just $ ExtraInternal $ toJSON e
}
)
(PG.fromText $ "PREPARE _naqi_vali_" <> name <> " AS " <> code)
[]
False
case result of
-- running the query failed
Left err ->
throwError $ NativeQueryValidationError err
-- running the query succeeded
Right () ->
pure ()

View File

@ -17,6 +17,7 @@ import Data.Typeable
import Hasura.Backends.Postgres.Connection qualified as Postgres
import Hasura.Backends.Postgres.Connection.VersionCheck (runCockroachVersionCheck)
import Hasura.Backends.Postgres.Execute.ConnectionTemplate qualified as Postgres
import Hasura.Backends.Postgres.Instances.NativeQueries (validateNativeQuery)
import Hasura.Backends.Postgres.Instances.PingSource (runCockroachDBPing)
import Hasura.Backends.Postgres.SQL.DML qualified as Postgres
import Hasura.Backends.Postgres.SQL.Types qualified as Postgres
@ -89,8 +90,6 @@ instance
where
type BackendConfig ('Postgres pgKind) = ()
type BackendInfo ('Postgres pgKind) = ()
type SourceConfig ('Postgres pgKind) = Postgres.PGSourceConfig
type SourceConnConfiguration ('Postgres pgKind) = Postgres.PostgresConnConfiguration
type TableName ('Postgres pgKind) = Postgres.QualifiedTable
type FunctionName ('Postgres pgKind) = Postgres.QualifiedFunction
type FunctionArgument ('Postgres pgKind) = Postgres.FunctionArg
@ -166,6 +165,14 @@ instance
resolveConnectionTemplate = Postgres.pgResolveConnectionTemplate
instance
( HasTag ('Postgres pgKind)
) =>
HasSourceConfiguration ('Postgres pgKind)
where
type SourceConfig ('Postgres pgKind) = Postgres.PGSourceConfig
type SourceConnConfiguration ('Postgres pgKind) = Postgres.PostgresConnConfiguration
instance
( HasTag ('Postgres pgKind),
Typeable ('Postgres pgKind),
@ -181,3 +188,4 @@ instance
trackNativeQuerySource = tnqSource
nativeQueryInfoName = nqiiRootFieldName
nativeQueryTrackToInfo = defaultNativeQueryTrackToInfo
validateNativeQueryAgainstSource = validateNativeQuery

View File

@ -1186,6 +1186,8 @@ instance ToSQL TopLevelCTE where
IIVariable v -> toSQL v
)
parts
-- if the user has a comment on the last line, this will make sure it doesn't interrupt the rest of the query
<> "\n"
-- | A @SELECT@ statement with Common Table Expressions.
-- <https://www.postgresql.org/docs/current/queries-with.html>

View File

@ -19,14 +19,14 @@ module Hasura.NativeQuery.API
)
where
import Control.Lens ((^?))
import Control.Lens (preview, (^?))
import Data.Aeson
import Hasura.Base.Error
import Hasura.EncJSON
import Hasura.NativeQuery.Types
import Hasura.Prelude
import Hasura.RQL.Types.Backend (Backend)
import Hasura.RQL.Types.Common (SourceName, successMsg)
import Hasura.RQL.Types.Common (SourceName, sourceNameToText, successMsg)
import Hasura.RQL.Types.Metadata
import Hasura.RQL.Types.Metadata.Backend
import Hasura.RQL.Types.Metadata.Object
@ -94,10 +94,17 @@ runTrackNativeQuery ::
runTrackNativeQuery (BackendTrackNativeQuery trackNativeQueryRequest) = do
throwIfFeatureDisabled
(metadata :: NativeQueryInfo b) <-
case nativeQueryTrackToInfo @b trackNativeQueryRequest of
sourceConnConfig <-
maybe (throw400 NotFound $ "Source " <> sourceNameToText source <> " not found.") pure
. preview (metaSources . ix source . toSourceMetadata @b . smConfiguration)
=<< getMetadata
(metadata :: NativeQueryInfo b) <- do
r <- liftIO $ runExceptT $ nativeQueryTrackToInfo @b sourceConnConfig trackNativeQueryRequest
case r of
Right nq -> pure nq
Left (NativeQueryParseError e) -> throw400 ParseFailed e
Left (NativeQueryValidationError e) -> throwError e
let fieldName = nativeQueryInfoName @b metadata
metadataObj =

View File

@ -182,16 +182,27 @@ data TrackNativeQueryImpl (b :: BackendType) = TrackNativeQueryImpl
}
-- | Default implementation of the method 'nativeQueryTrackToInfo'.
defaultNativeQueryTrackToInfo :: TrackNativeQueryImpl b -> Either NativeQueryParseError (NativeQueryInfoImpl b)
defaultNativeQueryTrackToInfo TrackNativeQueryImpl {..} = do
nqiiCode <- mapLeft NativeQueryParseError (parseInterpolatedQuery tnqCode)
defaultNativeQueryTrackToInfo ::
forall b m.
( MonadIO m,
MonadError NativeQueryError m,
NativeQueryMetadata b,
NativeQueryInfo b ~ NativeQueryInfoImpl b
) =>
SourceConnConfiguration b ->
TrackNativeQueryImpl b ->
m (NativeQueryInfoImpl b)
defaultNativeQueryTrackToInfo sourceConnConfig TrackNativeQueryImpl {..} = do
nqiiCode <- liftEither $ mapLeft NativeQueryParseError (parseInterpolatedQuery tnqCode)
let nqiiRootFieldName = tnqRootFieldName
nqiiReturns = tnqReturns
nqiiArguments = tnqArguments
nqiiDescription = tnqDescription
nqInfoImpl = NativeQueryInfoImpl {..}
pure $ NativeQueryInfoImpl {..}
where
nqiiRootFieldName = tnqRootFieldName
nqiiReturns = tnqReturns
nqiiArguments = tnqArguments
nqiiDescription = tnqDescription
validateNativeQueryAgainstSource @b sourceConnConfig nqInfoImpl
pure nqInfoImpl
instance (Backend b, HasCodec (ScalarType b)) => HasCodec (TrackNativeQueryImpl b) where
codec =

View File

@ -6,7 +6,7 @@
-- are free to provide their own as needed.
module Hasura.NativeQuery.Types
( NativeQueryMetadata (..),
NativeQueryParseError (..),
NativeQueryError (..),
BackendTrackNativeQuery (..),
)
where
@ -15,12 +15,12 @@ import Autodocodec
import Data.Aeson
import Data.Kind
import Data.Text.Extended (ToTxt)
import Hasura.Base.Error
import Hasura.Prelude
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.SourceConfiguration
import Hasura.SQL.Backend
type Representable a = (Show a, Eq a, Hashable a, NFData a)
type APIType a = (ToJSON a, FromJSON a)
-- | This type class models the types and functions necessary to talk about
@ -65,9 +65,14 @@ class
nativeQueryInfoName = absurd
-- | Projection function, producing a 'NativeQueryInfo b' from a 'TrackNativeQuery b'.
nativeQueryTrackToInfo :: TrackNativeQuery b -> Either NativeQueryParseError (NativeQueryInfo b)
default nativeQueryTrackToInfo :: (TrackNativeQuery b ~ Void) => TrackNativeQuery b -> Either NativeQueryParseError (NativeQueryInfo b)
nativeQueryTrackToInfo = absurd
nativeQueryTrackToInfo :: SourceConnConfiguration b -> TrackNativeQuery b -> ExceptT NativeQueryError IO (NativeQueryInfo b)
default nativeQueryTrackToInfo :: (TrackNativeQuery b ~ Void) => SourceConnConfiguration b -> TrackNativeQuery b -> ExceptT NativeQueryError IO (NativeQueryInfo b)
nativeQueryTrackToInfo _ = absurd
-- | Validate the native query against the database.
validateNativeQueryAgainstSource :: (MonadIO m, MonadError NativeQueryError m) => SourceConnConfiguration b -> NativeQueryInfo b -> m ()
default validateNativeQueryAgainstSource :: (NativeQueryInfo b ~ Void) => SourceConnConfiguration b -> NativeQueryInfo b -> m ()
validateNativeQueryAgainstSource _ = absurd
-- | Our API endpoint solution wraps all request payload types in 'AnyBackend'
-- for its multi-backend support, but type families must be fully applied to
@ -81,4 +86,6 @@ deriving newtype instance NativeQueryMetadata b => FromJSON (BackendTrackNativeQ
-- Things that might go wrong when converting a Native Query metadata request
-- into a valid metadata item (such as failure to interpolate the query)
newtype NativeQueryParseError = NativeQueryParseError Text
data NativeQueryError
= NativeQueryParseError Text
| NativeQueryValidationError QErr

View File

@ -2,13 +2,14 @@
module Hasura.RQL.Types.Backend
( Backend (..),
Representable,
SessionVarType,
XDisable,
XEnable,
ComputedFieldReturnType (..),
_ReturnsTable,
SupportedNamingCase (..),
HasSourceConfiguration (..),
Representable,
)
where
@ -27,13 +28,12 @@ import Hasura.Prelude
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.HealthCheckImplementation (HealthCheckImplementation)
import Hasura.RQL.Types.ResizePool (ServerReplicas)
import Hasura.RQL.Types.SourceConfiguration
import Hasura.SQL.Backend
import Hasura.SQL.Tag
import Hasura.SQL.Types
import Language.GraphQL.Draft.Syntax qualified as G
type Representable a = (Show a, Hashable a, NFData a)
type SessionVarType b = CollectableType (ScalarType b)
data ComputedFieldReturnType (b :: BackendType)
@ -71,7 +71,8 @@ data SupportedNamingCase = OnlyHasuraCase | AllConventions
-- type application or a 'Proxy' parameter to disambiguate between
-- different backends at the call site.
class
( Representable (BasicOrderType b),
( HasSourceConfiguration b,
Representable (BasicOrderType b),
Representable (Column b),
Representable (ComputedFieldDefinition b),
Representable (ComputedFieldImplicitArguments b),
@ -85,7 +86,6 @@ class
Representable (SQLExpression b),
Representable (ScalarSelectionArguments b),
Representable (ScalarType b),
Representable (SourceConnConfiguration b),
Representable (XComputedField b),
Representable (TableName b),
Eq (RawFunctionInfo b),
@ -105,13 +105,11 @@ class
FromJSON (HealthCheckTest b),
FromJSON (RawFunctionInfo b),
FromJSON (ScalarType b),
FromJSON (SourceConnConfiguration b),
FromJSON (TableName b),
FromJSONKey (Column b),
HasCodec (BackendSourceKind b),
HasCodec (Column b),
HasCodec (FunctionName b),
HasCodec (SourceConnConfiguration b),
HasCodec (TableName b),
ToJSON (BackendConfig b),
ToJSON (Column b),
@ -119,9 +117,7 @@ class
ToJSON (FunctionArgument b),
ToJSON (FunctionName b),
ToJSON (ScalarType b),
ToJSON (SourceConfig b),
ToJSON (TableName b),
ToJSON (SourceConnConfiguration b),
ToJSON (ExtraTableMetadata b),
ToJSON (SQLExpression b),
ToJSON (ComputedFieldDefinition b),
@ -153,7 +149,6 @@ class
Show (CountType b),
Eq (ScalarValue b),
Show (ScalarValue b),
Eq (SourceConfig b),
-- Extension constraints.
Eq (XNodesAgg b),
Show (XNodesAgg b),
@ -179,13 +174,6 @@ class
-- | Runtime backend info derived from (possibly enriched) BackendConfig and stored in SchemaCache
type BackendInfo b :: Type
-- | User facing connection configuration for a database.
type SourceConnConfiguration b :: Type
-- | Internal connection configuration for a database - connection string,
-- connection pool etc
type SourceConfig b :: Type
-- Fully qualified name of a table
type TableName b :: Type
@ -298,7 +286,7 @@ class
type BackendInsert b = Const Void
-- | Intermediate representation of Native Queries
-- | Intermediate representation of Native Queries.
-- The default implementation makes native queries uninstantiable.
--
-- It is parameterised over the type of fields, which changes during the IR

View File

@ -0,0 +1,36 @@
{-# LANGUAGE UndecidableInstances #-}
module Hasura.RQL.Types.SourceConfiguration
( HasSourceConfiguration (..),
Representable,
)
where
import Autodocodec (HasCodec)
import Data.Aeson.Extended
import Data.Kind (Type)
import Hasura.Prelude
import Hasura.SQL.Backend
import Hasura.SQL.Tag
type Representable a = (Show a, Eq a, Hashable a, NFData a)
class
( Representable (SourceConnConfiguration b),
HasCodec (SourceConnConfiguration b),
FromJSON (SourceConnConfiguration b),
ToJSON (SourceConfig b),
ToJSON (SourceConnConfiguration b),
Eq (SourceConfig b),
HasTag b
) =>
HasSourceConfiguration (b :: BackendType)
where
-- types
-- | User facing connection configuration for a database.
type SourceConnConfiguration b :: Type
-- | Internal connection configuration for a database - connection string,
-- connection pool etc
type SourceConfig b :: Type