Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions docs/references/errors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -305,10 +305,6 @@ Related to a :ref:`schema_cache`. Most of the time, these errors are solved by :
| | | in the ``columns`` query parameter is not found. |
| PGRST204 | | |
+---------------+-------------+-------------------------------------------------------------+
| .. _pgrst205: | 404 | Caused when the :ref:`table specified <tables_views>` in |
| | | the URI is not found. |
| PGRST205 | | |
+---------------+-------------+-------------------------------------------------------------+

.. _pgrst3**:

Expand Down
16 changes: 0 additions & 16 deletions src/PostgREST/Error.hs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ import PostgREST.SchemaCache.Relationship (Cardinality (..),
RelationshipsMap)
import PostgREST.SchemaCache.Routine (Routine (..),
RoutineParam (..))
import PostgREST.SchemaCache.Table (Table (..))
import Protolude


Expand Down Expand Up @@ -250,7 +249,6 @@ data SchemaCacheError
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
| NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine]
| ColumnNotFound Text Text
| TableNotFound Text Text [Table]
deriving Show

instance PgrstError SchemaCacheError where
Expand All @@ -259,7 +257,6 @@ instance PgrstError SchemaCacheError where
status NoRelBetween{} = HTTP.status400
status NoRpc{} = HTTP.status404
status ColumnNotFound{} = HTTP.status400
status TableNotFound{} = HTTP.status404

headers _ = mempty

Expand All @@ -269,7 +266,6 @@ instance ErrorBody SchemaCacheError where
code NoRpc{} = "PGRST202"
code AmbiguousRpc{} = "PGRST203"
code ColumnNotFound{} = "PGRST204"
code TableNotFound{} = "PGRST205"

message (NoRelBetween parent child _ _ _) = "Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache"
message (AmbiguousRelBetween parent child _) = "Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'"
Expand All @@ -282,7 +278,6 @@ instance ErrorBody SchemaCacheError where
fmtPrms p = if null argumentKeys then " without parameters" else p
message (AmbiguousRpc procs) = "Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs]
message (ColumnNotFound rel col) = "Could not find the '" <> col <> "' column of '" <> rel <> "' in the schema cache"
message (TableNotFound schemaName relName _) = "Could not find the table '" <> schemaName <> "." <> relName <> "' in the schema cache"

details (NoRelBetween parent child embedHint schema _) = Just $ JSON.String $ "Searched for a foreign key relationship between '" <> parent <> "' and '" <> child <> maybe mempty ("' using the hint '" <>) embedHint <> "' in the schema '" <> schema <> "', but no matches were found."
details (AmbiguousRelBetween _ _ rels) = Just $ JSON.toJSONList (compressedRel <$> rels)
Expand Down Expand Up @@ -313,7 +308,6 @@ instance ErrorBody SchemaCacheError where
where
onlySingleParams = isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream]
hint (AmbiguousRpc _) = Just "Try renaming the parameters or the function itself in the database so function overloading can be resolved"
hint (TableNotFound schemaName relName tbls) = JSON.String <$> tableNotFoundHint schemaName relName tbls

hint _ = Nothing

Expand Down Expand Up @@ -426,16 +420,6 @@ noRpcHint schema procName params allProcs overloadedProcs =
| null overloadedProcs = Fuzzy.getOne fuzzySetOfProcs procName
| otherwise = (procName <>) <$> Fuzzy.getOne fuzzySetOfParams (listToText params)

-- |
-- Do a fuzzy search in all tables in the same schema and return closest result
tableNotFoundHint :: Text -> Text -> [Table] -> Maybe Text
tableNotFoundHint schema tblName tblList
= fmap (\tbl -> "Perhaps you meant the table '" <> schema <> "." <> tbl <> "'") perhapsTable
where
perhapsTable = Fuzzy.getOne fuzzyTableSet tblName
fuzzyTableSet = Fuzzy.fromList [ tableName tbl | tbl <- tblList, tableSchema tbl == schema]


