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
|
-- 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.
|
-- with {sourceCTEName}. This way findRelation can find Relations with sourceCTEName.
|
||||||
toSourceRelation :: Relation -> Maybe Relation
|
toSourceRelation :: Relation -> Maybe Relation
|
||||||
toSourceRelation r@Relation{relFTable=ft}
|
toSourceRelation r@Relation{relTable=t}
|
||||||
| rootTableName == tableName ft = Just $ r {relFTable=ft {tableName=sourceCTEName}}
|
| rootTableName == tableName t = Just $ r {relTable=t {tableName=sourceCTEName}}
|
||||||
| otherwise = Nothing
|
| 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
|
-- 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.
|
-- 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) =
|
addRelations schema allRelations parentNode (Node (query@Select{from=tbl}, (nodeName, _, alias, relationDetail, depth)) forest) =
|
||||||
case parentNode of
|
case parentNode of
|
||||||
Just (Node (Select{from=parentNodeQi}, _) _) ->
|
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
|
newReadNode = (\r -> (query{from=newFrom r}, (nodeName, Just r, alias, Nothing, depth))) <$> rel
|
||||||
parentNodeTable = qiName parentNodeQi
|
parentNodeTable = qiName parentNodeQi
|
||||||
results = findRelation schema allRelations nodeName parentNodeTable relationDetail
|
results = findRelation schema allRelations parentNodeTable nodeName relationDetail
|
||||||
rel :: Either ApiRequestError Relation
|
rel :: Either ApiRequestError Relation
|
||||||
rel = case results of
|
rel = case results of
|
||||||
[] -> Left $ NoRelBetween parentNodeTable nodeName
|
[] -> Left $ NoRelBetween parentNodeTable nodeName
|
||||||
[r] -> Right r
|
[r] -> Right r
|
||||||
rs ->
|
rs ->
|
||||||
-- Temporary hack for handling a self reference relationship. In this case we get a parent and child rel with the same relTable/relFtable.
|
-- Hack for handling a self reference relationship.
|
||||||
-- We output the child rel, the parent can be obtained by using the fk column as an embed hint.
|
-- 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
|
let rel0 = head rs
|
||||||
rel1 = rs !! 1 in
|
rel1 = rs !! 1 in
|
||||||
if length rs == 2 && relTable rel0 == relTable rel1 && relFTable rel0 == relFTable rel1
|
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
|
else Left $ AmbiguousRelBetween parentNodeTable nodeName rs
|
||||||
in
|
in
|
||||||
Node <$> newReadNode <*> (updateForest . hush $ Node <$> newReadNode <*> pure forest)
|
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 :: Maybe ReadRequest -> Either ApiRequestError [ReadRequest]
|
||||||
updateForest rq = mapM (addRelations schema allRelations rq) forest
|
updateForest rq = mapM (addRelations schema allRelations rq) forest
|
||||||
|
|
||||||
findRelation :: Schema -> [Relation] -> NodeName -> TableName -> Maybe RelationDetail -> [Relation]
|
findRelation :: Schema -> [Relation] -> TableName -> NodeName -> Maybe RelationDetail -> [Relation]
|
||||||
findRelation schema allRelations nodeTableName parentNodeTableName relationDetail =
|
findRelation schema allRelations parentTableName nodeName relationDetail =
|
||||||
filter (\Relation{relTable, relColumns, relFTable, relFColumns, relType, relLinkTable} ->
|
filter (\Relation{relTable, relColumns, relFTable, relFColumns, relType, relLinkTable} ->
|
||||||
-- Both relation ends need to be on the exposed schema
|
-- Both relation ends need to be on the exposed schema
|
||||||
schema == tableSchema relTable && schema == tableSchema relFTable &&
|
schema == tableSchema relTable && schema == tableSchema relFTable &&
|
||||||
@@ -137,29 +138,30 @@ findRelation schema allRelations nodeTableName parentNodeTableName relationDetai
|
|||||||
|
|
||||||
-- (request) => projects { ..., clients{...} }
|
-- (request) => projects { ..., clients{...} }
|
||||||
-- will match
|
-- will match
|
||||||
-- (relation type) => parent
|
-- (relation type) => M2O
|
||||||
-- (entity) => clients {id}
|
-- (entity) => clients {id}
|
||||||
-- (foriegn entity) => projects {client_id}
|
-- (foriegn entity) => projects {client_id}
|
||||||
(
|
(
|
||||||
nodeTableName == tableName relTable && -- match relation table name
|
parentTableName == tableName relTable && -- projects
|
||||||
parentNodeTableName == tableName relFTable -- match relation foreign table name
|
nodeName == tableName relFTable -- clients
|
||||||
) ||
|
) ||
|
||||||
|
|
||||||
-- (request) => projects { ..., client_id{...} }
|
-- (request) => projects { ..., client_id{...} }
|
||||||
-- will match
|
-- will match
|
||||||
-- (relation type) => parent
|
-- (relation type) => M2O
|
||||||
-- (entity) => clients {id}
|
-- (entity) => clients {id}
|
||||||
-- (foriegn entity) => projects {client_id}
|
-- (foriegn entity) => projects {client_id}
|
||||||
(
|
(
|
||||||
parentNodeTableName == tableName relFTable &&
|
parentTableName == tableName relTable && -- projects
|
||||||
length relFColumns == 1 &&
|
length relColumns == 1 &&
|
||||||
-- match common foreign key names(table_name_id, table_name_fk) to table_name
|
-- 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{...} }
|
-- (request) => project_id { ..., client_id{...} }
|
||||||
-- will match
|
-- will match
|
||||||
-- (relation type) => parent
|
-- (relation type) => M2O
|
||||||
-- (entity) => clients {id}
|
-- (entity) => clients {id}
|
||||||
-- (foriegn entity) => projects {client_id}
|
-- (foriegn entity) => projects {client_id}
|
||||||
-- this case works becasue before reaching this place
|
-- this case works becasue before reaching this place
|
||||||
@@ -167,42 +169,42 @@ findRelation schema allRelations nodeTableName parentNodeTableName relationDetai
|
|||||||
|
|
||||||
Just rd ->
|
Just rd ->
|
||||||
|
|
||||||
-- (request) => clients { ..., projects.client_id{...} }
|
-- (request) => clients { ..., projects!client_id{...} }
|
||||||
-- will match
|
-- will match
|
||||||
-- (relation type) => child
|
-- (relation type) => O2M
|
||||||
-- (entity) => clients {id}
|
-- (entity) => clients {id}
|
||||||
-- (foriegn entity) => projects {client_id}
|
-- (foriegn entity) => projects {client_id}
|
||||||
(
|
(
|
||||||
relType == Child &&
|
relType == O2M &&
|
||||||
nodeTableName == tableName relTable && -- match relation table name
|
parentTableName == tableName relTable && -- clients
|
||||||
parentNodeTableName == tableName relFTable && -- match relation foreign table name
|
nodeName == tableName relFTable && -- projects
|
||||||
length relColumns == 1 &&
|
length relFColumns == 1 &&
|
||||||
rd == colName (unsafeHead relColumns)
|
rd == colName (unsafeHead relFColumns) -- rd is client_id
|
||||||
) ||
|
) ||
|
||||||
|
|
||||||
-- (request) => message { ..., person_detail.sender{...} }
|
-- (request) => message { ..., person_detail!sender{...} }
|
||||||
-- will match
|
-- will match
|
||||||
-- (relation type) => parent
|
-- (relation type) => M2O
|
||||||
-- (entity) => message {sender}
|
-- (entity) => message {sender}
|
||||||
-- (foriegn entity) => person_detail {id}
|
-- (foriegn entity) => person_detail {id}
|
||||||
(
|
(
|
||||||
relType == Parent &&
|
relType == M2O &&
|
||||||
nodeTableName == tableName relTable && -- match relation table name
|
parentTableName == tableName relTable && -- message
|
||||||
parentNodeTableName == tableName relFTable && -- match relation foreign table name
|
nodeName == tableName relFTable && -- person_detail
|
||||||
length relFColumns == 1 &&
|
length relColumns == 1 &&
|
||||||
rd == colName (unsafeHead relFColumns)
|
rd == colName (unsafeHead relColumns) -- rd is sender
|
||||||
) ||
|
) ||
|
||||||
|
|
||||||
-- (request) => tasks { ..., users.tasks_users{...} }
|
-- (request) => tasks { ..., users.tasks_users{...} }
|
||||||
-- will match
|
-- will match
|
||||||
-- (relation type) => many
|
-- (relation type) => M2M
|
||||||
-- (entity) => users
|
-- (entity) => users
|
||||||
-- (foriegn entity) => tasks
|
-- (foriegn entity) => tasks
|
||||||
(
|
(
|
||||||
relType == Many &&
|
relType == M2M &&
|
||||||
nodeTableName == tableName relTable && -- match relation table name
|
parentTableName == tableName relTable && -- tasks
|
||||||
parentNodeTableName == tableName relFTable && -- match relation foreign table name
|
nodeName == tableName relFTable && -- users
|
||||||
rd == tableName (fromJust relLinkTable)
|
rd == tableName (fromJust relLinkTable) -- rd is tasks_users
|
||||||
)
|
)
|
||||||
) allRelations
|
) allRelations
|
||||||
|
|
||||||
@@ -210,9 +212,9 @@ findRelation schema allRelations nodeTableName parentNodeTableName relationDetai
|
|||||||
addJoinConditions :: Maybe Alias -> ReadRequest -> Either ApiRequestError ReadRequest
|
addJoinConditions :: Maybe Alias -> ReadRequest -> Either ApiRequestError ReadRequest
|
||||||
addJoinConditions previousAlias (Node node@(query@Select{from=tbl}, nodeProps@(_, relation, _, _, depth)) forest) =
|
addJoinConditions previousAlias (Node node@(query@Select{from=tbl}, nodeProps@(_, relation, _, _, depth)) forest) =
|
||||||
case relation of
|
case relation of
|
||||||
Just rel@Relation{relType=Parent} -> Node (augmentQuery rel, nodeProps) <$> updatedForest
|
Just rel@Relation{relType=O2M} -> Node (augmentQuery rel, nodeProps) <$> updatedForest
|
||||||
Just rel@Relation{relType=Child} -> Node (augmentQuery rel, nodeProps) <$> updatedForest
|
Just rel@Relation{relType=M2O} -> Node (augmentQuery rel, nodeProps) <$> updatedForest
|
||||||
Just rel@Relation{relType=Many, relLinkTable=lTable} ->
|
Just rel@Relation{relType=M2M, relLinkTable=lTable} ->
|
||||||
case lTable of
|
case lTable of
|
||||||
Just linkTable ->
|
Just linkTable ->
|
||||||
let rq = augmentQuery rel in
|
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 :: Maybe Alias -> Maybe Alias -> Relation -> [JoinCondition]
|
||||||
getJoinConditions previousAlias newAlias (Relation Table{tableSchema=tSchema, tableName=tN} cols Table{tableName=ftN} fCols typ lt lc1 lc2) =
|
getJoinConditions previousAlias newAlias (Relation Table{tableSchema=tSchema, tableName=tN} cols Table{tableName=ftN} fCols typ lt lc1 lc2) =
|
||||||
case typ of
|
case typ of
|
||||||
Child ->
|
O2M ->
|
||||||
zipWith (toJoinCondition tN ftN) cols fCols
|
zipWith (toJoinCondition tN ftN) cols fCols
|
||||||
Parent ->
|
M2O ->
|
||||||
zipWith (toJoinCondition tN ftN) cols fCols
|
zipWith (toJoinCondition tN ftN) cols fCols
|
||||||
Many ->
|
M2M ->
|
||||||
let ltN = maybe "" tableName lt in
|
let ltN = maybe "" tableName lt in
|
||||||
zipWith (toJoinCondition tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toJoinCondition ftN ltN) fCols (fromMaybe [] lc2)
|
zipWith (toJoinCondition tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toJoinCondition ftN ltN) fCols (fromMaybe [] lc2)
|
||||||
where
|
where
|
||||||
@@ -249,8 +251,8 @@ getJoinConditions previousAlias newAlias (Relation Table{tableSchema=tSchema, ta
|
|||||||
toJoinCondition tb ftb c fc =
|
toJoinCondition tb ftb c fc =
|
||||||
let qi1 = removeSourceCTESchema tSchema tb
|
let qi1 = removeSourceCTESchema tSchema tb
|
||||||
qi2 = removeSourceCTESchema tSchema ftb in
|
qi2 = removeSourceCTESchema tSchema ftb in
|
||||||
JoinCondition (maybe qi1 (QualifiedIdentifier mempty) newAlias, colName c)
|
JoinCondition (maybe qi1 (QualifiedIdentifier mempty) previousAlias, colName c)
|
||||||
(maybe qi2 (QualifiedIdentifier mempty) previousAlias, colName fc)
|
(maybe qi2 (QualifiedIdentifier mempty) newAlias, colName fc)
|
||||||
|
|
||||||
-- On mutation and calling proc cases we wrap the target table in a WITH {sourceCTEName}
|
-- 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
|
-- 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`.
|
-- 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.
|
-- This also works for the other relType's.
|
||||||
fkCols = concat $ mapMaybe (\case
|
fkCols = concat $ mapMaybe (\case
|
||||||
Node (_, (_, Just Relation{relFColumns=cols, relType=relTyp}, _, _, _)) _ -> case relTyp of
|
Node (_, (_, Just Relation{relColumns=cols, relType=relTyp}, _, _, _)) _ -> case relTyp of
|
||||||
Parent -> Just cols
|
O2M -> Just cols
|
||||||
Child -> Just cols
|
M2O -> Just cols
|
||||||
Many -> Just cols
|
M2M -> Just cols
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
) forest
|
) forest
|
||||||
-- However if the "client_id" is present, e.g. mutateRequest to /projects?select=client_id,name,clients(name)
|
-- 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 -> PgVersion -> HT.Transaction DbStructure
|
||||||
getDbStructure schema pgVer = do
|
getDbStructure schema pgVer = do
|
||||||
HT.sql "set local schema ''" -- for getting the fully qualified name(schema.name) of every db object
|
HT.sql "set local schema ''" -- for getting the fully qualified name(schema.name) of every db object
|
||||||
tabs <- HT.statement () allTables
|
tabs <- HT.statement () allTables
|
||||||
cols <- HT.statement schema $ allColumns tabs
|
cols <- HT.statement schema $ allColumns tabs
|
||||||
srcCols <- HT.statement schema $ allSourceColumns cols pgVer
|
srcCols <- HT.statement schema $ allSourceColumns cols pgVer
|
||||||
childRels <- HT.statement () $ allChildRelations tabs cols
|
m2oRels <- HT.statement () $ allM2ORels tabs cols
|
||||||
keys <- HT.statement () $ allPrimaryKeys tabs
|
keys <- HT.statement () $ allPrimaryKeys tabs
|
||||||
procs <- HT.statement schema allProcs
|
procs <- HT.statement schema allProcs
|
||||||
|
|
||||||
let rels = addManyToManyRelations . addParentRelations $ addViewChildRelations srcCols childRels
|
let rels = addM2MRels . addO2MRels $ addViewM2ORels srcCols m2oRels
|
||||||
cols' = addForeignKeys rels cols
|
cols' = addForeignKeys rels cols
|
||||||
keys' = addViewPrimaryKeys srcCols keys
|
keys' = addViewPrimaryKeys srcCols keys
|
||||||
|
|
||||||
@@ -91,9 +91,9 @@ decodeColumns tables =
|
|||||||
<*> nullableColumn HD.text
|
<*> nullableColumn HD.text
|
||||||
<*> nullableColumn HD.text
|
<*> nullableColumn HD.text
|
||||||
|
|
||||||
decodeRelations :: [Table] -> [Column] -> HD.Result [Relation]
|
decodeRels :: [Table] -> [Column] -> HD.Result [Relation]
|
||||||
decodeRelations tables cols =
|
decodeRels tables cols =
|
||||||
mapMaybe (relationFromRow tables cols) <$> HD.rowList relRow
|
mapMaybe (relFromRow tables cols) <$> HD.rowList relRow
|
||||||
where
|
where
|
||||||
relRow = (,,,,,)
|
relRow = (,,,,,)
|
||||||
<$> column HD.text
|
<$> column HD.text
|
||||||
@@ -250,20 +250,20 @@ addForeignKeys rels = map addFk
|
|||||||
addFk col = col { colFK = fk col }
|
addFk col = col { colFK = fk col }
|
||||||
fk col = find (lookupFn col) rels >>= relToFk col
|
fk col = find (lookupFn col) rels >>= relToFk col
|
||||||
lookupFn :: Column -> Relation -> Bool
|
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
|
relToFk col Relation{relColumns=cols, relFColumns=colsF} = do
|
||||||
pos <- L.elemIndex col cols
|
pos <- L.elemIndex col cols
|
||||||
colF <- atMay colsF pos
|
colF <- atMay colsF pos
|
||||||
return $ ForeignKey colF
|
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
|
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
|
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
|
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
|
t1.c1----t2.c2 t1.c1----------t2.c2
|
||||||
-> \________
|
-> \________
|
||||||
\
|
\
|
||||||
t2_view.c2 t2_view.c1
|
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
|
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.
|
The logic for composite pks is similar just need to make sure all the Relation columns have source columns.
|
||||||
-}
|
-}
|
||||||
addViewChildRelations :: [SourceColumn] -> [Relation] -> [Relation]
|
addViewM2ORels :: [SourceColumn] -> [Relation] -> [Relation]
|
||||||
addViewChildRelations allSrcCols = concatMap (\rel ->
|
addViewM2ORels allSrcCols = concatMap (\rel ->
|
||||||
rel : case rel of
|
rel : case rel of
|
||||||
Relation{relType=Child, relTable, relColumns, relFTable, relFColumns} ->
|
Relation{relType=M2O, relTable, relColumns, relFTable, relFColumns} ->
|
||||||
|
|
||||||
let srcColsGroupedByView :: [Column] -> [[SourceColumn]]
|
let srcColsGroupedByView :: [Column] -> [[SourceColumn]]
|
||||||
srcColsGroupedByView relCols = L.groupBy (\(_, viewCol1) (_, viewCol2) -> colTable viewCol1 == colTable viewCol2) $
|
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
|
-- 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
|
srcCols `sortAccordingTo` cols = sortOn (\(k, _) -> L.lookup k $ zip cols [0::Int ..]) srcCols
|
||||||
|
|
||||||
viewTableChild =
|
viewTableM2O =
|
||||||
[ Relation (getView srcCols) (snd <$> srcCols `sortAccordingTo` relColumns)
|
[ Relation (getView srcCols) (snd <$> srcCols `sortAccordingTo` relColumns)
|
||||||
relFTable relFColumns
|
relFTable relFColumns
|
||||||
Child Nothing Nothing Nothing
|
M2O Nothing Nothing Nothing
|
||||||
| srcCols <- relSrcCols, srcCols `allSrcColsOf` relColumns ]
|
| srcCols <- relSrcCols, srcCols `allSrcColsOf` relColumns ]
|
||||||
|
|
||||||
tableViewChild =
|
tableViewM2O =
|
||||||
[ Relation relTable relColumns
|
[ Relation relTable relColumns
|
||||||
(getView fSrcCols) (snd <$> fSrcCols `sortAccordingTo` relFColumns)
|
(getView fSrcCols) (snd <$> fSrcCols `sortAccordingTo` relFColumns)
|
||||||
Child Nothing Nothing Nothing
|
M2O Nothing Nothing Nothing
|
||||||
| fSrcCols <- relFSrcCols, fSrcCols `allSrcColsOf` relFColumns ]
|
| fSrcCols <- relFSrcCols, fSrcCols `allSrcColsOf` relFColumns ]
|
||||||
|
|
||||||
viewViewChild =
|
viewViewM2O =
|
||||||
[ Relation (getView srcCols) (snd <$> srcCols `sortAccordingTo` relColumns)
|
[ Relation (getView srcCols) (snd <$> srcCols `sortAccordingTo` relColumns)
|
||||||
(getView fSrcCols) (snd <$> fSrcCols `sortAccordingTo` relFColumns)
|
(getView fSrcCols) (snd <$> fSrcCols `sortAccordingTo` relFColumns)
|
||||||
Child Nothing Nothing Nothing
|
M2O Nothing Nothing Nothing
|
||||||
| srcCols <- relSrcCols, srcCols `allSrcColsOf` relColumns
|
| srcCols <- relSrcCols, srcCols `allSrcColsOf` relColumns
|
||||||
, fSrcCols <- relFSrcCols, fSrcCols `allSrcColsOf` relFColumns ]
|
, fSrcCols <- relFSrcCols, fSrcCols `allSrcColsOf` relFColumns ]
|
||||||
|
|
||||||
in viewTableChild ++ tableViewChild ++ viewViewChild
|
in viewTableM2O ++ tableViewM2O ++ viewViewM2O
|
||||||
|
|
||||||
_ -> [])
|
_ -> [])
|
||||||
|
|
||||||
addParentRelations :: [Relation] -> [Relation]
|
addO2MRels :: [Relation] -> [Relation]
|
||||||
addParentRelations = concatMap (\rel@(Relation t c ft fc _ _ _ _) -> [rel, Relation ft fc t c Parent Nothing Nothing Nothing])
|
addO2MRels = concatMap (\rel@(Relation t c ft fc _ _ _ _) -> [rel, Relation ft fc t c O2M Nothing Nothing Nothing])
|
||||||
|
|
||||||
addManyToManyRelations :: [Relation] -> [Relation]
|
addM2MRels :: [Relation] -> [Relation]
|
||||||
addManyToManyRelations rels = rels ++ addMirrorRelation (mapMaybe link2Relation links)
|
addM2MRels rels = rels ++ addMirrorRelation (mapMaybe link2Relation links)
|
||||||
where
|
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 -> Text
|
||||||
groupFn Relation{relTable=Table{tableSchema=s, tableName=t}} = s <> "_" <> t
|
groupFn Relation{relTable=Table{tableSchema=s, tableName=t}} = s <> "_" <> t
|
||||||
-- Reference : https://wiki.haskell.org/99_questions/Solutions/26
|
-- Reference : https://wiki.haskell.org/99_questions/Solutions/26
|
||||||
@@ -342,12 +342,12 @@ addManyToManyRelations rels = rels ++ addMirrorRelation (mapMaybe link2Relation
|
|||||||
combinations 0 _ = [ [] ]
|
combinations 0 _ = [ [] ]
|
||||||
combinations n xs = [ y:ys | y:xs' <- tails xs
|
combinations n xs = [ y:ys | y:xs' <- tails xs
|
||||||
, ys <- combinations (n-1) 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 [
|
link2Relation [
|
||||||
Relation{relTable=lt, relColumns=lc1, relFTable=t, relFColumns=c},
|
Relation{relTable=lt, relColumns=lc1, relFTable=t, relFColumns=c},
|
||||||
Relation{ relColumns=lc2, relFTable=ft, relFColumns=fc}
|
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
|
| otherwise = Nothing
|
||||||
link2Relation _ = 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 Text -> [Text]
|
||||||
parseEnum = maybe [] (split (==','))
|
parseEnum = maybe [] (split (==','))
|
||||||
|
|
||||||
allChildRelations :: [Table] -> [Column] -> H.Statement () [Relation]
|
allM2ORels :: [Table] -> [Column] -> H.Statement () [Relation]
|
||||||
allChildRelations tabs cols =
|
allM2ORels tabs cols =
|
||||||
H.Statement sql HE.noParams (decodeRelations tabs cols) True
|
H.Statement sql HE.noParams (decodeRels tabs cols) True
|
||||||
where
|
where
|
||||||
sql = [q|
|
sql = [q|
|
||||||
SELECT ns1.nspname AS table_schema,
|
SELECT ns1.nspname AS table_schema,
|
||||||
@@ -597,9 +597,9 @@ allChildRelations tabs cols =
|
|||||||
WHERE confrelid != 0
|
WHERE confrelid != 0
|
||||||
ORDER BY (conrelid, column_info.nums) |]
|
ORDER BY (conrelid, column_info.nums) |]
|
||||||
|
|
||||||
relationFromRow :: [Table] -> [Column] -> (Text, Text, [Text], Text, Text, [Text]) -> Maybe Relation
|
relFromRow :: [Table] -> [Column] -> (Text, Text, [Text], Text, Text, [Text]) -> Maybe Relation
|
||||||
relationFromRow allTabs allCols (rs, rt, rcs, frs, frt, frcs) =
|
relFromRow allTabs allCols (rs, rt, rcs, frs, frt, frcs) =
|
||||||
Relation <$> table <*> cols <*> tableF <*> colsF <*> pure Child <*> pure Nothing <*> pure Nothing <*> pure Nothing
|
Relation <$> table <*> cols <*> tableF <*> colsF <*> pure M2O <*> pure Nothing <*> pure Nothing <*> pure Nothing
|
||||||
where
|
where
|
||||||
findTable s t = find (\tbl -> tableSchema tbl == s && tableName tbl == t) allTabs
|
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
|
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)
|
, "target" .= fmt (tableSchema fTab) (tableName fTab) (colName <$> relFColumns rel)
|
||||||
, "cardinality" .= (show $ relType rel :: Text)
|
, "cardinality" .= (show $ relType rel :: Text)
|
||||||
] ++
|
] ++
|
||||||
if relType rel == Many
|
if relType rel == M2M
|
||||||
then [
|
then [
|
||||||
"junction" .= case (relLinkTable rel, relLinkCols1 rel, relLinkCols2 rel) of
|
"junction" .= case (relLinkTable rel, relLinkCols1 rel, relLinkCols2 rel) of
|
||||||
(Just lt, Just lc1, Just lc2) -> fmtMany (tableSchema lt) (tableName lt) (colName <$> lc1) (colName <$> lc2)
|
(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) =
|
getJoinsSelects rr@(Node (_, (name, Just Relation{relType=relTyp,relTable=Table{tableName=table}}, alias, _, _)) _) (j,s) =
|
||||||
let subquery = readRequestToQuery rr in
|
let subquery = readRequestToQuery rr in
|
||||||
case relTyp of
|
case relTyp of
|
||||||
Child ->
|
M2O ->
|
||||||
let sel = "COALESCE(("
|
|
||||||
<> "SELECT json_agg(" <> pgFmtIdent table <> ".*) "
|
|
||||||
<> "FROM (" <> subquery <> ") " <> pgFmtIdent table
|
|
||||||
<> "), '[]') AS " <> pgFmtIdent (fromMaybe name alias) in
|
|
||||||
(j, sel:s)
|
|
||||||
Parent ->
|
|
||||||
let aliasOrName = fromMaybe name alias
|
let aliasOrName = fromMaybe name alias
|
||||||
localTableName = pgFmtIdent $ table <> "_" <> aliasOrName
|
localTableName = pgFmtIdent $ table <> "_" <> aliasOrName
|
||||||
sel = "row_to_json(" <> localTableName <> ".*) AS " <> pgFmtIdent aliasOrName
|
sel = "row_to_json(" <> localTableName <> ".*) AS " <> pgFmtIdent aliasOrName
|
||||||
joi = " LEFT JOIN LATERAL( " <> subquery <> " ) AS " <> localTableName <> " ON TRUE " in
|
joi = " LEFT JOIN LATERAL( " <> subquery <> " ) AS " <> localTableName <> " ON TRUE " in
|
||||||
(joi:j,sel:s)
|
(joi:j,sel:s)
|
||||||
Many ->
|
_ ->
|
||||||
let sel = "COALESCE (("
|
let sel = "COALESCE (("
|
||||||
<> "SELECT json_agg(" <> pgFmtIdent table <> ".*) "
|
<> "SELECT json_agg(" <> pgFmtIdent table <> ".*) "
|
||||||
<> "FROM (" <> subquery <> ") " <> pgFmtIdent table
|
<> "FROM (" <> subquery <> ") " <> pgFmtIdent table
|
||||||
|
|||||||
@@ -259,10 +259,14 @@ data QualifiedIdentifier = QualifiedIdentifier {
|
|||||||
|
|
||||||
-- | The relationship [cardinality](https://en.wikipedia.org/wiki/Cardinality_(data_modeling)).
|
-- | The relationship [cardinality](https://en.wikipedia.org/wiki/Cardinality_(data_modeling)).
|
||||||
-- | TODO: missing one-to-one
|
-- | TODO: missing one-to-one
|
||||||
data Cardinality = Child -- ^ a.k.a. many-to-one
|
data Cardinality = O2M -- ^ one-to-many, previously known as Parent
|
||||||
| Parent -- ^ a.k.a. one-to-many
|
| M2O -- ^ many-to-one, previously known as Child
|
||||||
| Many -- ^ a.k.a. many-to-many
|
| M2M -- ^ many-to-many, previously known as Many
|
||||||
deriving (Show, Eq)
|
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
|
The name 'Relation' here is used with the meaning
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ spec =
|
|||||||
{
|
{
|
||||||
"details": [
|
"details": [
|
||||||
{
|
{
|
||||||
"cardinality": "Parent",
|
"cardinality": "many-to-one",
|
||||||
"source": "test.person[id]",
|
"source": "test.message[sender]",
|
||||||
"target": "test.message[sender]"
|
"target": "test.person[id]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "Parent",
|
"cardinality": "many-to-one",
|
||||||
"source": "test.person_detail[id]",
|
"source": "test.message[sender]",
|
||||||
"target": "test.message[sender]"
|
"target": "test.person_detail[id]"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hint": "Disambiguate by choosing a relationship from the `details` key",
|
"hint": "Disambiguate by choosing a relationship from the `details` key",
|
||||||
@@ -42,85 +42,85 @@ spec =
|
|||||||
[json|
|
[json|
|
||||||
{
|
{
|
||||||
"details": [
|
"details": [
|
||||||
{
|
{
|
||||||
"cardinality": "Child",
|
"cardinality": "one-to-many",
|
||||||
"source": "test.articleStars[userId]",
|
"source": "test.users[id]",
|
||||||
"target": "test.users[id]"
|
"target": "test.articleStars[userId]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "Child",
|
"cardinality": "one-to-many",
|
||||||
"source": "test.limited_article_stars[user_id]",
|
"source": "test.users[id]",
|
||||||
"target": "test.users[id]"
|
"target": "test.limited_article_stars[user_id]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "Child",
|
"cardinality": "one-to-many",
|
||||||
"source": "test.comments[commenter_id]",
|
"source": "test.users[id]",
|
||||||
"target": "test.users[id]"
|
"target": "test.comments[commenter_id]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "Child",
|
"cardinality": "one-to-many",
|
||||||
"source": "test.users_projects[user_id]",
|
"source": "test.users[id]",
|
||||||
"target": "test.users[id]"
|
"target": "test.users_projects[user_id]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "Child",
|
"cardinality": "one-to-many",
|
||||||
"source": "test.users_tasks[user_id]",
|
"source": "test.users[id]",
|
||||||
"target": "test.users[id]"
|
"target": "test.users_tasks[user_id]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "Many",
|
"cardinality": "many-to-many",
|
||||||
"junction": "private.article_stars[article_id][user_id]",
|
"junction": "private.article_stars[user_id][article_id]",
|
||||||
"source": "test.articles[id]",
|
"source": "test.users[id]",
|
||||||
"target": "test.users[id]"
|
"target": "test.articles[id]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "Many",
|
"cardinality": "many-to-many",
|
||||||
"junction": "test.articleStars[articleId][userId]",
|
"junction": "test.articleStars[userId][articleId]",
|
||||||
"source": "test.articles[id]",
|
"source": "test.users[id]",
|
||||||
"target": "test.users[id]"
|
"target": "test.articles[id]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "Many",
|
"cardinality": "many-to-many",
|
||||||
"junction": "test.limited_article_stars[article_id][user_id]",
|
"junction": "test.limited_article_stars[user_id][article_id]",
|
||||||
"source": "test.articles[id]",
|
"source": "test.users[id]",
|
||||||
"target": "test.users[id]"
|
"target": "test.articles[id]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "Many",
|
"cardinality": "many-to-many",
|
||||||
"junction": "test.users_projects[project_id][user_id]",
|
"junction": "test.users_projects[user_id][project_id]",
|
||||||
"source": "test.projects[id]",
|
"source": "test.users[id]",
|
||||||
"target": "test.users[id]"
|
"target": "test.projects[id]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "Many",
|
"cardinality": "many-to-many",
|
||||||
"junction": "test.users_projects[project_id][user_id]",
|
"junction": "test.users_projects[user_id][project_id]",
|
||||||
"source": "test.materialized_projects[id]",
|
"source": "test.users[id]",
|
||||||
"target": "test.users[id]"
|
"target": "test.materialized_projects[id]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "Many",
|
"cardinality": "many-to-many",
|
||||||
"junction": "test.users_projects[project_id][user_id]",
|
"junction": "test.users_projects[user_id][project_id]",
|
||||||
"source": "test.projects_view[id]",
|
"source": "test.users[id]",
|
||||||
"target": "test.users[id]"
|
"target": "test.projects_view[id]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "Many",
|
"cardinality": "many-to-many",
|
||||||
"junction": "test.users_projects[project_id][user_id]",
|
"junction": "test.users_projects[user_id][project_id]",
|
||||||
"source": "test.projects_view_alt[t_id]",
|
"source": "test.users[id]",
|
||||||
"target": "test.users[id]"
|
"target": "test.projects_view_alt[t_id]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "Many",
|
"cardinality": "many-to-many",
|
||||||
"junction": "test.users_tasks[task_id][user_id]",
|
"junction": "test.users_tasks[user_id][task_id]",
|
||||||
"source": "test.tasks[id]",
|
"source": "test.users[id]",
|
||||||
"target": "test.users[id]"
|
"target": "test.tasks[id]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "Many",
|
"cardinality": "many-to-many",
|
||||||
"junction": "test.users_tasks[task_id][user_id]",
|
"junction": "test.users_tasks[user_id][task_id]",
|
||||||
"source": "test.filtered_tasks[myId]",
|
"source": "test.users[id]",
|
||||||
"target": "test.users[id]"
|
"target": "test.filtered_tasks[myId]"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hint": "Disambiguate by choosing a relationship from the `details` key",
|
"hint": "Disambiguate by choosing a relationship from the `details` key",
|
||||||
"message": "More than one relationship was found for users and id"
|
"message": "More than one relationship was found for users and id"
|
||||||
|
|||||||
Reference in New Issue
Block a user