diff --git a/Sources/PostgREST/PostgrestFilterBuilder.swift b/Sources/PostgREST/PostgrestFilterBuilder.swift index 02e50df82..374843d7d 100644 --- a/Sources/PostgREST/PostgrestFilterBuilder.swift +++ b/Sources/PostgREST/PostgrestFilterBuilder.swift @@ -2,8 +2,8 @@ import Foundation public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Sendable { public enum Operator: String, CaseIterable, Sendable { - case eq, neq, gt, gte, lt, lte, like, ilike, `is`, `in`, cs, cd, sl, sr, nxl, nxr, adj, ov, fts, - plfts, phfts, wfts + case eq, neq, gt, gte, lt, lte, like, ilike, match, imatch, `is`, isdistinct, `in`, cs, cd, sl, + sr, nxl, nxr, adj, ov, fts, plfts, phfts, wfts } // MARK: - Filters @@ -228,6 +228,38 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda return self } + /// Match only rows where `column` matches the regex `pattern` case-sensitively. + /// + /// - Parameters: + /// - column: The column to filter on + /// - pattern: The regex pattern to match with + public func match( + _ column: String, + pattern: any PostgrestFilterValue + ) -> PostgrestFilterBuilder { + let queryValue = pattern.rawValue + mutableState.withValue { + $0.request.query.append(URLQueryItem(name: column, value: "match.\(queryValue)")) + } + return self + } + + /// Match only rows where `column` matches the regex `pattern` case-insensitively. + /// + /// - Parameters: + /// - column: The column to filter on + /// - pattern: The regex pattern to match with + public func imatch( + _ column: String, + pattern: any PostgrestFilterValue + ) -> PostgrestFilterBuilder { + let queryValue = pattern.rawValue + mutableState.withValue { + $0.request.query.append(URLQueryItem(name: column, value: "imatch.\(queryValue)")) + } + return self + } + /// Match only rows where `column` IS `value`. /// /// 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 return self } + /// Match only rows where `column` IS DISTINCT FROM `value`. + /// + /// Unlike `.neq()`, this treats NULL as a comparable value. NULL IS DISTINCT FROM NULL is false, while NULL != NULL is true. + /// + /// - Parameters: + /// - column: The column to filter on + /// - value: The value to filter with + public func isDistinct( + _ column: String, + value: any PostgrestFilterValue + ) -> PostgrestFilterBuilder { + let queryValue = value.rawValue + mutableState.withValue { + $0.request.query.append(URLQueryItem(name: column, value: "isdistinct.\(queryValue)")) + } + return self + } + /// Match only rows where `column` is included in the `values` array. /// /// - Parameters: @@ -575,7 +625,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda query: String, config: String? = nil ) -> PostgrestFilterBuilder { - plfts(column, query: query, config: config) + textSearch(column, query: query, config: config, type: .plain) } public func phraseToFullTextSearch( @@ -583,7 +633,7 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda query: String, config: String? = nil ) -> PostgrestFilterBuilder { - phfts(column, query: query, config: config) + textSearch(column, query: query, config: config, type: .phrase) } public func webFullTextSearch( @@ -591,6 +641,6 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder, @unchecked Senda query: String, config: String? = nil ) -> PostgrestFilterBuilder { - wfts(column, query: query, config: config) + textSearch(column, query: query, config: config, type: .websearch) } } diff --git a/Tests/PostgRESTTests/PostgrestFilterBuilderTests.swift b/Tests/PostgRESTTests/PostgrestFilterBuilderTests.swift index e60d11787..3ef740c31 100644 --- a/Tests/PostgRESTTests/PostgrestFilterBuilderTests.swift +++ b/Tests/PostgRESTTests/PostgrestFilterBuilderTests.swift @@ -669,4 +669,85 @@ final class PostgrestFilterBuilderTests: PostgrestQueryTests { .fts("description", query: "programmer") .execute() } + + func testRegexMatchFilter() async throws { + Mock( + url: url.appendingPathComponent("users"), + ignoreQuery: true, + statusCode: 200, + data: [.get: Data("[]".utf8)] + ) + .snapshotRequest { + #""" + curl \ + --header "Accept: application/json" \ + --header "Content-Type: application/json" \ + --header "X-Client-Info: postgrest-swift/0.0.0" \ + --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \ + "http://localhost:54321/rest/v1/users?email=match.%5E.+@.+%5C..+$&select=*" + """# + } + .register() + + _ = + try await sut + .from("users") + .select() + .match("email", pattern: "^.+@.+\\..+$") + .execute() + } + + func testRegexImatchFilter() async throws { + Mock( + url: url.appendingPathComponent("users"), + ignoreQuery: true, + statusCode: 200, + data: [.get: Data("[]".utf8)] + ) + .snapshotRequest { + #""" + curl \ + --header "Accept: application/json" \ + --header "Content-Type: application/json" \ + --header "X-Client-Info: postgrest-swift/0.0.0" \ + --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \ + "http://localhost:54321/rest/v1/users?name=imatch.%5Ejohn&select=*" + """# + } + .register() + + _ = + try await sut + .from("users") + .select() + .imatch("name", pattern: "^john") + .execute() + } + + func testIsDistinctFilter() async throws { + Mock( + url: url.appendingPathComponent("users"), + ignoreQuery: true, + statusCode: 200, + data: [.get: Data("[]".utf8)] + ) + .snapshotRequest { + #""" + curl \ + --header "Accept: application/json" \ + --header "Content-Type: application/json" \ + --header "X-Client-Info: postgrest-swift/0.0.0" \ + --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \ + "http://localhost:54321/rest/v1/users?select=*&status=isdistinct.null" + """# + } + .register() + + _ = + try await sut + .from("users") + .select() + .isDistinct("status", value: "null") + .execute() + } } diff --git a/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.test-all-filters-and-count.txt b/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.test-all-filters-and-count.txt index ab0647bac..ea7a8aee8 100644 --- a/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.test-all-filters-and-count.txt +++ b/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.test-all-filters-and-count.txt @@ -2,4 +2,4 @@ curl \ --header "Accept: application/json" \ --header "Content-Type: application/json" \ --header "X-Client-Info: postgrest-swift/x.y.z" \ - "https://example.supabase.co/todos?column=eq.Some%20value&column=neq.Some%20value&column=gt.Some%20value&column=gte.Some%20value&column=lt.Some%20value&column=lte.Some%20value&column=like.Some%20value&column=ilike.Some%20value&column=is.Some%20value&column=in.Some%20value&column=cs.Some%20value&column=cd.Some%20value&column=sl.Some%20value&column=sr.Some%20value&column=nxl.Some%20value&column=nxr.Some%20value&column=adj.Some%20value&column=ov.Some%20value&column=fts.Some%20value&column=plfts.Some%20value&column=phfts.Some%20value&column=wfts.Some%20value&select=*" \ No newline at end of file + "https://example.supabase.co/todos?column=eq.Some%20value&column=neq.Some%20value&column=gt.Some%20value&column=gte.Some%20value&column=lt.Some%20value&column=lte.Some%20value&column=like.Some%20value&column=ilike.Some%20value&column=match.Some%20value&column=imatch.Some%20value&column=is.Some%20value&column=isdistinct.Some%20value&column=in.Some%20value&column=cs.Some%20value&column=cd.Some%20value&column=sl.Some%20value&column=sr.Some%20value&column=nxl.Some%20value&column=nxr.Some%20value&column=adj.Some%20value&column=ov.Some%20value&column=fts.Some%20value&column=plfts.Some%20value&column=phfts.Some%20value&column=wfts.Some%20value&select=*" \ No newline at end of file