Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 55 additions & 5 deletions Sources/PostgREST/PostgrestFilterBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`.
Expand All @@ -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:
Expand Down Expand Up @@ -575,22 +625,22 @@ 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(
_ column: String,
query: String,
config: String? = nil
) -> PostgrestFilterBuilder {
phfts(column, query: query, config: config)
textSearch(column, query: query, config: config, type: .phrase)
}

public func webFullTextSearch(
_ column: String,
query: String,
config: String? = nil
) -> PostgrestFilterBuilder {
wfts(column, query: query, config: config)
textSearch(column, query: query, config: config, type: .websearch)
}
}
81 changes: 81 additions & 0 deletions Tests/PostgRESTTests/PostgrestFilterBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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=*"
"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=*"
Loading