compressedRel :: Relationship -> JSON.Value
-- An ambiguousness error cannot happen for computed relationships TODO refactor so this mempty is not needed
compressedRel ComputedRelationship{} = JSON.object mempty
Expand Down
57 changes: 22 additions & 35 deletions src/PostgREST/Plan.hs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import qualified PostgREST.SchemaCache.Routine as Routine

import Data.Either.Combinators (mapLeft, mapRight)
import Data.List (delete, lookup)
import Data.Maybe (fromJust)
import Data.Tree (Tree (..))

import PostgREST.ApiRequest (ApiRequest (..))
Expand Down Expand Up @@ -172,20 +171,18 @@ dbActionPlan dbAct conf apiReq sCache = case dbAct of

wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Bool -> Either Error CrudPlan
wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} headersOnly = do
qi <- findTable identifier (dbTables sCache)
rPlan <- readPlan qi conf sCache apiRequest
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
rPlan <- readPlan identifier conf sCache apiRequest
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
return $ WrappedReadPlan rPlan SQL.Read handler mediaType headersOnly qi
return $ WrappedReadPlan rPlan SQL.Read handler mediaType headersOnly identifier

mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error CrudPlan
mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf sCache = do
qi <- findTable identifier (dbTables sCache)
rPlan <- readPlan qi conf sCache apiRequest
mPlan <- mutatePlan mutation qi apiRequest sCache rPlan
rPlan <- readPlan identifier conf sCache apiRequest
mPlan <- mutatePlan mutation identifier apiRequest sCache rPlan
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType mutation qi
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType mutation identifier

callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CrudPlan
callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{preferHandling, invalidPrefs, preferMaxAffected},..} invMethod = do
Expand Down Expand Up @@ -809,13 +806,6 @@ validateAggFunctions aggFunctionsAllowed (Node rp@ReadPlan {select} forest)
| not aggFunctionsAllowed && any (isJust . csAggFunction) select = Left $ ApiRequestError AggregatesNotAllowed
| otherwise = Node rp <$> traverse (validateAggFunctions aggFunctionsAllowed) forest

-- | Lookup table in the schema cache before creating read plan
findTable :: QualifiedIdentifier -> TablesMap -> Either Error QualifiedIdentifier
findTable qi@QualifiedIdentifier{..} tableMap =
case HM.lookup qi tableMap of
Nothing -> Left $ SchemaCacheErr $ TableNotFound qiSchema qiName (HM.elems tableMap)
Just _ -> Right qi

addFilters :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree
addFilters ctx ApiRequest{..} rReq =
foldr addFilterToNode (Right rReq) flts
Expand Down Expand Up @@ -992,7 +982,21 @@ updateNode f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) =
findNode = find (\(Node ReadPlan{relName, relAlias} _) -> relName == targetNodeName || relAlias == Just targetNodeName) forest

