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
This commit is contained in:
steve-chavez
2026-07-11 02:15:07 +00:00
committed by Laurence Isla
parent 2fa8de4e52
commit 490d1dc5d3
33 changed files with 385 additions and 69 deletions
+8 -2
View File
@@ -21,6 +21,7 @@ All notable changes to this project will be documented in this file. From versio
- Shutdown should wait for in flight requests by @mkleczek in #4702 - Shutdown should wait for in flight requests by @mkleczek in #4702
- Remove automatic transaction retries on `40001 (serialization_failure)` errors to prevent replication lag by @laurenceisla in #3673 - Remove automatic transaction retries on `40001 (serialization_failure)` errors to prevent replication lag by @laurenceisla in #3673
- Fix unexpected results when embedding and filtering the same table more than once by @laurenceisla in #4075 - Fix unexpected results when embedding and filtering the same table more than once by @laurenceisla in #4075
+ You need to set `url-use-legacy-target-names = false`.
- 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 - 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
- Stop reporting 503s errors unnecessarily while the schema cache is loading at startup by @mkleczek in #4880 - Stop reporting 503s errors unnecessarily while the schema cache is loading at startup by @mkleczek in #4880
- Fix responding with `Something went wrong` on Admin server when under EMFILE by @mkleczek in #5077 - Fix responding with `Something went wrong` on Admin server when under EMFILE by @mkleczek in #5077
@@ -34,8 +35,6 @@ All notable changes to this project will be documented in this file. From versio
+ Now fails at startup. Prior to this, it failed with `PGRST205` on requests related to these schemas. + Now fails at startup. Prior to this, it failed with `PGRST205` on requests related to these schemas.
- Build a static executable for aarch64-linux by @wolfgangwalther in #4193 - Build a static executable for aarch64-linux by @wolfgangwalther in #4193
- Build the minimal docker image for aarch64-linux by @wolfgangwalther in #4193 - Build the minimal docker image for aarch64-linux by @wolfgangwalther in #4193
- The name of an embedded table can no longer be used in filters if it has an alias by @laurenceisla in #4075
+ e.g. `?select=alias:table(*)&table.id=eq.1` is not possible anymore, use `?select=alias:table(*)&alias.id=eq.1` instead.
- Config `jwt-role-claim-key` now uses RFC 9535 syntax for JSON Path by @taimoorzaeem in #4984 - Config `jwt-role-claim-key` now uses RFC 9535 syntax for JSON Path by @taimoorzaeem in #4984
#### Changed Syntax for JWT Role Extraction #### Changed Syntax for JWT Role Extraction
@@ -50,6 +49,13 @@ The `jwt-role-claim-key` config should be updated according to the following:
+ Example: `.roles[?(@ ^== "postgrest_test_")]` -> `$.roles[?search(@, "^postgrest_test_")]` + Example: `.roles[?(@ ^== "postgrest_test_")]` -> `$.roles[?search(@, "^postgrest_test_")]`
- Detailed reference for syntax: [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html#name-jsonpath-syntax-and-semanti). - Detailed reference for syntax: [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html#name-jsonpath-syntax-and-semanti).
### Deprecated
- Deprecate filters, orders and limits with the name of an embedded table when it has an alias by @steve-chavez, @laurenceisla in #4075
+ e.g. `?select=alias:table(*)&table.id=eq.1` will not be possible anymore, use `?select=alias:table(*)&alias.id=eq.1` instead.
+ You will see a warning in the logs when this happens.
+ You can disable this behavior now by setting `url-use-legacy-target-names = false`.
## [14.14] - 2026-06-29 ## [14.14] - 2026-06-29
### Fixed ### Fixed
+34
View File
@@ -1013,3 +1013,37 @@ server-unix-socket-mode
.. code:: bash .. code:: bash
server-unix-socket-mode = "660" server-unix-socket-mode = "660"
.. _url-use-legacy-target-names:
url-use-legacy-target-names
---------------------------
=============== =================================
**Type** Boolean
**Default** True
**Reloadable** Y
**Environment** PGRST_URL_USE_LEGACY_TARGET_NAMES
**In-Database** pgrst.url_use_legacy_target_names
=============== =================================
When active, it allows using the the name of an embedded table in filters, orders or limits even if it has an alias:
.. code:: bash
curl "http://localhost:3000/table?select=alias:target(*)&target.order=id" -i
.. code:: text
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."
[...]
Note that the response includes a deprecation message in the ``Warning`` header.
This will also show in the PostgREST logs:
.. code::
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.
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`
This feature will be removed in a future release, so you should start using the ``alias`` in these cases.
+23 -5
View File
@@ -71,7 +71,7 @@ import qualified Data.List as L
import Data.Streaming.Network (bindPortTCP) import Data.Streaming.Network (bindPortTCP)
import qualified Data.Text as T import qualified Data.Text as T
import qualified Network.HTTP.Types as HTTP import qualified Network.HTTP.Types as HTTP
import Network.HTTP.Types.Header (hVary) import Network.HTTP.Types.Header (hVary, hWarning)
import qualified Network.Socket as NS import qualified Network.Socket as NS
import PostgREST.Unix (createAndBindDomainSocket) import PostgREST.Unix (createAndBindDomainSocket)
import System.Posix.Types (FileMode) import System.Posix.Types (FileMode)
@@ -210,6 +210,14 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
(parseTime, apiReq@ApiRequest{..}) <- withTiming conf $ liftEither . mapLeft Error.ApiRequestErr $ ApiRequest.userApiRequest conf prefs req body (parseTime, apiReq@ApiRequest{..}) <- withTiming conf $ liftEither . mapLeft Error.ApiRequestErr $ ApiRequest.userApiRequest conf prefs req body
(planTime, plan) <- withTiming conf $ liftEither $ Plan.actionPlan iAction conf apiReq sCache (planTime, plan) <- withTiming conf $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
let warnings = Plan.legacyWarnings plan
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."
legacyWarnHint = let replacement (relName, alias) = "`" <> relName <> "` to `" <> alias <> "`" in T.intercalate ", " (replacement <$> warnings)
shouldShowWarnings = configUrlUseLegacyTargetNames && not (null warnings)
liftIO $ when shouldShowWarnings $
observer $ LegacyTargetNameWarningObs (legacyWarnMsg, legacyWarnHint) iMethod (iPath <> Wai.rawQueryString req) -- TODO maybe store rawQueryString in ApiRequest for consistency
let mainQ = Query.mainQuery plan conf apiReq authResult configDbPreRequest let mainQ = Query.mainQuery plan conf apiReq authResult configDbPreRequest
tx = MainTx.mainTx mainQ conf authResult apiReq plan sCache tx = MainTx.mainTx mainQ conf authResult apiReq plan sCache
obsQuery s = when configLogQuery $ observer $ QueryObs mainQ s obsQuery s = when configLogQuery $ observer $ QueryObs mainQ s
@@ -235,12 +243,14 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
liftIO $ obsQuery status' liftIO $ obsQuery status'
liftEither response liftEither response
return $ toWaiResponse (ServerTiming jwtTime parseTime planTime txTime respTime) resp let warnHdrMsgs = if shouldShowWarnings then Just (legacyWarnMsg, legacyWarnHint) else Nothing
return $ toWaiResponse (ServerTiming jwtTime parseTime planTime txTime respTime) warnHdrMsgs resp
where where
toWaiResponse :: ServerTiming -> Response.PgrstResponse -> Wai.Response toWaiResponse :: ServerTiming -> Maybe (Text, Text) -> Response.PgrstResponse -> Wai.Response
toWaiResponse timing (Response.PgrstResponse st hdrs bod) = toWaiResponse timing warnMsgs (Response.PgrstResponse st hdrs bod) =
Wai.responseLBS st (hdrs ++ serverTimingHeaders timing ++ [varyHeader | not $ varyHeaderPresent hdrs]) bod Wai.responseLBS st (hdrs ++ serverTimingHeaders timing ++ warningHeaders warnMsgs ++ [varyHeader | not $ varyHeaderPresent hdrs]) bod
serverTimingHeaders :: ServerTiming -> [HTTP.Header] serverTimingHeaders :: ServerTiming -> [HTTP.Header]
serverTimingHeaders timing = [serverTimingHeader timing | configServerTimingEnabled] serverTimingHeaders timing = [serverTimingHeader timing | configServerTimingEnabled]
@@ -251,6 +261,14 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
varyHeaderPresent :: [HTTP.Header] -> Bool varyHeaderPresent :: [HTTP.Header] -> Bool
varyHeaderPresent = any (\(h, _v) -> h == hVary) varyHeaderPresent = any (\(h, _v) -> h == hVary)
warningHeaders :: Maybe (Text, Text) -> [HTTP.Header]
warningHeaders Nothing = []
warningHeaders (Just (msg, hint)) =
let warnMsg = msg <> " Update " <> hint <> " in query string filters, orders or limits."
pgrstVer = "PostgRESTv" <> BS.filter (/= ' ') prettyVersion
in
[(hWarning, "299 " <> pgrstVer <> " \"" <> encodeUtf8 warnMsg <> "\"")]
withTiming :: (MonadError e m, MonadIO m) => AppConfig -> m a -> m (Maybe Double, a) withTiming :: (MonadError e m, MonadIO m) => AppConfig -> m a -> m (Maybe Double, a)
withTiming AppConfig{configServerTimingEnabled} f = if configServerTimingEnabled withTiming AppConfig{configServerTimingEnabled} f = if configServerTimingEnabled
then do then do
+8
View File
@@ -121,6 +121,7 @@ data AppConfig = AppConfig
, configServerTimingEnabled :: Bool , configServerTimingEnabled :: Bool
, configServerUnixSocket :: Maybe FilePath , configServerUnixSocket :: Maybe FilePath
, configServerUnixSocketMode :: FileMode , configServerUnixSocketMode :: FileMode
, configUrlUseLegacyTargetNames :: Bool
, configAdminServerHost :: Text , configAdminServerHost :: Text
, configAdminServerPort :: Maybe Int , configAdminServerPort :: Maybe Int
, configAdminServerUnixSocket :: Maybe FilePath , configAdminServerUnixSocket :: Maybe FilePath
@@ -207,6 +208,7 @@ toText conf =
,("server-timing-enabled", T.toLower . show . configServerTimingEnabled) ,("server-timing-enabled", T.toLower . show . configServerTimingEnabled)
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket) ,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
,("server-unix-socket-mode", q . T.pack . showSocketMode) ,("server-unix-socket-mode", q . T.pack . showSocketMode)
,("url-use-legacy-target-names", T.toLower . show . configUrlUseLegacyTargetNames)
,("admin-server-host", q . configAdminServerHost) ,("admin-server-host", q . configAdminServerHost)
,("admin-server-port", maybe "\"\"" show . configAdminServerPort) ,("admin-server-port", maybe "\"\"" show . configAdminServerPort)
,("admin-server-unix-socket", q . maybe mempty T.pack . configAdminServerUnixSocket) ,("admin-server-unix-socket", q . maybe mempty T.pack . configAdminServerUnixSocket)
@@ -325,6 +327,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> (fromMaybe False <$> optBool "server-timing-enabled") <*> (fromMaybe False <$> optBool "server-timing-enabled")
<*> (fmap T.unpack <$> optString "server-unix-socket") <*> (fmap T.unpack <$> optString "server-unix-socket")
<*> parseSocketFileMode "server-unix-socket-mode" <*> parseSocketFileMode "server-unix-socket-mode"
<*> (fromMaybe True <$> optBool "url-use-legacy-target-names")
<*> (defaultServerHost <$> optWithAlias (optString "admin-server-host") <*> (defaultServerHost <$> optWithAlias (optString "admin-server-host")
(optString "server-host")) (optString "server-host"))
<*> parseAdminServerPort "admin-server-port" <*> parseAdminServerPort "admin-server-port"
@@ -794,4 +797,9 @@ exampleConfigFile = S.unlines
, "## Unix socket file mode" , "## Unix socket file mode"
, "## When none is provided, 660 is applied by default" , "## When none is provided, 660 is applied by default"
, "# server-unix-socket-mode = \"660\"" , "# server-unix-socket-mode = \"660\""
, ""
, "## Use legacy target names in relationship filters"
, "## If active, allows using the target name of the relationship in filters even if it has an alias."
, "## Otherwise it only allows the alias in filters"
, "url-use-legacy-target-names = true"
] ]
+1
View File
@@ -71,6 +71,7 @@ dbSettingsNames =
,"server_cors_allowed_origins" ,"server_cors_allowed_origins"
,"server_trace_header" ,"server_trace_header"
,"server_timing_enabled" ,"server_timing_enabled"
,"url_use_legacy_target_names"
] ]
queryPgVersion :: Session PgVersion queryPgVersion :: Session PgVersion
+4 -2
View File
@@ -175,7 +175,7 @@ instance ErrorBody ApiRequestError where
message InvalidFilters = "Filters must include all and only primary key columns with 'eq' operators" message InvalidFilters = "Filters must include all and only primary key columns with 'eq' operators"
message (UnacceptableSchema sch _) = "Invalid schema: " <> sch message (UnacceptableSchema sch _) = "Invalid schema: " <> sch
message (MediaTypeError cts) = "None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts) message (MediaTypeError cts) = "None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)
message (NotEmbedded resource) = "'" <> resource <> "' is not an embedded resource in this request" message (NotEmbedded resource _) = "'" <> resource <> "' is not an embedded resource in this request"
message GucHeadersError = "response.headers guc must be a JSON array composed of objects with a single key and a string value" message GucHeadersError = "response.headers guc must be a JSON array composed of objects with a single key and a string value"
message GucStatusError = "response.status guc must be a valid status code" message GucStatusError = "response.status guc must be a valid status code"
message PutLimitNotAllowedError = "limit/offset querystring parameters are not allowed for PUT" message PutLimitNotAllowedError = "limit/offset querystring parameters are not allowed for PUT"
@@ -207,11 +207,13 @@ instance ErrorBody ApiRequestError where
details (InvalidPreferences prefs) = Just $ JSON.String $ T.decodeUtf8 ("Invalid preferences: " <> BS.intercalate ", " prefs) details (InvalidPreferences prefs) = Just $ JSON.String $ T.decodeUtf8 ("Invalid preferences: " <> BS.intercalate ", " prefs)
details (MaxAffectedViolationError n) = Just $ JSON.String $ T.unwords ["The query affects", show n, "rows"] details (MaxAffectedViolationError n) = Just $ JSON.String $ T.unwords ["The query affects", show n, "rows"]
details (NotImplemented details') = Just $ JSON.String details' details (NotImplemented details') = Just $ JSON.String details'
details (NotEmbedded _ (Just _)) = Just $ JSON.String "Target names are not allowed in filters if they have an alias"
details _ = Nothing details _ = Nothing
-- HINT: Maybe JSON.Value -- HINT: Maybe JSON.Value
hint (NotEmbedded resource) = Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter." hint (NotEmbedded resource Nothing) = Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter."
hint (NotEmbedded _ (Just (name, alias))) = Just $ JSON.String $ "Change '" <> name <> "' to '" <> alias <> "' in filters, orders or limits."
hint (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorHint raiseErr hint (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorHint raiseErr
hint (UnacceptableSchema _ schemas) = Just $ JSON.String $ "Only the following schemas are exposed: " <> T.intercalate ", " schemas hint (UnacceptableSchema _ schemas) = Just $ JSON.String $ "Only the following schemas are exposed: " <> T.intercalate ", " schemas
+1 -1
View File
@@ -44,7 +44,7 @@ data ApiRequestError
| InvalidPreferences [ByteString] | InvalidPreferences [ByteString]
| InvalidRange RangeError | InvalidRange RangeError
| InvalidRpcMethod ByteString | InvalidRpcMethod ByteString
| NotEmbedded Text | NotEmbedded Text (Maybe (Text, Text))
| NotImplemented Text | NotImplemented Text
| PutLimitNotAllowedError | PutLimitNotAllowedError
| QueryParamError QPError | QueryParamError QPError
+4
View File
@@ -192,6 +192,10 @@ observationMessages = \case
let snipts = renderSnippet <$> [mqTxVars, fromMaybe mempty mqPreReq, mqMain, x, y, z, fromMaybe mempty mqExplain] let snipts = renderSnippet <$> [mqTxVars, fromMaybe mempty mqPreReq, mqMain, x, y, z, fromMaybe mempty mqExplain]
in in
showOnSingleLine '\n' . T.decodeUtf8 <$> filter (/= mempty) snipts showOnSingleLine '\n' . T.decodeUtf8 <$> filter (/= mempty) snipts
LegacyTargetNameWarningObs (warningMsg, warningHints) requestMethod requestTarget ->
[ "WARNING: " <> warningMsg
, "Update filters, orders or limits that use " <> warningHints <> " in " <> "`" <> T.decodeUtf8 (requestMethod <> " " <> requestTarget) <> "`"
]
ConfigReadErrorObs usageErr -> ConfigReadErrorObs usageErr ->
pure $ "Failed to query database settings for the config parameters." <> jsonMessage usageErr pure $ "Failed to query database settings for the config parameters." <> jsonMessage usageErr
QueryRoleSettingsErrorObs usageErr -> QueryRoleSettingsErrorObs usageErr ->
+1
View File
@@ -45,6 +45,7 @@ data Observation
| DBListenerGotConfigMsg ByteString | DBListenerGotConfigMsg ByteString
| DBListenerConnectionCleanupFail SomeException | DBListenerConnectionCleanupFail SomeException
| QueryObs MainQuery Status | QueryObs MainQuery Status
| LegacyTargetNameWarningObs (Text, Text) ByteString ByteString
| ConfigReadErrorObs SQL.UsageError | ConfigReadErrorObs SQL.UsageError
| ConfigInvalidObs Text | ConfigInvalidObs Text
| ConfigSucceededObs | ConfigSucceededObs
+69 -30
View File
@@ -23,6 +23,7 @@ module PostgREST.Plan
, InspectPlan(..) , InspectPlan(..)
, InfoPlan(..) , InfoPlan(..)
, CrudPlan(..) , CrudPlan(..)
, legacyWarnings
) where ) where
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
@@ -144,6 +145,25 @@ data InfoPlan
| RoutineInfoPlan Routine -- info about function | RoutineInfoPlan Routine -- info about function
| SchemaInfoPlan -- info about schema cache | SchemaInfoPlan -- info about schema cache
legacyWarnings :: ActionPlan -> [(Text, Text)]
legacyWarnings (NoDb _) = []
legacyWarnings (Db dbPlan) =
case dbPlan of
DbCrud _ crudPlan ->
readPlanWarnings $ case crudPlan of
WrappedReadPlan{wrReadPlan} -> wrReadPlan
MutateReadPlan{mrReadPlan} -> mrReadPlan
CallReadPlan{crReadPlan} -> crReadPlan
MayUseDb _ -> []
where
readPlanWarnings :: ReadPlanTree -> [(Text, Text)]
readPlanWarnings (Node rp forest) =
maybeToList (readPlanWarning rp) <> foldMap readPlanWarnings forest
readPlanWarning :: ReadPlan -> Maybe (Text, Text)
readPlanWarning ReadPlan{relName, relAlias = Just alias, relIsLegacyTargetNameMatch = True} = Just (relName, alias)
readPlanWarning _ = Nothing
actionPlan :: Action -> AppConfig -> ApiRequest -> SchemaCache -> Either Error ActionPlan actionPlan :: Action -> AppConfig -> ApiRequest -> SchemaCache -> Either Error ActionPlan
actionPlan act conf apiReq sCache = case act of actionPlan act conf apiReq sCache = case act of
ActDb dbAct -> Db <$> dbActionPlan dbAct conf apiReq sCache ActDb dbAct -> Db <$> dbActionPlan dbAct conf apiReq sCache
@@ -354,7 +374,7 @@ resolveQueryInputField ctx field opExpr = withTextParse ctx $ resolveTypeOrUnkno
-- | Adds filters, order, limits on its respective nodes. -- | Adds filters, order, limits on its respective nodes.
-- | Adds joins conditions obtained from resource embedding. -- | Adds joins conditions obtained from resource embedding.
readPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error ReadPlanTree readPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error ReadPlanTree
readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows, configDbAggregates} SchemaCache{dbTables, dbRelationships, dbRepresentations} apiRequest = readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows, configDbAggregates, configUrlUseLegacyTargetNames} SchemaCache{dbTables, dbRelationships, dbRepresentations} apiRequest =
let let
-- JSON output format hardcoded for now. In the future we might want to support other output mappings such as CSV. -- JSON output format hardcoded for now. In the future we might want to support other output mappings such as CSV.
ctx = ResolverContext dbTables dbRepresentations qi "json" ctx = ResolverContext dbTables dbRepresentations qi "json"
@@ -369,10 +389,10 @@ readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows, configDbAggregate
addAliases =<< addAliases =<<
expandStars ctx =<< expandStars ctx =<<
addRels qiSchema (iAction apiRequest) dbRelationships Nothing =<< addRels qiSchema (iAction apiRequest) dbRelationships Nothing =<<
addLogicTrees ctx apiRequest =<< addLogicTrees ctx apiRequest configUrlUseLegacyTargetNames =<<
addRanges apiRequest =<< addRanges apiRequest configUrlUseLegacyTargetNames =<<
addOrders ctx apiRequest =<< addOrders ctx apiRequest configUrlUseLegacyTargetNames =<<
addFilters ctx apiRequest (initReadRequest ctx $ QueryParams.qsSelect $ iQueryParams apiRequest) addFilters ctx apiRequest configUrlUseLegacyTargetNames (initReadRequest ctx $ QueryParams.qsSelect $ iQueryParams apiRequest)
-- Build the initial read plan tree -- Build the initial read plan tree
initReadRequest :: ResolverContext -> [Tree SelectItem] -> ReadPlanTree initReadRequest :: ResolverContext -> [Tree SelectItem] -> ReadPlanTree
@@ -380,7 +400,7 @@ initReadRequest ctx@ResolverContext{qi=QualifiedIdentifier{..}} =
foldr (treeEntry rootDepth) $ Node defReadPlan{from=qi ctx, relName=qiName, depth=rootDepth} [] foldr (treeEntry rootDepth) $ Node defReadPlan{from=qi ctx, relName=qiName, depth=rootDepth} []
where where
rootDepth = 0 rootDepth = 0
defReadPlan = ReadPlan [] (QualifiedIdentifier mempty mempty) Nothing [] [] allRange mempty Nothing [] Nothing mempty Nothing Nothing Nothing [] rootDepth defReadPlan = ReadPlan [] (QualifiedIdentifier mempty mempty) Nothing [] [] allRange mempty Nothing [] Nothing mempty Nothing Nothing Nothing [] rootDepth False
treeEntry :: Depth -> Tree SelectItem -> ReadPlanTree -> ReadPlanTree treeEntry :: Depth -> Tree SelectItem -> ReadPlanTree -> ReadPlanTree
treeEntry depth (Node si fldForest) (Node q rForest) = treeEntry depth (Node si fldForest) (Node q rForest) =
let nxtDepth = succ depth in let nxtDepth = succ depth in
@@ -816,8 +836,8 @@ findTable qi@QualifiedIdentifier{..} sc@SchemaCache{dbTables} =
Nothing -> Left $ SchemaCacheErr $ TableNotFound qiSchema qiName sc Nothing -> Left $ SchemaCacheErr $ TableNotFound qiSchema qiName sc
Just _ -> Right qi Just _ -> Right qi
addFilters :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree addFilters :: ResolverContext -> ApiRequest -> Bool -> ReadPlanTree -> Either Error ReadPlanTree
addFilters ctx ApiRequest{..} rReq = addFilters ctx ApiRequest{..} useTargetNames rReq =
foldr addFilterToNode (Right rReq) flts foldr addFilterToNode (Right rReq) flts
where where
QueryParams.QueryParams{..} = iQueryParams QueryParams.QueryParams{..} = iQueryParams
@@ -829,15 +849,15 @@ addFilters ctx ApiRequest{..} rReq =
addFilterToNode :: (EmbedPath, Filter) -> Either Error ReadPlanTree -> Either Error ReadPlanTree addFilterToNode :: (EmbedPath, Filter) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
addFilterToNode = addFilterToNode =
updateNode (\flt (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=addFilterToLogicForest (resolveFilter ctx{qi=fromTable} flt) lf} f) updateNode useTargetNames (\flt (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=addFilterToLogicForest (resolveFilter ctx{qi=fromTable} flt) lf} f)
addOrders :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree addOrders :: ResolverContext -> ApiRequest -> Bool -> ReadPlanTree -> Either Error ReadPlanTree
addOrders ctx ApiRequest{..} rReq = foldr addOrderToNode (Right rReq) qsOrder addOrders ctx ApiRequest{..} useTargetNames rReq = foldr addOrderToNode (Right rReq) qsOrder
where where
QueryParams.QueryParams{..} = iQueryParams QueryParams.QueryParams{..} = iQueryParams
addOrderToNode :: (EmbedPath, [OrderTerm]) -> Either Error ReadPlanTree -> Either Error ReadPlanTree addOrderToNode :: (EmbedPath, [OrderTerm]) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
addOrderToNode = updateNode (\o (Node q f) -> Node q{order=resolveOrder ctx <$> o} f) addOrderToNode = updateNode useTargetNames (\o (Node q f) -> Node q{order=resolveOrder ctx <$> o} f)
resolveOrder :: ResolverContext -> OrderTerm -> CoercibleOrderTerm resolveOrder :: ResolverContext -> OrderTerm -> CoercibleOrderTerm
resolveOrder _ (OrderRelationTerm a b c d) = CoercibleOrderRelationTerm a b c d resolveOrder _ (OrderRelationTerm a b c d) = CoercibleOrderRelationTerm a b c d
@@ -862,7 +882,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
then Right $ cot{coRelation=relAggAlias} then Right $ cot{coRelation=relAggAlias}
else Left $ ApiRequestErr $ RelatedOrderNotToOne (qiName from) name else Left $ ApiRequestErr $ RelatedOrderNotToOne (qiName from) name
Nothing -> Nothing ->
Left $ ApiRequestErr $ NotEmbedded coRelation Left $ ApiRequestErr $ NotEmbedded coRelation Nothing
-- | Searches for null filters on embeds, e.g. `projects=not.is.null` on `GET /clients?select=*,projects(*)&projects=not.is.null` -- | Searches for null filters on embeds, e.g. `projects=not.is.null` on `GET /clients?select=*,projects(*)&projects=not.is.null`
-- --
@@ -887,7 +907,8 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
-- relToParent = Nothing, -- relToParent = Nothing,
-- relJoinConds = [], -- relJoinConds = [],
-- relAlias = Nothing, relAggAlias = "clients_projects_1", relHint = Nothing, relJoinType = Nothing, relSpread = Nothing, depth = 1, -- relAlias = Nothing, relAggAlias = "clients_projects_1", relHint = Nothing, relJoinType = Nothing, relSpread = Nothing, depth = 1,
-- relSelect = [] -- relSelect = [],
-- relIsLegacyTargetNameMatch = False
-- }, -- },
-- subForest = [] -- subForest = []
-- } -- }
@@ -913,7 +934,8 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
-- ], -- ],
-- order = [], range_ = fullRange, relName = "clients", relToParent = Nothing, relJoinConds = [], relAlias = Nothing, relAggAlias = "", relHint = Nothing, -- order = [], range_ = fullRange, relName = "clients", relToParent = Nothing, relJoinConds = [], relAlias = Nothing, relAggAlias = "", relHint = Nothing,
-- relJoinType = Nothing, relSpread = Nothing, depth = 0, -- relJoinType = Nothing, relSpread = Nothing, depth = 0,
-- relSelect = [] -- relSelect = [],
-- relIsLegacyTargetNameMatch = False
-- }, -- },
-- subForest = subForst -- subForest = subForst
-- } -- }
@@ -949,8 +971,8 @@ addNullEmbedFilters (Node rp@ReadPlan{where_=curLogic} forest) = do
flt@(CoercibleStmnt _) -> flt@(CoercibleStmnt _) ->
Right flt Right flt
addRanges :: ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree addRanges :: ApiRequest -> Bool -> ReadPlanTree -> Either Error ReadPlanTree
addRanges ApiRequest{..} rReq = addRanges ApiRequest{..} useTargetNames rReq =
case iAction of case iAction of
ActDb (ActRelationMut _ _) -> Right rReq ActDb (ActRelationMut _ _) -> Right rReq
_ -> foldr addRangeToNode (Right rReq) =<< ranges _ -> foldr addRangeToNode (Right rReq) =<< ranges
@@ -959,10 +981,10 @@ addRanges ApiRequest{..} rReq =
ranges = first (ApiRequestErr . QueryParamError) $ QueryParams.pRequestRange `traverse` HM.toList iRange ranges = first (ApiRequestErr . QueryParamError) $ QueryParams.pRequestRange `traverse` HM.toList iRange
addRangeToNode :: (EmbedPath, NonnegRange) -> Either Error ReadPlanTree -> Either Error ReadPlanTree addRangeToNode :: (EmbedPath, NonnegRange) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
addRangeToNode = updateNode (\r (Node q f) -> Node q{range_=r} f) addRangeToNode = updateNode useTargetNames (\r (Node q f) -> Node q{range_=r} f)
addLogicTrees :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree addLogicTrees :: ResolverContext -> ApiRequest -> Bool -> ReadPlanTree -> Either Error ReadPlanTree
addLogicTrees ctx ApiRequest{..} rReq = addLogicTrees ctx ApiRequest{..} useTargetNames rReq =
foldr addLogicTreeToNode (Right rReq) logic foldr addLogicTreeToNode (Right rReq) logic
where where
QueryParams.QueryParams{..} = iQueryParams QueryParams.QueryParams{..} = iQueryParams
@@ -975,7 +997,7 @@ addLogicTrees ctx ApiRequest{..} rReq =
_ -> filter (not . null . fst) qsLogic _ -> 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 useTargetNames (\t (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=resolveLogicTree ctx{qi=fromTable} t:lf} f)
resolveLogicTree :: ResolverContext -> LogicTree -> CoercibleLogicTree resolveLogicTree :: ResolverContext -> LogicTree -> CoercibleLogicTree
resolveLogicTree ctx (Stmnt flt) = CoercibleStmnt $ resolveFilter ctx flt resolveLogicTree ctx (Stmnt flt) = CoercibleStmnt $ resolveFilter ctx flt
@@ -985,18 +1007,35 @@ resolveFilter :: ResolverContext -> Filter -> CoercibleFilter
resolveFilter ctx (Filter fld opExpr) = CoercibleFilter{field=resolveQueryInputField ctx fld opExpr, opExpr=opExpr} resolveFilter ctx (Filter fld opExpr) = CoercibleFilter{field=resolveQueryInputField ctx fld opExpr, opExpr=opExpr}
-- Find a Node of the Tree and apply a function to it -- Find a Node of the Tree and apply a function to it
updateNode :: (a -> ReadPlanTree -> ReadPlanTree) -> (EmbedPath, a) -> Either Error ReadPlanTree -> Either Error ReadPlanTree updateNode :: Bool -> (a -> ReadPlanTree -> ReadPlanTree) -> (EmbedPath, a) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
updateNode f ([], a) rr = f a <$> rr updateNode _ f ([], a) rr = f a <$> rr
updateNode _ _ (Left e) = Left e updateNode _ _ _ (Left e) = Left e
updateNode f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) = updateNode useTargetNames f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) =
case findNode of case findNode useTargetNames of
Nothing -> Left $ ApiRequestErr $ NotEmbedded targetNodeName Nothing ->
Left $ ApiRequestErr $ NotEmbedded targetNodeName findLegacyUsage
Just target -> Just target ->
(\node -> Node rootNode $ node : delete target forest) <$> (\node -> Node rootNode $ node : delete target forest) <$>
updateNode f (remainingPath, a) (Right target) updateNode useTargetNames f (remainingPath, a) (Right $ updateLegacyAttrs target)
where where
findNode :: Maybe ReadPlanTree findNode :: Bool -> Maybe ReadPlanTree
findNode = find (\(Node ReadPlan{relName, relAlias} _) -> fromMaybe relName relAlias == targetNodeName) forest findNode isLegacy = find (matchTarget isLegacy) forest
matchTarget :: Bool -> ReadPlanTree -> Bool
matchTarget isLegacy (Node ReadPlan{relName, relAlias} _)
| isLegacy = relName == targetNodeName || relAlias == Just targetNodeName
| otherwise = fromMaybe relName relAlias == targetNodeName
updateLegacyAttrs :: ReadPlanTree -> ReadPlanTree
updateLegacyAttrs node@(Node rPlan@ReadPlan{relName, relAlias} children)
| relName == targetNodeName && isJust relAlias && relAlias /= Just targetNodeName =
Node rPlan{relIsLegacyTargetNameMatch=True} children
| otherwise = node
findLegacyUsage :: Maybe (Text, Text)
findLegacyUsage
| useTargetNames = Nothing
| otherwise = (\(Node rp _) -> fromMaybe mempty $ readPlanWarning rp) . updateLegacyAttrs <$> findNode True
mutatePlan :: Mutation -> QualifiedIdentifier -> ApiRequest -> SchemaCache -> ReadPlanTree -> Either Error MutatePlan 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 =
+17 -17
View File
@@ -32,22 +32,22 @@ data JoinCondition =
-- TODO: Enforce uniqueness of columns by changing to a Set instead of a List where applicable -- TODO: Enforce uniqueness of columns by changing to a Set instead of a List where applicable
data ReadPlan = ReadPlan data ReadPlan = ReadPlan
{ select :: [CoercibleSelectField] { select :: [CoercibleSelectField]
, from :: QualifiedIdentifier , from :: QualifiedIdentifier
, fromAlias :: Maybe Alias , fromAlias :: Maybe Alias
, where_ :: [CoercibleLogicTree] , where_ :: [CoercibleLogicTree]
, order :: [CoercibleOrderTerm] , order :: [CoercibleOrderTerm]
, range_ :: NonnegRange , range_ :: NonnegRange
, relName :: NodeName , relName :: NodeName
, relToParent :: Maybe Relationship , relToParent :: Maybe Relationship
, relJoinConds :: [JoinCondition] , relJoinConds :: [JoinCondition]
, relAlias :: Maybe Alias , relAlias :: Maybe Alias
, relAggAlias :: Alias , relAggAlias :: Alias
, relHint :: Maybe Hint , relHint :: Maybe Hint
, relJoinType :: Maybe JoinType , relJoinType :: Maybe JoinType
, relSpread :: Maybe SpreadType , relSpread :: Maybe SpreadType
, relSelect :: [RelSelectField] , relSelect :: [RelSelectField]
, depth :: Depth , depth :: Depth -- ^ used for aliasing
-- ^ used for aliasing , relIsLegacyTargetNameMatch :: Bool -- ^ used to ease migration into a new version with breaking change
} }
deriving (Eq, Show) deriving (Eq, Show)
@@ -1,7 +1,59 @@
- - - qiName: awards
qiSchema: public
- public
- - relCardinality:
relColumns:
- - director_id
- id
relCons: awards_director_id_fkey
tag: M2O
relFTableIsView: false
relForeignTable:
qiName: directors
qiSchema: public
relIsSelf: false
relTable:
qiName: awards
qiSchema: public
relTableIsView: false
tag: Relationship
- relCardinality:
relColumns:
- - film_id
- id
relCons: awards_film_id_fkey
tag: M2O
relFTableIsView: false
relForeignTable:
qiName: films
qiSchema: public
relIsSelf: false
relTable:
qiName: awards
qiSchema: public
relTableIsView: false
tag: Relationship
- - - qiName: directors - - - qiName: directors
qiSchema: public qiSchema: public
- public - public
- - relCardinality: - - relCardinality:
relColumns:
- - id
- director_id
relCons: awards_director_id_fkey
tag: O2M
relFTableIsView: false
relForeignTable:
qiName: awards
qiSchema: public
relIsSelf: false
relTable:
qiName: directors
qiSchema: public
relTableIsView: false
tag: Relationship
- relCardinality:
relColumns: relColumns:
- - id - - id
- director_id - director_id
@@ -22,6 +74,22 @@
qiSchema: public qiSchema: public
- public - public
- - relCardinality: - - relCardinality:
relColumns:
- - id
- film_id
relCons: awards_film_id_fkey
tag: O2M
relFTableIsView: false
relForeignTable:
qiName: awards
qiSchema: public
relIsSelf: false
relTable:
qiName: films
qiSchema: public
relTableIsView: false
tag: Relationship
- relCardinality:
relColumns: relColumns:
- - director_id - - director_id
- id - id
@@ -126,6 +126,64 @@
tableSchema: public tableSchema: public
tableUpdatable: false tableUpdatable: false
- - qiName: awards
qiSchema: public
- tableColumns:
director_id:
colDefault: null
colDescription: null
colEnum: []
colMaxLen: null
colName: director_id
colNominalType: integer
colNullable: true
colType: integer
film_id:
colDefault: null
colDescription: null
colEnum: []
colMaxLen: null
colName: film_id
colNominalType: integer
colNullable: true
colType: integer
id:
colDefault: null
colDescription: null
colEnum: []
colMaxLen: null
colName: id
colNominalType: integer
colNullable: false
colType: integer
name:
colDefault: null
colDescription: null
colEnum: []
colMaxLen: null
colName: name
colNominalType: text
colNullable: true
colType: text
year:
colDefault: null
colDescription: null
colEnum: []
colMaxLen: null
colName: year
colNominalType: integer
colNullable: true
colType: integer
tableDeletable: true
tableDescription: null
tableInsertable: true
tableIsView: false
tableName: awards
tablePKCols:
- id
tableSchema: public
tableUpdatable: true
- - qiName: films - - qiName: films
qiSchema: public qiSchema: public
- tableColumns: - tableColumns:
+1
View File
@@ -42,3 +42,4 @@ server-timing-enabled = false
server-trace-header = "" server-trace-header = ""
server-unix-socket = "" server-unix-socket = ""
server-unix-socket-mode = "660" server-unix-socket-mode = "660"
url-use-legacy-target-names = true
@@ -42,3 +42,4 @@ server-timing-enabled = false
server-trace-header = "" server-trace-header = ""
server-unix-socket = "" server-unix-socket = ""
server-unix-socket-mode = "660" server-unix-socket-mode = "660"
url-use-legacy-target-names = true
@@ -42,3 +42,4 @@ server-timing-enabled = false
server-trace-header = "" server-trace-header = ""
server-unix-socket = "" server-unix-socket = ""
server-unix-socket-mode = "660" server-unix-socket-mode = "660"
url-use-legacy-target-names = true
+1
View File
@@ -42,3 +42,4 @@ server-timing-enabled = false
server-trace-header = "" server-trace-header = ""
server-unix-socket = "" server-unix-socket = ""
server-unix-socket-mode = "660" server-unix-socket-mode = "660"
url-use-legacy-target-names = true
@@ -44,3 +44,4 @@ server-timing-enabled = true
server-trace-header = "traceparent" server-trace-header = "traceparent"
server-unix-socket = "/tmp/pgrst_io_test.sock" server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777" server-unix-socket-mode = "777"
url-use-legacy-target-names = true
@@ -44,3 +44,4 @@ server-timing-enabled = false
server-trace-header = "CF-Ray" server-trace-header = "CF-Ray"
server-unix-socket = "/tmp/pgrst_io_test.sock" server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777" server-unix-socket-mode = "777"
url-use-legacy-target-names = true
@@ -44,3 +44,4 @@ server-timing-enabled = true
server-trace-header = "X-Request-Id" server-trace-header = "X-Request-Id"
server-unix-socket = "/tmp/pgrst_io_test.sock" server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777" server-unix-socket-mode = "777"
url-use-legacy-target-names = false
+1
View File
@@ -43,3 +43,4 @@ server-timing-enabled = false
server-trace-header = "" server-trace-header = ""
server-unix-socket = "" server-unix-socket = ""
server-unix-socket-mode = "660" server-unix-socket-mode = "660"
url-use-legacy-target-names = true
+1
View File
@@ -42,3 +42,4 @@ server-timing-enabled = false
server-trace-header = "" server-trace-header = ""
server-unix-socket = "" server-unix-socket = ""
server-unix-socket-mode = "660" server-unix-socket-mode = "660"
url-use-legacy-target-names = true
+1
View File
@@ -41,6 +41,7 @@ PGRST_SERVER_TRACE_HEADER: X-Request-Id
PGRST_SERVER_TIMING_ENABLED: true PGRST_SERVER_TIMING_ENABLED: true
PGRST_SERVER_UNIX_SOCKET: /tmp/pgrst_io_test.sock PGRST_SERVER_UNIX_SOCKET: /tmp/pgrst_io_test.sock
PGRST_SERVER_UNIX_SOCKET_MODE: 777 PGRST_SERVER_UNIX_SOCKET_MODE: 777
PGRST_URL_USE_LEGACY_TARGET_NAMES: false
PGRST_ADMIN_SERVER_HOST: 127.0.0.1 PGRST_ADMIN_SERVER_HOST: 127.0.0.1
PGRST_ADMIN_SERVER_PORT: 3001 PGRST_ADMIN_SERVER_PORT: 3001
PGRST_ADMIN_SERVER_UNIX_SOCKET: /tmp/admin_io_test.sock PGRST_ADMIN_SERVER_UNIX_SOCKET: /tmp/admin_io_test.sock
+1
View File
@@ -38,6 +38,7 @@ server-trace-header = "X-Request-Id"
server-timing-enabled = true server-timing-enabled = true
server-unix-socket = "/tmp/pgrst_io_test.sock" server-unix-socket = "/tmp/pgrst_io_test.sock"
server-unix-socket-mode = "777" server-unix-socket-mode = "777"
url-use-legacy-target-names = false
admin-server-port = 3001 admin-server-port = 3001
admin-server-host = "127.0.0.1" admin-server-host = "127.0.0.1"
admin-server-unix-socket = "/tmp/admin_io_test.sock" admin-server-unix-socket = "/tmp/admin_io_test.sock"
+2
View File
@@ -25,6 +25,7 @@ ALTER ROLE db_config_authenticator SET pgrst.openapi_server_proxy_uri = 'https:/
ALTER ROLE db_config_authenticator SET pgrst.server_cors_allowed_origins = 'http://origin.com'; ALTER ROLE db_config_authenticator SET pgrst.server_cors_allowed_origins = 'http://origin.com';
ALTER ROLE db_config_authenticator SET pgrst.server_timing_enabled = 'false'; ALTER ROLE db_config_authenticator SET pgrst.server_timing_enabled = 'false';
ALTER ROLE db_config_authenticator SET pgrst.server_trace_header = 'CF-Ray'; ALTER ROLE db_config_authenticator SET pgrst.server_trace_header = 'CF-Ray';
ALTER ROLE db_config_authenticator SET pgrst.url_use_legacy_target_names = 'true';
ALTER ROLE db_config_authenticator SET pgrst.db_hoisted_tx_settings = 'autovacuum_work_mem'; ALTER ROLE db_config_authenticator SET pgrst.db_hoisted_tx_settings = 'autovacuum_work_mem';
-- override with database specific setting -- override with database specific setting
@@ -81,6 +82,7 @@ ALTER ROLE other_authenticator SET pgrst.openapi_server_proxy_uri = 'https://oth
ALTER ROLE other_authenticator SET pgrst.server_cors_allowed_origins = 'http://otherorigin.com'; ALTER ROLE other_authenticator SET pgrst.server_cors_allowed_origins = 'http://otherorigin.com';
ALTER ROLE other_authenticator SET pgrst.server_timing_enabled = 'true'; ALTER ROLE other_authenticator SET pgrst.server_timing_enabled = 'true';
ALTER ROLE other_authenticator SET pgrst.server_trace_header = 'traceparent'; ALTER ROLE other_authenticator SET pgrst.server_trace_header = 'traceparent';
ALTER ROLE other_authenticator SET pgrst.url_use_legacy_target_names = 'true';
ALTER ROLE other_authenticator SET pgrst.db_hoisted_tx_settings = 'maintenance_work_mem'; ALTER ROLE other_authenticator SET pgrst.db_hoisted_tx_settings = 'maintenance_work_mem';
create schema postgrest; create schema postgrest;
+1 -1
View File
@@ -3,7 +3,7 @@ GRANT USAGE ON SCHEMA test TO postgrest_test_anonymous;
GRANT SELECT ON authors_only TO postgrest_test_author; GRANT SELECT ON authors_only TO postgrest_test_author;
GRANT SELECT ON projects TO postgrest_test_anonymous, postgrest_test_w_superuser_settings; GRANT SELECT ON projects TO postgrest_test_anonymous, postgrest_test_w_superuser_settings;
GRANT SELECT ON directors, films TO postgrest_test_anonymous, postgrest_test_w_superuser_settings; GRANT SELECT ON directors, films, awards TO postgrest_test_anonymous, postgrest_test_w_superuser_settings;
GRANT ALL ON cats TO postgrest_test_anonymous; GRANT ALL ON cats TO postgrest_test_anonymous;
GRANT ALL ON items_w_isolation_level TO postgrest_test_anonymous, postgrest_test_repeatable_read, postgrest_test_serializable; GRANT ALL ON items_w_isolation_level TO postgrest_test_anonymous, postgrest_test_repeatable_read, postgrest_test_serializable;
+8
View File
@@ -25,6 +25,14 @@ CREATE TABLE films (
on delete cascade on delete cascade
); );
CREATE TABLE awards (
id int primary key,
name text,
year int,
film_id int references films(id),
director_id int references directors(id)
);
-- data to test resource embedding -- data to test resource embedding
TRUNCATE TABLE directors CASCADE; TRUNCATE TABLE directors CASCADE;
INSERT INTO directors INSERT INTO directors
+30
View File
@@ -2089,3 +2089,33 @@ def test_work_mem_in_role_settings(defaultenv):
response = postgrest.session.post("/rpc/get_work_mem", headers=headers) response = postgrest.session.post("/rpc/get_work_mem", headers=headers)
assert response.status_code == 200 assert response.status_code == 200
assert response.text == '"3MB"' assert response.text == '"3MB"'
@pytest.mark.parametrize("enabled", ["true", "false"])
def test_use_legacy_target_names(enabled, defaultenv):
"Show a warning when a target name is used instead of an alias, only when config is enabled"
env = {
**defaultenv,
"PGRST_URL_USE_LEGACY_TARGET_NAMES": enabled,
}
with run(env=env) as postgrest:
response = postgrest.session.get(
"/directors?select=name,all_films:films(title),awards_2026:awards(name)&films.order=title&awards.year=eq.2026"
)
output = postgrest.read_stdout(nlines=10)
log_err_warning = "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."
log_err_hint = "Update filters, orders or limits that use `films` to `all_films`, `awards` to `awards_2026` in `GET /directors?select=name,all_films:films(title),awards_2026:awards(name)&films.order=title&awards.year=eq.2026`"
has_warning_log = any(log_err_warning in line for line in output)
has_hint_log = any(log_err_hint in line for line in output)
if enabled == "true":
assert response.status_code == 200
assert has_warning_log and has_hint_log
else:
assert response.status_code == 400
assert not has_warning_log and not has_hint_log
-1
View File
@@ -33,7 +33,6 @@ CREATE TABLE test.roles (
character TEXT character TEXT
); );
CREATE TABLE test.authors_only (); CREATE TABLE test.authors_only ();
CREATE FUNCTION test.call_me (name TEXT) RETURNS TEXT CREATE FUNCTION test.call_me (name TEXT) RETURNS TEXT
+1
View File
@@ -122,6 +122,7 @@ baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configInternalSCQuerySleepFst = Nothing , configInternalSCQuerySleepFst = Nothing
, configInternalSCQuerySleepSnd = Nothing , configInternalSCQuerySleepSnd = Nothing
, configServerTimingEnabled = True , configServerTimingEnabled = True
, configUrlUseLegacyTargetNames = True
} }
testCfg :: AppConfig testCfg :: AppConfig
+34 -10
View File
@@ -7,6 +7,7 @@ import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
import PostgREST.Config (AppConfig (..))
import PostgREST.Config.PgVersion (PgVersion, pgVersion190) import PostgREST.Config.PgVersion (PgVersion, pgVersion190)
import Protolude hiding (get) import Protolude hiding (get)
@@ -1071,16 +1072,6 @@ spec actualPgVersion withConfig = withConfig baseCfg $ do
{ "id":4,"children":[]} { "id":4,"children":[]}
]|] { matchHeaders = [matchContentTypeJson] } ]|] { matchHeaders = [matchContentTypeJson] }
it "works when embedding the same table more than once" $
get "/places?select=name,visits(id,start_time,visit_type),work_visits:visits(id,start_time,visit_type)&id=eq.1&visits.visit_type=neq.work&visits.start_time=gt.20250101+00:00&work_visits.visit_type=eq.work&work_visits.start_time=gt.20250101+00:00" `shouldRespondWith`
[json|[
{
"name":"Lake",
"visits":[{"id": 1, "start_time": "2025-01-01T10:00:00", "visit_type": "vacation"}, {"id": 2, "start_time": "2025-01-01T15:00:00", "visit_type": "vacation"}],
"work_visits":[{"id": 3, "start_time": "2025-01-01T20:00:00", "visit_type": "work"}]
}
]|] { matchHeaders = [matchContentTypeJson] }
describe "ordering response" $ do describe "ordering response" $ do
it "by a column asc" $ it "by a column asc" $
get "/items?id=lte.2&order=id.asc" get "/items?id=lte.2&order=id.asc"
@@ -1186,6 +1177,12 @@ spec actualPgVersion withConfig = withConfig baseCfg $ do
] ]
} }
it "filters, orders and limits aliased embeded entities using the target name" $
get "/projects?id=eq.1&select=id, name, the_tasks:tasks(id, name)&tasks.name=like.Code*&tasks.order=name.asc&tasks.limit=1" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","the_tasks":[{"id":2,"name":"Code w7"}]}]|]
{ matchHeaders = ["Warning" <:> "299 PostgRESTv15(pre-release) \"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 `tasks` to `the_tasks` in query string filters, orders or limits.\""] }
describe "Accept headers" $ do describe "Accept headers" $ do
it "should respond an unknown accept type with 406" $ it "should respond an unknown accept type with 406" $
request methodGet "/simple_pk" request methodGet "/simple_pk"
@@ -1690,3 +1687,30 @@ spec actualPgVersion withConfig = withConfig baseCfg $ do
[json| {"code":"PGRST125","details":null,"hint":null,"message":"Invalid path specified in request URL"} |] [json| {"code":"PGRST125","details":null,"hint":null,"message":"Invalid path specified in request URL"} |]
{ matchStatus = 404 { matchStatus = 404
, matchHeaders = ["Content-Length" <:> "96"]} , matchHeaders = ["Content-Length" <:> "96"]}
specLegacyTargetNames :: SpecWithConfig
specLegacyTargetNames withConfig = withConfig (baseCfg { configUrlUseLegacyTargetNames = False }) $
context "disable legacy target names" $ do
describe "Shaping response with select parameter" $ do
it "works when embedding the same table more than once" $
get "/places?select=name,visits(id,start_time,visit_type),work_visits:visits(id,start_time,visit_type)&id=eq.1&visits.visit_type=neq.work&visits.start_time=gt.20250101+00:00&work_visits.visit_type=eq.work&work_visits.start_time=gt.20250101+00:00" `shouldRespondWith`
[json|[
{
"name":"Lake",
"visits":[{"id": 1, "start_time": "2025-01-01T10:00:00", "visit_type": "vacation"}, {"id": 2, "start_time": "2025-01-01T15:00:00", "visit_type": "vacation"}],
"work_visits":[{"id": 3, "start_time": "2025-01-01T20:00:00", "visit_type": "work"}]
}
]|] { matchHeaders = [matchContentTypeJson] }
describe "ordering response" $
it "filters, orders or limits do not work with aliased embeded entities using the target name" $
get "/projects?id=eq.1&select=id, name, the_tasks:tasks(id, name)&tasks.name=like.Code*&tasks.order=name.asc&tasks.limit=1" `shouldRespondWith`
[json|
{
"code":"PGRST108",
"details":"Target names are not allowed in filters if they have an alias",
"hint":"Change 'tasks' to 'the_tasks' in filters, orders or limits.",
"message":"'tasks' is not an embedded resource in this request"
}
|]
{ matchStatus = 400,
matchHeaders = [matchContentTypeJson] }
+1
View File
@@ -152,6 +152,7 @@ main = do
, ("Feature.Query.PreparedStatementsSpec.spec" , Feature.Query.PreparedStatementsSpec.spec) , ("Feature.Query.PreparedStatementsSpec.spec" , Feature.Query.PreparedStatementsSpec.spec)
, ("Feature.Query.QueryLimitedSpec" , Feature.Query.QueryLimitedSpec.spec) , ("Feature.Query.QueryLimitedSpec" , Feature.Query.QueryLimitedSpec.spec)
, ("Feature.Query.QuerySpec" , Feature.Query.QuerySpec.spec actualPgVersion) , ("Feature.Query.QuerySpec" , Feature.Query.QuerySpec.spec actualPgVersion)
, ("Feature.Query.QuerySpec.specLegacyTargetNames" , Feature.Query.QuerySpec.specLegacyTargetNames)
, ("Feature.Query.RangeSpec" , Feature.Query.RangeSpec.spec) , ("Feature.Query.RangeSpec" , Feature.Query.RangeSpec.spec)
, ("Feature.Query.RawOutputTypesSpec" , Feature.Query.RawOutputTypesSpec.spec) , ("Feature.Query.RawOutputTypesSpec" , Feature.Query.RawOutputTypesSpec.spec)
, ("Feature.Query.RelatedQueriesSpec" , Feature.Query.RelatedQueriesSpec.spec) , ("Feature.Query.RelatedQueriesSpec" , Feature.Query.RelatedQueriesSpec.spec)
+1
View File
@@ -175,6 +175,7 @@ baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configServerTraceHeader = Nothing , configServerTraceHeader = Nothing
, configServerUnixSocket = Nothing , configServerUnixSocket = Nothing
, configServerUnixSocketMode = 432 , configServerUnixSocketMode = 432
, configUrlUseLegacyTargetNames = True
, configDbTxAllowOverride = True , configDbTxAllowOverride = True
, configDbTxRollbackAll = True , configDbTxRollbackAll = True
, configAdminServerHost = "localhost" , configAdminServerHost = "localhost"