From 5d7bcc69bd65da9df3dd84a8589b8391048fa514 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 16 Jun 2025 11:51:12 -0700 Subject: [PATCH 1/2] Add `QueryExpression.map,flatMap` This PR adds helpers that make it a little easier to work with optional query expressions in a builder. For example, if you want to execute a `LIKE` operator on an optional string, you currently have to resort to one of the following workarounds: ```swift .where { ($0.title ?? "").like("%foo%") } // or: .where { #sql("\($0.title) LIKE '%foo%') } ``` This PR introduces `map` and `flatMap` operations on optional `QueryExpression`s that unwraps the expression, giving you additional flexibility in how you express your builder code: ```swift .where { $0.title.map { $0.like("%foo%") } ?? false } ``` While this is more code than the above options, some may prefer its readability, and should we merge the other optional helpers from #61, it could be further shortened: ```swift .where { $0.title.map { $0.like("%foo%") } } ``` --- .../Extensions/QueryExpression.md | 5 +++ Sources/StructuredQueriesCore/Optional.swift | 40 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Extensions/QueryExpression.md b/Sources/StructuredQueriesCore/Documentation.docc/Extensions/QueryExpression.md index f1753714..70b00679 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Extensions/QueryExpression.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Extensions/QueryExpression.md @@ -28,3 +28,8 @@ - ``jsonArrayLength()`` - ``jsonGroupArray(order:filter:)`` + +### Optionality + +- ``map(_:)`` +- ``flatMap(_:)`` diff --git a/Sources/StructuredQueriesCore/Optional.swift b/Sources/StructuredQueriesCore/Optional.swift index 82c432f0..ccd53d78 100644 --- a/Sources/StructuredQueriesCore/Optional.swift +++ b/Sources/StructuredQueriesCore/Optional.swift @@ -137,3 +137,43 @@ where Wrapped.TableColumns: PrimaryKeyedTableDefinition { self[dynamicMember: \.primaryKey] } } + +extension QueryExpression where QueryValue: _OptionalProtocol { + /// Creates and optionalizes a new expression from this one by applying an unwrapped version of + /// this expression to a given closure. + /// + /// ```swift + /// Reminder.where { + /// $0.dueDate.map { $0 > Date() } + /// } + /// // SELECT … FROM "reminders" + /// // WHERE "reminders"."dueDate" > '2018-01-29 00:08:00.000' + /// ``` + /// + /// - Parameter transform: A closure that takes an unwrapped version of this expression. + /// - Returns: The result of the transform function, optionalized. + public func map( + _ transform: (SQLQueryExpression) -> some QueryExpression + ) -> some QueryExpression { + SQLQueryExpression(transform(SQLQueryExpression(queryFragment)).queryFragment) + } + + /// Creates a new optional expression from this one by applying an unwrapped version of this + /// expression to a given closure. + /// + /// ```swift + /// Reminder.select { + /// $0.dueDate.flatMap { $0.max() } + /// } + /// // SELECT max("reminders"."dueDate") FROM "reminders" + /// // => [Date?] + /// ``` + /// + /// - Parameter transform: A closure that takes an unwrapped version of this expression. + /// - Returns: The result of the transform function. + public func flatMap( + _ transform: (SQLQueryExpression) -> some QueryExpression + ) -> some QueryExpression { + SQLQueryExpression(transform(SQLQueryExpression(queryFragment)).queryFragment) + } +} From 265beeac5b1654b6008d0d9b0eb7f6e268f71b8a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 16 Jun 2025 12:00:22 -0700 Subject: [PATCH 2/2] tests --- .../StructuredQueriesTests/SelectTests.swift | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/Tests/StructuredQueriesTests/SelectTests.swift b/Tests/StructuredQueriesTests/SelectTests.swift index d1242435..a9579ba9 100644 --- a/Tests/StructuredQueriesTests/SelectTests.swift +++ b/Tests/StructuredQueriesTests/SelectTests.swift @@ -1153,6 +1153,50 @@ extension SnapshotTests { """ } } + + @Test func optionalMapAndFlatMap() { + do { + let query: some Statement = Reminder.select { + $0.priority.map { $0 < Priority.high } + } + assertQuery(query) { + """ + SELECT ("reminders"."priority" < 3) + FROM "reminders" + """ + } results: { + """ + ┌───────┐ + │ nil │ + │ nil │ + │ false │ + │ nil │ + │ nil │ + │ false │ + │ true │ + │ false │ + │ nil │ + │ true │ + └───────┘ + """ + } + } + do { + let query: some Statement = Reminder.select { $0.priority.flatMap { $0.max() } } + assertQuery(query) { + """ + SELECT max("reminders"."priority") + FROM "reminders" + """ + } results: { + """ + ┌───┐ + │ 3 │ + └───┘ + """ + } + } + } } }