diff --git a/src/PostgREST/DbRequestBuilder.hs b/src/PostgREST/DbRequestBuilder.hs index 61e799139..e282fc623 100644 --- a/src/PostgREST/DbRequestBuilder.hs +++ b/src/PostgREST/DbRequestBuilder.hs @@ -1,6 +1,7 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} module PostgREST.DbRequestBuilder ( readRequest , mutateRequest @@ -60,21 +61,21 @@ readRequest maxRows allRels proc apiRequest = _ -> Nothing - -- Build tree with a Level attribute so when an embed occurs and the parent node has the same name as the child we can differentiate them by having - -- an alias like "node_lvl", this is related to issue #987. + -- Build tree with a Depth attribute so when a self join occurs we can differentiate the parent and child tables by having + -- an alias like "table_depth", this is related to issue #987. buildReadRequest :: [Tree SelectItem] -> ReadRequest buildReadRequest fieldTree = - let rootLvl = 1 + let rootDepth = 0 rootNodeName = if action == ActionRead then rootTableName else sourceCTEName in - foldr (treeEntry rootLvl) (Node (Select [] [rootNodeName] [] [] [] allRange, (rootNodeName, Nothing, Nothing, Nothing, rootLvl)) []) fieldTree + foldr (treeEntry rootDepth) (Node (Select [] [rootNodeName] [] [] [] allRange, (rootNodeName, Nothing, Nothing, Nothing, rootDepth)) []) fieldTree where - treeEntry :: Level -> Tree SelectItem -> ReadRequest -> ReadRequest - treeEntry lvl (Node fld@((fn, _),_,alias,relationDetail) fldForest) (Node (q, i) rForest) = - let nxtLvl = succ lvl in + treeEntry :: Depth -> Tree SelectItem -> ReadRequest -> ReadRequest + treeEntry depth (Node fld@((fn, _),_,alias,relationDetail) fldForest) (Node (q, i) rForest) = + let nxtDepth = succ depth in case fldForest of [] -> Node (q {select=fld:select q}, i) rForest _ -> Node (q, i) $ - foldr (treeEntry nxtLvl) (Node (Select [] [fn] [] [] [] allRange, (fn, Nothing, alias, relationDetail, nxtLvl)) []) fldForest:rForest + foldr (treeEntry nxtDepth) (Node (Select [] [fn] [] [] [] allRange, (fn, Nothing, alias, relationDetail, nxtDepth)) []) fldForest:rForest relations :: [Relation] relations = case action of @@ -109,7 +110,7 @@ augumentRequestWithJoin schema allRels request = >>= addJoinConditions schema addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either ApiRequestError ReadRequest -addRelations schema allRelations parentNode (Node readNode@(query, (name, _, alias, relationDetail, level)) forest) = +addRelations schema allRelations parentNode (Node readNode@(query, (name, _, alias, relationDetail, depth)) forest) = case parentNode of (Just (Node (Select{from=[parentNodeTable]}, _) _)) -> Node <$> readNode' <*> forest' @@ -121,7 +122,7 @@ addRelations schema allRelations parentNode (Node readNode@(query, (name, _, ali rel = note (NoRelationBetween parentNodeTable name) $ findRelation schema name parentNodeTable relationDetail where - + findRelation s nodeTableName parentNodeTableName Nothing = find (\r -> s == tableSchema (relTable r) && -- match schema for relation table @@ -159,8 +160,8 @@ addRelations schema allRelations parentNode (Node readNode@(query, (name, _, ali -- addRelation will turn project_id to project so the above condition will match ) ) allRelations - - findRelation s nodeTableName parentNodeTableName (Just rd) = + + findRelation s nodeTableName parentNodeTableName (Just rd) = find (\r -> s == tableSchema (relTable r) && -- match schema for relation table s == tableSchema (relFTable r) && -- match schema for relation foriegn table @@ -176,7 +177,7 @@ addRelations schema allRelations parentNode (Node readNode@(query, (name, _, ali parentNodeTableName == tableName (relFTable r) && -- && -- match relation foreign table name length (relColumns r) == 1 && rd == (colName . unsafeHead . relColumns) r - ) + ) || @@ -190,17 +191,17 @@ addRelations schema allRelations parentNode (Node readNode@(query, (name, _, ali nodeTableName == tableName (relTable r) && -- match relation table name parentNodeTableName == tableName (relFTable r) && -- match relation foreign table name rd == tableName (fromJust (relLinkTable r)) - ) + ) ) ) allRelations n `colMatches` rc = (toS ("^" <> rc <> "_?(?:|[iI][dD]|[fF][kK])$") :: BS.ByteString) =~ (toS n :: BS.ByteString) - addRel :: (ReadQuery, (NodeName, Maybe Relation, Maybe Alias, Maybe RelationDetail, Level)) -> Relation -> (ReadQuery, (NodeName, Maybe Relation, Maybe Alias, Maybe RelationDetail, Level)) - addRel (query', (n, _, a, _, lvl)) r = (query' {from=fromRelation}, (n, Just r, a, Nothing, lvl)) + addRel :: (ReadQuery, (NodeName, Maybe Relation, Maybe Alias, Maybe RelationDetail, Depth)) -> Relation -> (ReadQuery, (NodeName, Maybe Relation, Maybe Alias, Maybe RelationDetail, Depth)) + addRel (query', (n, _, a, _, dpth)) r = (query' {from=fromRelation}, (n, Just r, a, Nothing, dpth)) where fromRelation = map (\t -> if t == n then tableName (relTable r) else t) (from query') _ -> n' <$> updateForest (Just (n' forest)) where - n' = Node (query, (name, Just r, alias, Nothing, level)) + n' = Node (query, (name, Just r, alias, Nothing, depth)) t = Table schema name Nothing True -- !!! TODO find another way to get the table from the query r = Relation t [] t [] Root Nothing Nothing Nothing where @@ -208,7 +209,7 @@ addRelations schema allRelations parentNode (Node readNode@(query, (name, _, ali updateForest n = mapM (addRelations schema allRelations n) forest addJoinConditions :: Schema -> ReadRequest -> Either ApiRequestError ReadRequest -addJoinConditions schema (Node node@(query, nodeProps@(_, relation, _, _, lvl)) forest) = +addJoinConditions schema (Node node@(query, nodeProps@(_, relation, _, _, _)) forest) = case relation of Just Relation{relType=Root} -> Node node <$> updatedForest -- this is the root node Just rel@Relation{relType=Parent} -> Node (augmentQuery rel, nodeProps) <$> updatedForest @@ -219,31 +220,23 @@ addJoinConditions schema (Node node@(query, nodeProps@(_, relation, _, _, lvl)) _ -> Left UnknownRelation where updatedForest = mapM (addJoinConditions schema) forest - augmentQuery rel = foldr addJoinCondToReadQuery query (getJoinConds lvl rel) - addJoinCondToReadQuery jc rq@Select{joinConds=jcs} = rq{joinConds=jc:jcs} + augmentQuery rel = foldr addJoinCond query (getJoinConditions rel) + addJoinCond :: JoinCondition -> ReadQuery -> ReadQuery + addJoinCond jc rq@Select{joinConditions=jcs} = rq{joinConditions=jc:jcs} -getJoinConds :: Integer -> Relation -> [JoinCond] -getJoinConds level (Relation t cols ft fcs typ lt lc1 lc2) = - case typ of - -- JoinCond needs the Level attr to know the tables aliases - -- The level depends on the sql query structure - -- Child has the embed as: - -- SELECT .., COALESCE(SELECT .. FROM ch AS ch_lvl_2 WHERE ch_lvl_2.col = p_lvl_1.col) FROM p AS p_lvl_1 - -- Parent has similar structure regarding the levels - -- Many has the embed as: - -- SELECT .., COALESCE(SELECT .. FROM ch AS ch_lvl_2, gch AS gch_lvl_2 WHERE ch_lvl_2.col = gch_lvl_2.col AND p_lvl_1.acol = ch_lvl_2.acol) - -- FROM p AS p_lvl_1 - Child -> zipWith (toJoinCond (tN, level) (ftN, level - 1)) cols fcs - Parent -> zipWith (toJoinCond (tN, level) (ftN, level - 1)) cols fcs - Many -> zipWith (toJoinCond (tN, level) (ltN, level)) cols (fromMaybe [] lc1) ++ zipWith (toJoinCond (ftN, level - 1) (ltN, level)) fcs (fromMaybe [] lc2) - Root -> undefined +getJoinConditions :: Relation -> [JoinCondition] +getJoinConditions (Relation Table{tableSchema=tSchema, tableName=tN} cols Table{tableName=ftN} fcs typ lt lc1 lc2) = + if | typ == Child || typ == Parent -> + zipWith (toJoinCondition tN ftN) cols fcs + | typ == Many -> + let ltN = fromMaybe "" (tableName <$> lt) in + zipWith (toJoinCondition tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toJoinCondition ftN ltN) fcs (fromMaybe [] lc2) + | typ == Root -> undefined where - s = if typ == Parent then "" else tableSchema t - tN = tableName t - ftN = tableName ft - ltN = fromMaybe "" (tableName <$> lt) - toJoinCond :: (Text, Integer) -> (Text, Integer) -> Column -> Column -> JoinCond - toJoinCond (tb, tLvl) (ftb, fLvl) c fc = JoinCond (QualifiedIdentifier s tb, colName c, tLvl) (QualifiedIdentifier s ftb, colName fc, fLvl) + toJoinCondition :: Text -> Text -> Column -> Column -> JoinCondition + toJoinCondition tb ftb c fc = + JoinCondition (QualifiedIdentifier tSchema tb, Nothing, colName c) + (QualifiedIdentifier tSchema ftb, Nothing, colName fc) addFiltersOrdersRanges :: ApiRequest -> Either ApiRequestError (ReadRequest -> ReadRequest) addFiltersOrdersRanges apiRequest = foldr1 (liftA2 (.)) [ diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index ab69af052..04600dbf0 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -218,20 +218,32 @@ requestToCountQuery schema (DbRead (Node (Select{where_=logicForest}, (mainTbl, qi = removeSourceCTESchema schema mainTbl requestToQuery :: Schema -> Bool -> DbRequest -> SqlQuery -requestToQuery schema isParent (DbRead (Node (Select colSelects tbls logicForest joinConds_ ordts range, (nodeName, maybeRelation, _, _, level)) forest)) = - query +requestToQuery schema isParent (DbRead (Node (Select colSelects tbls logicForest joinConditions_ ordts range, (nodeName, maybeRelation, _, _, depth)) forest)) = + unwords [ + "SELECT " <> intercalate ", " (map (pgFmtSelectItem qi) colSelects ++ selects), + "FROM " <> intercalate ", " tables, + unwords joins, + ("WHERE " <> intercalate " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition joinConds)) + `emptyOnFalse` (null logicForest && null joinConds), + ("ORDER BY " <> intercalate ", " (map (pgFmtOrderTerm qi) ordts)) `emptyOnFalse` null ordts, + ("LIMIT " <> maybe "ALL" show (rangeLimit range) <> " OFFSET " <> show (rangeOffset range)) `emptyOnFalse` (isParent || range == allRange) ] + where mainTbl = fromMaybe nodeName (tableName . relTable <$> maybeRelation) - tableAlias tbl = tbl <> "_" <> show level - qi = QualifiedIdentifier "" $ tableAlias mainTbl - query = unwords [ - "SELECT " <> intercalate ", " (map (pgFmtSelectItem qi) colSelects ++ selects), - "FROM " <> intercalate ", " (map (\t -> fromQi (removeSourceCTESchema schema t) <> " AS " <> pgFmtIdent (tableAlias t)) tbls), - unwords joins, - ("WHERE " <> intercalate " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCond joinConds_)) - `emptyOnFalse` (null logicForest && null joinConds_), - ("ORDER BY " <> intercalate ", " (map (pgFmtOrderTerm qi) ordts)) `emptyOnFalse` null ordts, - ("LIMIT " <> maybe "ALL" show (rangeLimit range) <> " OFFSET " <> show (rangeOffset range)) `emptyOnFalse` (isParent || range == allRange) ] + isSelfJoin = maybe False (\r -> relType r /= Root && relTable r == relFTable r) maybeRelation + (qi, tables, joinConds) = + let depthAlias name dpth = if dpth /= 0 then name <> "_" <> show dpth else name in -- Root node doesn't get aliased + if isSelfJoin + then ( + QualifiedIdentifier "" (depthAlias mainTbl depth), + (\t -> fromQi (removeSourceCTESchema schema t) <> " AS " <> pgFmtIdent (depthAlias t depth)) <$> tbls, + (\(JoinCondition (qi1, _, c1) (qi2, _, c2)) -> + JoinCondition (qi1, Just $ depthAlias (qiName qi1) depth, c1) + (qi2, Just $ depthAlias (qiName qi2) (depth - 1), c2)) <$> joinConditions_) + else ( + removeSourceCTESchema schema mainTbl, + fromQi . removeSourceCTESchema schema <$> tbls, + joinConditions_) (joins, selects) = foldr getQueryParts ([],[]) forest @@ -423,11 +435,12 @@ pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper (toS (pgFmtLit v) <> "::unknown ") (find ((==) . toLower $ v) ["null","true","false"]) -pgFmtJoinCond :: JoinCond -> SqlFragment -pgFmtJoinCond (JoinCond (qi, cName, lvl) (fQi, fcName, fLvl)) = - let qiAlias = QualifiedIdentifier "" (qiName qi <> "_" <> show lvl) - fQiAlias = QualifiedIdentifier "" (qiName fQi <> "_" <> show fLvl) in - pgFmtColumn qiAlias cName <> " = " <> pgFmtColumn fQiAlias fcName +pgFmtJoinCondition :: JoinCondition -> SqlFragment +pgFmtJoinCondition (JoinCondition (qi, al1, col1) (QualifiedIdentifier schema fTable, al2, col2)) = + pgFmtColumn (fromMaybe qi $ aliasToQi al1) col1 <> " = " <> + pgFmtColumn (fromMaybe (removeSourceCTESchema schema fTable) $ aliasToQi al2) col2 + where + aliasToQi al = QualifiedIdentifier "" <$> al pgFmtLogicTree :: QualifiedIdentifier -> LogicTree -> SqlFragment pgFmtLogicTree qi (Expr hasNot op forest) = notOp <> " (" <> intercalate (" " <> show op <> " ") (pgFmtLogicTree qi <$> forest) <> ")" diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index 2a219fc7c..e5584351f 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -276,15 +276,17 @@ type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe RelationDetail) -- | Path of the embedded levels, e.g "clients.projects.name=eq.." gives Path ["clients", "projects"] type EmbedPath = [Text] data Filter = Filter { field::Field, opExpr::OpExpr } deriving (Show, Eq) -data JoinCond = JoinCond (QualifiedIdentifier, FieldName, Level) (QualifiedIdentifier, FieldName, Level) deriving (Show, Eq) -type Level = Integer +data JoinCondition = JoinCondition (QualifiedIdentifier, Maybe Alias, FieldName) + (QualifiedIdentifier, Maybe Alias, FieldName) deriving (Show, Eq) -data ReadQuery = Select { select::[SelectItem], from::[TableName], where_::[LogicTree], joinConds::[JoinCond], order::[OrderTerm], range_::NonnegRange } deriving (Show, Eq) +data ReadQuery = Select { select::[SelectItem], from::[TableName], where_::[LogicTree], joinConditions::[JoinCondition], order::[OrderTerm], range_::NonnegRange } deriving (Show, Eq) data MutateQuery = Insert { in_::TableName, insPkCols::[Text], qPayload::PayloadJSON, onConflict:: Maybe PreferResolution, where_::[LogicTree], returning::[FieldName] } | Delete { in_::TableName, where_::[LogicTree], returning::[FieldName] } | Update { in_::TableName, qPayload::PayloadJSON, where_::[LogicTree], returning::[FieldName] } deriving (Show, Eq) -type ReadNode = (ReadQuery, (NodeName, Maybe Relation, Maybe Alias, Maybe RelationDetail, Level)) +type ReadNode = (ReadQuery, (NodeName, Maybe Relation, Maybe Alias, Maybe RelationDetail, Depth)) type ReadRequest = Tree ReadNode +-- Depth of the ReadRequest tree +type Depth = Integer type MutateRequest = MutateQuery data DbRequest = DbRead ReadRequest | DbMutate MutateRequest