Split up module Hasura.Backend.Postgres.Translate.Select into sub-modules

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4334
GitOrigin-RevId: d083512f3c4e534a10e571eeab10308ad45cc7a0
This commit is contained in:
Philip Lykke Carlsen 2022-04-22 15:38:35 +02:00 committed by hasura-bot
parent 4f9a08239d
commit 9a557ceeee
17 changed files with 2175 additions and 1811 deletions

View File

@ -506,6 +506,18 @@ library
, Hasura.Backends.Postgres.Translate.Mutation
, Hasura.Backends.Postgres.Translate.Returning
, Hasura.Backends.Postgres.Translate.Select
, Hasura.Backends.Postgres.Translate.Select.Aggregate
, Hasura.Backends.Postgres.Translate.Select.Aliases
, Hasura.Backends.Postgres.Translate.Select.AnnotatedFieldJSON
, Hasura.Backends.Postgres.Translate.Select.Aux1
, Hasura.Backends.Postgres.Translate.Select.Connection
, Hasura.Backends.Postgres.Translate.Select.Extractor
, Hasura.Backends.Postgres.Translate.Select.GenerateSelect
, Hasura.Backends.Postgres.Translate.Select.JoinTree
, Hasura.Backends.Postgres.Translate.Select.OrderBy
, Hasura.Backends.Postgres.Translate.Select.Process
, Hasura.Backends.Postgres.Translate.Select.Query
, Hasura.Backends.Postgres.Translate.Select.Streaming
, Hasura.Backends.Postgres.Translate.Types
, Hasura.Backends.Postgres.Translate.Update
, Hasura.Backends.Postgres.Types.BoolExp

View File

