refactor: improve disambiguation error message

* reverse backwards relationships
* remove redundancy from getJoinSelects
* properly name Cardinality constructors
This commit is contained in:
steve-chavez
2019-11-17 13:28:21 -05:00
committed by Steve Chavez
parent 4ef6926791
commit f9c64d9f65
6 changed files with 186 additions and 186 deletions
+51 -49
View File
@@ -62,9 +62,9 @@ rootWithRelations schema rootTableName allRels action = case action of
-- To enable embedding in the sourceCTEName cases we need to replace the foreign key tableName in the Relation
-- with {sourceCTEName}. This way findRelation can find Relations with sourceCTEName.
toSourceRelation :: Relation -> Maybe Relation
toSourceRelation r@Relation{relFTable=ft}
| rootTableName == tableName ft = Just $ r {relFTable=ft {tableName=sourceCTEName}}
| otherwise = Nothing
toSourceRelation r@Relation{relTable=t}
| rootTableName == tableName t = Just $ r {relTable=t {tableName=sourceCTEName}}
| otherwise = Nothing
-- Build the initial 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 http://github.com/PostgREST/postgrest/issues/987.
@@ -102,21 +102,22 @@ addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Eith
addRelations schema allRelations parentNode (Node (query@Select{from=tbl}, (nodeName, _, alias, relationDetail, depth)) forest) =
case parentNode of
Just (Node (Select{from=parentNodeQi}, _) _) ->
let newFrom r = if qiName tbl == nodeName then tableQi (relTable r) else tbl
let newFrom r = if qiName tbl == nodeName then tableQi (relFTable r) else tbl
newReadNode = (\r -> (query{from=newFrom r}, (nodeName, Just r, alias, Nothing, depth))) <$> rel
parentNodeTable = qiName parentNodeQi
results = findRelation schema allRelations nodeName parentNodeTable relationDetail
results = findRelation schema allRelations parentNodeTable nodeName relationDetail
rel :: Either ApiRequestError Relation
rel = case results of
[] -> Left $ NoRelBetween parentNodeTable nodeName
[r] -> Right r
rs ->
-- Temporary hack for handling a self reference relationship. In this case we get a parent and child rel with the same relTable/relFtable.
-- We output the child rel, the parent can be obtained by using the fk column as an embed hint.
-- Hack for handling a self reference relationship.
-- In this case we get an O2M and M2O rels with the same relTable and relFtable.
-- We output the O2M rel, the M2O rel can be obtained by using the fk column as an embed hint in findRelation.
let rel0 = head rs
rel1 = rs !! 1 in
if length rs == 2 && relTable rel0 == relTable rel1 && relFTable rel0 == relFTable rel1
then note (NoRelBetween parentNodeTable nodeName) (find (\r -> relType r == Child) rs)
then note (NoRelBetween parentNodeTable nodeName) (find (\r -> relType r == O2M) rs)
else Left $ AmbiguousRelBetween parentNodeTable nodeName rs
in
Node <$> newReadNode <*> (updateForest . hush $ Node <$> newReadNode <*> pure forest)
@@ -127,8 +128,8 @@ addRelations schema allRelations parentNode (Node (query@Select{from=tbl}, (node
updateForest :: Maybe ReadRequest -> Either ApiRequestError [ReadRequest]
updateForest rq = mapM (addRelations schema allRelations rq) forest
findRelation :: Schema -> [Relation] -> NodeName -> TableName -> Maybe RelationDetail -> [Relation]
findRelation schema allRelations nodeTableName parentNodeTableName relationDetail =
findRelation :: Schema -> [Relation] -> TableName -> NodeName -> Maybe RelationDetail -> [Relation]
findRelation schema allRelations parentTableName nodeName relationDetail =
filter (\Relation{relTable, relColumns, relFTable, relFColumns, relType, relLinkTable} ->
-- Both relation ends need to be on the exposed schema
schema == tableSchema relTable && schema == tableSchema relFTable &&
@@ -137,29 +138,30 @@ findRelation schema allRelations nodeTableName parentNodeTableName relationDetai
-- (request) => projects { ..., clients{...} }
-- will match
-- (relation type) => parent
-- (relation type) => M2O
-- (entity) => clients {id}
-- (foriegn entity) => projects {client_id}
(
nodeTableName == tableName relTable && -- match relation table name
parentNodeTableName == tableName relFTable -- match relation foreign table name
parentTableName == tableName relTable && -- projects
nodeName == tableName relFTable -- clients
) ||
-- (request) => projects { ..., client_id{...} }
-- will match
-- (relation type) => parent
-- (relation type) => M2O
-- (entity) => clients {id}
-- (foriegn entity) => projects {client_id}
(
parentNodeTableName == tableName relFTable &&
length relFColumns == 1 &&
parentTableName == tableName relTable && -- projects
length relColumns == 1 &&
-- match common foreign key names(table_name_id, table_name_fk) to table_name
(toS ("^" <> colName (unsafeHead relFColumns) <> "_?(?:|[iI][dD]|[fF][kK])$") :: BS.ByteString) =~ (toS nodeTableName :: BS.ByteString)
(toS ("^" <> colName (unsafeHead relColumns) <> "_?(?:|[iI][dD]|[fF][kK])$") :: BS.ByteString) =~
(toS nodeName :: BS.ByteString) -- client_id
)
-- (request) => project_id { ..., client_id{...} }
-- will match
-- (relation type) => parent
-- (relation type) => M2O
-- (entity) => clients {id}
-- (foriegn entity) => projects {client_id}
-- this case works becasue before reaching this place
@@ -167,42 +169,42 @@ findRelation schema allRelations nodeTableName parentNodeTableName relationDetai
Just rd ->
-- (request) => clients { ..., projects.client_id{...} }
-- (request) => clients { ..., projects!client_id{...} }
-- will match
-- (relation type) => child
-- (relation type) => O2M
-- (entity) => clients {id}
-- (foriegn entity) => projects {client_id}
(
relType == Child &&
nodeTableName == tableName relTable && -- match relation table name
parentNodeTableName == tableName relFTable && -- match relation foreign table name
length relColumns == 1 &&
rd == colName (unsafeHead relColumns)
relType == O2M &&
parentTableName == tableName relTable && -- clients
nodeName == tableName relFTable && -- projects
length relFColumns == 1 &&
rd == colName (unsafeHead relFColumns) -- rd is client_id
) ||
-- (request) => message { ..., person_detail.sender{...} }
-- (request) => message { ..., person_detail!sender{...} }
-- will match
-- (relation type) => parent
-- (relation type) => M2O
-- (entity) => message {sender}
-- (foriegn entity) => person_detail {id}
(
relType == Parent &&
nodeTableName == tableName relTable && -- match relation table name
parentNodeTableName == tableName relFTable && -- match relation foreign table name
length relFColumns == 1 &&
rd == colName (unsafeHead relFColumns)
relType == M2O &&
parentTableName == tableName relTable && -- message
nodeName == tableName relFTable && -- person_detail
length relColumns == 1 &&
rd == colName (unsafeHead relColumns) -- rd is sender
) ||
-- (request) => tasks { ..., users.tasks_users{...} }
-- will match
-- (relation type) => many
-- (relation type) => M2M
-- (entity) => users
-- (foriegn entity) => tasks
(
relType == Many &&
nodeTableName == tableName relTable && -- match relation table name
parentNodeTableName == tableName relFTable && -- match relation foreign table name
rd == tableName (fromJust relLinkTable)
relType == M2M &&
parentTableName == tableName relTable && -- tasks
nodeName == tableName relFTable && -- users
rd == tableName (fromJust relLinkTable) -- rd is tasks_users
)
) allRelations
@@ -210,9 +212,9 @@ findRelation schema allRelations nodeTableName parentNodeTableName relationDetai
addJoinConditions :: Maybe Alias -> ReadRequest -> Either ApiRequestError ReadRequest
addJoinConditions previousAlias (Node node@(query@Select{from=tbl}, nodeProps@(_, relation, _, _, depth)) forest) =
case relation of
Just rel@Relation{relType=Parent} -> Node (augmentQuery rel, nodeProps) <$> updatedForest
Just rel@Relation{relType=Child} -> Node (augmentQuery rel, nodeProps) <$> updatedForest
Just rel@Relation{relType=Many, relLinkTable=lTable} ->
Just rel@Relation{relType=O2M} -> Node (augmentQuery rel, nodeProps) <$> updatedForest
Just rel@Relation{relType=M2O} -> Node (augmentQuery rel, nodeProps) <$> updatedForest
Just rel@Relation{relType=M2M, relLinkTable=lTable} ->
case lTable of
Just linkTable ->
let rq = augmentQuery rel in
@@ -237,11 +239,11 @@ addJoinConditions previousAlias (Node node@(query@Select{from=tbl}, nodeProps@(_
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) =
case typ of
Child ->
O2M ->
zipWith (toJoinCondition tN ftN) cols fCols
Parent ->
M2O ->
zipWith (toJoinCondition tN ftN) cols fCols
Many ->
M2M ->
let ltN = maybe "" tableName lt in
zipWith (toJoinCondition tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toJoinCondition ftN ltN) fCols (fromMaybe [] lc2)
where
@@ -249,8 +251,8 @@ getJoinConditions previousAlias newAlias (Relation Table{tableSchema=tSchema, ta
toJoinCondition tb ftb c fc =
let qi1 = removeSourceCTESchema tSchema tb
qi2 = removeSourceCTESchema tSchema ftb in
JoinCondition (maybe qi1 (QualifiedIdentifier mempty) newAlias, colName c)
(maybe qi2 (QualifiedIdentifier mempty) previousAlias, colName fc)
JoinCondition (maybe qi1 (QualifiedIdentifier mempty) previousAlias, colName c)
(maybe qi2 (QualifiedIdentifier mempty) newAlias, colName fc)
-- On mutation and calling proc cases we wrap the target table in a WITH {sourceCTEName}
-- if this happens remove the schema `FROM "schema"."{sourceCTEName}"` and use only the
@@ -361,10 +363,10 @@ returningCols rr@(Node _ forest) = returnings
-- So this adds the foreign key columns to ensure the embedding succeeds, result would be `RETURNING name, client_id`.
-- This also works for the other relType's.
fkCols = concat $ mapMaybe (\case
Node (_, (_, Just Relation{relFColumns=cols, relType=relTyp}, _, _, _)) _ -> case relTyp of
Parent -> Just cols
Child -> Just cols
Many -> Just cols
Node (_, (_, Just Relation{relColumns=cols, relType=relTyp}, _, _, _)) _ -> case relTyp of
O2M -> Just cols
M2O -> Just cols
M2M -> Just cols
_ -> Nothing
) forest
-- However if the "client_id" is present, e.g. mutateRequest to /projects?select=client_id,name,clients(name)
+39 -39
View File
@@ -47,14 +47,14 @@ import Protolude
getDbStructure :: Schema -> PgVersion -> HT.Transaction DbStructure
getDbStructure schema pgVer = do
HT.sql "set local schema ''" -- for getting the fully qualified name(schema.name) of every db object
tabs <- HT.statement () allTables
cols <- HT.statement schema $ allColumns tabs
srcCols <- HT.statement schema $ allSourceColumns cols pgVer
childRels <- HT.statement () $ allChildRelations tabs cols
keys <- HT.statement () $ allPrimaryKeys tabs
procs <- HT.statement schema allProcs
tabs <- HT.statement () allTables
cols <- HT.statement schema $ allColumns tabs
srcCols <- HT.statement schema $ allSourceColumns cols pgVer
m2oRels <- HT.statement () $ allM2ORels tabs cols
keys <- HT.statement () $ allPrimaryKeys tabs
procs <- HT.statement schema allProcs
let rels = addManyToManyRelations . addParentRelations $ addViewChildRelations srcCols childRels
let rels = addM2MRels . addO2MRels $ addViewM2ORels srcCols m2oRels
cols' = addForeignKeys rels cols
keys' = addViewPrimaryKeys srcCols keys
@@ -91,9 +91,9 @@ decodeColumns tables =
<*> nullableColumn HD.text
<*> nullableColumn HD.text
decodeRelations :: [Table] -> [Column] -> HD.Result [Relation]
decodeRelations tables cols =
mapMaybe (relationFromRow tables cols) <$> HD.rowList relRow
decodeRels :: [Table] -> [Column] -> HD.Result [Relation]
decodeRels tables cols =
mapMaybe (relFromRow tables cols) <$> HD.rowList relRow
where
relRow = (,,,,,)
<$> column HD.text
@@ -250,20 +250,20 @@ addForeignKeys rels = map addFk
addFk col = col { colFK = fk col }
fk col = find (lookupFn col) rels >>= relToFk col
lookupFn :: Column -> Relation -> Bool
lookupFn c Relation{relColumns=cs, relType=rty} = c `elem` cs && rty==Child
lookupFn c Relation{relColumns=cs, relType=rty} = c `elem` cs && rty==M2O
relToFk col Relation{relColumns=cols, relFColumns=colsF} = do
pos <- L.elemIndex col cols
colF <- atMay colsF pos
return $ ForeignKey colF
{-
Adds Views Child Relations based on SourceColumns found, the logic is as follows:
Adds Views M2O Relations based on SourceColumns found, the logic is as follows:
Having a Relation{relTable=t1, relColumns=[c1], relFTable=t2, relFColumns=[c2], relType=Child} represented by:
Having a Relation{relTable=t1, relColumns=[c1], relFTable=t2, relFColumns=[c2], relType=M2O} represented by:
t1.c1------t2.c2
When only having a t1_view.c1 source column, we need to add a View-Table Child Relation
When only having a t1_view.c1 source column, we need to add a View-Table M2O Relation
t1.c1----t2.c2 t1.c1----------t2.c2
-> ________/
@@ -271,14 +271,14 @@ When only having a t1_view.c1 source column, we need to add a View-Table Child R
t1_view.c1 t1_view.c1
When only having a t2_view.c2 source column, we need to add a Table-View Child Relation
When only having a t2_view.c2 source column, we need to add a Table-View M2O Relation
t1.c1----t2.c2 t1.c1----------t2.c2
-> \________
\
t2_view.c2 t2_view.c1
When having t1_view.c1 and a t2_view.c2 source columns, we need to add a View-View Child Relation in addition to the prior
When having t1_view.c1 and a t2_view.c2 source columns, we need to add a View-View M2O Relation in addition to the prior
t1.c1----t2.c2 t1.c1----------t2.c2
-> \________/
@@ -287,10 +287,10 @@ When having t1_view.c1 and a t2_view.c2 source columns, we need to add a View-Vi
The logic for composite pks is similar just need to make sure all the Relation columns have source columns.
-}
addViewChildRelations :: [SourceColumn] -> [Relation] -> [Relation]
addViewChildRelations allSrcCols = concatMap (\rel ->
addViewM2ORels :: [SourceColumn] -> [Relation] -> [Relation]
addViewM2ORels allSrcCols = concatMap (\rel ->
rel : case rel of
Relation{relType=Child, relTable, relColumns, relFTable, relFColumns} ->
Relation{relType=M2O, relTable, relColumns, relFTable, relFColumns} ->
let srcColsGroupedByView :: [Column] -> [[SourceColumn]]
srcColsGroupedByView relCols = L.groupBy (\(_, viewCol1) (_, viewCol2) -> colTable viewCol1 == colTable viewCol2) $
@@ -305,36 +305,36 @@ addViewChildRelations allSrcCols = concatMap (\rel ->
-- TODO: This could be avoided if the Relation type is improved with a structure that maintains the association of relColumns and relFColumns
srcCols `sortAccordingTo` cols = sortOn (\(k, _) -> L.lookup k $ zip cols [0::Int ..]) srcCols
viewTableChild =
viewTableM2O =
[ Relation (getView srcCols) (snd <$> srcCols `sortAccordingTo` relColumns)
relFTable relFColumns
Child Nothing Nothing Nothing
M2O Nothing Nothing Nothing
| srcCols <- relSrcCols, srcCols `allSrcColsOf` relColumns ]
tableViewChild =
tableViewM2O =
[ Relation relTable relColumns
(getView fSrcCols) (snd <$> fSrcCols `sortAccordingTo` relFColumns)
Child Nothing Nothing Nothing
M2O Nothing Nothing Nothing
| fSrcCols <- relFSrcCols, fSrcCols `allSrcColsOf` relFColumns ]
viewViewChild =
viewViewM2O =
[ Relation (getView srcCols) (snd <$> srcCols `sortAccordingTo` relColumns)
(getView fSrcCols) (snd <$> fSrcCols `sortAccordingTo` relFColumns)
Child Nothing Nothing Nothing
M2O Nothing Nothing Nothing
| srcCols <- relSrcCols, srcCols `allSrcColsOf` relColumns
, fSrcCols <- relFSrcCols, fSrcCols `allSrcColsOf` relFColumns ]
in viewTableChild ++ tableViewChild ++ viewViewChild
in viewTableM2O ++ tableViewM2O ++ viewViewM2O
_ -> [])
addParentRelations :: [Relation] -> [Relation]
addParentRelations = concatMap (\rel@(Relation t c ft fc _ _ _ _) -> [rel, Relation ft fc t c Parent Nothing Nothing Nothing])
addO2MRels :: [Relation] -> [Relation]
addO2MRels = concatMap (\rel@(Relation t c ft fc _ _ _ _) -> [rel, Relation ft fc t c O2M Nothing Nothing Nothing])
addManyToManyRelations :: [Relation] -> [Relation]
addManyToManyRelations rels = rels ++ addMirrorRelation (mapMaybe link2Relation links)
addM2MRels :: [Relation] -> [Relation]
addM2MRels rels = rels ++ addMirrorRelation (mapMaybe link2Relation links)
where
links = join $ map (combinations 2) $ filter (not . null) $ groupWith groupFn $ filter ( (==Child). relType) rels
links = join $ map (combinations 2) $ filter (not . null) $ groupWith groupFn $ filter ( (==M2O). relType) rels
groupFn :: Relation -> Text
groupFn Relation{relTable=Table{tableSchema=s, tableName=t}} = s <> "_" <> t
-- Reference : https://wiki.haskell.org/99_questions/Solutions/26
@@ -342,12 +342,12 @@ addManyToManyRelations rels = rels ++ addMirrorRelation (mapMaybe link2Relation
combinations 0 _ = [ [] ]
combinations n xs = [ y:ys | y:xs' <- tails xs
, ys <- combinations (n-1) xs']
addMirrorRelation = concatMap (\rel@(Relation t c ft fc _ lt lc1 lc2) -> [rel, Relation ft fc t c Many lt lc2 lc1])
addMirrorRelation = concatMap (\rel@(Relation t c ft fc _ lt lc1 lc2) -> [rel, Relation ft fc t c M2M lt lc2 lc1])
link2Relation [
Relation{relTable=lt, relColumns=lc1, relFTable=t, relFColumns=c},
Relation{ relColumns=lc2, relFTable=ft, relFColumns=fc}
]
| lc1 /= lc2 && length lc1 == 1 && length lc2 == 1 = Just $ Relation t c ft fc Many (Just lt) (Just lc1) (Just lc2)
| lc1 /= lc2 && length lc1 == 1 && length lc2 == 1 = Just $ Relation t c ft fc M2M (Just lt) (Just lc1) (Just lc2)
| otherwise = Nothing
link2Relation _ = Nothing
@@ -567,9 +567,9 @@ columnFromRow tabs (s, t, n, desc, pos, nul, typ, u, l, p, d, e) = buildColumn <
parseEnum :: Maybe Text -> [Text]
parseEnum = maybe [] (split (==','))
allChildRelations :: [Table] -> [Column] -> H.Statement () [Relation]
allChildRelations tabs cols =
H.Statement sql HE.noParams (decodeRelations tabs cols) True
allM2ORels :: [Table] -> [Column] -> H.Statement () [Relation]
allM2ORels tabs cols =
H.Statement sql HE.noParams (decodeRels tabs cols) True
where
sql = [q|
SELECT ns1.nspname AS table_schema,
@@ -597,9 +597,9 @@ allChildRelations tabs cols =
WHERE confrelid != 0
ORDER BY (conrelid, column_info.nums) |]
relationFromRow :: [Table] -> [Column] -> (Text, Text, [Text], Text, Text, [Text]) -> Maybe Relation
relationFromRow allTabs allCols (rs, rt, rcs, frs, frt, frcs) =
Relation <$> table <*> cols <*> tableF <*> colsF <*> pure Child <*> pure Nothing <*> pure Nothing <*> pure Nothing
relFromRow :: [Table] -> [Column] -> (Text, Text, [Text], Text, Text, [Text]) -> Maybe Relation
relFromRow allTabs allCols (rs, rt, rcs, frs, frt, frcs) =
Relation <$> table <*> cols <*> tableF <*> colsF <*> pure M2O <*> pure Nothing <*> pure Nothing <*> pure Nothing
where
findTable s t = find (\tbl -> tableSchema tbl == s && tableName tbl == t) allTabs
findCol s t c = find (\col -> tableSchema (colTable col) == s && tableName (colTable col) == t && colName col == c) allCols
+1 -1
View File
@@ -107,7 +107,7 @@ compressedRel rel =
, "target" .= fmt (tableSchema fTab) (tableName fTab) (colName <$> relFColumns rel)
, "cardinality" .= (show $ relType rel :: Text)
] ++
if relType rel == Many
if relType rel == M2M
then [
"junction" .= case (relLinkTable rel, relLinkCols1 rel, relLinkCols2 rel) of
(Just lt, Just lc1, Just lc2) -> fmtMany (tableSchema lt) (tableName lt) (colName <$> lc1) (colName <$> lc2)
+2 -8
View File
@@ -55,19 +55,13 @@ getJoinsSelects :: ReadRequest -> ([SqlFragment], [SqlFragment]) -> ([SqlFragmen
getJoinsSelects rr@(Node (_, (name, Just Relation{relType=relTyp,relTable=Table{tableName=table}}, alias, _, _)) _) (j,s) =
let subquery = readRequestToQuery rr in
case relTyp of
Child ->
let sel = "COALESCE(("
<> "SELECT json_agg(" <> pgFmtIdent table <> ".*) "
<> "FROM (" <> subquery <> ") " <> pgFmtIdent table
<> "), '[]') AS " <> pgFmtIdent (fromMaybe name alias) in
(j, sel:s)
Parent ->
M2O ->
let aliasOrName = fromMaybe name alias
localTableName = pgFmtIdent $ table <> "_" <> aliasOrName
sel = "row_to_json(" <> localTableName <> ".*) AS " <> pgFmtIdent aliasOrName
joi = " LEFT JOIN LATERAL( " <> subquery <> " ) AS " <> localTableName <> " ON TRUE " in
(joi:j,sel:s)
Many ->
_ ->
let sel = "COALESCE (("
<> "SELECT json_agg(" <> pgFmtIdent table <> ".*) "
<> "FROM (" <> subquery <> ") " <> pgFmtIdent table
+8 -4
View File
@@ -259,10 +259,14 @@ data QualifiedIdentifier = QualifiedIdentifier {
-- | The relationship [cardinality](https://en.wikipedia.org/wiki/Cardinality_(data_modeling)).
-- | TODO: missing one-to-one
data Cardinality = Child -- ^ a.k.a. many-to-one
| Parent -- ^ a.k.a. one-to-many
| Many -- ^ a.k.a. many-to-many
deriving (Show, Eq)
data Cardinality = O2M -- ^ one-to-many, previously known as Parent
| M2O -- ^ many-to-one, previously known as Child
| M2M -- ^ many-to-many, previously known as Many
deriving Eq
instance Show Cardinality where
show O2M = "one-to-many"
show M2O = "many-to-one"
show M2M = "many-to-many"
{-|
The name 'Relation' here is used with the meaning
+85 -85
View File
@@ -20,14 +20,14 @@ spec =
{
"details": [
{
"cardinality": "Parent",
"source": "test.person[id]",
"target": "test.message[sender]"
"cardinality": "many-to-one",
"source": "test.message[sender]",
"target": "test.person[id]"
},
{
"cardinality": "Parent",
"source": "test.person_detail[id]",
"target": "test.message[sender]"
"cardinality": "many-to-one",
"source": "test.message[sender]",
"target": "test.person_detail[id]"
}
],
"hint": "Disambiguate by choosing a relationship from the `details` key",
@@ -42,85 +42,85 @@ spec =
[json|
{
"details": [
{
"cardinality": "Child",
"source": "test.articleStars[userId]",
"target": "test.users[id]"
},
{
"cardinality": "Child",
"source": "test.limited_article_stars[user_id]",
"target": "test.users[id]"
},
{
"cardinality": "Child",
"source": "test.comments[commenter_id]",
"target": "test.users[id]"
},
{
"cardinality": "Child",
"source": "test.users_projects[user_id]",
"target": "test.users[id]"
},
{
"cardinality": "Child",
"source": "test.users_tasks[user_id]",
"target": "test.users[id]"
},
{
"cardinality": "Many",
"junction": "private.article_stars[article_id][user_id]",
"source": "test.articles[id]",
"target": "test.users[id]"
},
{
"cardinality": "Many",
"junction": "test.articleStars[articleId][userId]",
"source": "test.articles[id]",
"target": "test.users[id]"
},
{
"cardinality": "Many",
"junction": "test.limited_article_stars[article_id][user_id]",
"source": "test.articles[id]",
"target": "test.users[id]"
},
{
"cardinality": "Many",
"junction": "test.users_projects[project_id][user_id]",
"source": "test.projects[id]",
"target": "test.users[id]"
},
{
"cardinality": "Many",
"junction": "test.users_projects[project_id][user_id]",
"source": "test.materialized_projects[id]",
"target": "test.users[id]"
},
{
"cardinality": "Many",
"junction": "test.users_projects[project_id][user_id]",
"source": "test.projects_view[id]",
"target": "test.users[id]"
},
{
"cardinality": "Many",
"junction": "test.users_projects[project_id][user_id]",
"source": "test.projects_view_alt[t_id]",
"target": "test.users[id]"
},
{
"cardinality": "Many",
"junction": "test.users_tasks[task_id][user_id]",
"source": "test.tasks[id]",
"target": "test.users[id]"
},
{
"cardinality": "Many",
"junction": "test.users_tasks[task_id][user_id]",
"source": "test.filtered_tasks[myId]",
"target": "test.users[id]"
}
{
"cardinality": "one-to-many",
"source": "test.users[id]",
"target": "test.articleStars[userId]"
},
{
"cardinality": "one-to-many",
"source": "test.users[id]",
"target": "test.limited_article_stars[user_id]"
},
{
"cardinality": "one-to-many",
"source": "test.users[id]",
"target": "test.comments[commenter_id]"
},
{
"cardinality": "one-to-many",
"source": "test.users[id]",
"target": "test.users_projects[user_id]"
},
{
"cardinality": "one-to-many",
"source": "test.users[id]",
"target": "test.users_tasks[user_id]"
},
{
"cardinality": "many-to-many",
"junction": "private.article_stars[user_id][article_id]",
"source": "test.users[id]",
"target": "test.articles[id]"
},
{
"cardinality": "many-to-many",
"junction": "test.articleStars[userId][articleId]",
"source": "test.users[id]",
"target": "test.articles[id]"
},
{
"cardinality": "many-to-many",
"junction": "test.limited_article_stars[user_id][article_id]",
"source": "test.users[id]",
"target": "test.articles[id]"
},
{
"cardinality": "many-to-many",
"junction": "test.users_projects[user_id][project_id]",
"source": "test.users[id]",
"target": "test.projects[id]"
},
{
"cardinality": "many-to-many",
"junction": "test.users_projects[user_id][project_id]",
"source": "test.users[id]",
"target": "test.materialized_projects[id]"
},
{
"cardinality": "many-to-many",
"junction": "test.users_projects[user_id][project_id]",
"source": "test.users[id]",
"target": "test.projects_view[id]"
},
{
"cardinality": "many-to-many",
"junction": "test.users_projects[user_id][project_id]",
"source": "test.users[id]",
"target": "test.projects_view_alt[t_id]"
},
{
"cardinality": "many-to-many",
"junction": "test.users_tasks[user_id][task_id]",
"source": "test.users[id]",
"target": "test.tasks[id]"
},
{
"cardinality": "many-to-many",
"junction": "test.users_tasks[user_id][task_id]",
"source": "test.users[id]",
"target": "test.filtered_tasks[myId]"
}
],
"hint": "Disambiguate by choosing a relationship from the `details` key",
"message": "More than one relationship was found for users and id"