Skip to content

Commit 78fbd8a

Browse files
authored
feat(postgrest): implement maxAffected method for row limit enforcement (#795)
1 parent ef671bf commit 78fbd8a

File tree

2 files changed

+171
-0
lines changed

2 files changed

+171
-0
lines changed

Sources/PostgREST/PostgrestTransformBuilder.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,19 @@ public class PostgrestTransformBuilder: PostgrestBuilder, @unchecked Sendable {
169169

170170
return self
171171
}
172+
173+
/// Set the maximum number of rows that can be affected by the query.
174+
///
175+
/// Only available in PostgREST v13+ and only works with PATCH, DELETE methods and RPC calls.
176+
/// Note: This method doesn't validate the HTTP method - ensure you only use it with compatible operations.
177+
///
178+
/// - Parameters:
179+
/// - value: The maximum number of rows that can be affected
180+
public func maxAffected(_ value: Int) -> PostgrestTransformBuilder {
181+
mutableState.withValue {
182+
$0.request.headers.appendOrUpdate(.prefer, value: "handling=strict")
183+
$0.request.headers.appendOrUpdate(.prefer, value: "max-affected=\(value)")
184+
}
185+
return self
186+
}
172187
}

Tests/PostgRESTTests/PostgrestTransformBuilderTests.swift

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,4 +415,160 @@ final class PostgrestTransformBuilderTests: PostgrestQueryTests {
415415

416416
XCTAssertTrue(explain.contains("Aggregate"))
417417
}
418+
419+
func testMaxAffectedOnUpdate() async throws {
420+
Mock(
421+
url: url.appendingPathComponent("users"),
422+
ignoreQuery: true,
423+
statusCode: 200,
424+
data: [
425+
.patch: Data("[]".utf8)
426+
]
427+
)
428+
.snapshotRequest {
429+
#"""
430+
curl \
431+
--request PATCH \
432+
--header "Accept: application/json" \
433+
--header "Content-Length: 20" \
434+
--header "Content-Type: application/json" \
435+
--header "Prefer: return=representation,handling=strict,max-affected=1" \
436+
--header "X-Client-Info: postgrest-swift/0.0.0" \
437+
--header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
438+
--data "{\"username\":\"admin\"}" \
439+
"http://localhost:54321/rest/v1/users?id=eq.1"
440+
"""#
441+
}
442+
.register()
443+
444+
try await sut
445+
.from("users")
446+
.update(["username": "admin"])
447+
.eq("id", value: 1)
448+
.maxAffected(1)
449+
.execute()
450+
}
451+
452+
func testMaxAffectedTwice() async throws {
453+
Mock(
454+
url: url.appendingPathComponent("users"),
455+
ignoreQuery: true,
456+
statusCode: 200,
457+
data: [
458+
.patch: Data("[]".utf8)
459+
]
460+
)
461+
.snapshotRequest {
462+
#"""
463+
curl \
464+
--request PATCH \
465+
--header "Accept: application/json" \
466+
--header "Content-Length: 20" \
467+
--header "Content-Type: application/json" \
468+
--header "Prefer: return=representation,handling=strict,max-affected=5" \
469+
--header "X-Client-Info: postgrest-swift/0.0.0" \
470+
--header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
471+
--data "{\"username\":\"admin\"}" \
472+
"http://localhost:54321/rest/v1/users?id=eq.1"
473+
"""#
474+
}
475+
.register()
476+
477+
try await sut
478+
.from("users")
479+
.update(["username": "admin"])
480+
.eq("id", value: 1)
481+
.maxAffected(1)
482+
.maxAffected(5)
483+
.execute()
484+
}
485+
486+
func testMaxAffectedOnDelete() async throws {
487+
Mock(
488+
url: url.appendingPathComponent("users"),
489+
ignoreQuery: true,
490+
statusCode: 200,
491+
data: [
492+
.delete: Data("[]".utf8)
493+
]
494+
)
495+
.snapshotRequest {
496+
#"""
497+
curl \
498+
--request DELETE \
499+
--header "Accept: application/json" \
500+
--header "Content-Type: application/json" \
501+
--header "Prefer: return=representation,handling=strict,max-affected=5" \
502+
--header "X-Client-Info: postgrest-swift/0.0.0" \
503+
--header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
504+
"http://localhost:54321/rest/v1/users?id=in.(1,2,3,4,5)"
505+
"""#
506+
}
507+
.register()
508+
509+
try await sut
510+
.from("users")
511+
.delete()
512+
.in("id", values: [1, 2, 3, 4, 5])
513+
.maxAffected(5)
514+
.execute()
515+
}
516+
517+
func testMaxAffectedOnRpc() async throws {
518+
Mock(
519+
url: url.appendingPathComponent("rpc/delete_users"),
520+
ignoreQuery: true,
521+
statusCode: 200,
522+
data: [
523+
.post: Data("[]".utf8)
524+
]
525+
)
526+
.snapshotRequest {
527+
#"""
528+
curl \
529+
--request POST \
530+
--header "Accept: application/json" \
531+
--header "Content-Type: application/json" \
532+
--header "Prefer: handling=strict,max-affected=10" \
533+
--header "X-Client-Info: postgrest-swift/0.0.0" \
534+
--header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
535+
"http://localhost:54321/rest/v1/rpc/delete_users"
536+
"""#
537+
}
538+
.register()
539+
540+
try await sut
541+
.rpc("delete_users")
542+
.maxAffected(10)
543+
.execute()
544+
}
545+
546+
func testMaxAffectedOnSelect() async throws {
547+
Mock(
548+
url: url.appendingPathComponent("users"),
549+
ignoreQuery: true,
550+
statusCode: 200,
551+
data: [
552+
.get: Data("[]".utf8)
553+
]
554+
)
555+
.snapshotRequest {
556+
#"""
557+
curl \
558+
--header "Accept: application/json" \
559+
--header "Content-Type: application/json" \
560+
--header "Prefer: handling=strict,max-affected=3" \
561+
--header "X-Client-Info: postgrest-swift/0.0.0" \
562+
--header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
563+
"http://localhost:54321/rest/v1/users?select=*"
564+
"""#
565+
}
566+
.register()
567+
568+
try await sut
569+
.from("users")
570+
.select()
571+
.maxAffected(3)
572+
.execute()
573+
}
418574
}

0 commit comments

Comments
 (0)