diff --git a/packages/postgrest/lib/src/postgrest_transform_builder.dart b/packages/postgrest/lib/src/postgrest_transform_builder.dart index 504e083da..36530a16f 100644 --- a/packages/postgrest/lib/src/postgrest_transform_builder.dart +++ b/packages/postgrest/lib/src/postgrest_transform_builder.dart @@ -246,6 +246,38 @@ class PostgrestTransformBuilder extends RawPostgrestBuilder { return ResponsePostgrestBuilder(_copyWithType(headers: newHeaders)); } + /// Sets the maximum number of rows that can be affected by the query. + /// + /// Only available with PATCH and DELETE operations. Requires PostgREST v13 or higher. + /// When the limit is exceeded, the query will fail with an error. + /// + /// ```dart + /// supabase.from('users').update({'active': false}).eq('status', 'inactive').maxAffected(5); + /// ``` + /// + /// ```dart + /// supabase.from('users').delete().eq('active', false).maxAffected(10); + /// ``` + PostgrestTransformBuilder maxAffected(int value) { + final newHeaders = {..._headers}; + + // Add handling=strict and max-affected headers + if (newHeaders['Prefer'] != null) { + var preferHeader = newHeaders['Prefer']!; + if (!preferHeader.contains('handling=strict')) { + preferHeader += ',handling=strict'; + } + if (!preferHeader.contains('max-affected=')) { + preferHeader += ',max-affected=$value'; + } + newHeaders['Prefer'] = preferHeader; + } else { + newHeaders['Prefer'] = 'handling=strict,max-affected=$value'; + } + + return PostgrestTransformBuilder(_copyWith(headers: newHeaders)); + } + /// Obtains the EXPLAIN plan for this request. /// /// Before using this method, you need to enable `explain()` on your diff --git a/packages/postgrest/test/transforms_test.dart b/packages/postgrest/test/transforms_test.dart index 486923dfc..58b93ce66 100644 --- a/packages/postgrest/test/transforms_test.dart +++ b/packages/postgrest/test/transforms_test.dart @@ -2,6 +2,7 @@ import 'package:collection/collection.dart'; import 'package:postgrest/postgrest.dart'; import 'package:test/test.dart'; +import 'custom_http_client.dart'; import 'reset_helper.dart'; void main() { @@ -343,4 +344,138 @@ void main() { expect(res, isNotNull); expect(res['type'], 'FeatureCollection'); }); + + group('maxAffected', () { + test('maxAffected method can be called on update operations', () { + expect( + () => postgrest + .from('users') + .update({'status': 'INACTIVE'}) + .eq('id', 1) + .maxAffected(1), + returnsNormally, + ); + }); + + test('maxAffected method can be called on delete operations', () { + expect( + () => postgrest.from('channels').delete().eq('id', 999).maxAffected(5), + returnsNormally, + ); + }); + + test('maxAffected method can be called on select operations', () { + expect( + () => postgrest.from('users').select().maxAffected(1), + returnsNormally, + ); + }); + + test('maxAffected method can be called on insert operations', () { + expect( + () => + postgrest.from('users').insert({'username': 'test'}).maxAffected(1), + returnsNormally, + ); + }); + + test('maxAffected method can be chained with select', () { + expect( + () => postgrest + .from('users') + .update({'status': 'INACTIVE'}) + .eq('id', 1) + .maxAffected(1) + .select(), + returnsNormally, + ); + }); + }); + + group('maxAffected integration', () { + late CustomHttpClient customHttpClient; + late PostgrestClient postgrestCustomHttpClient; + + setUp(() { + customHttpClient = CustomHttpClient(); + postgrestCustomHttpClient = PostgrestClient( + rootUrl, + httpClient: customHttpClient, + ); + }); + + test('maxAffected sets correct headers for update', () async { + try { + await postgrestCustomHttpClient + .from('users') + .update({'status': 'INACTIVE'}) + .eq('id', 1) + .maxAffected(5); + } catch (_) { + // Expected to fail with custom client, we just want to check headers + } + + expect(customHttpClient.lastRequest, isNotNull); + expect(customHttpClient.lastRequest!.headers['Prefer'], isNotNull); + expect(customHttpClient.lastRequest!.headers['Prefer'], + contains('handling=strict')); + expect(customHttpClient.lastRequest!.headers['Prefer'], + contains('max-affected=5')); + }); + + test('maxAffected sets correct headers for delete', () async { + try { + await postgrestCustomHttpClient + .from('users') + .delete() + .eq('id', 1) + .maxAffected(10); + } catch (_) { + // Expected to fail with custom client, we just want to check headers + } + + expect(customHttpClient.lastRequest, isNotNull); + expect(customHttpClient.lastRequest!.headers['Prefer'], isNotNull); + expect(customHttpClient.lastRequest!.headers['Prefer'], + contains('handling=strict')); + expect(customHttpClient.lastRequest!.headers['Prefer'], + contains('max-affected=10')); + }); + + test('maxAffected preserves existing Prefer headers', () async { + try { + await postgrestCustomHttpClient + .from('users') + .update({'status': 'INACTIVE'}) + .eq('id', 1) + .select() + .maxAffected(3); + } catch (_) { + // Expected to fail with custom client, we just want to check headers + } + + expect(customHttpClient.lastRequest, isNotNull); + final preferHeader = customHttpClient.lastRequest!.headers['Prefer']!; + expect(preferHeader, contains('return=representation')); + expect(preferHeader, contains('handling=strict')); + expect(preferHeader, contains('max-affected=3')); + }); + + test( + 'maxAffected works with select operations (sets headers but likely ineffective)', + () async { + try { + await postgrestCustomHttpClient.from('users').select().maxAffected(2); + } catch (_) { + // Expected to fail with custom client, we just want to check headers + } + + expect(customHttpClient.lastRequest, isNotNull); + expect(customHttpClient.lastRequest!.headers['Prefer'], isNotNull); + expect(customHttpClient.lastRequest!.headers['Prefer'], + contains('handling=strict')); + expect(customHttpClient.lastRequest!.headers['Prefer'], + contains('max-affected=2')); + }); + }); }