Skip to content

Commit d5ab7a6

Browse files
steve-chavezlaurenceisla
authored andcommitted
add: config to emit warning for legacy target names
Adds the `url_use_legacy_target_names` config. Enabled (default): * It allows using the resource name in filters, orders or limits when it has an alias, e.g. `table?select=alias:target(*)&target.id=eq.1` * Logs a WARNING with a hint to use the alias * Returns a Warning header in the response Disabled: * It returns an error, only the alias is allowed * No warnings returned This feature is deprecated
1 parent 3522917 commit d5ab7a6

33 files changed

Lines changed: 385 additions & 69 deletions

CHANGELOG.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ All notable changes to this project will be documented in this file. From versio
1515
- Log schema cache queries timings on `log-level=debug` by @steve-chavez in #4805
1616
- Add GHC runtime metrics to the metrics endpoint by @mkleczek in #4862
1717
- Support running the admin server on a unix socket by @wolfgangwalther in #5003
18+
- Add config `url-use-legacy-target-names` to allow using the embedded table name in filters, orders or limits when it has an alias by @laurenceisla in #4075
1819

1920
### Fixed
2021

2122
- Shutdown should wait for in flight requests by @mkleczek in #4702
2223
- Remove automatic transaction retries on `40001 (serialization_failure)` errors to prevent replication lag by @laurenceisla in #3673
2324
- Fix unexpected results when embedding and filtering the same table more than once by @laurenceisla in #4075
25+
+ You need to set `url-use-legacy-target-names = false`.
2426
- If the schema cache fails to reload, PostgREST will no longer stop serving requests and will continue doing so in a "best effort" basis by @mkleczek in #4873 #4869
2527
- Stop reporting 503s errors unnecessarily while the schema cache is loading at startup by @mkleczek in #4880
2628
- Fix admin server dying silently by @Vlix, @mkleczek, @steve-chavez in #5012
@@ -33,8 +35,6 @@ All notable changes to this project will be documented in this file. From versio
3335
+ Now fails at startup. Prior to this, it failed with `PGRST205` on requests related to these schemas.
3436
- Build a static executable for aarch64-linux by @wolfgangwalther in #4193
3537
- Build the minimal docker image for aarch64-linux by @wolfgangwalther in #4193
36-
- The name of an embedded table can no longer be used in filters if it has an alias by @laurenceisla in #4075
37-
+ e.g. `?select=alias:table(*)&table.id=eq.1` is not possible anymore, use `?select=alias:table(*)&alias.id=eq.1` instead.
3838
- Config `jwt-role-claim-key` now uses RFC 9535 syntax for JSON Path by @taimoorzaeem in #4984
3939

