refactor: improve disambiguation error message
* reverse backwards relationships * remove redundancy from getJoinSelects * properly name Cardinality constructors
This commit is contained in:
committed by
Steve Chavez
parent
4ef6926791
commit
f9c64d9f65
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user