diff --git a/src/PostgREST/DbRequestBuilder.hs b/src/PostgREST/DbRequestBuilder.hs index 5e68d4d402..90f3e58972 100644 --- a/src/PostgREST/DbRequestBuilder.hs +++ b/src/PostgREST/DbRequestBuilder.hs @@ -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) diff --git a/src/PostgREST/DbStructure.hs b/src/PostgREST/DbStructure.hs index 45cc589d8e..5c2e486c4f 100644 --- a/src/PostgREST/DbStructure.hs +++ b/src/PostgREST/DbStructure.hs @@ -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 - - let rels = addManyToManyRelations . addParentRelations $ addViewChildRelations srcCols childRels + 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 = 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 diff --git a/src/PostgREST/Error.hs b/src/PostgREST/Error.hs index 56fab98d64..8148b0c0b8 100644 --- a/src/PostgREST/Error.hs +++ b/src/PostgREST/Error.hs @@ -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) diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index cfb3b649c8..3d958d2d2f 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -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 diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index 79a38c8498..bc5ba18fb7 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -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 diff --git a/test/Feature/EmbedDisambiguationSpec.hs b/test/Feature/EmbedDisambiguationSpec.hs index 54eabcf251..9c6299bc1f 100644 --- a/test/Feature/EmbedDisambiguationSpec.hs +++ b/test/Feature/EmbedDisambiguationSpec.hs @@ -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"