4040
#### Changed Syntax for JWT Role Extraction
@@ -49,6 +49,12 @@ The `jwt-role-claim-key` config should be updated according to the following:
4949
+ Example: `.roles[?(@ ^== "postgrest_test_")]` -> `$.roles[?search(@, "^postgrest_test_")]`
5050
- Detailed reference for syntax: [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html#name-jsonpath-syntax-and-semanti).
5151

52+
### Deprecated
53+
54+
- Deprecate filters, orders and limits with the name of an embedded table when it has an alias by @steve-chavez, @laurenceisla in #4075
55+
+ e.g. `?select=alias:table(*)&table.id=eq.1` will not be possible anymore, use `?select=alias:table(*)&alias.id=eq.1` instead.
56+
+ You will see a warning in the logs when this happens.
57+
5258
## [14.14] - 2026-06-29
5359

5460
### Fixed

docs/references/configuration.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,3 +1013,37 @@ server-unix-socket-mode
10131013
.. code:: bash
10141014
10151015
server-unix-socket-mode = "660"
1016+
1017+
.. _url-use-legacy-target-names:
1018+
1019+
url-use-legacy-target-names
1020+
---------------------------
1021+
1022+
=============== =================================
1023+
**Type** Boolean
1024+
**Default** True
1025+
**Reloadable** Y
1026+
**Environment** PGRST_URL_USE_LEGACY_TARGET_NAMES
1027+
**In-Database** pgrst.url_use_legacy_target_names
1028+
=============== =================================
1029+
1030+
When active, it allows using the the name of an embedded table in filters, orders or limits even if it has an alias:
1031+
1032+
.. code:: bash
1033+
1034+
curl "http://localhost:3000/table?select=alias:target(*)&target.order=id" -i
1035+
1036+
.. code:: text
1037+
1038+
Warning: 299 PostgRESTv16 "Embedded resource was referenced by relation name even though it has an alias. This is deprecated and will stop working in a future release. Update `target` to `alias` in query string filters, orders or limits."
1039+
[...]
1040+
1041+
Note that the response includes a deprecation message in the ``Warning`` header.
1042+
This will also show in the PostgREST logs:
1043+
1044+
.. code::
1045+
1046+
28/May/2026:20:33:22 -0500: WARNING: Embedded resource was referenced by relation name even though it has an alias. This is deprecated and will stop working in a future release.
1047+
28/May/2026:20:33:22 -0500: Update filters, orders or limits that use `target` to `alias` in `GET /table?select=alias:target(*)&target.order=id`
1048+
1049+
This feature will be removed in a future release, so you should start using the ``alias`` in these cases.

src/PostgREST/App.hs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ import qualified Data.List as L
7171
import Data.Streaming.Network (bindPortTCP)
7272
import qualified Data.Text as T
7373
import qualified Network.HTTP.Types as HTTP
74-
import Network.HTTP.Types.Header (hVary)
74+
import Network.HTTP.Types.Header (hVary, hWarning)
7575
import qualified Network.Socket as NS
7676
import PostgREST.Unix (createAndBindDomainSocket)
7777
import System.Posix.Types (FileMode)
@@ -210,6 +210,14 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
210210
(parseTime, apiReq@ApiRequest{..}) <- withTiming conf $ liftEither . mapLeft Error.ApiRequestErr $ ApiRequest.userApiRequest conf prefs req body
211211
(planTime, plan) <- withTiming conf $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
212212

213+
let warnings = Plan.legacyWarnings plan
214+
legacyWarnMsg = "Embedded resource was referenced by relation name even though it has an alias. This is deprecated and will stop working in a future release."
215+
legacyWarnHint = let replacement (relName, alias) = "`" <> relName <> "` to `" <> alias <> "`" in T.intercalate ", " (replacement <$> warnings)
216+
shouldShowWarnings = configUrlUseLegacyTargetNames && not (null warnings)
217+
218+
liftIO $ when shouldShowWarnings $
219+
observer $ LegacyTargetNameWarningObs (legacyWarnMsg, legacyWarnHint) iMethod (iPath <> Wai.rawQueryString req) -- TODO maybe store rawQueryString in ApiRequest for consistency
220+
213221
let mainQ = Query.mainQuery plan conf apiReq authResult configDbPreRequest
214222
tx = MainTx.mainTx mainQ conf authResult apiReq plan sCache
215223
obsQuery s = when configLogQuery $ observer $ QueryObs mainQ s
@@ -235,12 +243,14 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
235243
liftIO $ obsQuery status'
236244
liftEither response
237245

238-
return $ toWaiResponse (ServerTiming jwtTime parseTime planTime txTime respTime) resp
246+
let warnHdrMsgs = if shouldShowWarnings then Just (legacyWarnMsg, legacyWarnHint) else Nothing
247+
248+
return $ toWaiResponse (ServerTiming jwtTime parseTime planTime txTime respTime) warnHdrMsgs resp
239249

240250
where
241-
toWaiResponse :: ServerTiming -> Response.PgrstResponse -> Wai.Response
242-
toWaiResponse timing (Response.PgrstResponse st hdrs bod) =
243-
Wai.responseLBS st (hdrs ++ serverTimingHeaders timing ++ [varyHeader | not $ varyHeaderPresent hdrs]) bod
251+
toWaiResponse :: ServerTiming -> Maybe (Text, Text) -> Response.PgrstResponse -> Wai.Response
252+
toWaiResponse timing warnMsgs (Response.PgrstResponse st hdrs bod) =
253+
Wai.responseLBS st (hdrs ++ serverTimingHeaders timing ++ warningHeaders warnMsgs ++ [varyHeader | not $ varyHeaderPresent hdrs]) bod
244254

245255
serverTimingHeaders :: ServerTiming -> [HTTP.Header]
246256
serverTimingHeaders timing = [serverTimingHeader timing | configServerTimingEnabled]
@@ -251,6 +261,14 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
251261
varyHeaderPresent :: [HTTP.Header] -> Bool
252262
varyHeaderPresent = any (\(h, _v) -> h == hVary)
253263

264+
warningHeaders :: Maybe (Text, Text) -> [HTTP.Header]
265+
warningHeaders Nothing = []
266+
warningHeaders (Just (msg, hint)) =
267+
let warnMsg = msg <> " Update " <> hint <> " in query string filters, orders or limits."
268+
pgrstVer = "PostgRESTv" <> BS.filter (/= ' ') prettyVersion
269+
in
270+
[(hWarning, "299 " <> pgrstVer <> " \"" <> encodeUtf8 warnMsg <> "\"")]
271+
254272
withTiming :: (MonadError e m, MonadIO m) => AppConfig -> m a -> m (Maybe Double, a)
255273
withTiming AppConfig{configServerTimingEnabled} f = if configServerTimingEnabled
256274
then do

src/PostgREST/Config.hs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ data AppConfig = AppConfig
121121
, configServerTimingEnabled :: Bool
122122
, configServerUnixSocket :: Maybe FilePath
123123
, configServerUnixSocketMode :: FileMode
124+
, configUrlUseLegacyTargetNames :: Bool
124125
, configAdminServerHost :: Text
125126
, configAdminServerPort :: Maybe Int
126127
, configAdminServerUnixSocket :: Maybe FilePath
@@ -207,6 +208,7 @@ toText conf =
207208
,("server-timing-enabled", T.toLower . show . configServerTimingEnabled)
208209
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
209210
,("server-unix-socket-mode", q . T.pack . showSocketMode)
211+
,("url-use-legacy-target-names", T.toLower . show . configUrlUseLegacyTargetNames)
210212
,("admin-server-host", q . configAdminServerHost)
211213
,("admin-server-port", maybe "\"\"" show . configAdminServerPort)
212214
,("admin-server-unix-socket", q . maybe mempty T.pack . configAdminServerUnixSocket)
@@ -325,6 +327,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
325327
<*> (fromMaybe False <$> optBool "server-timing-enabled")
326328
<*> (fmap T.unpack <$> optString "server-unix-socket")
327329
<*> parseSocketFileMode "server-unix-socket-mode"
330+
<*> (fromMaybe True <$> optBool "url-use-legacy-target-names")
328331
<*> (defaultServerHost <$> optWithAlias (optString "admin-server-host")
329332
(optString "server-host"))
330333
<*> parseAdminServerPort "admin-server-port"
@@ -794,4 +797,9 @@ exampleConfigFile = S.unlines
794797
, "## Unix socket file mode"
795798
, "## When none is provided, 660 is applied by default"
796799
, "# server-unix-socket-mode = \"660\""
800+
, ""
801+
, "## Use legacy target names in relationship filters"
802+
, "## If active, allows using the target name of the relationship in filters even if it has an alias."
803+
, "## Otherwise it only allows the alias in filters"
804+
, "url-use-legacy-target-names = true"
797805
]

