Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(postgrest): add geojson, explain, and new filters #343

Merged
merged 3 commits into from
Apr 17, 2024
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
78 changes: 78 additions & 0 deletions Sources/PostgREST/PostgrestFilterBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,36 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder {
like(column, pattern: value)
}

/// Match only rows where `column` matches all of `patterns` case-sensitively.
/// - Parameters:
/// - column: The column to filter on
/// - patterns: The patterns to match with
public func likeAllOf(
_ column: String,
patterns: [some URLQueryRepresentable]
) -> PostgrestFilterBuilder {
let queryValue = patterns.queryValue
mutableState.withValue {
$0.request.query.append(URLQueryItem(name: column, value: "like(all).\(queryValue)"))
}
return self
}

/// Match only rows where `column` matches any of `patterns` case-sensitively.
/// - Parameters:
/// - column: The column to filter on
/// - patterns: The patterns to match with
public func likeAnyOf(
_ column: String,
patterns: [some URLQueryRepresentable]
) -> PostgrestFilterBuilder {
let queryValue = patterns.queryValue
mutableState.withValue {
$0.request.query.append(URLQueryItem(name: column, value: "like(any).\(queryValue)"))
}
return self
}

/// Match only rows where `column` matches `pattern` case-insensitively.
///
/// - Parameters:
Expand All @@ -184,6 +214,36 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder {
ilike(column, pattern: value)
}

/// Match only rows where `column` matches all of `patterns` case-insensitively.
/// - Parameters:
/// - column: The column to filter on
/// - patterns: The patterns to match with
public func iLikeAllOf(
_ column: String,
patterns: [some URLQueryRepresentable]
) -> PostgrestFilterBuilder {
let queryValue = patterns.queryValue
mutableState.withValue {
$0.request.query.append(URLQueryItem(name: column, value: "ilike(all).\(queryValue)"))
}
return self
}

/// Match only rows where `column` matches any of `patterns` case-insensitively.
/// - Parameters:
/// - column: The column to filter on
/// - patterns: The patterns to match with
public func iLikeAnyOf(
_ column: String,
patterns: [some URLQueryRepresentable]
) -> PostgrestFilterBuilder {
let queryValue = patterns.queryValue
mutableState.withValue {
$0.request.query.append(URLQueryItem(name: column, value: "ilike(any).\(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 Down Expand Up @@ -250,6 +310,24 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder {
return self
}

/// Match only rows where every element appearing in `column` is contained by `value`.
///
/// Only relevant for jsonb, array, and range columns.
///
/// - Parameters:
/// - column: The jsonb, array, or range column to filter on
/// - value: The jsonb, array, or range value to filter with
public func containedBy(
_ column: String,
value: some URLQueryRepresentable
) -> PostgrestFilterBuilder {
let queryValue = value.queryValue
mutableState.withValue {
$0.request.query.append(URLQueryItem(name: column, value: "cd.\(queryValue)"))
}
return self
}

/// Match only rows where every element in `column` is less than any element in `range`.
///
/// Only relevant for range columns.
Expand Down
49 changes: 49 additions & 0 deletions Sources/PostgREST/PostgrestTransformBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,53 @@ public class PostgrestTransformBuilder: PostgrestBuilder {
}
return self
}

/// Return `value` as an object in [GeoJSON](https://geojson.org) format.
public func geojson() -> PostgrestTransformBuilder {
mutableState.withValue {
$0.request.headers["Accept"] = "application/geo+json"
}
return self
}

/// Return `data` as the EXPLAIN plan for the query.
///
/// You need to enable the [db_plan_enabled](https://supabase.com/docs/guides/database/debugging-performance#enabling-explain)
/// setting before using this method.
///
/// - Parameters:
/// - analyze: If `true`, the query will be executed and the actual run time will be returned
/// - verbose: If `true`, the query identifier will be returned and `data` will include the
/// output columns of the query
/// - settings: If `true`, include information on configuration parameters that affect query
/// planning
/// - buffers: If `true`, include information on buffer usage
/// - wal: If `true`, include information on WAL record generation
/// - format: The format of the output, can be `"text"` (default) or `"json"`
public func explain(
analyze: Bool = false,
verbose: Bool = false,
settings: Bool = false,
buffers: Bool = false,
wal: Bool = false,
format: String = "text"
) -> PostgrestTransformBuilder {
mutableState.withValue {
let options = [
analyze ? "analyze" : nil,
verbose ? "verbose" : nil,
settings ? "settings" : nil,
buffers ? "buffers" : nil,
wal ? "wal" : nil,
]
.compactMap { $0 }
.joined(separator: "|")
let forMediaType = $0.request.headers["Accept"] ?? "application/json"
$0.request
.headers["Accept"] =
"application/vnd.pgrst.plan+\"\(format)\"; for=\(forMediaType); options=\(options);"
}

return self
}
}
24 changes: 18 additions & 6 deletions Sources/PostgREST/URLQueryRepresentable.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import _Helpers
import Foundation

/// A type that can fit into the query part of a URL.
Expand Down Expand Up @@ -32,6 +33,20 @@ extension Array: URLQueryRepresentable where Element: URLQueryRepresentable {
}
}

