2022-02-08 12:24:34 +03:00
|
|
|
-- | Postgres Translate Select
|
|
|
|
--
|
|
|
|
-- This module is a translation layer between IR and postgres-specific select queries.
|
2021-10-06 17:47:47 +03:00
|
|
|
--
|
|
|
|
-- There are three main types of selects (as distinguished from the IR):
|
|
|
|
--
|
|
|
|
-- * "simple" selects
|
|
|
|
--
|
|
|
|
-- * aggregate selects
|
|
|
|
--
|
|
|
|
-- * connection selects (used for relay)
|
|
|
|
--
|
|
|
|
-- Most exports from this module showcase this distinction. The "interesting" parts
|
|
|
|
-- of the call tree of these functions is similar:
|
|
|
|
--
|
|
|
|
-- * 'selectQuerySQL' -> 'mkSQLSelect' -> 'processAnnSimpleSelect' -> 'processSelectParams'/'processAnnFields'
|
|
|
|
--
|
|
|
|
-- * 'selectAggregateQuerySQL' -> 'mkAggregateSelect' -> 'processAnnAggregateSelect' -> 'processSelectParams'/'processAnnFields'
|
|
|
|
--
|
|
|
|
-- * 'connetionSelectQuerySQL' -> 'mkConnectionSelect' -> 'processConnectionSelection' -> 'processSelectParams'
|
|
|
|
--
|
|
|
|
--
|
|
|
|
-- Random thoughts that might help when diving deeper in this module:
|
|
|
|
--
|
|
|
|
-- * Extractors are a pair of an SQL expression and an alias; they get
|
|
|
|
-- translated like "[SELECT ...] <expr> as <alias>"
|
|
|
|
-- * a 'SelectSource' consists of a prefix, a source, a boolean conditional
|
|
|
|
-- expression, and info on whether sorting or slicing is done
|
|
|
|
-- (needed to handle the LIMIT optimisation)
|
|
|
|
-- * For details on creating the selection tree for relationships via
|
|
|
|
-- @MonadWriter JoinTree@, see 'withWriteJoinTree'
|
2020-10-29 19:58:13 +03:00
|
|
|
module Hasura.Backends.Postgres.Translate.Select
|
2021-09-24 01:56:37 +03:00
|
|
|
( selectQuerySQL,
|
|
|
|
selectAggregateQuerySQL,
|
|
|
|
connectionSelectQuerySQL,
|
|
|
|
asSingleRowJsonResp,
|
|
|
|
mkSQLSelect,
|
|
|
|
mkAggregateSelect,
|
|
|
|
mkConnectionSelect,
|
|
|
|
PostgresAnnotatedFieldJSON,
|
|
|
|
)
|
|
|
|
where
|
|
|
|
|
|
|
|
import Control.Lens ((^?))
|
|
|
|
import Control.Monad.Writer.Strict
|
|
|
|
import Data.HashMap.Strict qualified as HM
|
|
|
|
import Data.List.NonEmpty qualified as NE
|
|
|
|
import Data.Text qualified as T
|
|
|
|
import Data.Text.Extended
|
|
|
|
import Database.PG.Query qualified as Q
|
|
|
|
import Hasura.Backends.Postgres.SQL.DML qualified as S
|
2022-02-08 12:24:34 +03:00
|
|
|
import Hasura.Backends.Postgres.SQL.IdentifierUniqueness
|
2021-09-24 01:56:37 +03:00
|
|
|
import Hasura.Backends.Postgres.SQL.Types
|
|
|
|
import Hasura.Backends.Postgres.Translate.BoolExp
|
|
|
|
import Hasura.Backends.Postgres.Translate.Column (toJSONableExp)
|
|
|
|
import Hasura.Backends.Postgres.Translate.Types
|
|
|
|
import Hasura.Base.Error
|
|
|
|
import Hasura.EncJSON
|
|
|
|
import Hasura.GraphQL.Schema.Common (currentNodeIdVersion, nodeIdVersionInt)
|
|
|
|
import Hasura.Prelude
|
|
|
|
import Hasura.RQL.DML.Internal
|
|
|
|
import Hasura.RQL.IR.OrderBy
|
|
|
|
import Hasura.RQL.IR.Select
|
2021-12-13 19:48:10 +03:00
|
|
|
import Hasura.RQL.Types
|
2021-09-24 01:56:37 +03:00
|
|
|
import Hasura.SQL.Types
|
|
|
|
|
2021-10-06 17:47:47 +03:00
|
|
|
-- | Translates IR to Postgres queries for simple SELECTs (select queries that
|
|
|
|
-- are not aggregations, including subscriptions).
|
|
|
|
--
|
|
|
|
-- See 'mkSQLSelect' for the Postgres AST.
|
2021-09-24 01:56:37 +03:00
|
|
|
selectQuerySQL ::
|
|
|
|
forall pgKind.
|
|
|
|
(Backend ('Postgres pgKind), PostgresAnnotatedFieldJSON pgKind) =>
|
|
|
|
JsonAggSelect ->
|
|
|
|
AnnSimpleSelect ('Postgres pgKind) ->
|
|
|
|
Q.Query
|
2020-10-29 19:58:13 +03:00
|
|
|
selectQuerySQL jsonAggSelect sel =
|
|
|
|
Q.fromBuilder $ toSQL $ mkSQLSelect jsonAggSelect sel
|
|
|
|
|
2021-10-06 17:47:47 +03:00
|
|
|
-- | Translates IR to Postgres queries for aggregated SELECTs.
|
|
|
|
--
|
|
|
|
-- See 'mkAggregateSelect' for the Postgres AST.
|
2021-09-24 01:56:37 +03:00
|
|
|
selectAggregateQuerySQL ::
|
|
|
|
forall pgKind.
|
|
|
|
(Backend ('Postgres pgKind), PostgresAnnotatedFieldJSON pgKind) =>
|
|
|
|
AnnAggregateSelect ('Postgres pgKind) ->
|
|
|
|
Q.Query
|
2020-10-29 19:58:13 +03:00
|
|
|
selectAggregateQuerySQL =
|
|
|
|
Q.fromBuilder . toSQL . mkAggregateSelect
|
|
|
|
|
2021-10-06 17:47:47 +03:00
|
|
|
-- | Translates IR to Postgres queries for "connection" queries (used for Relay).
|
|
|
|
--
|
|
|
|
-- See 'mkConnectionSelect' for the Postgres AST.
|
2021-09-24 01:56:37 +03:00
|
|
|
connectionSelectQuerySQL ::
|
|
|
|
forall pgKind.
|
|
|
|
( Backend ('Postgres pgKind),
|
|
|
|
PostgresAnnotatedFieldJSON pgKind
|
|
|
|
) =>
|
2021-12-07 16:12:02 +03:00
|
|
|
ConnectionSelect ('Postgres pgKind) Void S.SQLExp ->
|
2021-09-24 01:56:37 +03:00
|
|
|
Q.Query
|
2020-10-29 19:58:13 +03:00
|
|
|
connectionSelectQuerySQL =
|
|
|
|
Q.fromBuilder . toSQL . mkConnectionSelect
|
|
|
|
|
2021-10-06 17:47:47 +03:00
|
|
|
-- | Helper function with no relation to anything else in the module.
|
|
|
|
--
|
|
|
|
-- This function is generally used on the result of 'selectQuerySQL',
|
|
|
|
-- 'selectAggregateQuerySQL' or 'connectionSelectSQL' to run said query and get
|
|
|
|
-- back the resulting JSON.
|
|
|
|
--
|
|
|
|
-- TODO: Perhaps this helper could find a new home.
|
2021-09-24 01:56:37 +03:00
|
|
|
asSingleRowJsonResp ::
|
|
|
|
Q.Query ->
|
|
|
|
[Q.PrepArg] ->
|
|
|
|
Q.TxE QErr EncJSON
|
2020-10-29 19:58:13 +03:00
|
|
|
asSingleRowJsonResp query args =
|
|
|
|
encJFromBS . runIdentity . Q.getRow
|
2021-09-24 01:56:37 +03:00
|
|
|
<$> Q.rawQE dmlTxErrorHandler query args True
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2021-12-13 19:48:10 +03:00
|
|
|
-- | Converts a function name to an 'Identifier'.
|
2021-10-06 17:47:47 +03:00
|
|
|
--
|
|
|
|
-- If the schema name is public, it will just use its name, otherwise it will
|
|
|
|
-- prefix it by the schema name.
|
2020-10-27 13:34:31 +03:00
|
|
|
functionToIdentifier :: QualifiedFunction -> Identifier
|
|
|
|
functionToIdentifier = Identifier . qualifiedObjectToText
|
2018-11-16 15:40:23 +03:00
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
selectFromToFromItem :: Identifier -> SelectFrom ('Postgres pgKind) -> S.FromItem
|
2020-06-08 15:13:01 +03:00
|
|
|
selectFromToFromItem pfx = \case
|
2019-10-18 11:29:47 +03:00
|
|
|
FromTable tn -> S.FISimple tn Nothing
|
2021-12-13 19:48:10 +03:00
|
|
|
FromIdentifier i -> S.FIIdentifier $ toIdentifier i
|
2020-02-13 20:38:23 +03:00
|
|
|
FromFunction qf args defListM ->
|
2021-09-24 01:56:37 +03:00
|
|
|
S.FIFunc $
|
|
|
|
S.FunctionExp qf (fromTableRowArgs pfx args) $
|
|
|
|
Just $ S.mkFunctionAlias (functionToIdentifier qf) defListM
|
2019-10-18 11:29:47 +03:00
|
|
|
|
2020-02-13 20:38:23 +03:00
|
|
|
-- You should be able to retrieve this information
|
2020-06-08 15:13:01 +03:00
|
|
|
-- from the FromItem generated with selectFromToFromItem
|
2020-02-13 20:38:23 +03:00
|
|
|
-- however given from S.FromItem is modelled, it is not
|
2021-10-06 17:47:47 +03:00
|
|
|
-- possible currently.
|
|
|
|
--
|
|
|
|
-- More precisely, 'selectFromToFromItem' is injective but not surjective, so
|
|
|
|
-- any S.FromItem -> S.Qual function would have to be partial.
|
2021-04-22 00:44:37 +03:00
|
|
|
selectFromToQual :: SelectFrom ('Postgres pgKind) -> S.Qual
|
2020-06-08 15:13:01 +03:00
|
|
|
selectFromToQual = \case
|
2021-09-24 01:56:37 +03:00
|
|
|
FromTable table -> S.QualTable table
|
2021-12-13 19:48:10 +03:00
|
|
|
FromIdentifier i -> S.QualifiedIdentifier (toIdentifier i) Nothing
|
2020-10-27 13:34:31 +03:00
|
|
|
FromFunction qf _ _ -> S.QualifiedIdentifier (functionToIdentifier qf) Nothing
|
2018-11-16 15:40:23 +03:00
|
|
|
|
2022-02-23 23:17:58 +03:00
|
|
|
aggregateFieldToExp :: AggregateFields ('Postgres pgKind) -> StringifyNumbers -> S.SQLExp
|
2021-01-18 16:51:36 +03:00
|
|
|
aggregateFieldToExp aggFlds strfyNum = jsonRow
|
2018-10-31 15:51:20 +03:00
|
|
|
where
|
|
|
|
jsonRow = S.applyJsonBuildObj (concatMap aggToFlds aggFlds)
|
|
|
|
withAls fldName sqlExp = [S.SELit fldName, sqlExp]
|
2018-12-12 15:58:39 +03:00
|
|
|
aggToFlds (FieldName t, fld) = withAls t $ case fld of
|
2018-11-14 15:59:59 +03:00
|
|
|
AFCount cty -> S.SECount cty
|
2021-09-24 01:56:37 +03:00
|
|
|
AFOp aggOp -> aggOpToObj aggOp
|
|
|
|
AFExp e -> S.SELit e
|
2018-11-14 15:59:59 +03:00
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
aggOpToObj (AggregateOp opText flds) =
|
|
|
|
S.applyJsonBuildObj $ concatMap (colFldsToExtr opText) flds
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2021-01-18 16:51:36 +03:00
|
|
|
colFldsToExtr opText (FieldName t, CFCol col ty) =
|
2021-09-24 01:56:37 +03:00
|
|
|
[ S.SELit t,
|
|
|
|
toJSONableExp strfyNum ty False $
|
|
|
|
S.SEFnApp opText [S.SEIdentifier $ toIdentifier col] Nothing
|
2018-10-31 15:51:20 +03:00
|
|
|
]
|
2020-10-22 23:42:27 +03:00
|
|
|
colFldsToExtr _ (FieldName t, CFExp e) =
|
2021-09-24 01:56:37 +03:00
|
|
|
[S.SELit t, S.SELit e]
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2019-04-26 11:19:59 +03:00
|
|
|
asSingleRowExtr :: S.Alias -> S.SQLExp
|
|
|
|
asSingleRowExtr col =
|
|
|
|
S.SEFnApp "coalesce" [jsonAgg, S.SELit "null"] Nothing
|
2018-10-31 15:51:20 +03:00
|
|
|
where
|
2021-09-24 01:56:37 +03:00
|
|
|
jsonAgg =
|
|
|
|
S.SEOpApp
|
|
|
|
(S.SQLOp "->")
|
|
|
|
[ S.SEFnApp "json_agg" [S.SEIdentifier $ toIdentifier col] Nothing,
|
|
|
|
S.SEUnsafe "0"
|
|
|
|
]
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
withJsonAggExtr ::
|
|
|
|
PermissionLimitSubQuery -> Maybe S.OrderByExp -> S.Alias -> S.SQLExp
|
2020-06-08 15:13:01 +03:00
|
|
|
withJsonAggExtr permLimitSubQuery ordBy alias =
|
2019-08-01 08:09:52 +03:00
|
|
|
-- if select has aggregations then use subquery to apply permission limit
|
2020-06-08 15:13:01 +03:00
|
|
|
case permLimitSubQuery of
|
|
|
|
PLSQRequired permLimit -> withPermLimit permLimit
|
2021-09-24 01:56:37 +03:00
|
|
|
PLSQNotRequired -> simpleJsonAgg
|
2018-10-31 15:51:20 +03:00
|
|
|
where
|
2019-08-01 08:09:52 +03:00
|
|
|
simpleJsonAgg = mkSimpleJsonAgg rowIdenExp ordBy
|
2020-10-27 13:34:31 +03:00
|
|
|
rowIdenExp = S.SEIdentifier $ S.getAlias alias
|
|
|
|
subSelAls = Identifier "sub_query"
|
|
|
|
unnestTable = Identifier "unnest_table"
|
2019-04-26 11:19:59 +03:00
|
|
|
|
|
|
|
mkSimpleJsonAgg rowExp ob =
|
|
|
|
let jsonAggExp = S.SEFnApp "json_agg" [rowExp] ob
|
2021-09-24 01:56:37 +03:00
|
|
|
in S.SEFnApp "coalesce" [jsonAggExp, S.SELit "[]"] Nothing
|
2019-04-26 11:19:59 +03:00
|
|
|
|
|
|
|
withPermLimit limit =
|
|
|
|
let subSelect = mkSubSelect limit
|
2020-10-27 13:34:31 +03:00
|
|
|
rowIdentifier = S.mkQIdenExp subSelAls alias
|
|
|
|
extr = S.Extractor (mkSimpleJsonAgg rowIdentifier newOrderBy) Nothing
|
2021-09-24 01:56:37 +03:00
|
|
|
fromExp =
|
|
|
|
S.FromExp $
|
|
|
|
pure $
|
|
|
|
S.mkSelFromItem subSelect $ S.Alias subSelAls
|
|
|
|
in S.SESelect $
|
|
|
|
S.mkSelect
|
|
|
|
{ S.selExtr = pure extr,
|
|
|
|
S.selFrom = Just fromExp
|
|
|
|
}
|
2019-04-26 11:19:59 +03:00
|
|
|
|
|
|
|
mkSubSelect limit =
|
2021-09-24 01:56:37 +03:00
|
|
|
let jsonRowExtr =
|
|
|
|
flip S.Extractor (Just alias) $
|
|
|
|
S.mkQIdenExp unnestTable alias
|
2019-04-26 11:19:59 +03:00
|
|
|
obExtrs = flip map newOBAliases $ \a ->
|
2021-09-24 01:56:37 +03:00
|
|
|
S.Extractor (S.mkQIdenExp unnestTable a) $ Just $ S.Alias a
|
|
|
|
in S.mkSelect
|
|
|
|
{ S.selExtr = jsonRowExtr : obExtrs,
|
|
|
|
S.selFrom = Just $ S.FromExp $ pure unnestFromItem,
|
|
|
|
S.selLimit = Just $ S.LimitExp $ S.intToSQLExp limit,
|
|
|
|
S.selOrderBy = newOrderBy
|
|
|
|
}
|
2019-04-26 11:19:59 +03:00
|
|
|
|
|
|
|
unnestFromItem =
|
|
|
|
let arrayAggItems = flip map (rowIdenExp : obCols) $
|
2021-09-24 01:56:37 +03:00
|
|
|
\s -> S.SEFnApp "array_agg" [s] Nothing
|
|
|
|
in S.FIUnnest arrayAggItems (S.Alias unnestTable) $
|
|
|
|
rowIdenExp : map S.SEIdentifier newOBAliases
|
2019-04-26 11:19:59 +03:00
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
newOrderBy = S.OrderByExp <$> NE.nonEmpty newOBItems
|
2019-04-26 11:19:59 +03:00
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
(newOBItems, obCols, newOBAliases) = maybe ([], [], []) transformOrderBy ordBy
|
|
|
|
transformOrderBy (S.OrderByExp l) = unzip3 $
|
2021-09-24 01:56:37 +03:00
|
|
|
flip map (zip (toList l) [1 ..]) $ \(obItem, i :: Int) ->
|
|
|
|
let iden = Identifier $ "ob_col_" <> tshow i
|
|
|
|
in ( obItem {S.oColumn = S.SEIdentifier iden},
|
|
|
|
S.oColumn obItem,
|
|
|
|
iden
|
|
|
|
)
|
|
|
|
|
|
|
|
asJsonAggExtr ::
|
|
|
|
JsonAggSelect -> S.Alias -> PermissionLimitSubQuery -> Maybe S.OrderByExp -> S.Extractor
|
2020-06-08 15:13:01 +03:00
|
|
|
asJsonAggExtr jsonAggSelect als permLimitSubQuery ordByExpM =
|
2020-02-13 20:38:23 +03:00
|
|
|
flip S.Extractor (Just als) $ case jsonAggSelect of
|
2020-06-08 15:13:01 +03:00
|
|
|
JASMultipleRows -> withJsonAggExtr permLimitSubQuery ordByExpM als
|
2020-02-13 20:38:23 +03:00
|
|
|
JASSingleObject -> asSingleRowExtr als
|
2018-10-31 15:51:20 +03:00
|
|
|
|
|
|
|
-- array relationships are not grouped, so have to be prefixed by
|
|
|
|
-- parent's alias
|
2020-10-27 13:34:31 +03:00
|
|
|
mkUniqArrayRelationAlias :: FieldName -> [FieldName] -> Identifier
|
2020-06-08 15:13:01 +03:00
|
|
|
mkUniqArrayRelationAlias parAls flds =
|
|
|
|
let sortedFields = sort flds
|
2021-09-24 01:56:37 +03:00
|
|
|
in Identifier $
|
|
|
|
getFieldNameTxt parAls <> "."
|
|
|
|
<> T.intercalate "." (map getFieldNameTxt sortedFields)
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
mkArrayRelationTableAlias :: Identifier -> FieldName -> [FieldName] -> Identifier
|
2020-06-08 15:13:01 +03:00
|
|
|
mkArrayRelationTableAlias pfx parAls flds =
|
2020-10-27 13:34:31 +03:00
|
|
|
pfx <> Identifier ".ar." <> uniqArrRelAls
|
2018-10-31 15:51:20 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
uniqArrRelAls = mkUniqArrayRelationAlias parAls flds
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
mkObjectRelationTableAlias :: Identifier -> RelName -> Identifier
|
2020-06-08 15:13:01 +03:00
|
|
|
mkObjectRelationTableAlias pfx relName =
|
2020-10-27 13:34:31 +03:00
|
|
|
pfx <> Identifier ".or." <> toIdentifier relName
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
mkComputedFieldTableAlias :: Identifier -> FieldName -> Identifier
|
2020-06-08 15:13:01 +03:00
|
|
|
mkComputedFieldTableAlias pfx fldAls =
|
2020-10-27 13:34:31 +03:00
|
|
|
pfx <> Identifier ".cf." <> toIdentifier fldAls
|
2019-10-18 11:29:47 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
mkBaseTableAlias :: Identifier -> Identifier
|
2020-06-08 15:13:01 +03:00
|
|
|
mkBaseTableAlias pfx =
|
2020-10-27 13:34:31 +03:00
|
|
|
pfx <> Identifier ".base"
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
mkBaseTableColumnAlias :: Identifier -> PGCol -> Identifier
|
2020-06-08 15:13:01 +03:00
|
|
|
mkBaseTableColumnAlias pfx pgColumn =
|
2020-10-27 13:34:31 +03:00
|
|
|
pfx <> Identifier ".pg." <> toIdentifier pgColumn
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2021-07-27 19:27:28 +03:00
|
|
|
mkOrderByFieldName :: ToTxt a => a -> FieldName
|
|
|
|
mkOrderByFieldName name =
|
|
|
|
FieldName $ toTxt name <> "." <> "order_by"
|
2018-12-12 15:58:39 +03:00
|
|
|
|
2021-07-27 19:27:28 +03:00
|
|
|
mkAggregateOrderByAlias :: AnnotatedAggregateOrderBy ('Postgres pgKind) -> S.Alias
|
2021-09-24 01:56:37 +03:00
|
|
|
mkAggregateOrderByAlias =
|
|
|
|
(S.Alias . Identifier) . \case
|
|
|
|
AAOCount -> "count"
|
2022-01-19 11:37:50 +03:00
|
|
|
AAOOp opText col -> opText <> "." <> getPGColTxt (ciColumn col)
|
2021-09-24 01:56:37 +03:00
|
|
|
|
|
|
|
mkArrayRelationSourcePrefix ::
|
|
|
|
Identifier ->
|
|
|
|
FieldName ->
|
|
|
|
HM.HashMap FieldName [FieldName] ->
|
|
|
|
FieldName ->
|
|
|
|
Identifier
|
2020-06-08 15:13:01 +03:00
|
|
|
mkArrayRelationSourcePrefix parentSourcePrefix parentFieldName similarFieldsMap fieldName =
|
|
|
|
mkArrayRelationTableAlias parentSourcePrefix parentFieldName $
|
2021-09-24 01:56:37 +03:00
|
|
|
HM.lookupDefault [fieldName] fieldName similarFieldsMap
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
mkArrayRelationAlias ::
|
|
|
|
FieldName ->
|
|
|
|
HM.HashMap FieldName [FieldName] ->
|
|
|
|
FieldName ->
|
|
|
|
S.Alias
|
2020-06-08 15:13:01 +03:00
|
|
|
mkArrayRelationAlias parentFieldName similarFieldsMap fieldName =
|
2021-09-24 01:56:37 +03:00
|
|
|
S.Alias $
|
|
|
|
mkUniqArrayRelationAlias parentFieldName $
|
|
|
|
HM.lookupDefault [fieldName] fieldName similarFieldsMap
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
fromTableRowArgs ::
|
2021-12-13 19:48:10 +03:00
|
|
|
Identifier -> FunctionArgsExpTableRow S.SQLExp -> S.FunctionArgs
|
|
|
|
fromTableRowArgs prefix = toFunctionArgs . fmap toSQLExp
|
2019-10-18 11:29:47 +03:00
|
|
|
where
|
|
|
|
toFunctionArgs (FunctionArgsExp positional named) =
|
|
|
|
S.FunctionArgs positional named
|
2021-12-13 19:48:10 +03:00
|
|
|
toSQLExp =
|
|
|
|
onArgumentExp
|
|
|
|
(S.SERowIdentifier alias)
|
|
|
|
(S.mkQIdenExp alias . Identifier)
|
|
|
|
alias = mkBaseTableAlias prefix
|
2019-10-18 11:29:47 +03:00
|
|
|
|
2018-10-31 15:51:20 +03:00
|
|
|
-- uses row_to_json to build a json object
|
2021-09-24 01:56:37 +03:00
|
|
|
withRowToJSON ::
|
|
|
|
FieldName -> [S.Extractor] -> (S.Alias, S.SQLExp)
|
2018-10-31 15:51:20 +03:00
|
|
|
withRowToJSON parAls extrs =
|
|
|
|
(S.toAlias parAls, jsonRow)
|
|
|
|
where
|
|
|
|
jsonRow = S.applyRowToJson extrs
|
|
|
|
|
|
|
|
-- uses json_build_object to build a json object
|
2021-09-24 01:56:37 +03:00
|
|
|
withJsonBuildObj ::
|
|
|
|
FieldName -> [S.SQLExp] -> (S.Alias, S.SQLExp)
|
2018-10-31 15:51:20 +03:00
|
|
|
withJsonBuildObj parAls exps =
|
|
|
|
(S.toAlias parAls, jsonRow)
|
|
|
|
where
|
|
|
|
jsonRow = S.applyJsonBuildObj exps
|
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
-- | Forces aggregation
|
|
|
|
withForceAggregation :: S.TypeAnn -> S.SQLExp -> S.SQLExp
|
|
|
|
withForceAggregation tyAnn e =
|
|
|
|
-- bool_or to force aggregation
|
|
|
|
S.SEFnApp "coalesce" [e, S.SETyAnn (S.SEUnsafe "bool_or('true')") tyAnn] Nothing
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
mkAggregateOrderByExtractorAndFields ::
|
|
|
|
forall pgKind.
|
|
|
|
Backend ('Postgres pgKind) =>
|
|
|
|
AnnotatedAggregateOrderBy ('Postgres pgKind) ->
|
|
|
|
(S.Extractor, AggregateFields ('Postgres pgKind))
|
2020-06-08 15:13:01 +03:00
|
|
|
mkAggregateOrderByExtractorAndFields annAggOrderBy =
|
|
|
|
case annAggOrderBy of
|
2021-09-24 01:56:37 +03:00
|
|
|
AAOCount ->
|
|
|
|
( S.Extractor S.countStar alias,
|
|
|
|
[(FieldName "count", AFCount S.CTStar)]
|
2020-06-08 15:13:01 +03:00
|
|
|
)
|
|
|
|
AAOOp opText pgColumnInfo ->
|
2022-01-19 11:37:50 +03:00
|
|
|
let pgColumn = ciColumn pgColumnInfo
|
|
|
|
pgType = ciType pgColumnInfo
|
2021-09-24 01:56:37 +03:00
|
|
|
in ( S.Extractor (S.SEFnApp opText [S.SEIdentifier $ toIdentifier pgColumn] Nothing) alias,
|
|
|
|
[ ( FieldName opText,
|
|
|
|
AFOp $
|
|
|
|
AggregateOp
|
|
|
|
opText
|
|
|
|
[ ( fromCol @('Postgres pgKind) pgColumn,
|
|
|
|
CFCol pgColumn pgType
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
2018-12-12 15:58:39 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
alias = Just $ mkAggregateOrderByAlias annAggOrderBy
|
|
|
|
|
2021-09-21 13:39:34 +03:00
|
|
|
-- | Generate alias for order by extractors
|
2021-09-24 01:56:37 +03:00
|
|
|
mkAnnOrderByAlias ::
|
|
|
|
Identifier -> FieldName -> SimilarArrayFields -> AnnotatedOrderByElement ('Postgres pgKind) v -> S.Alias
|
2020-06-08 15:13:01 +03:00
|
|
|
mkAnnOrderByAlias pfx parAls similarFields = \case
|
|
|
|
AOCColumn pgColumnInfo ->
|
2022-01-19 11:37:50 +03:00
|
|
|
let pgColumn = ciColumn pgColumnInfo
|
2020-06-08 15:13:01 +03:00
|
|
|
obColAls = mkBaseTableColumnAlias pfx pgColumn
|
2021-09-24 01:56:37 +03:00
|
|
|
in S.Alias obColAls
|
2018-10-31 15:51:20 +03:00
|
|
|
-- "pfx.or.relname"."pfx.ob.or.relname.rest" AS "pfx.ob.or.relname.rest"
|
2020-06-08 15:13:01 +03:00
|
|
|
AOCObjectRelation relInfo _ rest ->
|
|
|
|
let rn = riName relInfo
|
2021-09-24 01:56:37 +03:00
|
|
|
relPfx = mkObjectRelationTableAlias pfx rn
|
2019-10-05 07:00:53 +03:00
|
|
|
ordByFldName = mkOrderByFieldName rn
|
2020-06-08 15:13:01 +03:00
|
|
|
nesAls = mkAnnOrderByAlias relPfx ordByFldName mempty rest
|
2021-09-24 01:56:37 +03:00
|
|
|
in nesAls
|
2020-06-08 15:13:01 +03:00
|
|
|
AOCArrayAggregation relInfo _ aggOrderBy ->
|
|
|
|
let rn = riName relInfo
|
2021-09-24 01:56:37 +03:00
|
|
|
arrPfx =
|
|
|
|
mkArrayRelationSourcePrefix pfx parAls similarFields $
|
|
|
|
mkOrderByFieldName rn
|
2020-10-27 13:34:31 +03:00
|
|
|
obAls = arrPfx <> Identifier "." <> toIdentifier (mkAggregateOrderByAlias aggOrderBy)
|
2021-09-24 01:56:37 +03:00
|
|
|
in S.Alias obAls
|
2021-07-27 19:27:28 +03:00
|
|
|
AOCComputedField cfOrderBy ->
|
|
|
|
let fieldName = fromComputedField $ _cfobName cfOrderBy
|
2021-09-24 01:56:37 +03:00
|
|
|
in case _cfobOrderByElement cfOrderBy of
|
|
|
|
CFOBEScalar _ -> S.Alias $ mkComputedFieldTableAlias pfx fieldName
|
|
|
|
CFOBETableAggregation _ _ aggOrderBy ->
|
|
|
|
let cfPfx = mkComputedFieldTableAlias pfx fieldName
|
|
|
|
obAls = cfPfx <> Identifier "." <> toIdentifier (mkAggregateOrderByAlias aggOrderBy)
|
|
|
|
in S.Alias obAls
|
|
|
|
|
|
|
|
applyDistinctOnAtBase ::
|
|
|
|
NE.NonEmpty PGCol -> S.DistinctExpr
|
2021-09-21 13:39:34 +03:00
|
|
|
applyDistinctOnAtBase =
|
|
|
|
S.DistinctOn . map (S.SEIdentifier . toIdentifier) . toList
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
applyDistinctOnAtNode ::
|
|
|
|
Identifier ->
|
|
|
|
NE.NonEmpty PGCol ->
|
|
|
|
( S.DistinctExpr,
|
|
|
|
[(S.Alias, S.SQLExp)] -- additional column extractors
|
|
|
|
)
|
2021-09-21 13:39:34 +03:00
|
|
|
applyDistinctOnAtNode pfx neCols = (distOnExp, colExtrs)
|
2018-11-23 04:53:56 +03:00
|
|
|
where
|
|
|
|
cols = toList neCols
|
2020-10-27 13:34:31 +03:00
|
|
|
distOnExp = S.DistinctOn $ map (S.SEIdentifier . toIdentifier . mkQColAls) cols
|
|
|
|
mkQCol c = S.mkQIdenExp (mkBaseTableAlias pfx) $ toIdentifier c
|
2020-06-08 15:13:01 +03:00
|
|
|
mkQColAls = S.Alias . mkBaseTableColumnAlias pfx
|
2018-11-23 04:53:56 +03:00
|
|
|
colExtrs = flip map cols $ mkQColAls &&& mkQCol
|
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
type SimilarArrayFields = HM.HashMap FieldName [FieldName]
|
2018-11-23 04:53:56 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
mkSimilarArrayFields ::
|
|
|
|
forall pgKind v.
|
|
|
|
(Backend ('Postgres pgKind), Eq v) =>
|
2021-12-07 16:12:02 +03:00
|
|
|
AnnFieldsG ('Postgres pgKind) Void v ->
|
2021-09-24 01:56:37 +03:00
|
|
|
Maybe (NE.NonEmpty (AnnotatedOrderByItemG ('Postgres pgKind) v)) ->
|
|
|
|
SimilarArrayFields
|
2020-06-08 15:13:01 +03:00
|
|
|
mkSimilarArrayFields annFields maybeOrderBys =
|
2021-09-24 01:56:37 +03:00
|
|
|
HM.fromList $
|
|
|
|
flip map allTuples $
|
|
|
|
\(relNameAndArgs, fieldName) -> (fieldName, getSimilarFields relNameAndArgs)
|
2018-10-31 15:51:20 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
getSimilarFields relNameAndArgs = map snd $ filter ((== relNameAndArgs) . fst) allTuples
|
|
|
|
allTuples = arrayRelationTuples <> aggOrderByRelationTuples
|
|
|
|
arrayRelationTuples =
|
|
|
|
let arrayFields = mapMaybe getAnnArr annFields
|
2021-09-24 01:56:37 +03:00
|
|
|
in flip map arrayFields $
|
|
|
|
\(f, relSel) -> (getArrayRelNameAndSelectArgs relSel, f)
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
aggOrderByRelationTuples =
|
2021-09-24 01:56:37 +03:00
|
|
|
let mkItem (relName, fieldName) =
|
|
|
|
( (relName, noSelectArgs),
|
|
|
|
fieldName
|
|
|
|
)
|
|
|
|
in map mkItem $
|
|
|
|
maybe
|
|
|
|
[]
|
|
|
|
(mapMaybe (fetchAggOrderByRels . obiColumn) . toList)
|
|
|
|
maybeOrderBys
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
fetchAggOrderByRels (AOCArrayAggregation ri _ _) =
|
|
|
|
Just (riName ri, mkOrderByFieldName $ riName ri)
|
2021-09-24 01:56:37 +03:00
|
|
|
fetchAggOrderByRels _ = Nothing
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
getArrayRelNameAndSelectArgs ::
|
|
|
|
ArraySelectG ('Postgres pgKind) r v ->
|
|
|
|
(RelName, SelectArgsG ('Postgres pgKind) v)
|
2020-06-08 15:13:01 +03:00
|
|
|
getArrayRelNameAndSelectArgs = \case
|
2022-03-10 09:17:48 +03:00
|
|
|
ASSimple r -> (_aarRelationshipName r, _asnArgs $ _aarAnnSelect r)
|
|
|
|
ASAggregate r -> (_aarRelationshipName r, _asnArgs $ _aarAnnSelect r)
|
|
|
|
ASConnection r -> (_aarRelationshipName r, _asnArgs $ _csSelect $ _aarAnnSelect r)
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
getAnnArr ::
|
|
|
|
(a, AnnFieldG ('Postgres pgKind) r v) ->
|
|
|
|
Maybe (a, ArraySelectG ('Postgres pgKind) r v)
|
2020-06-08 15:13:01 +03:00
|
|
|
getAnnArr (f, annFld) = case annFld of
|
|
|
|
AFArrayRelation (ASConnection _) -> Nothing
|
2021-09-24 01:56:37 +03:00
|
|
|
AFArrayRelation ar -> Just (f, ar)
|
|
|
|
_ -> Nothing
|
|
|
|
|
2021-10-06 17:47:47 +03:00
|
|
|
-- | This is the lowest level function which deals with @MonadWriter JoinTree@, whose
|
|
|
|
-- purpose is to essentially create the selection tree across relationships.
|
|
|
|
--
|
|
|
|
-- Each type of relationship uses a different kind of update function; see
|
|
|
|
-- 'withWriteObjectRelation', 'withWriteArrayRelation', 'withWriteArrayConnection',
|
|
|
|
-- and 'withWriteComputedFieldTableSet'.
|
|
|
|
--
|
|
|
|
-- See the definition of 'JoinTree' for details before diving further
|
|
|
|
-- (particularly its components and Monoid instance).
|
2021-09-24 01:56:37 +03:00
|
|
|
withWriteJoinTree ::
|
|
|
|
(MonadWriter JoinTree m) =>
|
|
|
|
(JoinTree -> b -> JoinTree) ->
|
|
|
|
m (a, b) ->
|
|
|
|
m a
|
2020-06-08 15:13:01 +03:00
|
|
|
withWriteJoinTree joinTreeUpdater action =
|
|
|
|
pass $ do
|
|
|
|
(out, result) <- action
|
|
|
|
let fromJoinTree joinTree =
|
|
|
|
joinTreeUpdater joinTree result
|
|
|
|
pure (out, fromJoinTree)
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
withWriteObjectRelation ::
|
|
|
|
(MonadWriter JoinTree m) =>
|
|
|
|
m
|
|
|
|
( ObjectRelationSource,
|
|
|
|
HM.HashMap S.Alias S.SQLExp,
|
|
|
|
a
|
|
|
|
) ->
|
|
|
|
m a
|
2020-06-08 15:13:01 +03:00
|
|
|
withWriteObjectRelation action =
|
|
|
|
withWriteJoinTree updateJoinTree $ do
|
|
|
|
(source, nodeExtractors, out) <- action
|
|
|
|
pure (out, (source, nodeExtractors))
|
2018-10-31 15:51:20 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
updateJoinTree joinTree (source, nodeExtractors) =
|
|
|
|
let selectNode = SelectNode nodeExtractors joinTree
|
2021-09-24 01:56:37 +03:00
|
|
|
in mempty {_jtObjectRelations = HM.singleton source selectNode}
|
|
|
|
|
|
|
|
withWriteArrayRelation ::
|
|
|
|
(MonadWriter JoinTree m) =>
|
|
|
|
m
|
|
|
|
( ArrayRelationSource,
|
|
|
|
S.Extractor,
|
|
|
|
HM.HashMap S.Alias S.SQLExp,
|
|
|
|
a
|
|
|
|
) ->
|
|
|
|
m a
|
2020-06-08 15:13:01 +03:00
|
|
|
withWriteArrayRelation action =
|
|
|
|
withWriteJoinTree updateJoinTree $ do
|
2021-09-24 01:56:37 +03:00
|
|
|
(source, topExtractor, nodeExtractors, out) <- action
|
|
|
|
pure (out, (source, topExtractor, nodeExtractors))
|
2019-08-01 08:09:52 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
updateJoinTree joinTree (source, topExtractor, nodeExtractors) =
|
2021-09-24 01:56:37 +03:00
|
|
|
let arraySelectNode =
|
|
|
|
MultiRowSelectNode [topExtractor] $
|
|
|
|
SelectNode nodeExtractors joinTree
|
|
|
|
in mempty {_jtArrayRelations = HM.singleton source arraySelectNode}
|
|
|
|
|
|
|
|
withWriteArrayConnection ::
|
|
|
|
(MonadWriter JoinTree m) =>
|
|
|
|
m
|
|
|
|
( ArrayConnectionSource,
|
|
|
|
S.Extractor,
|
|
|
|
HM.HashMap S.Alias S.SQLExp,
|
|
|
|
a
|
|
|
|
) ->
|
|
|
|
m a
|
2020-06-08 15:13:01 +03:00
|
|
|
withWriteArrayConnection action =
|
|
|
|
withWriteJoinTree updateJoinTree $ do
|
2021-09-24 01:56:37 +03:00
|
|
|
(source, topExtractor, nodeExtractors, out) <- action
|
|
|
|
pure (out, (source, topExtractor, nodeExtractors))
|
2018-12-12 15:58:39 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
updateJoinTree joinTree (source, topExtractor, nodeExtractors) =
|
2021-09-24 01:56:37 +03:00
|
|
|
let arraySelectNode =
|
|
|
|
MultiRowSelectNode [topExtractor] $
|
|
|
|
SelectNode nodeExtractors joinTree
|
|
|
|
in mempty {_jtArrayConnections = HM.singleton source arraySelectNode}
|
|
|
|
|
|
|
|
withWriteComputedFieldTableSet ::
|
|
|
|
(MonadWriter JoinTree m) =>
|
|
|
|
m
|
|
|
|
( ComputedFieldTableSetSource,
|
|
|
|
S.Extractor,
|
|
|
|
HM.HashMap S.Alias S.SQLExp,
|
|
|
|
a
|
|
|
|
) ->
|
|
|
|
m a
|
2020-06-08 15:13:01 +03:00
|
|
|
withWriteComputedFieldTableSet action =
|
|
|
|
withWriteJoinTree updateJoinTree $ do
|
2021-07-27 19:27:28 +03:00
|
|
|
(source, topExtractor, nodeExtractors, out) <- action
|
|
|
|
pure (out, (source, topExtractor, nodeExtractors))
|
2018-12-12 15:58:39 +03:00
|
|
|
where
|
2021-07-27 19:27:28 +03:00
|
|
|
updateJoinTree joinTree (source, topExtractor, nodeExtractors) =
|
|
|
|
let selectNode = MultiRowSelectNode [topExtractor] $ SelectNode nodeExtractors joinTree
|
2021-09-24 01:56:37 +03:00
|
|
|
in mempty {_jtComputedFieldTableSets = HM.singleton source selectNode}
|
|
|
|
|
|
|
|
processAnnSimpleSelect ::
|
|
|
|
forall pgKind m.
|
2022-02-23 23:17:58 +03:00
|
|
|
( MonadReader StringifyNumbers m,
|
2021-09-24 01:56:37 +03:00
|
|
|
MonadWriter JoinTree m,
|
|
|
|
Backend ('Postgres pgKind),
|
|
|
|
PostgresAnnotatedFieldJSON pgKind
|
|
|
|
) =>
|
|
|
|
SourcePrefixes ->
|
|
|
|
FieldName ->
|
|
|
|
PermissionLimitSubQuery ->
|
|
|
|
AnnSimpleSelect ('Postgres pgKind) ->
|
|
|
|
m
|
|
|
|
( SelectSource,
|
|
|
|
HM.HashMap S.Alias S.SQLExp
|
|
|
|
)
|
2020-06-08 15:13:01 +03:00
|
|
|
processAnnSimpleSelect sourcePrefixes fieldAlias permLimitSubQuery annSimpleSel = do
|
|
|
|
(selectSource, orderByAndDistinctExtrs, _) <-
|
2021-09-21 13:39:34 +03:00
|
|
|
processSelectParams
|
|
|
|
sourcePrefixes
|
|
|
|
fieldAlias
|
|
|
|
similarArrayFields
|
|
|
|
tableFrom
|
|
|
|
permLimitSubQuery
|
|
|
|
tablePermissions
|
|
|
|
tableArgs
|
2020-06-08 15:13:01 +03:00
|
|
|
annFieldsExtr <- processAnnFields (_pfThis sourcePrefixes) fieldAlias similarArrayFields annSelFields
|
|
|
|
let allExtractors = HM.fromList $ annFieldsExtr : orderByAndDistinctExtrs
|
|
|
|
pure (selectSource, allExtractors)
|
2018-12-12 15:58:39 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
AnnSelectG annSelFields tableFrom tablePermissions tableArgs _ = annSimpleSel
|
|
|
|
similarArrayFields =
|
|
|
|
mkSimilarArrayFields annSelFields $ _saOrderBy tableArgs
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
processAnnAggregateSelect ::
|
|
|
|
forall pgKind m.
|
2022-02-23 23:17:58 +03:00
|
|
|
( MonadReader StringifyNumbers m,
|
2021-09-24 01:56:37 +03:00
|
|
|
MonadWriter JoinTree m,
|
|
|
|
Backend ('Postgres pgKind),
|
|
|
|
PostgresAnnotatedFieldJSON pgKind
|
|
|
|
) =>
|
|
|
|
SourcePrefixes ->
|
|
|
|
FieldName ->
|
|
|
|
AnnAggregateSelect ('Postgres pgKind) ->
|
|
|
|
m
|
|
|
|
( SelectSource,
|
|
|
|
HM.HashMap S.Alias S.SQLExp,
|
|
|
|
S.Extractor
|
|
|
|
)
|
2020-06-08 15:13:01 +03:00
|
|
|
processAnnAggregateSelect sourcePrefixes fieldAlias annAggSel = do
|
|
|
|
(selectSource, orderByAndDistinctExtrs, _) <-
|
2021-09-21 13:39:34 +03:00
|
|
|
processSelectParams
|
|
|
|
sourcePrefixes
|
|
|
|
fieldAlias
|
|
|
|
similarArrayFields
|
|
|
|
tableFrom
|
|
|
|
permLimitSubQuery
|
|
|
|
tablePermissions
|
|
|
|
tableArgs
|
2020-06-08 15:13:01 +03:00
|
|
|
let thisSourcePrefix = _pfThis sourcePrefixes
|
|
|
|
processedFields <- forM aggSelFields $ \(fieldName, field) ->
|
2021-09-24 01:56:37 +03:00
|
|
|
(fieldName,)
|
|
|
|
<$> case field of
|
|
|
|
TAFAgg aggFields ->
|
|
|
|
pure
|
|
|
|
( aggregateFieldsToExtractorExps thisSourcePrefix aggFields,
|
|
|
|
aggregateFieldToExp aggFields strfyNum
|
|
|
|
)
|
|
|
|
TAFNodes _ annFields -> do
|
|
|
|
annFieldExtr <- processAnnFields thisSourcePrefix fieldName similarArrayFields annFields
|
|
|
|
pure
|
|
|
|
( [annFieldExtr],
|
|
|
|
withJsonAggExtr permLimitSubQuery (orderByForJsonAgg selectSource) $
|
|
|
|
S.Alias $ toIdentifier fieldName
|
|
|
|
)
|
|
|
|
TAFExp e ->
|
|
|
|
pure
|
|
|
|
( [],
|
|
|
|
withForceAggregation S.textTypeAnn $ S.SELit e
|
|
|
|
)
|
2018-12-12 15:58:39 +03:00
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
let topLevelExtractor =
|
2020-10-27 13:34:31 +03:00
|
|
|
flip S.Extractor (Just $ S.Alias $ toIdentifier fieldAlias) $
|
2021-09-24 01:56:37 +03:00
|
|
|
S.applyJsonBuildObj $
|
|
|
|
flip concatMap (map (second snd) processedFields) $
|
|
|
|
\(FieldName fieldText, fieldExp) -> [S.SELit fieldText, fieldExp]
|
|
|
|
nodeExtractors =
|
|
|
|
HM.fromList $
|
|
|
|
concatMap (fst . snd) processedFields <> orderByAndDistinctExtrs
|
2018-12-12 15:58:39 +03:00
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
pure (selectSource, nodeExtractors, topLevelExtractor)
|
|
|
|
where
|
2021-01-18 16:51:36 +03:00
|
|
|
AnnSelectG aggSelFields tableFrom tablePermissions tableArgs strfyNum = annAggSel
|
2020-06-08 15:13:01 +03:00
|
|
|
permLimit = _tpLimit tablePermissions
|
|
|
|
orderBy = _saOrderBy tableArgs
|
|
|
|
permLimitSubQuery = mkPermissionLimitSubQuery permLimit aggSelFields orderBy
|
|
|
|
similarArrayFields = HM.unions $
|
|
|
|
flip map (map snd aggSelFields) $ \case
|
|
|
|
TAFAgg _ -> mempty
|
2021-02-03 19:17:20 +03:00
|
|
|
TAFNodes _ annFlds ->
|
2020-06-08 15:13:01 +03:00
|
|
|
mkSimilarArrayFields annFlds orderBy
|
|
|
|
TAFExp _ -> mempty
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
mkPermissionLimitSubQuery ::
|
|
|
|
Maybe Int ->
|
|
|
|
TableAggregateFields ('Postgres pgKind) ->
|
|
|
|
Maybe (NE.NonEmpty (AnnotatedOrderByItem ('Postgres pgKind))) ->
|
|
|
|
PermissionLimitSubQuery
|
2020-06-08 15:13:01 +03:00
|
|
|
mkPermissionLimitSubQuery permLimit aggFields orderBys =
|
|
|
|
case permLimit of
|
|
|
|
Nothing -> PLSQNotRequired
|
|
|
|
Just limit ->
|
2021-09-24 01:56:37 +03:00
|
|
|
if hasAggregateField || hasAggOrderBy
|
|
|
|
then PLSQRequired limit
|
|
|
|
else PLSQNotRequired
|
2020-06-08 15:13:01 +03:00
|
|
|
where
|
|
|
|
hasAggregateField = flip any (map snd aggFields) $
|
|
|
|
\case
|
|
|
|
TAFAgg _ -> True
|
2021-09-24 01:56:37 +03:00
|
|
|
_ -> False
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
hasAggOrderBy = case orderBys of
|
|
|
|
Nothing -> False
|
|
|
|
Just l -> flip any (concatMap toList $ toList l) $
|
2021-09-24 01:56:37 +03:00
|
|
|
\case
|
|
|
|
AOCArrayAggregation {} -> True
|
|
|
|
_ -> False
|
|
|
|
|
|
|
|
processArrayRelation ::
|
|
|
|
forall pgKind m.
|
2022-02-23 23:17:58 +03:00
|
|
|
( MonadReader StringifyNumbers m,
|
2021-09-24 01:56:37 +03:00
|
|
|
MonadWriter JoinTree m,
|
|
|
|
Backend ('Postgres pgKind),
|
|
|
|
PostgresAnnotatedFieldJSON pgKind
|
|
|
|
) =>
|
|
|
|
SourcePrefixes ->
|
|
|
|
FieldName ->
|
|
|
|
S.Alias ->
|
|
|
|
ArraySelect ('Postgres pgKind) ->
|
|
|
|
m ()
|
2020-06-08 15:13:01 +03:00
|
|
|
processArrayRelation sourcePrefixes fieldAlias relAlias arrSel =
|
|
|
|
case arrSel of
|
|
|
|
ASSimple annArrRel -> withWriteArrayRelation $ do
|
|
|
|
let AnnRelationSelectG _ colMapping sel = annArrRel
|
|
|
|
permLimitSubQuery =
|
|
|
|
maybe PLSQNotRequired PLSQRequired $ _tpLimit $ _asnPerm sel
|
|
|
|
(source, nodeExtractors) <-
|
|
|
|
processAnnSimpleSelect sourcePrefixes fieldAlias permLimitSubQuery sel
|
2021-09-24 01:56:37 +03:00
|
|
|
let topExtr =
|
|
|
|
asJsonAggExtr
|
|
|
|
JASMultipleRows
|
|
|
|
(S.toAlias fieldAlias)
|
|
|
|
permLimitSubQuery
|
|
|
|
$ orderByForJsonAgg source
|
|
|
|
pure
|
|
|
|
( ArrayRelationSource relAlias colMapping source,
|
|
|
|
topExtr,
|
|
|
|
nodeExtractors,
|
|
|
|
()
|
|
|
|
)
|
2020-06-08 15:13:01 +03:00
|
|
|
ASAggregate aggSel -> withWriteArrayRelation $ do
|
|
|
|
let AnnRelationSelectG _ colMapping sel = aggSel
|
|
|
|
(source, nodeExtractors, topExtr) <-
|
|
|
|
processAnnAggregateSelect sourcePrefixes fieldAlias sel
|
2021-09-24 01:56:37 +03:00
|
|
|
pure
|
|
|
|
( ArrayRelationSource relAlias colMapping source,
|
|
|
|
topExtr,
|
|
|
|
nodeExtractors,
|
|
|
|
()
|
|
|
|
)
|
2020-06-08 15:13:01 +03:00
|
|
|
ASConnection connSel -> withWriteArrayConnection $ do
|
|
|
|
let AnnRelationSelectG _ colMapping sel = connSel
|
|
|
|
(source, topExtractor, nodeExtractors) <-
|
|
|
|
processConnectionSelect sourcePrefixes fieldAlias relAlias colMapping sel
|
2021-09-24 01:56:37 +03:00
|
|
|
pure
|
|
|
|
( source,
|
|
|
|
topExtractor,
|
|
|
|
nodeExtractors,
|
|
|
|
()
|
|
|
|
)
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2021-09-21 13:39:34 +03:00
|
|
|
{- Note [Optimizing queries using limit/offset]
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Refer to the issue https://github.com/hasura/graphql-engine/issues/5745
|
|
|
|
|
|
|
|
Before this change, limit/offset/distinct_on is applied at outer selection
|
|
|
|
node along with order by clause. This greatly reduces query performance if
|
|
|
|
our base selection table contains many rows and relationships are selected
|
|
|
|
which joins remote tables. We need to optimize application of limit wrt to
|
|
|
|
order by input.
|
|
|
|
|
|
|
|
If "Order by" is not present:
|
|
|
|
Apply limit/offset/distinct on at the base table selection
|
|
|
|
Else if "Order by" contains only columns:
|
|
|
|
Apply limit/offset/distinct_on at the base table selection along with order by
|
|
|
|
Otherwise:
|
|
|
|
Apply limit/offset/distinct_on at the node selection along with order by
|
|
|
|
-}
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
processSelectParams ::
|
|
|
|
forall pgKind m.
|
2022-02-23 23:17:58 +03:00
|
|
|
( MonadReader StringifyNumbers m,
|
2021-09-24 01:56:37 +03:00
|
|
|
MonadWriter JoinTree m,
|
|
|
|
Backend ('Postgres pgKind)
|
|
|
|
) =>
|
|
|
|
SourcePrefixes ->
|
|
|
|
FieldName ->
|
|
|
|
SimilarArrayFields ->
|
|
|
|
SelectFrom ('Postgres pgKind) ->
|
|
|
|
PermissionLimitSubQuery ->
|
|
|
|
TablePerm ('Postgres pgKind) ->
|
|
|
|
SelectArgs ('Postgres pgKind) ->
|
|
|
|
m
|
|
|
|
( SelectSource,
|
|
|
|
[(S.Alias, S.SQLExp)],
|
|
|
|
Maybe S.SQLExp -- Order by cursor
|
|
|
|
)
|
2020-06-08 15:13:01 +03:00
|
|
|
processSelectParams
|
2021-09-24 01:56:37 +03:00
|
|
|
sourcePrefixes
|
|
|
|
fieldAlias
|
|
|
|
similarArrFields
|
|
|
|
selectFrom
|
|
|
|
permLimitSubQ
|
|
|
|
tablePermissions
|
|
|
|
tableArgs = do
|
|
|
|
(additionalExtrs, selectSorting, cursorExp) <-
|
|
|
|
processOrderByItems thisSourcePrefix fieldAlias similarArrFields distM orderByM
|
|
|
|
let fromItem = selectFromToFromItem (_pfBase sourcePrefixes) selectFrom
|
|
|
|
finalWhere =
|
|
|
|
toSQLBoolExp (selectFromToQual selectFrom) $
|
|
|
|
maybe permFilter (andAnnBoolExps permFilter) whereM
|
|
|
|
sortingAndSlicing = SortingAndSlicing selectSorting selectSlicing
|
|
|
|
selectSource =
|
|
|
|
SelectSource
|
|
|
|
thisSourcePrefix
|
|
|
|
fromItem
|
|
|
|
finalWhere
|
|
|
|
sortingAndSlicing
|
|
|
|
pure
|
|
|
|
( selectSource,
|
|
|
|
additionalExtrs,
|
|
|
|
cursorExp
|
|
|
|
)
|
|
|
|
where
|
|
|
|
thisSourcePrefix = _pfThis sourcePrefixes
|
|
|
|
SelectArgs whereM orderByM inpLimitM offsetM distM = tableArgs
|
|
|
|
TablePerm permFilter permLimit = tablePermissions
|
|
|
|
selectSlicing = SelectSlicing finalLimit offsetM
|
|
|
|
finalLimit =
|
|
|
|
-- if sub query is required, then only use input limit
|
|
|
|
-- because permission limit is being applied in subquery
|
|
|
|
-- else compare input and permission limits
|
|
|
|
case permLimitSubQ of
|
|
|
|
PLSQRequired _ -> inpLimitM
|
|
|
|
PLSQNotRequired -> compareLimits
|
|
|
|
|
|
|
|
compareLimits =
|
|
|
|
case (inpLimitM, permLimit) of
|
|
|
|
(inpLim, Nothing) -> inpLim
|
|
|
|
(Nothing, permLim) -> permLim
|
|
|
|
(Just inp, Just perm) -> Just if inp < perm then inp else perm
|
|
|
|
|
|
|
|
processOrderByItems ::
|
|
|
|
forall pgKind m.
|
2022-02-23 23:17:58 +03:00
|
|
|
( MonadReader StringifyNumbers m,
|
2021-09-24 01:56:37 +03:00
|
|
|
MonadWriter JoinTree m,
|
|
|
|
Backend ('Postgres pgKind)
|
|
|
|
) =>
|
|
|
|
Identifier ->
|
|
|
|
FieldName ->
|
|
|
|
SimilarArrayFields ->
|
|
|
|
Maybe (NE.NonEmpty PGCol) ->
|
|
|
|
Maybe (NE.NonEmpty (AnnotatedOrderByItem ('Postgres pgKind))) ->
|
|
|
|
m
|
|
|
|
( [(S.Alias, S.SQLExp)], -- Order by Extractors
|
|
|
|
SelectSorting,
|
|
|
|
Maybe S.SQLExp -- The cursor expression
|
|
|
|
)
|
2021-09-21 13:39:34 +03:00
|
|
|
processOrderByItems sourcePrefix' fieldAlias' similarArrayFields distOnCols = \case
|
|
|
|
Nothing -> pure ([], NoSorting $ applyDistinctOnAtBase <$> distOnCols, Nothing)
|
|
|
|
Just orderByItems -> do
|
|
|
|
orderByItemExps <- forM orderByItems processAnnOrderByItem
|
|
|
|
let (sorting, distinctOnExtractors) = generateSorting orderByItemExps
|
|
|
|
orderByExtractors = concat $ toList $ map snd . toList <$> orderByItemExps
|
|
|
|
cursor = mkCursorExp $ toList orderByItemExps
|
|
|
|
pure (orderByExtractors <> distinctOnExtractors, sorting, Just cursor)
|
2020-06-08 15:13:01 +03:00
|
|
|
where
|
2021-09-24 01:56:37 +03:00
|
|
|
processAnnOrderByItem ::
|
|
|
|
AnnotatedOrderByItem ('Postgres pgKind) ->
|
|
|
|
m (OrderByItemG ('Postgres pgKind) (AnnotatedOrderByElement ('Postgres pgKind) (SQLExpression ('Postgres pgKind)), (S.Alias, SQLExpression ('Postgres pgKind))))
|
2020-06-08 15:13:01 +03:00
|
|
|
processAnnOrderByItem orderByItem =
|
2021-09-24 01:56:37 +03:00
|
|
|
forM orderByItem $ \ordByCol ->
|
|
|
|
(ordByCol,)
|
|
|
|
<$> processAnnotatedOrderByElement sourcePrefix' fieldAlias' ordByCol
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
processAnnotatedOrderByElement ::
|
|
|
|
Identifier -> FieldName -> AnnotatedOrderByElement ('Postgres pgKind) S.SQLExp -> m (S.Alias, (SQLExpression ('Postgres pgKind)))
|
2021-07-27 19:27:28 +03:00
|
|
|
processAnnotatedOrderByElement sourcePrefix fieldAlias annObCol = do
|
2020-06-08 15:13:01 +03:00
|
|
|
let ordByAlias = mkAnnOrderByAlias sourcePrefix fieldAlias similarArrayFields annObCol
|
2021-09-24 01:56:37 +03:00
|
|
|
(ordByAlias,) <$> case annObCol of
|
|
|
|
AOCColumn pgColInfo ->
|
|
|
|
pure $
|
2022-01-19 11:37:50 +03:00
|
|
|
S.mkQIdenExp (mkBaseTableAlias sourcePrefix) $ toIdentifier $ ciColumn pgColInfo
|
2020-06-08 15:13:01 +03:00
|
|
|
AOCObjectRelation relInfo relFilter rest -> withWriteObjectRelation $ do
|
server: fix the nullability of object relationships (fix hasura/graphql-engine#7201)
When adding object relationships, we set the nullability of the generated GraphQL field based on whether the database backend enforces that the referenced data always exists. For manual relationships (corresponding to `manual_configuration`), the database backend is unaware of any relationship between data, and hence such fields are always set to be nullable.
For relationships generated from foreign key constraints (corresponding to `foreign_key_constraint_on`), we distinguish between two cases:
1. The "forward" object relationship from a referencing table (i.e. which has the foreign key constraint) to a referenced table. This should be set to be non-nullable when all referencing columns are non-nullable. But in fact, it used to set it to be non-nullable if *any* referencing column is non-nullable, which is only correct in Postgres when `MATCH FULL` is set (a flag we don't consider). This fixes that by changing a boolean conjunction to a disjunction.
2. The "reverse" object relationship from a referenced table to a referencing table which has the foreign key constraint. This should always be set to be nullable. But in fact, it used to always be set to non-nullable, as was reported in hasura/graphql-engine#7201. This fixes that.
Moreover, we have moved the computation of the nullability from `Hasura.RQL.DDL.Relationship` to `Hasura.GraphQL.Schema.Select`: this nullability used to be passed through the `riIsNullable` field of `RelInfo`, but for array relationships this information is not actually used, and moreover the remaining fields of `RelInfo` are already enough to deduce the nullability.
This also adds regression tests for both (1) and (2) above.
https://github.com/hasura/graphql-engine-mono/pull/2159
GitOrigin-RevId: 617f12765614f49746d18d3368f41dfae2f3e6ca
2021-08-26 18:26:43 +03:00
|
|
|
let RelInfo relName _ colMapping relTable _ _ = relInfo
|
2020-06-08 15:13:01 +03:00
|
|
|
relSourcePrefix = mkObjectRelationTableAlias sourcePrefix relName
|
|
|
|
fieldName = mkOrderByFieldName relName
|
|
|
|
(relOrderByAlias, relOrdByExp) <-
|
2021-07-27 19:27:28 +03:00
|
|
|
processAnnotatedOrderByElement relSourcePrefix fieldName rest
|
2021-09-24 01:56:37 +03:00
|
|
|
let selectSource =
|
|
|
|
ObjectSelectSource
|
|
|
|
relSourcePrefix
|
|
|
|
(S.FISimple relTable Nothing)
|
|
|
|
(toSQLBoolExp (S.QualTable relTable) relFilter)
|
2020-06-08 15:13:01 +03:00
|
|
|
relSource = ObjectRelationSource relName colMapping selectSource
|
2021-09-24 01:56:37 +03:00
|
|
|
pure
|
|
|
|
( relSource,
|
|
|
|
HM.singleton relOrderByAlias relOrdByExp,
|
|
|
|
S.mkQIdenExp relSourcePrefix relOrderByAlias
|
|
|
|
)
|
2020-06-08 15:13:01 +03:00
|
|
|
AOCArrayAggregation relInfo relFilter aggOrderBy -> withWriteArrayRelation $ do
|
server: fix the nullability of object relationships (fix hasura/graphql-engine#7201)
When adding object relationships, we set the nullability of the generated GraphQL field based on whether the database backend enforces that the referenced data always exists. For manual relationships (corresponding to `manual_configuration`), the database backend is unaware of any relationship between data, and hence such fields are always set to be nullable.
For relationships generated from foreign key constraints (corresponding to `foreign_key_constraint_on`), we distinguish between two cases:
1. The "forward" object relationship from a referencing table (i.e. which has the foreign key constraint) to a referenced table. This should be set to be non-nullable when all referencing columns are non-nullable. But in fact, it used to set it to be non-nullable if *any* referencing column is non-nullable, which is only correct in Postgres when `MATCH FULL` is set (a flag we don't consider). This fixes that by changing a boolean conjunction to a disjunction.
2. The "reverse" object relationship from a referenced table to a referencing table which has the foreign key constraint. This should always be set to be nullable. But in fact, it used to always be set to non-nullable, as was reported in hasura/graphql-engine#7201. This fixes that.
Moreover, we have moved the computation of the nullability from `Hasura.RQL.DDL.Relationship` to `Hasura.GraphQL.Schema.Select`: this nullability used to be passed through the `riIsNullable` field of `RelInfo`, but for array relationships this information is not actually used, and moreover the remaining fields of `RelInfo` are already enough to deduce the nullability.
This also adds regression tests for both (1) and (2) above.
https://github.com/hasura/graphql-engine-mono/pull/2159
GitOrigin-RevId: 617f12765614f49746d18d3368f41dfae2f3e6ca
2021-08-26 18:26:43 +03:00
|
|
|
let RelInfo relName _ colMapping relTable _ _ = relInfo
|
2020-06-08 15:13:01 +03:00
|
|
|
fieldName = mkOrderByFieldName relName
|
2021-09-24 01:56:37 +03:00
|
|
|
relSourcePrefix =
|
|
|
|
mkArrayRelationSourcePrefix
|
|
|
|
sourcePrefix
|
|
|
|
fieldAlias
|
|
|
|
similarArrayFields
|
|
|
|
fieldName
|
2020-06-08 15:13:01 +03:00
|
|
|
relAlias = mkArrayRelationAlias fieldAlias similarArrayFields fieldName
|
|
|
|
(topExtractor, fields) = mkAggregateOrderByExtractorAndFields aggOrderBy
|
2021-09-24 01:56:37 +03:00
|
|
|
selectSource =
|
|
|
|
SelectSource
|
|
|
|
relSourcePrefix
|
|
|
|
(S.FISimple relTable Nothing)
|
|
|
|
(toSQLBoolExp (S.QualTable relTable) relFilter)
|
|
|
|
noSortingAndSlicing
|
2020-06-08 15:13:01 +03:00
|
|
|
relSource = ArrayRelationSource relAlias colMapping selectSource
|
2021-09-24 01:56:37 +03:00
|
|
|
pure
|
|
|
|
( relSource,
|
|
|
|
topExtractor,
|
|
|
|
HM.fromList $ aggregateFieldsToExtractorExps relSourcePrefix fields,
|
|
|
|
S.mkQIdenExp relSourcePrefix (mkAggregateOrderByAlias aggOrderBy)
|
|
|
|
)
|
|
|
|
AOCComputedField ComputedFieldOrderBy {..} ->
|
2021-07-27 19:27:28 +03:00
|
|
|
case _cfobOrderByElement of
|
2021-09-24 01:56:37 +03:00
|
|
|
CFOBEScalar _ -> do
|
2021-07-27 19:27:28 +03:00
|
|
|
let functionArgs = fromTableRowArgs sourcePrefix _cfobFunctionArgsExp
|
2021-09-24 01:56:37 +03:00
|
|
|
functionExp = S.FunctionExp _cfobFunction functionArgs Nothing
|
2021-07-27 19:27:28 +03:00
|
|
|
pure $ S.SEFunction functionExp
|
|
|
|
CFOBETableAggregation _ tableFilter aggOrderBy -> withWriteComputedFieldTableSet $ do
|
|
|
|
let fieldName = mkOrderByFieldName _cfobName
|
|
|
|
computedFieldSourcePrefix = mkComputedFieldTableAlias sourcePrefix fieldName
|
|
|
|
(topExtractor, fields) = mkAggregateOrderByExtractorAndFields aggOrderBy
|
2021-09-24 01:56:37 +03:00
|
|
|
fromItem =
|
|
|
|
selectFromToFromItem sourcePrefix $
|
|
|
|
FromFunction _cfobFunction _cfobFunctionArgsExp Nothing
|
2021-07-27 19:27:28 +03:00
|
|
|
functionQual = S.QualifiedIdentifier (functionToIdentifier _cfobFunction) Nothing
|
2021-09-24 01:56:37 +03:00
|
|
|
selectSource =
|
|
|
|
SelectSource
|
|
|
|
computedFieldSourcePrefix
|
|
|
|
fromItem
|
|
|
|
(toSQLBoolExp functionQual tableFilter)
|
|
|
|
noSortingAndSlicing
|
2021-07-27 19:27:28 +03:00
|
|
|
source = ComputedFieldTableSetSource fieldName selectSource
|
2021-09-24 01:56:37 +03:00
|
|
|
pure
|
|
|
|
( source,
|
|
|
|
topExtractor,
|
|
|
|
HM.fromList $ aggregateFieldsToExtractorExps computedFieldSourcePrefix fields,
|
|
|
|
S.mkQIdenExp computedFieldSourcePrefix (mkAggregateOrderByAlias aggOrderBy)
|
|
|
|
)
|
|
|
|
|
|
|
|
generateSorting ::
|
|
|
|
NE.NonEmpty (OrderByItemG ('Postgres pgKind) (AnnotatedOrderByElement ('Postgres pgKind) (SQLExpression ('Postgres pgKind)), (S.Alias, SQLExpression ('Postgres pgKind)))) ->
|
|
|
|
( SelectSorting,
|
|
|
|
[(S.Alias, SQLExpression ('Postgres pgKind))] -- 'distinct on' column extractors
|
|
|
|
)
|
2021-09-21 13:39:34 +03:00
|
|
|
generateSorting orderByExps@(firstOrderBy NE.:| restOrderBys) =
|
|
|
|
case fst $ obiColumn firstOrderBy of
|
|
|
|
AOCColumn columnInfo ->
|
|
|
|
-- If rest order by expressions are all columns then apply order by clause at base selection.
|
2021-09-24 01:56:37 +03:00
|
|
|
if all (isJust . getColumnOrderBy . obiColumn) restOrderBys
|
|
|
|
then -- Collect column order by expressions from the rest.
|
|
|
|
|
|
|
|
let restColumnOrderBys = mapMaybe (sequenceA . (getColumnOrderBy <$>)) restOrderBys
|
|
|
|
firstColumnOrderBy = firstOrderBy {obiColumn = columnInfo}
|
|
|
|
in sortAtNodeAndBase $ firstColumnOrderBy NE.:| restColumnOrderBys
|
|
|
|
else -- Else rest order by expressions contain atleast one non-column order by.
|
|
|
|
-- So, apply order by clause at node selection.
|
|
|
|
sortOnlyAtNode
|
|
|
|
_ ->
|
2021-09-21 13:39:34 +03:00
|
|
|
-- First order by expression is non-column. So, apply order by clause at node selection.
|
|
|
|
sortOnlyAtNode
|
|
|
|
where
|
|
|
|
getColumnOrderBy = (^? _AOCColumn) . fst
|
|
|
|
|
|
|
|
(nodeOrderBy, nodeDistinctOn, nodeDistinctOnExtractors) =
|
|
|
|
let toOrderByExp orderByItemExp =
|
|
|
|
let OrderByItemG obTyM expAlias obNullsM = fst . snd <$> orderByItemExp
|
2021-09-24 01:56:37 +03:00
|
|
|
in S.OrderByItem (S.SEIdentifier $ toIdentifier expAlias) obTyM obNullsM
|
2021-09-21 13:39:34 +03:00
|
|
|
orderByExp = S.OrderByExp $ toOrderByExp <$> orderByExps
|
|
|
|
(maybeDistOn, distOnExtrs) = NE.unzip $ applyDistinctOnAtNode sourcePrefix' <$> distOnCols
|
2021-09-24 01:56:37 +03:00
|
|
|
in (orderByExp, maybeDistOn, fromMaybe [] distOnExtrs)
|
2021-09-21 13:39:34 +03:00
|
|
|
|
|
|
|
sortOnlyAtNode =
|
|
|
|
(Sorting $ ASorting (nodeOrderBy, nodeDistinctOn) Nothing, nodeDistinctOnExtractors)
|
|
|
|
|
|
|
|
sortAtNodeAndBase baseColumnOrderBys =
|
|
|
|
let mkBaseOrderByItem (OrderByItemG orderByType columnInfo nullsOrder) =
|
|
|
|
S.OrderByItem
|
2022-01-19 11:37:50 +03:00
|
|
|
(S.SEIdentifier $ toIdentifier $ ciColumn columnInfo)
|
2021-09-24 01:56:37 +03:00
|
|
|
orderByType
|
|
|
|
nullsOrder
|
2021-09-21 13:39:34 +03:00
|
|
|
baseOrderByExp = S.OrderByExp $ mkBaseOrderByItem <$> baseColumnOrderBys
|
|
|
|
baseDistOnExp = applyDistinctOnAtBase <$> distOnCols
|
|
|
|
sorting = Sorting $ ASorting (nodeOrderBy, nodeDistinctOn) $ Just (baseOrderByExp, baseDistOnExp)
|
2021-09-24 01:56:37 +03:00
|
|
|
in (sorting, nodeDistinctOnExtractors)
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
mkCursorExp ::
|
|
|
|
[OrderByItemG ('Postgres pgKind) (AnnotatedOrderByElement ('Postgres pgKind) (SQLExpression ('Postgres pgKind)), (S.Alias, SQLExpression ('Postgres pgKind)))] ->
|
|
|
|
S.SQLExp
|
2020-06-08 15:13:01 +03:00
|
|
|
mkCursorExp orderByItemExps =
|
2021-09-24 01:56:37 +03:00
|
|
|
S.applyJsonBuildObj $
|
|
|
|
flip concatMap orderByItemExps $
|
|
|
|
\orderByItemExp ->
|
|
|
|
let OrderByItemG _ (annObCol, (_, valExp)) _ = orderByItemExp
|
|
|
|
in annObColToJSONField valExp annObCol
|
2020-06-08 15:13:01 +03:00
|
|
|
where
|
2021-07-27 19:27:28 +03:00
|
|
|
mkAggOrderByValExp valExp = \case
|
|
|
|
AAOCount -> [S.SELit "count", valExp]
|
|
|
|
AAOOp opText colInfo ->
|
2021-09-24 01:56:37 +03:00
|
|
|
[ S.SELit opText,
|
2022-01-19 11:37:50 +03:00
|
|
|
S.applyJsonBuildObj [S.SELit $ getPGColTxt $ ciColumn colInfo, valExp]
|
2021-07-27 19:27:28 +03:00
|
|
|
]
|
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
annObColToJSONField valExp = \case
|
2022-01-19 11:37:50 +03:00
|
|
|
AOCColumn pgCol -> [S.SELit $ getPGColTxt $ ciColumn pgCol, valExp]
|
2020-06-08 15:13:01 +03:00
|
|
|
AOCObjectRelation relInfo _ obCol ->
|
2021-09-24 01:56:37 +03:00
|
|
|
[ S.SELit $ relNameToTxt $ riName relInfo,
|
|
|
|
S.applyJsonBuildObj $ annObColToJSONField valExp obCol
|
2020-06-08 15:13:01 +03:00
|
|
|
]
|
|
|
|
AOCArrayAggregation relInfo _ aggOrderBy ->
|
2021-09-24 01:56:37 +03:00
|
|
|
[ S.SELit $ relNameToTxt (riName relInfo) <> "_aggregate",
|
|
|
|
S.applyJsonBuildObj $ mkAggOrderByValExp valExp aggOrderBy
|
2020-06-08 15:13:01 +03:00
|
|
|
]
|
2021-07-27 19:27:28 +03:00
|
|
|
AOCComputedField cfOrderBy ->
|
|
|
|
let fieldNameText = computedFieldNameToText $ _cfobName cfOrderBy
|
2021-09-24 01:56:37 +03:00
|
|
|
in case _cfobOrderByElement cfOrderBy of
|
|
|
|
CFOBEScalar _ -> [S.SELit fieldNameText, valExp]
|
|
|
|
CFOBETableAggregation _ _ aggOrderBy ->
|
|
|
|
[ S.SELit $ fieldNameText <> "_aggregate",
|
|
|
|
S.applyJsonBuildObj $ mkAggOrderByValExp valExp aggOrderBy
|
|
|
|
]
|
|
|
|
|
|
|
|
aggregateFieldsToExtractorExps ::
|
|
|
|
Identifier -> AggregateFields ('Postgres pgKind) -> [(S.Alias, S.SQLExp)]
|
2020-06-08 15:13:01 +03:00
|
|
|
aggregateFieldsToExtractorExps sourcePrefix aggregateFields =
|
|
|
|
flip concatMap aggregateFields $ \(_, field) ->
|
|
|
|
case field of
|
|
|
|
AFCount cty -> case cty of
|
2021-09-24 01:56:37 +03:00
|
|
|
S.CTStar -> []
|
|
|
|
S.CTSimple cols -> colsToExps cols
|
2020-06-08 15:13:01 +03:00
|
|
|
S.CTDistinct cols -> colsToExps cols
|
2021-09-24 01:56:37 +03:00
|
|
|
AFOp aggOp -> aggOpToExps aggOp
|
|
|
|
AFExp _ -> []
|
2020-06-08 15:13:01 +03:00
|
|
|
where
|
2021-01-18 16:51:36 +03:00
|
|
|
colsToExps = fmap mkColExp
|
|
|
|
|
|
|
|
aggOpToExps = mapMaybe colToMaybeExp . _aoFields
|
|
|
|
colToMaybeExp = \case
|
|
|
|
(_, CFCol col _) -> Just $ mkColExp col
|
2021-09-24 01:56:37 +03:00
|
|
|
_ -> Nothing
|
2018-10-31 15:51:20 +03:00
|
|
|
|
2021-01-18 16:51:36 +03:00
|
|
|
mkColExp c =
|
2020-10-27 13:34:31 +03:00
|
|
|
let qualCol = S.mkQIdenExp (mkBaseTableAlias sourcePrefix) (toIdentifier c)
|
|
|
|
colAls = toIdentifier c
|
2021-09-24 01:56:37 +03:00
|
|
|
in (S.Alias colAls, qualCol)
|
2018-10-31 15:51:20 +03:00
|
|
|
|
[Preview] Inherited roles for postgres read queries
fixes #3868
docker image - `hasura/graphql-engine:inherited-roles-preview-48b73a2de`
Note:
To be able to use the inherited roles feature, the graphql-engine should be started with the env variable `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` set to `inherited_roles`.
Introduction
------------
This PR implements the idea of multiple roles as presented in this [paper](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/FGALanguageICDE07.pdf). The multiple roles feature in this PR can be used via inherited roles. An inherited role is a role which can be created by combining multiple singular roles. For example, if there are two roles `author` and `editor` configured in the graphql-engine, then we can create a inherited role with the name of `combined_author_editor` role which will combine the select permissions of the `author` and `editor` roles and then make GraphQL queries using the `combined_author_editor`.
How are select permissions of different roles are combined?
------------------------------------------------------------
A select permission includes 5 things:
1. Columns accessible to the role
2. Row selection filter
3. Limit
4. Allow aggregation
5. Scalar computed fields accessible to the role
Suppose there are two roles, `role1` gives access to the `address` column with row filter `P1` and `role2` gives access to both the `address` and the `phone` column with row filter `P2` and we create a new role `combined_roles` which combines `role1` and `role2`.
Let's say the following GraphQL query is queried with the `combined_roles` role.
```graphql
query {
employees {
address
phone
}
}
```
This will translate to the following SQL query:
```sql
select
(case when (P1 or P2) then address else null end) as address,
(case when P2 then phone else null end) as phone
from employee
where (P1 or P2)
```
The other parameters of the select permission will be combined in the following manner:
1. Limit - Minimum of the limits will be the limit of the inherited role
2. Allow aggregations - If any of the role allows aggregation, then the inherited role will allow aggregation
3. Scalar computed fields - same as table column fields, as in the above example
APIs for inherited roles:
----------------------
1. `add_inherited_role`
`add_inherited_role` is the [metadata API](https://hasura.io/docs/1.0/graphql/core/api-reference/index.html#schema-metadata-api) to create a new inherited role. It accepts two arguments
`role_name`: the name of the inherited role to be added (String)
`role_set`: list of roles that need to be combined (Array of Strings)
Example:
```json
{
"type": "add_inherited_role",
"args": {
"role_name":"combined_user",
"role_set":[
"user",
"user1"
]
}
}
```
After adding the inherited role, the inherited role can be used like single roles like earlier
Note:
An inherited role can only be created with non-inherited/singular roles.
2. `drop_inherited_role`
The `drop_inherited_role` API accepts the name of the inherited role and drops it from the metadata. It accepts a single argument:
`role_name`: name of the inherited role to be dropped
Example:
```json
{
"type": "drop_inherited_role",
"args": {
"role_name":"combined_user"
}
}
```
Metadata
---------
The derived roles metadata will be included under the `experimental_features` key while exporting the metadata.
```json
{
"experimental_features": {
"derived_roles": [
{
"role_name": "manager_is_employee_too",
"role_set": [
"employee",
"manager"
]
}
]
}
}
```
Scope
------
Only postgres queries and subscriptions are supported in this PR.
Important points:
-----------------
1. All columns exposed to an inherited role will be marked as `nullable`, this is done so that cell value nullification can be done.
TODOs
-------
- [ ] Tests
- [ ] Test a GraphQL query running with a inherited role without enabling inherited roles in experimental features
- [] Tests for aggregate queries, limit, computed fields, functions, subscriptions (?)
- [ ] Introspection test with a inherited role (nullability changes in a inherited role)
- [ ] Docs
- [ ] Changelog
Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com>
GitOrigin-RevId: 3b8ee1e11f5ceca80fe294f8c074d42fbccfec63
2021-03-08 14:14:13 +03:00
|
|
|
{- Note: [SQL generation for inherited roles]
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
When a query is executed by an inherited role, each column may contain a predicate
|
2021-04-22 00:44:37 +03:00
|
|
|
(AnnColumnCaseBoolExp ('Postgres pgKind) SQLExp) along with it. The predicate is then
|
[Preview] Inherited roles for postgres read queries
fixes #3868
docker image - `hasura/graphql-engine:inherited-roles-preview-48b73a2de`
Note:
To be able to use the inherited roles feature, the graphql-engine should be started with the env variable `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` set to `inherited_roles`.
Introduction
------------
This PR implements the idea of multiple roles as presented in this [paper](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/FGALanguageICDE07.pdf). The multiple roles feature in this PR can be used via inherited roles. An inherited role is a role which can be created by combining multiple singular roles. For example, if there are two roles `author` and `editor` configured in the graphql-engine, then we can create a inherited role with the name of `combined_author_editor` role which will combine the select permissions of the `author` and `editor` roles and then make GraphQL queries using the `combined_author_editor`.
How are select permissions of different roles are combined?
------------------------------------------------------------
A select permission includes 5 things:
1. Columns accessible to the role
2. Row selection filter
3. Limit
4. Allow aggregation
5. Scalar computed fields accessible to the role
Suppose there are two roles, `role1` gives access to the `address` column with row filter `P1` and `role2` gives access to both the `address` and the `phone` column with row filter `P2` and we create a new role `combined_roles` which combines `role1` and `role2`.
Let's say the following GraphQL query is queried with the `combined_roles` role.
```graphql
query {
employees {
address
phone
}
}
```
This will translate to the following SQL query:
```sql
select
(case when (P1 or P2) then address else null end) as address,
(case when P2 then phone else null end) as phone
from employee
where (P1 or P2)
```
The other parameters of the select permission will be combined in the following manner:
1. Limit - Minimum of the limits will be the limit of the inherited role
2. Allow aggregations - If any of the role allows aggregation, then the inherited role will allow aggregation
3. Scalar computed fields - same as table column fields, as in the above example
APIs for inherited roles:
----------------------
1. `add_inherited_role`
`add_inherited_role` is the [metadata API](https://hasura.io/docs/1.0/graphql/core/api-reference/index.html#schema-metadata-api) to create a new inherited role. It accepts two arguments
`role_name`: the name of the inherited role to be added (String)
`role_set`: list of roles that need to be combined (Array of Strings)
Example:
```json
{
"type": "add_inherited_role",
"args": {
"role_name":"combined_user",
"role_set":[
"user",
"user1"
]
}
}
```
After adding the inherited role, the inherited role can be used like single roles like earlier
Note:
An inherited role can only be created with non-inherited/singular roles.
2. `drop_inherited_role`
The `drop_inherited_role` API accepts the name of the inherited role and drops it from the metadata. It accepts a single argument:
`role_name`: name of the inherited role to be dropped
Example:
```json
{
"type": "drop_inherited_role",
"args": {
"role_name":"combined_user"
}
}
```
Metadata
---------
The derived roles metadata will be included under the `experimental_features` key while exporting the metadata.
```json
{
"experimental_features": {
"derived_roles": [
{
"role_name": "manager_is_employee_too",
"role_set": [
"employee",
"manager"
]
}
]
}
}
```
Scope
------
Only postgres queries and subscriptions are supported in this PR.
Important points:
-----------------
1. All columns exposed to an inherited role will be marked as `nullable`, this is done so that cell value nullification can be done.
TODOs
-------
- [ ] Tests
- [ ] Test a GraphQL query running with a inherited role without enabling inherited roles in experimental features
- [] Tests for aggregate queries, limit, computed fields, functions, subscriptions (?)
- [ ] Introspection test with a inherited role (nullability changes in a inherited role)
- [ ] Docs
- [ ] Changelog
Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com>
GitOrigin-RevId: 3b8ee1e11f5ceca80fe294f8c074d42fbccfec63
2021-03-08 14:14:13 +03:00
|
|
|
converted to a BoolExp, which will be used to check if the said column should
|
|
|
|
be nullified. For example,
|
|
|
|
|
|
|
|
Suppose there are two roles, role1 gives access only to the `addr` column with
|
|
|
|
row filter P1 and role2 gives access to both addr and phone column with row
|
|
|
|
filter P2. The `OR`ing of the predicates will have already been done while
|
|
|
|
the schema has been generated. The SQL generated will look like this:
|
|
|
|
|
|
|
|
select
|
|
|
|
(case when (P1 or P2) then addr else null end) as addr,
|
|
|
|
(case when P2 then phone else null end) as phone
|
|
|
|
from employee
|
|
|
|
where (P1 or P2)
|
|
|
|
|
|
|
|
-}
|
|
|
|
|
2021-05-21 05:46:58 +03:00
|
|
|
class PostgresAnnotatedFieldJSON (pgKind :: PostgresKind) where
|
|
|
|
annRowToJson :: FieldName -> [(FieldName, S.SQLExp)] -> (S.Alias, S.SQLExp)
|
|
|
|
|
|
|
|
instance PostgresAnnotatedFieldJSON 'Vanilla where
|
|
|
|
annRowToJson fieldAlias fieldExps =
|
|
|
|
-- postgres ignores anything beyond 63 chars for an iden
|
|
|
|
-- in this case, we'll need to use json_build_object function
|
|
|
|
-- json_build_object is slower than row_to_json hence it is only
|
|
|
|
-- used when needed
|
2021-09-24 01:56:37 +03:00
|
|
|
if any ((> 63) . T.length . getFieldNameTxt . fst) fieldExps
|
|
|
|
then withJsonBuildObj fieldAlias $ concatMap toJsonBuildObjectExps fieldExps
|
|
|
|
else withRowToJSON fieldAlias $ map toRowToJsonExtr fieldExps
|
2021-05-21 05:46:58 +03:00
|
|
|
where
|
|
|
|
toJsonBuildObjectExps (fieldName, fieldExp) =
|
|
|
|
[S.SELit $ getFieldNameTxt fieldName, fieldExp]
|
|
|
|
|
|
|
|
toRowToJsonExtr (fieldName, fieldExp) =
|
|
|
|
S.Extractor fieldExp $ Just $ S.toAlias fieldName
|
|
|
|
|
|
|
|
instance PostgresAnnotatedFieldJSON 'Citus where
|
|
|
|
annRowToJson fieldAlias fieldExps =
|
|
|
|
-- Due to the restrictions Citus imposes on joins between tables of various
|
|
|
|
-- distribution types we cannot use row_to_json and have to only rely on
|
|
|
|
-- json_build_object.
|
|
|
|
withJsonBuildObj fieldAlias $ concatMap toJsonBuildObjectExps fieldExps
|
|
|
|
where
|
|
|
|
toJsonBuildObjectExps (fieldName, fieldExp) =
|
|
|
|
[S.SELit $ getFieldNameTxt fieldName, fieldExp]
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
processAnnFields ::
|
|
|
|
forall pgKind m.
|
2022-02-23 23:17:58 +03:00
|
|
|
( MonadReader StringifyNumbers m,
|
2021-09-24 01:56:37 +03:00
|
|
|
MonadWriter JoinTree m,
|
|
|
|
Backend ('Postgres pgKind),
|
|
|
|
PostgresAnnotatedFieldJSON pgKind
|
|
|
|
) =>
|
|
|
|
Identifier ->
|
|
|
|
FieldName ->
|
|
|
|
SimilarArrayFields ->
|
|
|
|
AnnFields ('Postgres pgKind) ->
|
|
|
|
m (S.Alias, S.SQLExp)
|
2020-06-08 15:13:01 +03:00
|
|
|
processAnnFields sourcePrefix fieldAlias similarArrFields annFields = do
|
|
|
|
fieldExps <- forM annFields $ \(fieldName, field) ->
|
2021-09-24 01:56:37 +03:00
|
|
|
(fieldName,)
|
|
|
|
<$> case field of
|
|
|
|
AFExpression t -> pure $ S.SELit t
|
|
|
|
AFNodeId _ tn pKeys -> pure $ mkNodeId tn pKeys
|
|
|
|
AFColumn c -> toSQLCol c
|
|
|
|
AFObjectRelation objSel -> withWriteObjectRelation $ do
|
|
|
|
let AnnRelationSelectG relName relMapping annObjSel = objSel
|
|
|
|
AnnObjectSelectG objAnnFields tableFrom tableFilter = annObjSel
|
|
|
|
objRelSourcePrefix = mkObjectRelationTableAlias sourcePrefix relName
|
|
|
|
sourcePrefixes = mkSourcePrefixes objRelSourcePrefix
|
|
|
|
annFieldsExtr <- processAnnFields (_pfThis sourcePrefixes) fieldName HM.empty objAnnFields
|
|
|
|
let selectSource =
|
|
|
|
ObjectSelectSource
|
|
|
|
(_pfThis sourcePrefixes)
|
|
|
|
(S.FISimple tableFrom Nothing)
|
|
|
|
(toSQLBoolExp (S.QualTable tableFrom) tableFilter)
|
|
|
|
objRelSource = ObjectRelationSource relName relMapping selectSource
|
|
|
|
pure
|
|
|
|
( objRelSource,
|
|
|
|
HM.fromList [annFieldsExtr],
|
|
|
|
S.mkQIdenExp objRelSourcePrefix fieldName
|
|
|
|
)
|
|
|
|
AFArrayRelation arrSel -> do
|
|
|
|
let arrRelSourcePrefix = mkArrayRelationSourcePrefix sourcePrefix fieldAlias similarArrFields fieldName
|
|
|
|
arrRelAlias = mkArrayRelationAlias fieldAlias similarArrFields fieldName
|
|
|
|
processArrayRelation (mkSourcePrefixes arrRelSourcePrefix) fieldName arrRelAlias arrSel
|
|
|
|
pure $ S.mkQIdenExp arrRelSourcePrefix fieldName
|
|
|
|
AFComputedField _ _ (CFSScalar scalar caseBoolExpMaybe) -> do
|
|
|
|
computedFieldSQLExp <- fromScalarComputedField scalar
|
|
|
|
-- The computed field is conditionally outputed depending
|
|
|
|
-- on the presence of `caseBoolExpMaybe` and the value it
|
|
|
|
-- evaluates to. `caseBoolExpMaybe` will be set only in the
|
|
|
|
-- case of an inherited role.
|
|
|
|
-- See [SQL generation for inherited role]
|
|
|
|
case caseBoolExpMaybe of
|
|
|
|
Nothing -> pure computedFieldSQLExp
|
|
|
|
Just caseBoolExp ->
|
|
|
|
let boolExp =
|
|
|
|
S.simplifyBoolExp $
|
|
|
|
toSQLBoolExp (S.QualifiedIdentifier baseTableIdentifier Nothing) $
|
|
|
|
_accColCaseBoolExpField <$> caseBoolExp
|
|
|
|
in pure $ S.SECond boolExp computedFieldSQLExp S.SENull
|
|
|
|
AFComputedField _ _ (CFSTable selectTy sel) -> withWriteComputedFieldTableSet $ do
|
|
|
|
let computedFieldSourcePrefix =
|
|
|
|
mkComputedFieldTableAlias sourcePrefix fieldName
|
|
|
|
(selectSource, nodeExtractors) <-
|
|
|
|
processAnnSimpleSelect
|
|
|
|
(mkSourcePrefixes computedFieldSourcePrefix)
|
|
|
|
fieldName
|
|
|
|
PLSQNotRequired
|
|
|
|
sel
|
|
|
|
let computedFieldTableSetSource = ComputedFieldTableSetSource fieldName selectSource
|
|
|
|
extractor =
|
|
|
|
asJsonAggExtr selectTy (S.toAlias fieldName) PLSQNotRequired $
|
|
|
|
orderByForJsonAgg selectSource
|
|
|
|
pure
|
|
|
|
( computedFieldTableSetSource,
|
|
|
|
extractor,
|
|
|
|
nodeExtractors,
|
|
|
|
S.mkQIdenExp computedFieldSourcePrefix fieldName
|
|
|
|
)
|
server: IR for DB-DB joins
### Description
This PR adds the required IR for DB to DB joins, based on @paf31 and @0x777 's `feature/db-to-db` branch.
To do so, it also refactors the IR to introduce a new type parameter, `r`, which is used to recursively constructs the `v` parameter of remote QueryDBs. When collecting remote joins, we replace `r` with `Const Void`, indicating at the type level that there cannot be any leftover remote join.
Furthermore, this PR refactors IR.Select for readability, moves some code from IR.Root to IR.Select to avoid having to deal with circular dependencies, and makes it compile by adding `error` in all new cases in the execution pipeline.
The diff doesn't make it clear, but most of Select.hs is actually unchanged. Declarations have just been reordered by topic, in the following order:
- type declarations
- instance declarations
- type aliases
- constructor functions
- traverse functions
https://github.com/hasura/graphql-engine-mono/pull/1580
Co-authored-by: Phil Freeman <630306+paf31@users.noreply.github.com>
GitOrigin-RevId: bbdcb4119cec8bb3fc32f1294f91b8dea0728721
2021-06-18 02:12:11 +03:00
|
|
|
|
2021-05-21 05:46:58 +03:00
|
|
|
pure $ annRowToJson @pgKind fieldAlias fieldExps
|
2020-06-08 15:13:01 +03:00
|
|
|
where
|
|
|
|
mkSourcePrefixes newPrefix = SourcePrefixes newPrefix sourcePrefix
|
|
|
|
|
[Preview] Inherited roles for postgres read queries
fixes #3868
docker image - `hasura/graphql-engine:inherited-roles-preview-48b73a2de`
Note:
To be able to use the inherited roles feature, the graphql-engine should be started with the env variable `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` set to `inherited_roles`.
Introduction
------------
This PR implements the idea of multiple roles as presented in this [paper](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/FGALanguageICDE07.pdf). The multiple roles feature in this PR can be used via inherited roles. An inherited role is a role which can be created by combining multiple singular roles. For example, if there are two roles `author` and `editor` configured in the graphql-engine, then we can create a inherited role with the name of `combined_author_editor` role which will combine the select permissions of the `author` and `editor` roles and then make GraphQL queries using the `combined_author_editor`.
How are select permissions of different roles are combined?
------------------------------------------------------------
A select permission includes 5 things:
1. Columns accessible to the role
2. Row selection filter
3. Limit
4. Allow aggregation
5. Scalar computed fields accessible to the role
Suppose there are two roles, `role1` gives access to the `address` column with row filter `P1` and `role2` gives access to both the `address` and the `phone` column with row filter `P2` and we create a new role `combined_roles` which combines `role1` and `role2`.
Let's say the following GraphQL query is queried with the `combined_roles` role.
```graphql
query {
employees {
address
phone
}
}
```
This will translate to the following SQL query:
```sql
select
(case when (P1 or P2) then address else null end) as address,
(case when P2 then phone else null end) as phone
from employee
where (P1 or P2)
```
The other parameters of the select permission will be combined in the following manner:
1. Limit - Minimum of the limits will be the limit of the inherited role
2. Allow aggregations - If any of the role allows aggregation, then the inherited role will allow aggregation
3. Scalar computed fields - same as table column fields, as in the above example
APIs for inherited roles:
----------------------
1. `add_inherited_role`
`add_inherited_role` is the [metadata API](https://hasura.io/docs/1.0/graphql/core/api-reference/index.html#schema-metadata-api) to create a new inherited role. It accepts two arguments
`role_name`: the name of the inherited role to be added (String)
`role_set`: list of roles that need to be combined (Array of Strings)
Example:
```json
{
"type": "add_inherited_role",
"args": {
"role_name":"combined_user",
"role_set":[
"user",
"user1"
]
}
}
```
After adding the inherited role, the inherited role can be used like single roles like earlier
Note:
An inherited role can only be created with non-inherited/singular roles.
2. `drop_inherited_role`
The `drop_inherited_role` API accepts the name of the inherited role and drops it from the metadata. It accepts a single argument:
`role_name`: name of the inherited role to be dropped
Example:
```json
{
"type": "drop_inherited_role",
"args": {
"role_name":"combined_user"
}
}
```
Metadata
---------
The derived roles metadata will be included under the `experimental_features` key while exporting the metadata.
```json
{
"experimental_features": {
"derived_roles": [
{
"role_name": "manager_is_employee_too",
"role_set": [
"employee",
"manager"
]
}
]
}
}
```
Scope
------
Only postgres queries and subscriptions are supported in this PR.
Important points:
-----------------
1. All columns exposed to an inherited role will be marked as `nullable`, this is done so that cell value nullification can be done.
TODOs
-------
- [ ] Tests
- [ ] Test a GraphQL query running with a inherited role without enabling inherited roles in experimental features
- [] Tests for aggregate queries, limit, computed fields, functions, subscriptions (?)
- [ ] Introspection test with a inherited role (nullability changes in a inherited role)
- [ ] Docs
- [ ] Changelog
Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com>
GitOrigin-RevId: 3b8ee1e11f5ceca80fe294f8c074d42fbccfec63
2021-03-08 14:14:13 +03:00
|
|
|
baseTableIdentifier = mkBaseTableAlias sourcePrefix
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
toSQLCol :: AnnColumnField ('Postgres pgKind) S.SQLExp -> m S.SQLExp
|
2021-09-22 13:43:05 +03:00
|
|
|
toSQLCol (AnnColumnField col typ asText colOpM caseBoolExpMaybe) = do
|
2020-06-08 15:13:01 +03:00
|
|
|
strfyNum <- ask
|
[Preview] Inherited roles for postgres read queries
fixes #3868
docker image - `hasura/graphql-engine:inherited-roles-preview-48b73a2de`
Note:
To be able to use the inherited roles feature, the graphql-engine should be started with the env variable `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` set to `inherited_roles`.
Introduction
------------
This PR implements the idea of multiple roles as presented in this [paper](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/FGALanguageICDE07.pdf). The multiple roles feature in this PR can be used via inherited roles. An inherited role is a role which can be created by combining multiple singular roles. For example, if there are two roles `author` and `editor` configured in the graphql-engine, then we can create a inherited role with the name of `combined_author_editor` role which will combine the select permissions of the `author` and `editor` roles and then make GraphQL queries using the `combined_author_editor`.
How are select permissions of different roles are combined?
------------------------------------------------------------
A select permission includes 5 things:
1. Columns accessible to the role
2. Row selection filter
3. Limit
4. Allow aggregation
5. Scalar computed fields accessible to the role
Suppose there are two roles, `role1` gives access to the `address` column with row filter `P1` and `role2` gives access to both the `address` and the `phone` column with row filter `P2` and we create a new role `combined_roles` which combines `role1` and `role2`.
Let's say the following GraphQL query is queried with the `combined_roles` role.
```graphql
query {
employees {
address
phone
}
}
```
This will translate to the following SQL query:
```sql
select
(case when (P1 or P2) then address else null end) as address,
(case when P2 then phone else null end) as phone
from employee
where (P1 or P2)
```
The other parameters of the select permission will be combined in the following manner:
1. Limit - Minimum of the limits will be the limit of the inherited role
2. Allow aggregations - If any of the role allows aggregation, then the inherited role will allow aggregation
3. Scalar computed fields - same as table column fields, as in the above example
APIs for inherited roles:
----------------------
1. `add_inherited_role`
`add_inherited_role` is the [metadata API](https://hasura.io/docs/1.0/graphql/core/api-reference/index.html#schema-metadata-api) to create a new inherited role. It accepts two arguments
`role_name`: the name of the inherited role to be added (String)
`role_set`: list of roles that need to be combined (Array of Strings)
Example:
```json
{
"type": "add_inherited_role",
"args": {
"role_name":"combined_user",
"role_set":[
"user",
"user1"
]
}
}
```
After adding the inherited role, the inherited role can be used like single roles like earlier
Note:
An inherited role can only be created with non-inherited/singular roles.
2. `drop_inherited_role`
The `drop_inherited_role` API accepts the name of the inherited role and drops it from the metadata. It accepts a single argument:
`role_name`: name of the inherited role to be dropped
Example:
```json
{
"type": "drop_inherited_role",
"args": {
"role_name":"combined_user"
}
}
```
Metadata
---------
The derived roles metadata will be included under the `experimental_features` key while exporting the metadata.
```json
{
"experimental_features": {
"derived_roles": [
{
"role_name": "manager_is_employee_too",
"role_set": [
"employee",
"manager"
]
}
]
}
}
```
Scope
------
Only postgres queries and subscriptions are supported in this PR.
Important points:
-----------------
1. All columns exposed to an inherited role will be marked as `nullable`, this is done so that cell value nullification can be done.
TODOs
-------
- [ ] Tests
- [ ] Test a GraphQL query running with a inherited role without enabling inherited roles in experimental features
- [] Tests for aggregate queries, limit, computed fields, functions, subscriptions (?)
- [ ] Introspection test with a inherited role (nullability changes in a inherited role)
- [ ] Docs
- [ ] Changelog
Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com>
GitOrigin-RevId: 3b8ee1e11f5ceca80fe294f8c074d42fbccfec63
2021-03-08 14:14:13 +03:00
|
|
|
let sqlExpression =
|
|
|
|
withColumnOp colOpM $
|
2021-09-24 01:56:37 +03:00
|
|
|
S.mkQIdenExp baseTableIdentifier col
|
[Preview] Inherited roles for postgres read queries
fixes #3868
docker image - `hasura/graphql-engine:inherited-roles-preview-48b73a2de`
Note:
To be able to use the inherited roles feature, the graphql-engine should be started with the env variable `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` set to `inherited_roles`.
Introduction
------------
This PR implements the idea of multiple roles as presented in this [paper](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/FGALanguageICDE07.pdf). The multiple roles feature in this PR can be used via inherited roles. An inherited role is a role which can be created by combining multiple singular roles. For example, if there are two roles `author` and `editor` configured in the graphql-engine, then we can create a inherited role with the name of `combined_author_editor` role which will combine the select permissions of the `author` and `editor` roles and then make GraphQL queries using the `combined_author_editor`.
How are select permissions of different roles are combined?
------------------------------------------------------------
A select permission includes 5 things:
1. Columns accessible to the role
2. Row selection filter
3. Limit
4. Allow aggregation
5. Scalar computed fields accessible to the role
Suppose there are two roles, `role1` gives access to the `address` column with row filter `P1` and `role2` gives access to both the `address` and the `phone` column with row filter `P2` and we create a new role `combined_roles` which combines `role1` and `role2`.
Let's say the following GraphQL query is queried with the `combined_roles` role.
```graphql
query {
employees {
address
phone
}
}
```
This will translate to the following SQL query:
```sql
select
(case when (P1 or P2) then address else null end) as address,
(case when P2 then phone else null end) as phone
from employee
where (P1 or P2)
```
The other parameters of the select permission will be combined in the following manner:
1. Limit - Minimum of the limits will be the limit of the inherited role
2. Allow aggregations - If any of the role allows aggregation, then the inherited role will allow aggregation
3. Scalar computed fields - same as table column fields, as in the above example
APIs for inherited roles:
----------------------
1. `add_inherited_role`
`add_inherited_role` is the [metadata API](https://hasura.io/docs/1.0/graphql/core/api-reference/index.html#schema-metadata-api) to create a new inherited role. It accepts two arguments
`role_name`: the name of the inherited role to be added (String)
`role_set`: list of roles that need to be combined (Array of Strings)
Example:
```json
{
"type": "add_inherited_role",
"args": {
"role_name":"combined_user",
"role_set":[
"user",
"user1"
]
}
}
```
After adding the inherited role, the inherited role can be used like single roles like earlier
Note:
An inherited role can only be created with non-inherited/singular roles.
2. `drop_inherited_role`
The `drop_inherited_role` API accepts the name of the inherited role and drops it from the metadata. It accepts a single argument:
`role_name`: name of the inherited role to be dropped
Example:
```json
{
"type": "drop_inherited_role",
"args": {
"role_name":"combined_user"
}
}
```
Metadata
---------
The derived roles metadata will be included under the `experimental_features` key while exporting the metadata.
```json
{
"experimental_features": {
"derived_roles": [
{
"role_name": "manager_is_employee_too",
"role_set": [
"employee",
"manager"
]
}
]
}
}
```
Scope
------
Only postgres queries and subscriptions are supported in this PR.
Important points:
-----------------
1. All columns exposed to an inherited role will be marked as `nullable`, this is done so that cell value nullification can be done.
TODOs
-------
- [ ] Tests
- [ ] Test a GraphQL query running with a inherited role without enabling inherited roles in experimental features
- [] Tests for aggregate queries, limit, computed fields, functions, subscriptions (?)
- [ ] Introspection test with a inherited role (nullability changes in a inherited role)
- [ ] Docs
- [ ] Changelog
Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com>
GitOrigin-RevId: 3b8ee1e11f5ceca80fe294f8c074d42fbccfec63
2021-03-08 14:14:13 +03:00
|
|
|
finalSQLExpression =
|
|
|
|
-- Check out [SQL generation for inherited role]
|
|
|
|
case caseBoolExpMaybe of
|
2021-09-24 01:56:37 +03:00
|
|
|
Nothing -> sqlExpression
|
[Preview] Inherited roles for postgres read queries
fixes #3868
docker image - `hasura/graphql-engine:inherited-roles-preview-48b73a2de`
Note:
To be able to use the inherited roles feature, the graphql-engine should be started with the env variable `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` set to `inherited_roles`.
Introduction
------------
This PR implements the idea of multiple roles as presented in this [paper](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/FGALanguageICDE07.pdf). The multiple roles feature in this PR can be used via inherited roles. An inherited role is a role which can be created by combining multiple singular roles. For example, if there are two roles `author` and `editor` configured in the graphql-engine, then we can create a inherited role with the name of `combined_author_editor` role which will combine the select permissions of the `author` and `editor` roles and then make GraphQL queries using the `combined_author_editor`.
How are select permissions of different roles are combined?
------------------------------------------------------------
A select permission includes 5 things:
1. Columns accessible to the role
2. Row selection filter
3. Limit
4. Allow aggregation
5. Scalar computed fields accessible to the role
Suppose there are two roles, `role1` gives access to the `address` column with row filter `P1` and `role2` gives access to both the `address` and the `phone` column with row filter `P2` and we create a new role `combined_roles` which combines `role1` and `role2`.
Let's say the following GraphQL query is queried with the `combined_roles` role.
```graphql
query {
employees {
address
phone
}
}
```
This will translate to the following SQL query:
```sql
select
(case when (P1 or P2) then address else null end) as address,
(case when P2 then phone else null end) as phone
from employee
where (P1 or P2)
```
The other parameters of the select permission will be combined in the following manner:
1. Limit - Minimum of the limits will be the limit of the inherited role
2. Allow aggregations - If any of the role allows aggregation, then the inherited role will allow aggregation
3. Scalar computed fields - same as table column fields, as in the above example
APIs for inherited roles:
----------------------
1. `add_inherited_role`
`add_inherited_role` is the [metadata API](https://hasura.io/docs/1.0/graphql/core/api-reference/index.html#schema-metadata-api) to create a new inherited role. It accepts two arguments
`role_name`: the name of the inherited role to be added (String)
`role_set`: list of roles that need to be combined (Array of Strings)
Example:
```json
{
"type": "add_inherited_role",
"args": {
"role_name":"combined_user",
"role_set":[
"user",
"user1"
]
}
}
```
After adding the inherited role, the inherited role can be used like single roles like earlier
Note:
An inherited role can only be created with non-inherited/singular roles.
2. `drop_inherited_role`
The `drop_inherited_role` API accepts the name of the inherited role and drops it from the metadata. It accepts a single argument:
`role_name`: name of the inherited role to be dropped
Example:
```json
{
"type": "drop_inherited_role",
"args": {
"role_name":"combined_user"
}
}
```
Metadata
---------
The derived roles metadata will be included under the `experimental_features` key while exporting the metadata.
```json
{
"experimental_features": {
"derived_roles": [
{
"role_name": "manager_is_employee_too",
"role_set": [
"employee",
"manager"
]
}
]
}
}
```
Scope
------
Only postgres queries and subscriptions are supported in this PR.
Important points:
-----------------
1. All columns exposed to an inherited role will be marked as `nullable`, this is done so that cell value nullification can be done.
TODOs
-------
- [ ] Tests
- [ ] Test a GraphQL query running with a inherited role without enabling inherited roles in experimental features
- [] Tests for aggregate queries, limit, computed fields, functions, subscriptions (?)
- [ ] Introspection test with a inherited role (nullability changes in a inherited role)
- [ ] Docs
- [ ] Changelog
Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com>
GitOrigin-RevId: 3b8ee1e11f5ceca80fe294f8c074d42fbccfec63
2021-03-08 14:14:13 +03:00
|
|
|
Just caseBoolExp ->
|
|
|
|
let boolExp =
|
2021-09-24 01:56:37 +03:00
|
|
|
S.simplifyBoolExp $
|
|
|
|
toSQLBoolExp (S.QualifiedIdentifier baseTableIdentifier Nothing) $
|
|
|
|
_accColCaseBoolExpField <$> caseBoolExp
|
|
|
|
in S.SECond boolExp sqlExpression S.SENull
|
2021-09-22 13:43:05 +03:00
|
|
|
pure $ toJSONableExp strfyNum typ asText finalSQLExpression
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
fromScalarComputedField :: ComputedFieldScalarSelect ('Postgres pgKind) S.SQLExp -> m S.SQLExp
|
2020-06-08 15:13:01 +03:00
|
|
|
fromScalarComputedField computedFieldScalar = do
|
|
|
|
strfyNum <- ask
|
2021-09-24 01:56:37 +03:00
|
|
|
pure $
|
|
|
|
toJSONableExp strfyNum (ColumnScalar ty) False $
|
|
|
|
withColumnOp colOpM $
|
|
|
|
S.SEFunction $ S.FunctionExp fn (fromTableRowArgs sourcePrefix args) Nothing
|
2020-06-08 15:13:01 +03:00
|
|
|
where
|
|
|
|
ComputedFieldScalarSelect fn args ty colOpM = computedFieldScalar
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
withColumnOp :: Maybe (ColumnOp ('Postgres pgKind)) -> S.SQLExp -> S.SQLExp
|
2020-06-08 15:13:01 +03:00
|
|
|
withColumnOp colOpM sqlExp = case colOpM of
|
2021-09-24 01:56:37 +03:00
|
|
|
Nothing -> sqlExp
|
2020-06-08 15:13:01 +03:00
|
|
|
Just (ColumnOp opText cExp) -> S.mkSQLOpExp opText sqlExp cExp
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
mkNodeId :: QualifiedTable -> PrimaryKeyColumns ('Postgres pgKind) -> S.SQLExp
|
2020-06-08 15:13:01 +03:00
|
|
|
mkNodeId (QualifiedObject tableSchema tableName) pkeyColumns =
|
2020-06-16 17:25:49 +03:00
|
|
|
let columnInfoToSQLExp pgColumnInfo =
|
2022-02-23 23:17:58 +03:00
|
|
|
toJSONableExp LeaveNumbersAlone (ciType pgColumnInfo) False $
|
2022-01-19 11:37:50 +03:00
|
|
|
S.mkQIdenExp (mkBaseTableAlias sourcePrefix) $ ciColumn pgColumnInfo
|
2021-09-24 01:56:37 +03:00
|
|
|
in -- See Note [Relay Node id].
|
|
|
|
encodeBase64 $
|
|
|
|
flip S.SETyAnn S.textTypeAnn $
|
|
|
|
S.applyJsonBuildArray $
|
|
|
|
[ S.intToSQLExp $ nodeIdVersionInt currentNodeIdVersion,
|
|
|
|
S.SELit (getSchemaTxt tableSchema),
|
|
|
|
S.SELit (toTxt tableName)
|
|
|
|
]
|
|
|
|
<> map columnInfoToSQLExp (toList pkeyColumns)
|
|
|
|
|
|
|
|
injectJoinCond ::
|
|
|
|
-- | Join condition
|
|
|
|
S.BoolExp ->
|
|
|
|
-- | Where condition
|
|
|
|
S.BoolExp ->
|
|
|
|
-- | New where frag
|
|
|
|
S.WhereFrag
|
2018-10-31 15:51:20 +03:00
|
|
|
injectJoinCond joinCond whereCond =
|
|
|
|
S.WhereFrag $ S.simplifyBoolExp $ S.BEBin S.AndOp joinCond whereCond
|
|
|
|
|
2019-12-13 00:46:33 +03:00
|
|
|
mkJoinCond :: S.Alias -> HashMap PGCol PGCol -> S.BoolExp
|
2019-10-18 11:29:47 +03:00
|
|
|
mkJoinCond baseTablepfx colMapn =
|
2021-09-24 01:56:37 +03:00
|
|
|
foldl' (S.BEBin S.AndOp) (S.BELit True) $
|
|
|
|
flip map (HM.toList colMapn) $ \(lCol, rCol) ->
|
|
|
|
S.BECompare S.SEQ (S.mkQIdenExp baseTablepfx lCol) (S.mkSIdenExp rCol)
|
|
|
|
|
|
|
|
generateSQLSelect ::
|
|
|
|
-- | Pre join condition
|
|
|
|
S.BoolExp ->
|
|
|
|
SelectSource ->
|
|
|
|
SelectNode ->
|
|
|
|
S.Select
|
2020-06-08 15:13:01 +03:00
|
|
|
generateSQLSelect joinCondition selectSource selectNode =
|
2018-10-31 15:51:20 +03:00
|
|
|
S.mkSelect
|
2021-09-24 01:56:37 +03:00
|
|
|
{ S.selExtr = [S.Extractor e $ Just a | (a, e) <- HM.toList extractors],
|
|
|
|
S.selFrom = Just $ S.FromExp [joinedFrom],
|
|
|
|
S.selOrderBy = nodeOrderBy,
|
|
|
|
S.selLimit = S.LimitExp . S.intToSQLExp <$> _ssLimit nodeSlicing,
|
|
|
|
S.selOffset = S.OffsetExp . S.int64ToSQLExp <$> _ssOffset nodeSlicing,
|
|
|
|
S.selDistinct = nodeDistinctOn
|
|
|
|
}
|
2018-10-31 15:51:20 +03:00
|
|
|
where
|
2021-09-21 13:39:34 +03:00
|
|
|
SelectSource sourcePrefix fromItem whereExp sortAndSlice = selectSource
|
2020-06-08 15:13:01 +03:00
|
|
|
SelectNode extractors joinTree = selectNode
|
|
|
|
JoinTree objectRelations arrayRelations arrayConnections computedFields = joinTree
|
2021-09-24 01:56:37 +03:00
|
|
|
ApplySortingAndSlicing
|
|
|
|
(baseOrderBy, baseSlicing, baseDistinctOn)
|
2021-09-21 13:39:34 +03:00
|
|
|
(nodeOrderBy, nodeSlicing, nodeDistinctOn) = applySortingAndSlicing sortAndSlice
|
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
-- this is the table which is aliased as "sourcePrefix.base"
|
2021-09-24 01:56:37 +03:00
|
|
|
baseSelect =
|
|
|
|
S.mkSelect
|
|
|
|
{ S.selExtr = [S.Extractor (S.SEStar Nothing) Nothing],
|
|
|
|
S.selFrom = Just $ S.FromExp [fromItem],
|
|
|
|
S.selWhere = Just $ injectJoinCond joinCondition whereExp,
|
|
|
|
S.selOrderBy = baseOrderBy,
|
|
|
|
S.selLimit = S.LimitExp . S.intToSQLExp <$> _ssLimit baseSlicing,
|
|
|
|
S.selOffset = S.OffsetExp . S.int64ToSQLExp <$> _ssOffset baseSlicing,
|
|
|
|
S.selDistinct = baseDistinctOn
|
|
|
|
}
|
2020-06-08 15:13:01 +03:00
|
|
|
baseSelectAlias = S.Alias $ mkBaseTableAlias sourcePrefix
|
|
|
|
baseFromItem = S.mkSelFromItem baseSelect baseSelectAlias
|
2018-10-31 15:51:20 +03:00
|
|
|
|
|
|
|
-- function to create a joined from item from two from items
|
|
|
|
leftOuterJoin current new =
|
2021-09-24 01:56:37 +03:00
|
|
|
S.FIJoin $
|
|
|
|
S.JoinExpr current S.LeftOuter new $
|
|
|
|
S.JoinOn $ S.BELit True
|
2018-10-31 15:51:20 +03:00
|
|
|
|
|
|
|
-- this is the from eexp for the final select
|
|
|
|
joinedFrom :: S.FromItem
|
2021-09-24 01:56:37 +03:00
|
|
|
joinedFrom =
|
|
|
|
foldl' leftOuterJoin baseFromItem $
|
|
|
|
map objectRelationToFromItem (HM.toList objectRelations)
|
|
|
|
<> map arrayRelationToFromItem (HM.toList arrayRelations)
|
|
|
|
<> map arrayConnectionToFromItem (HM.toList arrayConnections)
|
|
|
|
<> map computedFieldToFromItem (HM.toList computedFields)
|
|
|
|
|
|
|
|
objectRelationToFromItem ::
|
|
|
|
(ObjectRelationSource, SelectNode) -> S.FromItem
|
2020-06-08 15:13:01 +03:00
|
|
|
objectRelationToFromItem (objectRelationSource, node) =
|
2020-06-25 06:33:37 +03:00
|
|
|
let ObjectRelationSource _ colMapping objectSelectSource = objectRelationSource
|
|
|
|
alias = S.Alias $ _ossPrefix objectSelectSource
|
|
|
|
source = objectSelectSourceToSelectSource objectSelectSource
|
2020-06-08 15:13:01 +03:00
|
|
|
select = generateSQLSelect (mkJoinCond baseSelectAlias colMapping) source node
|
2021-09-24 01:56:37 +03:00
|
|
|
in S.mkLateralFromItem select alias
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
arrayRelationToFromItem ::
|
|
|
|
(ArrayRelationSource, MultiRowSelectNode) -> S.FromItem
|
2020-06-08 15:13:01 +03:00
|
|
|
arrayRelationToFromItem (arrayRelationSource, arraySelectNode) =
|
|
|
|
let ArrayRelationSource _ colMapping source = arrayRelationSource
|
|
|
|
alias = S.Alias $ _ssPrefix source
|
2021-09-24 01:56:37 +03:00
|
|
|
select =
|
|
|
|
generateSQLSelectFromArrayNode source arraySelectNode $
|
|
|
|
mkJoinCond baseSelectAlias colMapping
|
|
|
|
in S.mkLateralFromItem select alias
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
arrayConnectionToFromItem ::
|
|
|
|
(ArrayConnectionSource, MultiRowSelectNode) -> S.FromItem
|
2020-06-08 15:13:01 +03:00
|
|
|
arrayConnectionToFromItem (arrayConnectionSource, arraySelectNode) =
|
|
|
|
let selectWith = connectionToSelectWith baseSelectAlias arrayConnectionSource arraySelectNode
|
|
|
|
alias = S.Alias $ _ssPrefix $ _acsSource arrayConnectionSource
|
2021-09-24 01:56:37 +03:00
|
|
|
in S.FISelectWith (S.Lateral True) selectWith alias
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
computedFieldToFromItem ::
|
|
|
|
(ComputedFieldTableSetSource, MultiRowSelectNode) -> S.FromItem
|
2020-06-08 15:13:01 +03:00
|
|
|
computedFieldToFromItem (computedFieldTableSource, node) =
|
2021-07-27 19:27:28 +03:00
|
|
|
let ComputedFieldTableSetSource _ source = computedFieldTableSource
|
|
|
|
internalSelect = generateSQLSelect (S.BELit True) source $ _mrsnSelectNode node
|
2020-06-08 15:13:01 +03:00
|
|
|
alias = S.Alias $ _ssPrefix source
|
2021-09-24 01:56:37 +03:00
|
|
|
select =
|
|
|
|
S.mkSelect
|
|
|
|
{ S.selExtr = _mrsnTopExtractors node,
|
|
|
|
S.selFrom = Just $ S.FromExp [S.mkSelFromItem internalSelect alias]
|
|
|
|
}
|
|
|
|
in S.mkLateralFromItem select alias
|
|
|
|
|
|
|
|
generateSQLSelectFromArrayNode ::
|
|
|
|
SelectSource ->
|
|
|
|
MultiRowSelectNode ->
|
|
|
|
S.BoolExp ->
|
|
|
|
S.Select
|
2020-06-08 15:13:01 +03:00
|
|
|
generateSQLSelectFromArrayNode selectSource arraySelectNode joinCondition =
|
|
|
|
S.mkSelect
|
2021-09-24 01:56:37 +03:00
|
|
|
{ S.selExtr = topExtractors,
|
|
|
|
S.selFrom = Just $ S.FromExp [selectFrom]
|
|
|
|
}
|
2020-06-08 15:13:01 +03:00
|
|
|
where
|
2021-07-27 19:27:28 +03:00
|
|
|
MultiRowSelectNode topExtractors selectNode = arraySelectNode
|
2021-09-24 01:56:37 +03:00
|
|
|
selectFrom =
|
|
|
|
S.mkSelFromItem
|
|
|
|
(generateSQLSelect joinCondition selectSource selectNode)
|
|
|
|
$ S.Alias $ _ssPrefix selectSource
|
|
|
|
|
|
|
|
mkAggregateSelect ::
|
|
|
|
forall pgKind.
|
|
|
|
( Backend ('Postgres pgKind),
|
|
|
|
PostgresAnnotatedFieldJSON pgKind
|
|
|
|
) =>
|
|
|
|
AnnAggregateSelect ('Postgres pgKind) ->
|
|
|
|
S.Select
|
2020-06-08 15:13:01 +03:00
|
|
|
mkAggregateSelect annAggSel =
|
|
|
|
let ((selectSource, nodeExtractors, topExtractor), joinTree) =
|
2021-09-24 01:56:37 +03:00
|
|
|
runWriter $
|
|
|
|
flip runReaderT strfyNum $
|
|
|
|
processAnnAggregateSelect sourcePrefixes rootFieldName annAggSel
|
2020-06-08 15:13:01 +03:00
|
|
|
selectNode = SelectNode nodeExtractors joinTree
|
2021-07-27 19:27:28 +03:00
|
|
|
arrayNode = MultiRowSelectNode [topExtractor] selectNode
|
2021-09-24 01:56:37 +03:00
|
|
|
in prefixNumToAliases $
|
|
|
|
generateSQLSelectFromArrayNode selectSource arrayNode $ S.BELit True
|
2018-12-12 15:58:39 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
strfyNum = _asnStrfyNum annAggSel
|
|
|
|
rootFieldName = FieldName "root"
|
2020-10-27 13:34:31 +03:00
|
|
|
rootIdentifier = toIdentifier rootFieldName
|
|
|
|
sourcePrefixes = SourcePrefixes rootIdentifier rootIdentifier
|
2018-12-12 15:58:39 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
mkSQLSelect ::
|
|
|
|
forall pgKind.
|
|
|
|
( Backend ('Postgres pgKind),
|
|
|
|
PostgresAnnotatedFieldJSON pgKind
|
|
|
|
) =>
|
|
|
|
JsonAggSelect ->
|
|
|
|
AnnSimpleSelect ('Postgres pgKind) ->
|
|
|
|
S.Select
|
2020-02-13 20:38:23 +03:00
|
|
|
mkSQLSelect jsonAggSelect annSel =
|
2020-06-08 15:13:01 +03:00
|
|
|
let permLimitSubQuery = PLSQNotRequired
|
|
|
|
((selectSource, nodeExtractors), joinTree) =
|
2021-09-24 01:56:37 +03:00
|
|
|
runWriter $
|
|
|
|
flip runReaderT strfyNum $
|
|
|
|
processAnnSimpleSelect sourcePrefixes rootFldName permLimitSubQuery annSel
|
2020-06-08 15:13:01 +03:00
|
|
|
selectNode = SelectNode nodeExtractors joinTree
|
2021-09-24 01:56:37 +03:00
|
|
|
topExtractor =
|
|
|
|
asJsonAggExtr jsonAggSelect rootFldAls permLimitSubQuery $
|
|
|
|
orderByForJsonAgg selectSource
|
2021-07-27 19:27:28 +03:00
|
|
|
arrayNode = MultiRowSelectNode [topExtractor] selectNode
|
2021-09-24 01:56:37 +03:00
|
|
|
in prefixNumToAliases $
|
|
|
|
generateSQLSelectFromArrayNode selectSource arrayNode $ S.BELit True
|
2018-12-12 15:58:39 +03:00
|
|
|
where
|
2020-06-08 15:13:01 +03:00
|
|
|
strfyNum = _asnStrfyNum annSel
|
2020-10-27 13:34:31 +03:00
|
|
|
rootFldIdentifier = toIdentifier rootFldName
|
|
|
|
sourcePrefixes = SourcePrefixes rootFldIdentifier rootFldIdentifier
|
2018-12-12 15:58:39 +03:00
|
|
|
rootFldName = FieldName "root"
|
2021-09-24 01:56:37 +03:00
|
|
|
rootFldAls = S.Alias $ toIdentifier rootFldName
|
|
|
|
|
|
|
|
mkConnectionSelect ::
|
|
|
|
forall pgKind.
|
|
|
|
( Backend ('Postgres pgKind),
|
|
|
|
PostgresAnnotatedFieldJSON pgKind
|
|
|
|
) =>
|
2021-12-07 16:12:02 +03:00
|
|
|
ConnectionSelect ('Postgres pgKind) Void S.SQLExp ->
|
2021-09-24 01:56:37 +03:00
|
|
|
S.SelectWithG S.Select
|
2020-06-08 15:13:01 +03:00
|
|
|
mkConnectionSelect connectionSelect =
|
|
|
|
let ((connectionSource, topExtractor, nodeExtractors), joinTree) =
|
2021-09-24 01:56:37 +03:00
|
|
|
runWriter $
|
|
|
|
flip runReaderT strfyNum $
|
|
|
|
processConnectionSelect
|
|
|
|
sourcePrefixes
|
|
|
|
rootFieldName
|
|
|
|
(S.Alias rootIdentifier)
|
|
|
|
mempty
|
|
|
|
connectionSelect
|
|
|
|
selectNode =
|
|
|
|
MultiRowSelectNode [topExtractor] $
|
|
|
|
SelectNode nodeExtractors joinTree
|
|
|
|
in prefixNumToAliasesSelectWith $
|
|
|
|
connectionToSelectWith (S.Alias rootIdentifier) connectionSource selectNode
|
2020-06-08 15:13:01 +03:00
|
|
|
where
|
|
|
|
strfyNum = _asnStrfyNum $ _csSelect connectionSelect
|
|
|
|
rootFieldName = FieldName "root"
|
2020-10-27 13:34:31 +03:00
|
|
|
rootIdentifier = toIdentifier rootFieldName
|
|
|
|
sourcePrefixes = SourcePrefixes rootIdentifier rootIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
-- | First element extractor expression from given record set
|
|
|
|
-- For example:- To get first "id" column from given row set,
|
|
|
|
-- the function generates the SQL expression AS `(array_agg("id"))[1]`
|
|
|
|
mkFirstElementExp :: S.SQLExp -> S.SQLExp
|
2020-10-27 13:34:31 +03:00
|
|
|
mkFirstElementExp expIdentifier =
|
2020-06-08 15:13:01 +03:00
|
|
|
-- For Example
|
2020-10-27 13:34:31 +03:00
|
|
|
S.SEArrayIndex (S.SEFnApp "array_agg" [expIdentifier] Nothing) (S.intToSQLExp 1)
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
-- | Last element extractor expression from given record set.
|
|
|
|
-- For example:- To get first "id" column from given row set,
|
|
|
|
-- the function generates the SQL expression AS `(array_agg("id"))[array_length(array_agg("id"), 1)]`
|
|
|
|
mkLastElementExp :: S.SQLExp -> S.SQLExp
|
2020-10-27 13:34:31 +03:00
|
|
|
mkLastElementExp expIdentifier =
|
|
|
|
let arrayExp = S.SEFnApp "array_agg" [expIdentifier] Nothing
|
2021-09-24 01:56:37 +03:00
|
|
|
in S.SEArrayIndex arrayExp $
|
|
|
|
S.SEFnApp "array_length" [arrayExp, S.intToSQLExp 1] Nothing
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
cursorIdentifier :: Identifier
|
|
|
|
cursorIdentifier = Identifier "__cursor"
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
startCursorIdentifier :: Identifier
|
|
|
|
startCursorIdentifier = Identifier "__start_cursor"
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
endCursorIdentifier :: Identifier
|
|
|
|
endCursorIdentifier = Identifier "__end_cursor"
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
hasPreviousPageIdentifier :: Identifier
|
|
|
|
hasPreviousPageIdentifier = Identifier "__has_previous_page"
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
hasNextPageIdentifier :: Identifier
|
|
|
|
hasNextPageIdentifier = Identifier "__has_next_page"
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
pageInfoSelectAliasIdentifier :: Identifier
|
|
|
|
pageInfoSelectAliasIdentifier = Identifier "__page_info"
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
cursorsSelectAliasIdentifier :: Identifier
|
|
|
|
cursorsSelectAliasIdentifier = Identifier "__cursors_select"
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
encodeBase64 :: S.SQLExp -> S.SQLExp
|
|
|
|
encodeBase64 =
|
|
|
|
removeNewline . bytesToBase64Text . convertToBytes
|
|
|
|
where
|
|
|
|
convertToBytes e =
|
|
|
|
S.SEFnApp "convert_to" [e, S.SELit "UTF8"] Nothing
|
|
|
|
bytesToBase64Text e =
|
|
|
|
S.SEFnApp "encode" [e, S.SELit "base64"] Nothing
|
|
|
|
removeNewline e =
|
|
|
|
S.SEFnApp "regexp_replace" [e, S.SELit "\\n", S.SELit "", S.SELit "g"] Nothing
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
processConnectionSelect ::
|
|
|
|
forall pgKind m.
|
2022-02-23 23:17:58 +03:00
|
|
|
( MonadReader StringifyNumbers m,
|
2021-09-24 01:56:37 +03:00
|
|
|
MonadWriter JoinTree m,
|
|
|
|
Backend ('Postgres pgKind),
|
|
|
|
PostgresAnnotatedFieldJSON pgKind
|
|
|
|
) =>
|
|
|
|
SourcePrefixes ->
|
|
|
|
FieldName ->
|
|
|
|
S.Alias ->
|
|
|
|
HM.HashMap PGCol PGCol ->
|
2021-12-07 16:12:02 +03:00
|
|
|
ConnectionSelect ('Postgres pgKind) Void S.SQLExp ->
|
2021-09-24 01:56:37 +03:00
|
|
|
m
|
|
|
|
( ArrayConnectionSource,
|
|
|
|
S.Extractor,
|
|
|
|
HM.HashMap S.Alias S.SQLExp
|
|
|
|
)
|
2020-06-08 15:13:01 +03:00
|
|
|
processConnectionSelect sourcePrefixes fieldAlias relAlias colMapping connectionSelect = do
|
|
|
|
(selectSource, orderByAndDistinctExtrs, maybeOrderByCursor) <-
|
2021-09-21 13:39:34 +03:00
|
|
|
processSelectParams
|
|
|
|
sourcePrefixes
|
|
|
|
fieldAlias
|
|
|
|
similarArrayFields
|
|
|
|
selectFrom
|
|
|
|
permLimitSubQuery
|
|
|
|
tablePermissions
|
|
|
|
tableArgs
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
let mkCursorExtractor = (S.Alias cursorIdentifier,) . (`S.SETyAnn` S.textTypeAnn)
|
2020-06-08 15:13:01 +03:00
|
|
|
cursorExtractors = case maybeOrderByCursor of
|
|
|
|
Just orderByCursor -> [mkCursorExtractor orderByCursor]
|
|
|
|
Nothing ->
|
|
|
|
-- Extract primary key columns from base select along with cursor expression.
|
|
|
|
-- Those columns are required to perform connection split via a WHERE clause.
|
2020-06-16 17:25:49 +03:00
|
|
|
mkCursorExtractor primaryKeyColumnsObjectExp : primaryKeyColumnExtractors
|
2021-09-21 13:39:34 +03:00
|
|
|
(topExtractorExp, exps) <- flip runStateT [] $ processFields selectSource
|
2020-10-27 13:34:31 +03:00
|
|
|
let topExtractor = S.Extractor topExtractorExp $ Just $ S.Alias fieldIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
allExtractors = HM.fromList $ cursorExtractors <> exps <> orderByAndDistinctExtrs
|
2021-09-24 01:56:37 +03:00
|
|
|
arrayConnectionSource =
|
|
|
|
ArrayConnectionSource
|
|
|
|
relAlias
|
|
|
|
colMapping
|
|
|
|
(mkSplitBoolExp <$> maybeSplit)
|
|
|
|
maybeSlice
|
|
|
|
selectSource
|
|
|
|
pure
|
|
|
|
( arrayConnectionSource,
|
|
|
|
topExtractor,
|
|
|
|
allExtractors
|
|
|
|
)
|
2020-06-08 15:13:01 +03:00
|
|
|
where
|
2021-02-03 19:17:20 +03:00
|
|
|
ConnectionSelect _ primaryKeyColumns maybeSplit maybeSlice select = connectionSelect
|
2020-06-08 15:13:01 +03:00
|
|
|
AnnSelectG fields selectFrom tablePermissions tableArgs _ = select
|
2020-10-27 13:34:31 +03:00
|
|
|
fieldIdentifier = toIdentifier fieldAlias
|
2020-06-08 15:13:01 +03:00
|
|
|
thisPrefix = _pfThis sourcePrefixes
|
|
|
|
permLimitSubQuery = PLSQNotRequired
|
|
|
|
|
2020-06-16 17:25:49 +03:00
|
|
|
primaryKeyColumnsObjectExp =
|
2021-09-24 01:56:37 +03:00
|
|
|
S.applyJsonBuildObj $
|
|
|
|
flip concatMap (toList primaryKeyColumns) $
|
|
|
|
\pgColumnInfo ->
|
2022-01-19 11:37:50 +03:00
|
|
|
[ S.SELit $ getPGColTxt $ ciColumn pgColumnInfo,
|
2022-02-23 23:17:58 +03:00
|
|
|
toJSONableExp LeaveNumbersAlone (ciType pgColumnInfo) False $
|
2022-01-19 11:37:50 +03:00
|
|
|
S.mkQIdenExp (mkBaseTableAlias thisPrefix) $ ciColumn pgColumnInfo
|
2021-09-24 01:56:37 +03:00
|
|
|
]
|
2020-06-16 17:25:49 +03:00
|
|
|
|
2020-06-08 15:13:01 +03:00
|
|
|
primaryKeyColumnExtractors =
|
|
|
|
flip map (toList primaryKeyColumns) $
|
2021-09-24 01:56:37 +03:00
|
|
|
\pgColumnInfo ->
|
2022-01-19 11:37:50 +03:00
|
|
|
let pgColumn = ciColumn pgColumnInfo
|
2021-09-24 01:56:37 +03:00
|
|
|
in ( S.Alias $ mkBaseTableColumnAlias thisPrefix pgColumn,
|
|
|
|
S.mkQIdenExp (mkBaseTableAlias thisPrefix) pgColumn
|
|
|
|
)
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
mkSplitBoolExp (firstSplit NE.:| rest) =
|
|
|
|
S.BEBin S.OrOp (mkSplitCompareExp firstSplit) $ mkBoolExpFromRest firstSplit rest
|
|
|
|
where
|
|
|
|
mkBoolExpFromRest previousSplit =
|
|
|
|
S.BEBin S.AndOp (mkEqualityCompareExp previousSplit) . \case
|
2021-09-24 01:56:37 +03:00
|
|
|
[] -> S.BELit False
|
|
|
|
(thisSplit : remainingSplit) -> mkSplitBoolExp (thisSplit NE.:| remainingSplit)
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
mkSplitCompareExp (ConnectionSplit kind v (OrderByItemG obTyM obCol _)) =
|
|
|
|
let obAlias = mkAnnOrderByAlias thisPrefix fieldAlias similarArrayFields obCol
|
2020-11-12 12:25:48 +03:00
|
|
|
obTy = fromMaybe S.OTAsc obTyM
|
2020-06-08 15:13:01 +03:00
|
|
|
compareOp = case (kind, obTy) of
|
2021-09-24 01:56:37 +03:00
|
|
|
(CSKAfter, S.OTAsc) -> S.SGT
|
|
|
|
(CSKAfter, S.OTDesc) -> S.SLT
|
|
|
|
(CSKBefore, S.OTAsc) -> S.SLT
|
2020-06-08 15:13:01 +03:00
|
|
|
(CSKBefore, S.OTDesc) -> S.SGT
|
2021-09-24 01:56:37 +03:00
|
|
|
in S.BECompare compareOp (S.SEIdentifier $ toIdentifier obAlias) v
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
mkEqualityCompareExp (ConnectionSplit _ v orderByItem) =
|
2021-09-24 01:56:37 +03:00
|
|
|
let obAlias =
|
|
|
|
mkAnnOrderByAlias thisPrefix fieldAlias similarArrayFields $
|
|
|
|
obiColumn orderByItem
|
|
|
|
in S.BECompare S.SEQ (S.SEIdentifier $ toIdentifier obAlias) v
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
similarArrayFields = HM.unions $
|
|
|
|
flip map (map snd fields) $ \case
|
2021-09-24 01:56:37 +03:00
|
|
|
ConnectionTypename {} -> mempty
|
|
|
|
ConnectionPageInfo {} -> mempty
|
|
|
|
ConnectionEdges edges -> HM.unions $
|
|
|
|
flip map (map snd edges) $ \case
|
|
|
|
EdgeTypename {} -> mempty
|
|
|
|
EdgeCursor {} -> mempty
|
|
|
|
EdgeNode annFields ->
|
|
|
|
mkSimilarArrayFields annFields $ _saOrderBy tableArgs
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
mkSimpleJsonAgg rowExp ob =
|
|
|
|
let jsonAggExp = S.SEFnApp "json_agg" [rowExp] ob
|
2021-09-24 01:56:37 +03:00
|
|
|
in S.SEFnApp "coalesce" [jsonAggExp, S.SELit "[]"] Nothing
|
|
|
|
|
|
|
|
processFields ::
|
|
|
|
forall n.
|
2022-02-23 23:17:58 +03:00
|
|
|
( MonadReader StringifyNumbers n,
|
2021-09-24 01:56:37 +03:00
|
|
|
MonadWriter JoinTree n,
|
|
|
|
MonadState [(S.Alias, S.SQLExp)] n
|
|
|
|
) =>
|
|
|
|
SelectSource ->
|
|
|
|
n S.SQLExp
|
2021-09-21 13:39:34 +03:00
|
|
|
processFields selectSource =
|
2020-06-08 15:13:01 +03:00
|
|
|
fmap (S.applyJsonBuildObj . concat) $
|
2021-09-24 01:56:37 +03:00
|
|
|
forM fields $
|
|
|
|
\(FieldName fieldText, field) ->
|
|
|
|
(S.SELit fieldText :) . pure
|
|
|
|
<$> case field of
|
|
|
|
ConnectionTypename t -> pure $ withForceAggregation S.textTypeAnn $ S.SELit t
|
|
|
|
ConnectionPageInfo pageInfoFields -> pure $ processPageInfoFields pageInfoFields
|
|
|
|
ConnectionEdges edges ->
|
|
|
|
fmap (flip mkSimpleJsonAgg (orderByForJsonAgg selectSource) . S.applyJsonBuildObj . concat) $
|
|
|
|
forM edges $
|
|
|
|
\(FieldName edgeText, edge) ->
|
|
|
|
(S.SELit edgeText :) . pure
|
|
|
|
<$> case edge of
|
|
|
|
EdgeTypename t -> pure $ S.SELit t
|
|
|
|
EdgeCursor -> pure $ encodeBase64 $ S.SEIdentifier (toIdentifier cursorIdentifier)
|
|
|
|
EdgeNode annFields -> do
|
|
|
|
let edgeFieldName =
|
|
|
|
FieldName $
|
|
|
|
getFieldNameTxt fieldAlias <> "." <> fieldText <> "." <> edgeText
|
|
|
|
edgeFieldIdentifier = toIdentifier edgeFieldName
|
|
|
|
annFieldsExtrExp <- processAnnFields thisPrefix edgeFieldName similarArrayFields annFields
|
|
|
|
modify' (<> [annFieldsExtrExp])
|
|
|
|
pure $ S.SEIdentifier edgeFieldIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
processPageInfoFields infoFields =
|
2021-09-24 01:56:37 +03:00
|
|
|
S.applyJsonBuildObj $
|
|
|
|
flip concatMap infoFields $
|
|
|
|
\(FieldName fieldText, field) -> (:) (S.SELit fieldText) $ pure case field of
|
|
|
|
PageInfoTypename t -> withForceAggregation S.textTypeAnn $ S.SELit t
|
|
|
|
PageInfoHasNextPage ->
|
|
|
|
withForceAggregation S.boolTypeAnn $
|
|
|
|
mkSingleFieldSelect (S.SEIdentifier hasNextPageIdentifier) pageInfoSelectAliasIdentifier
|
|
|
|
PageInfoHasPreviousPage ->
|
|
|
|
withForceAggregation S.boolTypeAnn $
|
|
|
|
mkSingleFieldSelect (S.SEIdentifier hasPreviousPageIdentifier) pageInfoSelectAliasIdentifier
|
|
|
|
PageInfoStartCursor ->
|
|
|
|
withForceAggregation S.textTypeAnn $
|
|
|
|
encodeBase64 $ mkSingleFieldSelect (S.SEIdentifier startCursorIdentifier) cursorsSelectAliasIdentifier
|
|
|
|
PageInfoEndCursor ->
|
|
|
|
withForceAggregation S.textTypeAnn $
|
|
|
|
encodeBase64 $ mkSingleFieldSelect (S.SEIdentifier endCursorIdentifier) cursorsSelectAliasIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
where
|
2021-09-24 01:56:37 +03:00
|
|
|
mkSingleFieldSelect field fromIdentifier =
|
|
|
|
S.SESelect
|
|
|
|
S.mkSelect
|
|
|
|
{ S.selExtr = [S.Extractor field Nothing],
|
|
|
|
S.selFrom = Just $ S.FromExp [S.FIIdentifier fromIdentifier]
|
|
|
|
}
|
|
|
|
|
|
|
|
connectionToSelectWith ::
|
|
|
|
S.Alias ->
|
|
|
|
ArrayConnectionSource ->
|
|
|
|
MultiRowSelectNode ->
|
|
|
|
S.SelectWithG S.Select
|
2020-06-08 15:13:01 +03:00
|
|
|
connectionToSelectWith baseSelectAlias arrayConnectionSource arraySelectNode =
|
2021-09-24 01:56:37 +03:00
|
|
|
let extractionSelect =
|
|
|
|
S.mkSelect
|
|
|
|
{ S.selExtr = topExtractors,
|
|
|
|
S.selFrom = Just $ S.FromExp [S.FIIdentifier finalSelectIdentifier]
|
|
|
|
}
|
|
|
|
in S.SelectWith fromBaseSelections extractionSelect
|
2020-06-08 15:13:01 +03:00
|
|
|
where
|
|
|
|
ArrayConnectionSource _ columnMapping maybeSplit maybeSlice selectSource =
|
|
|
|
arrayConnectionSource
|
2021-07-27 19:27:28 +03:00
|
|
|
MultiRowSelectNode topExtractors selectNode = arraySelectNode
|
2020-10-27 13:34:31 +03:00
|
|
|
baseSelectIdentifier = Identifier "__base_select"
|
|
|
|
splitSelectIdentifier = Identifier "__split_select"
|
|
|
|
sliceSelectIdentifier = Identifier "__slice_select"
|
|
|
|
finalSelectIdentifier = Identifier "__final_select"
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
rowNumberIdentifier = Identifier "__row_number"
|
2020-06-08 15:13:01 +03:00
|
|
|
rowNumberExp = S.SEUnsafe "(row_number() over (partition by 1))"
|
2020-10-27 13:34:31 +03:00
|
|
|
startRowNumberIdentifier = Identifier "__start_row_number"
|
|
|
|
endRowNumberIdentifier = Identifier "__end_row_number"
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
startCursorExp = mkFirstElementExp $ S.SEIdentifier cursorIdentifier
|
|
|
|
endCursorExp = mkLastElementExp $ S.SEIdentifier cursorIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
startRowNumberExp = mkFirstElementExp $ S.SEIdentifier rowNumberIdentifier
|
|
|
|
endRowNumberExp = mkLastElementExp $ S.SEIdentifier rowNumberIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
fromBaseSelections =
|
|
|
|
let joinCond = mkJoinCond baseSelectAlias columnMapping
|
2021-09-24 01:56:37 +03:00
|
|
|
baseSelectFrom =
|
|
|
|
S.mkSelFromItem
|
|
|
|
(generateSQLSelect joinCond selectSource selectNode)
|
|
|
|
$ S.Alias $ _ssPrefix selectSource
|
2020-06-08 15:13:01 +03:00
|
|
|
select =
|
2021-09-24 01:56:37 +03:00
|
|
|
S.mkSelect
|
|
|
|
{ S.selExtr =
|
|
|
|
[ S.selectStar,
|
|
|
|
S.Extractor rowNumberExp $ Just $ S.Alias rowNumberIdentifier
|
|
|
|
],
|
|
|
|
S.selFrom = Just $ S.FromExp [baseSelectFrom]
|
|
|
|
}
|
|
|
|
in (S.Alias baseSelectIdentifier, select) : fromSplitSelection
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2020-10-27 13:34:31 +03:00
|
|
|
mkStarSelect fromIdentifier =
|
2021-09-24 01:56:37 +03:00
|
|
|
S.mkSelect
|
|
|
|
{ S.selExtr = [S.selectStar],
|
|
|
|
S.selFrom = Just $ S.FromExp [S.FIIdentifier fromIdentifier]
|
|
|
|
}
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
fromSplitSelection = case maybeSplit of
|
2021-09-24 01:56:37 +03:00
|
|
|
Nothing -> fromSliceSelection baseSelectIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
Just splitBool ->
|
|
|
|
let select =
|
2021-09-24 01:56:37 +03:00
|
|
|
(mkStarSelect baseSelectIdentifier) {S.selWhere = Just $ S.WhereFrag splitBool}
|
|
|
|
in (S.Alias splitSelectIdentifier, select) : fromSliceSelection splitSelectIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
fromSliceSelection prevSelect = case maybeSlice of
|
2021-09-24 01:56:37 +03:00
|
|
|
Nothing -> fromFinalSelect prevSelect
|
2020-06-08 15:13:01 +03:00
|
|
|
Just slice ->
|
|
|
|
let select = case slice of
|
|
|
|
SliceFirst limit ->
|
|
|
|
(mkStarSelect prevSelect)
|
2021-09-24 01:56:37 +03:00
|
|
|
{ S.selLimit = (Just . S.LimitExp . S.intToSQLExp) limit
|
|
|
|
}
|
2020-06-08 15:13:01 +03:00
|
|
|
SliceLast limit ->
|
|
|
|
let mkRowNumberOrderBy obType =
|
|
|
|
let orderByItem =
|
2020-10-27 13:34:31 +03:00
|
|
|
S.OrderByItem (S.SEIdentifier rowNumberIdentifier) (Just obType) Nothing
|
2021-09-24 01:56:37 +03:00
|
|
|
in S.OrderByExp $ orderByItem NE.:| []
|
2020-06-08 15:13:01 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
sliceLastSelect =
|
|
|
|
(mkStarSelect prevSelect)
|
|
|
|
{ S.selLimit = (Just . S.LimitExp . S.intToSQLExp) limit,
|
|
|
|
S.selOrderBy = Just $ mkRowNumberOrderBy S.OTDesc
|
|
|
|
}
|
2020-06-08 15:13:01 +03:00
|
|
|
sliceLastSelectFrom =
|
2020-10-27 13:34:31 +03:00
|
|
|
S.mkSelFromItem sliceLastSelect $ S.Alias sliceSelectIdentifier
|
2021-09-24 01:56:37 +03:00
|
|
|
in S.mkSelect
|
|
|
|
{ S.selExtr = [S.selectStar],
|
|
|
|
S.selFrom = Just $ S.FromExp [sliceLastSelectFrom],
|
|
|
|
S.selOrderBy = Just $ mkRowNumberOrderBy S.OTAsc
|
|
|
|
}
|
|
|
|
in (S.Alias sliceSelectIdentifier, select) : fromFinalSelect sliceSelectIdentifier
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
fromFinalSelect prevSelect =
|
|
|
|
let select = mkStarSelect prevSelect
|
2021-09-24 01:56:37 +03:00
|
|
|
in (S.Alias finalSelectIdentifier, select) : fromCursorSelection
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
fromCursorSelection =
|
2021-09-24 01:56:37 +03:00
|
|
|
let extrs =
|
|
|
|
[ S.Extractor startCursorExp $ Just $ S.Alias startCursorIdentifier,
|
|
|
|
S.Extractor endCursorExp $ Just $ S.Alias endCursorIdentifier,
|
|
|
|
S.Extractor startRowNumberExp $ Just $ S.Alias startRowNumberIdentifier,
|
|
|
|
S.Extractor endRowNumberExp $ Just $ S.Alias endRowNumberIdentifier
|
|
|
|
]
|
2020-06-08 15:13:01 +03:00
|
|
|
select =
|
2021-09-24 01:56:37 +03:00
|
|
|
S.mkSelect
|
|
|
|
{ S.selExtr = extrs,
|
|
|
|
S.selFrom = Just $ S.FromExp [S.FIIdentifier finalSelectIdentifier]
|
|
|
|
}
|
|
|
|
in (S.Alias cursorsSelectAliasIdentifier, select) : fromPageInfoSelection
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
fromPageInfoSelection =
|
2021-09-24 01:56:37 +03:00
|
|
|
let hasPrevPage =
|
|
|
|
S.SEBool $
|
|
|
|
S.mkExists (S.FIIdentifier baseSelectIdentifier) $
|
|
|
|
S.BECompare S.SLT (S.SEIdentifier rowNumberIdentifier) $
|
|
|
|
S.SESelect $
|
|
|
|
S.mkSelect
|
|
|
|
{ S.selFrom = Just $ S.FromExp [S.FIIdentifier cursorsSelectAliasIdentifier],
|
|
|
|
S.selExtr = [S.Extractor (S.SEIdentifier startRowNumberIdentifier) Nothing]
|
|
|
|
}
|
|
|
|
hasNextPage =
|
|
|
|
S.SEBool $
|
|
|
|
S.mkExists (S.FIIdentifier baseSelectIdentifier) $
|
|
|
|
S.BECompare S.SGT (S.SEIdentifier rowNumberIdentifier) $
|
|
|
|
S.SESelect $
|
|
|
|
S.mkSelect
|
|
|
|
{ S.selFrom = Just $ S.FromExp [S.FIIdentifier cursorsSelectAliasIdentifier],
|
|
|
|
S.selExtr = [S.Extractor (S.SEIdentifier endRowNumberIdentifier) Nothing]
|
|
|
|
}
|
2020-06-08 15:13:01 +03:00
|
|
|
|
|
|
|
select =
|
2021-09-24 01:56:37 +03:00
|
|
|
S.mkSelect
|
|
|
|
{ S.selExtr =
|
|
|
|
[ S.Extractor hasPrevPage $ Just $ S.Alias hasPreviousPageIdentifier,
|
|
|
|
S.Extractor hasNextPage $ Just $ S.Alias hasNextPageIdentifier
|
|
|
|
]
|
|
|
|
}
|
|
|
|
in pure (S.Alias pageInfoSelectAliasIdentifier, select)
|