diff --git a/CHANGELOG.md b/CHANGELOG.md index 089e4b626..a7130c0ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/PostgREST/DbRequestBuilder.hs b/src/PostgREST/DbRequestBuilder.hs index 8503d1136..e6a5865e6 100644 --- a/src/PostgREST/DbRequestBuilder.hs +++ b/src/PostgREST/DbRequestBuilder.hs @@ -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 = diff --git a/src/PostgREST/DbStructure.hs b/src/PostgREST/DbStructure.hs index 5c2e486c4..53d2f6c6e 100644 --- a/src/PostgREST/DbStructure.hs +++ b/src/PostgREST/DbStructure.hs @@ -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 diff --git a/src/PostgREST/Error.hs b/src/PostgREST/Error.hs index 8148b0c0b..05b74439f 100644 --- a/src/PostgREST/Error.hs +++ b/src/PostgREST/Error.hs @@ -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 diff --git a/src/PostgREST/Parsers.hs b/src/PostgREST/Parsers.hs index b1a3ad0f6..7962636ab 100644 --- a/src/PostgREST/Parsers.hs +++ b/src/PostgREST/Parsers.hs @@ -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 diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index bc5ba18fb..e4070b924 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -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)) diff --git a/test/Feature/EmbedDisambiguationSpec.hs b/test/Feature/EmbedDisambiguationSpec.hs index 9c6299bc1..242c5ece1 100644 --- a/test/Feature/EmbedDisambiguationSpec.hs +++ b/test/Feature/EmbedDisambiguationSpec.hs @@ -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] } + diff --git a/test/Feature/QuerySpec.hs b/test/Feature/QuerySpec.hs index da50751be..229cfb917 100644 --- a/test/Feature/QuerySpec.hs +++ b/test/Feature/QuerySpec.hs @@ -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|[ diff --git a/test/Feature/RpcSpec.hs b/test/Feature/RpcSpec.hs index 8751f7a35..a4c0c61a1 100644 --- a/test/Feature/RpcSpec.hs +++ b/test/Feature/RpcSpec.hs @@ -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] } diff --git a/test/fixtures/data.sql b/test/fixtures/data.sql index 6881631f2..c37da7519 100644 --- a/test/fixtures/data.sql +++ b/test/fixtures/data.sql @@ -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); diff --git a/test/fixtures/privileges.sql b/test/fixtures/privileges.sql index ec60c0626..cca4dd128 100644 --- a/test/fixtures/privileges.sql +++ b/test/fixtures/privileges.sql @@ -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; diff --git a/test/fixtures/schema.sql b/test/fixtures/schema.sql index 105e9fa06..5b3720f13 100755 --- a/test/fixtures/schema.sql +++ b/test/fixtures/schema.sql @@ -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);