mutatePlan :: Mutation -> QualifiedIdentifier -> ApiRequest -> SchemaCache -> ReadPlanTree -> Either Error MutatePlan
mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{dbTables, dbRepresentations} readReq =
mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{dbTables, dbRepresentations} readReq = do
tbl <- maybe (Left $ ApiRequestError InvalidResourcePath) Right $ HM.lookup qi dbTables
let ctx = ResolverContext dbTables dbRepresentations qi "json"
QueryParams.QueryParams{..} = iQueryParams
pkCols = tablePKCols tbl
confCols = fromMaybe pkCols qsOnConflict
returnings =
if preferRepresentation == Just None || isNothing preferRepresentation
then []
else S.toList $ inferColsEmbedNeeds readReq pkCols
logic = map (resolveLogicTree ctx . snd) qsLogic
combinedLogic = foldr (addFilterToLogicForest . resolveFilter ctx) logic qsFiltersRoot
body = payRaw <$> iPayload -- the body is assumed to be json at this stage(ApiRequest validates)
applyDefaults = preferMissing == Just ApplyDefaults
typedColumnsOrError = resolveOrError ctx tbl `traverse` S.toList iColumns
case mutation of
MutationCreate ->
mapRight (\typedColumns -> Insert qi typedColumns body ((,) <$> preferResolution <*> Just confCols) [] returnings pkCols applyDefaults) typedColumnsOrError
Expand All @@ -1009,23 +1013,6 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{
else
Left $ ApiRequestError InvalidFilters
MutationDelete -> Right $ Delete qi combinedLogic returnings
where
ctx = ResolverContext dbTables dbRepresentations qi "json"
confCols = fromMaybe pkCols qsOnConflict
QueryParams.QueryParams{..} = iQueryParams
returnings =
if preferRepresentation == Just None || isNothing preferRepresentation
then []
else S.toList $ inferColsEmbedNeeds readReq pkCols
-- TODO: remove fromJust by refactoring later
-- we can use fromJust, we have already looked up the table before building mutatePlan
tbl = fromJust $ HM.lookup qi dbTables
pkCols = maybe mempty tablePKCols (Just tbl)
logic = map (resolveLogicTree ctx . snd) qsLogic
combinedLogic = foldr (addFilterToLogicForest . resolveFilter ctx) logic qsFiltersRoot
body = payRaw <$> iPayload -- the body is assumed to be json at this stage(ApiRequest validates)
applyDefaults = preferMissing == Just ApplyDefaults
typedColumnsOrError = resolveOrError ctx tbl `traverse` S.toList iColumns

resolveOrError :: ResolverContext -> Table -> FieldName -> Either Error CoercibleField
resolveOrError ctx table field = case resolveTableFieldName table field Nothing of
Expand Down
4 changes: 2 additions & 2 deletions src/PostgREST/Response.hs
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,10 @@ actionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) _ versio
in
Right $ PgrstResponse HTTP.status200 (MediaType.toContentType MTOpenAPI : cLHeader ++ maybeToList (profileHeader schema negotiatedByProfile)) rsBody

actionResponse (NoDbResult (RelInfoPlan qi@QualifiedIdentifier{..})) _ _ _ SchemaCache{dbTables} _ _ =
actionResponse (NoDbResult (RelInfoPlan qi)) _ _ _ SchemaCache{dbTables} _ _ =
case HM.lookup qi dbTables of
Just tbl -> respondInfo $ allowH tbl
Nothing -> Left $ Error.SchemaCacheErr $ Error.TableNotFound qiSchema qiName (HM.elems dbTables)
Nothing -> Left $ Error.ApiRequestError Error.InvalidResourcePath
where
allowH table =
let hasPK = not . null $ tablePKCols table in
Expand Down
1 change: 0 additions & 1 deletion src/PostgREST/SchemaCache/Identifiers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ instance Hashable RelIdentifier

