mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
Logical models: check that specified named columns are returned from query
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8312 GitOrigin-RevId: fc7c7a14115c0b228abc2f48d3cfac51d2852277
This commit is contained in:
parent
9d30b7f5e9
commit
021e769235
@ -650,6 +650,39 @@ testValidation opts = do
|
|||||||
|
|
||||||
actual `shouldAtLeastBe` expected
|
actual `shouldAtLeastBe` expected
|
||||||
|
|
||||||
|
it "where the column names specified are not returned from the query" $
|
||||||
|
\testEnv -> do
|
||||||
|
let expected =
|
||||||
|
[yaml|
|
||||||
|
code: validation-failed
|
||||||
|
error: Failed to validate query
|
||||||
|
internal:
|
||||||
|
error:
|
||||||
|
message: column "text" does not exist
|
||||||
|
|]
|
||||||
|
actual <-
|
||||||
|
GraphqlEngine.postMetadataWithStatus
|
||||||
|
400
|
||||||
|
testEnv
|
||||||
|
[yaml|
|
||||||
|
type: pg_track_logical_model
|
||||||
|
args:
|
||||||
|
type: query
|
||||||
|
source: postgres
|
||||||
|
root_field_name: text_failing
|
||||||
|
code: |
|
||||||
|
SELECT {{text}} AS not_text
|
||||||
|
arguments:
|
||||||
|
text:
|
||||||
|
type: text
|
||||||
|
returns:
|
||||||
|
columns:
|
||||||
|
text:
|
||||||
|
type: text
|
||||||
|
|]
|
||||||
|
|
||||||
|
actual `shouldAtLeastBe` expected
|
||||||
|
|
||||||
it "that uses undeclared arguments" $
|
it "that uses undeclared arguments" $
|
||||||
\testEnvironment -> do
|
\testEnvironment -> do
|
||||||
let backendTypeMetadata = fromMaybe (error "Unknown backend") $ getBackendTypeConfig testEnvironment
|
let backendTypeMetadata = fromMaybe (error "Unknown backend") $ getBackendTypeConfig testEnvironment
|
||||||
|
@ -12,6 +12,7 @@ import Data.Map.Strict (Map)
|
|||||||
import Data.Map.Strict qualified as Map
|
import Data.Map.Strict qualified as Map
|
||||||
import Data.Set (Set)
|
import Data.Set (Set)
|
||||||
import Data.Set qualified as Set
|
import Data.Set qualified as Set
|
||||||
|
import Data.Text qualified as Text
|
||||||
import Data.Text.Extended (commaSeparated, toTxt)
|
import Data.Text.Extended (commaSeparated, toTxt)
|
||||||
import Database.PG.Query qualified as PG
|
import Database.PG.Query qualified as PG
|
||||||
import Hasura.Backends.Postgres.Connection qualified as PG
|
import Hasura.Backends.Postgres.Connection qualified as PG
|
||||||
@ -19,6 +20,7 @@ import Hasura.Backends.Postgres.Connection.Connect (withPostgresDB)
|
|||||||
import Hasura.Backends.Postgres.Instances.Types ()
|
import Hasura.Backends.Postgres.Instances.Types ()
|
||||||
import Hasura.Backends.Postgres.SQL.Types (PGScalarType, pgScalarTypeToText)
|
import Hasura.Backends.Postgres.SQL.Types (PGScalarType, pgScalarTypeToText)
|
||||||
import Hasura.Base.Error
|
import Hasura.Base.Error
|
||||||
|
import Hasura.CustomReturnType
|
||||||
import Hasura.LogicalModel.Metadata
|
import Hasura.LogicalModel.Metadata
|
||||||
( InterpolatedItem (..),
|
( InterpolatedItem (..),
|
||||||
InterpolatedQuery (..),
|
InterpolatedQuery (..),
|
||||||
@ -29,6 +31,44 @@ import Hasura.LogicalModel.Types (NullableScalarType (nstType), getLogicalModelN
|
|||||||
import Hasura.Prelude
|
import Hasura.Prelude
|
||||||
import Hasura.SQL.Backend
|
import Hasura.SQL.Backend
|
||||||
|
|
||||||
|
-- | Prepare a logical model query against a postgres-like database to validate it.
|
||||||
|
validateLogicalModel ::
|
||||||
|
forall m pgKind.
|
||||||
|
(MonadIO m, MonadError QErr m) =>
|
||||||
|
Env.Environment ->
|
||||||
|
PG.PostgresConnConfiguration ->
|
||||||
|
LogicalModelMetadata ('Postgres pgKind) ->
|
||||||
|
m ()
|
||||||
|
validateLogicalModel env connConf model = do
|
||||||
|
preparedQuery <- logicalModelToPreparedStatement model
|
||||||
|
|
||||||
|
-- We don't need to deallocate the prepared statement because 'withPostgresDB'
|
||||||
|
-- opens a new connection, runs a statement, and then closes the connection.
|
||||||
|
-- Since a prepared statement only lasts for the duration of the session, once
|
||||||
|
-- the session closes, the prepared statement is deallocated as well.
|
||||||
|
runRaw (PG.fromText $ preparedQuery)
|
||||||
|
where
|
||||||
|
runRaw :: PG.Query -> m ()
|
||||||
|
runRaw stmt =
|
||||||
|
liftEither
|
||||||
|
=<< liftIO
|
||||||
|
( withPostgresDB
|
||||||
|
env
|
||||||
|
connConf
|
||||||
|
( PG.rawQE
|
||||||
|
( \e ->
|
||||||
|
(err400 ValidationFailed "Failed to validate query")
|
||||||
|
{ qeInternal = Just $ ExtraInternal $ toJSON e
|
||||||
|
}
|
||||||
|
)
|
||||||
|
stmt
|
||||||
|
[]
|
||||||
|
False
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
-- | The environment and fresh-name generator used by 'renameIQ'.
|
-- | The environment and fresh-name generator used by 'renameIQ'.
|
||||||
data RenamingState = RenamingState
|
data RenamingState = RenamingState
|
||||||
{ rsNextFree :: Int,
|
{ rsNextFree :: Int,
|
||||||
@ -82,13 +122,19 @@ renameIQ = runRenaming . fmap InterpolatedQuery . mapM renameII . getInterpolate
|
|||||||
where
|
where
|
||||||
swap (a, b) = (b, a)
|
swap (a, b) = (b, a)
|
||||||
|
|
||||||
|
-- | Pretty print an interpolated query with numbered parameters.
|
||||||
renderIQ :: InterpolatedQuery Int -> Text
|
renderIQ :: InterpolatedQuery Int -> Text
|
||||||
renderIQ (InterpolatedQuery items) = mconcat (map printItem items)
|
renderIQ (InterpolatedQuery items) = foldMap printItem items
|
||||||
where
|
where
|
||||||
printItem :: InterpolatedItem Int -> Text
|
printItem :: InterpolatedItem Int -> Text
|
||||||
printItem (IIText t) = t
|
printItem (IIText t) = t
|
||||||
printItem (IIVariable i) = "$" <> tshow i
|
printItem (IIVariable i) = "$" <> tshow i
|
||||||
|
|
||||||
|
-----------------------------------------
|
||||||
|
|
||||||
|
-- | Convert a logical model to a prepared statement to be validate.
|
||||||
|
--
|
||||||
|
-- Used by 'validateLogicalModel'. Exported for testing.
|
||||||
logicalModelToPreparedStatement ::
|
logicalModelToPreparedStatement ::
|
||||||
forall m pgKind.
|
forall m pgKind.
|
||||||
MonadError QErr m =>
|
MonadError QErr m =>
|
||||||
@ -97,8 +143,8 @@ logicalModelToPreparedStatement ::
|
|||||||
logicalModelToPreparedStatement model = do
|
logicalModelToPreparedStatement model = do
|
||||||
let name = getLogicalModelName $ _lmmRootFieldName model
|
let name = getLogicalModelName $ _lmmRootFieldName model
|
||||||
let (preparedIQ, argumentMapping) = renameIQ $ _lmmCode model
|
let (preparedIQ, argumentMapping) = renameIQ $ _lmmCode model
|
||||||
code :: Text
|
logimoCode :: Text
|
||||||
code = renderIQ preparedIQ
|
logimoCode = renderIQ preparedIQ
|
||||||
prepname = "_logimo_vali_" <> toTxt name
|
prepname = "_logimo_vali_" <> toTxt name
|
||||||
|
|
||||||
occurringArguments, declaredArguments, undeclaredArguments :: Set LogicalModelArgumentName
|
occurringArguments, declaredArguments, undeclaredArguments :: Set LogicalModelArgumentName
|
||||||
@ -113,7 +159,24 @@ logicalModelToPreparedStatement model = do
|
|||||||
| argumentTypes /= mempty = "(" <> commaSeparated (pgScalarTypeToText <$> Map.elems argumentTypes) <> ")"
|
| argumentTypes /= mempty = "(" <> commaSeparated (pgScalarTypeToText <$> Map.elems argumentTypes) <> ")"
|
||||||
| otherwise = ""
|
| otherwise = ""
|
||||||
|
|
||||||
preparedQuery = "PREPARE " <> prepname <> argumentSignature <> " AS " <> code
|
returnedColumnNames :: Text
|
||||||
|
returnedColumnNames =
|
||||||
|
commaSeparated $ HashMap.keys (crtColumns (_lmmReturns model))
|
||||||
|
|
||||||
|
wrapInCTE :: Text -> Text
|
||||||
|
wrapInCTE query =
|
||||||
|
Text.intercalate
|
||||||
|
"\n"
|
||||||
|
[ "WITH " <> ctename <> " AS (",
|
||||||
|
query,
|
||||||
|
")",
|
||||||
|
"SELECT " <> returnedColumnNames,
|
||||||
|
"FROM " <> ctename
|
||||||
|
]
|
||||||
|
where
|
||||||
|
ctename = "_cte" <> prepname
|
||||||
|
|
||||||
|
preparedQuery = "PREPARE " <> prepname <> argumentSignature <> " AS " <> wrapInCTE logimoCode
|
||||||
|
|
||||||
when (Set.empty /= undeclaredArguments) $
|
when (Set.empty /= undeclaredArguments) $
|
||||||
throwError $
|
throwError $
|
||||||
@ -121,39 +184,3 @@ logicalModelToPreparedStatement model = do
|
|||||||
"Undeclared arguments: " <> commaSeparated (map tshow $ Set.toList undeclaredArguments)
|
"Undeclared arguments: " <> commaSeparated (map tshow $ Set.toList undeclaredArguments)
|
||||||
|
|
||||||
return preparedQuery
|
return preparedQuery
|
||||||
|
|
||||||
-- | Prepare a logical model query against a postgres-like database to validate it.
|
|
||||||
validateLogicalModel ::
|
|
||||||
forall m pgKind.
|
|
||||||
(MonadIO m, MonadError QErr m) =>
|
|
||||||
Env.Environment ->
|
|
||||||
PG.PostgresConnConfiguration ->
|
|
||||||
LogicalModelMetadata ('Postgres pgKind) ->
|
|
||||||
m ()
|
|
||||||
validateLogicalModel env connConf model = do
|
|
||||||
preparedQuery <- logicalModelToPreparedStatement model
|
|
||||||
|
|
||||||
-- We don't need to deallocate the prepared statement because 'withPostgresDB'
|
|
||||||
-- opens a new connection, runs a statement, and then closes the connection.
|
|
||||||
-- Since a prepared statement only lasts for the duration of the session, once
|
|
||||||
-- the session closes, the prepared statement is deallocated as well.
|
|
||||||
runRaw (PG.fromText $ preparedQuery)
|
|
||||||
where
|
|
||||||
runRaw :: PG.Query -> m ()
|
|
||||||
runRaw stmt =
|
|
||||||
liftEither
|
|
||||||
=<< liftIO
|
|
||||||
( withPostgresDB
|
|
||||||
env
|
|
||||||
connConf
|
|
||||||
( PG.rawQE
|
|
||||||
( \e ->
|
|
||||||
(err400 ValidationFailed "Failed to validate query")
|
|
||||||
{ qeInternal = Just $ ExtraInternal $ toJSON e
|
|
||||||
}
|
|
||||||
)
|
|
||||||
stmt
|
|
||||||
[]
|
|
||||||
False
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
@ -100,7 +100,8 @@ spec = do
|
|||||||
|
|
||||||
(first showQErr actual) `shouldSatisfy` isRight
|
(first showQErr actual) `shouldSatisfy` isRight
|
||||||
let Right rendered = actual
|
let Right rendered = actual
|
||||||
rendered `shouldBe` "PREPARE _logimo_vali_root_field_name(varchar) AS SELECT $1, $1"
|
rendered
|
||||||
|
`shouldBe` "PREPARE _logimo_vali_root_field_name(varchar) AS WITH _cte_logimo_vali_root_field_name AS (\nSELECT $1, $1\n)\nSELECT \nFROM _cte_logimo_vali_root_field_name"
|
||||||
|
|
||||||
it "Handles multiple variables " do
|
it "Handles multiple variables " do
|
||||||
let Right code = parseInterpolatedQuery "SELECT {{hey}}, {{ho}}"
|
let Right code = parseInterpolatedQuery "SELECT {{hey}}, {{ho}}"
|
||||||
@ -118,4 +119,5 @@ spec = do
|
|||||||
|
|
||||||
(first showQErr actual) `shouldSatisfy` isRight
|
(first showQErr actual) `shouldSatisfy` isRight
|
||||||
let Right rendered = actual
|
let Right rendered = actual
|
||||||
rendered `shouldBe` "PREPARE _logimo_vali_root_field_name(varchar, integer) AS SELECT $1, $2"
|
rendered
|
||||||
|
`shouldBe` "PREPARE _logimo_vali_root_field_name(varchar, integer) AS WITH _cte_logimo_vali_root_field_name AS (\nSELECT $1, $2\n)\nSELECT \nFROM _cte_logimo_vali_root_field_name"
|
||||||
|
Loading…
Reference in New Issue
Block a user