Compare commits

...
8 Commits
16 changed files with 153 additions and 26 deletions
+5 -5
View File
@@ -35,6 +35,11 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
<img width="296px" src="static/supabase.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://www.euronodes.com/postgrest" target="_blank">
<img width="296px" src="static/euronodes.svg">
</a>
</td>
</tr>
</tbody>
</table>
@@ -88,11 +93,6 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
<img width="222px" src="static/oblivious.jpg">
</a>
</td>
<td align="center" valign="middle">
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="222px" src="static/gnuhost.png">
</a>
</td>
</tr>
</tbody>
</table>
+13
View File
@@ -5,6 +5,19 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased
## [13.0.3] - 2025-06-16
### Fixed
- Fix `max-affected` preference not failing with RPC when `handling=strict` by @taimoorzaeem in #4100
- Fix a property definition's type in OpenAPI not showing the correct base type of a recursive domain by @laurenceisla in #4136
## [13.0.2] - 2025-06-02
### Fixed
- Fix regression that makes `ORDER BY` with nulls-order not work alongside limits by @laurenceisla in #4109
## [13.0.1] - 2025-06-01
### Fixed
+5
View File
@@ -44,6 +44,11 @@ API than you are likely to write from scratch.
<img width="296px" src="static/supabase.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://www.euronodes.com/postgrest" target="_blank">
<img width="296px" src="static/euronodes.svg">
</a>
</td>
</tr>
</tbody>
</table>
+5
View File
@@ -41,6 +41,11 @@ write from scratch.
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://www.euronodes.com/postgrest" target="_blank">
<img width="296px" src="static/euronodes.svg">
</a>
</td>
</tr>
</tbody>
</table>
+9 -2
View File
@@ -73,8 +73,15 @@ Sponsors
.. image:: ../static/supabase.png
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
.. image:: _static/empty.png
:target: #sponsors
.. container:: img-dark
.. image:: ../static/euronodes.svg
:target: https://www.euronodes.com/postgrest
.. container:: img-light
.. image:: ../static/euronodes.svg
:target: https://www.euronodes.com/postgrest
.. The static/empty.png(created with `convert -size 320x95 xc:#fcfcfc empty.png`) is an ugly workaround
to create space and center the logos. It's not easy to layout with restructuredText.
+4
View File
@@ -292,3 +292,7 @@ With :ref:`RPC <functions>`, the preference is honored completely on the basis o
"details": "The query affects 14 rows",
"hint": null
}
.. note::
It is important for functions to return ``SETOF`` or ``TABLE`` when called with ``max-affected`` preference. A violation of this would cause a :ref:`PGRST128 <pgrst128>` error.
+4
View File
@@ -267,6 +267,10 @@ Related to the HTTP request elements.
| | | implemented. |
| PGRST127 | | |
+---------------+-------------+-------------------------------------------------------------+
| .. _pgrst128: | 400 | ``max-affected`` preference is violated with ``RPC`` call. |
| | | See :ref:`prefer_max_affected`. |
| PGRST128 | | |
+---------------+-------------+-------------------------------------------------------------+
.. _pgrst2**:
+1 -1
View File
@@ -1,5 +1,5 @@
name: postgrest
version: 13.0.1
version: 13.0.3
synopsis: REST API for any Postgres database
description: Reads the schema of a PostgreSQL database and creates RESTful routes
for tables, views, and functions, supporting all HTTP methods that security
+4
View File
@@ -96,6 +96,7 @@ data ApiRequestError
| MaxAffectedViolationError Integer
| InvalidResourcePath
| OpenAPIDisabled
| MaxAffectedRpcViolation
deriving Show
data QPError = QPError Text Text
@@ -138,6 +139,7 @@ instance PgrstError ApiRequestError where
status MaxAffectedViolationError{} = HTTP.status400
status InvalidResourcePath = HTTP.status404
status OpenAPIDisabled = HTTP.status404
status MaxAffectedRpcViolation = HTTP.status400
headers _ = mempty
@@ -184,6 +186,7 @@ instance ErrorBody ApiRequestError where
code InvalidResourcePath = "PGRST125"
code OpenAPIDisabled = "PGRST126"
code NotImplemented{} = "PGRST127"
code MaxAffectedRpcViolation = "PGRST128"
-- MESSAGE: Text
message (QueryParamError (QPError msg _)) = msg
@@ -209,6 +212,7 @@ instance ErrorBody ApiRequestError where
message InvalidResourcePath = "Invalid path specified in request URL"
message OpenAPIDisabled = "Root endpoint metadata is disabled"
message (NotImplemented _) = "Feature not implemented"
message MaxAffectedRpcViolation = "Function must return SETOF or TABLE when max-affected preference is used with handling=strict"
-- DETAILS: Maybe JSON.Value
details (QueryParamError (QPError _ dets)) = Just $ JSON.String dets
+8 -2
View File
@@ -72,7 +72,8 @@ import PostgREST.SchemaCache.Routine (MediaHandler (..),
RoutineParam (..),
funcReturnsCompositeAlias,
funcReturnsScalar,
funcReturnsSetOfScalar)
funcReturnsSetOfScalar,
funcReturnsSingle)
import PostgREST.SchemaCache.Table (Column (..), Table (..),
TablesMap,
tableColumnsList,
@@ -172,7 +173,7 @@ mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..}
return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType mutation qi
callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CallReadPlan
callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{preferHandling, invalidPrefs},..} invMethod = do
callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{preferHandling, invalidPrefs, preferMaxAffected},..} invMethod = do
let paramKeys = case invMethod of
InvRead _ -> S.fromList $ fst <$> qsParams'
Inv -> iColumns
@@ -192,10 +193,15 @@ callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferenc
cPlan = callPlan proc apiRequest paramKeys args rPlan
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
failMaxAffectedRpcReturnsSingle (preferMaxAffected, preferHandling) proc
return $ CallReadPlan rPlan cPlan txMode proc handler mediaType invMethod identifier
where
qsParams' = QueryParams.qsParams iQueryParams
failMaxAffectedRpcReturnsSingle :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> Routine -> Either Error ()
failMaxAffectedRpcReturnsSingle (Just (PreferMaxAffected _), Just Strict) rout = if funcReturnsSingle rout then Left $ ApiRequestError MaxAffectedRpcViolation else Right ()
failMaxAffectedRpcReturnsSingle _ _ = Right ()
hasDefaultSelect :: ReadPlanTree -> Bool
hasDefaultSelect (Node ReadPlan{select=[CoercibleSelectField{csField=CoercibleField{cfName}}]} []) = cfName == "*"
hasDefaultSelect _ = False
+2 -2
View File
@@ -53,8 +53,8 @@ readPlanToQuery node@(Node ReadPlan{select,from=mainQi,fromAlias,where_=logicFor
(if null logicForest && null relJoinConds
then mempty
else " WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition relJoinConds)) <> " " <>
groupF qi select relSelect <>
orderF qi order <>
groupF qi select relSelect <> " " <>
orderF qi order <> " " <>
limitOffsetF readRange
where
fromFrag = fromF relToParent mainQi fromAlias
+23 -14
View File
@@ -375,32 +375,40 @@ accessibleFuncs = SQL.Statement sql params decodeFuncs
(snd >$< arrayParam HE.text)
sql = funcsSqlQuery <> " AND has_function_privilege(p.oid, 'execute')"
funcsSqlQuery :: SqlQuery
funcsSqlQuery = encodeUtf8 [trimming|
-- Recursively get the base types of domains
WITH
baseTypesCte :: Text
baseTypesCte = [trimming|
-- Recursively get the base types of domains
base_types AS (
WITH RECURSIVE
recurse AS (
SELECT
oid,
typbasetype,
COALESCE(NULLIF(typbasetype, 0), oid) AS base
typnamespace AS base_namespace,
COALESCE(NULLIF(typbasetype, 0), oid) AS base_type
FROM pg_type
UNION
SELECT
t.oid,
b.typbasetype,
COALESCE(NULLIF(b.typbasetype, 0), b.oid) AS base
b.typnamespace AS base_namespace,
COALESCE(NULLIF(b.typbasetype, 0), b.oid) AS base_type
FROM recurse t
JOIN pg_type b ON t.typbasetype = b.oid
)
SELECT
oid,
base
base_namespace,
base_type
FROM recurse
WHERE typbasetype = 0
),
)
|]
funcsSqlQuery :: SqlQuery
funcsSqlQuery = encodeUtf8 [trimming|
WITH
$baseTypesCte,
arguments AS (
SELECT
oid,
@@ -440,7 +448,7 @@ funcsSqlQuery = encodeUtf8 [trimming|
-- if any TABLE, INOUT or OUT arguments present, treat as composite
or COALESCE(proargmodes::text[] && '{t,b,o}', false)
) AS rettype_is_composite,
bt.oid <> bt.base as rettype_is_composite_alias,
bt.oid <> bt.base_type as rettype_is_composite_alias,
p.provolatile,
p.provariadic > 0 as hasvariadic,
lower((regexp_split_to_array((regexp_split_to_array(iso_config, '='))[2], ','))[1]) AS transaction_isolation_level,
@@ -449,7 +457,7 @@ funcsSqlQuery = encodeUtf8 [trimming|
LEFT JOIN arguments a ON a.oid = p.oid
JOIN pg_namespace pn ON pn.oid = p.pronamespace
JOIN base_types bt ON bt.oid = p.prorettype
JOIN pg_type t ON t.oid = bt.base
JOIN pg_type t ON t.oid = bt.base_type
JOIN pg_namespace tn ON tn.oid = t.typnamespace
LEFT JOIN pg_class comp ON comp.oid = t.typrelid
LEFT JOIN pg_description as d ON d.objoid = p.oid AND d.classoid = 'pg_proc'::regclass
@@ -615,6 +623,7 @@ tablesSqlQuery =
-- generated columns are only available from pg >= 10 but the query is agnostic to versions. dep.deptype = 'i' is done because there are other 'a' dependencies on PKs
encodeUtf8 [trimming|
WITH
$baseTypesCte,
columns AS (
SELECT
c.oid AS relid,
@@ -631,7 +640,7 @@ tablesSqlQuery =
CASE
WHEN t.typtype = 'd' THEN
CASE
WHEN bt.typnamespace = 'pg_catalog'::regnamespace THEN format_type(t.typbasetype, NULL::integer)
WHEN bt.base_namespace = 'pg_catalog'::regnamespace THEN format_type(bt.base_type, NULL::integer)
ELSE format_type(a.atttypid, a.atttypmod)
END
ELSE
@@ -645,7 +654,7 @@ tablesSqlQuery =
information_schema._pg_truetypid(a.*, t.*),
information_schema._pg_truetypmod(a.*, t.*)
)::integer AS character_maximum_length,
COALESCE(bt.oid, t.oid) AS base_type,
bt.base_type,
a.attnum::integer AS position
FROM pg_attribute a
LEFT JOIN pg_description AS d
@@ -656,8 +665,8 @@ tablesSqlQuery =
ON a.attrelid = c.oid
JOIN pg_type t
ON a.atttypid = t.oid
LEFT JOIN pg_type bt
ON t.typtype = 'd' AND t.typbasetype = bt.oid
LEFT JOIN base_types bt
ON t.oid = bt.oid
LEFT JOIN pg_depend seq
ON seq.refobjid = a.attrelid and seq.refobjsubid = a.attnum and seq.deptype = 'i'
WHERE
+4
View File
@@ -0,0 +1,4 @@
<svg width="917" height="146" viewBox="0 0 917 146" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.5462 82.5408H83.3862C80.9862 64.1408 69.1462 52.6208 52.0262 52.6208C35.3862 52.6208 22.1062 64.9408 19.5462 82.5408ZM102.906 92.6208V97.5808H19.7062C23.0662 115.341 37.1462 127.181 55.3862 127.181C67.8662 127.181 79.3862 121.261 86.2662 112.941L98.7462 125.261C88.5062 138.381 73.1462 145.101 54.5862 145.101C23.0662 145.101 0.186172 122.701 0.186172 89.9008C0.186172 58.3808 22.5862 34.7008 52.5062 34.7008C83.3862 34.7008 102.906 57.2608 102.906 92.6208ZM129.367 35.5008V98.7008C129.367 118.701 139.447 127.821 155.927 127.821C173.687 127.821 187.927 112.941 187.927 92.7808V35.5008H206.327V144.301H189.527L187.927 123.181V122.061C182.647 132.301 171.447 145.101 148.407 145.101C126.167 145.101 110.967 131.181 110.967 105.901V35.5008H129.367ZM217.72 144.301V35.5008H234.52L236.12 56.1408C241.88 42.2208 253.24 34.7008 268.6 34.7008C272.44 34.7008 275.96 35.0208 279.16 35.5008L277.72 53.5808C274.68 52.7808 271.16 52.4608 266.36 52.4608C247 52.4608 236.12 68.3008 236.12 91.6608V144.301H217.72ZM278.311 89.9008C278.311 56.7808 300.071 34.7008 332.551 34.7008C365.191 34.7008 386.791 56.7808 386.791 89.9008C386.791 123.021 365.191 145.101 332.551 145.101C300.071 145.101 278.311 123.021 278.311 89.9008ZM297.031 89.7408C297.031 112.141 311.591 127.021 332.551 127.021C353.671 127.021 368.071 112.141 368.071 89.7408C368.071 67.6608 353.671 52.7808 332.551 52.7808C311.591 52.7808 297.031 67.6608 297.031 89.7408Z" fill="#FF0831"/>
<path d="M471.711 144.301V86.7008C471.711 64.1408 461.311 51.9808 443.711 51.9808C426.911 51.9808 413.151 69.1008 413.151 88.9408V144.301H394.751V35.5008H411.551L413.151 56.6208V59.5008C418.271 48.3008 428.671 34.7008 449.631 34.7008C473.631 34.7008 490.111 51.6608 490.111 76.9408V144.301H471.711ZM498.467 89.9008C498.467 56.7808 520.227 34.7008 552.707 34.7008C585.347 34.7008 606.947 56.7808 606.947 89.9008C606.947 123.021 585.347 145.101 552.707 145.101C520.227 145.101 498.467 123.021 498.467 89.9008ZM517.187 89.7408C517.187 112.141 531.747 127.021 552.707 127.021C573.827 127.021 588.227 112.141 588.227 89.7408C588.227 67.6608 573.827 52.7808 552.707 52.7808C531.747 52.7808 517.187 67.6608 517.187 89.7408ZM697.147 97.5808V85.5808C697.147 68.1408 682.907 52.4608 664.987 52.4608C647.067 52.4608 632.027 67.6608 632.027 89.7408C632.027 112.141 646.427 127.341 665.307 127.341C684.027 127.341 697.147 115.341 697.147 97.5808ZM698.747 144.301L697.307 127.501C691.707 136.301 681.307 145.101 662.427 145.101C634.107 145.101 613.307 123.021 613.307 89.9008C613.307 56.7808 634.427 34.7008 661.307 34.7008C680.187 34.7008 690.907 44.1408 697.147 53.9008V0.300776H715.547V144.301H698.747ZM744.702 82.5408H808.542C806.142 64.1408 794.302 52.6208 777.182 52.6208C760.542 52.6208 747.262 64.9408 744.702 82.5408ZM828.062 92.6208V97.5808H744.862C748.222 115.341 762.302 127.181 780.542 127.181C793.022 127.181 804.542 121.261 811.422 112.941L823.902 125.261C813.662 138.381 798.302 145.101 779.742 145.101C748.222 145.101 725.342 122.701 725.342 89.9008C725.342 58.3808 747.742 34.7008 777.662 34.7008C808.542 34.7008 828.062 57.2608 828.062 92.6208ZM916.161 115.181C916.161 132.781 900.481 145.101 876.641 145.101C858.721 145.101 844.321 137.581 834.561 127.021L843.841 112.781C851.361 120.781 861.761 128.141 876.801 128.141C889.761 128.141 897.441 122.221 897.441 114.221C897.441 90.5408 836.801 104.941 836.801 67.1808C836.801 47.9808 853.121 34.7008 874.721 34.7008C892.001 34.7008 905.761 42.0608 914.881 53.9008L902.721 65.4208C895.841 56.6208 884.961 52.1408 873.761 52.1408C862.721 52.1408 855.041 58.7008 855.041 67.0208C855.041 89.1008 916.161 75.5008 916.161 115.181Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -191,3 +191,48 @@ spec =
""
{ matchStatus = 204
, matchHeaders = ["Preference-Applied" <:> "handling=lenient"]}
context "test Prefer: max-affected with rpc" $ do
it "should fail with rpc when deleting rows more than prefered with returns setof" $
request methodPost "/rpc/delete_items_returns_setof"
[("Prefer", "handling=strict, max-affected=10")]
""
`shouldRespondWith`
[json| {"code":"PGRST124","details":"The query affects 15 rows","hint":null,"message":"Query result exceeds max-affected preference constraint"} |]
{ matchStatus = 400 }
it "should fail with rpc when deleting rows more than prefered with returns table" $
request methodPost "/rpc/delete_items_returns_table"
[("Prefer", "handling=strict, max-affected=10")]
""
`shouldRespondWith`
[json| {"code":"PGRST124","details":"The query affects 15 rows","hint":null,"message":"Query result exceeds max-affected preference constraint"} |]
{ matchStatus = 400 }
it "should succeed with rpc deleting rows less than prefered with returns setof" $
request methodPost "/rpc/delete_items_returns_setof"
[("Prefer", "handling=strict, max-affected=20")]
""
`shouldRespondWith`
[json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},
{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},
{"id":14},{"id":15}]|]
{ matchStatus = 200 }
it "should succeed with rpc deleting rows less than prefered with returns table" $
request methodPost "/rpc/delete_items_returns_table"
[("Prefer", "handling=strict, max-affected=20")]
""
`shouldRespondWith`
[json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},
{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},
{"id":14},{"id":15}]|]
{ matchStatus = 200 }
it "should fail with rpc when returns void with handling=strict" $
request methodPost "/rpc/delete_items_returns_void"
[("Prefer", "handling=strict, max-affected=20")]
""
`shouldRespondWith`
[json| {"code":"PGRST128","details":null,"hint":null,"message":"Function must return SETOF or TABLE when max-affected preference is used with handling=strict"} |]
{ matchStatus = 400 }
+8
View File
@@ -223,6 +223,14 @@ spec = do
, "Content-Range" <:> "2-4/*" ]
}
it "works alongside order by with nulls order" $
get "/clients?select=id,projects(id,tasks(id))&order=id.asc.nullslast&limit=1&projects.order=id.asc.nullsfirst&projects.limit=2"
`shouldRespondWith`
[json|[{"id":1,"projects":[{"id": 1, "tasks": [{"id": 1}, {"id": 2}]}, {"id": 2, "tasks": [{"id": 3}, {"id": 4}]}]}]|]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/*"]
}
context "succeeds if offset equals 0 as a no-op" $ do
it "no items" $ do
get "/items?offset=0&id=eq.0"
+13
View File
@@ -3815,3 +3815,16 @@ set oid = 'test.collision_test_table'::regclass::oid
where oid = 'test.collision_test_func'::regproc::oid;
comment on function test.collision_test_func(id integer) is 'fizzbuzz';
create or replace function test.delete_items_returns_setof() returns setof items as $$
delete from items where id <= 15 returning *; -- deletes 15 items, then return them
$$ language sql;
create or replace function test.delete_items_returns_table() returns table(id bigint) as $$
delete from items where id <= 15 returning *;
$$ language sql;
create or replace function test.delete_items_returns_void() returns void as $$
delete from items;
$$ language sql;