Skip to content

Commit 6f1f469

Browse files
grdsdevclaude
andcommitted
feat(postgrest): implement maxAffected method
Add maxAffected method to PostgrestTransformBuilder that sets the maximum number of rows that can be affected by update and delete operations. - Add maxAffected method to PostgrestTransformBuilder class - Set handling=strict and max-affected={value} in Prefer header - Preserve existing Prefer headers when adding maxAffected - Support method chaining with other transform methods - Add comprehensive unit and integration tests - Documentation notes PostgREST v13+ requirement 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 3237352 commit 6f1f469

File tree

2 files changed

+168
-0
lines changed

2 files changed

+168
-0
lines changed

packages/postgrest/lib/src/postgrest_transform_builder.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,38 @@ class PostgrestTransformBuilder<T> extends RawPostgrestBuilder<T, T, T> {
246246
return ResponsePostgrestBuilder(_copyWithType(headers: newHeaders));
247247
}
248248

249+
/// Sets the maximum number of rows that can be affected by the query.
250+
///
251+
/// Only available with PATCH and DELETE operations. Requires PostgREST v13 or higher.
252+
/// When the limit is exceeded, the query will fail with an error.
253+
///
254+
/// ```dart
255+
/// supabase.from('users').update({'active': false}).eq('status', 'inactive').maxAffected(5);
256+
/// ```
257+
///
258+
/// ```dart
259+
/// supabase.from('users').delete().eq('active', false).maxAffected(10);
260+
/// ```
261+
PostgrestTransformBuilder<T> maxAffected(int value) {
262+
final newHeaders = {..._headers};
263+
264+
// Add handling=strict and max-affected headers
265+
if (newHeaders['Prefer'] != null) {
266+
var preferHeader = newHeaders['Prefer']!;
267+
if (!preferHeader.contains('handling=strict')) {
268+
preferHeader += ',handling=strict';
269+
}
270+
if (!preferHeader.contains('max-affected=')) {
271+
preferHeader += ',max-affected=$value';
272+
}
273+
newHeaders['Prefer'] = preferHeader;
274+
} else {
275+
newHeaders['Prefer'] = 'handling=strict,max-affected=$value';
276+
}
277+
278+
return PostgrestTransformBuilder(_copyWith(headers: newHeaders));
279+
}
280+
249281
/// Obtains the EXPLAIN plan for this request.
250282
///
251283
/// Before using this method, you need to enable `explain()` on your

packages/postgrest/test/transforms_test.dart

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
22
import 'package:postgrest/postgrest.dart';
33
import 'package:test/test.dart';
44

5+
import 'custom_http_client.dart';
56
import 'reset_helper.dart';
67

78
void main() {
@@ -343,4 +344,139 @@ void main() {
343344
expect(res, isNotNull);
344345
expect(res['type'], 'FeatureCollection');
345346
});
347+
348+
group('maxAffected', () {
349+
test('maxAffected method can be called on update operations', () {
350+
expect(
351+
() => postgrest
352+
.from('users')
353+
.update({'status': 'INACTIVE'})
354+
.eq('id', 1)
355+
.maxAffected(1),
356+
returnsNormally,
357+
);
358+
});
359+
360+
test('maxAffected method can be called on delete operations', () {
361+
expect(
362+
() => postgrest
363+
.from('channels')
364+
.delete()
365+
.eq('id', 999)
366+
.maxAffected(5),
367+
returnsNormally,
368+
);
369+
});
370+
371+
test('maxAffected method can be called on select operations', () {
372+
expect(
373+
() => postgrest.from('users').select().maxAffected(1),
374+
returnsNormally,
375+
);
376+
});
377+
378+
test('maxAffected method can be called on insert operations', () {
379+
expect(
380+
() => postgrest
381+
.from('users')
382+
.insert({'username': 'test'})
383+
.maxAffected(1),
384+
returnsNormally,
385+
);
386+
});
387+
388+
test('maxAffected method can be chained with select', () {
389+
expect(
390+
() => postgrest
391+
.from('users')
392+
.update({'status': 'INACTIVE'})
393+
.eq('id', 1)
394+
.maxAffected(1)
395+
.select(),
396+
returnsNormally,
397+
);
398+
});
399+
});
400+
401+
group('maxAffected integration', () {
402+
late CustomHttpClient customHttpClient;
403+
late PostgrestClient postgrestCustomHttpClient;
404+
405+
setUp(() {
406+
customHttpClient = CustomHttpClient();
407+
postgrestCustomHttpClient = PostgrestClient(
408+
rootUrl,
409+
httpClient: customHttpClient,
410+
);
411+
});
412+
413+
test('maxAffected sets correct headers for update', () async {
414+
try {
415+
await postgrestCustomHttpClient
416+
.from('users')
417+
.update({'status': 'INACTIVE'})
418+
.eq('id', 1)
419+
.maxAffected(5);
420+
} catch (_) {
421+
// Expected to fail with custom client, we just want to check headers
422+
}
423+
424+
expect(customHttpClient.lastRequest, isNotNull);
425+
expect(customHttpClient.lastRequest!.headers['Prefer'], isNotNull);
426+
expect(customHttpClient.lastRequest!.headers['Prefer'], contains('handling=strict'));
427+
expect(customHttpClient.lastRequest!.headers['Prefer'], contains('max-affected=5'));
428+
});
429+
430+
test('maxAffected sets correct headers for delete', () async {
431+
try {
432+
await postgrestCustomHttpClient
433+
.from('users')
434+
.delete()
435+
.eq('id', 1)
436+
.maxAffected(10);
437+
} catch (_) {
438+
// Expected to fail with custom client, we just want to check headers
439+
}
440+
441+
expect(customHttpClient.lastRequest, isNotNull);
442+
expect(customHttpClient.lastRequest!.headers['Prefer'], isNotNull);
443+
expect(customHttpClient.lastRequest!.headers['Prefer'], contains('handling=strict'));
444+
expect(customHttpClient.lastRequest!.headers['Prefer'], contains('max-affected=10'));
445+
});
446+
447+
test('maxAffected preserves existing Prefer headers', () async {
448+
try {
449+
await postgrestCustomHttpClient
450+
.from('users')
451+
.update({'status': 'INACTIVE'})
452+
.eq('id', 1)
453+
.select()
454+
.maxAffected(3);
455+
} catch (_) {
456+
// Expected to fail with custom client, we just want to check headers
457+
}
458+
459+
expect(customHttpClient.lastRequest, isNotNull);
460+
final preferHeader = customHttpClient.lastRequest!.headers['Prefer']!;
461+
expect(preferHeader, contains('return=representation'));
462+
expect(preferHeader, contains('handling=strict'));
463+
expect(preferHeader, contains('max-affected=3'));
464+
});
465+
466+
test('maxAffected works with select operations (sets headers but likely ineffective)', () async {
467+
try {
468+
await postgrestCustomHttpClient
469+
.from('users')
470+
.select()
471+
.maxAffected(2);
472+
} catch (_) {
473+
// Expected to fail with custom client, we just want to check headers
474+
}
475+
476+
expect(customHttpClient.lastRequest, isNotNull);
477+
expect(customHttpClient.lastRequest!.headers['Prefer'], isNotNull);
478+
expect(customHttpClient.lastRequest!.headers['Prefer'], contains('handling=strict'));
479+
expect(customHttpClient.lastRequest!.headers['Prefer'], contains('max-affected=2'));
480+
});
481+
});
346482
}

0 commit comments

Comments
 (0)