From 205966438197f201fb4f284f3fb3a52997ffc96c Mon Sep 17 00:00:00 2001 From: Vinzent Date: Thu, 16 Jul 2026 23:50:55 +0200 Subject: [PATCH 1/4] docs: add limitations to stream method --- .../lib/src/supabase_query_builder.dart | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/supabase/lib/src/supabase_query_builder.dart b/packages/supabase/lib/src/supabase_query_builder.dart index a8d6b48ef..358b2e418 100644 --- a/packages/supabase/lib/src/supabase_query_builder.dart +++ b/packages/supabase/lib/src/supabase_query_builder.dart @@ -23,24 +23,42 @@ class SupabaseQueryBuilder extends PostgrestQueryBuilder { url: Uri.parse(url), ); - /// Combines the current state of your table from PostgREST with changes from the realtime server to return real-time data from your table as a [Stream]. + /// Combines the current state of your table from PostgREST with changes from + /// the realtime server to return real-time data from your table as a [Stream]. /// - /// Realtime is disabled by default for new tables. You can turn it on by [managing replication](https://supabase.com/docs/guides/realtime/subscribing-to-database-changes#enable-postgres-changes). + /// Realtime is disabled by default for new tables. You can turn it on by + /// [managing replication](https://supabase.com/docs/guides/realtime/subscribing-to-database-changes#enable-postgres-changes). /// - /// Pass the list of primary key column names to [primaryKey], which will be used to update and delete the proper records internally as the stream receives real-time updates. + /// Pass the list of primary key column names to [primaryKey], which will be + /// used to update and delete the proper records internally as the stream + /// receives realtime updates. /// - /// The underlying [RealtimeChannel] is public by default. Set [private] to `true` to make it private, which requires additional RLS policies to be set up. See https://supabase.com/docs/guides/realtime/authorization for more details. + /// The underlying [RealtimeChannel] is public by default. Set [private] to + /// true` to make it private, which requires additional RLS policies to be + /// set up. See https://supabase.com/docs/guides/realtime/authorization for + /// more details. /// - /// It handles the lifecycle of the realtime connection and automatically refetches data from PostgREST when needed. + /// It handles the life cycle of the realtime connection and automatically + /// refetches data from PostgREST when needed. /// - /// Make sure to provide `onError` and `onDone` callbacks to [Stream.listen] to handle errors and completion of the stream. + /// Make sure to provide `onError` and `onDone` callbacks to [Stream.listen] + /// to handle errors and completion of the stream. /// The stream gets closed when the realtime connection is closed. /// + /// Be aware of the following limitations when using streams: + /// - When using filters like `eq` only realtime updates matching the filter + /// will be received. Therefore, an update that changes a record to no + /// longer match the filter will not be received, and the record will remain + /// in the stream. + /// - By default, for DELETE events only the primary key columns can be used. + /// Refer to the documentation about [receiving old records](https://supabase.com/docs/guides/realtime/postgres-changes?queryGroups=language&language=dart#receiving-old-records). + /// /// ```dart /// supabase.from('chats').stream(primaryKey: ['id']).listen(_onChatsReceived); /// ``` /// - /// `eq`, `neq`, `lt`, `lte`, `gt` or `gte` and `order`, `limit` filter are available to limit the data being queried. + /// `eq`, `neq`, `lt`, `lte`, `gt` or `gte` and `order`, `limit` filter are + /// available to limit the data being queried. /// /// ```dart /// supabase.from('chats').stream(primaryKey: ['id']).eq('room_id','123').order('created_at').limit(20).listen(_onChatsReceived); From 7c974bf2b686251774644841bafc6452d1f9b26a Mon Sep 17 00:00:00 2001 From: Vinzent Date: Thu, 16 Jul 2026 23:56:09 +0200 Subject: [PATCH 2/4] feat: add missing realtime filters to stream --- .../lib/src/supabase_query_builder.dart | 5 +- .../lib/src/supabase_stream_builder.dart | 35 ++++--- .../src/supabase_stream_filter_builder.dart | 99 ++++++++++++++++++- 3 files changed, 124 insertions(+), 15 deletions(-) diff --git a/packages/supabase/lib/src/supabase_query_builder.dart b/packages/supabase/lib/src/supabase_query_builder.dart index 358b2e418..e957d1074 100644 --- a/packages/supabase/lib/src/supabase_query_builder.dart +++ b/packages/supabase/lib/src/supabase_query_builder.dart @@ -57,8 +57,9 @@ class SupabaseQueryBuilder extends PostgrestQueryBuilder { /// supabase.from('chats').stream(primaryKey: ['id']).listen(_onChatsReceived); /// ``` /// - /// `eq`, `neq`, `lt`, `lte`, `gt` or `gte` and `order`, `limit` filter are - /// available to limit the data being queried. + /// `eq`, `neq`, `lt`, `lte`, `gt` `gte`, `like`, `ilike`, `match`, `imatch`, + /// or `isDistinct` and `order`, `limit` filter are available to limit the + /// data being queried. /// /// ```dart /// supabase.from('chats').stream(primaryKey: ['id']).eq('room_id','123').order('created_at').limit(20).listen(_onChatsReceived); diff --git a/packages/supabase/lib/src/supabase_stream_builder.dart b/packages/supabase/lib/src/supabase_stream_builder.dart index 0247bf5ae..463b2ddfc 100644 --- a/packages/supabase/lib/src/supabase_stream_builder.dart +++ b/packages/supabase/lib/src/supabase_stream_builder.dart @@ -256,18 +256,29 @@ class SupabaseStreamBuilder extends Stream { _streamFilter!.column, _streamFilter!.value, ), - // These operators are only reachable through the realtime - // `onPostgresChanges` API, not through `.stream()`'s filter builder, - // so they can never be set on `_streamFilter`. Guard the exhaustive - // switch defensively in case that ever changes. - PostgresChangeFilterType.like || - PostgresChangeFilterType.ilike || - PostgresChangeFilterType.isFilter || - PostgresChangeFilterType.match || - PostgresChangeFilterType.imatch || - PostgresChangeFilterType.isDistinct => throw UnsupportedError( - 'The "${_streamFilter!.type.name}" filter is not supported by ' - '`.stream()`. Use one of eq, neq, lt, lte, gt, gte or inFilter.', + PostgresChangeFilterType.like => query.like( + _streamFilter!.column, + _streamFilter!.value, + ), + PostgresChangeFilterType.ilike => query.ilike( + _streamFilter!.column, + _streamFilter!.value, + ), + PostgresChangeFilterType.match => query.matchRegex( + _streamFilter!.column, + _streamFilter!.value, + ), + PostgresChangeFilterType.imatch => query.imatchRegex( + _streamFilter!.column, + _streamFilter!.value, + ), + PostgresChangeFilterType.isFilter => query.isFilter( + _streamFilter!.column, + _streamFilter!.value, + ), + PostgresChangeFilterType.isDistinct => query.isDistinct( + _streamFilter!.column, + _streamFilter!.value, ), }; } diff --git a/packages/supabase/lib/src/supabase_stream_filter_builder.dart b/packages/supabase/lib/src/supabase_stream_filter_builder.dart index adff3a650..559f5b328 100644 --- a/packages/supabase/lib/src/supabase_stream_filter_builder.dart +++ b/packages/supabase/lib/src/supabase_stream_filter_builder.dart @@ -107,7 +107,7 @@ class SupabaseStreamFilterBuilder extends SupabaseStreamBuilder { return this; } - /// Filters the results where [column] is included in [value]. + /// Filters the results where [column] is included in [values]. /// /// Only one filter can be applied to `.stream()`. /// @@ -122,4 +122,101 @@ class SupabaseStreamFilterBuilder extends SupabaseStreamBuilder { ); return this; } + + /// Filters the results where [column] matches the [pattern] case-sensitive. + /// + /// Only one filter can be applied to `.stream()`. + /// + /// ```dart + /// supabase.from('users').stream(primaryKey: ['id']).like('title', '%foo%'); + /// ``` + SupabaseStreamBuilder like(String column, String pattern) { + _streamFilter = ( + type: PostgresChangeFilterType.like, + column: column, + value: pattern, + ); + return this; + } + + /// Filters the results where [column] matches the [pattern] case-insensitive. + /// + /// Only one filter can be applied to `.stream()`. + /// + /// ```dart + /// supabase.from('users').stream(primaryKey: ['id']).ilike('title', '%foo%'); + /// ``` + SupabaseStreamBuilder ilike(String column, String pattern) { + _streamFilter = ( + type: PostgresChangeFilterType.ilike, + column: column, + value: pattern, + ); + return this; + } + + /// Filters the results where [column] matches the POSIX [regex] case-sensitive. + /// + /// Only one filter can be applied to `.stream()`. + /// + /// ```dart + /// supabase.from('users').stream(primaryKey: ['id']).match('slug', r'^post-\d+$'); + /// ``` + SupabaseStreamBuilder match(String column, String regex) { + _streamFilter = ( + type: PostgresChangeFilterType.match, + column: column, + value: regex, + ); + return this; + } + + /// Filters the results where [column] matches the POSIX [regex] case-insensitive. + /// + /// Only one filter can be applied to `.stream()`. + /// + /// ```dart + /// supabase.from('users').stream(primaryKey: ['id']).imatch('slug', r'^post-\d+$'); + /// ``` + SupabaseStreamBuilder imatch(String column, String regex) { + _streamFilter = ( + type: PostgresChangeFilterType.imatch, + column: column, + value: regex, + ); + return this; + } + + /// Filters the results where [column] is `null`, `true` or `false`. + /// + /// Only one filter can be applied to `.stream()`. + /// + /// ```dart + /// supabase.from('users').stream(primaryKey: ['id']).isFilter('data', null); + /// ``` + SupabaseStreamBuilder isFilter(String column, bool? value) { + _streamFilter = ( + type: PostgresChangeFilterType.isFilter, + column: column, + value: value, + ); + return this; + } + + /// Filters the results where [column] is not equal to [value] treating `null + /// as a distinct value. + /// + /// Only one filter can be applied to `.stream()`. + /// + /// ```dart + /// supabase.from('users').stream(primaryKey: ['id']).isDistinct('age', null); + /// ``` + SupabaseStreamBuilder isDistinct(String column, Object? value) { + _streamFilter = ( + type: PostgresChangeFilterType.isDistinct, + column: column, + value: value, + ); + return this; + } } From cace5bcf3c2b9e0d2a63662980fcf98debfe132c Mon Sep 17 00:00:00 2001 From: Vinzent Date: Fri, 17 Jul 2026 00:09:22 +0200 Subject: [PATCH 3/4] feat: support multiple filters on stream method --- .../lib/src/supabase_stream_builder.dart | 81 +++++++------- .../src/supabase_stream_filter_builder.dart | 104 +++++++----------- 2 files changed, 79 insertions(+), 106 deletions(-) diff --git a/packages/supabase/lib/src/supabase_stream_builder.dart b/packages/supabase/lib/src/supabase_stream_builder.dart index 463b2ddfc..0d3acdc83 100644 --- a/packages/supabase/lib/src/supabase_stream_builder.dart +++ b/packages/supabase/lib/src/supabase_stream_builder.dart @@ -58,9 +58,8 @@ class SupabaseStreamBuilder extends Stream { /// Contains the combined data of postgrest and realtime to emit as stream. SupabaseStreamEvent _streamData = []; - /// `eq` filter used for both postgrest and realtime - // ignore: avoid-unassigned-fields - _StreamPostgrestFilter? _streamFilter; + /// Filters to be applied to the stream + List<_StreamPostgrestFilter> _streamFilter = []; /// Which column to order by and whether it's ascending _Order? _orderBy; @@ -147,16 +146,16 @@ class SupabaseStreamBuilder extends Stream { } void _getStreamData() { - final currentStreamFilter = _streamFilter; _streamData = []; - PostgresChangeFilter? realtimeFilter; - if (currentStreamFilter != null) { - realtimeFilter = PostgresChangeFilter( - type: currentStreamFilter.type, - column: currentStreamFilter.column, - value: currentStreamFilter.value, - ); - } + List realtimeFilter = _streamFilter + .map( + (filter) => PostgresChangeFilter( + column: filter.column, + type: filter.type, + value: filter.value, + ), + ) + .toList(); _channel = _realtimeClient.channel( _realtimeTopic, @@ -170,7 +169,7 @@ class SupabaseStreamBuilder extends Stream { event: PostgresChangeEvent.all, schema: _schema, table: _table, - filter: realtimeFilter, + filters: realtimeFilter, callback: (payload) { switch (payload.eventType) { case PostgresChangeEvent.insert: @@ -226,59 +225,59 @@ class SupabaseStreamBuilder extends Stream { Future _getPostgrestData() async { PostgrestFilterBuilder query = _queryBuilder.select(); - if (_streamFilter != null) { - query = switch (_streamFilter!.type) { + for (final filter in _streamFilter) { + query = switch (filter.type) { PostgresChangeFilterType.eq => query.eq( - _streamFilter!.column, - _streamFilter!.value, + filter.column, + filter.value, ), PostgresChangeFilterType.neq => query.neq( - _streamFilter!.column, - _streamFilter!.value, + filter.column, + filter.value, ), PostgresChangeFilterType.lt => query.lt( - _streamFilter!.column, - _streamFilter!.value, + filter.column, + filter.value, ), PostgresChangeFilterType.lte => query.lte( - _streamFilter!.column, - _streamFilter!.value, + filter.column, + filter.value, ), PostgresChangeFilterType.gt => query.gt( - _streamFilter!.column, - _streamFilter!.value, + filter.column, + filter.value, ), PostgresChangeFilterType.gte => query.gte( - _streamFilter!.column, - _streamFilter!.value, + filter.column, + filter.value, ), PostgresChangeFilterType.inFilter => query.inFilter( - _streamFilter!.column, - _streamFilter!.value, + filter.column, + filter.value, ), PostgresChangeFilterType.like => query.like( - _streamFilter!.column, - _streamFilter!.value, + filter.column, + filter.value, ), PostgresChangeFilterType.ilike => query.ilike( - _streamFilter!.column, - _streamFilter!.value, + filter.column, + filter.value, ), PostgresChangeFilterType.match => query.matchRegex( - _streamFilter!.column, - _streamFilter!.value, + filter.column, + filter.value, ), PostgresChangeFilterType.imatch => query.imatchRegex( - _streamFilter!.column, - _streamFilter!.value, + filter.column, + filter.value, ), PostgresChangeFilterType.isFilter => query.isFilter( - _streamFilter!.column, - _streamFilter!.value, + filter.column, + filter.value, ), PostgresChangeFilterType.isDistinct => query.isDistinct( - _streamFilter!.column, - _streamFilter!.value, + filter.column, + filter.value, ), }; } diff --git a/packages/supabase/lib/src/supabase_stream_filter_builder.dart b/packages/supabase/lib/src/supabase_stream_filter_builder.dart index 559f5b328..52c3406b7 100644 --- a/packages/supabase/lib/src/supabase_stream_filter_builder.dart +++ b/packages/supabase/lib/src/supabase_stream_filter_builder.dart @@ -13,210 +13,184 @@ class SupabaseStreamFilterBuilder extends SupabaseStreamBuilder { /// Filters the results where [column] equals [value]. /// - /// Only one filter can be applied to `.stream()`. - /// /// ```dart /// supabase.from('users').stream(primaryKey: ['id']).eq('name', 'Supabase'); /// ``` - SupabaseStreamBuilder eq(String column, Object value) { - _streamFilter = ( + SupabaseStreamFilterBuilder eq(String column, Object value) { + _streamFilter.add(( type: PostgresChangeFilterType.eq, column: column, value: value, - ); + )); return this; } /// Filters the results where [column] does not equal [value]. /// - /// Only one filter can be applied to `.stream()`. - /// /// ```dart /// supabase.from('users').stream(primaryKey: ['id']).neq('name', 'Supabase'); /// ``` - SupabaseStreamBuilder neq(String column, Object value) { - _streamFilter = ( + SupabaseStreamFilterBuilder neq(String column, Object value) { + _streamFilter.add(( type: PostgresChangeFilterType.neq, column: column, value: value, - ); + )); return this; } /// Filters the results where [column] is less than [value]. /// - /// Only one filter can be applied to `.stream()`. - /// /// ```dart /// supabase.from('users').stream(primaryKey: ['id']).lt('likes', 100); /// ``` - SupabaseStreamBuilder lt(String column, Object value) { - _streamFilter = ( + SupabaseStreamFilterBuilder lt(String column, Object value) { + _streamFilter.add(( type: PostgresChangeFilterType.lt, column: column, value: value, - ); + )); return this; } /// Filters the results where [column] is less than or equal to [value]. /// - /// Only one filter can be applied to `.stream()`. - /// /// ```dart /// supabase.from('users').stream(primaryKey: ['id']).lte('likes', 100); /// ``` - SupabaseStreamBuilder lte(String column, Object value) { - _streamFilter = ( + SupabaseStreamFilterBuilder lte(String column, Object value) { + _streamFilter.add(( type: PostgresChangeFilterType.lte, column: column, value: value, - ); + )); return this; } /// Filters the results where [column] is greater than [value]. /// - /// Only one filter can be applied to `.stream()`. - /// /// ```dart /// supabase.from('users').stream(primaryKey: ['id']).gt('likes', '100'); /// ``` - SupabaseStreamBuilder gt(String column, Object value) { - _streamFilter = ( + SupabaseStreamFilterBuilder gt(String column, Object value) { + _streamFilter.add(( type: PostgresChangeFilterType.gt, column: column, value: value, - ); + )); return this; } /// Filters the results where [column] is greater than or equal to [value]. /// - /// Only one filter can be applied to `.stream()`. - /// /// ```dart /// supabase.from('users').stream(primaryKey: ['id']).gte('likes', 100); /// ``` - SupabaseStreamBuilder gte(String column, Object value) { - _streamFilter = ( + SupabaseStreamFilterBuilder gte(String column, Object value) { + _streamFilter.add(( type: PostgresChangeFilterType.gte, column: column, value: value, - ); + )); return this; } /// Filters the results where [column] is included in [values]. /// - /// Only one filter can be applied to `.stream()`. - /// /// ```dart /// supabase.from('users').stream(primaryKey: ['id']).inFilter('name', ['Andy', 'Amy', 'Terry']); /// ``` - SupabaseStreamBuilder inFilter(String column, List values) { - _streamFilter = ( + SupabaseStreamFilterBuilder inFilter(String column, List values) { + _streamFilter.add(( type: PostgresChangeFilterType.inFilter, column: column, value: values, - ); + )); return this; } /// Filters the results where [column] matches the [pattern] case-sensitive. /// - /// Only one filter can be applied to `.stream()`. - /// /// ```dart /// supabase.from('users').stream(primaryKey: ['id']).like('title', '%foo%'); /// ``` - SupabaseStreamBuilder like(String column, String pattern) { - _streamFilter = ( + SupabaseStreamFilterBuilder like(String column, String pattern) { + _streamFilter.add(( type: PostgresChangeFilterType.like, column: column, value: pattern, - ); + )); return this; } /// Filters the results where [column] matches the [pattern] case-insensitive. /// - /// Only one filter can be applied to `.stream()`. - /// /// ```dart /// supabase.from('users').stream(primaryKey: ['id']).ilike('title', '%foo%'); /// ``` - SupabaseStreamBuilder ilike(String column, String pattern) { - _streamFilter = ( + SupabaseStreamFilterBuilder ilike(String column, String pattern) { + _streamFilter.add(( type: PostgresChangeFilterType.ilike, column: column, value: pattern, - ); + )); return this; } /// Filters the results where [column] matches the POSIX [regex] case-sensitive. /// - /// Only one filter can be applied to `.stream()`. - /// /// ```dart /// supabase.from('users').stream(primaryKey: ['id']).match('slug', r'^post-\d+$'); /// ``` - SupabaseStreamBuilder match(String column, String regex) { - _streamFilter = ( + SupabaseStreamFilterBuilder match(String column, String regex) { + _streamFilter.add(( type: PostgresChangeFilterType.match, column: column, value: regex, - ); + )); return this; } /// Filters the results where [column] matches the POSIX [regex] case-insensitive. /// - /// Only one filter can be applied to `.stream()`. - /// /// ```dart /// supabase.from('users').stream(primaryKey: ['id']).imatch('slug', r'^post-\d+$'); /// ``` - SupabaseStreamBuilder imatch(String column, String regex) { - _streamFilter = ( + SupabaseStreamFilterBuilder imatch(String column, String regex) { + _streamFilter.add(( type: PostgresChangeFilterType.imatch, column: column, value: regex, - ); + )); return this; } /// Filters the results where [column] is `null`, `true` or `false`. /// - /// Only one filter can be applied to `.stream()`. - /// /// ```dart /// supabase.from('users').stream(primaryKey: ['id']).isFilter('data', null); /// ``` - SupabaseStreamBuilder isFilter(String column, bool? value) { - _streamFilter = ( + SupabaseStreamFilterBuilder isFilter(String column, bool? value) { + _streamFilter.add(( type: PostgresChangeFilterType.isFilter, column: column, value: value, - ); + )); return this; } /// Filters the results where [column] is not equal to [value] treating `null /// as a distinct value. /// - /// Only one filter can be applied to `.stream()`. - /// /// ```dart /// supabase.from('users').stream(primaryKey: ['id']).isDistinct('age', null); /// ``` - SupabaseStreamBuilder isDistinct(String column, Object? value) { - _streamFilter = ( + SupabaseStreamFilterBuilder isDistinct(String column, Object? value) { + _streamFilter.add(( type: PostgresChangeFilterType.isDistinct, column: column, value: value, - ); + )); return this; } } From 3302d8364b99a565be07eac104650e94c5a376be Mon Sep 17 00:00:00 2001 From: Vinzent Date: Fri, 17 Jul 2026 00:14:54 +0200 Subject: [PATCH 4/4] style: add missing final --- packages/supabase/lib/src/supabase_stream_builder.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/supabase/lib/src/supabase_stream_builder.dart b/packages/supabase/lib/src/supabase_stream_builder.dart index 0d3acdc83..b5f5724d3 100644 --- a/packages/supabase/lib/src/supabase_stream_builder.dart +++ b/packages/supabase/lib/src/supabase_stream_builder.dart @@ -59,7 +59,7 @@ class SupabaseStreamBuilder extends Stream { SupabaseStreamEvent _streamData = []; /// Filters to be applied to the stream - List<_StreamPostgrestFilter> _streamFilter = []; + final List<_StreamPostgrestFilter> _streamFilter = []; /// Which column to order by and whether it's ascending _Order? _orderBy;