Fix #1221, embedding when having a self join

This commit is contained in:
steve-chavez
2019-02-04 10:22:02 -05:00
committed by Steve Chávez
parent 1037313e77
commit 673aa25082
8 changed files with 105 additions and 48 deletions
+1
View File
@@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Fixed ### Fixed
- #1223, Fix incorrect OpenAPI externalDocs url - @steve-chavez - #1223, Fix incorrect OpenAPI externalDocs url - @steve-chavez
- #1221, Fix embedding other resources when having a self join - @steve-chavez
## [5.2.0] - 2018-12-12 ## [5.2.0] - 2018-12-12
+25 -14
View File
@@ -76,7 +76,7 @@ readRequest maxRows allRels proc apiRequest =
buildReadRequest fieldTree = buildReadRequest fieldTree =
let rootDepth = 0 let rootDepth = 0
rootNodeName = if action == ActionRead then rootTableName else sourceCTEName in rootNodeName = if action == ActionRead then rootTableName else sourceCTEName in
foldr (treeEntry rootDepth) (Node (Select [] rootNodeName [] [] [] [] allRange, (rootNodeName, Nothing, Nothing, Nothing, rootDepth)) []) fieldTree foldr (treeEntry rootDepth) (Node (Select [] rootNodeName Nothing [] [] [] [] allRange, (rootNodeName, Nothing, Nothing, Nothing, rootDepth)) []) fieldTree
where where
treeEntry :: Depth -> Tree SelectItem -> ReadRequest -> ReadRequest treeEntry :: Depth -> Tree SelectItem -> ReadRequest -> ReadRequest
treeEntry depth (Node fld@((fn, _),_,alias,relationDetail) fldForest) (Node (q, i) rForest) = treeEntry depth (Node fld@((fn, _),_,alias,relationDetail) fldForest) (Node (q, i) rForest) =
@@ -84,7 +84,7 @@ readRequest maxRows allRels proc apiRequest =
case fldForest of case fldForest of
[] -> Node (q {select=fld:select q}, i) rForest [] -> Node (q {select=fld:select q}, i) rForest
_ -> Node (q, i) $ _ -> Node (q, i) $
foldr (treeEntry nxtDepth) (Node (Select [] fn [] [] [] [] allRange, (fn, Nothing, alias, relationDetail, nxtDepth)) []) fldForest:rForest foldr (treeEntry nxtDepth) (Node (Select [] fn Nothing [] [] [] [] allRange, (fn, Nothing, alias, relationDetail, nxtDepth)) []) fldForest:rForest
relations :: [Relation] relations :: [Relation]
relations = case action of relations = case action of
@@ -116,7 +116,7 @@ treeRestrictRange maxRows_ request = pure $ nodeRestrictRange maxRows_ `fmap` re
augumentRequestWithJoin :: Schema -> [Relation] -> ReadRequest -> Either ApiRequestError ReadRequest augumentRequestWithJoin :: Schema -> [Relation] -> ReadRequest -> Either ApiRequestError ReadRequest
augumentRequestWithJoin schema allRels request = augumentRequestWithJoin schema allRels request =
addRelations schema allRels Nothing request addRelations schema allRels Nothing request
>>= addJoinConditions schema >>= addJoinConditions schema Nothing
addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either ApiRequestError ReadRequest addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either ApiRequestError ReadRequest
addRelations schema allRelations parentNode (Node (query@Select{from=tbl}, (nodeName, _, alias, relationDetail, depth)) forest) = addRelations schema allRelations parentNode (Node (query@Select{from=tbl}, (nodeName, _, alias, relationDetail, depth)) forest) =
@@ -216,10 +216,11 @@ findRelation schema allRelations nodeTableName parentNodeTableName relationDetai
) )
) allRelations ) allRelations
addJoinConditions :: Schema -> ReadRequest -> Either ApiRequestError ReadRequest -- previousAlias is only used for the case of self joins
addJoinConditions schema (Node node@(query, nodeProps@(_, relation, _, _, _)) forest) = addJoinConditions :: Schema -> Maybe Alias -> ReadRequest -> Either ApiRequestError ReadRequest
addJoinConditions schema previousAlias (Node node@(query@Select{from=tbl}, nodeProps@(_, relation, _, _, depth)) forest) =
case relation of case relation of
Just Relation{relType=Root} -> Node node <$> updatedForest -- this is the root node Just Relation{relType=Root} -> Node node <$> updatedForest -- this is the root node
Just rel@Relation{relType=Parent} -> Node (augmentQuery rel, nodeProps) <$> updatedForest Just rel@Relation{relType=Parent} -> Node (augmentQuery rel, nodeProps) <$> updatedForest
Just rel@Relation{relType=Child} -> Node (augmentQuery rel, nodeProps) <$> updatedForest Just rel@Relation{relType=Child} -> Node (augmentQuery rel, nodeProps) <$> updatedForest
Just rel@Relation{relType=Many, relLinkTable=(Just linkTable)} -> Just rel@Relation{relType=Many, relLinkTable=(Just linkTable)} ->
@@ -227,13 +228,21 @@ addJoinConditions schema (Node node@(query, nodeProps@(_, relation, _, _, _)) fo
Node (rq{implicitJoins=tableName linkTable:implicitJoins rq}, nodeProps) <$> updatedForest Node (rq{implicitJoins=tableName linkTable:implicitJoins rq}, nodeProps) <$> updatedForest
_ -> Left UnknownRelation _ -> Left UnknownRelation
where where
updatedForest = mapM (addJoinConditions schema) forest newAlias = case isSelfJoin <$> relation of
augmentQuery rel = foldr addJoinCond query (getJoinConditions rel) Just True
addJoinCond :: JoinCondition -> ReadQuery -> ReadQuery | depth /= 0 -> Just (tbl <> "_" <> show depth) -- root node doesn't get aliased
addJoinCond jc rq@Select{joinConditions=jcs} = rq{joinConditions=jc:jcs} | otherwise -> Nothing
_ -> Nothing
augmentQuery rel =
foldr
(\jc rq@Select{joinConditions=jcs} -> rq{joinConditions=jc:jcs})
query{fromAlias=newAlias}
(getJoinConditions previousAlias newAlias rel)
updatedForest = mapM (addJoinConditions schema newAlias) forest
getJoinConditions :: Relation -> [JoinCondition] -- previousAlias and newAlias are used in the case of self joins
getJoinConditions (Relation Table{tableSchema=tSchema, tableName=tN} cols Table{tableName=ftN} fCols typ lt lc1 lc2) = getJoinConditions :: Maybe Alias -> Maybe Alias -> Relation -> [JoinCondition]
getJoinConditions previousAlias newAlias (Relation Table{tableSchema=tSchema, tableName=tN} cols Table{tableName=ftN} fCols typ lt lc1 lc2) =
if | typ == Child || typ == Parent -> if | typ == Child || typ == Parent ->
zipWith (toJoinCondition tN ftN) cols fCols zipWith (toJoinCondition tN ftN) cols fCols
| typ == Many -> | typ == Many ->
@@ -243,8 +252,10 @@ getJoinConditions (Relation Table{tableSchema=tSchema, tableName=tN} cols Table{
where where
toJoinCondition :: Text -> Text -> Column -> Column -> JoinCondition toJoinCondition :: Text -> Text -> Column -> Column -> JoinCondition
toJoinCondition tb ftb c fc = toJoinCondition tb ftb c fc =
JoinCondition (QualifiedIdentifier tSchema tb, Nothing, colName c) let qi1 = QualifiedIdentifier tSchema tb
(QualifiedIdentifier tSchema ftb, Nothing, colName fc) qi2 = QualifiedIdentifier tSchema ftb in
JoinCondition (maybe qi1 (QualifiedIdentifier mempty) newAlias, colName c)
(maybe qi2 (QualifiedIdentifier mempty) previousAlias, colName fc)
addFiltersOrdersRanges :: ApiRequest -> Either ApiRequestError (ReadRequest -> ReadRequest) addFiltersOrdersRanges :: ApiRequest -> Either ApiRequestError (ReadRequest -> ReadRequest)
addFiltersOrdersRanges apiRequest = foldr1 (liftA2 (.)) [ addFiltersOrdersRanges apiRequest = foldr1 (liftA2 (.)) [
+11 -24
View File
@@ -219,32 +219,21 @@ requestToCountQuery schema (DbRead (Node (Select{where_=logicForest}, (mainTbl,
qi = removeSourceCTESchema schema mainTbl qi = removeSourceCTESchema schema mainTbl
requestToQuery :: Schema -> Bool -> DbRequest -> SqlQuery requestToQuery :: Schema -> Bool -> DbRequest -> SqlQuery
requestToQuery schema isParent (DbRead (Node (Select colSelects tbl implJoins logicForest joinConditions_ ordts range, (_, maybeRelation, _, _, depth)) forest)) = requestToQuery schema isParent (DbRead (Node (Select colSelects tbl tblAlias implJoins logicForest joinConditions_ ordts range, _) forest)) =
unwords [ unwords [
"SELECT " <> intercalate ", " (map (pgFmtSelectItem qi) colSelects ++ selects), "SELECT " <> intercalate ", " (map (pgFmtSelectItem qi) colSelects ++ selects),
"FROM " <> intercalate ", " tables, "FROM " <> intercalate ", " (tabl : implJs),
unwords joins, unwords joins,
("WHERE " <> intercalate " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition joinConds)) ("WHERE " <> intercalate " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition joinConditions_))
`emptyOnFalse` (null logicForest && null joinConds), `emptyOnFalse` (null logicForest && null joinConditions_),
("ORDER BY " <> intercalate ", " (map (pgFmtOrderTerm qi) ordts)) `emptyOnFalse` null ordts, ("ORDER BY " <> intercalate ", " (map (pgFmtOrderTerm qi) ordts)) `emptyOnFalse` null ordts,
("LIMIT " <> maybe "ALL" show (rangeLimit range) <> " OFFSET " <> show (rangeOffset range)) `emptyOnFalse` (isParent || range == allRange) ] ("LIMIT " <> maybe "ALL" show (rangeLimit range) <> " OFFSET " <> show (rangeOffset range)) `emptyOnFalse` (isParent || range == allRange) ]
where where
tbls = tbl:implJoins implJs = fromQi . QualifiedIdentifier schema <$> implJoins
isSelfJoin = maybe False (\r -> relType r /= Root && relTable r == relFTable r) maybeRelation mainQi = removeSourceCTESchema schema tbl
(qi, tables, joinConds) = tabl = fromQi mainQi <> maybe mempty (\a -> " AS " <> pgFmtIdent a) tblAlias
let depthAlias name dpth = if dpth /= 0 then name <> "_" <> show dpth else name in -- Root node doesn't get aliased qi = maybe mainQi (QualifiedIdentifier mempty) tblAlias
if isSelfJoin
then (
QualifiedIdentifier "" (depthAlias tbl 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 tbl,
fromQi . removeSourceCTESchema schema <$> tbls,
joinConditions_)
(joins, selects) = foldr getQueryParts ([],[]) forest (joins, selects) = foldr getQueryParts ([],[]) forest
@@ -437,11 +426,9 @@ pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper
(find ((==) . toLower $ v) ["null","true","false"]) (find ((==) . toLower $ v) ["null","true","false"])
pgFmtJoinCondition :: JoinCondition -> SqlFragment pgFmtJoinCondition :: JoinCondition -> SqlFragment
pgFmtJoinCondition (JoinCondition (qi, al1, col1) (QualifiedIdentifier schema fTable, al2, col2)) = pgFmtJoinCondition (JoinCondition (qi, col1) (QualifiedIdentifier schema fTable, col2)) =
pgFmtColumn (fromMaybe qi $ aliasToQi al1) col1 <> " = " <> pgFmtColumn qi col1 <> " = " <>
pgFmtColumn (fromMaybe (removeSourceCTESchema schema fTable) $ aliasToQi al2) col2 pgFmtColumn (removeSourceCTESchema schema fTable) col2
where
aliasToQi al = QualifiedIdentifier "" <$> al
pgFmtLogicTree :: QualifiedIdentifier -> LogicTree -> SqlFragment pgFmtLogicTree :: QualifiedIdentifier -> LogicTree -> SqlFragment
pgFmtLogicTree qi (Expr hasNot op forest) = notOp <> " (" <> intercalate (" " <> show op <> " ") (pgFmtLogicTree qi <$> forest) <> ")" pgFmtLogicTree qi (Expr hasNot op forest) = notOp <> " (" <> intercalate (" " <> show op <> " ") (pgFmtLogicTree qi <$> forest) <> ")"
+8 -3
View File
@@ -184,6 +184,9 @@ data Relation = Relation {
, relLinkCols2 :: Maybe [Column] , relLinkCols2 :: Maybe [Column]
} deriving (Show, Eq) } deriving (Show, Eq)
isSelfJoin :: Relation -> Bool
isSelfJoin r = relType r /= Root && relTable r == relFTable r
-- | Cached attributes of a JSON payload -- | Cached attributes of a JSON payload
data PayloadJSON = PayloadJSON { data PayloadJSON = PayloadJSON {
-- | This is the raw ByteString that comes from the request body. -- | This is the raw ByteString that comes from the request body.
@@ -307,13 +310,15 @@ type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe RelationDetail)
-- | Path of the embedded levels, e.g "clients.projects.name=eq.." gives Path ["clients", "projects"] -- | Path of the embedded levels, e.g "clients.projects.name=eq.." gives Path ["clients", "projects"]
type EmbedPath = [Text] type EmbedPath = [Text]
data Filter = Filter { field::Field, opExpr::OpExpr } deriving (Show, Eq) data Filter = Filter { field::Field, opExpr::OpExpr } deriving (Show, Eq)
data JoinCondition = JoinCondition (QualifiedIdentifier, Maybe Alias, FieldName) data JoinCondition = JoinCondition (QualifiedIdentifier, FieldName)
(QualifiedIdentifier, Maybe Alias, FieldName) deriving (Show, Eq) (QualifiedIdentifier, FieldName) deriving (Show, Eq)
data ReadQuery = Select { data ReadQuery = Select {
select :: [SelectItem] select :: [SelectItem]
, from :: TableName , from :: TableName
-- | Only used for many to many joins. Parent and Child joins use explicit joins. -- | A table alias is used in case of self joins
, fromAlias :: Maybe Alias
-- | Only used for Many to Many joins. Parent and Child joins use explicit joins.
, implicitJoins :: [TableName] , implicitJoins :: [TableName]
, where_ :: [LogicTree] , where_ :: [LogicTree]
, joinConditions :: [JoinCondition] , joinConditions :: [JoinCondition]
+36
View File
@@ -589,6 +589,42 @@ spec = do
] ]
}]|] { matchHeaders = [matchContentTypeJson] } }]|] { matchHeaders = [matchContentTypeJson] }
it "embeds other relations(manager) besides the self reference" $ do
get "/organizations?select=name,manager(name),referee(name,manager(name),auditor(name,manager(name))),auditor(name,manager(name),referee(name,manager(name)))&id=eq.5" `shouldRespondWith`
[json|[{
"name":"Cyberdyne",
"manager":{"name":"Cyberdyne Manager"},
"referee":{
"name":"Acme",
"manager":{"name":"Acme Manager"},
"auditor":{
"name":"Auditor Org",
"manager":{"name":"Auditor Manager"}}},
"auditor":{
"name":"Umbrella",
"manager":{"name":"Umbrella Manager"},
"referee":{
"name":"Referee Org",
"manager":{"name":"Referee Manager"}}}
}]|] { matchHeaders = [matchContentTypeJson] }
get "/organizations?select=name,manager(name),auditees:organizations.auditor(name,manager(name),refereeds:organizations.referee(name,manager(name)))&id=eq.2" `shouldRespondWith`
[json|[{
"name":"Auditor Org",
"manager":{"name":"Auditor Manager"},
"auditees":[
{"name":"Acme",
"manager":{"name":"Acme Manager"},
"refereeds":[
{"name":"Cyberdyne",
"manager":{"name":"Cyberdyne Manager"}},
{"name":"Oscorp",
"manager":{"name":"Oscorp Manager"}}]},
{"name":"Umbrella",
"manager":{"name":"Umbrella Manager"},
"refereeds":[]}]
}]|] { matchHeaders = [matchContentTypeJson] }
describe "ordering response" $ do describe "ordering response" $ do
it "by a column asc" $ it "by a column asc" $
get "/items?id=lte.2&order=id.asc" get "/items?id=lte.2&order=id.asc"
+14 -4
View File
@@ -354,11 +354,21 @@ INSERT INTO family_tree VALUES ('3', 'Kid Two', '1');
INSERT INTO family_tree VALUES ('4', 'Grandkid One', '2'); INSERT INTO family_tree VALUES ('4', 'Grandkid One', '2');
INSERT INTO family_tree VALUES ('5', 'Grandkid Two', '3'); INSERT INTO family_tree VALUES ('5', 'Grandkid Two', '3');
TRUNCATE TABLE managers CASCADE;
INSERT INTO managers VALUES (1, 'Referee Manager');
INSERT INTO managers VALUES (2, 'Auditor Manager');
INSERT INTO managers VALUES (3, 'Acme Manager');
INSERT INTO managers VALUES (4, 'Umbrella Manager');
INSERT INTO managers VALUES (5, 'Cyberdyne Manager');
INSERT INTO managers VALUES (6, 'Oscorp Manager');
TRUNCATE TABLE organizations CASCADE; TRUNCATE TABLE organizations CASCADE;
INSERT INTO organizations VALUES (1, 'Referee Org', null, null); INSERT INTO organizations VALUES (1, 'Referee Org', null, null, 1);
INSERT INTO organizations VALUES (2, 'Auditor Org', null, null); INSERT INTO organizations VALUES (2, 'Auditor Org', null, null, 2);
INSERT INTO organizations VALUES (3, 'Acme', 1, 2); INSERT INTO organizations VALUES (3, 'Acme', 1, 2, 3);
INSERT INTO organizations VALUES (4, 'Umbrella', 1, 2); INSERT INTO organizations VALUES (4, 'Umbrella', 1, 2, 4);
INSERT INTO organizations VALUES (5, 'Cyberdyne', 3, 4, 5);
INSERT INTO organizations VALUES (6, 'Oscorp', 3, 4, 6);
SET search_path = private, pg_catalog; SET search_path = private, pg_catalog;
+1
View File
@@ -68,6 +68,7 @@ GRANT ALL ON TABLE
, tiobe_pls , tiobe_pls
, only_pk , only_pk
, family_tree , family_tree
, managers
, organizations , organizations
, authors , authors
, books , books
+9 -3
View File
@@ -1115,8 +1115,8 @@ CREATE FUNCTION setprojects(id_l int, id_h int, name text) RETURNS SETOF project
$_$; $_$;
create table images ( create table images (
name text not null, name text not null,
img bytea not null img bytea not null
); );
create view images_base64 as ( create view images_base64 as (
@@ -1386,11 +1386,17 @@ create table test.family_tree (
); );
alter table only test.family_tree add constraint pptr foreign key (parent) references test.family_tree(id); alter table only test.family_tree add constraint pptr foreign key (parent) references test.family_tree(id);
create table test.managers (
id integer primary key,
name text
);
create table test.organizations ( create table test.organizations (
id integer primary key, id integer primary key,
name text, name text,
referee integer, referee integer,
auditor integer auditor integer,
manager_id integer references managers(id)
); );
alter table only test.organizations add constraint pptr1 foreign key (referee) references test.organizations(id); alter table only test.organizations add constraint pptr1 foreign key (referee) references test.organizations(id);
alter table only test.organizations add constraint pptr2 foreign key (auditor) references test.organizations(id); alter table only test.organizations add constraint pptr2 foreign key (auditor) references test.organizations(id);