From 8228938812462a75ed6e9c47caf1af3b42b3f0bc Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 25 Nov 2025 08:21:10 -0300 Subject: [PATCH 1/3] feat(postgrest): add missing PostgREST v12 operators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three missing operators from PostgREST v12 specification: - matchRegex: PostgreSQL regex matching (~) - case sensitive - imatchRegex: PostgreSQL regex matching (~*) - case insensitive - isDistinct: IS DISTINCT FROM - treats NULL as comparable value All existing operators were reviewed and verified against v12 docs. Tests added for all new operators. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../lib/src/postgrest_filter_builder.dart | 39 +++++++++++++++++++ packages/postgrest/test/filter_test.dart | 36 +++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/packages/postgrest/lib/src/postgrest_filter_builder.dart b/packages/postgrest/lib/src/postgrest_filter_builder.dart index 97798fe6a..b922903ba 100644 --- a/packages/postgrest/lib/src/postgrest_filter_builder.dart +++ b/packages/postgrest/lib/src/postgrest_filter_builder.dart @@ -476,6 +476,45 @@ class PostgrestFilterBuilder extends PostgrestTransformBuilder { return copyWithUrl(url); } + /// Finds all rows whose value in the stated [column] matches the supplied [pattern] using PostgreSQL regular expression (case sensitive). + /// + /// ```dart + /// await supabase + /// .from('users') + /// .select() + /// .matchRegex('username', '^sup.*'); + /// ``` + PostgrestFilterBuilder matchRegex(String column, String pattern) { + return copyWithUrl(appendSearchParams(column, 'match.$pattern')); + } + + /// Finds all rows whose value in the stated [column] matches the supplied [pattern] using PostgreSQL regular expression (case insensitive). + /// + /// ```dart + /// await supabase + /// .from('users') + /// .select() + /// .imatchRegex('username', '^SUP.*'); + /// ``` + PostgrestFilterBuilder imatchRegex(String column, String pattern) { + return copyWithUrl(appendSearchParams(column, 'imatch.$pattern')); + } + + /// Finds all rows whose value on the stated [column] is not equal to the specified [value], treating `NULL` as a comparable value. + /// + /// This is different from [neq] which treats `NULL` specially. + /// + /// ```dart + /// await supabase + /// .from('users') + /// .select() + /// .isDistinct('age', null); + /// ``` + // ignore: non_constant_identifier_names + PostgrestFilterBuilder isDistinct(String column, Object? value) { + return copyWithUrl(appendSearchParams(column, 'isdistinct.$value')); + } + @override PostgrestFilterBuilder setHeader(String key, String value) { return PostgrestFilterBuilder( diff --git a/packages/postgrest/test/filter_test.dart b/packages/postgrest/test/filter_test.dart index 606a3ac6f..220c27d15 100644 --- a/packages/postgrest/test/filter_test.dart +++ b/packages/postgrest/test/filter_test.dart @@ -477,6 +477,42 @@ void main() { expect(res[0]['username'], 'supabot'); }); + test('matchRegex - regex match (case sensitive)', () async { + final res = await postgrest + .from('users') + .select('username') + .matchRegex('username', '^supa.*'); + expect(res, isNotEmpty); + for (final item in res) { + expect((item['username'] as String).startsWith('supa'), true); + } + }); + + test('imatchRegex - regex match (case insensitive)', () async { + final res = await postgrest + .from('users') + .select('username') + .imatchRegex('username', '^SUPA.*'); + expect(res, isNotEmpty); + for (final item in res) { + expect( + (item['username'] as String).toLowerCase().startsWith('supa'), true); + } + }); + + test('isDistinct - treats NULL as comparable', () async { + final res = await postgrest + .from('users') + .select('username, data') + .isDistinct('data', null); + expect(res, isNotEmpty); + // isDistinct should return rows where data IS DISTINCT FROM null + // which means rows where data is NOT null + for (final item in res) { + expect(item['data'] != null, true); + } + }); + test('filter on rpc', () async { final List res = await postgrest.rpc('get_username_and_status', params: {'name_param': 'supabot'}).neq('status', 'ONLINE'); From 1fcd5008340eecf059a418f172eb25370f06d288 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Wed, 10 Dec 2025 08:25:32 -0300 Subject: [PATCH 2/3] test: fix isDistinct test to test with a non-null value --- packages/postgrest/test/filter_test.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/postgrest/test/filter_test.dart b/packages/postgrest/test/filter_test.dart index 220c27d15..49ac4c55a 100644 --- a/packages/postgrest/test/filter_test.dart +++ b/packages/postgrest/test/filter_test.dart @@ -500,19 +500,16 @@ void main() { } }); - test('isDistinct - treats NULL as comparable', () async { + test('isDistinct', () async { final res = await postgrest .from('users') - .select('username, data') - .isDistinct('data', null); + .select('username,status') + .isDistinct('status', 'ONLINE'); expect(res, isNotEmpty); - // isDistinct should return rows where data IS DISTINCT FROM null - // which means rows where data is NOT null for (final item in res) { - expect(item['data'] != null, true); + expect(item['status'] != 'ONLINE', true); } }); - test('filter on rpc', () async { final List res = await postgrest.rpc('get_username_and_status', params: {'name_param': 'supabot'}).neq('status', 'ONLINE'); From e2a35a3ae3787d01fac275663c49ffa924afceb2 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Wed, 10 Dec 2025 10:19:50 -0300 Subject: [PATCH 3/3] chore(examples): update gradle to 8.13.1 --- .../example/android/gradle/wrapper/gradle-wrapper.properties | 2 +- packages/supabase_flutter/example/android/settings.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/supabase_flutter/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/supabase_flutter/example/android/gradle/wrapper/gradle-wrapper.properties index d5ce57cba..ae4378fe9 100644 --- a/packages/supabase_flutter/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/supabase_flutter/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip \ No newline at end of file +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip \ No newline at end of file diff --git a/packages/supabase_flutter/example/android/settings.gradle b/packages/supabase_flutter/example/android/settings.gradle index ed68c2f27..6c34fad02 100644 --- a/packages/supabase_flutter/example/android/settings.gradle +++ b/packages/supabase_flutter/example/android/settings.gradle @@ -18,7 +18,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.6.0" apply false + id "com.android.application" version '8.13.1' apply false id "org.jetbrains.kotlin.android" version "2.1.20" apply false }