-- | Represents a pg identifier with a prepended schema name "schema.table".
-- When qiSchema is "", the schema is defined by the pg search_path.
-- TODO: Refactor this, we also use QI for procedure names
data QualifiedIdentifier = QualifiedIdentifier
{ qiSchema :: Schema
, qiName :: TableName
Expand Down
4 changes: 2 additions & 2 deletions test/io/test_big_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,6 @@ def test_should_not_fail_with_stack_overflow(defaultenv):

with run(env=env, wait_max_seconds=30) as postgrest:
response = postgrest.session.get("/unknown-table?select=unknown-rel(*)")
assert response.status_code == 404
assert response.status_code == 400
data = response.json()
assert data["code"] == "PGRST205"
assert data["code"] == "PGRST200"
78 changes: 43 additions & 35 deletions test/io/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,15 @@ def test_admin_works_with_host_special_values(specialhostvalue, defaultenv):
def test_log_level(level, defaultenv):
"log_level should filter request logging"

def drain_stdout(proc):
lines = []
while True:
chunk = proc.read_stdout(nlines=20)
if not chunk:
break
lines.extend(chunk)
return lines

env = {**defaultenv, "PGRST_LOG_LEVEL": level}

# any token to test 500 response for "Server lacks JWT secret"
Expand All @@ -953,57 +962,56 @@ def test_log_level(level, defaultenv):
response = postgrest.session.get("/")
assert response.status_code == 200

output = sorted(postgrest.read_stdout(nlines=7))
output = drain_stdout(postgrest)
http_lines = [line for line in output if line.startswith("- - ")]

def has_match(pattern):
return any(re.match(pattern, line) for line in http_lines)

if level == "crit":
assert len(output) == 0
elif level == "error":
assert re.match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
output[0],
assert has_match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+'
)
assert len(output) == 1
assert len(http_lines) == 1
elif level == "warn":
assert re.match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
output[0],
assert has_match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+'
)
assert re.match(
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
output[1],
assert has_match(
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+'
)
assert len(output) == 2
assert len(http_lines) == 2
elif level == "info":
assert re.match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
output[0],
assert has_match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+'
)
assert re.match(
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
output[1],
assert has_match(
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+'
)
assert re.match(
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
output[2],
assert has_match(
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+'
)
assert len(output) == 3
assert len(http_lines) == 3
elif level == "debug":
assert re.match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
output[0],
assert has_match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+'
)
assert re.match(
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
output[1],
assert has_match(
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+'
)
assert re.match(
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
output[2],
assert has_match(
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+'
)

assert len(output) == 7
assert any("Connection" and "is available" in line for line in output)
assert any("Connection" and "is used" in line for line in output)
assert len(http_lines) == 3
connection_lines = [line for line in output if "Connection" in line]
available_lines = [
line for line in connection_lines if "is available" in line
]
used_lines = [line for line in connection_lines if "is used" in line]
assert len(available_lines) == 2
assert len(used_lines) == 2


@pytest.mark.parametrize("level", ["crit", "error", "warn", "info", "debug"])
Expand Down
8 changes: 6 additions & 2 deletions test/spec/Feature/ConcurrentSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ spec =
it "should not raise 'transaction in progress' error" $
raceTest 10 $
get "/fakefake"
`shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.factories'","message":"Could not find the table 'test.fakefake' in the schema cache"} |]
`shouldRespondWith` [json|
{ "hint": null,
"details":null,
"code":"42P01",
"message":"relation \"test.fakefake\" does not exist"
} |]
{ matchStatus = 404
, matchHeaders = []
}
Expand Down
7 changes: 1 addition & 6 deletions test/spec/Feature/Query/DeleteSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,7 @@ spec =

context "totally unknown route" $
it "fails with 404" $
request methodDelete "/foozle?id=eq.101" [] ""
`shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.foo'","message":"Could not find the table 'test.foozle' in the schema cache"} |]
{ matchStatus = 404
, matchHeaders = []
}
request methodDelete "/foozle?id=eq.101" [] "" `shouldRespondWith` 404

context "table with limited privileges" $ do
it "fails deleting the row when return=representation and selecting all the columns" $
Expand Down
6 changes: 3 additions & 3 deletions test/spec/Feature/Query/ErrorSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ pgErrorCodeMapping = do
it "works with SchemaCache error" $
get "/non_existent_table"
`shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.collision_test_table'","message":"Could not find the table 'test.non_existent_table' in the schema cache"} |]
[json| {"code":"42P01","details":null,"hint":null,"message":"relation \"test.non_existent_table\" does not exist"} |]
{ matchStatus = 404
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PGRST205"
, "Content-Length" <:> "182" ]
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=42P01"
, "Content-Length" <:> "107" ]
}

it "works with Jwt error" $ do
Expand Down
2 changes: 1 addition & 1 deletion test/spec/Feature/Query/InsertSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ spec actualPgVersion = do
{"id": 204, "body": "yyy"},
{"id": 205, "body": "zzz"}]|]
`shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.articles'","message":"Could not find the table 'test.garlic' in the schema cache"} |]
[json| {"code":"PGRST125","details":null,"hint":null,"message":"Invalid path specified in request URL"} |]
{ matchStatus = 404
, matchHeaders = []
}
Expand Down
Loading
Loading