Compare commits

...
9 Commits
Author SHA1 Message Date
steve-chavez 5485b8ca9a bump version to 14.4 2026-01-29 14:13:49 -05:00
steve-chavez 895e9c536c chore: remove wrong entry in CHANGELOG 2026-01-29 13:24:20 -05:00
Michal Kleczekandsteve-chavez 16c767134c fix: listener running with exception masked after first failure 2026-01-29 13:22:46 -05:00
Laurence IslaandSteve Chavez 0a8b836435 fix: filtering the returned representation whenn using or/and filters on mutations
(cherry picked from commit 1682677297)
2026-01-29 09:16:08 -05:00
Michal KleczekandSteve Chavez 5796f86100 fix: ensure Listener connections are released
retryingListen function potentially leaks database connections. This patch ensures the connections are released in case of listen/notify errors.

(cherry picked from commit 00c7cb1a22)
2026-01-28 18:26:15 -05:00
Wolfgang Walther 101eac1cce docs: fix links
datrium.com doesn't exist anymore, while euronodes.com seems to only
fail SSL in CI.
2026-01-28 09:57:23 +01:00
renovate[bot]andWolfgang Walther 1ae14afdf2 chore(deps): update haskell-actions/setup action to v2.10.2 2026-01-11 17:36:28 +00:00
Wolfgang Walther 69090bd224 ci: pin backport action to version instead of default branch 2026-01-11 18:34:43 +01:00
renovate[bot]andWolfgang Walther 5d5160fbd7 chore(deps): update haskell-actions/setup action to v2.10.1 2026-01-05 19:01:32 +00:00
10 changed files with 81 additions and 35 deletions
+1 -1
View File
@@ -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 }}
+2 -2
View File
@@ -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
+6 -2
View File
@@ -4,9 +4,13 @@ All notable changes to this project will be documented in this file. From versio
## Unreleased ## Unreleased
### Added ## [14.4] - 2026-01-29
- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359 ### 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
+1
View File
@@ -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
-1
View File
@@ -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
View File
@@ -1,5 +1,5 @@
name: postgrest name: postgrest
version: 14.3 version: 14.4
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
+36 -21
View File
@@ -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
+6 -4
View File
@@ -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
@@ -118,6 +119,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 ->
@@ -164,9 +167,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
+8 -1
View File
@@ -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)
+20 -2
View File
@@ -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