diff --git a/src/postgrest/src/postgrest/base_request_builder.py b/src/postgrest/src/postgrest/base_request_builder.py index 973316fe..3e7ff0aa 100644 --- a/src/postgrest/src/postgrest/base_request_builder.py +++ b/src/postgrest/src/postgrest/base_request_builder.py @@ -526,6 +526,26 @@ def match(self: Self, query: Dict[str, Any]) -> Self: return updated_query + def max_affected(self: Self, value: int) -> Self: + """Set the maximum number of rows that can be affected by the query. + + Only available in PostgREST v13+ and only works with PATCH and DELETE methods. + + Args: + value: The maximum number of rows that can be affected + """ + prefer_header = self.headers.get("Prefer", "") + if prefer_header: + if "handling=strict" not in prefer_header: + prefer_header += ",handling=strict" + else: + prefer_header = "handling=strict" + + prefer_header += f",max-affected={value}" + + self.headers["Prefer"] = prefer_header + return self + class BaseSelectRequestBuilder(BaseFilterRequestBuilder[_ReturnT]): def __init__( diff --git a/src/postgrest/tests/_async/test_filter_request_builder.py b/src/postgrest/tests/_async/test_filter_request_builder.py index b381e801..1174e1ea 100644 --- a/src/postgrest/tests/_async/test_filter_request_builder.py +++ b/src/postgrest/tests/_async/test_filter_request_builder.py @@ -241,3 +241,34 @@ def test_or_in_contain(filter_request_builder): str(builder.params) == "or=%28id.in.%285%2C6%2C7%29%2C+arraycol.cs.%7B%27a%27%2C%27b%27%7D%29" ) + + +def test_max_affected(filter_request_builder): + builder = filter_request_builder.max_affected(5) + + assert builder.headers["prefer"] == "handling=strict,max-affected=5" + + +def test_max_affected_with_existing_prefer_header(filter_request_builder): + # Set an existing prefer header + filter_request_builder.headers["prefer"] = "return=representation" + builder = filter_request_builder.max_affected(10) + + assert ( + builder.headers["prefer"] + == "return=representation,handling=strict,max-affected=10" + ) + + +def test_max_affected_with_existing_handling_strict(filter_request_builder): + # Set an existing prefer header with handling=strict + filter_request_builder.headers["prefer"] = "handling=strict,return=minimal" + builder = filter_request_builder.max_affected(3) + + assert builder.headers["prefer"] == "handling=strict,return=minimal,max-affected=3" + + +def test_max_affected_returns_self(filter_request_builder): + builder = filter_request_builder.max_affected(1) + + assert builder is filter_request_builder diff --git a/src/postgrest/tests/_async/test_request_builder.py b/src/postgrest/tests/_async/test_request_builder.py index 07224344..e3986182 100644 --- a/src/postgrest/tests/_async/test_request_builder.py +++ b/src/postgrest/tests/_async/test_request_builder.py @@ -147,6 +147,15 @@ def test_update_with_count(self, request_builder: AsyncRequestBuilder): assert builder.http_method == "PATCH" assert builder.json == {"key1": "val1"} + def test_update_with_max_affected(self, request_builder: AsyncRequestBuilder): + builder = request_builder.update({"key1": "val1"}).max_affected(5) + + assert "handling=strict" in builder.headers["prefer"] + assert "max-affected=5" in builder.headers["prefer"] + assert "return=representation" in builder.headers["prefer"] + assert builder.http_method == "PATCH" + assert builder.json == {"key1": "val1"} + class TestDelete: def test_delete(self, request_builder: AsyncRequestBuilder): @@ -166,6 +175,15 @@ def test_delete_with_count(self, request_builder: AsyncRequestBuilder): assert builder.http_method == "DELETE" assert builder.json == {} + def test_delete_with_max_affected(self, request_builder: AsyncRequestBuilder): + builder = request_builder.delete().max_affected(10) + + assert "handling=strict" in builder.headers["prefer"] + assert "max-affected=10" in builder.headers["prefer"] + assert "return=representation" in builder.headers["prefer"] + assert builder.http_method == "DELETE" + assert builder.json == {} + class TestTextSearch: def test_text_search(self, request_builder: AsyncRequestBuilder): diff --git a/src/postgrest/tests/_sync/test_filter_request_builder.py b/src/postgrest/tests/_sync/test_filter_request_builder.py index ef28f210..2eae647f 100644 --- a/src/postgrest/tests/_sync/test_filter_request_builder.py +++ b/src/postgrest/tests/_sync/test_filter_request_builder.py @@ -241,3 +241,34 @@ def test_or_in_contain(filter_request_builder): str(builder.params) == "or=%28id.in.%285%2C6%2C7%29%2C+arraycol.cs.%7B%27a%27%2C%27b%27%7D%29" ) + + +def test_max_affected(filter_request_builder): + builder = filter_request_builder.max_affected(5) + + assert builder.headers["prefer"] == "handling=strict,max-affected=5" + + +def test_max_affected_with_existing_prefer_header(filter_request_builder): + # Set an existing prefer header + filter_request_builder.headers["prefer"] = "return=representation" + builder = filter_request_builder.max_affected(10) + + assert ( + builder.headers["prefer"] + == "return=representation,handling=strict,max-affected=10" + ) + + +def test_max_affected_with_existing_handling_strict(filter_request_builder): + # Set an existing prefer header with handling=strict + filter_request_builder.headers["prefer"] = "handling=strict,return=minimal" + builder = filter_request_builder.max_affected(3) + + assert builder.headers["prefer"] == "handling=strict,return=minimal,max-affected=3" + + +def test_max_affected_returns_self(filter_request_builder): + builder = filter_request_builder.max_affected(1) + + assert builder is filter_request_builder diff --git a/src/postgrest/tests/_sync/test_request_builder.py b/src/postgrest/tests/_sync/test_request_builder.py index 6f89d52e..865703cf 100644 --- a/src/postgrest/tests/_sync/test_request_builder.py +++ b/src/postgrest/tests/_sync/test_request_builder.py @@ -147,6 +147,15 @@ def test_update_with_count(self, request_builder: SyncRequestBuilder): assert builder.http_method == "PATCH" assert builder.json == {"key1": "val1"} + def test_update_with_max_affected(self, request_builder: SyncRequestBuilder): + builder = request_builder.update({"key1": "val1"}).max_affected(5) + + assert "handling=strict" in builder.headers["prefer"] + assert "max-affected=5" in builder.headers["prefer"] + assert "return=representation" in builder.headers["prefer"] + assert builder.http_method == "PATCH" + assert builder.json == {"key1": "val1"} + class TestDelete: def test_delete(self, request_builder: SyncRequestBuilder): @@ -166,6 +175,15 @@ def test_delete_with_count(self, request_builder: SyncRequestBuilder): assert builder.http_method == "DELETE" assert builder.json == {} + def test_delete_with_max_affected(self, request_builder: SyncRequestBuilder): + builder = request_builder.delete().max_affected(10) + + assert "handling=strict" in builder.headers["prefer"] + assert "max-affected=10" in builder.headers["prefer"] + assert "return=representation" in builder.headers["prefer"] + assert builder.http_method == "DELETE" + assert builder.json == {} + class TestTextSearch: def test_text_search(self, request_builder: SyncRequestBuilder): diff --git a/uv.lock b/uv.lock index 7e637721..d7cd56a5 100644 --- a/uv.lock +++ b/uv.lock @@ -2785,7 +2785,6 @@ dev = [ { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx-press-theme" }, { name = "sphinx-toolbox" }, - { name = "types-deprecated" }, { name = "unasync" }, ] docs = [ @@ -2801,7 +2800,6 @@ lints = [ { name = "python-lsp-ruff" }, { name = "python-lsp-server" }, { name = "ruff" }, - { name = "types-deprecated" }, { name = "unasync" }, ] tests = [ @@ -2832,7 +2830,6 @@ dev = [ { name = "sphinx", specifier = ">=7.1.2" }, { name = "sphinx-press-theme", specifier = ">=0.9.1" }, { name = "sphinx-toolbox", specifier = ">=3.4.0" }, - { name = "types-deprecated", specifier = ">=1.2.15" }, { name = "unasync", specifier = ">=0.6.0" }, ] docs = [ @@ -2846,7 +2843,6 @@ lints = [ { name = "python-lsp-ruff", specifier = ">=2.2.2,<3.0.0" }, { name = "python-lsp-server", specifier = ">=1.12.2,<2.0.0" }, { name = "ruff", specifier = ">=0.12.1" }, - { name = "types-deprecated", specifier = ">=1.2.15" }, { name = "unasync", specifier = ">=0.6.0" }, ] tests = [ @@ -3153,15 +3149,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, ] -[[package]] -name = "types-deprecated" -version = "1.2.15.20250304" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0e/67/eeefaaabb03b288aad85483d410452c8bbcbf8b2bd876b0e467ebd97415b/types_deprecated-1.2.15.20250304.tar.gz", hash = "sha256:c329030553029de5cc6cb30f269c11f4e00e598c4241290179f63cda7d33f719", size = 8015, upload-time = "2025-03-04T02:48:17.894Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/e3/c18aa72ab84e0bc127a3a94e93be1a6ac2cb281371d3a45376ab7cfdd31c/types_deprecated-1.2.15.20250304-py3-none-any.whl", hash = "sha256:86a65aa550ea8acf49f27e226b8953288cd851de887970fbbdf2239c116c3107", size = 8553, upload-time = "2025-03-04T02:48:16.666Z" }, -] - [[package]] name = "typing-extensions" version = "4.15.0"