Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d89826b982 | ||
|
|
94350fdde6 | ||
|
|
230eb3630c | ||
|
|
5485b8ca9a | ||
|
|
895e9c536c | ||
|
|
16c767134c | ||
|
|
0a8b836435 | ||
|
|
5796f86100 | ||
|
|
101eac1cce | ||
|
|
1ae14afdf2 | ||
|
|
69090bd224 | ||
|
|
5d5160fbd7 |
@@ -38,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
# This is required for backport action to cherry-pick the PR
|
# This is required for backport action to cherry-pick the PR
|
||||||
- name: Fetch PR ref
|
- name: Fetch PR ref
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
token: ${{ steps.app-token.outputs.token }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- uses: haskell-actions/setup@0512451d82f3ca8c147db62e30464e7c4ca63d30 # v2.9.1
|
- uses: haskell-actions/setup@dc63c94789664bb2910876ec3dfeeaa24d23b96b # v2.10.2
|
||||||
with:
|
with:
|
||||||
# This must match the version in stack.yaml's resolver
|
# This must match the version in stack.yaml's resolver
|
||||||
ghc-version: 9.6.7
|
ghc-version: 9.6.7
|
||||||
@@ -177,7 +177,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- uses: haskell-actions/setup@0512451d82f3ca8c147db62e30464e7c4ca63d30 # v2.9.1
|
- uses: haskell-actions/setup@dc63c94789664bb2910876ec3dfeeaa24d23b96b # v2.10.2
|
||||||
with:
|
with:
|
||||||
ghc-version: ${{ matrix.ghc }}
|
ghc-version: ${{ matrix.ghc }}
|
||||||
- name: Cache .cabal
|
- name: Cache .cabal
|
||||||
|
|||||||
@@ -4,9 +4,33 @@ All notable changes to this project will be documented in this file. From versio
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## [14.5] - 2026-02-12
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359
|
- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359
|
||||||
|
- Add a `HINT` when the LISTEN channel stops working due to a PostgreSQL bug by @laurenceisla in #4581
|
||||||
|
- Add string slicing operator for `jwt-role-claim-key` by @taimoorzaeem in #4599
|
||||||
|
- Log host, port and pg version of listener database connection by @mkleczek in #4617 #4618
|
||||||
|
- Optimize requests with `Prefer: count=exact` that do not use ranges or `db-max-rows` by @laurenceisla in #3957
|
||||||
|
+ Removed unnecessary double count when building the `Content-Range`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Don't hide async exceptions in logs by @stevechavez in #4646
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359
|
||||||
|
+ Now fails at startup. Prior to this, it failed with `PGRST205` on requests related to these schemas.
|
||||||
|
|
||||||
|
## [14.4] - 2026-01-29
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Ensure Listener connections are released by @mkleczek in #4614
|
||||||
|
- Fix incorrectly filtering the returned representation for PATCH requests when using `or/and` filters by @laurenceisla in #3707
|
||||||
|
- Fix listener running with exception masked after first failure by @mkleczek #4615
|
||||||
|
|
||||||
## [14.3] - 2026-01-03
|
## [14.3] - 2026-01-03
|
||||||
|
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ linkcheck_ignore = [
|
|||||||
r"https://www.cybertec-postgresql.com/.*",
|
r"https://www.cybertec-postgresql.com/.*",
|
||||||
# Odd SSL error
|
# Odd SSL error
|
||||||
r"https://www.dripdepot.com",
|
r"https://www.dripdepot.com",
|
||||||
|
r"https://www.euronodes.com",
|
||||||
# New GitHub UI delays comment load, so anchor fails
|
# New GitHub UI delays comment load, so anchor fails
|
||||||
r"https://github.com/.*#issuecomment",
|
r"https://github.com/.*#issuecomment",
|
||||||
# Random 500 Internal Server Error
|
# Random 500 Internal Server Error
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Supported PostgreSQL versions
|
|||||||
=============================
|
=============================
|
||||||
|
|
||||||
=============== =================================
|
=============== =================================
|
||||||
**Supported** PostgreSQL >= 12
|
**Supported** PostgreSQL >= 13
|
||||||
=============== =================================
|
=============== =================================
|
||||||
|
|
||||||
PostgREST works with all PostgreSQL versions still `officially supported <https://www.postgresql.org/support/versioning/>`_.
|
PostgREST works with all PostgreSQL versions still `officially supported <https://www.postgresql.org/support/versioning/>`_.
|
||||||
|
|||||||
@@ -213,7 +213,6 @@ In Production
|
|||||||
Here are some companies that use PostgREST in production.
|
Here are some companies that use PostgREST in production.
|
||||||
|
|
||||||
* `Catarse <https://www.catarse.me>`_
|
* `Catarse <https://www.catarse.me>`_
|
||||||
* `Datrium <https://www.datrium.com>`_
|
|
||||||
* `Drip Depot <https://www.dripdepot.com>`_
|
* `Drip Depot <https://www.dripdepot.com>`_
|
||||||
* `Image-charts <https://www.image-charts.com>`_
|
* `Image-charts <https://www.image-charts.com>`_
|
||||||
* `Netwo <https://www.netwo.io>`_
|
* `Netwo <https://www.netwo.io>`_
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
name: postgrest
|
name: postgrest
|
||||||
version: 14.3
|
version: 14.5
|
||||||
synopsis: REST API for any Postgres database
|
synopsis: REST API for any Postgres database
|
||||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||||
for tables, views, and functions, supporting all HTTP methods that security
|
for tables, views, and functions, supporting all HTTP methods that security
|
||||||
|
|||||||
+26
-4
@@ -9,18 +9,24 @@ Some of its functionality includes:
|
|||||||
- Producing HTTP Headers according to RFCs.
|
- Producing HTTP Headers according to RFCs.
|
||||||
- Content Negotiation
|
- Content Negotiation
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
|
{-# LANGUAGE ViewPatterns #-}
|
||||||
module PostgREST.App
|
module PostgREST.App
|
||||||
( postgrest
|
( postgrest
|
||||||
, run
|
, run
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
|
||||||
|
import GHC.IO.Exception (IOErrorType (..))
|
||||||
|
import System.IO.Error (ioeGetErrorType)
|
||||||
|
|
||||||
import Control.Monad.Except (liftEither)
|
import Control.Monad.Except (liftEither)
|
||||||
import Data.Either.Combinators (mapLeft, whenLeft)
|
import Data.Either.Combinators (mapLeft, whenLeft)
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.String (IsString (..))
|
import Data.String (IsString (..))
|
||||||
import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
|
import Network.Wai.Handler.Warp (defaultSettings, setHost,
|
||||||
|
setOnException, setPort,
|
||||||
setServerName)
|
setServerName)
|
||||||
|
|
||||||
import qualified Data.Text.Encoding as T
|
import qualified Data.Text.Encoding as T
|
||||||
@@ -63,7 +69,6 @@ type Handler = ExceptT Error
|
|||||||
|
|
||||||
run :: AppState -> IO ()
|
run :: AppState -> IO ()
|
||||||
run appState = do
|
run appState = do
|
||||||
let observer = AppState.getObserver appState
|
|
||||||
conf@AppConfig{..} <- AppState.getConfig appState
|
conf@AppConfig{..} <- AppState.getConfig appState
|
||||||
|
|
||||||
AppState.schemaCacheLoader appState -- Loads the initial SchemaCache
|
AppState.schemaCacheLoader appState -- Loads the initial SchemaCache
|
||||||
@@ -79,7 +84,24 @@ run appState = do
|
|||||||
address <- resolveSocketToAddress (AppState.getSocketREST appState)
|
address <- resolveSocketToAddress (AppState.getSocketREST appState)
|
||||||
observer $ AppServerAddressObs address
|
observer $ AppServerAddressObs address
|
||||||
|
|
||||||
Warp.runSettingsSocket (serverSettings conf) (AppState.getSocketREST appState) app
|
Warp.runSettingsSocket (serverSettings conf & setOnException onWarpException) (AppState.getSocketREST appState) app
|
||||||
|
where
|
||||||
|
observer = AppState.getObserver appState
|
||||||
|
|
||||||
|
onWarpException :: Maybe Wai.Request -> SomeException -> IO ()
|
||||||
|
onWarpException _ ex =
|
||||||
|
when (shouldDisplayException ex) $
|
||||||
|
observer $ WarpErrorObs $ show ex
|
||||||
|
|
||||||
|
-- Similar to wai defaultShouldDisplayException in
|
||||||
|
-- https://github.com/yesodweb/wai//blob/8c3882c60f6abe043889fc20c7efd3fa9747fa4a/warp/Network/Wai/Handler/Warp/Settings.hs#L251-L258
|
||||||
|
-- but without omitting AsyncException since it's important to log for ThreadKilled, StackOverflow and other cases.
|
||||||
|
-- We want to reuse this to avoid flooding the logs for some transient failure cases.
|
||||||
|
shouldDisplayException :: SomeException -> Bool
|
||||||
|
shouldDisplayException se
|
||||||
|
| Just (_ :: Warp.InvalidRequest) <- fromException se = False
|
||||||
|
| Just (ioeGetErrorType -> et) <- fromException se, et == ResourceVanished || et == InvalidArgument = False
|
||||||
|
| otherwise = True
|
||||||
|
|
||||||
serverSettings :: AppConfig -> Warp.Settings
|
serverSettings :: AppConfig -> Warp.Settings
|
||||||
serverSettings AppConfig{..} =
|
serverSettings AppConfig{..} =
|
||||||
|
|||||||
+36
-21
@@ -1,3 +1,4 @@
|
|||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE MultiWayIf #-}
|
{-# LANGUAGE MultiWayIf #-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ import PostgREST.Version (prettyVersion)
|
|||||||
import qualified PostgREST.AppState as AppState
|
import qualified PostgREST.AppState as AppState
|
||||||
import qualified PostgREST.Config as Config
|
import qualified PostgREST.Config as Config
|
||||||
|
|
||||||
|
import Data.Either.Combinators (whenRight)
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
-- | Starts the Listener in a thread
|
-- | Starts the Listener in a thread
|
||||||
@@ -22,15 +24,16 @@ runListener :: AppState -> IO ()
|
|||||||
runListener appState = do
|
runListener appState = do
|
||||||
AppConfig{..} <- getConfig appState
|
AppConfig{..} <- getConfig appState
|
||||||
when configDbChannelEnabled $
|
when configDbChannelEnabled $
|
||||||
void . forkIO $ retryingListen appState
|
void . forkIO . void $ retryingListen appState
|
||||||
|
|
||||||
-- | Starts a LISTEN connection and handles notifications. It recovers with exponential backoff with a cap of 32 seconds, if the LISTEN connection is lost.
|
-- | Starts a LISTEN connection and handles notifications. It recovers with exponential backoff with a cap of 32 seconds, if the LISTEN connection is lost.
|
||||||
retryingListen :: AppState -> IO ()
|
-- | This function never returns (but can throw) and return type enforces that.
|
||||||
|
retryingListen :: AppState -> IO Void
|
||||||
retryingListen appState = do
|
retryingListen appState = do
|
||||||
AppConfig{..} <- AppState.getConfig appState
|
AppConfig{..} <- AppState.getConfig appState
|
||||||
let
|
let
|
||||||
dbChannel = toS configDbChannel
|
dbChannel = toS configDbChannel
|
||||||
handleFinally err = do
|
onError err = do
|
||||||
AppState.putIsListenerOn appState False
|
AppState.putIsListenerOn appState False
|
||||||
observer $ DBListenFail dbChannel (Right err)
|
observer $ DBListenFail dbChannel (Right err)
|
||||||
unless configDbPoolAutomaticRecovery $
|
unless configDbPoolAutomaticRecovery $
|
||||||
@@ -42,29 +45,39 @@ retryingListen appState = do
|
|||||||
threadDelay (delay * oneSecondInMicro)
|
threadDelay (delay * oneSecondInMicro)
|
||||||
unless (delay == maxDelay) $
|
unless (delay == maxDelay) $
|
||||||
AppState.putNextListenerDelay appState (delay * 2)
|
AppState.putNextListenerDelay appState (delay * 2)
|
||||||
|
-- loop running the listener
|
||||||
retryingListen appState
|
retryingListen appState
|
||||||
|
|
||||||
-- forkFinally allows to detect if the thread dies
|
-- Execute the listener with with error handling
|
||||||
void . flip forkFinally handleFinally $ do
|
handle onError $ do
|
||||||
dbOrError <- SQL.acquire $ toUtf8 (Config.addTargetSessionAttrs $ Config.addFallbackAppName prettyVersion configDbUri)
|
-- Make sure we don't leak connections on errors
|
||||||
case dbOrError of
|
bracket
|
||||||
Right db -> do
|
-- acquire connection
|
||||||
SQL.listen db $ SQL.toPgIdentifier dbChannel
|
(SQL.acquire $ toUtf8 (Config.addTargetSessionAttrs $ Config.addFallbackAppName prettyVersion configDbUri))
|
||||||
AppState.putIsListenerOn appState True
|
-- release connection
|
||||||
|
(`whenRight` releaseConnection) $
|
||||||
|
-- use connection
|
||||||
|
\case
|
||||||
|
Right db -> do
|
||||||
|
SQL.listen db $ SQL.toPgIdentifier dbChannel
|
||||||
|
AppState.putIsListenerOn appState True
|
||||||
|
|
||||||
delay <- AppState.getNextListenerDelay appState
|
delay <- AppState.getNextListenerDelay appState
|
||||||
when (delay > 1) $ do -- if we did a retry
|
when (delay > 1) $ do -- if we did a retry
|
||||||
-- assume we lost notifications, refresh the schema cache
|
-- assume we lost notifications, refresh the schema cache
|
||||||
AppState.schemaCacheLoader appState
|
AppState.schemaCacheLoader appState
|
||||||
-- reset the delay
|
-- reset the delay
|
||||||
AppState.putNextListenerDelay appState 1
|
AppState.putNextListenerDelay appState 1
|
||||||
|
|
||||||
observer $ DBListenStart dbChannel
|
observer $ DBListenStart dbChannel
|
||||||
SQL.waitForNotifications handleNotification db
|
|
||||||
|
|
||||||
Left err -> do
|
-- wait for notifications
|
||||||
observer $ DBListenFail dbChannel (Left err)
|
-- this will never return, in case of an error it will throw and be caught by onError
|
||||||
exitFailure
|
forever $ SQL.waitForNotifications handleNotification db
|
||||||
|
|
||||||
|
Left err -> do
|
||||||
|
observer $ DBListenFail dbChannel (Left err)
|
||||||
|
exitFailure
|
||||||
where
|
where
|
||||||
observer = AppState.getObserver appState
|
observer = AppState.getObserver appState
|
||||||
mainThreadId = AppState.getMainThreadId appState
|
mainThreadId = AppState.getMainThreadId appState
|
||||||
@@ -79,3 +92,5 @@ retryingListen appState = do
|
|||||||
|
|
||||||
cacheReloader =
|
cacheReloader =
|
||||||
AppState.schemaCacheLoader appState
|
AppState.schemaCacheLoader appState
|
||||||
|
|
||||||
|
releaseConnection = void . forkIO . handle (observer . DBListenerConnectionCleanupFail) . SQL.release
|
||||||
|
|||||||
@@ -44,10 +44,11 @@ data Observation
|
|||||||
| SchemaCacheLoadedObs Double
|
| SchemaCacheLoadedObs Double
|
||||||
| ConnectionRetryObs Int
|
| ConnectionRetryObs Int
|
||||||
| DBListenStart Text
|
| DBListenStart Text
|
||||||
| DBListenFail Text (Either SQL.ConnectionError (Either SomeException ()))
|
| DBListenFail Text (Either SQL.ConnectionError SomeException)
|
||||||
| DBListenRetry Int
|
| DBListenRetry Int
|
||||||
| DBListenerGotSCacheMsg ByteString
|
| DBListenerGotSCacheMsg ByteString
|
||||||
| DBListenerGotConfigMsg ByteString
|
| DBListenerGotConfigMsg ByteString
|
||||||
|
| DBListenerConnectionCleanupFail SomeException
|
||||||
| QueryObs MainQuery Status
|
| QueryObs MainQuery Status
|
||||||
| ConfigReadErrorObs SQL.UsageError
|
| ConfigReadErrorObs SQL.UsageError
|
||||||
| ConfigInvalidObs Text
|
| ConfigInvalidObs Text
|
||||||
@@ -62,6 +63,7 @@ data Observation
|
|||||||
| PoolRequestFullfilled
|
| PoolRequestFullfilled
|
||||||
| JwtCacheLookup Bool
|
| JwtCacheLookup Bool
|
||||||
| JwtCacheEviction
|
| JwtCacheEviction
|
||||||
|
| WarpErrorObs Text
|
||||||
|
|
||||||
data ObsFatalError = ServerAuthError | ServerPgrstBug | ServerError42P05 | ServerError08P01
|
data ObsFatalError = ServerAuthError | ServerPgrstBug | ServerError42P05 | ServerError08P01
|
||||||
|
|
||||||
@@ -118,6 +120,8 @@ observationMessage = \case
|
|||||||
"Received a schema cache reload message on the " <> show channel <> " channel"
|
"Received a schema cache reload message on the " <> show channel <> " channel"
|
||||||
DBListenerGotConfigMsg channel ->
|
DBListenerGotConfigMsg channel ->
|
||||||
"Received a config reload message on the " <> show channel <> " channel"
|
"Received a config reload message on the " <> show channel <> " channel"
|
||||||
|
DBListenerConnectionCleanupFail ex ->
|
||||||
|
"Failed during listener connection cleanup: " <> showOnSingleLine '\t' (show ex)
|
||||||
QueryObs{} ->
|
QueryObs{} ->
|
||||||
mempty -- TODO pending refactor: The logic for printing the query cannot be done here. Join the observationMessage function into observationLogger to avoid this mempty.
|
mempty -- TODO pending refactor: The logic for printing the query cannot be done here. Join the observationMessage function into observationLogger to avoid this mempty.
|
||||||
ConfigReadErrorObs usageErr ->
|
ConfigReadErrorObs usageErr ->
|
||||||
@@ -154,6 +158,8 @@ observationMessage = \case
|
|||||||
"Looked up a JWT in JWT cache"
|
"Looked up a JWT in JWT cache"
|
||||||
JwtCacheEviction ->
|
JwtCacheEviction ->
|
||||||
"Evicted entry from JWT cache"
|
"Evicted entry from JWT cache"
|
||||||
|
WarpErrorObs txt ->
|
||||||
|
"Warp server error: " <> txt
|
||||||
where
|
where
|
||||||
showMillis :: Double -> Text
|
showMillis :: Double -> Text
|
||||||
showMillis x = toS $ showFFloat (Just 1) x ""
|
showMillis x = toS $ showFFloat (Just 1) x ""
|
||||||
@@ -164,9 +170,8 @@ observationMessage = \case
|
|||||||
showListenerConnError :: SQL.ConnectionError -> Text
|
showListenerConnError :: SQL.ConnectionError -> Text
|
||||||
showListenerConnError = maybe "Connection error" (showOnSingleLine '\t' . T.decodeUtf8)
|
showListenerConnError = maybe "Connection error" (showOnSingleLine '\t' . T.decodeUtf8)
|
||||||
|
|
||||||
showListenerException :: Either SomeException () -> Text
|
showListenerException :: SomeException -> Text
|
||||||
showListenerException (Right _) = "Failed getting notifications" -- should not happen as the listener will never finish (hasql-notifications uses `forever` internally) with a Right result
|
showListenerException = showOnSingleLine '\t' . show
|
||||||
showListenerException (Left e) = showOnSingleLine '\t' $ show e
|
|
||||||
|
|
||||||
|
|
||||||
showOnSingleLine :: Char -> Text -> Text
|
showOnSingleLine :: Char -> Text -> Text
|
||||||
|
|||||||
@@ -965,10 +965,17 @@ addRanges ApiRequest{..} rReq =
|
|||||||
|
|
||||||
addLogicTrees :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree
|
addLogicTrees :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree
|
||||||
addLogicTrees ctx ApiRequest{..} rReq =
|
addLogicTrees ctx ApiRequest{..} rReq =
|
||||||
foldr addLogicTreeToNode (Right rReq) qsLogic
|
foldr addLogicTreeToNode (Right rReq) logic
|
||||||
where
|
where
|
||||||
QueryParams.QueryParams{..} = iQueryParams
|
QueryParams.QueryParams{..} = iQueryParams
|
||||||
|
|
||||||
|
logic =
|
||||||
|
case iAction of
|
||||||
|
ActDb (ActRelationRead _ _) -> qsLogic
|
||||||
|
ActDb (ActRoutine _ _) -> qsLogic
|
||||||
|
-- For mutations, take the non-root logic filters. These will only affect the embeddings and not the top level of the returned representation.
|
||||||
|
_ -> filter (not . null . fst) qsLogic
|
||||||
|
|
||||||
addLogicTreeToNode :: (EmbedPath, LogicTree) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
addLogicTreeToNode :: (EmbedPath, LogicTree) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
||||||
addLogicTreeToNode = updateNode (\t (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=resolveLogicTree ctx{qi=fromTable} t:lf} f)
|
addLogicTreeToNode = updateNode (\t (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=resolveLogicTree ctx{qi=fromTable} t:lf} f)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import requests
|
||||||
|
|
||||||
from postgrest import run
|
from postgrest import run
|
||||||
|
|
||||||
@@ -54,6 +55,25 @@ def test_openapi_in_big_schema(defaultenv):
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_stackoverflow_is_logged(defaultenv):
|
||||||
|
"Stack overflow errors should be logged with the Warp error message"
|
||||||
|
|
||||||
|
env = {
|
||||||
|
**defaultenv,
|
||||||
|
"PGRST_DB_SCHEMAS": "apflora",
|
||||||
|
"PGRST_DB_ANON_ROLE": "postgrest_test_anonymous",
|
||||||
|
}
|
||||||
|
|
||||||
|
with run(env=env, wait_max_seconds=30, no_startup_stdout=False) as postgrest:
|
||||||
|
with pytest.raises(requests.exceptions.ConnectionError):
|
||||||
|
postgrest.session.get("/")
|
||||||
|
|
||||||
|
output = postgrest.read_stdout(nlines=10)
|
||||||
|
output.extend(postgrest.read_stdout(nlines=10))
|
||||||
|
|
||||||
|
assert any("Warp server error: stack overflow" in line for line in output)
|
||||||
|
|
||||||
|
|
||||||
# See: https://github.com/PostgREST/postgrest/issues/3329
|
# See: https://github.com/PostgREST/postgrest/issues/3329
|
||||||
def test_should_not_fail_with_stack_overflow(defaultenv):
|
def test_should_not_fail_with_stack_overflow(defaultenv):
|
||||||
"requesting a non-existent relationship should not fail with stack overflow due to fuzzy search of candidates"
|
"requesting a non-existent relationship should not fail with stack overflow due to fuzzy search of candidates"
|
||||||
|
|||||||
@@ -252,21 +252,39 @@ spec =
|
|||||||
[json|[{"id": 7, "entities":null}, {"id": 8, "entities": {"id": 2}}, {"id": 9, "entities": {"id": 3}}]|]
|
[json|[{"id": 7, "entities":null}, {"id": 8, "entities": {"id": 2}}, {"id": 9, "entities": {"id": 3}}]|]
|
||||||
{ matchStatus = 201 }
|
{ matchStatus = 201 }
|
||||||
|
|
||||||
context "used with PATCH" $
|
context "used with PATCH" $ do
|
||||||
it "succeeds when using and/or params" $
|
it "succeeds when using and/or params" $
|
||||||
request methodPatch "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"
|
request methodPatch "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"
|
||||||
[("Prefer", "return=representation")]
|
[("Prefer", "return=representation")]
|
||||||
[json|{ name : "updated grandchild entity"}|] `shouldRespondWith`
|
[json|{ name : "updated grandchild entity"}|] `shouldRespondWith`
|
||||||
[json|[{ "id": 1, "name" : "updated grandchild entity"},{ "id": 2, "name" : "updated grandchild entity"}]|]
|
[json|[{ "id": 1, "name" : "updated grandchild entity"},{ "id": 2, "name" : "updated grandchild entity"}]|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "succeeds when the filtered column is modified" $
|
||||||
|
request methodPatch "/entities?select=id,name&or=(name.is.null,name.like.*test*)"
|
||||||
|
[("Prefer", "return=representation")]
|
||||||
|
[json|{ "name" : "updated entity" }|] `shouldRespondWith`
|
||||||
|
[json|[{ "id": 4, "name": "updated entity" }]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
it "succeeds when the filtered column is not selected in the returned representation" $
|
||||||
|
request methodPatch "/entities?select=id&or=(name.is.null,name.like.*test*)"
|
||||||
|
[("Prefer", "return=representation")]
|
||||||
|
[json|{ "name" : "updated entity" }|] `shouldRespondWith`
|
||||||
|
[json|[{ "id": 4 }]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
context "used with DELETE" $
|
context "used with DELETE" $ do
|
||||||
it "succeeds when using and/or params" $
|
it "succeeds when using and/or params" $
|
||||||
request methodDelete "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"
|
request methodDelete "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"
|
||||||
[("Prefer", "return=representation")]
|
[("Prefer", "return=representation")]
|
||||||
""
|
""
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|[{ "id": 1, "name" : "grandchild entity 1" },{ "id": 2, "name" : "grandchild entity 2" }]|]
|
[json|[{ "id": 1, "name" : "grandchild entity 1" },{ "id": 2, "name" : "grandchild entity 2" }]|]
|
||||||
|
it "succeeds when the filtered column is not selected in the returned representation" $
|
||||||
|
request methodDelete "/entities?select=id&or=(name.is.null,name.like.*test*)"
|
||||||
|
[("Prefer", "return=representation")]
|
||||||
|
""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[{ "id": 4 }]|]
|
||||||
|
|
||||||
it "can query columns that begin with and/or reserved words" $
|
it "can query columns that begin with and/or reserved words" $
|
||||||
get "/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)" `shouldRespondWith` 200
|
get "/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)" `shouldRespondWith` 200
|
||||||
|
|||||||
Reference in New Issue
Block a user