graphql-engine/server/src-lib/Hasura/Backends/Postgres/DDL/BoolExp.hs
Philip Lykke Carlsen c4536e801c Correctly translate permissions on functions to SQL, and simplify Bool Exp IR
Prior to this change, the SQL expression that resulted from translating permissions on functions would refer to the table of the function's return type, rather than the set of rows selected from the function being called.

Now the SQL that results from translating permissions correctly refer to the selected rows.

This PR also contains the suggested additions of https://github.com/hasura/graphql-engine-mono/pull/2563#discussion_r726116863, which simplifies the Boolean Expression IR, but in turn makes the Schema Dependency Discovery algorithm work a bit harder.

We are changing the definition of `data OpExpG`, but the format accepted by its JSON parser remains unchanged. While there does exist a generically derived `instance ToJSON OpExpG` this is only used in the (unpublished) `/v1/metadata/dump_internal_state` API.

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2609
Co-authored-by: Gil Mizrahi <8547573+soupi@users.noreply.github.com>
GitOrigin-RevId: bb9a0b4addbc239499dd2268909220196984df72
2021-10-18 09:17:42 +00:00

285 lines
14 KiB
Haskell

module Hasura.Backends.Postgres.DDL.BoolExp
( parseBoolExpOperations,
)
where
import Data.Aeson
import Data.HashMap.Strict qualified as Map
import Data.Text qualified as T
import Data.Text.Extended
import Hasura.Backends.Postgres.SQL.Types
import Hasura.Backends.Postgres.Types.BoolExp
import Hasura.Base.Error
import Hasura.Prelude
import Hasura.RQL.IR.BoolExp
import Hasura.RQL.Types.Backend
import Hasura.RQL.Types.Column
import Hasura.RQL.Types.SchemaCache
import Hasura.RQL.Types.Table
import Hasura.SQL.Backend
import Hasura.SQL.Types
parseBoolExpOperations ::
forall pgKind m v.
( Backend ('Postgres pgKind),
MonadError QErr m,
TableCoreInfoRM ('Postgres pgKind) m
) =>
ValueParser ('Postgres pgKind) m v ->
QualifiedTable ->
FieldInfoMap (FieldInfo ('Postgres pgKind)) ->
ColumnReference ('Postgres pgKind) ->
Value ->
m [OpExpG ('Postgres pgKind) v]
parseBoolExpOperations rhsParser rootTable fim columnRef value = do
restrictJSONColumn
withPathK (toTxt columnRef) $ parseOperations columnRef value
where
restrictJSONColumn :: m ()
restrictJSONColumn = case columnReferenceType columnRef of
ColumnScalar PGJSON ->
throwError (err400 UnexpectedPayload "JSON column can not be part of boolean expression")
_ -> pure ()
parseOperations :: ColumnReference ('Postgres pgKind) -> Value -> m [OpExpG ('Postgres pgKind) v]
parseOperations column = \case
Object o -> mapM (parseOperation column) (Map.toList o)
val -> pure . AEQ False <$> rhsParser columnType val
where
columnType = CollectableTypeScalar $ columnReferenceType column
parseOperation :: ColumnReference ('Postgres pgKind) -> (Text, Value) -> m (OpExpG ('Postgres pgKind) v)
parseOperation column (opStr, val) = withPathK opStr $
case opStr of
"$cast" -> parseCast
"_cast" -> parseCast
"$eq" -> parseEq
"_eq" -> parseEq
"$ne" -> parseNe
"_ne" -> parseNe
"$neq" -> parseNe
"_neq" -> parseNe
"$in" -> parseIn
"_in" -> parseIn
"$nin" -> parseNin
"_nin" -> parseNin
"$gt" -> parseGt
"_gt" -> parseGt
"$lt" -> parseLt
"_lt" -> parseLt
"$gte" -> parseGte
"_gte" -> parseGte
"$lte" -> parseLte
"_lte" -> parseLte
"$like" -> parseLike
"_like" -> parseLike
"$nlike" -> parseNlike
"_nlike" -> parseNlike
"$ilike" -> parseIlike
"_ilike" -> parseIlike
"$nilike" -> parseNilike
"_nilike" -> parseNilike
"$similar" -> parseSimilar
"_similar" -> parseSimilar
"$nsimilar" -> parseNsimilar
"_nsimilar" -> parseNsimilar
"$regex" -> parseRegex
"_regex" -> parseRegex
"$iregex" -> parseIRegex
"_iregex" -> parseIRegex
"$nregex" -> parseNRegex
"_nregex" -> parseNRegex
"$niregex" -> parseNIRegex
"_niregex" -> parseNIRegex
"$is_null" -> parseIsNull
"_is_null" -> parseIsNull
-- jsonb type
"_contains" -> guardType [PGJSONB] >> ABackendSpecific . AContains <$> parseOne
"$contains" -> guardType [PGJSONB] >> ABackendSpecific . AContains <$> parseOne
"_contained_in" -> guardType [PGJSONB] >> ABackendSpecific . AContainedIn <$> parseOne
"$contained_in" -> guardType [PGJSONB] >> ABackendSpecific . AContainedIn <$> parseOne
"_has_key" -> guardType [PGJSONB] >> ABackendSpecific . AHasKey <$> parseWithTy (ColumnScalar PGText)
"$has_key" -> guardType [PGJSONB] >> ABackendSpecific . AHasKey <$> parseWithTy (ColumnScalar PGText)
"_has_keys_any" -> guardType [PGJSONB] >> ABackendSpecific . AHasKeysAny <$> parseManyWithType (ColumnScalar PGText)
"$has_keys_any" -> guardType [PGJSONB] >> ABackendSpecific . AHasKeysAny <$> parseManyWithType (ColumnScalar PGText)
"_has_keys_all" -> guardType [PGJSONB] >> ABackendSpecific . AHasKeysAll <$> parseManyWithType (ColumnScalar PGText)
"$has_keys_all" -> guardType [PGJSONB] >> ABackendSpecific . AHasKeysAll <$> parseManyWithType (ColumnScalar PGText)
-- geometry types
"_st_contains" -> parseGeometryOp ASTContains
"$st_contains" -> parseGeometryOp ASTContains
"_st_crosses" -> parseGeometryOp ASTCrosses
"$st_crosses" -> parseGeometryOp ASTCrosses
"_st_equals" -> parseGeometryOp ASTEquals
"$st_equals" -> parseGeometryOp ASTEquals
"_st_overlaps" -> parseGeometryOp ASTOverlaps
"$st_overlaps" -> parseGeometryOp ASTOverlaps
"_st_touches" -> parseGeometryOp ASTTouches
"$st_touches" -> parseGeometryOp ASTTouches
"_st_within" -> parseGeometryOp ASTWithin
"$st_within" -> parseGeometryOp ASTWithin
-- geometry and geography types
"_st_intersects" -> parseGeometryOrGeographyOp ASTIntersects
"$st_intersects" -> parseGeometryOrGeographyOp ASTIntersects
"_st_3d_intersects" -> parseGeometryOp AST3DIntersects
"$st_3d_intersects" -> parseGeometryOp AST3DIntersects
"_st_d_within" -> parseSTDWithinObj
"$st_d_within" -> parseSTDWithinObj
"_st_3d_d_within" -> parseST3DDWithinObj
"$st_3d_d_within" -> parseST3DDWithinObj
"$ceq" -> parseCeq
"_ceq" -> parseCeq
"$cne" -> parseCne
"_cne" -> parseCne
"$cneq" -> parseCne
"_cneq" -> parseCne
"$cgt" -> parseCgt
"_cgt" -> parseCgt
"$clt" -> parseClt
"_clt" -> parseClt
"$cgte" -> parseCgte
"_cgte" -> parseCgte
"$clte" -> parseClte
"_clte" -> parseClte
-- ltree types
"_ancestor" -> guardType [PGLtree] >> ABackendSpecific . AAncestor <$> parseOne
"$ancestor" -> guardType [PGLtree] >> ABackendSpecific . AAncestor <$> parseOne
"_ancestor_any" -> guardType [PGLtree] >> ABackendSpecific . AAncestorAny <$> parseManyWithType (ColumnScalar PGLtree)
"$ancestor_any" -> guardType [PGLtree] >> ABackendSpecific . AAncestorAny <$> parseManyWithType (ColumnScalar PGLtree)
"_descendant" -> guardType [PGLtree] >> ABackendSpecific . ADescendant <$> parseOne
"$descendant" -> guardType [PGLtree] >> ABackendSpecific . ADescendant <$> parseOne
"_descendant_any" -> guardType [PGLtree] >> ABackendSpecific . ADescendantAny <$> parseManyWithType (ColumnScalar PGLtree)
"$descendant_any" -> guardType [PGLtree] >> ABackendSpecific . ADescendantAny <$> parseManyWithType (ColumnScalar PGLtree)
"_matches" -> guardType [PGLtree] >> ABackendSpecific . AMatches <$> parseWithTy (ColumnScalar PGLquery)
"$matches" -> guardType [PGLtree] >> ABackendSpecific . AMatches <$> parseWithTy (ColumnScalar PGLquery)
"_matches_any" -> guardType [PGLtree] >> ABackendSpecific . AMatchesAny <$> parseManyWithType (ColumnScalar PGLquery)
"$matches_any" -> guardType [PGLtree] >> ABackendSpecific . AMatchesAny <$> parseManyWithType (ColumnScalar PGLquery)
"_matches_fulltext" -> guardType [PGLtree] >> ABackendSpecific . AMatchesFulltext <$> parseWithTy (ColumnScalar PGLtxtquery)
"$matches_fulltext" -> guardType [PGLtree] >> ABackendSpecific . AMatchesFulltext <$> parseWithTy (ColumnScalar PGLtxtquery)
x -> throw400 UnexpectedPayload $ "Unknown operator : " <> x
where
colTy = columnReferenceType column
parseIsNull = bool ANISNOTNULL ANISNULL <$> parseVal -- is null
parseEq = AEQ False <$> parseOne -- equals
parseNe = ANE False <$> parseOne -- <>
parseIn = AIN <$> parseManyWithType colTy -- in an array
parseNin = ANIN <$> parseManyWithType colTy -- not in an array
parseGt = AGT <$> parseOne -- >
parseLt = ALT <$> parseOne -- <
parseGte = AGTE <$> parseOne -- >=
parseLte = ALTE <$> parseOne -- <=
parseCeq = CEQ <$> decodeAndValidateRhsCol val
parseCne = CNE <$> decodeAndValidateRhsCol val
parseCgt = CGT <$> decodeAndValidateRhsCol val
parseClt = CLT <$> decodeAndValidateRhsCol val
parseCgte = CGTE <$> decodeAndValidateRhsCol val
parseClte = CLTE <$> decodeAndValidateRhsCol val
parseLike = guardType stringTypes >> ALIKE <$> parseOne
parseNlike = guardType stringTypes >> ANLIKE <$> parseOne
parseIlike = guardType stringTypes >> ABackendSpecific . AILIKE <$> parseOne
parseNilike = guardType stringTypes >> ABackendSpecific . ANILIKE <$> parseOne
parseRegex = guardType stringTypes >> ABackendSpecific . AREGEX <$> parseOne
parseIRegex = guardType stringTypes >> ABackendSpecific . AIREGEX <$> parseOne
parseNRegex = guardType stringTypes >> ABackendSpecific . ANREGEX <$> parseOne
parseNIRegex = guardType stringTypes >> ABackendSpecific . ANIREGEX <$> parseOne
parseSimilar = guardType stringTypes >> ABackendSpecific . ASIMILAR <$> parseOne
parseNsimilar = guardType stringTypes >> ABackendSpecific . ANSIMILAR <$> parseOne
parseCast = do
castOperations <- parseVal
parsedCastOperations <-
forM (Map.toList castOperations) $ \(targetTypeName, castedComparisons) -> do
let targetType = textToPGScalarType targetTypeName
castedColumn = ColumnReferenceCast column (ColumnScalar targetType)
checkValidCast targetType
parsedCastedComparisons <-
withPathK targetTypeName $
parseOperations castedColumn castedComparisons
return (targetType, parsedCastedComparisons)
return . ACast $ Map.fromList parsedCastOperations
checkValidCast targetType = case (colTy, targetType) of
(ColumnScalar PGGeometry, PGGeography) -> return ()
(ColumnScalar PGGeography, PGGeometry) -> return ()
_ ->
throw400 UnexpectedPayload $
"cannot cast column of type " <> colTy <<> " to type " <>> targetType
parseGeometryOp f =
guardType [PGGeometry] >> ABackendSpecific . f <$> parseOneNoSess colTy val
parseGeometryOrGeographyOp f =
guardType geoTypes >> ABackendSpecific . f <$> parseOneNoSess colTy val
parseSTDWithinObj =
ABackendSpecific <$> case colTy of
ColumnScalar PGGeometry -> do
DWithinGeomOp distVal fromVal <- parseVal
dist <- withPathK "distance" $ parseOneNoSess (ColumnScalar PGFloat) distVal
from <- withPathK "from" $ parseOneNoSess colTy fromVal
return $ ASTDWithinGeom $ DWithinGeomOp dist from
ColumnScalar PGGeography -> do
DWithinGeogOp distVal fromVal sphVal <- parseVal
dist <- withPathK "distance" $ parseOneNoSess (ColumnScalar PGFloat) distVal
from <- withPathK "from" $ parseOneNoSess colTy fromVal
useSpheroid <- withPathK "use_spheroid" $ parseOneNoSess (ColumnScalar PGBoolean) sphVal
return $ ASTDWithinGeog $ DWithinGeogOp dist from useSpheroid
_ -> throwError $ buildMsg colTy [PGGeometry, PGGeography]
decodeAndValidateRhsCol :: Value -> m (RootOrCurrentColumn ('Postgres pgKind))
decodeAndValidateRhsCol v = case v of
String _ -> go IsCurrent fim v
Array path -> case toList path of
[] -> throw400 Unexpected "path cannot be empty"
[col] -> go IsCurrent fim col
[String "$", col] -> do
rootTableInfo <-
lookupTableCoreInfo rootTable
>>= flip onNothing (throw500 $ "unexpected: " <> rootTable <<> " doesn't exist")
go IsRoot (_tciFieldInfoMap rootTableInfo) col
_ -> throw400 NotSupported "Relationship references are not supported in column comparison RHS"
_ -> throw400 Unexpected "a boolean expression JSON must be either a string or an array"
where
go rootInfo fieldsInfoMap columnValue = do
colName <- decodeValue columnValue
colInfo <- validateRhsCol fieldsInfoMap colName
pure $ RootOrCurrentColumn rootInfo colInfo
parseST3DDWithinObj =
ABackendSpecific <$> do
guardType [PGGeometry]
DWithinGeomOp distVal fromVal <- parseVal
dist <- withPathK "distance" $ parseOneNoSess (ColumnScalar PGFloat) distVal
from <- withPathK "from" $ parseOneNoSess colTy fromVal
return $ AST3DDWithinGeom $ DWithinGeomOp dist from
validateRhsCol fieldInfoMap rhsCol = do
rhsType <- askColumnType fieldInfoMap rhsCol "column operators can only compare postgres columns"
when (colTy /= rhsType) $
throw400 UnexpectedPayload $
"incompatible column types: "
<> column <<> " has type "
<> colTy <<> ", but "
<> rhsCol <<> " has type " <>> rhsType
pure rhsCol
parseWithTy ty = rhsParser (CollectableTypeScalar ty) val
-- parse one with the column's type
parseOne = parseWithTy colTy
parseOneNoSess ty = rhsParser (CollectableTypeScalar ty)
parseManyWithType ty = rhsParser (CollectableTypeArray ty) val
guardType validTys =
unless (isScalarColumnWhere (`elem` validTys) colTy) $
throwError $ buildMsg colTy validTys
buildMsg ty expTys =
err400 UnexpectedPayload $
" is of type " <> ty <<> "; this operator works only on columns of type "
<> T.intercalate "/" (map dquote expTys)
parseVal :: (FromJSON a) => m a
parseVal = decodeValue val