module Hasura.RQL.DML.Select ( runSelect, ) where import Control.Monad.Trans.Control (MonadBaseControl) import Data.HashMap.Strict qualified as HashMap import Data.List.NonEmpty qualified as NE import Data.Sequence qualified as DS import Data.Text.Extended import Database.PG.Query qualified as PG import Hasura.Backends.Postgres.Connection.MonadTx import Hasura.Backends.Postgres.Execute.Types import Hasura.Backends.Postgres.SQL.DML qualified as S import Hasura.Backends.Postgres.SQL.Types hiding (TableName) import Hasura.Backends.Postgres.Translate.Select import Hasura.Backends.Postgres.Translate.Select.Internal.Helpers (selectToSelectWithM, toQuery) import Hasura.Base.Error import Hasura.EncJSON import Hasura.LogicalModel.Cache (LogicalModelCache, LogicalModelInfo (..)) import Hasura.LogicalModel.Fields (LogicalModelFieldsRM, runLogicalModelFieldsLookup) import Hasura.Prelude import Hasura.RQL.DML.Internal import Hasura.RQL.DML.Types import Hasura.RQL.IR.BoolExp import Hasura.RQL.IR.OrderBy import Hasura.RQL.IR.Select import Hasura.RQL.Types.Backend import Hasura.RQL.Types.BackendType import Hasura.RQL.Types.Column import Hasura.RQL.Types.Common import Hasura.RQL.Types.Metadata import Hasura.RQL.Types.Relationships.Local import Hasura.RQL.Types.SchemaCache import Hasura.Session import Hasura.Table.Cache import Hasura.Tracing qualified as Tracing type SelectQExt = SelectG (ExtCol ('Postgres 'Vanilla)) (BoolExp ('Postgres 'Vanilla)) Int -- Columns in RQL -- This technically doesn't need to be generalized to all backends as -- it is specific to this module; however the generalization work was -- already done, and there's no particular reason to force this to be -- specific. data ExtCol (b :: BackendType) = ECSimple (Column b) | ECRel RelName (Maybe RelName) SelectQExt convSelCol :: (UserInfoM m, QErrM m, TableInfoRM ('Postgres 'Vanilla) m) => FieldInfoMap (FieldInfo ('Postgres 'Vanilla)) -> SelPermInfo ('Postgres 'Vanilla) -> SelCol -> m [ExtCol ('Postgres 'Vanilla)] convSelCol _ _ (SCExtSimple cn) = pure [ECSimple cn] convSelCol fieldInfoMap _ (SCExtRel rn malias selQ) = do -- Point to the name key let pgWhenRelErr = "only relationships can be expanded" relInfo <- withPathK "name" $ askRelType fieldInfoMap rn pgWhenRelErr case relInfo of (RelInfo {riTarget = RelTargetNativeQuery _}) -> error "convSelCol RelTargetNativeQuery" (RelInfo {riTarget = RelTargetTable relTable}) -> do (rfim, rspi) <- fetchRelDet rn relTable resolvedSelQ <- resolveStar rfim rspi selQ pure [ECRel rn malias resolvedSelQ] convSelCol fieldInfoMap spi (SCStar wildcard) = convWildcard fieldInfoMap spi wildcard convWildcard :: (UserInfoM m, QErrM m, TableInfoRM ('Postgres 'Vanilla) m) => FieldInfoMap (FieldInfo ('Postgres 'Vanilla)) -> SelPermInfo ('Postgres 'Vanilla) -> Wildcard -> m [ExtCol ('Postgres 'Vanilla)] convWildcard fieldInfoMap selPermInfo wildcard = case wildcard of Star -> pure simpleCols (StarDot wc) -> (simpleCols ++) <$> (catMaybes <$> relExtCols wc) where cols = spiCols selPermInfo pgCols = map structuredColumnInfoColumn $ getCols fieldInfoMap relColInfos = getRels fieldInfoMap simpleCols = map ECSimple $ filter (`HashMap.member` cols) pgCols mkRelCol _wc (RelInfo {riTarget = RelTargetNativeQuery _}) = error "convWildcard RelTargetNativeQuery" mkRelCol wc (RelInfo {riName = relName, riTarget = RelTargetTable relTableName}) = do relTabInfo <- fetchRelTabInfo relTableName mRelSelPerm <- askPermInfo permSel relTabInfo forM mRelSelPerm $ \relSelPermInfo -> do rExtCols <- convWildcard (_tciFieldInfoMap $ _tiCoreInfo relTabInfo) relSelPermInfo wc pure $ ECRel relName Nothing $ SelectG rExtCols Nothing Nothing Nothing Nothing relExtCols wc = mapM (mkRelCol wc) relColInfos resolveStar :: (UserInfoM m, QErrM m, TableInfoRM ('Postgres 'Vanilla) m) => FieldInfoMap (FieldInfo ('Postgres 'Vanilla)) -> SelPermInfo ('Postgres 'Vanilla) -> SelectQ -> m SelectQExt resolveStar fim selPermInfo (SelectG selCols mWh mOb mLt mOf) = do procOverrides <- fmap (concat . catMaybes) $ withPathK "columns" $ indexedForM selCols $ \selCol -> case selCol of (SCStar _) -> pure Nothing _ -> Just <$> convSelCol fim selPermInfo selCol everything <- case wildcards of [] -> pure [] _ -> convWildcard fim selPermInfo $ maximum wildcards let extCols = unionBy equals procOverrides everything pure $ SelectG extCols mWh mOb mLt mOf where wildcards = lefts $ map mkEither selCols mkEither (SCStar wc) = Left wc mkEither selCol = Right selCol equals (ECSimple x) (ECSimple y) = x == y equals (ECRel x _ _) (ECRel y _ _) = x == y equals _ _ = False convOrderByElem :: (UserInfoM m, QErrM m, TableInfoRM ('Postgres 'Vanilla) m) => SessionVariableBuilder m -> (FieldInfoMap (FieldInfo ('Postgres 'Vanilla)), SelPermInfo ('Postgres 'Vanilla)) -> OrderByCol -> m (AnnotatedOrderByElement ('Postgres 'Vanilla) S.SQLExp) convOrderByElem sessVarBldr (flds, spi) = \case OCPG fldName -> do fldInfo <- askFieldInfo flds fldName case fldInfo of FIColumn (SCIScalarColumn colInfo) -> do checkSelOnCol spi (ciColumn colInfo) let redactionExp = fromMaybe NoRedaction $ HashMap.lookup (ciColumn colInfo) (spiCols spi) resolvedRedactionExp <- convAnnRedactionExpPartialSQL sessVarBldr redactionExp let ty = ciType colInfo if isScalarColumnWhere isGeoType ty then throw400 UnexpectedPayload $ fldName <<> " has type 'geometry' and cannot be used in order_by" else pure $ AOCColumn colInfo resolvedRedactionExp FIRelationship _ -> throw400 UnexpectedPayload $ fldName <<> " is a relationship and should be expanded" FIComputedField _ -> throw400 UnexpectedPayload $ fldName <<> " is a computed field and can't be used in 'order_by'" -- TODO Rakesh (from master) FIRemoteRelationship {} -> throw400 UnexpectedPayload (fldName <<> " is a remote field") OCRel fldName rest -> do fldInfo <- askFieldInfo flds fldName case fldInfo of FIColumn _ -> throw400 UnexpectedPayload $ fldName <<> " is a Postgres column and cannot be chained further" FIComputedField _ -> throw400 UnexpectedPayload $ fldName <<> " is a computed field and can't be used in 'order_by'" FIRelationship relInfo -> do relTableName <- case riTarget relInfo of RelTargetTable tn -> pure tn RelTargetNativeQuery _ -> error "convOrderByElem RelTargetNativeQuery" when (riType relInfo == ArrRel) $ throw400 UnexpectedPayload $ fldName <<> " is an array relationship and can't be used in 'order_by'" (relFim, relSelPermInfo) <- fetchRelDet (riName relInfo) relTableName resolvedSelFltr <- convAnnBoolExpPartialSQL sessVarBldr $ spiFilter relSelPermInfo AOCObjectRelation relInfo resolvedSelFltr <$> convOrderByElem sessVarBldr (relFim, relSelPermInfo) rest FIRemoteRelationship {} -> throw400 UnexpectedPayload (fldName <<> " is a remote field") convSelectQ :: ( UserInfoM m, QErrM m, TableInfoRM ('Postgres 'Vanilla) m, LogicalModelFieldsRM ('Postgres 'Vanilla) m ) => SQLGenCtx -> TableName ('Postgres 'Vanilla) -> FieldInfoMap (FieldInfo ('Postgres 'Vanilla)) -> -- Table information of current table SelPermInfo ('Postgres 'Vanilla) -> -- Additional select permission info SelectQExt -> -- Given Select Query SessionVariableBuilder m -> ValueParser ('Postgres 'Vanilla) m S.SQLExp -> m (AnnSimpleSelect ('Postgres 'Vanilla)) convSelectQ sqlGen table fieldInfoMap selPermInfo selQ sessVarBldr prepValBldr = do -- Convert where clause wClause <- forM (sqWhere selQ) $ \boolExp -> withPathK "where" $ convBoolExp fieldInfoMap selPermInfo boolExp sessVarBldr fieldInfoMap prepValBldr annFlds <- withPathK "columns" $ indexedForM (sqColumns selQ) $ \case (ECSimple pgCol) -> do (colInfo, redactionExp) <- convExtSimple fieldInfoMap selPermInfo pgCol resolvedRedactionExp <- convAnnRedactionExpPartialSQL sessVarBldr redactionExp pure (fromCol @('Postgres 'Vanilla) pgCol, mkAnnColumnField (ciColumn colInfo) (ciType colInfo) resolvedRedactionExp Nothing) (ECRel relName mAlias relSelQ) -> do annRel <- convExtRel sqlGen fieldInfoMap relName mAlias relSelQ sessVarBldr prepValBldr pure ( fromRel $ fromMaybe relName mAlias, either AFObjectRelation AFArrayRelation annRel ) annOrdByML <- forM (sqOrderBy selQ) $ \(OrderByExp obItems) -> withPathK "order_by" $ indexedForM obItems $ mapM $ convOrderByElem sessVarBldr (fieldInfoMap, selPermInfo) let annOrdByM = NE.nonEmpty =<< annOrdByML -- validate limit and offset values withPathK "limit" $ mapM_ onlyPositiveInt mQueryLimit withPathK "offset" $ mapM_ onlyPositiveInt mQueryOffset resolvedSelFltr <- convAnnBoolExpPartialSQL sessVarBldr $ spiFilter selPermInfo let tabFrom = FromTable table tabPerm = TablePerm resolvedSelFltr mPermLimit tabArgs = SelectArgs wClause annOrdByM mQueryLimit (fromIntegral <$> mQueryOffset) Nothing strfyNum = stringifyNum sqlGen pure $ AnnSelectG annFlds tabFrom tabPerm tabArgs strfyNum Nothing where mQueryOffset = sqOffset selQ mQueryLimit = sqLimit selQ mPermLimit = spiLimit selPermInfo convExtSimple :: (UserInfoM m, QErrM m) => FieldInfoMap (FieldInfo ('Postgres 'Vanilla)) -> SelPermInfo ('Postgres 'Vanilla) -> PGCol -> m (ColumnInfo ('Postgres 'Vanilla), AnnRedactionExpPartialSQL ('Postgres 'Vanilla)) convExtSimple fieldInfoMap selPermInfo pgCol = do checkSelOnCol selPermInfo pgCol colInfo <- askColInfo fieldInfoMap pgCol relWhenPGErr pure (colInfo, fromMaybe NoRedaction $ HashMap.lookup pgCol (spiCols selPermInfo)) where relWhenPGErr = "relationships have to be expanded" convExtRel :: ( UserInfoM m, QErrM m, TableInfoRM ('Postgres 'Vanilla) m, LogicalModelFieldsRM ('Postgres 'Vanilla) m ) => SQLGenCtx -> FieldInfoMap (FieldInfo ('Postgres 'Vanilla)) -> RelName -> Maybe RelName -> SelectQExt -> SessionVariableBuilder m -> ValueParser ('Postgres 'Vanilla) m S.SQLExp -> m (Either (ObjectRelationSelect ('Postgres 'Vanilla)) (ArraySelect ('Postgres 'Vanilla))) convExtRel sqlGen fieldInfoMap relName mAlias selQ sessVarBldr prepValBldr = do -- Point to the name key relInfo <- withPathK "name" $ askRelType fieldInfoMap relName pgWhenRelErr let (RelInfo {riType = relTy, riMapping = colMapping, riTarget = relTarget}) = relInfo relTableName <- case relTarget of RelTargetNativeQuery _ -> error "convExtRel RelTargetNativeQuery" RelTargetTable tn -> pure tn (relCIM, relSPI) <- fetchRelDet relName relTableName annSel <- convSelectQ sqlGen relTableName relCIM relSPI selQ sessVarBldr prepValBldr case relTy of ObjRel -> do when misused $ throw400 UnexpectedPayload objRelMisuseMsg pure $ Left $ AnnRelationSelectG (fromMaybe relName mAlias) colMapping Nullable $ AnnObjectSelectG (_asnFields annSel) (FromTable relTableName) $ _tpFilter $ _asnPerm annSel ArrRel -> pure $ Right $ ASSimple $ AnnRelationSelectG (fromMaybe relName mAlias) colMapping Nullable annSel where pgWhenRelErr = "only relationships can be expanded" misused = or [ isJust (sqWhere selQ), isJust (sqLimit selQ), isJust (sqOffset selQ), isJust (sqOrderBy selQ) ] objRelMisuseMsg = "when selecting an 'obj_relationship' 'where', 'order_by', 'limit' and 'offset' can't be used" convSelectQuery :: ( UserInfoM m, QErrM m, TableInfoRM ('Postgres 'Vanilla) m, LogicalModelFieldsRM ('Postgres 'Vanilla) m ) => SQLGenCtx -> SessionVariableBuilder m -> ValueParser ('Postgres 'Vanilla) m S.SQLExp -> SelectQuery -> m (AnnSimpleSelect ('Postgres 'Vanilla)) convSelectQuery sqlGen sessVarBldr prepArgBuilder (DMLQuery _ qt selQ) = do tabInfo <- withPathK "table" $ askTableInfoSource qt selPermInfo <- askSelPermInfo tabInfo let fieldInfo = _tciFieldInfoMap $ _tiCoreInfo tabInfo extSelQ <- resolveStar fieldInfo selPermInfo selQ validateHeaders $ spiRequiredHeaders selPermInfo convSelectQ sqlGen qt fieldInfo selPermInfo extSelQ sessVarBldr prepArgBuilder selectP2 :: UserInfo -> JsonAggSelect -> (AnnSimpleSelect ('Postgres 'Vanilla), DS.Seq PG.PrepArg) -> PG.TxE QErr EncJSON selectP2 userInfo jsonAggSelect (sel, p) = do selectWithQuery <- selectToSelectWithM $ mkSQLSelect userInfo jsonAggSelect sel let selectSQL = toQuery $ selectWithQuery runIdentity . PG.getRow <$> PG.rawQE dmlTxErrorHandler selectSQL (toList p) True phaseOne :: (QErrM m, UserInfoM m, CacheRM m) => SQLGenCtx -> SelectQuery -> m (AnnSimpleSelect ('Postgres 'Vanilla), DS.Seq PG.PrepArg) phaseOne sqlGen query = do let sourceName = getSourceDMLQuery query tableCache :: TableCache ('Postgres 'Vanilla) <- fold <$> askTableCache sourceName logicalModelCache :: LogicalModelCache ('Postgres 'Vanilla) <- fold <$> askLogicalModelCache sourceName flip runTableCacheRT tableCache $ runLogicalModelFieldsLookup _lmiFields logicalModelCache $ runDMLP1T $ convSelectQuery sqlGen sessVarFromCurrentSetting (valueParserWithCollectableType binRHSBuilder) query phaseTwo :: (MonadTx m) => UserInfo -> (AnnSimpleSelect ('Postgres 'Vanilla), DS.Seq PG.PrepArg) -> m EncJSON phaseTwo userInfo = liftTx . selectP2 userInfo JASMultipleRows runSelect :: ( QErrM m, UserInfoM m, CacheRM m, MonadIO m, MonadBaseControl IO m, Tracing.MonadTrace m, MetadataM m ) => SQLGenCtx -> SelectQuery -> m EncJSON runSelect sqlGen q = do sourceConfig <- askSourceConfig @('Postgres 'Vanilla) (getSourceDMLQuery q) userInfo <- askUserInfo phaseOne sqlGen q >>= runTxWithCtx (_pscExecCtx sourceConfig) (Tx PG.ReadOnly Nothing) LegacyRQLQuery . (phaseTwo userInfo)