Allow specifying the constraint name to disambiguate an embedding (#1430)

Makes previous duck typing regex unnecessary since the FK can be renamed
to a singular name or to any other format.

* Remove embedding with duck typed column names
* Allow embedding by foreign key name
* Add junction disambiguation tests
This commit is contained in:
Steve Chavez
2020-01-06 09:42:33 -05:00
committed by GitHub
parent 99b13fa25f
commit 663faa1f82
12 changed files with 699 additions and 703 deletions
+3
View File
@@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #1383, Add support for HEAD request - @steve-chavez - #1383, Add support for HEAD request - @steve-chavez
- #1378, Add support for `Prefer: count=planned` and `Prefer: count=estimated` on GET /table - @steve-chavez - #1378, Add support for `Prefer: count=planned` and `Prefer: count=estimated` on GET /table - @steve-chavez
- #1327, Add support for optional query parameter `on_conflict` to upsert with specified keys for POST - @ykst - #1327, Add support for optional query parameter `on_conflict` to upsert with specified keys for POST - @ykst
- #1430, Allow specifying the foreign key constraint name(`/source?select=fk_constraint(*)`) to disambiguate an embedding - @steve-chavez
### Fixed ### Fixed
@@ -25,6 +26,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #1385, bulk RPC call now should be done by specifying a `Prefer: params=multiple-objects` header - @steve-chavez - #1385, bulk RPC call now should be done by specifying a `Prefer: params=multiple-objects` header - @steve-chavez
- #1401, resource embedding now outputs an error when multiple relationships between two tables are found - @steve-chavez - #1401, resource embedding now outputs an error when multiple relationships between two tables are found - @steve-chavez
- #1423, default Unix Socket file mode from 755 to 660 - @dwagin - #1423, default Unix Socket file mode from 755 to 660 - @dwagin
- #1430, Remove embedding with duck typed column names `GET /projects?select=client(*)`- @steve-chavez
+ You can rename the foreign key to `client` to make this request work in the new version: `alter table projects rename constraint projects_client_id_fkey to client`
## [6.0.2] - 2019-08-22 ## [6.0.2] - 2019-08-22
+95 -129
View File
@@ -16,18 +16,14 @@ module PostgREST.DbRequestBuilder (
, mutateRequest , mutateRequest
) where ) where
import qualified Data.ByteString.Char8 as BS import qualified Data.HashMap.Strict as M
import qualified Data.HashMap.Strict as M import qualified Data.Set as S
import qualified Data.Set as S
import Control.Arrow ((***)) import Control.Arrow ((***))
import Data.Either.Combinators (mapLeft) import Data.Either.Combinators (mapLeft)
import Data.Foldable (foldr1) import Data.Foldable (foldr1)
import Data.List (delete, head, (!!)) import Data.List (delete)
import Data.Maybe (fromJust)
import Data.Text (isInfixOf) import Data.Text (isInfixOf)
import Text.Regex.TDFA ((=~))
import Unsafe (unsafeHead)
import Control.Applicative import Control.Applicative
import Data.Tree import Data.Tree
@@ -38,7 +34,7 @@ import PostgREST.Error (ApiRequestError (..), errorResponseFor)
import PostgREST.Parsers import PostgREST.Parsers
import PostgREST.RangeQuery (NonnegRange, allRange, restrictRange) import PostgREST.RangeQuery (NonnegRange, allRange, restrictRange)
import PostgREST.Types import PostgREST.Types
import Protolude hiding (from, head) import Protolude hiding (from)
readRequest :: Schema -> TableName -> Maybe Integer -> [Relation] -> ApiRequest -> Either Response ReadRequest readRequest :: Schema -> TableName -> Maybe Integer -> [Relation] -> ApiRequest -> Either Response ReadRequest
readRequest schema rootTableName maxRows allRels apiRequest = readRequest schema rootTableName maxRows allRels apiRequest =
@@ -49,20 +45,20 @@ readRequest schema rootTableName maxRows allRels apiRequest =
(initReadRequest rootName <$> pRequestSelect sel) (initReadRequest rootName <$> pRequestSelect sel)
where where
sel = fromMaybe "*" $ iSelect apiRequest -- default to all columns requested (SELECT *) for a non existent ?select querystring param sel = fromMaybe "*" $ iSelect apiRequest -- default to all columns requested (SELECT *) for a non existent ?select querystring param
(rootName, rootRels) = rootWithRelations schema rootTableName allRels (iAction apiRequest) (rootName, rootRels) = rootWithRels schema rootTableName allRels (iAction apiRequest)
-- Get the root table name with its relations according to the Action type. -- Get the root table name with its relationships according to the Action type.
-- This is done because of the shape of the final SQL Query. The mutation cases are wrapped in a WITH {sourceCTEName}(see Statements.hs). -- This is done because of the shape of the final SQL Query. The mutation cases are wrapped in a WITH {sourceCTEName}(see Statements.hs).
-- So we need a FROM {sourceCTEName} instead of FROM {tableName}. -- So we need a FROM {sourceCTEName} instead of FROM {tableName}.
rootWithRelations :: Schema -> TableName -> [Relation] -> Action -> (QualifiedIdentifier, [Relation]) rootWithRels :: Schema -> TableName -> [Relation] -> Action -> (QualifiedIdentifier, [Relation])
rootWithRelations schema rootTableName allRels action = case action of rootWithRels schema rootTableName allRels action = case action of
ActionRead _ -> (QualifiedIdentifier schema rootTableName, allRels) -- normal read case ActionRead _ -> (QualifiedIdentifier schema rootTableName, allRels) -- normal read case
_ -> (QualifiedIdentifier mempty sourceCTEName, mapMaybe toSourceRelation allRels ++ allRels) -- mutation cases and calling proc _ -> (QualifiedIdentifier mempty sourceCTEName, mapMaybe toSourceRel allRels ++ allRels) -- mutation cases and calling proc
where where
-- 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 findRel can find relationships with sourceCTEName.
toSourceRelation :: Relation -> Maybe Relation toSourceRel :: Relation -> Maybe Relation
toSourceRelation r@Relation{relTable=t} toSourceRel r@Relation{relTable=t}
| rootTableName == tableName t = Just $ r {relTable=t {tableName=sourceCTEName}} | rootTableName == tableName t = Just $ r {relTable=t {tableName=sourceCTEName}}
| otherwise = Nothing | otherwise = Nothing
@@ -77,14 +73,14 @@ initReadRequest rootQi =
rootName = qiName rootQi rootName = qiName rootQi
initial = Node (Select [] rootQi Nothing [] [] [] [] allRange, (rootName, Nothing, Nothing, Nothing, rootDepth)) [] initial = Node (Select [] rootQi Nothing [] [] [] [] allRange, (rootName, Nothing, Nothing, Nothing, rootDepth)) []
treeEntry :: Depth -> Tree SelectItem -> ReadRequest -> ReadRequest treeEntry :: Depth -> Tree SelectItem -> ReadRequest -> ReadRequest
treeEntry depth (Node fld@((fn, _),_,alias,relationDetail) fldForest) (Node (q, i) rForest) = treeEntry depth (Node fld@((fn, _),_,alias, embedHint) fldForest) (Node (q, i) rForest) =
let nxtDepth = succ depth in let nxtDepth = succ depth in
case fldForest of case fldForest of
[] -> Node (q {select=fld:select q}, i) rForest [] -> Node (q {select=fld:select q}, i) rForest
_ -> Node (q, i) $ _ -> Node (q, i) $
foldr (treeEntry nxtDepth) foldr (treeEntry nxtDepth)
(Node (Select [] (QualifiedIdentifier rootSchema fn) Nothing [] [] [] [] allRange, (Node (Select [] (QualifiedIdentifier rootSchema fn) Nothing [] [] [] [] allRange,
(fn, Nothing, alias, relationDetail, nxtDepth)) []) (fn, Nothing, alias, embedHint, nxtDepth)) [])
fldForest:rForest fldForest:rForest
treeRestrictRange :: Maybe Integer -> ReadRequest -> Either ApiRequestError ReadRequest treeRestrictRange :: Maybe Integer -> ReadRequest -> Either ApiRequestError ReadRequest
@@ -95,30 +91,16 @@ treeRestrictRange maxRows request = pure $ nodeRestrictRange maxRows <$> request
augumentRequestWithJoin :: Schema -> [Relation] -> ReadRequest -> Either ApiRequestError ReadRequest augumentRequestWithJoin :: Schema -> [Relation] -> ReadRequest -> Either ApiRequestError ReadRequest
augumentRequestWithJoin schema allRels request = augumentRequestWithJoin schema allRels request =
addRelations schema allRels Nothing request addRels schema allRels Nothing request
>>= addJoinConditions Nothing >>= addJoinConditions Nothing
addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either ApiRequestError ReadRequest addRels :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either ApiRequestError ReadRequest
addRelations schema allRelations parentNode (Node (query@Select{from=tbl}, (nodeName, _, alias, relationDetail, depth)) forest) = addRels schema allRels parentNode (Node (query@Select{from=tbl}, (nodeName, _, alias, hint, 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 (relFTable 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 rel = findRel schema allRels (qiName parentNodeQi) nodeName hint
results = findRelation schema allRelations parentNodeTable nodeName relationDetail
rel :: Either ApiRequestError Relation
rel = case results of
[] -> Left $ NoRelBetween parentNodeTable nodeName
[r] -> Right r
rs ->
-- 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 == O2M) 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)
_ -> _ ->
@@ -126,126 +108,110 @@ addRelations schema allRelations parentNode (Node (query@Select{from=tbl}, (node
Node rn <$> updateForest (Just $ Node rn forest) Node rn <$> updateForest (Just $ Node rn forest)
where where
updateForest :: Maybe ReadRequest -> Either ApiRequestError [ReadRequest] updateForest :: Maybe ReadRequest -> Either ApiRequestError [ReadRequest]
updateForest rq = mapM (addRelations schema allRelations rq) forest updateForest rq = mapM (addRels schema allRels rq) forest
findRelation :: Schema -> [Relation] -> TableName -> NodeName -> Maybe RelationDetail -> [Relation] -- Finds a relationship between an origin and a target in the request: /origin?select=target(*)
findRelation schema allRelations parentTableName nodeName relationDetail = -- If more than one relationship is found then the request is ambiguous and we return an error.
filter (\Relation{relTable, relColumns, relFTable, relFColumns, relType, relLinkTable} -> -- In that case the request can be disambiguated by adding precision to the target or by using a hint: /origin?select=target!hint(*)
-- Both relation ends need to be on the exposed schema -- The elements will be matched according to these rules:
schema == tableSchema relTable && schema == tableSchema relFTable && -- origin = table / view
case relationDetail of -- target = table / view / constraint / column-from-origin
Nothing -> -- hint = table / view / constraint / column-from-origin / column-from-target
-- (hint can take table / view values to aid in finding the junction in an m2m relationship)
-- (request) => projects { ..., clients{...} } findRel :: Schema -> [Relation] -> NodeName -> NodeName -> Maybe EmbedHint -> Either ApiRequestError Relation
-- will match findRel schema allRels origin target hint =
-- (relation type) => M2O case rel of
-- (entity) => clients {id} [] -> Left $ NoRelBetween origin target
-- (foriegn entity) => projects {client_id} [r] -> Right r
rs ->
-- Return error if more than one relationship is found, unless we're in a self reference case.
--
-- Here we handle a self reference relationship to not cause a breaking change:
-- In a self reference we get two relationships with the same foreign key and relTable/relFtable but with different cardinalities(m2o/o2m)
-- We output the O2M rel, the M2O rel can be obtained by using the origin column as an embed hint.
let [rel0, rel1] = take 2 rs in
if length rs == 2 && relConstraint rel0 == relConstraint rel1 && relTable rel0 == relTable rel1 && relFTable rel0 == relFTable rel1
then note (NoRelBetween origin target) (find (\r -> relType r == O2M) rs)
else Left $ AmbiguousRelBetween origin target rs
where
matchFKSingleCol hint_ cols = length cols == 1 && hint_ == (colName <$> head cols)
rel = filter (
\Relation{relTable, relColumns, relConstraint, relFTable, relFColumns, relType, relJunction} ->
-- Both relationship ends need to be on the exposed schema
schema == tableSchema relTable && schema == tableSchema relFTable &&
( (
parentTableName == tableName relTable && -- projects -- /projects?select=clients(*)
nodeName == tableName relFTable -- clients origin == tableName relTable && -- projects
) || target == tableName relFTable || -- clients
-- (request) => projects { ..., client_id{...} } -- /projects?select=projects_client_id_fkey(*)
-- will match (
-- (relation type) => M2O origin == tableName relTable && -- projects
-- (entity) => clients {id} Just target == relConstraint -- projects_client_id_fkey
-- (foriegn entity) => projects {client_id} ) ||
( -- /projects?select=client_id(*)
parentTableName == tableName relTable && -- projects (
length relColumns == 1 && origin == tableName relTable && -- projects
-- match common foreign key names(table_name_id, table_name_fk) to table_name matchFKSingleCol (Just target) relColumns -- client_id
(toS ("^" <> colName (unsafeHead relColumns) <> "_?(?:|[iI][dD]|[fF][kK])$") :: BS.ByteString) =~ )
(toS nodeName :: BS.ByteString) -- client_id ) && (
isNothing hint || -- hint is optional
-- /projects?select=clients!projects_client_id_fkey(*)
hint == relConstraint || -- projects_client_id_fkey
-- /projects?select=clients!client_id(*) or /projects?select=clients!id(*)
matchFKSingleCol hint relColumns || -- client_id
matchFKSingleCol hint relFColumns || -- id
-- /users?select=tasks!users_tasks(*)
(
relType == M2M && -- many-to-many between users and tasks
hint == (tableName . junTable <$> relJunction) -- users_tasks
)
) )
) allRels
-- (request) => project_id { ..., client_id{...} }
-- will match
-- (relation type) => M2O
-- (entity) => clients {id}
-- (foriegn entity) => projects {client_id}
-- this case works becasue before reaching this place
-- addRelation will turn project_id to project so the above condition will match
Just rd ->
-- (request) => clients { ..., projects!client_id{...} }
-- will match
-- (relation type) => O2M
-- (entity) => clients {id}
-- (foriegn entity) => projects {client_id}
(
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{...} }
-- will match
-- (relation type) => M2O
-- (entity) => message {sender}
-- (foriegn entity) => person_detail {id}
(
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) => M2M
-- (entity) => users
-- (foriegn entity) => tasks
(
relType == M2M &&
parentTableName == tableName relTable && -- tasks
nodeName == tableName relFTable && -- users
rd == tableName (fromJust relLinkTable) -- rd is tasks_users
)
) allRelations
-- previousAlias is only used for the case of self joins -- previousAlias is only used for the case of self joins
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@(_, rel, _, _, depth)) forest) =
case relation of case rel of
Just rel@Relation{relType=O2M} -> Node (augmentQuery rel, nodeProps) <$> updatedForest Just r@Relation{relType=O2M} -> Node (augmentQuery r, nodeProps) <$> updatedForest
Just rel@Relation{relType=M2O} -> Node (augmentQuery rel, nodeProps) <$> updatedForest Just r@Relation{relType=M2O} -> Node (augmentQuery r, nodeProps) <$> updatedForest
Just rel@Relation{relType=M2M, relLinkTable=lTable} -> Just r@Relation{relType=M2M, relJunction=junction} ->
case lTable of case junction of
Just linkTable -> Just Junction{junTable} ->
let rq = augmentQuery rel in let rq = augmentQuery r in
Node (rq{implicitJoins=tableQi linkTable:implicitJoins rq}, nodeProps) <$> updatedForest Node (rq{implicitJoins=tableQi junTable:implicitJoins rq}, nodeProps) <$> updatedForest
Nothing -> Nothing ->
Left UnknownRelation Left UnknownRelation
Nothing -> Node node <$> updatedForest Nothing -> Node node <$> updatedForest
where where
newAlias = case isSelfReference <$> relation of newAlias = case isSelfReference <$> rel of
Just True Just True
| depth /= 0 -> Just (qiName tbl <> "_" <> show depth) -- root node doesn't get aliased | depth /= 0 -> Just (qiName tbl <> "_" <> show depth) -- root node doesn't get aliased
| otherwise -> Nothing | otherwise -> Nothing
_ -> Nothing _ -> Nothing
augmentQuery rel = augmentQuery r =
foldr foldr
(\jc rq@Select{joinConditions=jcs} -> rq{joinConditions=jc:jcs}) (\jc rq@Select{joinConditions=jcs} -> rq{joinConditions=jc:jcs})
query{fromAlias=newAlias} query{fromAlias=newAlias}
(getJoinConditions previousAlias newAlias rel) (getJoinConditions previousAlias newAlias r)
updatedForest = mapM (addJoinConditions newAlias) forest updatedForest = mapM (addJoinConditions newAlias) forest
-- previousAlias and newAlias are used in the case of self joins -- previousAlias and newAlias are used in the case of self joins
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 jun) =
case typ of case typ of
O2M -> O2M ->
zipWith (toJoinCondition tN ftN) cols fCols zipWith (toJoinCondition tN ftN) cols fCols
M2O -> M2O ->
zipWith (toJoinCondition tN ftN) cols fCols zipWith (toJoinCondition tN ftN) cols fCols
M2M -> M2M -> case jun of
let ltN = maybe "" tableName lt in Just (Junction jt _ jc1 _ jc2) ->
zipWith (toJoinCondition tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toJoinCondition ftN ltN) fCols (fromMaybe [] lc2) let jtn = tableName jt in
zipWith (toJoinCondition tN jtn) cols jc1 ++ zipWith (toJoinCondition ftN jtn) fCols jc2
Nothing -> []
where where
toJoinCondition :: Text -> Text -> Column -> Column -> JoinCondition toJoinCondition :: Text -> Text -> Column -> Column -> JoinCondition
toJoinCondition tb ftb c fc = toJoinCondition tb ftb c fc =
+34 -33
View File
@@ -95,9 +95,10 @@ decodeRels :: [Table] -> [Column] -> HD.Result [Relation]
decodeRels tables cols = decodeRels tables cols =
mapMaybe (relFromRow tables cols) <$> HD.rowList relRow mapMaybe (relFromRow tables cols) <$> HD.rowList relRow
where where
relRow = (,,,,,) relRow = (,,,,,,)
<$> column HD.text <$> column HD.text
<*> column HD.text <*> column HD.text
<*> column HD.text
<*> column (HD.array (HD.dimension replicateM (element HD.text))) <*> column (HD.array (HD.dimension replicateM (element HD.text)))
<*> column HD.text <*> column HD.text
<*> column HD.text <*> column HD.text
@@ -290,7 +291,7 @@ The logic for composite pks is similar just need to make sure all the Relation c
addViewM2ORels :: [SourceColumn] -> [Relation] -> [Relation] addViewM2ORels :: [SourceColumn] -> [Relation] -> [Relation]
addViewM2ORels allSrcCols = concatMap (\rel -> addViewM2ORels allSrcCols = concatMap (\rel ->
rel : case rel of rel : case rel of
Relation{relType=M2O, relTable, relColumns, relFTable, relFColumns} -> Relation{relType=M2O, relTable, relColumns, relConstraint, 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) $
@@ -307,20 +308,22 @@ addViewM2ORels allSrcCols = concatMap (\rel ->
viewTableM2O = viewTableM2O =
[ Relation (getView srcCols) (snd <$> srcCols `sortAccordingTo` relColumns) [ Relation (getView srcCols) (snd <$> srcCols `sortAccordingTo` relColumns)
relFTable relFColumns relConstraint relFTable relFColumns
M2O Nothing Nothing Nothing M2O Nothing
| srcCols <- relSrcCols, srcCols `allSrcColsOf` relColumns ] | srcCols <- relSrcCols, srcCols `allSrcColsOf` relColumns ]
tableViewM2O = tableViewM2O =
[ Relation relTable relColumns [ Relation relTable relColumns
relConstraint
(getView fSrcCols) (snd <$> fSrcCols `sortAccordingTo` relFColumns) (getView fSrcCols) (snd <$> fSrcCols `sortAccordingTo` relFColumns)
M2O Nothing Nothing Nothing M2O Nothing
| fSrcCols <- relFSrcCols, fSrcCols `allSrcColsOf` relFColumns ] | fSrcCols <- relFSrcCols, fSrcCols `allSrcColsOf` relFColumns ]
viewViewM2O = viewViewM2O =
[ Relation (getView srcCols) (snd <$> srcCols `sortAccordingTo` relColumns) [ Relation (getView srcCols) (snd <$> srcCols `sortAccordingTo` relColumns)
relConstraint
(getView fSrcCols) (snd <$> fSrcCols `sortAccordingTo` relFColumns) (getView fSrcCols) (snd <$> fSrcCols `sortAccordingTo` relFColumns)
M2O Nothing Nothing Nothing M2O Nothing
| srcCols <- relSrcCols, srcCols `allSrcColsOf` relColumns | srcCols <- relSrcCols, srcCols `allSrcColsOf` relColumns
, fSrcCols <- relFSrcCols, fSrcCols `allSrcColsOf` relFColumns ] , fSrcCols <- relFSrcCols, fSrcCols `allSrcColsOf` relFColumns ]
@@ -329,12 +332,12 @@ addViewM2ORels allSrcCols = concatMap (\rel ->
_ -> []) _ -> [])
addO2MRels :: [Relation] -> [Relation] addO2MRels :: [Relation] -> [Relation]
addO2MRels = concatMap (\rel@(Relation t c ft fc _ _ _ _) -> [rel, Relation ft fc t c O2M Nothing Nothing Nothing]) addO2MRels = concatMap (\rel@(Relation t c cn ft fc _ _) -> [rel, Relation ft fc cn t c O2M Nothing])
addM2MRels :: [Relation] -> [Relation] addM2MRels :: [Relation] -> [Relation]
addM2MRels rels = rels ++ addMirrorRelation (mapMaybe link2Relation links) addM2MRels rels = rels ++ addMirrorRel (mapMaybe junction2Rel junctions)
where where
links = join $ map (combinations 2) $ filter (not . null) $ groupWith groupFn $ filter ( (==M2O). relType) rels junctions = 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,14 +345,15 @@ addM2MRels rels = rels ++ addMirrorRelation (mapMaybe link2Relation links)
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 M2M lt lc2 lc1]) junction2Rel [
link2Relation [ Relation{relTable=jt, relColumns=jc1, relConstraint=const1, relFTable=t, relFColumns=c},
Relation{relTable=lt, relColumns=lc1, relFTable=t, relFColumns=c}, Relation{ relColumns=jc2, relConstraint=const2, 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 M2M (Just lt) (Just lc1) (Just lc2) | jc1 /= jc2 && length jc1 == 1 && length jc2 == 1 = Just $ Relation t c Nothing ft fc M2M (Just $ Junction jt const1 jc1 const2 jc2)
| otherwise = Nothing | otherwise = Nothing
link2Relation _ = Nothing junction2Rel _ = Nothing
addMirrorRel = concatMap (\rel@(Relation t c _ ft fc _ (Just (Junction jt const1 jc1 const2 jc2))) ->
[rel, Relation ft fc Nothing t c M2M (Just (Junction jt const2 jc2 const1 jc1))])
addViewPrimaryKeys :: [SourceColumn] -> [PrimaryKey] -> [PrimaryKey] addViewPrimaryKeys :: [SourceColumn] -> [PrimaryKey] -> [PrimaryKey]
addViewPrimaryKeys srcCols = concatMap (\pk -> addViewPrimaryKeys srcCols = concatMap (\pk ->
@@ -574,32 +578,29 @@ allM2ORels tabs cols =
sql = [q| sql = [q|
SELECT ns1.nspname AS table_schema, SELECT ns1.nspname AS table_schema,
tab.relname AS table_name, tab.relname AS table_name,
conname AS constraint_name,
column_info.cols AS columns, column_info.cols AS columns,
ns2.nspname AS foreign_table_schema, ns2.nspname AS foreign_table_schema,
other.relname AS foreign_table_name, other.relname AS foreign_table_name,
column_info.refs AS foreign_columns column_info.refs AS foreign_columns
FROM pg_constraint, FROM pg_constraint,
LATERAL (SELECT array_agg(cols.attname) AS cols, LATERAL (
array_agg(cols.attnum) AS nums, SELECT array_agg(cols.attname) AS cols,
array_agg(refs.attname) AS refs array_agg(cols.attnum) AS nums,
FROM ( SELECT unnest(conkey) AS col, unnest(confkey) AS ref) k, array_agg(refs.attname) AS refs
LATERAL (SELECT * FROM pg_attribute FROM ( SELECT unnest(conkey) AS col, unnest(confkey) AS ref) k,
WHERE attrelid = conrelid AND attnum = col) LATERAL (SELECT * FROM pg_attribute WHERE attrelid = conrelid AND attnum = col) AS cols,
AS cols, LATERAL (SELECT * FROM pg_attribute WHERE attrelid = confrelid AND attnum = ref) AS refs) AS column_info,
LATERAL (SELECT * FROM pg_attribute LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = connamespace) AS ns1,
WHERE attrelid = confrelid AND attnum = ref) LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = conrelid) AS tab,
AS refs) LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = confrelid) AS other,
AS column_info, LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = other.relnamespace) AS ns2
LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = connamespace) AS ns1,
LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = conrelid) AS tab,
LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = confrelid) AS other,
LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = other.relnamespace) AS ns2
WHERE confrelid != 0 WHERE confrelid != 0
ORDER BY (conrelid, column_info.nums) |] ORDER BY (conrelid, column_info.nums) |]
relFromRow :: [Table] -> [Column] -> (Text, Text, [Text], Text, Text, [Text]) -> Maybe Relation relFromRow :: [Table] -> [Column] -> (Text, Text, Text, [Text], Text, Text, [Text]) -> Maybe Relation
relFromRow allTabs allCols (rs, rt, rcs, frs, frt, frcs) = relFromRow allTabs allCols (rs, rt, cn, rcs, frs, frt, frcs) =
Relation <$> table <*> cols <*> tableF <*> colsF <*> pure M2O <*> pure Nothing <*> pure Nothing <*> pure Nothing Relation <$> table <*> cols <*> pure (Just cn) <*> tableF <*> colsF <*> pure M2O <*> 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
+14 -18
View File
@@ -80,9 +80,9 @@ instance JSON.ToJSON ApiRequestError where
toJSON UnknownRelation = JSON.object [ toJSON UnknownRelation = JSON.object [
"message" .= ("Unknown relation" :: Text)] "message" .= ("Unknown relation" :: Text)]
toJSON (NoRelBetween parent child) = JSON.object [ toJSON (NoRelBetween parent child) = JSON.object [
"message" .= ("Could not find foreign keys between these entities, No relation found between " <> parent <> " and " <> child :: Text)] "message" .= ("Could not find foreign keys between these entities. No relationship found between " <> parent <> " and " <> child :: Text)]
toJSON (AmbiguousRelBetween parent child rels) = JSON.object [ toJSON (AmbiguousRelBetween parent child rels) = JSON.object [
"hint" .= ("Disambiguate by choosing a relationship from the `details` key" :: Text), "hint" .= ("By following the 'details' key, disambiguate the request by changing the url to /origin?select=relationship(*) or /origin?select=target!relationship(*)" :: Text),
"message" .= ("More than one relationship was found for " <> parent <> " and " <> child :: Text), "message" .= ("More than one relationship was found for " <> parent <> " and " <> child :: Text),
"details" .= (compressedRel <$> rels) ] "details" .= (compressedRel <$> rels) ]
toJSON UnsupportedVerb = JSON.object [ toJSON UnsupportedVerb = JSON.object [
@@ -93,27 +93,23 @@ instance JSON.ToJSON ApiRequestError where
compressedRel :: Relation -> JSON.Value compressedRel :: Relation -> JSON.Value
compressedRel rel = compressedRel rel =
let let
-- | Format like "test.orders[billing_address_id]". For easier debugging the format is compressed instead of structured. fmtTbl tbl = tableSchema tbl <> "." <> tableName tbl
fmt sch tbl cols = schTbl sch tbl <> joinCols cols fmtEls els = "[" <> T.intercalate ", " els <> "]"
fmtMany sch tbl cols1 cols2 = schTbl sch tbl <> joinCols cols1 <> joinCols cols2
schTbl sch tbl = sch <> "." <> tbl
joinCols cols = "[" <> T.intercalate ", " cols <> "]"
tab = relTable rel
fTab = relFTable rel
in in
JSON.object $ [ JSON.object $ [
"source" .= fmt (tableSchema tab) (tableName tab) (colName <$> relColumns rel) "origin" .= fmtTbl (relTable rel)
, "target" .= fmt (tableSchema fTab) (tableName fTab) (colName <$> relFColumns rel) , "target" .= fmtTbl (relFTable rel)
, "cardinality" .= (show $ relType rel :: Text) , "cardinality" .= (show $ relType rel :: Text)
] ++ ] ++
if relType rel == M2M case (relType rel, relJunction rel, relConstraint rel) of
then [ (M2M, Just (Junction jt (Just const1) _ (Just const2) _), _) -> [
"junction" .= case (relLinkTable rel, relLinkCols1 rel, relLinkCols2 rel) of "relationship" .= (fmtTbl jt <> fmtEls [const1] <> fmtEls [const2])
(Just lt, Just lc1, Just lc2) -> fmtMany (tableSchema lt) (tableName lt) (colName <$> lc1) (colName <$> lc2)
_ -> toS $ JSON.encode JSON.Null
] ]
else mempty (_, _, Just relCon) -> [
"relationship" .= (relCon <> fmtEls (colName <$> relColumns rel) <> fmtEls (colName <$> relFColumns rel))
]
(_, _, _) ->
mempty
data PgError = PgError Authenticated P.UsageError data PgError = PgError Authenticated P.UsageError
type Authenticated = Bool type Authenticated = Bool
+5 -5
View File
@@ -134,12 +134,12 @@ pRelationSelect :: Parser SelectItem
pRelationSelect = lexeme $ try ( do pRelationSelect = lexeme $ try ( do
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) ) alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
fld <- pField fld <- pField
relationDetail <- optionMaybe ( hint <- optionMaybe (
try ( char '!' *> pFieldName ) <|> try ( char '!' *> pFieldName) <|>
try ( char '.' *> pFieldName ) -- TODO deprecated, remove in next major version -- deprecated, remove in next major version
try ( char '.' *> pFieldName)
) )
return (fld, Nothing, alias, hint)
return (fld, Nothing, alias, relationDetail)
) )
pFieldSelect :: Parser SelectItem pFieldSelect :: Parser SelectItem
+28 -20
View File
@@ -264,28 +264,34 @@ data Cardinality = O2M -- ^ one-to-many, previously known as Parent
| M2M -- ^ many-to-many, previously known as Many | M2M -- ^ many-to-many, previously known as Many
deriving Eq deriving Eq
instance Show Cardinality where instance Show Cardinality where
show O2M = "one-to-many" show O2M = "o2m"
show M2O = "many-to-one" show M2O = "m2o"
show M2M = "many-to-many" show M2M = "m2m"
type ConstraintName = Text
{-| {-|
The name 'Relation' here is used with the meaning "Relation"ship between two tables.
"What is the relation between the current node and the parent node". The order of the relColumns and relFColumns should be maintained to get the join conditions right.
It has nothing to do with PostgreSQL referring to tables/views as relations.
The order of the relColumns and relFColumns should be maintained to get
the join conditions right.
TODO merge relColumns and relFColumns to a tuple or Data.Bimap TODO merge relColumns and relFColumns to a tuple or Data.Bimap
-} -}
data Relation = Relation { data Relation = Relation {
relTable :: Table relTable :: Table
, relColumns :: [Column] , relColumns :: [Column]
, relFTable :: Table , relConstraint :: Maybe ConstraintName -- ^ Just on O2M/M2O, Nothing on M2M
, relFColumns :: [Column] , relFTable :: Table
, relType :: Cardinality , relFColumns :: [Column]
-- The Link attrs are used when Cardinality == Many , relType :: Cardinality
, relLinkTable :: Maybe Table , relJunction :: Maybe Junction -- ^ Junction for M2M Cardinality
, relLinkCols1 :: Maybe [Column] } deriving (Show, Eq)
, relLinkCols2 :: Maybe [Column]
-- | Junction table on an M2M relationship
data Junction = Junction {
junTable :: Table
, junConstraint1 :: Maybe ConstraintName
, junCols1 :: [Column]
, junConstraint2 :: Maybe ConstraintName
, junCols2 :: [Column]
} deriving (Show, Eq) } deriving (Show, Eq)
isSelfReference :: Relation -> Bool isSelfReference :: Relation -> Bool
@@ -409,8 +415,10 @@ toHeaders = map $ \(GucHeader (k, v)) -> (CI.mk $ toS k, toS v)
This type will hold information about which particular 'Relation' between two tables to choose when there are multiple ones. This type will hold information about which particular 'Relation' between two tables to choose when there are multiple ones.
Specifically, it will contain the name of the foreign key or the join table in many to many relations. Specifically, it will contain the name of the foreign key or the join table in many to many relations.
-} -}
type RelationDetail = Text type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe EmbedHint)
type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe RelationDetail) -- | Disambiguates an embedding operation when there's multiple relationships between two tables.
-- | Can be the name of a foreign key constraint, column name or the junction in an m2m relationship.
type EmbedHint = Text
-- | Path of the embedded levels, e.g "clients.projects.name=eq.." gives Path ["clients", "projects"] -- | Path of the embedded levels, e.g "clients.projects.name=eq.." gives Path ["clients", "projects"]
type EmbedPath = [Text] type EmbedPath = [Text]
data Filter = Filter { field::Field, opExpr::OpExpr } deriving (Show, Eq) data Filter = Filter { field::Field, opExpr::OpExpr } deriving (Show, Eq)
@@ -453,7 +461,7 @@ data MutateQuery =
type ReadRequest = Tree ReadNode type ReadRequest = Tree ReadNode
type MutateRequest = MutateQuery type MutateRequest = MutateQuery
type ReadNode = (ReadQuery, (NodeName, Maybe Relation, Maybe Alias, Maybe RelationDetail, Depth)) type ReadNode = (ReadQuery, (NodeName, Maybe Relation, Maybe Alias, Maybe EmbedHint, Depth))
type Depth = Integer type Depth = Integer
-- First level FieldNames(e.g get a,b from /table?select=a,b,other(c,d)) -- First level FieldNames(e.g get a,b from /table?select=a,b,other(c,d))
+287 -150
View File
@@ -13,170 +13,306 @@ import SpecHelper
spec :: SpecWith Application spec :: SpecWith Application
spec = spec =
describe "resource embedding disambiguation" $ do describe "resource embedding disambiguation" $ do
context "ambiguous requests that give 300 Multiple Choices" $ do
it "gives a 300 Multiple Choices error when the request is ambiguous" $ do it "errs when there's a table and view that point to the same fk" $
get "/message?select=id,body,sender(name,sent)" `shouldRespondWith` get "/message?select=id,body,sender(name,sent)" `shouldRespondWith`
[json| [json|
{
"details": [
{ {
"cardinality": "many-to-one", "details": [
"source": "test.message[sender]", {
"target": "test.person[id]" "cardinality": "m2o",
}, "relationship": "message_sender_fkey[sender][id]",
{ "origin": "test.message",
"cardinality": "many-to-one", "target": "test.person"
"source": "test.message[sender]", },
"target": "test.person_detail[id]" {
"cardinality": "m2o",
"relationship": "message_sender_fkey[sender][id]",
"origin": "test.message",
"target": "test.person_detail"
}
],
"hint": "By following the 'details' key, disambiguate the request by changing the url to /origin?select=relationship(*) or /origin?select=target!relationship(*)",
"message": "More than one relationship was found for message and sender"
} }
], |]
"hint": "Disambiguate by choosing a relationship from the `details` key", { matchStatus = 300
"message": "More than one relationship was found for message and sender" , matchHeaders = [matchContentTypeJson]
} }
|]
{ matchStatus = 300
, matchHeaders = [matchContentTypeJson]
}
get "/users?select=*,id(*)" `shouldRespondWith` it "errs when there are o2m and m2m cardinalities to the target table" $
[json| get "/sites?select=*,big_projects(*)" `shouldRespondWith`
{ [json|
"details": [ {
"details": [
{
"cardinality": "m2o",
"relationship": "main_project[main_project_id][big_project_id]",
"origin": "test.sites",
"target": "test.big_projects"
},
{
"cardinality": "m2m",
"relationship": "test.jobs[jobs_site_id_fkey][jobs_big_project_id_fkey]",
"origin": "test.sites",
"target": "test.big_projects"
},
{
"cardinality": "m2m",
"relationship": "test.main_jobs[jobs_site_id_fkey][jobs_big_project_id_fkey]",
"origin": "test.sites",
"target": "test.big_projects"
}
],
"hint": "By following the 'details' key, disambiguate the request by changing the url to /origin?select=relationship(*) or /origin?select=target!relationship(*)",
"message": "More than one relationship was found for sites and big_projects"
}
|]
{ matchStatus = 300
, matchHeaders = [matchContentTypeJson]
}
it "errs on an ambiguous embed that has a circular reference" $
get "/agents?select=*,departments(*)" `shouldRespondWith`
[json|
{
"details": [
{
"cardinality": "m2o",
"relationship": "agents_department_id_fkey[department_id][id]",
"origin": "test.agents",
"target": "test.departments"
},
{
"cardinality": "o2m",
"relationship": "departments_head_id_fkey[id][head_id]",
"origin": "test.agents",
"target": "test.departments"
}
],
"hint": "By following the 'details' key, disambiguate the request by changing the url to /origin?select=relationship(*) or /origin?select=target!relationship(*)",
"message": "More than one relationship was found for agents and departments"
}
|]
{ matchStatus = 300
, matchHeaders = [matchContentTypeJson]
}
it "errs when there are more than two fks on a junction table(currently impossible to disambiguate, only choice is to split the table)" $
-- We have 4 possibilities for doing the junction JOIN here.
-- This could be solved by specifying two additional fks, like whatev_projects!fk1!fk2(*)
-- If the need arises this capability can be added later without causing a breaking change
get "/whatev_sites?select=*,whatev_projects(*)" `shouldRespondWith`
[json|
{
"details": [
{
"cardinality": "m2m",
"relationship": "test.whatev_jobs[whatev_jobs_site_id_1_fkey][whatev_jobs_project_id_1_fkey]",
"origin": "test.whatev_sites",
"target": "test.whatev_projects"
},
{
"cardinality": "m2m",
"relationship": "test.whatev_jobs[whatev_jobs_site_id_1_fkey][whatev_jobs_project_id_2_fkey]",
"origin": "test.whatev_sites",
"target": "test.whatev_projects"
},
{
"cardinality": "m2m",
"relationship": "test.whatev_jobs[whatev_jobs_site_id_2_fkey][whatev_jobs_project_id_1_fkey]",
"origin": "test.whatev_sites",
"target": "test.whatev_projects"
},
{
"cardinality": "m2m",
"relationship": "test.whatev_jobs[whatev_jobs_site_id_2_fkey][whatev_jobs_project_id_2_fkey]",
"origin": "test.whatev_sites",
"target": "test.whatev_projects"
}
],
"hint": "By following the 'details' key, disambiguate the request by changing the url to /origin?select=relationship(*) or /origin?select=target!relationship(*)",
"message": "More than one relationship was found for whatev_sites and whatev_projects"
}
|]
{ matchStatus = 300
, matchHeaders = [matchContentTypeJson]
}
context "disambiguating requests with embed hints" $ do
context "using FK to specify the relationship" $ do
it "can embed by FK name" $
get "/projects?id=in.(1,3)&select=id,name,client(id,name)" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client":{"id":2,"name":"Apple"}}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can embed by FK name and select the FK column at the same time" $
get "/projects?id=in.(1,3)&select=id,name,client_id,client(id,name)" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","client_id":1,"client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client":{"id":2,"name":"Apple"}}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can embed parent with view!fk and grandparent by using fk" $
get "/tasks?id=eq.1&select=id,name,projects_view!project(id,name,client(id,name))" `shouldRespondWith`
[str|[{"id":1,"name":"Design w7","projects_view":{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}}}]|]
it "can embed by using a composite FK name" $
get "/unit_workdays?select=unit_id,day,fst_shift(car_id,schedule(name)),snd_shift(camera_id,schedule(name))" `shouldRespondWith`
[json| [
{ {
"cardinality": "one-to-many", "day": "2019-12-02",
"source": "test.users[id]", "fst_shift": {
"target": "test.articleStars[userId]" "car_id": "CAR-349",
}, "schedule": {
{ "name": "morning"
"cardinality": "one-to-many", }
"source": "test.users[id]", },
"target": "test.limited_article_stars[user_id]" "snd_shift": {
}, "camera_id": "CAM-123",
{ "schedule": {
"cardinality": "one-to-many", "name": "night"
"source": "test.users[id]", }
"target": "test.comments[commenter_id]" },
}, "unit_id": 1
{
"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", { matchHeaders = [matchContentTypeJson] }
"message": "More than one relationship was found for users and id"
}
|]
{ matchStatus = 300
, matchHeaders = [matchContentTypeJson]
}
it "works when requesting children 2 levels" $ it "embeds by using two fks pointing to the same table" $
get "/clients?id=eq.1&select=id,projects:projects!client_id(id,tasks(id))" `shouldRespondWith` get "/orders?id=eq.1&select=id, name, billing(address), shipping(address)" `shouldRespondWith`
[json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|] [json|[{"id":1,"name":"order 1","billing":{"address": "address 1"},"shipping":{"address": "address 2"}}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "works with parent relation" $ it "fails if the fk is not known" $
get "/message?select=id,body,sender:person!sender(name),recipient:person!recipient(name)&id=lt.4" `shouldRespondWith` get "/message?select=id,sender:person!space(name)&id=lt.4" `shouldRespondWith`
[json| [json|{"message":"Could not find foreign keys between these entities. No relationship found between message and person"}|]
[{"id":1,"body":"Hello Jane","sender":{"name":"John"},"recipient":{"name":"Jane"}}, { matchStatus = 400
{"id":2,"body":"Hi John","sender":{"name":"Jane"},"recipient":{"name":"John"}}, , matchHeaders = [matchContentTypeJson] }
{"id":3,"body":"How are you doing?","sender":{"name":"John"},"recipient":{"name":"Jane"}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "fails with an unknown relation" $ it "can request a parent with fk" $
get "/message?select=id,sender:person!space(name)&id=lt.4" `shouldRespondWith` get "/comments?select=content,user(name)" `shouldRespondWith`
[json|{"message":"Could not find foreign keys between these entities, No relation found between message and person"}|] [json|[ { "content": "Needs to be delivered ASAP", "user": { "name": "Angela Martin" } } ]|]
{ matchStatus = 400 { matchHeaders = [matchContentTypeJson] }
, matchHeaders = [matchContentTypeJson] }
it "works with a parent view relation" $ it "can request two parents with fks" $
get "/message?select=id,body,sender:person_detail!sender(name,sent),recipient:person_detail!recipient(name,received)&id=lt.4" `shouldRespondWith` get "/articleStars?select=createdAt,article(owner),user(name)&limit=1" `shouldRespondWith`
[json| [json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"owner": "postgrest_test_authenticator"},"user":{"name": "Angela Martin"}}]|]
[{"id":1,"body":"Hello Jane","sender":{"name":"John","sent":2},"recipient":{"name":"Jane","received":2}}, { matchHeaders = [matchContentTypeJson] }
{"id":2,"body":"Hi John","sender":{"name":"Jane","sent":1},"recipient":{"name":"John","received":1}},
{"id":3,"body":"How are you doing?","sender":{"name":"John","sent":2},"recipient":{"name":"Jane","received":2}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "works with many<->many relation" $ it "can specify a view!fk" $
get "/tasks?select=id,users:users!users_tasks(id)" `shouldRespondWith` get "/message?select=id,body,sender:person_detail!message_sender_fkey(name,sent),recipient:person_detail!message_recipient_fkey(name,received)&id=lt.4" `shouldRespondWith`
[json|[{"id":1,"users":[{"id":1},{"id":3}]},{"id":2,"users":[{"id":1}]},{"id":3,"users":[{"id":1}]},{"id":4,"users":[{"id":1}]},{"id":5,"users":[{"id":2},{"id":3}]},{"id":6,"users":[{"id":2}]},{"id":7,"users":[{"id":2}]},{"id":8,"users":[]}]|] [json|
{ matchHeaders = [matchContentTypeJson] } [{"id":1,"body":"Hello Jane","sender":{"name":"John","sent":2},"recipient":{"name":"Jane","received":2}},
{"id":2,"body":"Hi John","sender":{"name":"Jane","sent":1},"recipient":{"name":"John","received":1}},
{"id":3,"body":"How are you doing?","sender":{"name":"John","sent":2},"recipient":{"name":"Jane","received":2}}] |]
{ matchHeaders = [matchContentTypeJson] }
context "using FK col to specify the relationship" $ do it "can specify a table!fk hint and request children 2 levels" $
it "can embed by FK column name" $ get "/clients?id=eq.1&select=id,projects:projects!client(id,tasks(id))" `shouldRespondWith`
get "/projects?id=in.(1,3)&select=id,name,client_id(id,name)" `shouldRespondWith` [json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|]
[json|[{"id":1,"name":"Windows 7","client_id":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":{"id":2,"name":"Apple"}}]|] { matchHeaders = [matchContentTypeJson] }
{ matchHeaders = [matchContentTypeJson] }
it "can embed by FK column name and select the FK value at the same time, if aliased" $ it "can disambiguate with the fk in case of an o2m and m2m relationship to the same table" $
get "/projects?id=in.(1,3)&select=id,name,client_id,client:client_id(id,name)" `shouldRespondWith` get "/sites?select=name,main_project(name)&site_id=eq.1" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","client_id":1,"client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client":{"id":2,"name":"Apple"}}]|] [json| [ { "name": "site 1", "main_project": { "name": "big project 1" } } ] |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "requests parents two levels up" $ context "using the column name of the FK to specify the relationship" $ do
get "/tasks?id=eq.1&select=id,name,project:projects!project_id(id,name,client:client_id(id,name))" `shouldRespondWith` it "can embed by column" $
[str|[{"id":1,"name":"Design w7","project":{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}}}]|] get "/projects?id=in.(1,3)&select=id,name,client_id(id,name)" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","client_id":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":{"id":2,"name":"Apple"}}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can embed by column and select the column at the same time, if aliased" $
get "/projects?id=in.(1,3)&select=id,name,client_id,client:client_id(id,name)" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","client_id":1,"client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client":{"id":2,"name":"Apple"}}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can embed parent by using view!column and grandparent by using the column" $
get "/tasks?id=eq.1&select=id,name,project:projects_view!project_id(id,name,client:client_id(id,name))" `shouldRespondWith`
[str|[{"id":1,"name":"Design w7","project":{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}}}]|]
it "can specify table!column" $
get "/message?select=id,body,sender:person!sender(name),recipient:person!recipient(name)&id=lt.4" `shouldRespondWith`
[json|
[{"id":1,"body":"Hello Jane","sender":{"name":"John"},"recipient":{"name":"Jane"}},
{"id":2,"body":"Hi John","sender":{"name":"Jane"},"recipient":{"name":"John"}},
{"id":3,"body":"How are you doing?","sender":{"name":"John"},"recipient":{"name":"Jane"}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "will embed using a column that has uppercase chars" $
get "/ghostBusters?select=escapeId(*)" `shouldRespondWith`
[json| [{"escapeId":{"so6meIdColumn":1}},{"escapeId":{"so6meIdColumn":3}},{"escapeId":{"so6meIdColumn":5}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "embeds by using two columns pointing to the same table" $
get "/orders?id=eq.1&select=id, name, billing_address_id(id), shipping_address_id(id)" `shouldRespondWith`
[json|[{"id":1,"name":"order 1","billing_address_id":{"id":1},"shipping_address_id":{"id":2}}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can disambiguate with the column in case of an o2m and m2m relationship to the same table" $
get "/sites?select=name,main_project_id(name)&site_id=eq.1" `shouldRespondWith`
[json| [ { "name": "site 1", "main_project_id": { "name": "big project 1" } } ] |]
{ matchHeaders = [matchContentTypeJson] }
context "using the junction to disambiguate the request" $
it "can specify the junction of an m2m relationship" $ do
get "/sites?select=*,big_projects!jobs(name)&site_id=in.(1,2)" `shouldRespondWith`
[json|
[
{
"big_projects": [
{
"name": "big project 1"
}
],
"main_project_id": 1,
"name": "site 1",
"site_id": 1
},
{
"big_projects": [
{
"name": "big project 1"
},
{
"name": "big project 2"
}
],
"main_project_id": null,
"name": "site 2",
"site_id": 2
}
]
|]
get "/sites?select=*,big_projects!main_jobs(name)&site_id=in.(1,2)" `shouldRespondWith`
[json|
[
{
"big_projects": [
{
"name": "big project 1"
}
],
"main_project_id": 1,
"name": "site 1",
"site_id": 1
},
{
"big_projects": [],
"main_project_id": null,
"name": "site 2",
"site_id": 2
}
]
|]
{ matchHeaders = [matchContentTypeJson] }
context "using a FK column and a FK to specify the relationship" $
it "embeds by using a column and a fk pointing to the same table" $
get "/orders?id=eq.1&select=id, name, billing_address_id(id), shipping(id)" `shouldRespondWith`
[json|[{"id":1,"name":"order 1","billing_address_id":{"id":1},"shipping":{"id":2}}]|]
{ matchHeaders = [matchContentTypeJson] }
context "tables with self reference foreign keys" $ do context "tables with self reference foreign keys" $ do
context "one self reference foreign key" $ do context "one self reference foreign key" $ do
@@ -286,7 +422,7 @@ spec =
"refereeds":[]}] "refereeds":[]}]
}]|] { matchHeaders = [matchContentTypeJson] } }]|] { matchHeaders = [matchContentTypeJson] }
-- TODO Remove in next major version(7.0) -- TODO Remove in next major version
describe "old dot '.' symbol, deprecated" $ describe "old dot '.' symbol, deprecated" $
it "still works" $ do it "still works" $ do
get "/clients?id=eq.1&select=id,projects:projects.client_id(id,tasks(id))" `shouldRespondWith` get "/clients?id=eq.1&select=id,projects:projects.client_id(id,tasks(id))" `shouldRespondWith`
@@ -295,3 +431,4 @@ spec =
get "/tasks?select=id,users:users.users_tasks(id)" `shouldRespondWith` get "/tasks?select=id,users:users.users_tasks(id)" `shouldRespondWith`
[json|[{"id":1,"users":[{"id":1},{"id":3}]},{"id":2,"users":[{"id":1}]},{"id":3,"users":[{"id":1}]},{"id":4,"users":[{"id":1}]},{"id":5,"users":[{"id":2},{"id":3}]},{"id":6,"users":[{"id":2}]},{"id":7,"users":[{"id":2}]},{"id":8,"users":[]}]|] [json|[{"id":1,"users":[{"id":1},{"id":3}]},{"id":2,"users":[{"id":1}]},{"id":3,"users":[{"id":1}]},{"id":4,"users":[{"id":1}]},{"id":5,"users":[{"id":2},{"id":3}]},{"id":6,"users":[{"id":2}]},{"id":7,"users":[{"id":2}]},{"id":8,"users":[]}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
+7 -37
View File
@@ -4,7 +4,7 @@ import Network.Wai (Application)
import Network.Wai.Test (SResponse (simpleHeaders)) import Network.Wai.Test (SResponse (simpleHeaders))
import Network.HTTP.Types import Network.HTTP.Types
import Test.Hspec import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
@@ -198,7 +198,7 @@ spec actualPgVersion = do
it "matches filtering nested items 2" $ it "matches filtering nested items 2" $
get "/clients?select=id,projects(id,tasks2(id,name))&projects.tasks.name=like.Design*" get "/clients?select=id,projects(id,tasks2(id,name))&projects.tasks.name=like.Design*"
`shouldRespondWith` [json| {"message":"Could not find foreign keys between these entities, No relation found between projects and tasks2"}|] `shouldRespondWith` [json| {"message":"Could not find foreign keys between these entities. No relationship found between projects and tasks2"}|]
{ matchStatus = 400 { matchStatus = 400
, matchHeaders = [matchContentTypeJson] , matchHeaders = [matchContentTypeJson]
} }
@@ -265,19 +265,8 @@ spec actualPgVersion = do
[json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|] [json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "requesting parent without specifying primary key" $
get "/projects?select=name,client(name)" `shouldRespondWith`
[json|[
{"name":"Windows 7","client":{"name": "Microsoft"}},
{"name":"Windows 10","client":{"name": "Microsoft"}},
{"name":"IOS","client":{"name": "Apple"}},
{"name":"OSX","client":{"name": "Apple"}},
{"name":"Orphan","client":null}
]|]
{ matchHeaders = [matchContentTypeJson] }
it "requesting parent and renaming primary key" $ it "requesting parent and renaming primary key" $
get "/projects?select=name,client(clientId:id,name)" `shouldRespondWith` get "/projects?select=name,client:clients(clientId:id,name)" `shouldRespondWith`
[json|[ [json|[
{"name":"Windows 7","client":{"name": "Microsoft", "clientId": 1}}, {"name":"Windows 7","client":{"name": "Microsoft", "clientId": 1}},
{"name":"Windows 10","client":{"name": "Microsoft", "clientId": 1}}, {"name":"Windows 10","client":{"name": "Microsoft", "clientId": 1}},
@@ -295,13 +284,8 @@ spec actualPgVersion = do
[json|[{"id":1,"commenter_id":1,"user_id":2,"task_id":6,"content":"Needs to be delivered ASAP","users_tasks":{"taskId": 6}}]|] [json|[{"id":1,"commenter_id":1,"user_id":2,"task_id":6,"content":"Needs to be delivered ASAP","users_tasks":{"taskId": 6}}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "embed data with two fk pointing to the same table" $
get "/orders?id=eq.1&select=id, name, billing_address_id(id), shipping_address_id(id)" `shouldRespondWith`
[str|[{"id":1,"name":"order 1","billing_address_id":{"id":1},"shipping_address_id":{"id":2}}]|]
it "requesting parents and children while renaming them" $ it "requesting parents and children while renaming them" $
get "/projects?id=eq.1&select=myId:id, name, project_client:client_id(*), project_tasks:tasks(id, name)" `shouldRespondWith` get "/projects?id=eq.1&select=myId:id, name, project_client:clients(*), project_tasks:tasks(id, name)" `shouldRespondWith`
[json|[{"myId":1,"name":"Windows 7","project_client":{"id":1,"name":"Microsoft"},"project_tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|] [json|[{"myId":1,"name":"Windows 7","project_client":{"id":1,"name":"Microsoft"},"project_tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
@@ -343,11 +327,6 @@ spec actualPgVersion = do
[json|[{"user_id":2,"task_id":6,"comments":[{"content":"Needs to be delivered ASAP"}]}]|] [json|[{"user_id":2,"task_id":6,"comments":[{"content":"Needs to be delivered ASAP"}]}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "can select by column name sans id" $
get "/projects?id=in.(1,3)&select=id,name,client_id,client(id,name)" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","client_id":1,"client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client":{"id":2,"name":"Apple"}}]|]
{ matchHeaders = [matchContentTypeJson] }
describe "view embedding" $ do describe "view embedding" $ do
it "can detect fk relations through views to tables in the public schema" $ it "can detect fk relations through views to tables in the public schema" $
get "/consumers_view?select=*,orders_view(*)" `shouldRespondWith` 200 get "/consumers_view?select=*,orders_view(*)" `shouldRespondWith` 200
@@ -355,8 +334,8 @@ spec actualPgVersion = do
it "can detect fk relations through materialized views to tables in the public schema" $ it "can detect fk relations through materialized views to tables in the public schema" $
get "/materialized_projects?select=*,users(*)" `shouldRespondWith` 200 get "/materialized_projects?select=*,users(*)" `shouldRespondWith` 200
it "can request parent without specifying primary key" $ it "can request two parents" $
get "/articleStars?select=createdAt,article(owner),user(name)&limit=1" `shouldRespondWith` get "/articleStars?select=createdAt,article:articles(owner),user:users(name)&limit=1" `shouldRespondWith`
[json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"owner": "postgrest_test_authenticator"},"user":{"name": "Angela Martin"}}]|] [json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"owner": "postgrest_test_authenticator"},"user":{"name": "Angela Martin"}}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
@@ -448,7 +427,7 @@ spec actualPgVersion = do
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "can embed a view that has group by" $ it "can embed a view that has group by" $
get "/projects_count_grouped_by?select=number_of_projects,client(name)&order=number_of_projects" `shouldRespondWith` get "/projects_count_grouped_by?select=number_of_projects,client:clients(name)&order=number_of_projects" `shouldRespondWith`
[json| [json|
[{"number_of_projects":1,"client":null}, [{"number_of_projects":1,"client":null},
{"number_of_projects":2,"client":{"name":"Microsoft"}}, {"number_of_projects":2,"client":{"name":"Microsoft"}},
@@ -612,10 +591,6 @@ spec actualPgVersion = do
get "/projects?id=eq.1&select=id, name, clients(id, name)&clients.order=name.asc" `shouldRespondWith` get "/projects?id=eq.1&select=id, name, clients(id, name)&clients.order=name.asc" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"}}]|] [str|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"}}]|]
it "ordering embeded parents does not break things when using ducktape names" $
get "/projects?id=eq.1&select=id, name, client(id, name)&client.order=name.asc" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","client":{"id":1,"name":"Microsoft"}}]|]
context "order syntax errors" $ do context "order syntax errors" $ do
it "gives meaningful error messages when asc/desc/nulls{first,last} are misspelled" $ do it "gives meaningful error messages when asc/desc/nulls{first,last} are misspelled" $ do
get "/items?order=id.ac" `shouldRespondWith` get "/items?order=id.ac" `shouldRespondWith`
@@ -745,11 +720,6 @@ spec actualPgVersion = do
[json| [{"ghostBusters":[{"escapeId":1}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":3}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":5}]}] |] [json| [{"ghostBusters":[{"escapeId":1}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":3}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":5}]}] |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "will embed using a column" $
get "/ghostBusters?select=escapeId(*)" `shouldRespondWith`
[json| [{"escapeId":{"so6meIdColumn":1}},{"escapeId":{"so6meIdColumn":3}},{"escapeId":{"so6meIdColumn":5}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "will select and filter a column that has spaces" $ it "will select and filter a column that has spaces" $
get "/Server%20Today?select=Just%20A%20Server%20Model&Just%20A%20Server%20Model=like.*91*" `shouldRespondWith` get "/Server%20Today?select=Just%20A%20Server%20Model&Just%20A%20Server%20Model=like.*91*" `shouldRespondWith`
[json|[ [json|[
+2 -2
View File
@@ -158,10 +158,10 @@ spec actualPgVersion =
context "foreign entities embedding" $ do context "foreign entities embedding" $ do
it "can embed if related tables are in the exposed schema" $ do it "can embed if related tables are in the exposed schema" $ do
post "/rpc/getproject?select=id,name,client(id),tasks(id)" [json| { "id": 1} |] `shouldRespondWith` post "/rpc/getproject?select=id,name,client:clients(id),tasks(id)" [json| { "id": 1} |] `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|] [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
get "/rpc/getproject?id=1&select=id,name,client(id),tasks(id)" `shouldRespondWith` get "/rpc/getproject?id=1&select=id,name,client:clients(id),tasks(id)" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|] [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
+46
View File
@@ -526,3 +526,49 @@ INSERT INTO private.pages VALUES (2, 'http://postgrest.org/en/v6.0/admin.html');
TRUNCATE TABLE private.referrals CASCADE; TRUNCATE TABLE private.referrals CASCADE;
INSERT INTO private.referrals VALUES ('github.com', 1); INSERT INTO private.referrals VALUES ('github.com', 1);
INSERT INTO private.referrals VALUES ('hub.docker.com', 2); INSERT INTO private.referrals VALUES ('hub.docker.com', 2);
TRUNCATE TABLE big_projects CASCADE;
INSERT INTO big_projects (big_project_id, name)
VALUES (1, 'big project 1'),
(2, 'big project 2');
TRUNCATE TABLE sites CASCADE;
INSERT INTO sites (site_id, name, main_project_id)
VALUES (1, 'site 1', 1),
(2, 'site 2', null),
(3, 'site 3', 2),
(4, 'site 4', null);
TRUNCATE TABLE jobs CASCADE;
INSERT INTO jobs (job_id, name, site_id, big_project_id)
VALUES ('bc5d5362-b881-438f-b9f5-7417e08704ed', 'job 1-1', 1, 1),
('3bd52697-033b-4edd-8a28-46a9c04b7c1e', 'job 2-1', 2, 1),
('e6e67e4e-19b1-11e9-ab14-d663bd873d93', 'job 2-2', 2, 2);
TRUNCATE TABLE departments CASCADE;
TRUNCATE TABLE agents CASCADE;
INSERT INTO agents (id, name)
VALUES (1, 'agent 1'),
(2, 'agent 2'),
(3, 'agent 3'),
(4, 'agent 4');
INSERT INTO departments (id, name, head_id)
VALUES (1, 'dep 1', 1),
(2, 'dep 3', 3);
UPDATE agents SET department_id = 1 WHERE id in (1, 2);
UPDATE agents SET department_id = 2 WHERE id in (3, 4);
TRUNCATE TABLE schedules CASCADE;
INSERT INTO schedules VALUES(1, 'morning', '06:00:00', '11:59:00');
INSERT INTO schedules VALUES(2, 'afternoon', '12:00:00', '17:59:00');
INSERT INTO schedules VALUES(3, 'night', '18:00:00', '23:59:00');
INSERT INTO schedules VALUES(4, 'early morning', '00:00:00', '05:59:00');
TRUNCATE TABLE activities CASCADE;
INSERT INTO activities(id, schedule_id, car_id) VALUES(1, 1, 'CAR-349');
INSERT INTO activities(id, schedule_id, camera_id) VALUES(2, 3, 'CAM-123');
TRUNCATE TABLE unit_workdays CASCADE;
INSERT INTO unit_workdays VALUES(1, '2019-12-02', 1, 1, 2, 3);
+12
View File
@@ -108,6 +108,18 @@ GRANT ALL ON TABLE
, web_content , web_content
, pages , pages
, referrals , referrals
, big_projects
, sites
, jobs
, main_jobs
, whatev_projects
, whatev_sites
, whatev_jobs
, agents
, departments
, schedules
, activities
, unit_workdays
TO postgrest_test_anonymous; TO postgrest_test_anonymous;
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous; GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
+166 -309
View File
@@ -103,15 +103,10 @@ SET default_tablespace = '';
SET default_with_oids = false; SET default_with_oids = false;
--
-- Name: items; Type: TABLE; Schema: test; Owner: -
--
CREATE TABLE items ( CREATE TABLE items (
id bigint NOT NULL id bigserial primary key
); );
CREATE FUNCTION always_true(test.items) RETURNS boolean CREATE FUNCTION always_true(test.items) RETURNS boolean
LANGUAGE sql STABLE LANGUAGE sql STABLE
AS $$ SELECT true $$; AS $$ SELECT true $$;
@@ -364,59 +359,8 @@ CREATE TABLE auth (
pass character(60) NOT NULL pass character(60) NOT NULL
); );
SET search_path = private, pg_catalog;
--
-- Name: article_stars; Type: TABLE; Schema: private; Owner: -
--
CREATE TABLE article_stars (
article_id integer NOT NULL,
user_id integer NOT NULL,
created_at timestamp without time zone DEFAULT now() NOT NULL
);
--
-- Name: articles; Type: TABLE; Schema: private; Owner: -
--
CREATE TABLE articles (
id integer NOT NULL,
body text,
owner name NOT NULL
);
SET search_path = test, pg_catalog; SET search_path = test, pg_catalog;
CREATE VIEW limited_article_stars AS
SELECT article_id, user_id, created_at FROM private.article_stars;
--
-- Name: articleStars; Type: VIEW; Schema: test; Owner: -
--
CREATE VIEW "articleStars" AS
SELECT article_stars.article_id AS "articleId",
article_stars.user_id AS "userId",
article_stars.created_at AS "createdAt"
FROM private.article_stars;
--
-- Name: articles; Type: VIEW; Schema: test; Owner: -
--
CREATE VIEW articles AS
SELECT articles.id,
articles.body,
articles.owner
FROM private.articles;
-- --
-- Name: authors_only; Type: TABLE; Schema: test; Owner: - -- Name: authors_only; Type: TABLE; Schema: test; Owner: -
-- --
@@ -463,24 +407,10 @@ ALTER SEQUENCE auto_incrementing_pk_id_seq OWNED BY auto_incrementing_pk.id;
-- --
CREATE TABLE clients ( CREATE TABLE clients (
id integer NOT NULL, id integer primary key,
name text NOT NULL name text NOT NULL
); );
--
-- Name: comments; Type: TABLE; Schema: test; Owner: -
--
CREATE TABLE comments (
id integer NOT NULL,
commenter_id integer NOT NULL,
user_id integer NOT NULL,
task_id integer NOT NULL,
content text NOT NULL
);
-- --
-- Name: complex_items; Type: TABLE; Schema: test; Owner: - -- Name: complex_items; Type: TABLE; Schema: test; Owner: -
-- --
@@ -574,26 +504,6 @@ CREATE VIEW insertable_view_with_join AS
FROM (has_fk FROM (has_fk
JOIN auto_incrementing_pk USING (id)); JOIN auto_incrementing_pk USING (id));
--
-- Name: items_id_seq; Type: SEQUENCE; Schema: test; Owner: -
--
CREATE SEQUENCE items_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: items_id_seq; Type: SEQUENCE OWNED BY; Schema: test; Owner: -
--
ALTER SEQUENCE items_id_seq OWNED BY items.id;
-- --
-- Name: json; Type: TABLE; Schema: test; Owner: - -- Name: json; Type: TABLE; Schema: test; Owner: -
-- --
@@ -663,11 +573,11 @@ CREATE TABLE insertonly (
-- --
CREATE TABLE projects ( CREATE TABLE projects (
id integer NOT NULL, id integer primary key,
name text NOT NULL, name text NOT NULL,
client_id integer client_id integer REFERENCES clients(id)
); );
alter table projects rename constraint projects_client_id_fkey to client;
-- --
-- Name: projects_view; Type: VIEW; Schema: test; Owner: - -- Name: projects_view; Type: VIEW; Schema: test; Owner: -
@@ -695,25 +605,23 @@ CREATE TABLE simple_pk (
extra character varying NOT NULL extra character varying NOT NULL
); );
-- CREATE TABLE users (
-- Name: users_projects; Type: TABLE; Schema: test; Owner: - id integer primary key,
-- name text NOT NULL
);
CREATE TABLE users_projects ( CREATE TABLE users_projects (
user_id integer NOT NULL, user_id integer NOT NULL REFERENCES users(id),
project_id integer NOT NULL project_id integer NOT NULL REFERENCES projects(id),
PRIMARY KEY (project_id, user_id)
); );
--
-- Name: tasks; Type: TABLE; Schema: test; Owner: -
--
CREATE TABLE tasks ( CREATE TABLE tasks (
id integer NOT NULL, id integer primary key,
name text NOT NULL, name text NOT NULL,
project_id integer project_id integer REFERENCES projects(id)
); );
alter table tasks rename constraint tasks_project_id_fkey to project;
CREATE OR REPLACE VIEW filtered_tasks AS CREATE OR REPLACE VIEW filtered_tasks AS
SELECT id AS "myId", name, project_id AS "projectID" SELECT id AS "myId", name, project_id AS "projectID"
@@ -725,6 +633,53 @@ project_id IN (
SELECT project_id FROM users_projects WHERE user_id = 1 SELECT project_id FROM users_projects WHERE user_id = 1
); );
CREATE TABLE users_tasks (
user_id integer NOT NULL REFERENCES users(id),
task_id integer NOT NULL REFERENCES tasks(id),
primary key (task_id, user_id)
);
CREATE TABLE comments (
id integer primary key,
commenter_id integer NOT NULL,
user_id integer NOT NULL,
task_id integer NOT NULL,
content text NOT NULL
);
alter table only comments
add constraint "user" foreign key (commenter_id) references users(id),
add constraint comments_task_id_fkey foreign key (task_id, user_id) references users_tasks(task_id, user_id);
create table private.articles (
id integer primary key,
body text,
owner name not null
);
create table private.article_stars (
article_id integer not null,
user_id integer not null,
created_at timestamp without time zone default now() not null,
primary key (article_id, user_id)
);
alter table only private.article_stars
add constraint article foreign key (article_id) references private.articles(id),
add constraint "user" foreign key (user_id) references test.users(id);
CREATE VIEW limited_article_stars AS
SELECT article_id, user_id, created_at FROM private.article_stars;
CREATE VIEW "articleStars" AS
SELECT article_stars.article_id AS "articleId",
article_stars.user_id AS "userId",
article_stars.created_at AS "createdAt"
FROM private.article_stars;
CREATE VIEW articles AS
SELECT articles.id,
articles.body,
articles.owner
FROM private.articles;
-- --
-- Name: tsearch; Type: TABLE; Schema: test; Owner: - -- Name: tsearch; Type: TABLE; Schema: test; Owner: -
@@ -734,28 +689,6 @@ CREATE TABLE tsearch (
text_search_vector tsvector text_search_vector tsvector
); );
--
-- Name: users; Type: TABLE; Schema: test; Owner: -
--
CREATE TABLE users (
id integer NOT NULL,
name text NOT NULL
);
--
-- Name: users_tasks; Type: TABLE; Schema: test; Owner: -
--
CREATE TABLE users_tasks (
user_id integer NOT NULL,
task_id integer NOT NULL
);
CREATE TABLE "Escap3e;" ( CREATE TABLE "Escap3e;" (
"so6meIdColumn" integer primary key "so6meIdColumn" integer primary key
); );
@@ -773,7 +706,6 @@ CREATE TABLE clashing_column (
t text t text
); );
-- --
-- Name: id; Type: DEFAULT; Schema: test; Owner: - -- Name: id; Type: DEFAULT; Schema: test; Owner: -
-- --
@@ -788,13 +720,6 @@ ALTER TABLE ONLY auto_incrementing_pk ALTER COLUMN id SET DEFAULT nextval('auto_
ALTER TABLE ONLY has_fk ALTER COLUMN id SET DEFAULT nextval('has_fk_id_seq'::regclass); ALTER TABLE ONLY has_fk ALTER COLUMN id SET DEFAULT nextval('has_fk_id_seq'::regclass);
--
-- Name: id; Type: DEFAULT; Schema: test; Owner: -
--
ALTER TABLE ONLY items ALTER COLUMN id SET DEFAULT nextval('items_id_seq'::regclass);
SET search_path = postgrest, pg_catalog; SET search_path = postgrest, pg_catalog;
-- --
@@ -805,24 +730,6 @@ ALTER TABLE ONLY auth
ADD CONSTRAINT auth_pkey PRIMARY KEY (id); ADD CONSTRAINT auth_pkey PRIMARY KEY (id);
SET search_path = private, pg_catalog;
--
-- Name: articles_pkey; Type: CONSTRAINT; Schema: private; Owner: -
--
ALTER TABLE ONLY articles
ADD CONSTRAINT articles_pkey PRIMARY KEY (id);
--
-- Name: user_article; Type: CONSTRAINT; Schema: private; Owner: -
--
ALTER TABLE ONLY article_stars
ADD CONSTRAINT user_article PRIMARY KEY (article_id, user_id);
SET search_path = test, pg_catalog; SET search_path = test, pg_catalog;
-- --
@@ -840,23 +747,6 @@ ALTER TABLE ONLY authors_only
ALTER TABLE ONLY auto_incrementing_pk ALTER TABLE ONLY auto_incrementing_pk
ADD CONSTRAINT auto_incrementing_pk_pkey PRIMARY KEY (id); ADD CONSTRAINT auto_incrementing_pk_pkey PRIMARY KEY (id);
--
-- Name: clients_pkey; Type: CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY clients
ADD CONSTRAINT clients_pkey PRIMARY KEY (id);
--
-- Name: comments_pkey; Type: CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY comments
ADD CONSTRAINT comments_pkey PRIMARY KEY (id);
-- --
-- Name: complex_items_pkey; Type: CONSTRAINT; Schema: test; Owner: - -- Name: complex_items_pkey; Type: CONSTRAINT; Schema: test; Owner: -
-- --
@@ -888,15 +778,6 @@ ALTER TABLE ONLY simple_pk
ALTER TABLE ONLY has_fk ALTER TABLE ONLY has_fk
ADD CONSTRAINT has_fk_pkey PRIMARY KEY (id); ADD CONSTRAINT has_fk_pkey PRIMARY KEY (id);
--
-- Name: items_pkey; Type: CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY items
ADD CONSTRAINT items_pkey PRIMARY KEY (id);
-- --
-- Name: menagerie_pkey; Type: CONSTRAINT; Schema: test; Owner: - -- Name: menagerie_pkey; Type: CONSTRAINT; Schema: test; Owner: -
-- --
@@ -905,46 +786,6 @@ ALTER TABLE ONLY menagerie
ADD CONSTRAINT menagerie_pkey PRIMARY KEY ("integer"); ADD CONSTRAINT menagerie_pkey PRIMARY KEY ("integer");
--
-- Name: project_user; Type: CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY users_projects
ADD CONSTRAINT project_user PRIMARY KEY (project_id, user_id);
--
-- Name: projects_pkey; Type: CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY projects
ADD CONSTRAINT projects_pkey PRIMARY KEY (id);
--
-- Name: task_user; Type: CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY users_tasks
ADD CONSTRAINT task_user PRIMARY KEY (task_id, user_id);
--
-- Name: tasks_pkey; Type: CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY tasks
ADD CONSTRAINT tasks_pkey PRIMARY KEY (id);
--
-- Name: users_pkey; Type: CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
SET search_path = postgrest, pg_catalog; SET search_path = postgrest, pg_catalog;
-- --
@@ -971,43 +812,8 @@ SET search_path = test, pg_catalog;
CREATE TRIGGER secrets_owner_track BEFORE INSERT OR UPDATE ON authors_only FOR EACH ROW EXECUTE PROCEDURE postgrest.set_authors_only_owner(); CREATE TRIGGER secrets_owner_track BEFORE INSERT OR UPDATE ON authors_only FOR EACH ROW EXECUTE PROCEDURE postgrest.set_authors_only_owner();
SET search_path = private, pg_catalog;
--
-- Name: article_stars_article_id_fkey; Type: FK CONSTRAINT; Schema: private; Owner: -
--
ALTER TABLE ONLY article_stars
ADD CONSTRAINT article_stars_article_id_fkey FOREIGN KEY (article_id) REFERENCES articles(id);
--
-- Name: article_stars_user_id_fkey; Type: FK CONSTRAINT; Schema: private; Owner: -
--
ALTER TABLE ONLY article_stars
ADD CONSTRAINT article_stars_user_id_fkey FOREIGN KEY (user_id) REFERENCES test.users(id);
SET search_path = test, pg_catalog; SET search_path = test, pg_catalog;
--
-- Name: comments_commenter_id_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY comments
ADD CONSTRAINT comments_commenter_id_fkey FOREIGN KEY (commenter_id) REFERENCES users(id);
--
-- Name: comments_task_id_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY comments
ADD CONSTRAINT comments_task_id_fkey FOREIGN KEY (task_id, user_id) REFERENCES users_tasks(task_id, user_id);
-- --
-- Name: has_fk_fk_fkey; Type: FK CONSTRAINT; Schema: test; Owner: - -- Name: has_fk_fk_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -
-- --
@@ -1023,55 +829,6 @@ ALTER TABLE ONLY has_fk
ALTER TABLE ONLY has_fk ALTER TABLE ONLY has_fk
ADD CONSTRAINT has_fk_simple_fk_fkey FOREIGN KEY (simple_fk) REFERENCES simple_pk(k); ADD CONSTRAINT has_fk_simple_fk_fkey FOREIGN KEY (simple_fk) REFERENCES simple_pk(k);
--
-- Name: projects_client_id_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY projects
ADD CONSTRAINT projects_client_id_fkey FOREIGN KEY (client_id) REFERENCES clients(id);
--
-- Name: tasks_project_id_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY tasks
ADD CONSTRAINT tasks_project_id_fkey FOREIGN KEY (project_id) REFERENCES projects(id);
--
-- Name: users_projects_project_id_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY users_projects
ADD CONSTRAINT users_projects_project_id_fkey FOREIGN KEY (project_id) REFERENCES projects(id);
--
-- Name: users_projects_user_id_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY users_projects
ADD CONSTRAINT users_projects_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
--
-- Name: users_tasks_task_id_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY users_tasks
ADD CONSTRAINT users_tasks_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id);
--
-- Name: users_tasks_user_id_fkey; Type: FK CONSTRAINT; Schema: test; Owner: -
--
ALTER TABLE ONLY users_tasks
ADD CONSTRAINT users_tasks_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
create table addresses ( create table addresses (
id int not null unique, id int not null unique,
address text not null address text not null
@@ -1083,6 +840,8 @@ create table orders (
billing_address_id int references addresses(id), billing_address_id int references addresses(id),
shipping_address_id int references addresses(id) shipping_address_id int references addresses(id)
); );
alter table orders rename constraint orders_billing_address_id_fkey to billing;
alter table orders rename constraint orders_shipping_address_id_fkey to shipping;
CREATE FUNCTION getproject(id int) RETURNS SETOF projects CREATE FUNCTION getproject(id int) RETURNS SETOF projects
LANGUAGE sql LANGUAGE sql
@@ -1406,12 +1165,11 @@ create table test.managers (
create table test.organizations ( create table test.organizations (
id integer primary key, id integer primary key,
name text, name text,
referee integer, referee integer references organizations(id),
auditor integer, auditor integer references organizations(id),
manager_id integer references managers(id) manager_id integer references managers(id)
); );
alter table only test.organizations add constraint pptr1 foreign key (referee) references test.organizations(id); alter table only test.organizations rename constraint organizations_manager_id_fkey to manager;
alter table only test.organizations add constraint pptr2 foreign key (auditor) references test.organizations(id);
create table private.authors( create table private.authors(
id integer primary key, id integer primary key,
@@ -1785,3 +1543,102 @@ create table private.referrals (
create view test.pages as select * from private.pages; create view test.pages as select * from private.pages;
create view test.referrals as select * from private.referrals; create view test.referrals as select * from private.referrals;
create table big_projects (
big_project_id serial primary key,
name text
);
create table sites (
site_id serial primary key
, name text
, main_project_id int null references big_projects (big_project_id)
);
alter table sites rename constraint sites_main_project_id_fkey to main_project;
create table jobs (
job_id uuid primary key
, name text
, site_id int not null references sites (site_id)
, big_project_id int not null references big_projects (big_project_id)
);
create view main_jobs as
select * from jobs
where site_id in (select site_id from sites where main_project_id is not null);
-- junction in a private schema, just to make sure we don't leak it on resource embedding
-- if it leaks it would show on the disambiguation error tests
create view private.priv_jobs as
select * from jobs;
-- tables to show our limitation when trying to do an m2m embed
-- with a junction table that has more than two foreign keys
create table whatev_projects (
id serial primary key,
name text
);
create table whatev_sites (
id serial primary key
, name text
);
create table whatev_jobs (
job_id uuid primary key
, name text
, site_id_1 int not null references whatev_sites (id)
, project_id_1 int not null references whatev_projects (id)
, site_id_2 int not null references whatev_sites (id)
, project_id_2 int not null references whatev_projects (id)
);
-- circular reference
create table agents (
id int primary key
, name text
, department_id int
);
create table departments (
id int primary key
, name text
, head_id int references agents(id)
);
ALTER TABLE agents
ADD CONSTRAINT agents_department_id_fkey foreign key (department_id) REFERENCES departments(id);
-- composite key disambiguation
create table schedules (
id int primary key
, name text
, start_at timetz
, end_at timetz
);
create table activities (
id int
, schedule_id int
, car_id text
, camera_id text
, primary key (id, schedule_id)
);
alter table activities
add constraint schedule foreign key (schedule_id)
references schedules (id);
create table unit_workdays (
unit_id int
, day date
, fst_shift_activity_id int
, fst_shift_schedule_id int
, snd_shift_activity_id int
, snd_shift_schedule_id int
, primary key (unit_id, day)
);
alter table unit_workdays
add constraint fst_shift foreign key (fst_shift_activity_id, fst_shift_schedule_id)
references activities (id, schedule_id),
add constraint snd_shift foreign key (snd_shift_activity_id, snd_shift_schedule_id)
references activities (id, schedule_id);