extension AnyJSON: URLQueryRepresentable {
public var queryValue: String {
switch self {
case let .array(array): array.queryValue
case let .object(object): object.queryValue
case let .string(string): string.queryValue
case let .double(double): double.queryValue
case let .integer(integer): integer.queryValue
case let .bool(bool): bool.queryValue
case .null: "NULL"
}
}
}

extension Optional: URLQueryRepresentable where Wrapped: URLQueryRepresentable {
public var queryValue: String {
if let value = self {
Expand All @@ -42,13 +57,10 @@ extension Optional: URLQueryRepresentable where Wrapped: URLQueryRepresentable {
}
}

extension Dictionary: URLQueryRepresentable
where
Key: URLQueryRepresentable,
Value: URLQueryRepresentable
{
extension JSONObject: URLQueryRepresentable {
public var queryValue: String {
JSONSerialization.stringfy(self)
let value = mapValues(\.value)
return JSONSerialization.stringfy(value)
}
}

Expand Down
36 changes: 35 additions & 1 deletion Tests/PostgRESTTests/BuildURLRequestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ struct User: Encodable {
var username: String?
}

@MainActor
final class BuildURLRequestTests: XCTestCase {
let url = URL(string: "https://example.supabase.co")!

Expand Down Expand Up @@ -172,6 +171,41 @@ final class BuildURLRequestTests: XCTestCase {
.select()
.is("email", value: String?.none)
},
TestCase(name: "likeAllOf") { client in
client.from("users")
.select()
.likeAllOf("email", patterns: ["%@supabase.io", "%@supabase.com"])
},
TestCase(name: "likeAnyOf") { client in
client.from("users")
.select()
.likeAnyOf("email", patterns: ["%@supabase.io", "%@supabase.com"])
},
TestCase(name: "iLikeAllOf") { client in
client.from("users")
.select()
.iLikeAllOf("email", patterns: ["%@supabase.io", "%@supabase.com"])
},
TestCase(name: "iLikeAnyOf") { client in
client.from("users")
.select()
.iLikeAnyOf("email", patterns: ["%@supabase.io", "%@supabase.com"])
},
TestCase(name: "containedBy using array") { client in
client.from("users")
.select()
.containedBy("id", value: ["a", "b", "c"])
},
TestCase(name: "containedBy using range") { client in
client.from("users")
.select()
.containedBy("age", value: "[10,20]")
},
TestCase(name: "containedBy using json") { client in
client.from("users")
.select()
.containedBy("userMetadata", value: ["age": 18])
},
]

for testCase in testCases {
Expand Down
18 changes: 14 additions & 4 deletions Tests/PostgRESTTests/URLQueryRepresentableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,19 @@ final class URLQueryRepresentableTests: XCTestCase {
XCTAssertEqual(queryValue, "{is:online,faction:red}")
}

func testDictionary() {
let dictionary = ["postalcode": 90210]
let queryValue = dictionary.queryValue
XCTAssertEqual(queryValue, "{\"postalcode\":90210}")
func testAnyJSON() {
XCTAssertEqual(
AnyJSON.array(["is:online", "faction:red"]).queryValue,
"{is:online,faction:red}"
)
XCTAssertEqual(
AnyJSON.object(["postalcode": 90210]).queryValue,
"{\"postalcode\":90210}"
)
XCTAssertEqual(AnyJSON.string("string").queryValue, "string")
XCTAssertEqual(AnyJSON.double(3.14).queryValue, "3.14")
XCTAssertEqual(AnyJSON.integer(3).queryValue, "3")
XCTAssertEqual(AnyJSON.bool(true).queryValue, "true")
XCTAssertEqual(AnyJSON.null.queryValue, "NULL")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
curl \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "X-Client-Info: postgrest-swift/x.y.z" \
"https://example.supabase.co/users?id=cd.%7Ba,b,c%7D&select=*"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
curl \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "X-Client-Info: postgrest-swift/x.y.z" \
"https://example.supabase.co/users?select=*&userMetadata=cd.%7B%22age%22:18%7D"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
curl \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "X-Client-Info: postgrest-swift/x.y.z" \
"https://example.supabase.co/users?age=cd.%5B10,20%5D&select=*"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
curl \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "X-Client-Info: postgrest-swift/x.y.z" \
"https://example.supabase.co/users?email=ilike(all).%7B%25@supabase.io,%25@supabase.com%7D&select=*"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
curl \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "X-Client-Info: postgrest-swift/x.y.z" \
"https://example.supabase.co/users?email=ilike(any).%7B%25@supabase.io,%25@supabase.com%7D&select=*"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
curl \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "X-Client-Info: postgrest-swift/x.y.z" \
"https://example.supabase.co/users?email=like(all).%7B%25@supabase.io,%25@supabase.com%7D&select=*"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
curl \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "X-Client-Info: postgrest-swift/x.y.z" \
"https://example.supabase.co/users?email=like(any).%7B%25@supabase.io,%25@supabase.com%7D&select=*"
Loading