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:
@@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- #1383, Add support for HEAD request - @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
|
||||
- #1430, Allow specifying the foreign key constraint name(`/source?select=fk_constraint(*)`) to disambiguate an embedding - @steve-chavez
|
||||
|
||||
### 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
|
||||
- #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
|
||||
- #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
|
||||
|
||||
|
||||
@@ -16,18 +16,14 @@ module PostgREST.DbRequestBuilder (
|
||||
, mutateRequest
|
||||
) where
|
||||
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import qualified Data.Set as S
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import qualified Data.Set as S
|
||||
|
||||
import Control.Arrow ((***))
|
||||
import Data.Either.Combinators (mapLeft)
|
||||
import Data.Foldable (foldr1)
|
||||
import Data.List (delete, head, (!!))
|
||||
import Data.Maybe (fromJust)
|
||||
import Data.List (delete)
|
||||
import Data.Text (isInfixOf)
|
||||
import Text.Regex.TDFA ((=~))
|
||||
import Unsafe (unsafeHead)
|
||||
|
||||
import Control.Applicative
|
||||
import Data.Tree
|
||||
@@ -38,7 +34,7 @@ import PostgREST.Error (ApiRequestError (..), errorResponseFor)
|
||||
import PostgREST.Parsers
|
||||
import PostgREST.RangeQuery (NonnegRange, allRange, restrictRange)
|
||||
import PostgREST.Types
|
||||
import Protolude hiding (from, head)
|
||||
import Protolude hiding (from)
|
||||
|
||||
readRequest :: Schema -> TableName -> Maybe Integer -> [Relation] -> ApiRequest -> Either Response ReadRequest
|
||||
readRequest schema rootTableName maxRows allRels apiRequest =
|
||||
@@ -49,20 +45,20 @@ readRequest schema rootTableName maxRows allRels apiRequest =
|
||||
(initReadRequest rootName <$> pRequestSelect sel)
|
||||
where
|
||||
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).
|
||||
-- So we need a FROM {sourceCTEName} instead of FROM {tableName}.
|
||||
rootWithRelations :: Schema -> TableName -> [Relation] -> Action -> (QualifiedIdentifier, [Relation])
|
||||
rootWithRelations schema rootTableName allRels action = case action of
|
||||
rootWithRels :: Schema -> TableName -> [Relation] -> Action -> (QualifiedIdentifier, [Relation])
|
||||
rootWithRels schema rootTableName allRels action = case action of
|
||||
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
|
||||
-- To enable embedding in the sourceCTEName cases we need to replace the foreign key tableName in the Relation
|
||||
-- with {sourceCTEName}. This way findRelation can find Relations with sourceCTEName.
|
||||
toSourceRelation :: Relation -> Maybe Relation
|
||||
toSourceRelation r@Relation{relTable=t}
|
||||
-- with {sourceCTEName}. This way findRel can find relationships with sourceCTEName.
|
||||
toSourceRel :: Relation -> Maybe Relation
|
||||
toSourceRel r@Relation{relTable=t}
|
||||
| rootTableName == tableName t = Just $ r {relTable=t {tableName=sourceCTEName}}
|
||||
| otherwise = Nothing
|
||||
|
||||
@@ -77,14 +73,14 @@ initReadRequest rootQi =
|
||||
rootName = qiName rootQi
|
||||
initial = Node (Select [] rootQi Nothing [] [] [] [] allRange, (rootName, Nothing, Nothing, Nothing, rootDepth)) []
|
||||
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
|
||||
case fldForest of
|
||||
[] -> Node (q {select=fld:select q}, i) rForest
|
||||
_ -> Node (q, i) $
|
||||
foldr (treeEntry nxtDepth)
|
||||
(Node (Select [] (QualifiedIdentifier rootSchema fn) Nothing [] [] [] [] allRange,
|
||||
(fn, Nothing, alias, relationDetail, nxtDepth)) [])
|
||||
(fn, Nothing, alias, embedHint, nxtDepth)) [])
|
||||
fldForest:rForest
|
||||
|
||||
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 allRels request =
|
||||
addRelations schema allRels Nothing request
|
||||
addRels schema allRels Nothing request
|
||||
>>= addJoinConditions Nothing
|
||||
|
||||
addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either ApiRequestError ReadRequest
|
||||
addRelations schema allRelations parentNode (Node (query@Select{from=tbl}, (nodeName, _, alias, relationDetail, depth)) forest) =
|
||||
addRels :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either ApiRequestError ReadRequest
|
||||
addRels schema allRels parentNode (Node (query@Select{from=tbl}, (nodeName, _, alias, hint, depth)) forest) =
|
||||
case parentNode of
|
||||
Just (Node (Select{from=parentNodeQi}, _) _) ->
|
||||
let newFrom r = if qiName tbl == nodeName then tableQi (relFTable r) else tbl
|
||||
newReadNode = (\r -> (query{from=newFrom r}, (nodeName, Just r, alias, Nothing, depth))) <$> rel
|
||||
parentNodeTable = qiName parentNodeQi
|
||||
results = findRelation schema allRelations 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
|
||||
rel = findRel schema allRels (qiName parentNodeQi) nodeName hint
|
||||
in
|
||||
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)
|
||||
where
|
||||
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]
|
||||
findRelation schema allRelations parentTableName nodeName relationDetail =
|
||||
filter (\Relation{relTable, relColumns, relFTable, relFColumns, relType, relLinkTable} ->
|
||||
-- Both relation ends need to be on the exposed schema
|
||||
schema == tableSchema relTable && schema == tableSchema relFTable &&
|
||||
case relationDetail of
|
||||
Nothing ->
|
||||
|
||||
-- (request) => projects { ..., clients{...} }
|
||||
-- will match
|
||||
-- (relation type) => M2O
|
||||
-- (entity) => clients {id}
|
||||
-- (foriegn entity) => projects {client_id}
|
||||
-- Finds a relationship between an origin and a target in the request: /origin?select=target(*)
|
||||
-- If more than one relationship is found then the request is ambiguous and we return an error.
|
||||
-- In that case the request can be disambiguated by adding precision to the target or by using a hint: /origin?select=target!hint(*)
|
||||
-- The elements will be matched according to these rules:
|
||||
-- origin = table / view
|
||||
-- target = table / view / constraint / column-from-origin
|
||||
-- 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)
|
||||
findRel :: Schema -> [Relation] -> NodeName -> NodeName -> Maybe EmbedHint -> Either ApiRequestError Relation
|
||||
findRel schema allRels origin target hint =
|
||||
case rel of
|
||||
[] -> Left $ NoRelBetween origin target
|
||||
[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
|
||||
nodeName == tableName relFTable -- clients
|
||||
) ||
|
||||
-- /projects?select=clients(*)
|
||||
origin == tableName relTable && -- projects
|
||||
target == tableName relFTable || -- clients
|
||||
|
||||
-- (request) => projects { ..., client_id{...} }
|
||||
-- will match
|
||||
-- (relation type) => M2O
|
||||
-- (entity) => clients {id}
|
||||
-- (foriegn entity) => projects {client_id}
|
||||
(
|
||||
parentTableName == tableName relTable && -- projects
|
||||
length relColumns == 1 &&
|
||||
-- match common foreign key names(table_name_id, table_name_fk) to table_name
|
||||
(toS ("^" <> colName (unsafeHead relColumns) <> "_?(?:|[iI][dD]|[fF][kK])$") :: BS.ByteString) =~
|
||||
(toS nodeName :: BS.ByteString) -- client_id
|
||||
-- /projects?select=projects_client_id_fkey(*)
|
||||
(
|
||||
origin == tableName relTable && -- projects
|
||||
Just target == relConstraint -- projects_client_id_fkey
|
||||
) ||
|
||||
-- /projects?select=client_id(*)
|
||||
(
|
||||
origin == tableName relTable && -- projects
|
||||
matchFKSingleCol (Just target) relColumns -- 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
|
||||
)
|
||||
)
|
||||
|
||||
-- (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
|
||||
) allRels
|
||||
|
||||
-- previousAlias is only used for the case of self joins
|
||||
addJoinConditions :: Maybe Alias -> ReadRequest -> Either ApiRequestError ReadRequest
|
||||
addJoinConditions previousAlias (Node node@(query@Select{from=tbl}, nodeProps@(_, relation, _, _, depth)) forest) =
|
||||
case relation of
|
||||
Just rel@Relation{relType=O2M} -> Node (augmentQuery rel, nodeProps) <$> updatedForest
|
||||
Just rel@Relation{relType=M2O} -> Node (augmentQuery rel, nodeProps) <$> updatedForest
|
||||
Just rel@Relation{relType=M2M, relLinkTable=lTable} ->
|
||||
case lTable of
|
||||
Just linkTable ->
|
||||
let rq = augmentQuery rel in
|
||||
Node (rq{implicitJoins=tableQi linkTable:implicitJoins rq}, nodeProps) <$> updatedForest
|
||||
addJoinConditions previousAlias (Node node@(query@Select{from=tbl}, nodeProps@(_, rel, _, _, depth)) forest) =
|
||||
case rel of
|
||||
Just r@Relation{relType=O2M} -> Node (augmentQuery r, nodeProps) <$> updatedForest
|
||||
Just r@Relation{relType=M2O} -> Node (augmentQuery r, nodeProps) <$> updatedForest
|
||||
Just r@Relation{relType=M2M, relJunction=junction} ->
|
||||
case junction of
|
||||
Just Junction{junTable} ->
|
||||
let rq = augmentQuery r in
|
||||
Node (rq{implicitJoins=tableQi junTable:implicitJoins rq}, nodeProps) <$> updatedForest
|
||||
Nothing ->
|
||||
Left UnknownRelation
|
||||
Nothing -> Node node <$> updatedForest
|
||||
where
|
||||
newAlias = case isSelfReference <$> relation of
|
||||
newAlias = case isSelfReference <$> rel of
|
||||
Just True
|
||||
| depth /= 0 -> Just (qiName tbl <> "_" <> show depth) -- root node doesn't get aliased
|
||||
| otherwise -> Nothing
|
||||
_ -> Nothing
|
||||
augmentQuery rel =
|
||||
augmentQuery r =
|
||||
foldr
|
||||
(\jc rq@Select{joinConditions=jcs} -> rq{joinConditions=jc:jcs})
|
||||
query{fromAlias=newAlias}
|
||||
(getJoinConditions previousAlias newAlias rel)
|
||||
(getJoinConditions previousAlias newAlias r)
|
||||
updatedForest = mapM (addJoinConditions newAlias) forest
|
||||
|
||||
-- previousAlias and newAlias are used in the case of self joins
|
||||
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
|
||||
O2M ->
|
||||
zipWith (toJoinCondition tN ftN) cols fCols
|
||||
M2O ->
|
||||
zipWith (toJoinCondition tN ftN) cols fCols
|
||||
M2M ->
|
||||
let ltN = maybe "" tableName lt in
|
||||
zipWith (toJoinCondition tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toJoinCondition ftN ltN) fCols (fromMaybe [] lc2)
|
||||
M2M -> case jun of
|
||||
Just (Junction jt _ jc1 _ jc2) ->
|
||||
let jtn = tableName jt in
|
||||
zipWith (toJoinCondition tN jtn) cols jc1 ++ zipWith (toJoinCondition ftN jtn) fCols jc2
|
||||
Nothing -> []
|
||||
where
|
||||
toJoinCondition :: Text -> Text -> Column -> Column -> JoinCondition
|
||||
toJoinCondition tb ftb c fc =
|
||||
|
||||
@@ -95,9 +95,10 @@ decodeRels :: [Table] -> [Column] -> HD.Result [Relation]
|
||||
decodeRels tables cols =
|
||||
mapMaybe (relFromRow tables cols) <$> HD.rowList relRow
|
||||
where
|
||||
relRow = (,,,,,)
|
||||
relRow = (,,,,,,)
|
||||
<$> column HD.text
|
||||
<*> column HD.text
|
||||
<*> column HD.text
|
||||
<*> column (HD.array (HD.dimension replicateM (element 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 allSrcCols = concatMap (\rel ->
|
||||
rel : case rel of
|
||||
Relation{relType=M2O, relTable, relColumns, relFTable, relFColumns} ->
|
||||
Relation{relType=M2O, relTable, relColumns, relConstraint, relFTable, relFColumns} ->
|
||||
|
||||
let srcColsGroupedByView :: [Column] -> [[SourceColumn]]
|
||||
srcColsGroupedByView relCols = L.groupBy (\(_, viewCol1) (_, viewCol2) -> colTable viewCol1 == colTable viewCol2) $
|
||||
@@ -307,20 +308,22 @@ addViewM2ORels allSrcCols = concatMap (\rel ->
|
||||
|
||||
viewTableM2O =
|
||||
[ Relation (getView srcCols) (snd <$> srcCols `sortAccordingTo` relColumns)
|
||||
relFTable relFColumns
|
||||
M2O Nothing Nothing Nothing
|
||||
relConstraint relFTable relFColumns
|
||||
M2O Nothing
|
||||
| srcCols <- relSrcCols, srcCols `allSrcColsOf` relColumns ]
|
||||
|
||||
tableViewM2O =
|
||||
[ Relation relTable relColumns
|
||||
relConstraint
|
||||
(getView fSrcCols) (snd <$> fSrcCols `sortAccordingTo` relFColumns)
|
||||
M2O Nothing Nothing Nothing
|
||||
M2O Nothing
|
||||
| fSrcCols <- relFSrcCols, fSrcCols `allSrcColsOf` relFColumns ]
|
||||
|
||||
viewViewM2O =
|
||||
[ Relation (getView srcCols) (snd <$> srcCols `sortAccordingTo` relColumns)
|
||||
relConstraint
|
||||
(getView fSrcCols) (snd <$> fSrcCols `sortAccordingTo` relFColumns)
|
||||
M2O Nothing Nothing Nothing
|
||||
M2O Nothing
|
||||
| srcCols <- relSrcCols, srcCols `allSrcColsOf` relColumns
|
||||
, fSrcCols <- relFSrcCols, fSrcCols `allSrcColsOf` relFColumns ]
|
||||
|
||||
@@ -329,12 +332,12 @@ addViewM2ORels allSrcCols = concatMap (\rel ->
|
||||
_ -> [])
|
||||
|
||||
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 rels = rels ++ addMirrorRelation (mapMaybe link2Relation links)
|
||||
addM2MRels rels = rels ++ addMirrorRel (mapMaybe junction2Rel junctions)
|
||||
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{relTable=Table{tableSchema=s, tableName=t}} = s <> "_" <> t
|
||||
-- Reference : https://wiki.haskell.org/99_questions/Solutions/26
|
||||
@@ -342,14 +345,15 @@ addM2MRels rels = rels ++ addMirrorRelation (mapMaybe link2Relation links)
|
||||
combinations 0 _ = [ [] ]
|
||||
combinations n xs = [ y:ys | y:xs' <- tails xs
|
||||
, ys <- combinations (n-1) xs']
|
||||
addMirrorRelation = concatMap (\rel@(Relation t c ft fc _ lt lc1 lc2) -> [rel, Relation ft fc t c M2M lt lc2 lc1])
|
||||
link2Relation [
|
||||
Relation{relTable=lt, relColumns=lc1, relFTable=t, relFColumns=c},
|
||||
Relation{ relColumns=lc2, relFTable=ft, relFColumns=fc}
|
||||
junction2Rel [
|
||||
Relation{relTable=jt, relColumns=jc1, relConstraint=const1, relFTable=t, relFColumns=c},
|
||||
Relation{ relColumns=jc2, relConstraint=const2, 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
|
||||
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 srcCols = concatMap (\pk ->
|
||||
@@ -574,32 +578,29 @@ allM2ORels tabs cols =
|
||||
sql = [q|
|
||||
SELECT ns1.nspname AS table_schema,
|
||||
tab.relname AS table_name,
|
||||
conname AS constraint_name,
|
||||
column_info.cols AS columns,
|
||||
ns2.nspname AS foreign_table_schema,
|
||||
other.relname AS foreign_table_name,
|
||||
column_info.refs AS foreign_columns
|
||||
FROM pg_constraint,
|
||||
LATERAL (SELECT array_agg(cols.attname) AS cols,
|
||||
array_agg(cols.attnum) AS nums,
|
||||
array_agg(refs.attname) AS refs
|
||||
FROM ( SELECT unnest(conkey) AS col, unnest(confkey) AS ref) k,
|
||||
LATERAL (SELECT * FROM pg_attribute
|
||||
WHERE attrelid = conrelid AND attnum = col)
|
||||
AS cols,
|
||||
LATERAL (SELECT * FROM pg_attribute
|
||||
WHERE attrelid = confrelid AND attnum = ref)
|
||||
AS refs)
|
||||
AS column_info,
|
||||
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
|
||||
LATERAL (
|
||||
SELECT array_agg(cols.attname) AS cols,
|
||||
array_agg(cols.attnum) AS nums,
|
||||
array_agg(refs.attname) AS refs
|
||||
FROM ( SELECT unnest(conkey) AS col, unnest(confkey) AS ref) k,
|
||||
LATERAL (SELECT * FROM pg_attribute WHERE attrelid = conrelid AND attnum = col) AS cols,
|
||||
LATERAL (SELECT * FROM pg_attribute WHERE attrelid = confrelid AND attnum = ref) AS refs) AS column_info,
|
||||
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
|
||||
ORDER BY (conrelid, column_info.nums) |]
|
||||
|
||||
relFromRow :: [Table] -> [Column] -> (Text, Text, [Text], Text, Text, [Text]) -> Maybe Relation
|
||||
relFromRow allTabs allCols (rs, rt, rcs, frs, frt, frcs) =
|
||||
Relation <$> table <*> cols <*> tableF <*> colsF <*> pure M2O <*> pure Nothing <*> pure Nothing <*> pure Nothing
|
||||
relFromRow :: [Table] -> [Column] -> (Text, Text, Text, [Text], Text, Text, [Text]) -> Maybe Relation
|
||||
relFromRow allTabs allCols (rs, rt, cn, rcs, frs, frt, frcs) =
|
||||
Relation <$> table <*> cols <*> pure (Just cn) <*> tableF <*> colsF <*> pure M2O <*> pure Nothing
|
||||
where
|
||||
findTable s t = find (\tbl -> tableSchema tbl == s && tableName tbl == t) allTabs
|
||||
findCol s t c = find (\col -> tableSchema (colTable col) == s && tableName (colTable col) == t && colName col == c) allCols
|
||||
|
||||
+14
-18
@@ -80,9 +80,9 @@ instance JSON.ToJSON ApiRequestError where
|
||||
toJSON UnknownRelation = JSON.object [
|
||||
"message" .= ("Unknown relation" :: Text)]
|
||||
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 [
|
||||
"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),
|
||||
"details" .= (compressedRel <$> rels) ]
|
||||
toJSON UnsupportedVerb = JSON.object [
|
||||
@@ -93,27 +93,23 @@ instance JSON.ToJSON ApiRequestError where
|
||||
compressedRel :: Relation -> JSON.Value
|
||||
compressedRel rel =
|
||||
let
|
||||
-- | Format like "test.orders[billing_address_id]". For easier debugging the format is compressed instead of structured.
|
||||
fmt sch tbl cols = schTbl sch tbl <> joinCols cols
|
||||
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
|
||||
fmtTbl tbl = tableSchema tbl <> "." <> tableName tbl
|
||||
fmtEls els = "[" <> T.intercalate ", " els <> "]"
|
||||
in
|
||||
JSON.object $ [
|
||||
"source" .= fmt (tableSchema tab) (tableName tab) (colName <$> relColumns rel)
|
||||
, "target" .= fmt (tableSchema fTab) (tableName fTab) (colName <$> relFColumns rel)
|
||||
"origin" .= fmtTbl (relTable rel)
|
||||
, "target" .= fmtTbl (relFTable rel)
|
||||
, "cardinality" .= (show $ relType rel :: Text)
|
||||
] ++
|
||||
if relType rel == M2M
|
||||
then [
|
||||
"junction" .= case (relLinkTable rel, relLinkCols1 rel, relLinkCols2 rel) of
|
||||
(Just lt, Just lc1, Just lc2) -> fmtMany (tableSchema lt) (tableName lt) (colName <$> lc1) (colName <$> lc2)
|
||||
_ -> toS $ JSON.encode JSON.Null
|
||||
case (relType rel, relJunction rel, relConstraint rel) of
|
||||
(M2M, Just (Junction jt (Just const1) _ (Just const2) _), _) -> [
|
||||
"relationship" .= (fmtTbl jt <> fmtEls [const1] <> fmtEls [const2])
|
||||
]
|
||||
else mempty
|
||||
(_, _, Just relCon) -> [
|
||||
"relationship" .= (relCon <> fmtEls (colName <$> relColumns rel) <> fmtEls (colName <$> relFColumns rel))
|
||||
]
|
||||
(_, _, _) ->
|
||||
mempty
|
||||
|
||||
data PgError = PgError Authenticated P.UsageError
|
||||
type Authenticated = Bool
|
||||
|
||||
@@ -134,12 +134,12 @@ pRelationSelect :: Parser SelectItem
|
||||
pRelationSelect = lexeme $ try ( do
|
||||
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
||||
fld <- pField
|
||||
relationDetail <- optionMaybe (
|
||||
try ( char '!' *> pFieldName ) <|>
|
||||
try ( char '.' *> pFieldName ) -- TODO deprecated, remove in next major version
|
||||
hint <- optionMaybe (
|
||||
try ( char '!' *> pFieldName) <|>
|
||||
-- deprecated, remove in next major version
|
||||
try ( char '.' *> pFieldName)
|
||||
)
|
||||
|
||||
return (fld, Nothing, alias, relationDetail)
|
||||
return (fld, Nothing, alias, hint)
|
||||
)
|
||||
|
||||
pFieldSelect :: Parser SelectItem
|
||||
|
||||
+28
-20
@@ -264,28 +264,34 @@ data Cardinality = O2M -- ^ one-to-many, previously known as Parent
|
||||
| M2M -- ^ many-to-many, previously known as Many
|
||||
deriving Eq
|
||||
instance Show Cardinality where
|
||||
show O2M = "one-to-many"
|
||||
show M2O = "many-to-one"
|
||||
show M2M = "many-to-many"
|
||||
show O2M = "o2m"
|
||||
show M2O = "m2o"
|
||||
show M2M = "m2m"
|
||||
|
||||
type ConstraintName = Text
|
||||
|
||||
{-|
|
||||
The name 'Relation' here is used with the meaning
|
||||
"What is the relation between the current node and the parent node".
|
||||
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.
|
||||
"Relation"ship between two tables.
|
||||
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
|
||||
-}
|
||||
data Relation = Relation {
|
||||
relTable :: Table
|
||||
, relColumns :: [Column]
|
||||
, relFTable :: Table
|
||||
, relFColumns :: [Column]
|
||||
, relType :: Cardinality
|
||||
-- The Link attrs are used when Cardinality == Many
|
||||
, relLinkTable :: Maybe Table
|
||||
, relLinkCols1 :: Maybe [Column]
|
||||
, relLinkCols2 :: Maybe [Column]
|
||||
relTable :: Table
|
||||
, relColumns :: [Column]
|
||||
, relConstraint :: Maybe ConstraintName -- ^ Just on O2M/M2O, Nothing on M2M
|
||||
, relFTable :: Table
|
||||
, relFColumns :: [Column]
|
||||
, relType :: Cardinality
|
||||
, relJunction :: Maybe Junction -- ^ Junction for M2M Cardinality
|
||||
} deriving (Show, Eq)
|
||||
|
||||
-- | Junction table on an M2M relationship
|
||||
data Junction = Junction {
|
||||
junTable :: Table
|
||||
, junConstraint1 :: Maybe ConstraintName
|
||||
, junCols1 :: [Column]
|
||||
, junConstraint2 :: Maybe ConstraintName
|
||||
, junCols2 :: [Column]
|
||||
} deriving (Show, Eq)
|
||||
|
||||
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.
|
||||
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 RelationDetail)
|
||||
type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe EmbedHint)
|
||||
-- | 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"]
|
||||
type EmbedPath = [Text]
|
||||
data Filter = Filter { field::Field, opExpr::OpExpr } deriving (Show, Eq)
|
||||
@@ -453,7 +461,7 @@ data MutateQuery =
|
||||
type ReadRequest = Tree ReadNode
|
||||
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
|
||||
|
||||
-- First level FieldNames(e.g get a,b from /table?select=a,b,other(c,d))
|
||||
|
||||
@@ -13,170 +13,306 @@ import SpecHelper
|
||||
spec :: SpecWith Application
|
||||
spec =
|
||||
describe "resource embedding disambiguation" $ do
|
||||
|
||||
it "gives a 300 Multiple Choices error when the request is ambiguous" $ do
|
||||
get "/message?select=id,body,sender(name,sent)" `shouldRespondWith`
|
||||
[json|
|
||||
{
|
||||
"details": [
|
||||
context "ambiguous requests that give 300 Multiple Choices" $ 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`
|
||||
[json|
|
||||
{
|
||||
"cardinality": "many-to-one",
|
||||
"source": "test.message[sender]",
|
||||
"target": "test.person[id]"
|
||||
},
|
||||
{
|
||||
"cardinality": "many-to-one",
|
||||
"source": "test.message[sender]",
|
||||
"target": "test.person_detail[id]"
|
||||
"details": [
|
||||
{
|
||||
"cardinality": "m2o",
|
||||
"relationship": "message_sender_fkey[sender][id]",
|
||||
"origin": "test.message",
|
||||
"target": "test.person"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"message": "More than one relationship was found for message and sender"
|
||||
|]
|
||||
{ matchStatus = 300
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
|]
|
||||
{ matchStatus = 300
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
|
||||
get "/users?select=*,id(*)" `shouldRespondWith`
|
||||
[json|
|
||||
{
|
||||
"details": [
|
||||
it "errs when there are o2m and m2m cardinalities to the target table" $
|
||||
get "/sites?select=*,big_projects(*)" `shouldRespondWith`
|
||||
[json|
|
||||
{
|
||||
"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",
|
||||
"source": "test.users[id]",
|
||||
"target": "test.articleStars[userId]"
|
||||
},
|
||||
{
|
||||
"cardinality": "one-to-many",
|
||||
"source": "test.users[id]",
|
||||
"target": "test.limited_article_stars[user_id]"
|
||||
},
|
||||
{
|
||||
"cardinality": "one-to-many",
|
||||
"source": "test.users[id]",
|
||||
"target": "test.comments[commenter_id]"
|
||||
},
|
||||
{
|
||||
"cardinality": "one-to-many",
|
||||
"source": "test.users[id]",
|
||||
"target": "test.users_projects[user_id]"
|
||||
},
|
||||
{
|
||||
"cardinality": "one-to-many",
|
||||
"source": "test.users[id]",
|
||||
"target": "test.users_tasks[user_id]"
|
||||
},
|
||||
{
|
||||
"cardinality": "many-to-many",
|
||||
"junction": "private.article_stars[user_id][article_id]",
|
||||
"source": "test.users[id]",
|
||||
"target": "test.articles[id]"
|
||||
},
|
||||
{
|
||||
"cardinality": "many-to-many",
|
||||
"junction": "test.articleStars[userId][articleId]",
|
||||
"source": "test.users[id]",
|
||||
"target": "test.articles[id]"
|
||||
},
|
||||
{
|
||||
"cardinality": "many-to-many",
|
||||
"junction": "test.limited_article_stars[user_id][article_id]",
|
||||
"source": "test.users[id]",
|
||||
"target": "test.articles[id]"
|
||||
},
|
||||
{
|
||||
"cardinality": "many-to-many",
|
||||
"junction": "test.users_projects[user_id][project_id]",
|
||||
"source": "test.users[id]",
|
||||
"target": "test.projects[id]"
|
||||
},
|
||||
{
|
||||
"cardinality": "many-to-many",
|
||||
"junction": "test.users_projects[user_id][project_id]",
|
||||
"source": "test.users[id]",
|
||||
"target": "test.materialized_projects[id]"
|
||||
},
|
||||
{
|
||||
"cardinality": "many-to-many",
|
||||
"junction": "test.users_projects[user_id][project_id]",
|
||||
"source": "test.users[id]",
|
||||
"target": "test.projects_view[id]"
|
||||
},
|
||||
{
|
||||
"cardinality": "many-to-many",
|
||||
"junction": "test.users_projects[user_id][project_id]",
|
||||
"source": "test.users[id]",
|
||||
"target": "test.projects_view_alt[t_id]"
|
||||
},
|
||||
{
|
||||
"cardinality": "many-to-many",
|
||||
"junction": "test.users_tasks[user_id][task_id]",
|
||||
"source": "test.users[id]",
|
||||
"target": "test.tasks[id]"
|
||||
},
|
||||
{
|
||||
"cardinality": "many-to-many",
|
||||
"junction": "test.users_tasks[user_id][task_id]",
|
||||
"source": "test.users[id]",
|
||||
"target": "test.filtered_tasks[myId]"
|
||||
"day": "2019-12-02",
|
||||
"fst_shift": {
|
||||
"car_id": "CAR-349",
|
||||
"schedule": {
|
||||
"name": "morning"
|
||||
}
|
||||
},
|
||||
"snd_shift": {
|
||||
"camera_id": "CAM-123",
|
||||
"schedule": {
|
||||
"name": "night"
|
||||
}
|
||||
},
|
||||
"unit_id": 1
|
||||
}
|
||||
],
|
||||
"hint": "Disambiguate by choosing a relationship from the `details` key",
|
||||
"message": "More than one relationship was found for users and id"
|
||||
}
|
||||
|]
|
||||
{ matchStatus = 300
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works when requesting children 2 levels" $
|
||||
get "/clients?id=eq.1&select=id,projects:projects!client_id(id,tasks(id))" `shouldRespondWith`
|
||||
[json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
it "embeds by using two fks pointing to the same table" $
|
||||
get "/orders?id=eq.1&select=id, name, billing(address), shipping(address)" `shouldRespondWith`
|
||||
[json|[{"id":1,"name":"order 1","billing":{"address": "address 1"},"shipping":{"address": "address 2"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works with parent relation" $
|
||||
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 "fails if the fk is not known" $
|
||||
get "/message?select=id,sender:person!space(name)&id=lt.4" `shouldRespondWith`
|
||||
[json|{"message":"Could not find foreign keys between these entities. No relationship found between message and person"}|]
|
||||
{ matchStatus = 400
|
||||
, matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "fails with an unknown relation" $
|
||||
get "/message?select=id,sender:person!space(name)&id=lt.4" `shouldRespondWith`
|
||||
[json|{"message":"Could not find foreign keys between these entities, No relation found between message and person"}|]
|
||||
{ matchStatus = 400
|
||||
, matchHeaders = [matchContentTypeJson] }
|
||||
it "can request a parent with fk" $
|
||||
get "/comments?select=content,user(name)" `shouldRespondWith`
|
||||
[json|[ { "content": "Needs to be delivered ASAP", "user": { "name": "Angela Martin" } } ]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works with a parent view relation" $
|
||||
get "/message?select=id,body,sender:person_detail!sender(name,sent),recipient:person_detail!recipient(name,received)&id=lt.4" `shouldRespondWith`
|
||||
[json|
|
||||
[{"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] }
|
||||
it "can request two parents with fks" $
|
||||
get "/articleStars?select=createdAt,article(owner),user(name)&limit=1" `shouldRespondWith`
|
||||
[json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"owner": "postgrest_test_authenticator"},"user":{"name": "Angela Martin"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works with many<->many relation" $
|
||||
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":[]}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
it "can specify a view!fk" $
|
||||
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,"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 embed by FK column name" $
|
||||
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 specify a table!fk hint and request children 2 levels" $
|
||||
get "/clients?id=eq.1&select=id,projects:projects!client(id,tasks(id))" `shouldRespondWith`
|
||||
[json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can embed by FK column name and select the FK value 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 disambiguate with the fk in case of an o2m and m2m relationship to the same table" $
|
||||
get "/sites?select=name,main_project(name)&site_id=eq.1" `shouldRespondWith`
|
||||
[json| [ { "name": "site 1", "main_project": { "name": "big project 1" } } ] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "requests parents two levels up" $
|
||||
get "/tasks?id=eq.1&select=id,name,project:projects!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"}}}]|]
|
||||
context "using the column name of the FK to specify the relationship" $ do
|
||||
it "can embed by column" $
|
||||
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 "one self reference foreign key" $ do
|
||||
@@ -286,7 +422,7 @@ spec =
|
||||
"refereeds":[]}]
|
||||
}]|] { matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
-- TODO Remove in next major version(7.0)
|
||||
-- TODO Remove in next major version
|
||||
describe "old dot '.' symbol, deprecated" $
|
||||
it "still works" $ do
|
||||
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`
|
||||
[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] }
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import Network.Wai (Application)
|
||||
import Network.Wai.Test (SResponse (simpleHeaders))
|
||||
|
||||
import Network.HTTP.Types
|
||||
import Test.Hspec
|
||||
import Test.Hspec hiding (pendingWith)
|
||||
import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
|
||||
@@ -198,7 +198,7 @@ spec actualPgVersion = do
|
||||
|
||||
it "matches filtering nested items 2" $
|
||||
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
|
||||
, 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"}]}]|]
|
||||
{ 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" $
|
||||
get "/projects?select=name,client(clientId:id,name)" `shouldRespondWith`
|
||||
get "/projects?select=name,client:clients(clientId:id,name)" `shouldRespondWith`
|
||||
[json|[
|
||||
{"name":"Windows 7","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}}]|]
|
||||
{ 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" $
|
||||
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"}]}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
@@ -343,11 +327,6 @@ spec actualPgVersion = do
|
||||
[json|[{"user_id":2,"task_id":6,"comments":[{"content":"Needs to be delivered ASAP"}]}]|]
|
||||
{ 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
|
||||
it "can detect fk relations through views to tables in the public schema" $
|
||||
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" $
|
||||
get "/materialized_projects?select=*,users(*)" `shouldRespondWith` 200
|
||||
|
||||
it "can request parent without specifying primary key" $
|
||||
get "/articleStars?select=createdAt,article(owner),user(name)&limit=1" `shouldRespondWith`
|
||||
it "can request two parents" $
|
||||
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"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
@@ -448,7 +427,7 @@ spec actualPgVersion = do
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
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|
|
||||
[{"number_of_projects":1,"client":null},
|
||||
{"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`
|
||||
[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
|
||||
it "gives meaningful error messages when asc/desc/nulls{first,last} are misspelled" $ do
|
||||
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}]}] |]
|
||||
{ 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" $
|
||||
get "/Server%20Today?select=Just%20A%20Server%20Model&Just%20A%20Server%20Model=like.*91*" `shouldRespondWith`
|
||||
[json|[
|
||||
|
||||
@@ -158,10 +158,10 @@ spec actualPgVersion =
|
||||
|
||||
context "foreign entities embedding" $ 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}]}]|]
|
||||
{ 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}]}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
|
||||
Vendored
+46
@@ -526,3 +526,49 @@ INSERT INTO private.pages VALUES (2, 'http://postgrest.org/en/v6.0/admin.html');
|
||||
TRUNCATE TABLE private.referrals CASCADE;
|
||||
INSERT INTO private.referrals VALUES ('github.com', 1);
|
||||
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);
|
||||
|
||||
Vendored
+12
@@ -108,6 +108,18 @@ GRANT ALL ON TABLE
|
||||
, web_content
|
||||
, pages
|
||||
, referrals
|
||||
, big_projects
|
||||
, sites
|
||||
, jobs
|
||||
, main_jobs
|
||||
, whatev_projects
|
||||
, whatev_sites
|
||||
, whatev_jobs
|
||||
, agents
|
||||
, departments
|
||||
, schedules
|
||||
, activities
|
||||
, unit_workdays
|
||||
TO postgrest_test_anonymous;
|
||||
|
||||
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
|
||||
|
||||
Vendored
+166
-309
@@ -103,15 +103,10 @@ SET default_tablespace = '';
|
||||
|
||||
SET default_with_oids = false;
|
||||
|
||||
--
|
||||
-- Name: items; Type: TABLE; Schema: test; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE items (
|
||||
id bigint NOT NULL
|
||||
id bigserial primary key
|
||||
);
|
||||
|
||||
|
||||
CREATE FUNCTION always_true(test.items) RETURNS boolean
|
||||
LANGUAGE sql STABLE
|
||||
AS $$ SELECT true $$;
|
||||
@@ -364,59 +359,8 @@ CREATE TABLE auth (
|
||||
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;
|
||||
|
||||
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: -
|
||||
--
|
||||
@@ -463,24 +407,10 @@ ALTER SEQUENCE auto_incrementing_pk_id_seq OWNED BY auto_incrementing_pk.id;
|
||||
--
|
||||
|
||||
CREATE TABLE clients (
|
||||
id integer NOT NULL,
|
||||
id integer primary key,
|
||||
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: -
|
||||
--
|
||||
@@ -574,26 +504,6 @@ CREATE VIEW insertable_view_with_join AS
|
||||
FROM (has_fk
|
||||
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: -
|
||||
--
|
||||
@@ -663,11 +573,11 @@ CREATE TABLE insertonly (
|
||||
--
|
||||
|
||||
CREATE TABLE projects (
|
||||
id integer NOT NULL,
|
||||
id integer primary key,
|
||||
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: -
|
||||
@@ -695,25 +605,23 @@ CREATE TABLE simple_pk (
|
||||
extra character varying NOT NULL
|
||||
);
|
||||
|
||||
--
|
||||
-- Name: users_projects; Type: TABLE; Schema: test; Owner: -
|
||||
--
|
||||
CREATE TABLE users (
|
||||
id integer primary key,
|
||||
name text NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE users_projects (
|
||||
user_id integer NOT NULL,
|
||||
project_id integer NOT NULL
|
||||
user_id integer NOT NULL REFERENCES users(id),
|
||||
project_id integer NOT NULL REFERENCES projects(id),
|
||||
PRIMARY KEY (project_id, user_id)
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: tasks; Type: TABLE; Schema: test; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE tasks (
|
||||
id integer NOT NULL,
|
||||
id integer primary key,
|
||||
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
|
||||
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
|
||||
);
|
||||
|
||||
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: -
|
||||
@@ -734,28 +689,6 @@ CREATE TABLE tsearch (
|
||||
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;" (
|
||||
"so6meIdColumn" integer primary key
|
||||
);
|
||||
@@ -773,7 +706,6 @@ CREATE TABLE clashing_column (
|
||||
t text
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- 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);
|
||||
|
||||
|
||||
--
|
||||
-- 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;
|
||||
|
||||
--
|
||||
@@ -805,24 +730,6 @@ ALTER TABLE ONLY auth
|
||||
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;
|
||||
|
||||
--
|
||||
@@ -840,23 +747,6 @@ ALTER TABLE ONLY authors_only
|
||||
ALTER TABLE ONLY auto_incrementing_pk
|
||||
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: -
|
||||
--
|
||||
@@ -888,15 +778,6 @@ ALTER TABLE ONLY simple_pk
|
||||
ALTER TABLE ONLY has_fk
|
||||
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: -
|
||||
--
|
||||
@@ -905,46 +786,6 @@ ALTER TABLE ONLY menagerie
|
||||
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;
|
||||
|
||||
--
|
||||
@@ -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();
|
||||
|
||||
|
||||
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;
|
||||
|
||||
--
|
||||
-- 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: -
|
||||
--
|
||||
@@ -1023,55 +829,6 @@ 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);
|
||||
|
||||
|
||||
--
|
||||
-- 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 (
|
||||
id int not null unique,
|
||||
address text not null
|
||||
@@ -1083,6 +840,8 @@ create table orders (
|
||||
billing_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
|
||||
LANGUAGE sql
|
||||
@@ -1406,12 +1165,11 @@ create table test.managers (
|
||||
create table test.organizations (
|
||||
id integer primary key,
|
||||
name text,
|
||||
referee integer,
|
||||
auditor integer,
|
||||
referee integer references organizations(id),
|
||||
auditor integer references organizations(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 add constraint pptr2 foreign key (auditor) references test.organizations(id);
|
||||
alter table only test.organizations rename constraint organizations_manager_id_fkey to manager;
|
||||
|
||||
create table private.authors(
|
||||
id integer primary key,
|
||||
@@ -1785,3 +1543,102 @@ create table private.referrals (
|
||||
create view test.pages as select * from private.pages;
|
||||
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user