mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 12:31:52 +03:00
e5f88d8039
## Description This change adds support for querying into nested arrays in Data Connector agents that support such a concept (currently MongoDB). ### DC API changes - New API type `ColumnType` which allows representing the type of a "column" as either a scalar type, an object reference or an array of `ColumnType`s. This recursive definition allows arbitrary nesting of arrays of types. - The `type` fields in the API types `ColumnInfo` and `ColumnInsertSchema` now take a `ColumnType` instead of a `ScalarType`. - To ensure backwards compatibility, a `ColumnType` representing a scalar serialises and deserialises to the same representation as `ScalarType`. - In queries, the `Field` type now has a new constructor `NestedArrayField`. This contains a nested `Field` along with optional `limit`, `offset`, `where` and `order_by` arguments. (These optional arguments are not yet used by either HGE or the MongoDB agent.) ### MongoDB Haskell agent changes - The `/schema` endpoint will now recognise arrays within the JSON validation schema and generate corresponding arrays in the DC schema. - The `/query` endpoint will now handle `NestedArrayField`s within queries (although it does not yet handle `limit`, `offset`, `where` and `order_by`). ### HGE server changes - The `Backend` type class adds a new type family `XNestedArrays b` to enable nested arrays on a per-backend basis (currently enabled only for the `DataConnector` backend. - Within `RawColumnInfo` the column type is now represented by a new type `RawColumnType b` which mirrors the shape of the DC API `ColumnType`, but uses `XNestedObjects b` and `XNestedArrays b` type families to allow turning nested object and array supports on or off for a particular backend. In the `DataConnector` backend `API.CustomType` is converted into `RawColumnInfo 'DataConnector` while building the schema. - In the next stage of schema building, the `RawColumnInfo` is converted into a `StructuredColumnInfo` which allows us to represent the three different types of columns: scalar, object and array. TODO: the `StructuredColumnInfo` looks very similar to the Logical Model types. The main difference is that it uses the `XNestedObjects` and `XNestedArrays` type families. We should be able to combine these two representations. - The `StructuredColumnInfo` is then placed into a `FIColumn` `FieldInfo`. This involved some refactoring of `FieldInfo` as I had previously split out `FINestedObject` into a separate constructor. However it works out better to represent all "column" fields (i.e. scalar, object and array) using `FIColumn` as this make it easier to implement permission checking correctly. This is the reason the `StructuredColumnInfo` was needed. - Next, the `FieldInfo` are used to generate `FieldParser`s. We add a new constructor to `AnnFieldG` for `AFNestedArray`. An `AFNestedArray` field parser can contain either a simple array selection or an array aggregate. Simple array `FieldParsers` are currently limited to subfield selection. We will add support for limit, offset, where and order_by in a future PR. We also don't yet generate array aggregate `FieldParsers. - The new `AFNestedArray` field is handled by the `QueryPlan` module in the `DataConnector` backend. There we generate an `API.NestedArrayField` from the AFNestedArray. We also handle nested arrays when reshaping the response from the DC agent. ## Limitations - Support for limit, offset, filter (where) and order_by is not yet fully implemented, although it should not be hard to add this - Support for aggregations on nested arrays is not yet fully implemented - Permissions involving nested arrays (and objects) not yet implemented - This should be integrated with Logical Model types, but that will happen in a separate PR PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9149 GitOrigin-RevId: 0e7b71a994fc1d2ca1ef73bfe7b96e95b5328531
370 lines
13 KiB
Haskell
370 lines
13 KiB
Haskell
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 (selectToSelectWith, toQuery)
|
|
import Hasura.Base.Error
|
|
import Hasura.EncJSON
|
|
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 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
|
|
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
|
|
) =>
|
|
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, caseBoolExpMaybe) <- convExtSimple fieldInfoMap selPermInfo pgCol
|
|
resolvedCaseBoolExp <-
|
|
traverse (convAnnColumnCaseBoolExpPartialSQL sessVarBldr) caseBoolExpMaybe
|
|
pure (fromCol @('Postgres 'Vanilla) pgCol, mkAnnColumnField (ciColumn colInfo) (ciType colInfo) resolvedCaseBoolExp 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), Maybe (AnnColumnCaseBoolExpPartialSQL ('Postgres 'Vanilla)))
|
|
convExtSimple fieldInfoMap selPermInfo pgCol = do
|
|
checkSelOnCol selPermInfo pgCol
|
|
colInfo <- askColInfo fieldInfoMap pgCol relWhenPGErr
|
|
pure (colInfo, join $ HashMap.lookup pgCol (spiCols selPermInfo))
|
|
where
|
|
relWhenPGErr = "relationships have to be expanded"
|
|
|
|
convExtRel ::
|
|
( UserInfoM m,
|
|
QErrM m,
|
|
TableInfoRM ('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 $
|
|
AnnObjectSelectG (_asnFields annSel) (FromTable relTableName) $
|
|
_tpFilter $
|
|
_asnPerm annSel
|
|
ArrRel ->
|
|
pure $
|
|
Right $
|
|
ASSimple $
|
|
AnnRelationSelectG
|
|
(fromMaybe relName mAlias)
|
|
colMapping
|
|
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
|
|
) =>
|
|
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 :: JsonAggSelect -> (AnnSimpleSelect ('Postgres 'Vanilla), DS.Seq PG.PrepArg) -> PG.TxE QErr EncJSON
|
|
selectP2 jsonAggSelect (sel, p) =
|
|
runIdentity . PG.getRow
|
|
<$> PG.rawQE dmlTxErrorHandler selectSQL (toList p) True
|
|
where
|
|
selectSQL =
|
|
toQuery $
|
|
selectToSelectWith $
|
|
mkSQLSelect jsonAggSelect sel
|
|
|
|
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
|
|
flip runTableCacheRT tableCache $
|
|
runDMLP1T $
|
|
convSelectQuery sqlGen sessVarFromCurrentSetting (valueParserWithCollectableType binRHSBuilder) query
|
|
|
|
phaseTwo :: (MonadTx m) => (AnnSimpleSelect ('Postgres 'Vanilla), DS.Seq PG.PrepArg) -> m EncJSON
|
|
phaseTwo =
|
|
liftTx . selectP2 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)
|
|
phaseOne sqlGen q >>= runTxWithCtx (_pscExecCtx sourceConfig) (Tx PG.ReadOnly Nothing) LegacyRQLQuery . phaseTwo
|