Skip to content

Commit 8610dde

Browse files
grdsdevclaude
andcommitted
feat(postgrest): add missing operators match, imatch, and isDistinct
Implements missing PostgREST v12 operators to bring the Swift SDK up to date with the official PostgREST API specification. New operators: - match(): Regex pattern matching (case-sensitive) using ~ operator - imatch(): Regex pattern matching (case-insensitive) using ~* operator - isDistinct(): IS DISTINCT FROM operator for NULL-aware comparison Also fixes semantic helper methods plainToFullTextSearch(), phraseToFullTextSearch(), and webFullTextSearch() to call textSearch() directly with the appropriate type parameter. Includes comprehensive tests for all new operators. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent d35111b commit 8610dde

File tree

2 files changed

+136
-5
lines changed

2 files changed

+136
-5
lines changed

Sources/PostgREST/PostgrestFilterBuilder.swift

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import Foundation
22

33
public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Sendable {
44
public enum Operator: String, CaseIterable, Sendable {
5-
case eq, neq, gt, gte, lt, lte, like, ilike, `is`, `in`, cs, cd, sl, sr, nxl, nxr, adj, ov, fts,
6-
plfts, phfts, wfts
5+
case eq, neq, gt, gte, lt, lte, like, ilike, match, imatch, `is`, isdistinct, `in`, cs, cd, sl,
6+
sr, nxl, nxr, adj, ov, fts, plfts, phfts, wfts
77
}
88

99
// MARK: - Filters
@@ -228,6 +228,38 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda
228228
return self
229229
}
230230

231+
/// Match only rows where `column` matches the regex `pattern` case-sensitively.
232+
///
233+
/// - Parameters:
234+
/// - column: The column to filter on
235+
/// - pattern: The regex pattern to match with
236+
public func match(
237+
_ column: String,
238+
pattern: any PostgrestFilterValue
239+
) -> PostgrestFilterBuilder {
240+
let queryValue = pattern.rawValue
241+
mutableState.withValue {
242+
$0.request.query.append(URLQueryItem(name: column, value: "match.\(queryValue)"))
243+
}
244+
return self
245+
}
246+
247+
/// Match only rows where `column` matches the regex `pattern` case-insensitively.
248+
///
249+
/// - Parameters:
250+
/// - column: The column to filter on
251+
/// - pattern: The regex pattern to match with
252+
public func imatch(
253+
_ column: String,
254+
pattern: any PostgrestFilterValue
255+
) -> PostgrestFilterBuilder {
256+
let queryValue = pattern.rawValue
257+
mutableState.withValue {
258+
$0.request.query.append(URLQueryItem(name: column, value: "imatch.\(queryValue)"))
259+
}
260+
return self
261+
}
262+
231263
/// Match only rows where `column` IS `value`.
232264
///
233265
/// For non-boolean columns, this is only relevant for checking if the value of `column` is NULL by setting `value` to `null`.
@@ -247,6 +279,24 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda
247279
return self
248280
}
249281

282+
/// Match only rows where `column` IS DISTINCT FROM `value`.
283+
///
284+
/// Unlike `.neq()`, this treats NULL as a comparable value. NULL IS DISTINCT FROM NULL is false, while NULL != NULL is true.
285+
///
286+
/// - Parameters:
287+
/// - column: The column to filter on
288+
/// - value: The value to filter with
289+
public func isDistinct(
290+
_ column: String,
291+
value: any PostgrestFilterValue
292+
) -> PostgrestFilterBuilder {
293+
let queryValue = value.rawValue
294+
mutableState.withValue {
295+
$0.request.query.append(URLQueryItem(name: column, value: "isdistinct.\(queryValue)"))
296+
}
297+
return self
298+
}
299+
250300
/// Match only rows where `column` is included in the `values` array.
251301
///
252302
/// - Parameters:
@@ -575,22 +625,22 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda
575625
query: String,
576626
config: String? = nil
577627
) -> PostgrestFilterBuilder {
578-
plfts(column, query: query, config: config)
628+
textSearch(column, query: query, config: config, type: .plain)
579629
}
580630

581631
public func phraseToFullTextSearch(
582632
_ column: String,
583633
query: String,
584634
config: String? = nil
585635
) -> PostgrestFilterBuilder {
586-
phfts(column, query: query, config: config)
636+
textSearch(column, query: query, config: config, type: .phrase)
587637
}
588638

589639
public func webFullTextSearch(
590640
_ column: String,
591641
query: String,
592642
config: String? = nil
593643
) -> PostgrestFilterBuilder {
594-
wfts(column, query: query, config: config)
644+
textSearch(column, query: query, config: config, type: .websearch)
595645
}
596646
}

Tests/PostgRESTTests/PostgrestFilterBuilderTests.swift

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,4 +669,85 @@ final class PostgrestFilterBuilderTests: PostgrestQueryTests {
669669
.fts("description", query: "programmer")
670670
.execute()
671671
}
672+
673+
func testRegexMatchFilter() async throws {
674+
Mock(
675+
url: url.appendingPathComponent("users"),
676+
ignoreQuery: true,
677+
statusCode: 200,
678+
data: [.get: Data("[]".utf8)]
679+
)
680+
.snapshotRequest {
681+
#"""
682+
curl \
683+
--header "Accept: application/json" \
684+
--header "Content-Type: application/json" \
685+
--header "X-Client-Info: postgrest-swift/0.0.0" \
686+
--header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
687+
"http://localhost:54321/rest/v1/users?email=match.%5E.+@.+%5C..+$&select=*"
688+
"""#
689+
}
690+
.register()
691+
692+
_ =
693+
try await sut
694+
.from("users")
695+
.select()
696+
.match("email", pattern: "^.+@.+\\..+$")
697+
.execute()
698+
}
699+
700+
func testRegexImatchFilter() async throws {
701+
Mock(
702+
url: url.appendingPathComponent("users"),
703+
ignoreQuery: true,
704+
statusCode: 200,
705+
data: [.get: Data("[]".utf8)]
706+
)
707+
.snapshotRequest {
708+
#"""
709+
curl \
710+
--header "Accept: application/json" \
711+
--header "Content-Type: application/json" \
712+
--header "X-Client-Info: postgrest-swift/0.0.0" \
713+
--header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
714+
"http://localhost:54321/rest/v1/users?name=imatch.%5Ejohn&select=*"
715+
"""#
716+
}
717+
.register()
718+
719+
_ =
720+
try await sut
721+
.from("users")
722+
.select()
723+
.imatch("name", pattern: "^john")
724+
.execute()
725+
}
726+
727+
func testIsDistinctFilter() async throws {
728+
Mock(
729+
url: url.appendingPathComponent("users"),
730+
ignoreQuery: true,
731+
statusCode: 200,
732+
data: [.get: Data("[]".utf8)]
733+
)
734+
.snapshotRequest {
735+
#"""
736+
curl \
737+
--header "Accept: application/json" \
738+
--header "Content-Type: application/json" \
739+
--header "X-Client-Info: postgrest-swift/0.0.0" \
740+
--header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \
741+
"http://localhost:54321/rest/v1/users?select=*&status=isdistinct.null"
742+
"""#
743+
}
744+
.register()
745+
746+
_ =
747+
try await sut
748+
.from("users")
749+
.select()
750+
.isDistinct("status", value: "null")
751+
.execute()
752+
}
672753
}

0 commit comments

Comments
 (0)