diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Articles/CompilerPerformance.md b/Sources/StructuredQueriesCore/Documentation.docc/Articles/CompilerPerformance.md index df1b4a35..aa594ef3 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Articles/CompilerPerformance.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Articles/CompilerPerformance.md @@ -12,84 +12,6 @@ The library provides a few tools to help mitigate this problem so that you can c benefits of type-safety and expressivity in your queries, while also helping out Swift in compiling your queries. -### Method operators - -By far the easiest way to mitigate compiler performance problems in complex expressions is to use -the method version of the various operators SQL has to offer, _e.g._ using -[`eq`]() instead of [`==`](). Consider a -database schema that has a "reminders" table, a "tags" table, as well as a many-to-many join table -that can associated any number of tags to any number of reminders: - -```swift -@Table struct Reminder: Identifiable { - let id: Int - var title = "" - var isCompleted = false -} -@Table struct Tag: Identifiable { - let id: Int - var title = "" -} -@Table struct ReminderTag { - let reminderID: Reminder.ID - let tagID: Tag.ID -} -``` - -With this schema it is possible to write a query that selects all reminders' titles, along with a -comma-separated string of every tag associated with each reminder: - -```swift -Reminder - .group(by: \.id) - .join(ReminderTag.all) { $0.id == $1.reminderID } - .join(Tag.all) { $1.tagID == $2.id } - .select { ($0.title, $2.title.groupConcat()) } -``` - -While this is a moderately complex query, it is definitely something that should compile quite -quickly, but unfortunately Swift currently cannot type-check it quickly enough (as of Swift 6.1). -The problem is that the overload space of `==` is so large that Swift has too many possibilities -to choose from when compiling this expression. - -The easiest fix is to use the dedicated [`eq`]() methods that have a -much smaller overload space: - -```diff - Reminder - .group(by: \.id) -- .join(ReminderTag.all) { $0.id == $1.reminderID } -+ .join(ReminderTag.all) { $0.id.eq($1.reminderID) } -- .join(Tag.all) { $1.tagID == $2.id } -+ .join(Tag.all) { $1.tagID.eq($2.id) } - .select { ($0.title, $2.title.groupConcat()) } -``` - -With that one change the expression now compiles immediately. We find that the equality operator -is by far the worst when it comes to compilation speed, and so we always recommend using - [`eq`]() over [`==`](), but other -operators can benefit from this too if you notice problems, such as - [`neq`]() over [`!=`](), - [`gt`]() over [`>`]((_:_:)>), and so on. Here is -a table of method equivalents to the most common operators: - -| Operator | Method | -| ---------------------------------- | ---------------- | -| `lhs == rhs` | `lhs.eq(rhs)` | -| `lhs == rhs` (involving optionals) | `lhs.is(rhs)` | -| `lhs != rhs` | `lhs.neq(rhs)` | -| `lhs != rhs` (involving optionals) | `lhs.isNot(rhs)` | -| `lhs && rhs` | `lhs.and(rhs)` | -| `lhs \|\| rhs` | `lhs.or(rhs)` | -| `!value` | `value.not()` | -| `lhs < rhs` | `lhs.lt(rhs)` | -| `lhs > rhs` | `lhs.gt(rhs)` | -| `lhs <= rhs` | `lhs.lte(rhs)` | -| `lhs >= rhs` | `lhs.gte(rhs)` | - -Often one does not need to convert _every_ operator to the method style. You can usually do it for -just a few operators to get a big boost, and we recommend starting with `==` and `!=`. - ### The #sql macro The library ships with a tool that allows one to write safe SQL strings _via_ the `#sql` macro (see diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Articles/Operators.md b/Sources/StructuredQueriesCore/Documentation.docc/Articles/Operators.md index ca2bf94f..1e5b127b 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Articles/Operators.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Articles/Operators.md @@ -13,17 +13,21 @@ used for string concatenation, not `||`: | Swift | SQL equivalent | | --------------------------- | ---------------- | -| `==` | `=`, `IS` | -| `!=` | `<>`, `IS NOT` | | `&&` | `AND` | | `\|\|` | `OR` | | `!` | `NOT` | | `+` (string concatenation) | `\|\|` | | `??` | `coalesce(_, _)` | -> Tip: Heavily overloaded Swift operators can tax the compiler, and so the library often provides -> method equivalents to alleviate this. For example, `==` is aliased to `eq`. See -> for a full list and more. +> `==` and `!=` are _not_ overloaded in Swift for a couple reasons: +> +> 1. Equality in SQL involves "three-valued logic": true, false, and null. Because of this there +> is more than one equality operation: `=` and `IS`, along with their negations, `<>` and +> `IS NOT`. To best preserve the intent of the generated SQL, StructuredQueries provides `eq` +> and `is`, along with their negations, `neq` and `isNot`. +> +> 2. These operators are the most heavily overloaded in Swift and can strain the compiler in +> complex queries. Other SQL operators are translated to method equivalents in Swift: @@ -56,8 +60,8 @@ Explore the full list of operators below. ### Equality -- ``QueryExpression/==(_:_:)`` -- ``QueryExpression/!=(_:_:)`` +- ``QueryExpression/eq(_:)`` +- ``QueryExpression/neq(_:)`` - ``QueryExpression/is(_:)`` - ``QueryExpression/isNot(_:)`` diff --git a/Sources/StructuredQueriesCore/Operators.swift b/Sources/StructuredQueriesCore/Operators.swift index 1688da7b..fb7b13ff 100644 --- a/Sources/StructuredQueriesCore/Operators.swift +++ b/Sources/StructuredQueriesCore/Operators.swift @@ -1,46 +1,4 @@ extension QueryExpression where QueryValue: QueryRepresentable { - /// A predicate expression indicating whether two query expressions are equal. - /// - /// ```swift - /// Reminder.where { $0.title == "Buy milk" } - /// // SELECT … FROM "reminders" WHERE "reminders"."title" = 'Buy milk' - /// ``` - /// - /// > Important: Overloaded operators can strain the Swift compiler's type checking ability. - /// > Consider using ``eq(_:)``, instead. - /// - /// - Parameters: - /// - lhs: An expression to compare. - /// - rhs: Another expression to compare. - /// - Returns: A predicate expression. - public static func == ( - lhs: Self, - rhs: some QueryExpression - ) -> some QueryExpression { - lhs.eq(rhs) - } - - /// A predicate expression indicating whether two query expressions are not equal. - /// - /// ```swift - /// Reminder.where { $0.title != "Buy milk" } - /// // SELECT … FROM "reminders" WHERE "reminders"."title" <> 'Buy milk' - /// ``` - /// - /// > Important: Overloaded operators can strain the Swift compiler's type checking ability. - /// > Consider using ``neq(_:)``, instead. - /// - /// - Parameters: - /// - lhs: An expression to compare. - /// - rhs: Another expression to compare. - /// - Returns: A predicate expression. - public static func != ( - lhs: Self, - rhs: some QueryExpression - ) -> some QueryExpression { - lhs.neq(rhs) - } - /// Returns a predicate expression indicating whether two query expressions are equal. /// /// ```swift @@ -100,10 +58,92 @@ extension QueryExpression where QueryValue: QueryRepresentable { where QueryValue._Optionalized.Wrapped == Other._Optionalized.Wrapped { BinaryOperator(lhs: self, operator: "IS NOT", rhs: other) } -} -private func isNull(_ expression: some QueryExpression) -> Bool { - (expression as? any _OptionalProtocol).map { $0._wrapped == nil } ?? false + @available(*, unavailable, message: "Use 'eq' (or 'is') instead.") + public static func == ( + lhs: Self, + rhs: some QueryExpression + ) -> some QueryExpression { + BinaryOperator(lhs: lhs, operator: "=", rhs: rhs) + } + + @available(*, unavailable, message: "Use 'eq' (or 'is') instead.") + public static func == ( + lhs: Self, + rhs: some QueryExpression + ) -> some QueryExpression { + BinaryOperator(lhs: lhs, operator: "=", rhs: rhs) + } + + @available(*, unavailable, message: "Use 'eq' (or 'is') instead.") + public static func == ( + lhs: Self, + rhs: some QueryExpression + ) -> some QueryExpression where QueryValue: _OptionalProtocol { + BinaryOperator(lhs: lhs, operator: "=", rhs: rhs) + } + + @available(*, deprecated, message: "Use 'is' instead.") + public static func == ( + lhs: Self, + rhs: _Null + ) -> some QueryExpression where QueryValue: _OptionalProtocol { + BinaryOperator(lhs: lhs, operator: "IS", rhs: rhs) + } + + @available(*, deprecated, message: "Use 'is' instead.") + public static func == ( + lhs: _Null, + rhs: Self + ) -> some QueryExpression where QueryValue: _OptionalProtocol { + BinaryOperator(lhs: lhs, operator: "IS", rhs: rhs) + } + + @available( + *, unavailable, message: "Use 'neq' or (or 'isNot') instead." + ) + public static func != ( + lhs: Self, + rhs: some QueryExpression + ) -> some QueryExpression { + BinaryOperator(lhs: lhs, operator: "<>", rhs: rhs) + } + + @available( + *, unavailable, message: "Use 'neq' or (or 'isNot') instead." + ) + public static func != ( + lhs: Self, + rhs: some QueryExpression + ) -> some QueryExpression { + BinaryOperator(lhs: lhs, operator: "<>", rhs: rhs) + } + + @available( + *, unavailable, message: "Use 'neq' or (or 'isNot') instead." + ) + public static func != ( + lhs: Self, + rhs: some QueryExpression + ) -> some QueryExpression where QueryValue: _OptionalProtocol { + BinaryOperator(lhs: lhs, operator: "<>", rhs: rhs) + } + + @available(*, deprecated, message: "Use 'isNot' instead.") + public static func != ( + lhs: Self, + rhs: _Null + ) -> some QueryExpression where QueryValue: _OptionalProtocol { + BinaryOperator(lhs: lhs, operator: "<>", rhs: rhs) + } + + @available(*, deprecated, message: "Use 'isNot' instead.") + public static func != ( + lhs: _Null, + rhs: Self + ) -> some QueryExpression where QueryValue: _OptionalProtocol { + BinaryOperator(lhs: lhs, operator: "<>", rhs: rhs) + } } extension QueryExpression where QueryValue: QueryRepresentable & QueryExpression { @@ -170,82 +210,6 @@ extension QueryExpression where QueryValue: QueryRepresentable & _OptionalProtoc } } -// NB: This overload is required due to an overload resolution bug of 'Updates[dynamicMember:]'. -@_disfavoredOverload -@_documentation(visibility: private) -public func == ( - lhs: any QueryExpression, - rhs: some QueryExpression -) -> some QueryExpression { - BinaryOperator(lhs: lhs, operator: isNull(rhs) ? "IS" : "=", rhs: rhs) -} - -// NB: This overload is required due to an overload resolution bug of 'Updates[dynamicMember:]'. -@_disfavoredOverload -@_documentation(visibility: private) -public func != ( - lhs: any QueryExpression, - rhs: some QueryExpression -) -> some QueryExpression { - BinaryOperator(lhs: lhs, operator: isNull(rhs) ? "IS NOT" : "<>", rhs: rhs) -} - -// NB: This overload is required due to an overload resolution bug of 'Updates[dynamicMember:]'. -@_documentation(visibility: private) -@_disfavoredOverload -public func == ( - lhs: any QueryExpression, - rhs: some QueryExpression -) -> some QueryExpression { - BinaryOperator(lhs: lhs, operator: "IS", rhs: rhs) -} - -// NB: This overload is required due to an overload resolution bug of 'Updates[dynamicMember:]'. -@_documentation(visibility: private) -@_disfavoredOverload -public func != ( - lhs: any QueryExpression, - rhs: some QueryExpression -) -> some QueryExpression { - BinaryOperator(lhs: lhs, operator: "IS NOT", rhs: rhs) -} - -// NB: This overload is required due to an overload resolution bug of 'Updates[dynamicMember:]'. -@_documentation(visibility: private) -public func == ( - lhs: any QueryExpression, - rhs: some QueryExpression -) -> some QueryExpression { - BinaryOperator(lhs: lhs, operator: "IS", rhs: rhs) -} - -// NB: This overload is required due to an overload resolution bug of 'Updates[dynamicMember:]'. -@_documentation(visibility: private) -public func != ( - lhs: any QueryExpression, - rhs: some QueryExpression -) -> some QueryExpression { - BinaryOperator(lhs: lhs, operator: "IS NOT", rhs: rhs) -} - -// NB: This overload is required due to an overload resolution bug of 'Updates[dynamicMember:]'. -@_documentation(visibility: private) -public func == ( - lhs: any QueryExpression, - rhs: _Null -) -> some QueryExpression { - SQLQueryExpression(lhs).is(rhs) -} - -// NB: This overload is required due to an overload resolution bug of 'Updates[dynamicMember:]'. -@_documentation(visibility: private) -public func != ( - lhs: any QueryExpression, - rhs: _Null -) -> some QueryExpression { - SQLQueryExpression(lhs).isNot(rhs) -} - extension QueryExpression where QueryValue: _OptionalPromotable { /// Returns a predicate expression indicating whether the value of the first expression is less /// than that of the second expression. diff --git a/Tests/StructuredQueriesTests/DeleteTests.swift b/Tests/StructuredQueriesTests/DeleteTests.swift index 9a40470b..93855939 100644 --- a/Tests/StructuredQueriesTests/DeleteTests.swift +++ b/Tests/StructuredQueriesTests/DeleteTests.swift @@ -43,7 +43,7 @@ extension SnapshotTests { } @Test func deleteID1() { - assertQuery(Reminder.delete().where { $0.id == 1 }.returning(\.self)) { + assertQuery(Reminder.delete().where { $0.id.eq(1) }.returning(\.self)) { """ DELETE FROM "reminders" WHERE (("reminders"."id") = (1)) @@ -129,7 +129,7 @@ extension SnapshotTests { enum R: AliasName {} assertQuery( RemindersList.as(R.self) - .where { $0.id == 1 } + .where { $0.id.eq(1) } .delete() .returning(\.self) ) { diff --git a/Tests/StructuredQueriesTests/InsertTests.swift b/Tests/StructuredQueriesTests/InsertTests.swift index fda7f5c7..5508bcf1 100644 --- a/Tests/StructuredQueriesTests/InsertTests.swift +++ b/Tests/StructuredQueriesTests/InsertTests.swift @@ -384,7 +384,7 @@ extension SnapshotTests { } @Test func upsertWithID() { - assertQuery(Reminder.where { $0.id == 1 }) { + assertQuery(Reminder.where { $0.id.eq(1) }) { """ SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "reminders" diff --git a/Tests/StructuredQueriesTests/OperatorsTests.swift b/Tests/StructuredQueriesTests/OperatorsTests.swift index 20d294b5..46f87244 100644 --- a/Tests/StructuredQueriesTests/OperatorsTests.swift +++ b/Tests/StructuredQueriesTests/OperatorsTests.swift @@ -7,103 +7,6 @@ import Testing extension SnapshotTests { struct OperatorsTests { - @Test func equality() { - assertInlineSnapshot(of: Row.columns.c == Row.columns.c, as: .sql) { - """ - ("rows"."c") = ("rows"."c") - """ - } - assertInlineSnapshot(of: Row.columns.c == Row.columns.a, as: .sql) { - """ - ("rows"."c") = ("rows"."a") - """ - } - assertInlineSnapshot(of: Row.columns.c == nil as Int?, as: .sql) { - """ - ("rows"."c") IS (NULL) - """ - } - assertInlineSnapshot(of: Row.columns.a == Row.columns.c, as: .sql) { - """ - ("rows"."a") IS ("rows"."c") - """ - } - assertInlineSnapshot(of: Row.columns.a == Row.columns.a, as: .sql) { - """ - ("rows"."a") IS ("rows"."a") - """ - } - assertInlineSnapshot(of: Row.columns.a == nil as Int?, as: .sql) { - """ - ("rows"."a") IS (NULL) - """ - } - assertInlineSnapshot(of: nil as Int? == Row.columns.c, as: .sql) { - """ - (NULL) IS ("rows"."c") - """ - } - assertInlineSnapshot(of: nil as Int? == Row.columns.a, as: .sql) { - """ - (NULL) IS ("rows"."a") - """ - } - assertInlineSnapshot(of: Row.columns.c != Row.columns.c, as: .sql) { - """ - ("rows"."c") <> ("rows"."c") - """ - } - assertInlineSnapshot(of: Row.columns.c != Row.columns.a, as: .sql) { - """ - ("rows"."c") <> ("rows"."a") - """ - } - assertInlineSnapshot(of: Row.columns.c != nil as Int?, as: .sql) { - """ - ("rows"."c") IS NOT (NULL) - """ - } - assertInlineSnapshot(of: Row.columns.a != Row.columns.c, as: .sql) { - """ - ("rows"."a") IS NOT ("rows"."c") - """ - } - assertInlineSnapshot(of: Row.columns.a != Row.columns.a, as: .sql) { - """ - ("rows"."a") IS NOT ("rows"."a") - """ - } - assertInlineSnapshot(of: Row.columns.a != nil as Int?, as: .sql) { - """ - ("rows"."a") IS NOT (NULL) - """ - } - assertInlineSnapshot(of: nil as Int? != Row.columns.c, as: .sql) { - """ - (NULL) IS NOT ("rows"."c") - """ - } - assertInlineSnapshot(of: nil as Int? != Row.columns.a, as: .sql) { - """ - (NULL) IS NOT ("rows"."a") - """ - } - } - - @available(*, deprecated) - @Test func deprecatedEquality() { - assertInlineSnapshot(of: Row.columns.c == nil, as: .sql) { - """ - ("rows"."c") IS (NULL) - """ - } - assertInlineSnapshot(of: Row.columns.c != nil, as: .sql) { - """ - ("rows"."c") IS NOT (NULL) - """ - } - } - @Test func comparison() { assertInlineSnapshot(of: Row.columns.c < Row.columns.c, as: .sql) { """ @@ -591,7 +494,7 @@ extension SnapshotTests { └──────┘ """ } - assertQuery(Values(Reminder.where { $0.id == 1 }.exists())) { + assertQuery(Values(Reminder.where { $0.id.eq(1) }.exists())) { """ SELECT EXISTS ( SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" @@ -606,7 +509,7 @@ extension SnapshotTests { └──────┘ """ } - assertQuery(Values(Reminder.where { $0.id == 100 }.exists())) { + assertQuery(Values(Reminder.where { $0.id.eq(100) }.exists())) { """ SELECT EXISTS ( SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" diff --git a/Tests/StructuredQueriesTests/PrimaryKeyedTableTests.swift b/Tests/StructuredQueriesTests/PrimaryKeyedTableTests.swift index 8f10316e..e449fff2 100644 --- a/Tests/StructuredQueriesTests/PrimaryKeyedTableTests.swift +++ b/Tests/StructuredQueriesTests/PrimaryKeyedTableTests.swift @@ -213,7 +213,7 @@ extension SnapshotTests { @Test func findByIDWithJoin() { assertQuery( Reminder - .join(RemindersList.all) { $0.remindersListID == $1.id } + .join(RemindersList.all) { $0.remindersListID.eq($1.id) } .select { ($0.title, $1.title) } .find(2) ) { diff --git a/Tests/StructuredQueriesTests/SelectTests.swift b/Tests/StructuredQueriesTests/SelectTests.swift index b4163a0a..c48ec8cb 100644 --- a/Tests/StructuredQueriesTests/SelectTests.swift +++ b/Tests/StructuredQueriesTests/SelectTests.swift @@ -1628,7 +1628,7 @@ extension SnapshotTests { extension Reminder.TableColumns { var isHighPriority: some QueryExpression { - self.priority == Priority.high + self.priority.is(Priority.high) } } diff --git a/Tests/StructuredQueriesTests/UpdateTests.swift b/Tests/StructuredQueriesTests/UpdateTests.swift index 09e093bf..bd41072f 100644 --- a/Tests/StructuredQueriesTests/UpdateTests.swift +++ b/Tests/StructuredQueriesTests/UpdateTests.swift @@ -40,7 +40,7 @@ extension SnapshotTests { } assertQuery( Reminder - .where { $0.priority == nil } + .where { $0.priority.is(nil) } .update { $0.isCompleted = true } .returning { ($0.title, $0.priority, $0.isCompleted) } ) { @@ -307,7 +307,7 @@ extension SnapshotTests { .find(1) .update { $0.dueDate = Case() - .when($0.dueDate == nil, then: #sql("'2018-01-29 00:08:00.000'")) + .when($0.dueDate.is(nil), then: #sql("'2018-01-29 00:08:00.000'")) } assertQuery(