From 2d8190c7a74ab4ff2456b4d73d65239d59dc7f5b Mon Sep 17 00:00:00 2001 From: Bortlesboat <169967362+Bortlesboat@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:56:32 -0400 Subject: [PATCH] fix(postgrest): preserve text search negation --- .../src/postgrest/_async/request_builder.py | 4 +-- .../src/postgrest/_sync/request_builder.py | 4 +-- .../tests/_async/test_request_builder.py | 30 +++++++++++++++++++ .../tests/_sync/test_request_builder.py | 30 +++++++++++++++++++ 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/postgrest/src/postgrest/_async/request_builder.py b/src/postgrest/src/postgrest/_async/request_builder.py index f5c34e5f..384956c3 100644 --- a/src/postgrest/src/postgrest/_async/request_builder.py +++ b/src/postgrest/src/postgrest/_async/request_builder.py @@ -234,9 +234,7 @@ def text_search( elif type_ == "web_search": type_part = "w" config_part = f"({options.get('config')})" if options.get("config") else "" - self.request.params = self.request.params.add( - column, f"{type_part}fts{config_part}.{query}" - ) + self.filter(column, f"{type_part}fts{config_part}", query) return AsyncQueryRequestBuilder(self.request) diff --git a/src/postgrest/src/postgrest/_sync/request_builder.py b/src/postgrest/src/postgrest/_sync/request_builder.py index df502b69..c6a23bfc 100644 --- a/src/postgrest/src/postgrest/_sync/request_builder.py +++ b/src/postgrest/src/postgrest/_sync/request_builder.py @@ -234,9 +234,7 @@ def text_search( elif type_ == "web_search": type_part = "w" config_part = f"({options.get('config')})" if options.get("config") else "" - self.request.params = self.request.params.add( - column, f"{type_part}fts{config_part}.{query}" - ) + self.filter(column, f"{type_part}fts{config_part}", query) return SyncQueryRequestBuilder(self.request) diff --git a/src/postgrest/tests/_async/test_request_builder.py b/src/postgrest/tests/_async/test_request_builder.py index c755e70d..85e32d1e 100644 --- a/src/postgrest/tests/_async/test_request_builder.py +++ b/src/postgrest/tests/_async/test_request_builder.py @@ -241,6 +241,36 @@ def test_delete_with_select(self, request_builder: AsyncRequestBuilder): class TestTextSearch: + @pytest.mark.parametrize( + "mode,operator", + [ + (None, "fts"), + ("plain", "plfts"), + ("phrase", "phfts"), + ("web_search", "wfts"), + ], + ) + @pytest.mark.parametrize("config", [None, "english"]) + @pytest.mark.parametrize("negate", [False, True]) + def test_negation_is_applied_only_to_text_search( + self, request_builder: AsyncRequestBuilder, mode, operator, config, negate + ): + selected = request_builder.select("content") + if negate: + selected = selected.not_ + + builder = selected.text_search( + "content", "fat cat", {"type": mode, "config": config} + ) + + expected_operator = ("not." if negate else "") + operator + if config: + expected_operator += f"({config})" + assert builder.request.params["content"] == f"{expected_operator}.fat cat" + assert not selected.negate_next + selected.eq("published", "true") + assert builder.request.params["published"] == "eq.true" + def test_text_search(self, request_builder: AsyncRequestBuilder): builder = request_builder.select("catchphrase").text_search( "catchphrase", diff --git a/src/postgrest/tests/_sync/test_request_builder.py b/src/postgrest/tests/_sync/test_request_builder.py index 443e05a6..b5cfdd8f 100644 --- a/src/postgrest/tests/_sync/test_request_builder.py +++ b/src/postgrest/tests/_sync/test_request_builder.py @@ -241,6 +241,36 @@ def test_delete_with_select(self, request_builder: SyncRequestBuilder): class TestTextSearch: + @pytest.mark.parametrize( + "mode,operator", + [ + (None, "fts"), + ("plain", "plfts"), + ("phrase", "phfts"), + ("web_search", "wfts"), + ], + ) + @pytest.mark.parametrize("config", [None, "english"]) + @pytest.mark.parametrize("negate", [False, True]) + def test_negation_is_applied_only_to_text_search( + self, request_builder: SyncRequestBuilder, mode, operator, config, negate + ): + selected = request_builder.select("content") + if negate: + selected = selected.not_ + + builder = selected.text_search( + "content", "fat cat", {"type": mode, "config": config} + ) + + expected_operator = ("not." if negate else "") + operator + if config: + expected_operator += f"({config})" + assert builder.request.params["content"] == f"{expected_operator}.fat cat" + assert not selected.negate_next + selected.eq("published", "true") + assert builder.request.params["published"] == "eq.true" + def test_text_search(self, request_builder: SyncRequestBuilder): builder = request_builder.select("catchphrase").text_search( "catchphrase",