src/PostgREST/Config/Database.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ dbSettingsNames =
7171
,"server_cors_allowed_origins"
7272
,"server_trace_header"
7373
,"server_timing_enabled"
74+
,"url_use_legacy_target_names"
7475
]
7576

7677
queryPgVersion :: Session PgVersion

src/PostgREST/Error.hs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ instance ErrorBody ApiRequestError where
175175
message InvalidFilters = "Filters must include all and only primary key columns with 'eq' operators"
176176
message (UnacceptableSchema sch _) = "Invalid schema: " <> sch
177177
message (MediaTypeError cts) = "None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)
178-
message (NotEmbedded resource) = "'" <> resource <> "' is not an embedded resource in this request"
178+
message (NotEmbedded resource _) = "'" <> resource <> "' is not an embedded resource in this request"
179179
message GucHeadersError = "response.headers guc must be a JSON array composed of objects with a single key and a string value"
180180
message GucStatusError = "response.status guc must be a valid status code"
181181
message PutLimitNotAllowedError = "limit/offset querystring parameters are not allowed for PUT"
@@ -207,11 +207,13 @@ instance ErrorBody ApiRequestError where
207207
details (InvalidPreferences prefs) = Just $ JSON.String $ T.decodeUtf8 ("Invalid preferences: " <> BS.intercalate ", " prefs)
208208
details (MaxAffectedViolationError n) = Just $ JSON.String $ T.unwords ["The query affects", show n, "rows"]
209209
details (NotImplemented details') = Just $ JSON.String details'
210+
details (NotEmbedded _ (Just _)) = Just $ JSON.String "Target names are not allowed in filters if they have an alias"
210211

211212
details _ = Nothing
212213

213214
-- HINT: Maybe JSON.Value
214-
hint (NotEmbedded resource) = Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter."
215+
hint (NotEmbedded resource Nothing) = Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter."
216+
hint (NotEmbedded _ (Just (name, alias))) = Just $ JSON.String $ "Change '" <> name <> "' to '" <> alias <> "' in filters, orders or limits."
215217
hint (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorHint raiseErr
216218
hint (UnacceptableSchema _ schemas) = Just $ JSON.String $ "Only the following schemas are exposed: " <> T.intercalate ", " schemas
217219

src/PostgREST/Error/Types.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ data ApiRequestError
4444
| InvalidPreferences [ByteString]
4545
| InvalidRange RangeError
4646
| InvalidRpcMethod ByteString
47-
| NotEmbedded Text
47+
| NotEmbedded Text (Maybe (Text, Text))
4848
| NotImplemented Text
4949
| PutLimitNotAllowedError
5050
| QueryParamError QPError

src/PostgREST/Logger.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ observationMessages = \case
192192
let snipts = renderSnippet <$> [mqTxVars, fromMaybe mempty mqPreReq, mqMain, x, y, z, fromMaybe mempty mqExplain]
193193
in
194194
showOnSingleLine '\n' . T.decodeUtf8 <$> filter (/= mempty) snipts
195+
LegacyTargetNameWarningObs (warningMsg, warningHints) requestMethod requestTarget ->
196+
[ "WARNING: " <> warningMsg
197+
, "Update filters, orders or limits that use " <> warningHints <> " in " <> "`" <> T.decodeUtf8 (requestMethod <> " " <> requestTarget) <> "`"
198+
]
195199
ConfigReadErrorObs usageErr ->
196200
pure $ "Failed to query database settings for the config parameters." <> jsonMessage usageErr
197201
QueryRoleSettingsErrorObs usageErr ->

src/PostgREST/Observation.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ data Observation
4545
| DBListenerGotConfigMsg ByteString
4646
| DBListenerConnectionCleanupFail SomeException
4747
| QueryObs MainQuery Status
48+
| LegacyTargetNameWarningObs (Text, Text) ByteString ByteString
4849
| ConfigReadErrorObs SQL.UsageError
4950
| ConfigInvalidObs Text
5051
| ConfigSucceededObs

0 commit comments

Comments
 (0)