Skip to content

Commit ae2d12d

Browse files
grdsdevclaude
andauthored
feat(postgrest): Implement maxAffected method (#1226)
* 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]> * style: dart format --------- Co-authored-by: Claude <[email protected]>
1 parent 3237352 commit ae2d12d

File tree

2 files changed

+167
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)