@ -41,7 +41,7 @@ import Hasura.Backends.Postgres.Translate.Select (PostgresAnnotatedFieldJSON)
import Hasura.Backends.Postgres.Translate.Select qualified as DS
import Hasura.Backends.Postgres.Types.Update
import Hasura.Base.Error (QErr)
import Hasura.EncJSON (EncJSON, encJFromJValue)
import Hasura.EncJSON (EncJSON, encJFromBS, encJFromJValue)
import Hasura.GraphQL.Execute.Backend
( BackendExecute (..),
DBStepInfo (..),
@ -393,7 +393,18 @@ mkCurPlanTx userInfo ps@(PreparedSql q prepMap) =
-- WARNING: this quietly assumes the intmap keys are contiguous
prepArgs = fst <$> IntMap.elems args
in (,Just ps) $
Tracing.trace "Postgres" $ liftTx $ DS.asSingleRowJsonResp q prepArgs
Tracing.trace "Postgres" $ liftTx $ asSingleRowJsonResp q prepArgs
-- | This function is generally used on the result of 'selectQuerySQL',
-- 'selectAggregateQuerySQL' or 'connectionSelectSQL' to run said query and get
-- back the resulting JSON.
asSingleRowJsonResp ::
Q.Query ->
[Q.PrepArg] ->
Q.TxE QErr EncJSON
asSingleRowJsonResp query args =
encJFromBS . runIdentity . Q.getRow
<$> Q.rawQE dmlTxErrorHandler query args True
-- convert a query from an intermediate representation to... another
irToRootFieldPlan ::

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,68 @@
-- | This module defines the top-level translation functions pertaining to
-- queries that use aggregation (i.e., /not/ so-called "simple" selects) into
-- Postgres AST.
module Hasura.Backends.Postgres.Translate.Select.Aggregate
( mkAggregateSelect,
selectAggregateQuerySQL,
)
where
import Control.Monad.Writer.Strict (runWriter)
import Database.PG.Query (Query, fromBuilder)
import Hasura.Backends.Postgres.SQL.DML (BoolExp (BELit), Select)
import Hasura.Backends.Postgres.SQL.IdentifierUniqueness
( prefixNumToAliases,
)
import Hasura.Backends.Postgres.SQL.Types
( IsIdentifier (toIdentifier),
)
import Hasura.Backends.Postgres.Translate.Select.AnnotatedFieldJSON
import Hasura.Backends.Postgres.Translate.Select.GenerateSelect (generateSQLSelectFromArrayNode)
import Hasura.Backends.Postgres.Translate.Select.Process (processAnnAggregateSelect)
import Hasura.Backends.Postgres.Translate.Types
( MultiRowSelectNode (MultiRowSelectNode),
SelectNode (SelectNode),
SourcePrefixes (SourcePrefixes),
)
import Hasura.Prelude
import Hasura.RQL.IR.Select
( AnnAggregateSelect,
AnnSelectG (_asnStrfyNum),
)
import Hasura.RQL.Types.Backend (Backend)
import Hasura.RQL.Types.Common (FieldName (FieldName))
import Hasura.SQL.Backend (BackendType (Postgres))
import Hasura.SQL.Types (ToSQL (toSQL))
-- | Translates IR to Postgres queries for aggregated SELECTs.
--
-- See 'mkAggregateSelect' for the Postgres AST.
selectAggregateQuerySQL ::
forall pgKind.
(Backend ('Postgres pgKind), PostgresAnnotatedFieldJSON pgKind) =>
AnnAggregateSelect ('Postgres pgKind) ->
Query
selectAggregateQuerySQL =
fromBuilder . toSQL . mkAggregateSelect
mkAggregateSelect ::
forall pgKind.
( Backend ('Postgres pgKind),
PostgresAnnotatedFieldJSON pgKind
) =>
AnnAggregateSelect ('Postgres pgKind) ->
Select
mkAggregateSelect annAggSel =
let ((selectSource, nodeExtractors, topExtractor), joinTree) =
runWriter $
flip runReaderT strfyNum $
processAnnAggregateSelect sourcePrefixes rootFieldName annAggSel
selectNode = SelectNode nodeExtractors joinTree
arrayNode = MultiRowSelectNode [topExtractor] selectNode
in prefixNumToAliases $
generateSQLSelectFromArrayNode selectSource arrayNode $ BELit True
where
strfyNum = _asnStrfyNum annAggSel
rootFieldName = FieldName "root"
rootIdentifier = toIdentifier rootFieldName
sourcePrefixes = SourcePrefixes rootIdentifier rootIdentifier

View File

@ -0,0 +1,107 @@
{-# OPTIONS_GHC -Wno-missing-export-lists #-}
-- | Stuff gutted from Translate.Select
module Hasura.Backends.Postgres.Translate.Select.Aliases where
import Control.Monad.Writer.Strict
import Data.HashMap.Strict qualified as HM
import Data.Text qualified as T
import Data.Text.Extended
import Hasura.Backends.Postgres.SQL.DML qualified as S
import Hasura.Backends.Postgres.SQL.Types
import Hasura.Backends.Postgres.Translate.Types
import Hasura.Prelude
import Hasura.RQL.IR.Select
import Hasura.RQL.Types
-- | Generate alias for order by extractors
mkAnnOrderByAlias ::
Identifier -> FieldName -> SimilarArrayFields -> AnnotatedOrderByElement ('Postgres pgKind) v -> S.Alias
mkAnnOrderByAlias pfx parAls similarFields = \case
AOCColumn pgColumnInfo ->
let pgColumn = ciColumn pgColumnInfo
obColAls = mkBaseTableColumnAlias pfx pgColumn
in S.Alias obColAls
-- "pfx.or.relname"."pfx.ob.or.relname.rest" AS "pfx.ob.or.relname.rest"
AOCObjectRelation relInfo _ rest ->
let rn = riName relInfo
relPfx = mkObjectRelationTableAlias pfx rn
ordByFldName = mkOrderByFieldName rn
nesAls = mkAnnOrderByAlias relPfx ordByFldName mempty rest
in nesAls
AOCArrayAggregation relInfo _ aggOrderBy ->
let rn = riName relInfo
arrPfx =
mkArrayRelationSourcePrefix pfx parAls similarFields $
mkOrderByFieldName rn
obAls = arrPfx <> Identifier "." <> toIdentifier (mkAggregateOrderByAlias aggOrderBy)
in S.Alias obAls
AOCComputedField cfOrderBy ->
let fieldName = fromComputedField $ _cfobName cfOrderBy
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
-- array relationships are not grouped, so have to be prefixed by
-- parent's alias
mkUniqArrayRelationAlias :: FieldName -> [FieldName] -> Identifier
mkUniqArrayRelationAlias parAls flds =
let sortedFields = sort flds
in Identifier $
getFieldNameTxt parAls <> "."
<> T.intercalate "." (map getFieldNameTxt sortedFields)
mkArrayRelationTableAlias :: Identifier -> FieldName -> [FieldName] -> Identifier
mkArrayRelationTableAlias pfx parAls flds =
pfx <> Identifier ".ar." <> uniqArrRelAls
where
uniqArrRelAls = mkUniqArrayRelationAlias parAls flds
mkObjectRelationTableAlias :: Identifier -> RelName -> Identifier
mkObjectRelationTableAlias pfx relName =
pfx <> Identifier ".or." <> toIdentifier relName
mkComputedFieldTableAlias :: Identifier -> FieldName -> Identifier
mkComputedFieldTableAlias pfx fldAls =
pfx <> Identifier ".cf." <> toIdentifier fldAls
mkBaseTableAlias :: Identifier -> Identifier
mkBaseTableAlias pfx =
pfx <> Identifier ".base"
mkBaseTableColumnAlias :: Identifier -> PGCol -> Identifier
mkBaseTableColumnAlias pfx pgColumn =
pfx <> Identifier ".pg." <> toIdentifier pgColumn
mkAggregateOrderByAlias :: AnnotatedAggregateOrderBy ('Postgres pgKind) -> S.Alias
mkAggregateOrderByAlias =
(S.Alias . Identifier) . \case
AAOCount -> "count"
AAOOp opText col -> opText <> "." <> getPGColTxt (ciColumn col)
mkOrderByFieldName :: ToTxt a => a -> FieldName
mkOrderByFieldName name =
FieldName $ toTxt name <> "." <> "order_by"
mkArrayRelationSourcePrefix ::
Identifier ->
FieldName ->
HM.HashMap FieldName [FieldName] ->
FieldName ->
Identifier
mkArrayRelationSourcePrefix parentSourcePrefix parentFieldName similarFieldsMap fieldName =
mkArrayRelationTableAlias parentSourcePrefix parentFieldName $
HM.lookupDefault [fieldName] fieldName similarFieldsMap
mkArrayRelationAlias ::
FieldName ->
HM.HashMap FieldName [FieldName] ->
FieldName ->
S.Alias
mkArrayRelationAlias parentFieldName similarFieldsMap fieldName =
S.Alias $
mkUniqArrayRelationAlias parentFieldName $
HM.lookupDefault [fieldName] fieldName similarFieldsMap

View File

@ -0,0 +1,49 @@
-- | This module defines the type class 'PostgresAnnotatedFieldJSON'.
module Hasura.Backends.Postgres.Translate.Select.AnnotatedFieldJSON
( PostgresAnnotatedFieldJSON (..),
)
where
import Data.Text qualified as T
import Hasura.Backends.Postgres.SQL.DML qualified as S
import Hasura.Backends.Postgres.Translate.Select.Aux1 (withJsonBuildObj)
import Hasura.Prelude
import Hasura.RQL.Types.Common (FieldName (getFieldNameTxt))
import Hasura.SQL.Backend (PostgresKind (..))
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
if any ((> 63) . T.length . getFieldNameTxt . fst) fieldExps
then withJsonBuildObj fieldAlias $ concatMap toJsonBuildObjectExps fieldExps
else withRowToJSON fieldAlias $ map toRowToJsonExtr fieldExps
where
toJsonBuildObjectExps (fieldName, fieldExp) =
[S.SELit $ getFieldNameTxt fieldName, fieldExp]
toRowToJsonExtr (fieldName, fieldExp) =
S.Extractor fieldExp $ Just $ S.toAlias fieldName
-- uses row_to_json to build a json object
withRowToJSON ::
FieldName -> [S.Extractor] -> (S.Alias, S.SQLExp)
withRowToJSON parAls extrs =
(S.toAlias parAls, jsonRow)
where
jsonRow = S.applyRowToJson extrs
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]

View File

@ -0,0 +1,103 @@
{-# OPTIONS_GHC -Wno-missing-export-lists #-}
-- | Stuff gutted from Translate.Select.
module Hasura.Backends.Postgres.Translate.Select.Aux1 where
import Hasura.Backends.Postgres.SQL.DML qualified as S
import Hasura.Backends.Postgres.SQL.Types (Identifier (..), QualifiedFunction, qualifiedObjectToText, toIdentifier)
import Hasura.Backends.Postgres.Translate.Select.Aliases
import Hasura.Prelude
import Hasura.RQL.IR
import Hasura.RQL.Types (BackendType (Postgres))
import Hasura.RQL.Types.Common (FieldName)
-- | 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
mkFirstElementExp expIdentifier =
-- For Example
S.SEArrayIndex (S.SEFnApp "array_agg" [expIdentifier] Nothing) (S.intToSQLExp 1)
-- | 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
mkLastElementExp expIdentifier =
let arrayExp = S.SEFnApp "array_agg" [expIdentifier] Nothing
in S.SEArrayIndex arrayExp $
S.SEFnApp "array_length" [arrayExp, S.intToSQLExp 1] Nothing
cursorIdentifier :: Identifier
cursorIdentifier = Identifier "__cursor"
startCursorIdentifier :: Identifier
startCursorIdentifier = Identifier "__start_cursor"
endCursorIdentifier :: Identifier
endCursorIdentifier = Identifier "__end_cursor"
hasPreviousPageIdentifier :: Identifier
hasPreviousPageIdentifier = Identifier "__has_previous_page"
hasNextPageIdentifier :: Identifier
hasNextPageIdentifier = Identifier "__has_next_page"
pageInfoSelectAliasIdentifier :: Identifier
pageInfoSelectAliasIdentifier = Identifier "__page_info"
cursorsSelectAliasIdentifier :: Identifier
cursorsSelectAliasIdentifier = Identifier "__cursors_select"
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
fromTableRowArgs ::
Identifier -> FunctionArgsExpTableRow S.SQLExp -> S.FunctionArgs
fromTableRowArgs prefix = toFunctionArgs . fmap toSQLExp
where
toFunctionArgs (FunctionArgsExp positional named) =
S.FunctionArgs positional named
toSQLExp =
onArgumentExp
(S.SERowIdentifier alias)
(S.mkQIdenExp alias . Identifier)
alias = mkBaseTableAlias prefix
selectFromToFromItem :: Identifier -> SelectFrom ('Postgres pgKind) -> S.FromItem
selectFromToFromItem pfx = \case
FromTable tn -> S.FISimple tn Nothing
FromIdentifier i -> S.FIIdentifier $ toIdentifier i
FromFunction qf args defListM ->
S.FIFunc $
S.FunctionExp qf (fromTableRowArgs pfx args) $
Just $ S.mkFunctionAlias (functionToIdentifier qf) defListM
-- | Converts a function name to an 'Identifier'.
--
-- If the schema name is public, it will just use its name, otherwise it will
-- prefix it by the schema name.
functionToIdentifier :: QualifiedFunction -> Identifier
functionToIdentifier = Identifier . qualifiedObjectToText
-- uses json_build_object to build a json object
withJsonBuildObj ::
FieldName -> [S.SQLExp] -> (S.Alias, S.SQLExp)
withJsonBuildObj parAls exps =
(S.toAlias parAls, jsonRow)
where
jsonRow = S.applyJsonBuildObj exps
-- | 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

View File

@ -0,0 +1,67 @@
-- | This module defines the top-level translation functions pertaining to
-- translating Connection (i.e. Relay) queries into Postgres AST.
module Hasura.Backends.Postgres.Translate.Select.Connection
( connectionSelectQuerySQL,
mkConnectionSelect,
)
where
import Control.Monad.Writer (runWriter)
import Database.PG.Query (Query, fromBuilder)
import Hasura.Backends.Postgres.SQL.DML qualified as S
import Hasura.Backends.Postgres.SQL.IdentifierUniqueness (prefixNumToAliasesSelectWith)
import Hasura.Backends.Postgres.SQL.Types
import Hasura.Backends.Postgres.Translate.Select.AnnotatedFieldJSON
import Hasura.Backends.Postgres.Translate.Select.GenerateSelect (connectionToSelectWith)
import Hasura.Backends.Postgres.Translate.Select.Process (processConnectionSelect)
import Hasura.Backends.Postgres.Translate.Types
import Hasura.Prelude
import Hasura.RQL.IR.Select
( AnnSelectG (_asnStrfyNum),
ConnectionSelect (_csSelect),
)
import Hasura.RQL.Types.Backend (Backend)
import Hasura.RQL.Types.Common (FieldName (FieldName))
import Hasura.SQL.Backend (BackendType (Postgres))
import Hasura.SQL.Types (ToSQL (toSQL))
-- | Translates IR to Postgres queries for "connection" queries (used for Relay).
--
-- See 'mkConnectionSelect' for the Postgres AST.
connectionSelectQuerySQL ::
forall pgKind.
( Backend ('Postgres pgKind),
PostgresAnnotatedFieldJSON pgKind
) =>
ConnectionSelect ('Postgres pgKind) Void S.SQLExp ->
Query
connectionSelectQuerySQL =
fromBuilder . toSQL . mkConnectionSelect
mkConnectionSelect ::
forall pgKind.
( Backend ('Postgres pgKind),
PostgresAnnotatedFieldJSON pgKind
) =>
ConnectionSelect ('Postgres pgKind) Void S.SQLExp ->
S.SelectWithG S.Select
mkConnectionSelect connectionSelect =
let ((connectionSource, topExtractor, nodeExtractors), joinTree) =
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
where
strfyNum = _asnStrfyNum $ _csSelect connectionSelect
rootFieldName = FieldName "root"
rootIdentifier = toIdentifier rootFieldName
sourcePrefixes = SourcePrefixes rootIdentifier rootIdentifier

View File

@ -0,0 +1,146 @@
{-# OPTIONS_GHC -Wno-missing-export-lists #-}
-- | Stuff gutted from Translate.Select
module Hasura.Backends.Postgres.Translate.Select.Extractor where
import Control.Monad.Writer.Strict
import Data.List.NonEmpty qualified as NE
import Hasura.Backends.Postgres.SQL.DML qualified as S
import Hasura.Backends.Postgres.SQL.Types
import Hasura.Backends.Postgres.Translate.Select.Aliases
import Hasura.Backends.Postgres.Translate.Types (PermissionLimitSubQuery (..))
import Hasura.Prelude
import Hasura.RQL.IR.Select
import Hasura.RQL.Types
aggregateFieldsToExtractorExps ::
Identifier -> AggregateFields ('Postgres pgKind) -> [(S.Alias, S.SQLExp)]
aggregateFieldsToExtractorExps sourcePrefix aggregateFields =
flip concatMap aggregateFields $ \(_, field) ->
case field of
AFCount cty -> case cty of
S.CTStar -> []
S.CTSimple cols -> colsToExps cols
S.CTDistinct cols -> colsToExps cols
AFOp aggOp -> aggOpToExps aggOp
AFExp _ -> []
where
colsToExps = fmap mkColExp
aggOpToExps = mapMaybe colToMaybeExp . _aoFields
colToMaybeExp = \case
(_, CFCol col _) -> Just $ mkColExp col
_ -> Nothing
mkColExp c =
let qualCol = S.mkQIdenExp (mkBaseTableAlias sourcePrefix) (toIdentifier c)
colAls = toIdentifier c
in (S.Alias colAls, qualCol)
mkAggregateOrderByExtractorAndFields ::
forall pgKind.
Backend ('Postgres pgKind) =>
AnnotatedAggregateOrderBy ('Postgres pgKind) ->
(S.Extractor, AggregateFields ('Postgres pgKind))
mkAggregateOrderByExtractorAndFields annAggOrderBy =
case annAggOrderBy of
AAOCount ->
( S.Extractor S.countStar alias,
[(FieldName "count", AFCount S.CTStar)]
)
AAOOp opText pgColumnInfo ->
let pgColumn = ciColumn pgColumnInfo
pgType = ciType pgColumnInfo
in ( S.Extractor (S.SEFnApp opText [S.SEIdentifier $ toIdentifier pgColumn] Nothing) alias,
[ ( FieldName opText,
AFOp $
AggregateOp
opText
[ ( fromCol @('Postgres pgKind) pgColumn,
CFCol pgColumn pgType
)
]
)
]
)
where
alias = Just $ mkAggregateOrderByAlias annAggOrderBy
withJsonAggExtr ::
PermissionLimitSubQuery -> Maybe S.OrderByExp -> S.Alias -> S.SQLExp
withJsonAggExtr permLimitSubQuery ordBy alias =
-- if select has aggregations then use subquery to apply permission limit
case permLimitSubQuery of
PLSQRequired permLimit -> withPermLimit permLimit
PLSQNotRequired -> simpleJsonAgg
where
simpleJsonAgg = mkSimpleJsonAgg rowIdenExp ordBy
rowIdenExp = S.SEIdentifier $ S.getAlias alias
subSelAls = Identifier "sub_query"
unnestTable = Identifier "unnest_table"
mkSimpleJsonAgg rowExp ob =
let jsonAggExp = S.SEFnApp "json_agg" [rowExp] ob
in S.SEFnApp "coalesce" [jsonAggExp, S.SELit "[]"] Nothing
withPermLimit limit =
let subSelect = mkSubSelect limit
rowIdentifier = S.mkQIdenExp subSelAls alias
extr = S.Extractor (mkSimpleJsonAgg rowIdentifier newOrderBy) Nothing
fromExp =
S.FromExp $
pure $
S.mkSelFromItem subSelect $ S.Alias subSelAls
in S.SESelect $
S.mkSelect
{ S.selExtr = pure extr,
S.selFrom = Just fromExp
}
mkSubSelect limit =
let jsonRowExtr =
flip S.Extractor (Just alias) $
S.mkQIdenExp unnestTable alias
obExtrs = flip map newOBAliases $ \a ->
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
}
unnestFromItem =
let arrayAggItems = flip map (rowIdenExp : obCols) $
\s -> S.SEFnApp "array_agg" [s] Nothing
in S.FIUnnest arrayAggItems (S.Alias unnestTable) $
rowIdenExp : map S.SEIdentifier newOBAliases
newOrderBy = S.OrderByExp <$> NE.nonEmpty newOBItems
(newOBItems, obCols, newOBAliases) = maybe ([], [], []) transformOrderBy ordBy
transformOrderBy (S.OrderByExp l) = unzip3 $
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
)
asSingleRowExtr :: S.Alias -> S.SQLExp
asSingleRowExtr col =
S.SEFnApp "coalesce" [jsonAgg, S.SELit "null"] Nothing
where
jsonAgg =
S.SEOpApp
(S.SQLOp "->")
[ S.SEFnApp "json_agg" [S.SEIdentifier $ toIdentifier col] Nothing,
S.SEUnsafe "0"
]
asJsonAggExtr ::
JsonAggSelect -> S.Alias -> PermissionLimitSubQuery -> Maybe S.OrderByExp -> S.Extractor
asJsonAggExtr jsonAggSelect als permLimitSubQuery ordByExpM =
flip S.Extractor (Just als) $ case jsonAggSelect of
JASMultipleRows -> withJsonAggExtr permLimitSubQuery ordByExpM als
JASSingleObject -> asSingleRowExtr als

View File

@ -0,0 +1,288 @@
-- | This module contains worker definitions pertaining to generating 'Select'
-- AST nodes.
--
-- NOTE: 'generateSQLSelect' is mutually recursive with
-- 'connectionToSelectWith', thus these two cohabitate in this module.
module Hasura.Backends.Postgres.Translate.Select.GenerateSelect
( generateSQLSelect,
generateSQLSelectFromArrayNode,
connectionToSelectWith,
)
where
import Data.HashMap.Strict qualified as HM
import Data.List.NonEmpty qualified as NE
import Hasura.Backends.Postgres.SQL.DML qualified as S
import Hasura.Backends.Postgres.SQL.Types
import Hasura.Backends.Postgres.Translate.Select.Aliases (mkBaseTableAlias)
import Hasura.Backends.Postgres.Translate.Select.Aux1
( cursorIdentifier,
cursorsSelectAliasIdentifier,
endCursorIdentifier,
hasNextPageIdentifier,
hasPreviousPageIdentifier,
mkFirstElementExp,
mkLastElementExp,
pageInfoSelectAliasIdentifier,
startCursorIdentifier,
)
import Hasura.Backends.Postgres.Translate.Types
import Hasura.Prelude
import Hasura.RQL.IR.Select (ConnectionSlice (SliceFirst, SliceLast))
generateSQLSelect ::
-- | Pre join condition
S.BoolExp ->
SelectSource ->
SelectNode ->
S.Select
generateSQLSelect joinCondition selectSource selectNode =
S.mkSelect
{ 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
}
where
SelectSource sourcePrefix fromItem whereExp sortAndSlice = selectSource
SelectNode extractors joinTree = selectNode
JoinTree objectRelations arrayRelations arrayConnections computedFields = joinTree
ApplySortingAndSlicing
(baseOrderBy, baseSlicing, baseDistinctOn)
(nodeOrderBy, nodeSlicing, nodeDistinctOn) = applySortingAndSlicing sortAndSlice
-- this is the table which is aliased as "sourcePrefix.base"
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
}
baseSelectAlias = S.Alias $ mkBaseTableAlias sourcePrefix
baseFromItem = S.mkSelFromItem baseSelect baseSelectAlias
injectJoinCond ::
S.BoolExp -> -- Join condition
S.BoolExp -> -- Where condition
S.WhereFrag -- New where frag
injectJoinCond joinCond whereCond =
S.WhereFrag $ S.simplifyBoolExp $ S.BEBin S.AndOp joinCond whereCond
-- function to create a joined from item from two from items
leftOuterJoin current new =
S.FIJoin $
S.JoinExpr current S.LeftOuter new $
S.JoinOn $ S.BELit True
-- this is the from eexp for the final select
joinedFrom :: S.FromItem
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
objectRelationToFromItem (objectRelationSource, node) =
let ObjectRelationSource _ colMapping objectSelectSource = objectRelationSource
alias = S.Alias $ _ossPrefix objectSelectSource
source = objectSelectSourceToSelectSource objectSelectSource
select = generateSQLSelect (mkJoinCond baseSelectAlias colMapping) source node
in S.mkLateralFromItem select alias
arrayRelationToFromItem ::
(ArrayRelationSource, MultiRowSelectNode) -> S.FromItem
arrayRelationToFromItem (arrayRelationSource, arraySelectNode) =
let ArrayRelationSource _ colMapping source = arrayRelationSource
alias = S.Alias $ _ssPrefix source
select =
generateSQLSelectFromArrayNode source arraySelectNode $
mkJoinCond baseSelectAlias colMapping
in S.mkLateralFromItem select alias
arrayConnectionToFromItem ::
(ArrayConnectionSource, MultiRowSelectNode) -> S.FromItem
arrayConnectionToFromItem (arrayConnectionSource, arraySelectNode) =
let selectWith = connectionToSelectWith baseSelectAlias arrayConnectionSource arraySelectNode
alias = S.Alias $ _ssPrefix $ _acsSource arrayConnectionSource
in S.FISelectWith (S.Lateral True) selectWith alias
computedFieldToFromItem ::
(ComputedFieldTableSetSource, MultiRowSelectNode) -> S.FromItem
computedFieldToFromItem (computedFieldTableSource, node) =
let ComputedFieldTableSetSource _ source = computedFieldTableSource
internalSelect = generateSQLSelect (S.BELit True) source $ _mrsnSelectNode node
alias = S.Alias $ _ssPrefix source
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
generateSQLSelectFromArrayNode selectSource arraySelectNode joinCondition =
S.mkSelect
{ S.selExtr = topExtractors,
S.selFrom = Just $ S.FromExp [selectFrom]
}
where
MultiRowSelectNode topExtractors selectNode = arraySelectNode
selectFrom =
S.mkSelFromItem
(generateSQLSelect joinCondition selectSource selectNode)
$ S.Alias $ _ssPrefix selectSource
mkJoinCond :: S.Alias -> HashMap PGCol PGCol -> S.BoolExp
mkJoinCond baseTablepfx colMapn =
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)
connectionToSelectWith ::
S.Alias ->
ArrayConnectionSource ->
MultiRowSelectNode ->
S.SelectWithG S.Select
connectionToSelectWith baseSelectAlias arrayConnectionSource arraySelectNode =
let extractionSelect =
S.mkSelect
{ S.selExtr = topExtractors,
S.selFrom = Just $ S.FromExp [S.FIIdentifier finalSelectIdentifier]
}
in S.SelectWith fromBaseSelections extractionSelect
where
ArrayConnectionSource _ columnMapping maybeSplit maybeSlice selectSource =
arrayConnectionSource
MultiRowSelectNode topExtractors selectNode = arraySelectNode
baseSelectIdentifier = Identifier "__base_select"
splitSelectIdentifier = Identifier "__split_select"
sliceSelectIdentifier = Identifier "__slice_select"
finalSelectIdentifier = Identifier "__final_select"
rowNumberIdentifier = Identifier "__row_number"
rowNumberExp = S.SEUnsafe "(row_number() over (partition by 1))"
startRowNumberIdentifier = Identifier "__start_row_number"
endRowNumberIdentifier = Identifier "__end_row_number"
startCursorExp = mkFirstElementExp $ S.SEIdentifier cursorIdentifier
endCursorExp = mkLastElementExp $ S.SEIdentifier cursorIdentifier
startRowNumberExp = mkFirstElementExp $ S.SEIdentifier rowNumberIdentifier
endRowNumberExp = mkLastElementExp $ S.SEIdentifier rowNumberIdentifier
fromBaseSelections =
let joinCond = mkJoinCond baseSelectAlias columnMapping
baseSelectFrom =
S.mkSelFromItem
(generateSQLSelect joinCond selectSource selectNode)
$ S.Alias $ _ssPrefix selectSource
select =
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
mkStarSelect fromIdentifier =
S.mkSelect
{ S.selExtr = [S.selectStar],
S.selFrom = Just $ S.FromExp [S.FIIdentifier fromIdentifier]
}
fromSplitSelection = case maybeSplit of
Nothing -> fromSliceSelection baseSelectIdentifier
Just splitBool ->
let select =
(mkStarSelect baseSelectIdentifier) {S.selWhere = Just $ S.WhereFrag splitBool}
in (S.Alias splitSelectIdentifier, select) : fromSliceSelection splitSelectIdentifier
fromSliceSelection prevSelect = case maybeSlice of
Nothing -> fromFinalSelect prevSelect
Just slice ->
let select = case slice of
SliceFirst limit ->
(mkStarSelect prevSelect)
{ S.selLimit = (Just . S.LimitExp . S.intToSQLExp) limit
}
SliceLast limit ->
let mkRowNumberOrderBy obType =
let orderByItem =
S.OrderByItem (S.SEIdentifier rowNumberIdentifier) (Just obType) Nothing
in S.OrderByExp $ orderByItem NE.:| []
sliceLastSelect =
(mkStarSelect prevSelect)
{ S.selLimit = (Just . S.LimitExp . S.intToSQLExp) limit,
S.selOrderBy = Just $ mkRowNumberOrderBy S.OTDesc
}
sliceLastSelectFrom =
S.mkSelFromItem sliceLastSelect $ S.Alias sliceSelectIdentifier
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
fromFinalSelect prevSelect =
let select = mkStarSelect prevSelect
in (S.Alias finalSelectIdentifier, select) : fromCursorSelection
fromCursorSelection =
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
]
select =
S.mkSelect
{ S.selExtr = extrs,
S.selFrom = Just $ S.FromExp [S.FIIdentifier finalSelectIdentifier]
}
in (S.Alias cursorsSelectAliasIdentifier, select) : fromPageInfoSelection
fromPageInfoSelection =
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]
}
select =
S.mkSelect
{ S.selExtr =
[ S.Extractor hasPrevPage $ Just $ S.Alias hasPreviousPageIdentifier,
S.Extractor hasNextPage $ Just $ S.Alias hasNextPageIdentifier
]
}
in pure (S.Alias pageInfoSelectAliasIdentifier, select)

View File

@ -0,0 +1,106 @@
{-# OPTIONS_GHC -Wno-missing-export-lists #-}
-- | Stuff gutted from Translate.Select
module Hasura.Backends.Postgres.Translate.Select.JoinTree where
import Control.Monad.Writer.Strict
import Data.HashMap.Strict qualified as HM
import Hasura.Backends.Postgres.SQL.DML qualified as S
import Hasura.Backends.Postgres.Translate.Types
import Hasura.Prelude
-- | 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).
withWriteJoinTree ::
(MonadWriter JoinTree m) =>
(JoinTree -> b -> JoinTree) ->
m (a, b) ->
m a
withWriteJoinTree joinTreeUpdater action =
pass $ do
(out, result) <- action
let fromJoinTree joinTree =
joinTreeUpdater joinTree result
pure (out, fromJoinTree)
withWriteObjectRelation ::
(MonadWriter JoinTree m) =>
m
( ObjectRelationSource,
HM.HashMap S.Alias S.SQLExp,
a
) ->
m a
withWriteObjectRelation action =
withWriteJoinTree updateJoinTree $ do
(source, nodeExtractors, out) <- action
pure (out, (source, nodeExtractors))
where
updateJoinTree joinTree (source, nodeExtractors) =
let selectNode = SelectNode nodeExtractors joinTree
in mempty {_jtObjectRelations = HM.singleton source selectNode}
withWriteArrayRelation ::
(MonadWriter JoinTree m) =>
m
( ArrayRelationSource,
S.Extractor,
HM.HashMap S.Alias S.SQLExp,
a
) ->
m a
withWriteArrayRelation action =
withWriteJoinTree updateJoinTree $ do
(source, topExtractor, nodeExtractors, out) <- action
pure (out, (source, topExtractor, nodeExtractors))
where
updateJoinTree joinTree (source, topExtractor, nodeExtractors) =
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
withWriteArrayConnection action =
withWriteJoinTree updateJoinTree $ do
(source, topExtractor, nodeExtractors, out) <- action
pure (out, (source, topExtractor, nodeExtractors))
where
updateJoinTree joinTree (source, topExtractor, nodeExtractors) =
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
withWriteComputedFieldTableSet action =
withWriteJoinTree updateJoinTree $ do
(source, topExtractor, nodeExtractors, out) <- action
pure (out, (source, topExtractor, nodeExtractors))
where
updateJoinTree joinTree (source, topExtractor, nodeExtractors) =
let selectNode = MultiRowSelectNode [topExtractor] $ SelectNode nodeExtractors joinTree
in mempty {_jtComputedFieldTableSets = HM.singleton source selectNode}

View File

@ -0,0 +1,274 @@
-- | This module relates to the processing of Order-By clauses.
module Hasura.Backends.Postgres.Translate.Select.OrderBy (processOrderByItems) where
import Control.Lens ((^?))
import Data.HashMap.Strict qualified as HM
import Data.List.NonEmpty qualified as NE
import Hasura.Backends.Postgres.SQL.DML qualified as S
import Hasura.Backends.Postgres.SQL.Types
( Identifier,
IsIdentifier (toIdentifier),
PGCol (..),
)
import Hasura.Backends.Postgres.Translate.BoolExp (toSQLBoolExp)
import Hasura.Backends.Postgres.Translate.Select.Aliases
( mkAggregateOrderByAlias,
mkAnnOrderByAlias,
mkArrayRelationAlias,
mkArrayRelationSourcePrefix,
mkBaseTableAlias,
mkBaseTableColumnAlias,
mkComputedFieldTableAlias,
mkObjectRelationTableAlias,
mkOrderByFieldName,
)
import Hasura.Backends.Postgres.Translate.Select.Aux1
( fromTableRowArgs,
functionToIdentifier,
selectFromToFromItem,
)
import Hasura.Backends.Postgres.Translate.Select.Extractor
( aggregateFieldsToExtractorExps,
mkAggregateOrderByExtractorAndFields,
)
import Hasura.Backends.Postgres.Translate.Select.JoinTree
( withWriteArrayRelation,
withWriteComputedFieldTableSet,
withWriteObjectRelation,
)
import Hasura.Backends.Postgres.Translate.Types
import Hasura.Prelude
import Hasura.RQL.IR.OrderBy
( OrderByItemG (OrderByItemG, obiColumn),
)
import Hasura.RQL.IR.Select
import Hasura.RQL.Types
{- 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
-}
processOrderByItems ::
forall pgKind m.
( MonadReader StringifyNumbers m,
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
)
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)
where
processAnnOrderByItem ::
AnnotatedOrderByItem ('Postgres pgKind) ->
m (OrderByItemG ('Postgres pgKind) (AnnotatedOrderByElement ('Postgres pgKind) (SQLExpression ('Postgres pgKind)), (S.Alias, SQLExpression ('Postgres pgKind))))
processAnnOrderByItem orderByItem =
forM orderByItem $ \ordByCol ->
(ordByCol,)
<$> processAnnotatedOrderByElement sourcePrefix' fieldAlias' ordByCol
processAnnotatedOrderByElement ::
Identifier -> FieldName -> AnnotatedOrderByElement ('Postgres pgKind) S.SQLExp -> m (S.Alias, (SQLExpression ('Postgres pgKind)))
processAnnotatedOrderByElement sourcePrefix fieldAlias annObCol = do
let ordByAlias = mkAnnOrderByAlias sourcePrefix fieldAlias similarArrayFields annObCol
(ordByAlias,) <$> case annObCol of
AOCColumn pgColInfo ->
pure $
S.mkQIdenExp (mkBaseTableAlias sourcePrefix) $ toIdentifier $ ciColumn pgColInfo
AOCObjectRelation relInfo relFilter rest -> withWriteObjectRelation $ do
let RelInfo relName _ colMapping relTable _ _ = relInfo
relSourcePrefix = mkObjectRelationTableAlias sourcePrefix relName
fieldName = mkOrderByFieldName relName
(relOrderByAlias, relOrdByExp) <-
processAnnotatedOrderByElement relSourcePrefix fieldName rest
let selectSource =
ObjectSelectSource
relSourcePrefix
(S.FISimple relTable Nothing)
(toSQLBoolExp (S.QualTable relTable) relFilter)
relSource = ObjectRelationSource relName colMapping selectSource
pure
( relSource,
HM.singleton relOrderByAlias relOrdByExp,
S.mkQIdenExp relSourcePrefix relOrderByAlias
)
AOCArrayAggregation relInfo relFilter aggOrderBy -> withWriteArrayRelation $ do
let RelInfo relName _ colMapping relTable _ _ = relInfo
fieldName = mkOrderByFieldName relName
relSourcePrefix =
mkArrayRelationSourcePrefix
sourcePrefix
fieldAlias
similarArrayFields
fieldName
relAlias = mkArrayRelationAlias fieldAlias similarArrayFields fieldName
(topExtractor, fields) = mkAggregateOrderByExtractorAndFields aggOrderBy
selectSource =
SelectSource
relSourcePrefix
(S.FISimple relTable Nothing)
(toSQLBoolExp (S.QualTable relTable) relFilter)
noSortingAndSlicing
relSource = ArrayRelationSource relAlias colMapping selectSource
pure
( relSource,
topExtractor,
HM.fromList $ aggregateFieldsToExtractorExps relSourcePrefix fields,
S.mkQIdenExp relSourcePrefix (mkAggregateOrderByAlias aggOrderBy)
)
AOCComputedField ComputedFieldOrderBy {..} ->
case _cfobOrderByElement of
CFOBEScalar _ -> do
let functionArgs = fromTableRowArgs sourcePrefix _cfobFunctionArgsExp
functionExp = S.FunctionExp _cfobFunction functionArgs Nothing
pure $ S.SEFunction functionExp
CFOBETableAggregation _ tableFilter aggOrderBy -> withWriteComputedFieldTableSet $ do
let fieldName = mkOrderByFieldName _cfobName
computedFieldSourcePrefix = mkComputedFieldTableAlias sourcePrefix fieldName
(topExtractor, fields) = mkAggregateOrderByExtractorAndFields aggOrderBy
fromItem =
selectFromToFromItem sourcePrefix $
FromFunction _cfobFunction _cfobFunctionArgsExp Nothing
functionQual = S.QualifiedIdentifier (functionToIdentifier _cfobFunction) Nothing
selectSource =
SelectSource
computedFieldSourcePrefix
fromItem
(toSQLBoolExp functionQual tableFilter)
noSortingAndSlicing
source = ComputedFieldTableSetSource fieldName selectSource
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
)
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.
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
_ ->
-- 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
in S.OrderByItem (S.SEIdentifier $ toIdentifier expAlias) obTyM obNullsM
orderByExp = S.OrderByExp $ toOrderByExp <$> orderByExps
(maybeDistOn, distOnExtrs) = NE.unzip $ applyDistinctOnAtNode sourcePrefix' <$> distOnCols
in (orderByExp, maybeDistOn, fromMaybe [] distOnExtrs)
sortOnlyAtNode =
(Sorting $ ASorting (nodeOrderBy, nodeDistinctOn) Nothing, nodeDistinctOnExtractors)
sortAtNodeAndBase baseColumnOrderBys =
let mkBaseOrderByItem (OrderByItemG orderByType columnInfo nullsOrder) =
S.OrderByItem
(S.SEIdentifier $ toIdentifier $ ciColumn columnInfo)
orderByType
nullsOrder
baseOrderByExp = S.OrderByExp $ mkBaseOrderByItem <$> baseColumnOrderBys
baseDistOnExp = applyDistinctOnAtBase <$> distOnCols
sorting = Sorting $ ASorting (nodeOrderBy, nodeDistinctOn) $ Just (baseOrderByExp, baseDistOnExp)
in (sorting, nodeDistinctOnExtractors)
mkCursorExp ::
[OrderByItemG ('Postgres pgKind) (AnnotatedOrderByElement ('Postgres pgKind) (SQLExpression ('Postgres pgKind)), (S.Alias, SQLExpression ('Postgres pgKind)))] ->
S.SQLExp
mkCursorExp orderByItemExps =
S.applyJsonBuildObj $
flip concatMap orderByItemExps $
\orderByItemExp ->
let OrderByItemG _ (annObCol, (_, valExp)) _ = orderByItemExp
in annObColToJSONField valExp annObCol
where
mkAggOrderByValExp valExp = \case
AAOCount -> [S.SELit "count", valExp]
AAOOp opText colInfo ->
[ S.SELit opText,
S.applyJsonBuildObj [S.SELit $ getPGColTxt $ ciColumn colInfo, valExp]
]
annObColToJSONField valExp = \case
AOCColumn pgCol -> [S.SELit $ getPGColTxt $ ciColumn pgCol, valExp]
AOCObjectRelation relInfo _ obCol ->
[ S.SELit $ relNameToTxt $ riName relInfo,
S.applyJsonBuildObj $ annObColToJSONField valExp obCol
]
AOCArrayAggregation relInfo _ aggOrderBy ->
[ S.SELit $ relNameToTxt (riName relInfo) <> "_aggregate",
S.applyJsonBuildObj $ mkAggOrderByValExp valExp aggOrderBy
]
AOCComputedField cfOrderBy ->
let fieldNameText = computedFieldNameToText $ _cfobName cfOrderBy
in case _cfobOrderByElement cfOrderBy of
CFOBEScalar _ -> [S.SELit fieldNameText, valExp]
CFOBETableAggregation _ _ aggOrderBy ->
[ S.SELit $ fieldNameText <> "_aggregate",
S.applyJsonBuildObj $ mkAggOrderByValExp valExp aggOrderBy
]
applyDistinctOnAtBase ::
NE.NonEmpty PGCol -> S.DistinctExpr
applyDistinctOnAtBase =
S.DistinctOn . map (S.SEIdentifier . toIdentifier) . toList
applyDistinctOnAtNode ::
Identifier ->
NE.NonEmpty PGCol ->
( S.DistinctExpr,
[(S.Alias, S.SQLExp)] -- additional column extractors
)
applyDistinctOnAtNode pfx neCols = (distOnExp, colExtrs)
where
cols = toList neCols
distOnExp = S.DistinctOn $ map (S.SEIdentifier . toIdentifier . mkQColAls) cols
mkQCol c = S.mkQIdenExp (mkBaseTableAlias pfx) $ toIdentifier c
mkQColAls = S.Alias . mkBaseTableColumnAlias pfx
colExtrs = flip map cols $ mkQColAls &&& mkQCol

View File

@ -0,0 +1,696 @@
-- | This module defines functions that translate from the Postgres IR into
-- Postgres SQL AST.
--
-- NOTE: These functions 'processAnnAggregateSelect', 'processAnnSimpleSelect',
-- 'processConnectionSelect', are all mutually recursive.
module Hasura.Backends.Postgres.Translate.Select.Process
( processAnnAggregateSelect,
processAnnSimpleSelect,
processConnectionSelect,
)
where
import Data.HashMap.Strict qualified as HM
import Data.List.NonEmpty qualified as NE
import Data.Text.Extended (ToTxt (toTxt))
import Hasura.Backends.Postgres.SQL.DML qualified as S
import Hasura.Backends.Postgres.SQL.Types
( Identifier,
IsIdentifier (toIdentifier),
PGCol (..),
QualifiedObject (QualifiedObject),
QualifiedTable,
SchemaName (getSchemaTxt),
)
import Hasura.Backends.Postgres.Translate.BoolExp (toSQLBoolExp)
import Hasura.Backends.Postgres.Translate.Column (toJSONableExp)
import Hasura.Backends.Postgres.Translate.Select.Aliases
( mkAnnOrderByAlias,
mkArrayRelationAlias,
mkArrayRelationSourcePrefix,
mkBaseTableAlias,
mkBaseTableColumnAlias,
mkComputedFieldTableAlias,
mkObjectRelationTableAlias,
mkOrderByFieldName,
)
import Hasura.Backends.Postgres.Translate.Select.AnnotatedFieldJSON
import Hasura.Backends.Postgres.Translate.Select.Aux1
( cursorIdentifier,
cursorsSelectAliasIdentifier,
encodeBase64,
endCursorIdentifier,
fromTableRowArgs,
functionToIdentifier,
hasNextPageIdentifier,
hasPreviousPageIdentifier,
pageInfoSelectAliasIdentifier,
selectFromToFromItem,
startCursorIdentifier,
withForceAggregation,
)
import Hasura.Backends.Postgres.Translate.Select.Extractor
( aggregateFieldsToExtractorExps,
asJsonAggExtr,
withJsonAggExtr,
)
import Hasura.Backends.Postgres.Translate.Select.JoinTree
( withWriteArrayConnection,
withWriteArrayRelation,
withWriteComputedFieldTableSet,
withWriteObjectRelation,
)
import Hasura.Backends.Postgres.Translate.Select.OrderBy (processOrderByItems)
import Hasura.Backends.Postgres.Translate.Types
import Hasura.GraphQL.Schema.Common (currentNodeIdVersion, nodeIdVersionInt)
import Hasura.Prelude
import Hasura.RQL.IR.OrderBy (OrderByItemG (OrderByItemG, obiColumn))
import Hasura.RQL.IR.Select
import Hasura.RQL.Types
processSelectParams ::
forall pgKind m.
( MonadReader StringifyNumbers m,
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
)
processSelectParams
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
-- You should be able to retrieve this information
-- from the FromItem generated with selectFromToFromItem
-- however given from S.FromItem is modelled, it is not
-- possible currently.
--
-- More precisely, 'selectFromToFromItem' is injective but not surjective, so
-- any S.FromItem -> S.Qual function would have to be partial.
selectFromToQual :: SelectFrom ('Postgres pgKind) -> S.Qual
selectFromToQual = \case
FromTable table -> S.QualTable table
FromIdentifier i -> S.QualifiedIdentifier (toIdentifier i) Nothing
FromFunction qf _ _ -> S.QualifiedIdentifier (functionToIdentifier qf) Nothing
processAnnAggregateSelect ::
forall pgKind m.
( MonadReader StringifyNumbers m,
MonadWriter JoinTree m,
Backend ('Postgres pgKind),
PostgresAnnotatedFieldJSON pgKind
) =>
SourcePrefixes ->
FieldName ->
AnnAggregateSelect ('Postgres pgKind) ->
m
( SelectSource,
HM.HashMap S.Alias S.SQLExp,
S.Extractor
)
processAnnAggregateSelect sourcePrefixes fieldAlias annAggSel = do
(selectSource, orderByAndDistinctExtrs, _) <-
processSelectParams
sourcePrefixes
fieldAlias
similarArrayFields
tableFrom
permLimitSubQuery
tablePermissions
tableArgs
let thisSourcePrefix = _pfThis sourcePrefixes
processedFields <- forM aggSelFields $ \(fieldName, field) ->
(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
)
let topLevelExtractor =
flip S.Extractor (Just $ S.Alias $ toIdentifier fieldAlias) $
S.applyJsonBuildObj $
flip concatMap (map (second snd) processedFields) $
\(FieldName fieldText, fieldExp) -> [S.SELit fieldText, fieldExp]
nodeExtractors =
HM.fromList $
concatMap (fst . snd) processedFields <> orderByAndDistinctExtrs
pure (selectSource, nodeExtractors, topLevelExtractor)
where
AnnSelectG aggSelFields tableFrom tablePermissions tableArgs strfyNum = annAggSel
permLimit = _tpLimit tablePermissions
orderBy = _saOrderBy tableArgs
permLimitSubQuery = mkPermissionLimitSubQuery permLimit aggSelFields orderBy
similarArrayFields = HM.unions $
flip map (map snd aggSelFields) $ \case
TAFAgg _ -> mempty
TAFNodes _ annFlds ->
mkSimilarArrayFields annFlds orderBy
TAFExp _ -> mempty
mkPermissionLimitSubQuery ::
Maybe Int ->
TableAggregateFields ('Postgres pgKind) ->
Maybe (NE.NonEmpty (AnnotatedOrderByItem ('Postgres pgKind))) ->
PermissionLimitSubQuery
mkPermissionLimitSubQuery permLimit' aggFields orderBys =
case permLimit' of
Nothing -> PLSQNotRequired
Just limit ->
if hasAggregateField || hasAggOrderBy
then PLSQRequired limit
else PLSQNotRequired
where
hasAggregateField = flip any (map snd aggFields) $
\case
TAFAgg _ -> True
_ -> False
hasAggOrderBy = case orderBys of
Nothing -> False
Just l -> flip any (concatMap toList $ toList l) $
\case
AOCArrayAggregation {} -> True
_ -> False
processAnnFields ::
forall pgKind m.
( MonadReader StringifyNumbers m,
MonadWriter JoinTree m,
Backend ('Postgres pgKind),
PostgresAnnotatedFieldJSON pgKind
) =>
Identifier ->
FieldName ->
SimilarArrayFields ->
AnnFields ('Postgres pgKind) ->
m (S.Alias, S.SQLExp)
processAnnFields sourcePrefix fieldAlias similarArrFields annFields = do
fieldExps <- forM annFields $ \(fieldName, field) ->
(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
)
pure $ annRowToJson @pgKind fieldAlias fieldExps
where
mkSourcePrefixes newPrefix = SourcePrefixes newPrefix sourcePrefix
baseTableIdentifier = mkBaseTableAlias sourcePrefix
toSQLCol :: AnnColumnField ('Postgres pgKind) S.SQLExp -> m S.SQLExp
toSQLCol (AnnColumnField col typ asText colOpM caseBoolExpMaybe) = do
strfyNum <- ask
let sqlExpression =
withColumnOp colOpM $
S.mkQIdenExp baseTableIdentifier col
finalSQLExpression =
-- Check out [SQL generation for inherited role]
case caseBoolExpMaybe of
Nothing -> sqlExpression
Just caseBoolExp ->
let boolExp =
S.simplifyBoolExp $
toSQLBoolExp (S.QualifiedIdentifier baseTableIdentifier Nothing) $
_accColCaseBoolExpField <$> caseBoolExp
in S.SECond boolExp sqlExpression S.SENull
pure $ toJSONableExp strfyNum typ asText finalSQLExpression
fromScalarComputedField :: ComputedFieldScalarSelect ('Postgres pgKind) S.SQLExp -> m S.SQLExp
fromScalarComputedField computedFieldScalar = do
strfyNum <- ask
pure $
toJSONableExp strfyNum (ColumnScalar ty) False $
withColumnOp colOpM $
S.SEFunction $ S.FunctionExp fn (fromTableRowArgs sourcePrefix args) Nothing
where
ComputedFieldScalarSelect fn args ty colOpM = computedFieldScalar
withColumnOp :: Maybe (ColumnOp ('Postgres pgKind)) -> S.SQLExp -> S.SQLExp
withColumnOp colOpM sqlExp = case colOpM of
Nothing -> sqlExp
Just (ColumnOp opText cExp) -> S.mkSQLOpExp opText sqlExp cExp
mkNodeId :: QualifiedTable -> PrimaryKeyColumns ('Postgres pgKind) -> S.SQLExp
mkNodeId (QualifiedObject tableSchema tableName) pkeyColumns =
let columnInfoToSQLExp pgColumnInfo =
toJSONableExp LeaveNumbersAlone (ciType pgColumnInfo) False $
S.mkQIdenExp (mkBaseTableAlias sourcePrefix) $ ciColumn pgColumnInfo
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)
mkSimilarArrayFields ::
forall pgKind v.
(Backend ('Postgres pgKind), Eq v) =>
AnnFieldsG ('Postgres pgKind) Void v ->
Maybe (NE.NonEmpty (AnnotatedOrderByItemG ('Postgres pgKind) v)) ->
SimilarArrayFields
mkSimilarArrayFields annFields maybeOrderBys =
HM.fromList $
flip map allTuples $
\(relNameAndArgs, fieldName) -> (fieldName, getSimilarFields relNameAndArgs)
where
getSimilarFields relNameAndArgs = map snd $ filter ((== relNameAndArgs) . fst) allTuples
allTuples = arrayRelationTuples <> aggOrderByRelationTuples
arrayRelationTuples =
let arrayFields = mapMaybe getAnnArr annFields
in flip map arrayFields $
\(f, relSel) -> (getArrayRelNameAndSelectArgs relSel, f)
getAnnArr ::
(a, AnnFieldG ('Postgres pgKind) r v) ->
Maybe (a, ArraySelectG ('Postgres pgKind) r v)
getAnnArr (f, annFld) = case annFld of
AFArrayRelation (ASConnection _) -> Nothing
AFArrayRelation ar -> Just (f, ar)
_ -> Nothing
aggOrderByRelationTuples =
let mkItem (relName, fieldName) =
( (relName, noSelectArgs),
fieldName
)
in map mkItem $
maybe
[]
(mapMaybe (fetchAggOrderByRels . obiColumn) . toList)
maybeOrderBys
fetchAggOrderByRels (AOCArrayAggregation ri _ _) =
Just (riName ri, mkOrderByFieldName $ riName ri)
fetchAggOrderByRels _ = Nothing
getArrayRelNameAndSelectArgs ::
ArraySelectG ('Postgres pgKind) r v ->
(RelName, SelectArgsG ('Postgres pgKind) v)
getArrayRelNameAndSelectArgs = \case
ASSimple r -> (_aarRelationshipName r, _asnArgs $ _aarAnnSelect r)
ASAggregate r -> (_aarRelationshipName r, _asnArgs $ _aarAnnSelect r)
ASConnection r -> (_aarRelationshipName r, _asnArgs $ _csSelect $ _aarAnnSelect r)
processArrayRelation ::
forall pgKind m.
( MonadReader StringifyNumbers m,
MonadWriter JoinTree m,
Backend ('Postgres pgKind),
PostgresAnnotatedFieldJSON pgKind
) =>
SourcePrefixes ->
FieldName ->
S.Alias ->
ArraySelect ('Postgres pgKind) ->
m ()
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
let topExtr =
asJsonAggExtr
JASMultipleRows
(S.toAlias fieldAlias)
permLimitSubQuery
$ orderByForJsonAgg source
pure
( ArrayRelationSource relAlias colMapping source,
topExtr,
nodeExtractors,
()
)
ASAggregate aggSel -> withWriteArrayRelation $ do
let AnnRelationSelectG _ colMapping sel = aggSel
(source, nodeExtractors, topExtr) <-
processAnnAggregateSelect sourcePrefixes fieldAlias sel
pure
( ArrayRelationSource relAlias colMapping source,
topExtr,
nodeExtractors,
()
)
ASConnection connSel -> withWriteArrayConnection $ do
let AnnRelationSelectG _ colMapping sel = connSel
(source, topExtractor, nodeExtractors) <-
processConnectionSelect sourcePrefixes fieldAlias relAlias colMapping sel
pure
( source,
topExtractor,
nodeExtractors,
()
)
aggregateFieldToExp :: AggregateFields ('Postgres pgKind) -> StringifyNumbers -> S.SQLExp
aggregateFieldToExp aggFlds strfyNum = jsonRow
where
jsonRow = S.applyJsonBuildObj (concatMap aggToFlds aggFlds)
withAls fldName sqlExp = [S.SELit fldName, sqlExp]
aggToFlds (FieldName t, fld) = withAls t $ case fld of
AFCount cty -> S.SECount cty
AFOp aggOp -> aggOpToObj aggOp
AFExp e -> S.SELit e
aggOpToObj (AggregateOp opText flds) =
S.applyJsonBuildObj $ concatMap (colFldsToExtr opText) flds
colFldsToExtr opText (FieldName t, CFCol col ty) =
[ S.SELit t,
toJSONableExp strfyNum ty False $
S.SEFnApp opText [S.SEIdentifier $ toIdentifier col] Nothing
]
colFldsToExtr _ (FieldName t, CFExp e) =
[S.SELit t, S.SELit e]
processAnnSimpleSelect ::
forall pgKind m.
( MonadReader StringifyNumbers m,
MonadWriter JoinTree m,
Backend ('Postgres pgKind),
PostgresAnnotatedFieldJSON pgKind
) =>
SourcePrefixes ->
FieldName ->
PermissionLimitSubQuery ->
AnnSimpleSelect ('Postgres pgKind) ->
m
( SelectSource,
HM.HashMap S.Alias S.SQLExp
)
processAnnSimpleSelect sourcePrefixes fieldAlias permLimitSubQuery annSimpleSel = do
(selectSource, orderByAndDistinctExtrs, _) <-
processSelectParams
sourcePrefixes
fieldAlias
similarArrayFields
tableFrom
permLimitSubQuery
tablePermissions
tableArgs
annFieldsExtr <- processAnnFields (_pfThis sourcePrefixes) fieldAlias similarArrayFields annSelFields
let allExtractors = HM.fromList $ annFieldsExtr : orderByAndDistinctExtrs
pure (selectSource, allExtractors)
where
AnnSelectG annSelFields tableFrom tablePermissions tableArgs _ = annSimpleSel
similarArrayFields =
mkSimilarArrayFields annSelFields $ _saOrderBy tableArgs
processConnectionSelect ::
forall pgKind m.
( MonadReader StringifyNumbers m,
MonadWriter JoinTree m,
Backend ('Postgres pgKind),
PostgresAnnotatedFieldJSON pgKind
) =>
SourcePrefixes ->
FieldName ->
S.Alias ->
HM.HashMap PGCol PGCol ->
ConnectionSelect ('Postgres pgKind) Void S.SQLExp ->
m
( ArrayConnectionSource,
S.Extractor,
HM.HashMap S.Alias S.SQLExp
)
processConnectionSelect sourcePrefixes fieldAlias relAlias colMapping connectionSelect = do
(selectSource, orderByAndDistinctExtrs, maybeOrderByCursor) <-
processSelectParams
sourcePrefixes
fieldAlias
similarArrayFields
selectFrom
permLimitSubQuery
tablePermissions
tableArgs
let mkCursorExtractor = (S.Alias cursorIdentifier,) . (`S.SETyAnn` S.textTypeAnn)
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.
mkCursorExtractor primaryKeyColumnsObjectExp : primaryKeyColumnExtractors
(topExtractorExp, exps) <- flip runStateT [] $ processFields selectSource
let topExtractor = S.Extractor topExtractorExp $ Just $ S.Alias fieldIdentifier
allExtractors = HM.fromList $ cursorExtractors <> exps <> orderByAndDistinctExtrs
arrayConnectionSource =
ArrayConnectionSource
relAlias
colMapping
(mkSplitBoolExp <$> maybeSplit)
maybeSlice
selectSource
pure
( arrayConnectionSource,
topExtractor,
allExtractors
)
where
ConnectionSelect _ primaryKeyColumns maybeSplit maybeSlice select = connectionSelect
AnnSelectG fields selectFrom tablePermissions tableArgs _ = select
fieldIdentifier = toIdentifier fieldAlias
thisPrefix = _pfThis sourcePrefixes
permLimitSubQuery = PLSQNotRequired
primaryKeyColumnsObjectExp =
S.applyJsonBuildObj $
flip concatMap (toList primaryKeyColumns) $
\pgColumnInfo ->
[ S.SELit $ getPGColTxt $ ciColumn pgColumnInfo,
toJSONableExp LeaveNumbersAlone (ciType pgColumnInfo) False $
S.mkQIdenExp (mkBaseTableAlias thisPrefix) $ ciColumn pgColumnInfo
]
primaryKeyColumnExtractors =
flip map (toList primaryKeyColumns) $
\pgColumnInfo ->
let pgColumn = ciColumn pgColumnInfo
in ( S.Alias $ mkBaseTableColumnAlias thisPrefix pgColumn,
S.mkQIdenExp (mkBaseTableAlias thisPrefix) pgColumn
)
mkSplitBoolExp (firstSplit NE.:| rest) =
S.BEBin S.OrOp (mkSplitCompareExp firstSplit) $ mkBoolExpFromRest firstSplit rest
where
mkBoolExpFromRest previousSplit =
S.BEBin S.AndOp (mkEqualityCompareExp previousSplit) . \case
[] -> S.BELit False
(thisSplit : remainingSplit) -> mkSplitBoolExp (thisSplit NE.:| remainingSplit)
mkSplitCompareExp (ConnectionSplit kind v (OrderByItemG obTyM obCol _)) =
let obAlias = mkAnnOrderByAlias thisPrefix fieldAlias similarArrayFields obCol
obTy = fromMaybe S.OTAsc obTyM
compareOp = case (kind, obTy) of
(CSKAfter, S.OTAsc) -> S.SGT
(CSKAfter, S.OTDesc) -> S.SLT
(CSKBefore, S.OTAsc) -> S.SLT
(CSKBefore, S.OTDesc) -> S.SGT
in S.BECompare compareOp (S.SEIdentifier $ toIdentifier obAlias) v
mkEqualityCompareExp (ConnectionSplit _ v orderByItem) =
let obAlias =
mkAnnOrderByAlias thisPrefix fieldAlias similarArrayFields $
obiColumn orderByItem
in S.BECompare S.SEQ (S.SEIdentifier $ toIdentifier obAlias) v
similarArrayFields = HM.unions $
flip map (map snd fields) $ \case
ConnectionTypename {} -> mempty
ConnectionPageInfo {} -> mempty
ConnectionEdges edges -> HM.unions $
flip map (map snd edges) $ \case
EdgeTypename {} -> mempty
EdgeCursor {} -> mempty
EdgeNode annFields ->
mkSimilarArrayFields annFields $ _saOrderBy tableArgs
mkSimpleJsonAgg rowExp ob =
let jsonAggExp = S.SEFnApp "json_agg" [rowExp] ob
in S.SEFnApp "coalesce" [jsonAggExp, S.SELit "[]"] Nothing
processFields ::
forall n.
( MonadReader StringifyNumbers n,
MonadWriter JoinTree n,
MonadState [(S.Alias, S.SQLExp)] n
) =>
SelectSource ->
n S.SQLExp
processFields selectSource =
fmap (S.applyJsonBuildObj . concat) $
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
processPageInfoFields infoFields =
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
where
mkSingleFieldSelect field fromIdentifier =
S.SESelect
S.mkSelect
{ S.selExtr = [S.Extractor field Nothing],
S.selFrom = Just $ S.FromExp [S.FIIdentifier fromIdentifier]
}

View File

@ -0,0 +1,72 @@
-- | This module defines the top-level translation functions pertaining to
-- queries that are not aggregation queries, i.e. so-called "simple" selects
-- into Postgres AST.
module Hasura.Backends.Postgres.Translate.Select.Query
( mkSQLSelect,
selectQuerySQL,
)
where
import Control.Monad.Writer.Strict (runWriter)
import Database.PG.Query (Query, fromBuilder)
import Hasura.Backends.Postgres.SQL.DML qualified as S
import Hasura.Backends.Postgres.SQL.IdentifierUniqueness (prefixNumToAliases)
import Hasura.Backends.Postgres.SQL.Types (IsIdentifier (toIdentifier))
import Hasura.Backends.Postgres.Translate.Select.AnnotatedFieldJSON
import Hasura.Backends.Postgres.Translate.Select.Extractor (asJsonAggExtr)
import Hasura.Backends.Postgres.Translate.Select.GenerateSelect (generateSQLSelectFromArrayNode)
import Hasura.Backends.Postgres.Translate.Select.Process (processAnnSimpleSelect)
import Hasura.Backends.Postgres.Translate.Types
import Hasura.Prelude
import Hasura.RQL.IR.Select
( AnnSelectG (_asnStrfyNum),
AnnSimpleSelect,
)
import Hasura.RQL.Types.Backend (Backend)
import Hasura.RQL.Types.Common
( FieldName (FieldName),
JsonAggSelect,
)
import Hasura.SQL.Backend (BackendType (Postgres))
import Hasura.SQL.Types (ToSQL (toSQL))
mkSQLSelect ::
forall pgKind.
( Backend ('Postgres pgKind),
PostgresAnnotatedFieldJSON pgKind
) =>
JsonAggSelect ->
AnnSimpleSelect ('Postgres pgKind) ->
S.Select
mkSQLSelect jsonAggSelect annSel =
let permLimitSubQuery = PLSQNotRequired
((selectSource, nodeExtractors), joinTree) =
runWriter $
flip runReaderT strfyNum $
processAnnSimpleSelect sourcePrefixes rootFldName permLimitSubQuery annSel
selectNode = SelectNode nodeExtractors joinTree
topExtractor =
asJsonAggExtr jsonAggSelect rootFldAls permLimitSubQuery $
orderByForJsonAgg selectSource
arrayNode = MultiRowSelectNode [topExtractor] selectNode
in prefixNumToAliases $
generateSQLSelectFromArrayNode selectSource arrayNode $ S.BELit True
where
strfyNum = _asnStrfyNum annSel
rootFldIdentifier = toIdentifier rootFldName
sourcePrefixes = SourcePrefixes rootFldIdentifier rootFldIdentifier
rootFldName = FieldName "root"
rootFldAls = S.Alias $ toIdentifier rootFldName
-- | Translates IR to Postgres queries for simple SELECTs (select queries that
-- are not aggregations, including subscriptions).
--
-- See 'mkSQLSelect' for the Postgres AST.
selectQuerySQL ::
forall pgKind.
(Backend ('Postgres pgKind), PostgresAnnotatedFieldJSON pgKind) =>
JsonAggSelect ->
AnnSimpleSelect ('Postgres pgKind) ->
Query
selectQuerySQL jsonAggSelect sel =
fromBuilder $ toSQL $ mkSQLSelect jsonAggSelect sel

View File

@ -0,0 +1,141 @@
-- | This module defines the top-level translation functions pertaining to
-- streaming selects into Postgres AST.
module Hasura.Backends.Postgres.Translate.Select.Streaming
( mkStreamSQLSelect,
selectStreamQuerySQL,
)
where
import Control.Monad.Writer.Strict (runWriter)
import Database.PG.Query (Query, fromBuilder)
import Hasura.Backends.Postgres.SQL.DML qualified as S
import Hasura.Backends.Postgres.SQL.IdentifierUniqueness (prefixNumToAliases)
import Hasura.Backends.Postgres.SQL.Types
import Hasura.Backends.Postgres.SQL.Value (withConstructorFn)
import Hasura.Backends.Postgres.Translate.Select.Aliases (mkBaseTableColumnAlias)
import Hasura.Backends.Postgres.Translate.Select.AnnotatedFieldJSON
import Hasura.Backends.Postgres.Translate.Select.Extractor (asJsonAggExtr)
import Hasura.Backends.Postgres.Translate.Select.GenerateSelect (generateSQLSelectFromArrayNode)
import Hasura.Backends.Postgres.Translate.Select.Process (processAnnSimpleSelect)
import Hasura.Backends.Postgres.Translate.Types
( MultiRowSelectNode (MultiRowSelectNode),
PermissionLimitSubQuery (PLSQNotRequired),
SelectNode (SelectNode),
SourcePrefixes (SourcePrefixes),
orderByForJsonAgg,
)
import Hasura.Backends.Postgres.Types.Column (unsafePGColumnToBackend)
import Hasura.Prelude
import Hasura.RQL.IR.BoolExp
( AnnBoolExpFld (AVColumn),
GBoolExp (BoolFld),
OpExpG (AGT, ALT),
andAnnBoolExps,
)
import Hasura.RQL.IR.OrderBy (OrderByItemG (OrderByItemG))
import Hasura.RQL.IR.Select
import Hasura.RQL.Types.Backend (Backend)
import Hasura.RQL.Types.Column
( ColumnInfo (ciColumn),
ColumnValue (cvType),
)
import Hasura.RQL.Types.Common
( FieldName (FieldName),
JsonAggSelect (JASMultipleRows),
)
import Hasura.RQL.Types.Subscription
( CursorOrdering (CODescending),
)
import Hasura.SQL.Backend (BackendType (Postgres))
import Hasura.SQL.Types
( CollectableType (CollectableTypeArray, CollectableTypeScalar),
ToSQL (toSQL),
)
selectStreamQuerySQL ::
forall pgKind.
(Backend ('Postgres pgKind), PostgresAnnotatedFieldJSON pgKind) =>
AnnSimpleStreamSelect ('Postgres pgKind) ->
Query
selectStreamQuerySQL sel =
fromBuilder $ toSQL $ mkStreamSQLSelect sel
mkStreamSQLSelect ::
forall pgKind.
( Backend ('Postgres pgKind),
PostgresAnnotatedFieldJSON pgKind
) =>
AnnSimpleStreamSelect ('Postgres pgKind) ->
S.Select
mkStreamSQLSelect (AnnSelectStreamG () fields from perm args strfyNum) =
let cursorArg = _ssaCursorArg args
cursorColInfo = _sciColInfo cursorArg
annOrderbyCol = AOCColumn cursorColInfo
basicOrderType =
bool S.OTAsc S.OTDesc $ _sciOrdering cursorArg == CODescending
orderByItems =
nonEmpty $ pure $ OrderByItemG (Just basicOrderType) annOrderbyCol Nothing
cursorBoolExp =
let orderByOpExp = bool ALT AGT $ basicOrderType == S.OTAsc
sqlExp =
fromResVars
(CollectableTypeScalar $ unsafePGColumnToBackend $ cvType (_sciInitialValue cursorArg))
["cursor", getPGColTxt $ ciColumn cursorColInfo]
in BoolFld $ AVColumn cursorColInfo [(orderByOpExp sqlExp)]
selectArgs =
noSelectArgs
{ _saWhere =
Just $ maybe cursorBoolExp (andAnnBoolExps cursorBoolExp) $ _ssaWhere args,
_saOrderBy = orderByItems,
_saLimit = Just $ _ssaBatchSize args
}
sqlSelect = AnnSelectG fields from perm selectArgs strfyNum
permLimitSubQuery = PLSQNotRequired
((selectSource, nodeExtractors), joinTree) =
runWriter $
flip runReaderT strfyNum $
processAnnSimpleSelect sourcePrefixes rootFldName permLimitSubQuery sqlSelect
selectNode = SelectNode nodeExtractors joinTree
topExtractor =
asJsonAggExtr JASMultipleRows rootFldAls permLimitSubQuery $
orderByForJsonAgg selectSource
cursorLatestValueExp :: S.SQLExp =
let pgColumn = ciColumn cursorColInfo
mkMaxOrMinSQLExp maxOrMin col =
S.SEFnApp maxOrMin [S.SEIdentifier col] Nothing
maxOrMinTxt = bool "MIN" "MAX" $ basicOrderType == S.OTAsc
-- text encoding the cursor value while it's fetched from the DB, because
-- we can then directly reuse this value, otherwise if this were json encoded
-- then we'd have to parse the value and then convert it into a text encoded value
colExp =
[ S.SELit (getPGColTxt pgColumn),
S.SETyAnn
(mkMaxOrMinSQLExp maxOrMinTxt $ mkBaseTableColumnAlias rootFldIdentifier pgColumn)
S.textTypeAnn
]
in -- SELECT json_build_object ('col1', MAX(col1) :: text)
S.SEFnApp "json_build_object" colExp Nothing
cursorLatestValueExtractor = S.Extractor cursorLatestValueExp (Just $ S.Alias $ Identifier "cursor")
arrayNode = MultiRowSelectNode [topExtractor, cursorLatestValueExtractor] selectNode
in prefixNumToAliases $
generateSQLSelectFromArrayNode selectSource arrayNode $ S.BELit True
where
rootFldIdentifier = toIdentifier rootFldName
sourcePrefixes = SourcePrefixes rootFldIdentifier rootFldIdentifier
rootFldName = FieldName "root"
rootFldAls = S.Alias $ toIdentifier rootFldName
-- TODO: these functions also exist in `resolveMultiplexedValue`, de-duplicate these!
fromResVars pgType jPath =
addTypeAnnotation pgType $
S.SEOpApp
(S.SQLOp "#>>")
[ S.SEQIdentifier $ S.QIdentifier (S.QualifiedIdentifier (Identifier "_subs") Nothing) (Identifier "result_vars"),
S.SEArray $ map S.SELit jPath
]
addTypeAnnotation pgType =
flip S.SETyAnn (S.mkTypeAnn pgType) . case pgType of
CollectableTypeScalar scalarType -> withConstructorFn scalarType
CollectableTypeArray _ -> id

View File

@ -20,6 +20,7 @@ module Hasura.Backends.Postgres.Translate.Types
SelectSource (SelectSource, _ssPrefix),
SortingAndSlicing (SortingAndSlicing),
SourcePrefixes (..),
SimilarArrayFields,
applySortingAndSlicing,
noSortingAndSlicing,
objectSelectSourceToSelectSource,
@ -243,3 +244,5 @@ data PermissionLimitSubQuery
PLSQRequired !Int
| PLSQNotRequired
deriving (Show, Eq)
type SimilarArrayFields = HM.HashMap FieldName [FieldName]

View File

@ -40,7 +40,6 @@ import Hasura.Backends.Postgres.Execute.Prepare
import Hasura.Backends.Postgres.SQL.DML qualified as S
import Hasura.Backends.Postgres.SQL.Types
import Hasura.Backends.Postgres.SQL.Value (PGScalarValue (..))
import Hasura.Backends.Postgres.Translate.Select (asSingleRowJsonResp)
import Hasura.Backends.Postgres.Translate.Select qualified as RS
import Hasura.Base.Error
import Hasura.EncJSON
@ -56,6 +55,7 @@ import Hasura.Prelude
import Hasura.RQL.DDL.Headers
import Hasura.RQL.DDL.Webhook.Transform
import Hasura.RQL.DDL.Webhook.Transform.Class (mkReqTransformCtx)
import Hasura.RQL.DML.Internal (dmlTxErrorHandler)
import Hasura.RQL.IR.Action qualified as RA
import Hasura.RQL.IR.Select qualified as RS
import Hasura.RQL.Types
@ -108,6 +108,17 @@ runActionExecution userInfo aep =
liftEitherM $ runExceptT $ runTx (_pscExecCtx srcConfig) Q.ReadOnly $ liftTx $ asSingleRowJsonResp querySQL []
AEPAsyncMutation actionId -> pure $ (,Nothing) $ encJFromJValue $ actionIdToText actionId
-- | This function is generally used on the result of 'selectQuerySQL',
-- 'selectAggregateQuerySQL' or 'connectionSelectSQL' to run said query and get
-- back the resulting JSON.
asSingleRowJsonResp ::
Q.Query ->
[Q.PrepArg] ->
Q.TxE QErr EncJSON
asSingleRowJsonResp query args =
encJFromBS . runIdentity . Q.getRow
<$> Q.rawQE dmlTxErrorHandler query args True
-- | Synchronously execute webhook handler and resolve response to action "output"
resolveActionExecution ::
Env.Environment ->