{-# LANGUAGE PartialTypeSignatures #-} -- | Postgres Translate BoolExp -- -- Convert IR boolean expressions to Postgres-specific SQL expressions. module Hasura.Backends.Postgres.Translate.BoolExp ( toSQLBoolExp, annBoolExp, ) where import Data.HashMap.Strict qualified as M import Data.Text.Extended (ToTxt) import Hasura.Backends.Postgres.SQL.DML qualified as S import Hasura.Backends.Postgres.SQL.Types hiding (TableName) import Hasura.Backends.Postgres.Types.BoolExp import Hasura.Backends.Postgres.Types.Function (onArgumentExp) import Hasura.Base.Error import Hasura.Prelude import Hasura.RQL.IR.BoolExp import Hasura.RQL.IR.BoolExp.AggregationPredicates (AggregationPredicate (..), AggregationPredicateArguments (..), AggregationPredicatesImplementation (..)) import Hasura.RQL.Types.Backend import Hasura.RQL.Types.BoolExp import Hasura.RQL.Types.Column import Hasura.RQL.Types.Common import Hasura.RQL.Types.Function import Hasura.RQL.Types.Metadata.Backend import Hasura.RQL.Types.Relationships.Local import Hasura.RQL.Types.SchemaCache hiding (BoolExpCtx (..), BoolExpM (..)) import Hasura.RQL.Types.Table import Hasura.SQL.Backend import Hasura.SQL.Types -- This convoluted expression instead of col = val -- to handle the case of col : null equalsBoolExpBuilder :: SQLExpression ('Postgres pgKind) -> SQLExpression ('Postgres pgKind) -> S.BoolExp equalsBoolExpBuilder qualColExp rhsExp = S.BEBin S.OrOp (S.BECompare S.SEQ qualColExp rhsExp) ( S.BEBin S.AndOp (S.BENull qualColExp) (S.BENull rhsExp) ) notEqualsBoolExpBuilder :: SQLExpression ('Postgres pgKind) -> SQLExpression ('Postgres pgKind) -> S.BoolExp notEqualsBoolExpBuilder qualColExp rhsExp = S.BEBin S.OrOp (S.BECompare S.SNE qualColExp rhsExp) ( S.BEBin S.AndOp (S.BENotNull qualColExp) (S.BENull rhsExp) ) annBoolExp :: (QErrM m, TableCoreInfoRM b m, BackendMetadata b) => BoolExpRHSParser b m v -> TableName b -> FieldInfoMap (FieldInfo b) -> GBoolExp b ColExp -> m (AnnBoolExp b v) annBoolExp rhsParser rootTable fim boolExp = case boolExp of BoolAnd exps -> BoolAnd <$> procExps exps BoolOr exps -> BoolOr <$> procExps exps BoolNot e -> BoolNot <$> annBoolExp rhsParser rootTable fim e BoolExists (GExists refqt whereExp) -> withPathK "_exists" $ do refFields <- withPathK "_table" $ askFieldInfoMapSource refqt annWhereExp <- withPathK "_where" $ annBoolExp rhsParser rootTable refFields whereExp return $ BoolExists $ GExists refqt annWhereExp BoolField fld -> BoolField <$> annColExp rhsParser rootTable fim fld where procExps = mapM (annBoolExp rhsParser rootTable fim) annColExp :: (QErrM m, TableCoreInfoRM b m, BackendMetadata b) => BoolExpRHSParser b m v -> TableName b -> FieldInfoMap (FieldInfo b) -> ColExp -> m (AnnBoolExpFld b v) annColExp rhsParser rootTable colInfoMap (ColExp fieldName colVal) = do colInfo <- askFieldInfo colInfoMap fieldName case colInfo of FIColumn pgi -> AVColumn pgi <$> parseBoolExpOperations (_berpValueParser rhsParser) rootTable colInfoMap (ColumnReferenceColumn pgi) colVal FIRelationship relInfo -> do relBoolExp <- decodeValue colVal relFieldInfoMap <- askFieldInfoMapSource $ riRTable relInfo annRelBoolExp <- annBoolExp rhsParser rootTable relFieldInfoMap $ unBoolExp relBoolExp return $ AVRelationship relInfo annRelBoolExp FIComputedField computedFieldInfo -> AVComputedField <$> buildComputedFieldBooleanExp (BoolExpResolver annBoolExp) rhsParser rootTable colInfoMap computedFieldInfo colVal -- Using remote fields in the boolean expression is not supported. FIRemoteRelationship {} -> throw400 UnexpectedPayload "remote field unsupported" -- | Translate an IR boolean expression to an SQL boolean expression. References -- to columns etc are relative to the given 'rootReference'. toSQLBoolExp :: forall pgKind. Backend ('Postgres pgKind) => -- | The name of the tabular value in query scope that the boolean expression -- applies to S.Qual -> -- | The boolean expression to translate AnnBoolExpSQL ('Postgres pgKind) -> S.BoolExp toSQLBoolExp rootReference e = evalState ( runReaderT (unBoolExpM (translateBoolExp e)) initialCtx ) 0 where initialCtx = BoolExpCtx { currTableReference = rootReference, rootReference = rootReference } -- | The table context of boolean expression translation. This is used to -- resolve references to fields, as those may refer to the so-called 'root -- table' (identified by a '$'-sign in the expression input syntax) or the -- 'current' table. data BoolExpCtx = BoolExpCtx { -- | Reference to the current tabular value. currTableReference :: S.Qual, -- | Reference to the root tabular value. rootReference :: S.Qual } -- | The monad that carries the translation of boolean expressions. This -- supports the generation of fresh names for aliasing sub-expressions and -- maintains the table context of the expressions being translated. newtype BoolExpM a = BoolExpM {unBoolExpM :: ReaderT BoolExpCtx (State Word64) a} deriving (Functor, Applicative, Monad, MonadReader BoolExpCtx, MonadState Word64) -- | Translate a 'GBoolExp' with annotated SQLExpressions in the leaves into a -- bare SQL Boolean Expression. translateBoolExp :: forall pgKind. (Backend ('Postgres pgKind)) => AnnBoolExpSQL ('Postgres pgKind) -> BoolExpM S.BoolExp translateBoolExp = \case BoolAnd bes -> do sqlBExps <- mapM translateBoolExp bes return $ sqlAnd sqlBExps BoolOr bes -> do sqlBExps <- mapM translateBoolExp bes return $ foldr (S.BEBin S.OrOp) (S.BELit False) sqlBExps BoolNot notExp -> S.BENot <$> translateBoolExp notExp BoolExists (GExists currTableReference wh) -> do whereExp <- withCurrentTable (S.QualTable currTableReference) (translateBoolExp wh) return $ S.mkExists (S.FISimple currTableReference Nothing) whereExp BoolField boolExp -> case boolExp of AVColumn colInfo opExps -> do BoolExpCtx {rootReference, currTableReference} <- ask let colFld = fromCol @('Postgres pgKind) $ ciColumn colInfo bExps = map (mkFieldCompExp rootReference currTableReference $ LColumn colFld) opExps return $ sqlAnd bExps AVRelationship (RelInfo _ _ colMapping relTN _ _) nesAnn -> do -- Convert the where clause on the relationship aliasRelTN <- freshIdentifier relTN annRelBoolExp <- withCurrentTable (S.QualifiedIdentifier aliasRelTN Nothing) (translateBoolExp nesAnn) BoolExpCtx {currTableReference} <- ask let tableRelExp = translateTableRelationship colMapping aliasRelTN currTableReference let innerBoolExp = S.BEBin S.AndOp tableRelExp annRelBoolExp return $ S.mkExists (S.FISimple relTN $ Just $ S.toTableAlias aliasRelTN) innerBoolExp AVComputedField (AnnComputedFieldBoolExp _ _ function sessionArgPresence cfBoolExp) -> do case cfBoolExp of CFBEScalar opExps -> do BoolExpCtx {rootReference, currTableReference} <- ask -- Convert the where clause on scalar computed field let bExps = map (mkFieldCompExp rootReference currTableReference $ LComputedField function sessionArgPresence) opExps pure $ sqlAnd bExps CFBETable _ be -> do -- Convert the where clause on table computed field BoolExpCtx {currTableReference} <- ask aliasFunction <- freshIdentifier function let functionExp = mkComputedFieldFunctionExp currTableReference function sessionArgPresence $ Just $ S.toTableAlias aliasFunction S.mkExists (S.FIFunc functionExp) <$> withCurrentTable (S.QualifiedIdentifier aliasFunction Nothing) (translateBoolExp be) AVAggregationPredicates aggPreds -> translateAVAggregationPredicates aggPreds -- | Call a given translation action recursively using the given identifier for the 'current' table. withCurrentTable :: forall a. S.Qual -> BoolExpM a -> BoolExpM a withCurrentTable curr = local (\e -> e {currTableReference = curr}) -- | Draw a fresh identifier intended to alias the given object. freshIdentifier :: forall a. ToTxt a => QualifiedObject a -> BoolExpM Identifier freshIdentifier obj = do curVarNum <- get put $ curVarNum + 1 let newIdentifier = Identifier $ "_be_" <> tshow curVarNum <> "_" <> snakeCaseQualifiedObject obj return newIdentifier identifierWithSuffix :: ToTxt a => QualifiedObject a -> Text -> Identifier identifierWithSuffix relTableName name = Identifier (snakeCaseQualifiedObject relTableName <> "_" <> name) -- | Given a GraphQL aggregation filter of the form: -- > { where: {