From bfce09eeb7cdba374cb3073ba5b4a90ee88461f7 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 12 Jun 2025 11:12:14 -0500 Subject: [PATCH 01/32] Temporary triggers --- Sources/StructuredQueriesCore/Triggers.swift | 101 ++++++++++++++++++ .../Support/Schema.swift | 4 +- .../TriggersTests.swift | 38 +++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 Sources/StructuredQueriesCore/Triggers.swift create mode 100644 Tests/StructuredQueriesTests/TriggersTests.swift diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift new file mode 100644 index 00000000..589a4b47 --- /dev/null +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -0,0 +1,101 @@ +extension Table { + public static func createTemporaryTrigger( + _ when: TemporaryTrigger.When, + fileID: StaticString = #fileID, + line: UInt = #line, + column: UInt = #column + ) -> TemporaryTrigger { + TemporaryTrigger(when: when, fileID: fileID, line: line, column: column) + } +} + +// TODO: 'RAISE' +public struct TemporaryTrigger: Statement { + public typealias From = Never + public typealias Joins = () + public typealias QueryValue = () + + // TODO: 'WHEN expr'? + public enum When: QueryExpression { + public typealias QueryValue = () + + public enum Operation: QueryExpression { + public typealias QueryValue = () + + public enum Old: AliasName { public static var aliasName: String { "old" } } + public enum New: AliasName { public static var aliasName: String { "new" } } + + case insert(@Sendable (TableAlias.TableColumns) -> Begin) + // TODO: 'OF column-name, …'? + case update( + @Sendable (TableAlias.TableColumns, TableAlias.TableColumns) -> Begin + ) + case delete(@Sendable (TableAlias.TableColumns) -> Begin) + + var description: String { + switch self { + case .insert: "insert" + case .update: "update" + case .delete: "delete" + } + } + + public var queryFragment: QueryFragment { + var query: QueryFragment + var begin: QueryFragment + switch self { + case .insert(let statement): + query = "INSERT" + begin = statement(On.as(New.self).columns).query + case .update(let statement): + query = "UPDATE" + begin = statement(On.as(Old.self).columns, On.as(New.self).columns).query + case .delete(let statement): + query = "DELETE" + begin = statement(On.as(Old.self).columns).query + } + query.append(" ON \(On.self)\(.newlineOrSpace)FOR EACH ROW BEGIN") + query.append("\(.newlineOrSpace)\(begin.indented());\(.newlineOrSpace)END") + return query + } + } + + case before(Operation) + case after(Operation) + // TODO: 'insteadOf'? + + var description: String { + switch self { + case .before(let operation): + "before_\(operation.description)" + case .after(let operation): + "after_\(operation.description)" + } + } + + public var queryFragment: QueryFragment { + switch self { + case .before(let operation): + "BEFORE \(operation)" + case .after(let operation): + "AFTER \(operation)" + } + } + } + + fileprivate let when: When + let fileID: StaticString + let line: UInt + let column: UInt + + public var query: QueryFragment { + let query: QueryFragment = """ + CREATE TEMPORARY TRIGGER\(.newlineOrSpace)\(triggerName.indented())\(.newlineOrSpace)\(when) + """ + return "\(raw: query.debugDescription)" + } + + private var triggerName: QueryFragment { + "\(quote: "\(when.description)_on_\(On.tableName)@\(fileID):\(line):\(column)")" + } +} diff --git a/Tests/StructuredQueriesTests/Support/Schema.swift b/Tests/StructuredQueriesTests/Support/Schema.swift index 162ce25f..e50c2e98 100644 --- a/Tests/StructuredQueriesTests/Support/Schema.swift +++ b/Tests/StructuredQueriesTests/Support/Schema.swift @@ -12,6 +12,7 @@ struct RemindersList: Codable, Equatable, Identifiable { let id: Int var color = 0x4a99ef var title = "" + var position = 0 } @Table @@ -79,7 +80,8 @@ extension Database { CREATE TABLE "remindersLists" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "color" INTEGER NOT NULL DEFAULT 4889071, - "title" TEXT NOT NULL DEFAULT '' + "title" TEXT NOT NULL DEFAULT '', + "position" INTEGER NOT NULL DEFAULT 0 ) """ ) diff --git a/Tests/StructuredQueriesTests/TriggersTests.swift b/Tests/StructuredQueriesTests/TriggersTests.swift new file mode 100644 index 00000000..c5eceea4 --- /dev/null +++ b/Tests/StructuredQueriesTests/TriggersTests.swift @@ -0,0 +1,38 @@ +import Dependencies +import Foundation +import InlineSnapshotTesting +import StructuredQueries +import StructuredQueriesSQLite +import Testing + +extension SnapshotTests { + @Suite struct TriggersTests { + @Test func basics() { + assertQuery( + RemindersList.createTemporaryTrigger( + .after(.insert { new in + RemindersList + .update { + $0.position = RemindersList.select { ($0.position.max() ?? -1) + 1 } + } + .where { $0.id.eq(new.id) } + }) + ) + ) { + """ + CREATE TEMPORARY TRIGGER + "after_insert_on_remindersLists@StructuredQueriesTests/TriggersTests.swift:12:45" + AFTER INSERT ON "remindersLists" + FOR EACH ROW BEGIN + UPDATE "remindersLists" + SET "position" = ( + SELECT (coalesce(max("remindersLists"."position"), -1) + 1) + FROM "remindersLists" + ) + WHERE ("remindersLists"."id" = "new"."id"); + END + """ + } + } + } +} From 9d2a53a383cab0ccc711f2e18f261c151e81fdac Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 12 Jun 2025 16:41:08 -0500 Subject: [PATCH 02/32] wip --- Sources/StructuredQueriesCore/Triggers.swift | 228 +++++++++++++----- .../TriggersTests.swift | 4 +- 2 files changed, 166 insertions(+), 66 deletions(-) diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index 589a4b47..88037be6 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -1,101 +1,201 @@ extension Table { - public static func createTemporaryTrigger( - _ when: TemporaryTrigger.When, + /// A `CREATE TEMPORARY TRIGGER` statement. + /// + /// - Parameters: + /// - name: The trigger's name. By default a unique name is generated depending using the table, + /// operation, and source location. + /// - ifNotExists: Adds an `IF NOT EXISTS` clause to the `CREATE TRIGGER` statement. + /// - operation: The trigger's operation. + /// - fileID: The source `#fileID` associated with the trigger. + /// - line: The source `#line` associated with the trigger. + /// - column: The source `#column` associated with the trigger. + /// - Returns: A temporary trigger. + public static func createTemporaryTrigger( + _ name: String? = nil, + ifNotExists: Bool = false, + after operation: TemporaryTrigger.Operation, fileID: StaticString = #fileID, line: UInt = #line, column: UInt = #column - ) -> TemporaryTrigger { - TemporaryTrigger(when: when, fileID: fileID, line: line, column: column) + ) -> TemporaryTrigger { + TemporaryTrigger( + name: name, + ifNotExists: ifNotExists, + operation: operation, + fileID: fileID, + line: line, + column: column + ) } } -// TODO: 'RAISE' -public struct TemporaryTrigger: Statement { +public struct TemporaryTrigger: Statement { public typealias From = Never public typealias Joins = () public typealias QueryValue = () - // TODO: 'WHEN expr'? - public enum When: QueryExpression { + public struct Operation: QueryExpression { public typealias QueryValue = () - public enum Operation: QueryExpression { - public typealias QueryValue = () + public enum _Old: AliasName { public static var aliasName: String { "old" } } + public enum _New: AliasName { public static var aliasName: String { "new" } } - public enum Old: AliasName { public static var aliasName: String { "old" } } - public enum New: AliasName { public static var aliasName: String { "new" } } + public typealias Old = TableAlias.TableColumns + public typealias New = TableAlias.TableColumns + + /// An `AFTER INSERT` trigger operation. + /// + /// - Parameters: + /// - perform: A statement to perform for each triggered row. + /// - condition: A predicate that must be satisfied to perform the given statement. + /// - Returns: An `AFTER INSERT` trigger operation. + public static func insert( + forEachRow perform: (New) -> some Statement, + when condition: ((New) -> any QueryExpression)? = nil + ) -> Self { + Self( + kind: .insert(operation: perform(On.as(_New.self).columns).query), + when: condition?(On.as(_New.self).columns).queryFragment + ) + } - case insert(@Sendable (TableAlias.TableColumns) -> Begin) - // TODO: 'OF column-name, …'? - case update( - @Sendable (TableAlias.TableColumns, TableAlias.TableColumns) -> Begin + /// An `AFTER UPDATE` trigger operation. + /// + /// - Parameters: + /// - perform: A statement to perform for each triggered row. + /// - condition: A predicate that must be satisfied to perform the given statement. + /// - Returns: An `AFTER UPDATE` trigger operation. + public static func update( + forEachRow perform: (Old, New) -> some Statement, + when condition: ((Old, New) -> any QueryExpression)? = nil + ) -> Self { + update( + of: { _ in }, + forEachRow: perform, + when: condition ) - case delete(@Sendable (TableAlias.TableColumns) -> Begin) + } - var description: String { - switch self { - case .insert: "insert" - case .update: "update" - case .delete: "delete" - } + /// An `AFTER UPDATE` trigger operation. + /// + /// - Parameters: + /// - columns: Updated columns to scope the operation to. + /// - perform: A statement to perform for each triggered row. + /// - condition: A predicate that must be satisfied to perform the given statement. + /// - Returns: An `AFTER UPDATE` trigger operation. + public static func update( + of columns: (On.TableColumns) -> (repeat TableColumn), + forEachRow perform: (Old, New) -> some Statement, + when condition: ((Old, New) -> any QueryExpression)? = nil + ) -> Self { + var columnNames: [String] = [] + for column in repeat each columns(On.columns) { + columnNames.append(column.name) } + return Self( + kind: .update( + operation: perform(On.as(_Old.self).columns, On.as(_New.self).columns).query, + columnNames: columnNames + ), + when: condition?(On.as(_Old.self).columns, On.as(_New.self).columns).queryFragment + ) + } - public var queryFragment: QueryFragment { - var query: QueryFragment - var begin: QueryFragment - switch self { - case .insert(let statement): - query = "INSERT" - begin = statement(On.as(New.self).columns).query - case .update(let statement): - query = "UPDATE" - begin = statement(On.as(Old.self).columns, On.as(New.self).columns).query - case .delete(let statement): - query = "DELETE" - begin = statement(On.as(Old.self).columns).query - } - query.append(" ON \(On.self)\(.newlineOrSpace)FOR EACH ROW BEGIN") - query.append("\(.newlineOrSpace)\(begin.indented());\(.newlineOrSpace)END") - return query - } + /// An `AFTER DELETE` trigger operation. + /// + /// - Parameters: + /// - perform: A statement to perform for each triggered row. + /// - condition: A predicate that must be satisfied to perform the given statement. + /// - Returns: An `AFTER DELETE` trigger operation. + public static func delete( + forEachRow perform: (Old) -> some Statement, + when condition: ((Old) -> any QueryExpression)? = nil + ) -> Self { + Self( + kind: .delete(operation: perform(On.as(_Old.self).columns).query), + when: condition?(On.as(_Old.self).columns).queryFragment + ) + } + + private enum Kind { + case insert(operation: QueryFragment) + case update(operation: QueryFragment, columnNames: [String]) + case delete(operation: QueryFragment) } - case before(Operation) - case after(Operation) - // TODO: 'insteadOf'? + private let kind: Kind + private let when: QueryFragment? - var description: String { - switch self { - case .before(let operation): - "before_\(operation.description)" - case .after(let operation): - "after_\(operation.description)" + public var queryFragment: QueryFragment { + var query: QueryFragment = "AFTER" + let statement: QueryFragment + switch kind { + case .insert(let begin): + query.append(" INSERT") + statement = begin + case .update(let begin, let columnNames): + query.append(" UPDATE") + if !columnNames.isEmpty { + query.append( + " OF \(columnNames.map { QueryFragment(quote: $0) }.joined(separator: ", "))" + ) + } + statement = begin + case .delete(let begin): + query.append(" DELETE") + statement = begin } + query.append(" ON \(On.self)\(.newlineOrSpace)FOR EACH ROW") + if let when { + query.append(" WHEN \(when)") + } + query.append(" BEGIN") + query.append("\(.newlineOrSpace)\(statement.indented());\(.newlineOrSpace)END") + return query } - public var queryFragment: QueryFragment { - switch self { - case .before(let operation): - "BEFORE \(operation)" - case .after(let operation): - "AFTER \(operation)" + fileprivate var description: String { + switch kind { + case .insert: "after_insert" + case .update: "after_update" + case .delete: "after_delete" } } } - fileprivate let when: When - let fileID: StaticString - let line: UInt - let column: UInt + fileprivate let name: String? + fileprivate let ifNotExists: Bool + fileprivate let operation: Operation + fileprivate let fileID: StaticString + fileprivate let line: UInt + fileprivate let column: UInt + + /// Returns a `DROP TRIGGER` statement for this trigger. + /// + /// - Parameter ifExists: Adds an `IF EXISTS` condition to the `DROP TRIGGER`. + /// - Returns: A `DROP TRIGGER` statement for this trigger. + public func drop(ifExists: Bool = false) -> some Statement { + var query: QueryFragment = "DROP TRIGGER" + if ifExists { + query.append(" IF EXISTS") + } + query.append(" \(triggerName)") + return SQLQueryExpression(query) + } public var query: QueryFragment { - let query: QueryFragment = """ - CREATE TEMPORARY TRIGGER\(.newlineOrSpace)\(triggerName.indented())\(.newlineOrSpace)\(when) - """ + var query: QueryFragment = "CREATE TEMPORARY TRIGGER" + if ifNotExists { + query.append(" IF NOT EXISTS") + } + query.append("\(.newlineOrSpace)\(triggerName.indented())\(.newlineOrSpace)\(operation)") return "\(raw: query.debugDescription)" } private var triggerName: QueryFragment { - "\(quote: "\(when.description)_on_\(On.tableName)@\(fileID):\(line):\(column)")" + guard let name else { + return "\(quote: "\(operation.description)_on_\(On.tableName)@\(fileID):\(line):\(column)")" + } + return "\(quote: name)" } } diff --git a/Tests/StructuredQueriesTests/TriggersTests.swift b/Tests/StructuredQueriesTests/TriggersTests.swift index c5eceea4..803970cc 100644 --- a/Tests/StructuredQueriesTests/TriggersTests.swift +++ b/Tests/StructuredQueriesTests/TriggersTests.swift @@ -10,13 +10,13 @@ extension SnapshotTests { @Test func basics() { assertQuery( RemindersList.createTemporaryTrigger( - .after(.insert { new in + after: .insert { new in RemindersList .update { $0.position = RemindersList.select { ($0.position.max() ?? -1) + 1 } } .where { $0.id.eq(new.id) } - }) + } ) ) { """ From bfd70684f7935a8355f5e6a6c7130c8ec5f4edee Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 13 Jun 2025 10:09:35 -0700 Subject: [PATCH 03/32] touch triggers --- Sources/StructuredQueriesCore/Triggers.swift | 44 ++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index 88037be6..5d3dd70a 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -1,3 +1,5 @@ +import Foundation + extension Table { /// A `CREATE TEMPORARY TRIGGER` statement. /// @@ -27,6 +29,48 @@ extension Table { column: column ) } + + public func createTemporaryTrigger( + _ name: String? = nil, + ifNotExists: Bool = false, + afterUpdateTouch updates: (Updates) -> Void, + fileID: StaticString = #fileID, + line: UInt = #line, + column: UInt = #column + ) -> TemporaryTrigger { + TemporaryTrigger( + name: name, + ifNotExists: ifNotExists, + operation: .update { _, _ in + Self.update { updates($0) } + }, + fileID: fileID, + line: line, + column: column + ) + } + + public func createTemporaryTrigger( + _ name: String? = nil, + ifNotExists: Bool = false, + afterUpdateTouch date: KeyPath>, + fileID: StaticString = #fileID, + line: UInt = #line, + column: UInt = #column + ) -> TemporaryTrigger { + TemporaryTrigger( + name: name, + ifNotExists: ifNotExists, + operation: .update { _, _ in + Self.update { + $0[dynamicMember: date] = SQLQueryExpression("datetime('subsec')") + } + }, + fileID: fileID, + line: line, + column: column + ) + } } public struct TemporaryTrigger: Statement { From a72716b70ff8dac01f16e2fc1b26a009349e035c Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 13 Jun 2025 10:16:14 -0700 Subject: [PATCH 04/32] fix --- Sources/StructuredQueriesCore/Triggers.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index 5d3dd70a..119e92ce 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -30,7 +30,7 @@ extension Table { ) } - public func createTemporaryTrigger( + public static func createTemporaryTrigger( _ name: String? = nil, ifNotExists: Bool = false, afterUpdateTouch updates: (Updates) -> Void, @@ -50,7 +50,7 @@ extension Table { ) } - public func createTemporaryTrigger( + public static func createTemporaryTrigger( _ name: String? = nil, ifNotExists: Bool = false, afterUpdateTouch date: KeyPath>, From ecac05b65a171e1ce23a5470a58c4482d00e5bc2 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 13 Jun 2025 10:21:59 -0700 Subject: [PATCH 05/32] wip --- Sources/StructuredQueriesCore/Triggers.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index 119e92ce..e5e5491c 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -33,7 +33,7 @@ extension Table { public static func createTemporaryTrigger( _ name: String? = nil, ifNotExists: Bool = false, - afterUpdateTouch updates: (Updates) -> Void, + afterUpdateTouch updates: (inout Updates) -> Void, fileID: StaticString = #fileID, line: UInt = #line, column: UInt = #column @@ -42,7 +42,7 @@ extension Table { name: name, ifNotExists: ifNotExists, operation: .update { _, _ in - Self.update { updates($0) } + Self.update { updates(&$0) } }, fileID: fileID, line: line, @@ -50,6 +50,7 @@ extension Table { ) } + // TODO: Touchable protocol with Date: Touchable, UUID: Touchable, ? public static func createTemporaryTrigger( _ name: String? = nil, ifNotExists: Bool = false, From c79a759c1eeb10f422699bf8d0614b7905f16055 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 13 Jun 2025 10:23:52 -0700 Subject: [PATCH 06/32] wip --- Sources/StructuredQueriesCore/Triggers.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index e5e5491c..fa22b04a 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -72,6 +72,8 @@ extension Table { column: column ) } + + // TODO: createTemporaryTrigge(afterUpdate: { $0... }, touch: { $0... = }) } public struct TemporaryTrigger: Statement { From 4b33440ba8f3f797dd66f1e15ff5c4fc3cef2492 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 13 Jun 2025 14:41:13 -0700 Subject: [PATCH 07/32] wip --- .../Documentation.docc/Articles/Triggers.md | 189 ++++++++++++++++++ .../StructuredQueriesCore.md | 1 + Sources/StructuredQueriesCore/Triggers.swift | 1 + 3 files changed, 191 insertions(+) create mode 100644 Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md b/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md new file mode 100644 index 00000000..0d72814b --- /dev/null +++ b/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md @@ -0,0 +1,189 @@ +# Triggers + +Learn how to build trigger statements that can monitor the database for events and react. + +## Overview + +[Triggers](https://sqlite.org/lang_createtrigger.html) are operations that execute in your database +when some specific database event occurs. StructuredQueries comes with tools to create _temporary_ +triggers in a type-safe and schema-safe fashion. + +### Trigger basics + +One of the most common use cases for a trigger is refreshing an "updatedAt" timestamp on a row when +it is updated in the database. One can create such a trigger SQL statement using the +``Table/createTemporaryTrigger(_:ifNotExists:after:fileID:line:column:)`` static method: + +@Row { + @Column { + ```swift + Reminder.createTemporaryTrigger( + "reminders_updatedAt", + after: .update { _, _ in + Reminder.update { + $0.updatedAt = #sql("datetime('subsec')") + } + } + ) + ``` + } + @Column { + ```sql + CREATE TEMPORARY TRIGGER "reminders_updatedAt" + AFTER UPDATE ON "reminders" + FOR EACH ROW + BEGIN + UPDATE "reminders" + SET "updatedAt" = datetime('subsec'); + END + ``` + } +} + +This will make it so that anytime a reminder is updated in the database its `updatedAt` will be +refreshed with the current time immediately. + +This pattern of updating a timestamp when a row changes is so common that the library comes with +a specialized tool just for that kind of trigger, ``Table/createTemporaryTrigger(_:ifNotExists:afterUpdateTouch:fileID:line:column:)``: + +@Row { + @Column { + ```swift + Reminder + .createTemporaryTrigger( + afterUpdateTouch: { + $0.updatedAt = datetime('subsec') + } + ) + ``` + } + @Column { + ```sql + CREATE TEMPORARY TRIGGER "reminders_updatedAt" + AFTER UPDATE ON "reminders" + FOR EACH ROW + BEGIN + UPDATE "reminders" + SET "updatedAt" = datetime('subsec'); + END + ``` + } +} + +And further, the pattern of specifically updating a _timestamp_ column is so common that the library +comes with another specialized too just for that kind of trigger, +``Table/createTemporaryTrigger(_:ifNotExists:afterUpdateTouch:fileID:line:column:)``: + + +@Row { + @Column { + ```swift + Reminder + .createTemporaryTrigger( + afterUpdateTouch: \.updatedAt + ) + ``` + } + @Column { + ```sql + CREATE TEMPORARY TRIGGER "reminders_updatedAt" + AFTER UPDATE ON "reminders" + FOR EACH ROW + BEGIN + UPDATE "reminders" + SET "updatedAt" = datetime('subsec'); + END + ``` + } +} + + +### More types of triggers + +There are 3 kinds of triggers depending on the event being listened for in the database: an +update trigger, an insert trigger, and a deletion trigger. For each of these kinds of triggers one +can perform 4 kinds of actions: a select, insert, update or delete. All 12 combinations of these +kinds of triggers are supported by the library. + +> Note: Technically SQLite supports "BEFORE" triggers and "AFTER" triggers, but the documentation +> recommends against using "BEFORE" triggers as it can lead to undefined behavior. Therefore +> Structured Queries does not expose "BEFORE" triggers in its API. If you want to go against the +> recommendations of SQLite and create a "BEFORE" trigger, you can always write a trigger as a +> SQL string (see for more info). + +Here are a few examples to show you the possibilities with triggers: + +#### Non-empty tables + +One can use triggers to enforce that a table is never fully emptied out. For example, suppose you +want to make sure that the "remindersLists" table always has at least one row. Then one can +use an "AFTER DELETE" trigger with an "INSERT" action to insert a stub reminders list when it +detects the last list was deleted: + +@Row { + @Column { + ```swift + RemindersList.createTemporaryTrigger( + "nonEmptyRemindersLists", + after: .delete { _ in + RemindersList + .insert { + RemindersList.Draft(title: "Personal") + } + } when: { _ in + !RemindersList.all.exists() + } + ) + ``` + } + @Column { + ```sql + CREATE TEMPORARY TRIGGER "nonEmptyRemindersLists" + AFTER DELETE ON "remindersLists" + FOR EACH ROW WHEN NOT (EXISTS (SELECT * FROM "remindersLists")) + BEGIN + INSERT INTO "remindersLists" + ("id", "color", "title") + VALUES + (NULL, 0xffaaff00, 'Personal'); + END + ``` + } +} + +#### Invoke Swift code from triggers + +One can use triggers with a "SELECT" action to invoke Swift code when an event occurs in your +database. For example, suppose you want to execute a Swift function a new reminder is inserted +into the database. First you must register the function with SQLite and that depends on what +SQLite driver you are using ([here][grdb-add-function] is how to do it in GRDB). Suppose we +registered a function called `didInsertReminder`, and further suppose it takes one argument +of the ID of the newly inserted reminder. + +Then one can invoke this function whenever a reminder is inserted into the database with the +following trigger: + +[grdb-add-function]: https://swiftpackageindex.com/groue/grdb.swift/v7.5.0/documentation/grdb/database/add(function:) + +@Row { + @Column { + ```swift + RemindersList.createTemporaryTrigger( + "insertReminderCallback", + after: .insert { new in + #sql("SELECT didInsertReminder(\(new.id))") + } + ) + ``` + } + @Column { + ```sql + CREATE TEMPORARY TRIGGER "insertReminderCallback" + AFTER DELETE ON "reminders" + FOR EACH ROW + BEGIN + SELECT didInsertReminder("new"."id") + END + ``` + } +} diff --git a/Sources/StructuredQueriesCore/Documentation.docc/StructuredQueriesCore.md b/Sources/StructuredQueriesCore/Documentation.docc/StructuredQueriesCore.md index 414c6d67..657ce76c 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/StructuredQueriesCore.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/StructuredQueriesCore.md @@ -124,6 +124,7 @@ reading to learn more about building SQL with StructuredQueries. - - - +- - - diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index fa22b04a..448a9f73 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -74,6 +74,7 @@ extension Table { } // TODO: createTemporaryTrigge(afterUpdate: { $0... }, touch: { $0... = }) + // TODO: createTemporaryTrigge(afterUpdate: \.self, touch: \.updatedAt) } public struct TemporaryTrigger: Statement { From 3be74390f0e8182e7604700c0fb6f7ee2db7eadb Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 16 Jun 2025 09:34:18 -0700 Subject: [PATCH 08/32] wip --- Sources/StructuredQueriesCore/Triggers.swift | 30 +++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index 448a9f73..9b9acf1d 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -30,6 +30,8 @@ extension Table { ) } + // TODO: write tests on these below: + public static func createTemporaryTrigger( _ name: String? = nil, ifNotExists: Bool = false, @@ -38,11 +40,13 @@ extension Table { line: UInt = #line, column: UInt = #column ) -> TemporaryTrigger { - TemporaryTrigger( - name: name, + Self.createTemporaryTrigger( + name, ifNotExists: ifNotExists, - operation: .update { _, _ in - Self.update { updates(&$0) } + after: .update { _, new in + Self + .where { $0.rowid.eq(new.rowid) } + .update { updates(&$0) } }, fileID: fileID, line: line, @@ -59,22 +63,20 @@ extension Table { line: UInt = #line, column: UInt = #column ) -> TemporaryTrigger { - TemporaryTrigger( - name: name, + Self.createTemporaryTrigger( + name, ifNotExists: ifNotExists, - operation: .update { _, _ in - Self.update { - $0[dynamicMember: date] = SQLQueryExpression("datetime('subsec')") - } + afterUpdateTouch: { + $0[dynamicMember: date] = SQLQueryExpression("datetime('subsec')") }, fileID: fileID, line: line, - column: column - ) + column: column) } - // TODO: createTemporaryTrigge(afterUpdate: { $0... }, touch: { $0... = }) - // TODO: createTemporaryTrigge(afterUpdate: \.self, touch: \.updatedAt) + // TODO: createTemporaryTrigger(afterUpdateTouch: \.updatedAt) + // TODO: createTemporaryTrigger(afterUpdate: { $0... }, touch: { $0... = }) + // TODO: createTemporaryTrigger(afterUpdate: \.self, touch: \.updatedAt) } public struct TemporaryTrigger: Statement { From 6f33d626921cb5aa89ea9c76e909ba98e9a62a99 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 16 Jun 2025 12:33:04 -0700 Subject: [PATCH 09/32] wip --- Sources/StructuredQueriesCore/Triggers.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index 9b9acf1d..1492c42e 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -3,6 +3,8 @@ import Foundation extension Table { /// A `CREATE TEMPORARY TRIGGER` statement. /// + /// > Important: TODO: explain how implicit names are handled and how trigger helpers should always take file/line/column. and put in name/file/line/column parameters. + /// /// - Parameters: /// - name: The trigger's name. By default a unique name is generated depending using the table, /// operation, and source location. @@ -74,6 +76,7 @@ extension Table { column: column) } + // TODO: createTemporaryTrigger(afterUpdateTouch: \.updatedAt) // TODO: createTemporaryTrigger(afterUpdate: { $0... }, touch: { $0... = }) // TODO: createTemporaryTrigger(afterUpdate: \.self, touch: \.updatedAt) From 42779c05ae6399f7f5514ecf8e8b765c7678b9df Mon Sep 17 00:00:00 2001 From: Brandon Williams <135203+mbrandonw@users.noreply.github.com> Date: Mon, 16 Jun 2025 08:28:06 -0700 Subject: [PATCH 10/32] Remove trailing comma (#75) * Remove trailing comma while we support Swift 6.0 * compile for swift 6.0 --- .github/workflows/ci.yml | 1 + Sources/StructuredQueriesCore/Internal/Deprecations.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5727296..70ba6966 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,7 @@ jobs: strategy: matrix: swift: + - '6.0' - '6.1' runs-on: ubuntu-latest container: swift:${{ matrix.swift }} diff --git a/Sources/StructuredQueriesCore/Internal/Deprecations.swift b/Sources/StructuredQueriesCore/Internal/Deprecations.swift index 8dc54d96..6648ec88 100644 --- a/Sources/StructuredQueriesCore/Internal/Deprecations.swift +++ b/Sources/StructuredQueriesCore/Internal/Deprecations.swift @@ -53,7 +53,7 @@ extension Table { or conflictResolution: ConflictResolution? = nil, _ columns: (TableColumns) -> (TableColumn, repeat TableColumn), select selection: () -> Select<(C1, repeat each C2), From, Joins>, - onConflict updates: ((inout Updates) -> Void)?, + onConflict updates: ((inout Updates) -> Void)? ) -> InsertOf where C1.QueryValue == V1, (repeat (each C2).QueryValue) == (repeat each V2) { insert(or: conflictResolution, columns, select: selection, onConflictDoUpdate: updates) From 067c9194a3b9889504abd2c25e4e3ec2dfbf4d09 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 16 Jun 2025 12:04:34 -0700 Subject: [PATCH 11/32] Don't require decodable fields in `GROUP BY` (#79) This PR allows the following to work without qualifying the expression type: ```diff Reminder.group { - #sql("date(\($0.dueDate))", as: Date?.self) + #sql("date(\($0.dueDate))") } ``` --- Sources/StructuredQueries/Macros.swift | 7 +++++ .../Statements/Select.swift | 26 ++++--------------- .../Statements/Where.swift | 14 +++------- .../StructuredQueriesTests/SelectTests.swift | 17 ++++++++++++ 4 files changed, 33 insertions(+), 31 deletions(-) diff --git a/Sources/StructuredQueries/Macros.swift b/Sources/StructuredQueries/Macros.swift index 14f24d94..95accb33 100644 --- a/Sources/StructuredQueries/Macros.swift +++ b/Sources/StructuredQueries/Macros.swift @@ -132,3 +132,10 @@ public macro sql( as queryValueType: QueryValue.Type = QueryValue.self ) -> SQLQueryExpression = #externalMacro(module: "StructuredQueriesMacros", type: "SQLMacro") + +@freestanding(expression) +public macro sql( + _ queryFragment: QueryFragment, + as queryValueType: Any.Type = Any.self +) -> SQLQueryExpression = + #externalMacro(module: "StructuredQueriesMacros", type: "SQLMacro") diff --git a/Sources/StructuredQueriesCore/Statements/Select.swift b/Sources/StructuredQueriesCore/Statements/Select.swift index d33b02b7..fcb11d0b 100644 --- a/Sources/StructuredQueriesCore/Statements/Select.swift +++ b/Sources/StructuredQueriesCore/Statements/Select.swift @@ -212,7 +212,7 @@ extension Table { /// - Returns: A select statement that groups by the given column. public static func group( by grouping: (TableColumns) -> C - ) -> SelectOf where C.QueryValue: QueryDecodable { + ) -> SelectOf { Where().group(by: grouping) } @@ -226,12 +226,7 @@ extension Table { each C3: QueryExpression >( by grouping: (TableColumns) -> (C1, C2, repeat each C3) - ) -> SelectOf - where - C1.QueryValue: QueryDecodable, - C2.QueryValue: QueryDecodable, - repeat (each C3).QueryValue: QueryDecodable - { + ) -> SelectOf { Where().group(by: grouping) } @@ -1182,8 +1177,7 @@ extension Select { /// - Returns: A new select statement that groups by the given column. public func group( by grouping: (From.TableColumns, repeat (each J).TableColumns) -> C - ) -> Self - where C.QueryValue: QueryDecodable, Joins == (repeat each J) { + ) -> Self where Joins == (repeat each J) { _group(by: grouping) } @@ -1199,13 +1193,7 @@ extension Select { each J: Table >( by grouping: (From.TableColumns, repeat (each J).TableColumns) -> (C1, C2, repeat each C3) - ) -> Self - where - C1.QueryValue: QueryDecodable, - C2.QueryValue: QueryDecodable, - repeat (each C3).QueryValue: QueryDecodable, - Joins == (repeat each J) - { + ) -> Self where Joins == (repeat each J) { _group(by: grouping) } @@ -1214,11 +1202,7 @@ extension Select { each J: Table >( by grouping: (From.TableColumns, repeat (each J).TableColumns) -> (repeat each C) - ) -> Self - where - repeat (each C).QueryValue: QueryDecodable, - Joins == (repeat each J) - { + ) -> Self where Joins == (repeat each J) { var select = self select.group .append( diff --git a/Sources/StructuredQueriesCore/Statements/Where.swift b/Sources/StructuredQueriesCore/Statements/Where.swift index 146fa7d4..f4c48bc8 100644 --- a/Sources/StructuredQueriesCore/Statements/Where.swift +++ b/Sources/StructuredQueriesCore/Statements/Where.swift @@ -387,22 +387,16 @@ extension Where: SelectStatement { } /// A select statement for the filtered table grouped by the given column. - public func group(by grouping: (From.TableColumns) -> C) -> Select< - (), From, () - > - where C.QueryValue: QueryDecodable { + public func group( + by grouping: (From.TableColumns) -> C + ) -> Select<(), From, ()> { asSelect().group(by: grouping) } /// A select statement for the filtered table grouped by the given columns. public func group( by grouping: (From.TableColumns) -> (C1, C2, repeat each C3) - ) -> SelectOf - where - C1.QueryValue: QueryDecodable, - C2.QueryValue: QueryDecodable, - repeat (each C3).QueryValue: QueryDecodable - { + ) -> SelectOf { asSelect().group(by: grouping) } diff --git a/Tests/StructuredQueriesTests/SelectTests.swift b/Tests/StructuredQueriesTests/SelectTests.swift index d1242435..a0cb9eb9 100644 --- a/Tests/StructuredQueriesTests/SelectTests.swift +++ b/Tests/StructuredQueriesTests/SelectTests.swift @@ -644,6 +644,23 @@ extension SnapshotTests { └───────┴───┘ """ } + + assertQuery( + Reminder.select { ($0.isCompleted, $0.id.count()) }.group { #sql("\($0.isCompleted)") } + ) { + """ + SELECT "reminders"."isCompleted", count("reminders"."id") + FROM "reminders" + GROUP BY "reminders"."isCompleted" + """ + } results: { + """ + ┌───────┬───┐ + │ false │ 7 │ + │ true │ 3 │ + └───────┴───┘ + """ + } } @Test func having() { From 81600caa1d9352bb9e42f82cd6efdcbfca53f1a9 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 16 Jun 2025 12:09:14 -0700 Subject: [PATCH 12/32] Add `QueryExpression.map,flatMap` (#80) * 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%") } } ``` * tests --- .../Extensions/QueryExpression.md | 5 +++ Sources/StructuredQueriesCore/Optional.swift | 40 +++++++++++++++++ .../StructuredQueriesTests/SelectTests.swift | 44 +++++++++++++++++++ 3 files changed, 89 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) + } +} diff --git a/Tests/StructuredQueriesTests/SelectTests.swift b/Tests/StructuredQueriesTests/SelectTests.swift index a0cb9eb9..03f1f404 100644 --- a/Tests/StructuredQueriesTests/SelectTests.swift +++ b/Tests/StructuredQueriesTests/SelectTests.swift @@ -1170,6 +1170,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 │ + └───┘ + """ + } + } + } } } From f8699232a8d95634297c2cf3f17f8392da03ecdc Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 16 Jun 2025 12:33:40 -0700 Subject: [PATCH 13/32] wip --- Sources/StructuredQueriesCore/Triggers.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index 1492c42e..c7ef9849 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -73,7 +73,8 @@ extension Table { }, fileID: fileID, line: line, - column: column) + column: column + ) } From 31737697f9d74f897528996245dda7f989a97521 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 16 Jun 2025 14:11:08 -0700 Subject: [PATCH 14/32] wip --- Sources/StructuredQueriesCore/Triggers.swift | 64 +++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index c7ef9849..e89f047f 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -1,4 +1,5 @@ import Foundation +import IssueReporting extension Table { /// A `CREATE TEMPORARY TRIGGER` statement. @@ -243,7 +244,68 @@ public struct TemporaryTrigger: Statement { query.append(" IF NOT EXISTS") } query.append("\(.newlineOrSpace)\(triggerName.indented())\(.newlineOrSpace)\(operation)") - return "\(raw: query.debugDescription)" + return query.segments.reduce(into: QueryFragment()) { + switch $1 { + case .sql(let sql): + $0.append("\(raw: sql)") + case .binding(let binding): + switch binding { + case .blob(let blob): + reportIssue( + """ + Cannot bind bytes to a trigger statement. To hardcode a constant BLOB, use the '#sql' \ + macro. + """ + ) + let hex = blob.reduce(into: "") { + let hex = String($1, radix: 16) + if hex.count == 1 { + $0.append("0") + } + $0.append(hex) + } + $0.append("unhex(\(quote: hex, delimiter: .text))") + case .double(let double): + $0.append("\(raw: double)") + case .date(let date): + reportIssue( + """ + Cannot bind a date to a trigger statement. Specify dates using the '#sql' macro, \ + instead. For example, the current date: + + #sql("datetime()") + + Or a constant date: + + #sql("'2018-01-29 00:08:00'") + """ + ) + $0.append("\(quote: date.iso8601String, delimiter: .text)") + case .int(let int): + $0.append("\(raw: int)") + case .null: + $0.append("NULL") + case .text(let string): + $0.append("\(quote: string, delimiter: .text)") + case .uuid(let uuid): + reportIssue( + """ + Cannot bind a UUID to a trigger statement. Specify UUIDs using the '#sql' macro, \ + instead. For example, a random UUID: + + #sql("uuid()") + + Or a constant UUID: + + #sql("'00000000-0000-0000-0000-000000000000'") + """ + ) + $0.append("\(quote: uuid.uuidString.lowercased(), delimiter: .text)") + case .invalid(let error): + $0.append("\(.invalid(error.underlyingError))") + } + } + } } private var triggerName: QueryFragment { From b4ce72c7b3ff686be92c13ee3d7e6ce21bf6a992 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 16 Jun 2025 14:21:21 -0700 Subject: [PATCH 15/32] wip --- .../TriggersTests.swift | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Tests/StructuredQueriesTests/TriggersTests.swift b/Tests/StructuredQueriesTests/TriggersTests.swift index 803970cc..3e3c3ef6 100644 --- a/Tests/StructuredQueriesTests/TriggersTests.swift +++ b/Tests/StructuredQueriesTests/TriggersTests.swift @@ -34,5 +34,43 @@ extension SnapshotTests { """ } } + + @Test func dateDiagnostic() { + withKnownIssue { + assertQuery( + Reminder.createTemporaryTrigger( + after: .update { _, new in + Reminder + .update { $0.dueDate = Date(timeIntervalSinceReferenceDate: 0) } + .where { $0.id.eq(new.id) } + } + ) + ) { + """ + CREATE TEMPORARY TRIGGER + "after_update_on_reminders@StructuredQueriesTests/TriggersTests.swift:41:42" + AFTER UPDATE ON "reminders" + FOR EACH ROW BEGIN + UPDATE "reminders" + SET "dueDate" = '2001-01-01 00:00:00.000' + WHERE ("reminders"."id" = "new"."id"); + END + """ + } + } matching: { + $0.description.contains( + """ + Cannot bind a date to a trigger statement. Specify dates using the '#sql' macro, \ + instead. For example, the current date: + + #sql("datetime()") + + Or a constant date: + + #sql("'2018-01-29 00:08:00'") + """ + ) + } + } } } From 6ade611b120b5830b93c8d407bdc151a5504a3aa Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 16 Jun 2025 14:27:36 -0700 Subject: [PATCH 16/32] wip --- Sources/StructuredQueries/Macros.swift | 2 +- .../Documentation.docc/Articles/Triggers.md | 39 +++++++++---------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/Sources/StructuredQueries/Macros.swift b/Sources/StructuredQueries/Macros.swift index 95accb33..778d0fa5 100644 --- a/Sources/StructuredQueries/Macros.swift +++ b/Sources/StructuredQueries/Macros.swift @@ -46,7 +46,7 @@ public macro Column( type: "ColumnMacro" ) -/// Tells Structured Queries not to consider the annotated property a column of the table +/// Tells StructuredQueries not to consider the annotated property a column of the table. /// /// Like SwiftData's `@Transient` macro, but for SQL. @attached(peer) diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md b/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md index 0d72814b..deeb0723 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md @@ -44,7 +44,8 @@ This will make it so that anytime a reminder is updated in the database its `upd refreshed with the current time immediately. This pattern of updating a timestamp when a row changes is so common that the library comes with -a specialized tool just for that kind of trigger, ``Table/createTemporaryTrigger(_:ifNotExists:afterUpdateTouch:fileID:line:column:)``: +a specialized tool just for that kind of trigger, +``Table/createTemporaryTrigger(_:ifNotExists:afterUpdateTouch:fileID:line:column:)``: @Row { @Column { @@ -97,28 +98,27 @@ comes with another specialized too just for that kind of trigger, } } - ### More types of triggers -There are 3 kinds of triggers depending on the event being listened for in the database: an -update trigger, an insert trigger, and a deletion trigger. For each of these kinds of triggers one -can perform 4 kinds of actions: a select, insert, update or delete. All 12 combinations of these -kinds of triggers are supported by the library. +There are 3 kinds of triggers depending on the event being listened for in the database: inserts, +updates, and deletes. For each of these kinds of triggers one can perform 4 kinds of actions: a +select, insert, update, or delete. All 12 combinations of these kinds of triggers are supported by +the library. -> Note: Technically SQLite supports "BEFORE" triggers and "AFTER" triggers, but the documentation -> recommends against using "BEFORE" triggers as it can lead to undefined behavior. Therefore -> Structured Queries does not expose "BEFORE" triggers in its API. If you want to go against the -> recommendations of SQLite and create a "BEFORE" trigger, you can always write a trigger as a -> SQL string (see for more info). +> Note: Technically SQLite supports `BEFORE` triggers and `AFTER` triggers, but the documentation +> recommends against using `BEFORE` triggers as it can lead to undefined behavior. Therefore +> StructuredQueries does not expose `BEFORE` triggers in its API. If you want to go against the +> recommendations of SQLite and create a `BEFORE` trigger, you can always write a trigger as a SQL +> string (see for more info). Here are a few examples to show you the possibilities with triggers: #### Non-empty tables One can use triggers to enforce that a table is never fully emptied out. For example, suppose you -want to make sure that the "remindersLists" table always has at least one row. Then one can -use an "AFTER DELETE" trigger with an "INSERT" action to insert a stub reminders list when it -detects the last list was deleted: +want to make sure that the `remindersLists` table always has at least one row. Then one can use an +`AFTER DELETE` trigger with an `INSERT` action to insert a stub reminders list when it detects the +last list was deleted: @Row { @Column { @@ -153,15 +153,14 @@ detects the last list was deleted: #### Invoke Swift code from triggers -One can use triggers with a "SELECT" action to invoke Swift code when an event occurs in your +One can use triggers with a `SELECT` action to invoke Swift code when an event occurs in your database. For example, suppose you want to execute a Swift function a new reminder is inserted into the database. First you must register the function with SQLite and that depends on what -SQLite driver you are using ([here][grdb-add-function] is how to do it in GRDB). Suppose we -registered a function called `didInsertReminder`, and further suppose it takes one argument -of the ID of the newly inserted reminder. +SQLite driver you are using ([here][grdb-add-function] is how to do it in GRDB). -Then one can invoke this function whenever a reminder is inserted into the database with the -following trigger: +Suppose we registered a function called `didInsertReminder`, and further suppose it takes one +argument of the ID of the newly inserted reminder. Then one can invoke this function whenever a +reminder is inserted into the database with the following trigger: [grdb-add-function]: https://swiftpackageindex.com/groue/grdb.swift/v7.5.0/documentation/grdb/database/add(function:) From 7b81eb46a10afd42d3bf40448c78d0cfbd59d56a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 16 Jun 2025 14:47:39 -0700 Subject: [PATCH 17/32] wip --- .../Documentation.docc/Articles/Triggers.md | 16 +- Sources/StructuredQueriesCore/Triggers.swift | 3 +- .../StructuredQueriesTests/DeleteTests.swift | 17 +- .../StructuredQueriesTests/InsertTests.swift | 46 +-- .../JSONFunctionsTests.swift | 5 +- Tests/StructuredQueriesTests/LiveTests.swift | 43 +-- .../SQLMacroTests.swift | 33 +- .../StructuredQueriesTests/SelectTests.swift | 328 +++++++++--------- .../SelectionTests.swift | 16 +- 9 files changed, 259 insertions(+), 248 deletions(-) diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md b/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md index deeb0723..ad2e7ee1 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md @@ -11,7 +11,7 @@ triggers in a type-safe and schema-safe fashion. ### Trigger basics One of the most common use cases for a trigger is refreshing an "updatedAt" timestamp on a row when -it is updated in the database. One can create such a trigger SQL statement using the +it is updated in the database. One can create such a trigger SQL statement using the ``Table/createTemporaryTrigger(_:ifNotExists:after:fileID:line:column:)`` static method: @Row { @@ -20,8 +20,8 @@ it is updated in the database. One can create such a trigger SQL statement using Reminder.createTemporaryTrigger( "reminders_updatedAt", after: .update { _, _ in - Reminder.update { - $0.updatedAt = #sql("datetime('subsec')") + Reminder.update { + $0.updatedAt = #sql("datetime('subsec')") } } ) @@ -40,7 +40,7 @@ it is updated in the database. One can create such a trigger SQL statement using } } -This will make it so that anytime a reminder is updated in the database its `updatedAt` will be +This will make it so that anytime a reminder is updated in the database its `updatedAt` will be refreshed with the current time immediately. This pattern of updating a timestamp when a row changes is so common that the library comes with @@ -52,7 +52,7 @@ a specialized tool just for that kind of trigger, ```swift Reminder .createTemporaryTrigger( - afterUpdateTouch: { + afterUpdateTouch: { $0.updatedAt = datetime('subsec') } ) @@ -72,7 +72,7 @@ a specialized tool just for that kind of trigger, } And further, the pattern of specifically updating a _timestamp_ column is so common that the library -comes with another specialized too just for that kind of trigger, +comes with another specialized too just for that kind of trigger, ``Table/createTemporaryTrigger(_:ifNotExists:afterUpdateTouch:fileID:line:column:)``: @@ -127,8 +127,8 @@ last list was deleted: "nonEmptyRemindersLists", after: .delete { _ in RemindersList - .insert { - RemindersList.Draft(title: "Personal") + .insert { + RemindersList.Draft(title: "Personal") } } when: { _ in !RemindersList.all.exists() diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index e89f047f..578acf90 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -78,7 +78,6 @@ extension Table { ) } - // TODO: createTemporaryTrigger(afterUpdateTouch: \.updatedAt) // TODO: createTemporaryTrigger(afterUpdate: { $0... }, touch: { $0... = }) // TODO: createTemporaryTrigger(afterUpdate: \.self, touch: \.updatedAt) @@ -97,7 +96,7 @@ public struct TemporaryTrigger: Statement { public typealias Old = TableAlias.TableColumns public typealias New = TableAlias.TableColumns - + /// An `AFTER INSERT` trigger operation. /// /// - Parameters: diff --git a/Tests/StructuredQueriesTests/DeleteTests.swift b/Tests/StructuredQueriesTests/DeleteTests.swift index 157b3fcd..c006b39c 100644 --- a/Tests/StructuredQueriesTests/DeleteTests.swift +++ b/Tests/StructuredQueriesTests/DeleteTests.swift @@ -135,17 +135,18 @@ extension SnapshotTests { """ DELETE FROM "remindersLists" AS "rs" WHERE ("rs"."id" = 1) - RETURNING "id", "color", "title" + RETURNING "id", "color", "title", "position" """ } results: { """ - ┌─────────────────────┐ - │ RemindersList( │ - │ id: 1, │ - │ color: 4889071, │ - │ title: "Personal" │ - │ ) │ - └─────────────────────┘ + ┌──────────────────────┐ + │ RemindersList( │ + │ id: 1, │ + │ color: 4889071, │ + │ title: "Personal", │ + │ position: 0 │ + │ ) │ + └──────────────────────┘ """ } } diff --git a/Tests/StructuredQueriesTests/InsertTests.swift b/Tests/StructuredQueriesTests/InsertTests.swift index c7406866..52b8e285 100644 --- a/Tests/StructuredQueriesTests/InsertTests.swift +++ b/Tests/StructuredQueriesTests/InsertTests.swift @@ -411,12 +411,12 @@ extension SnapshotTests { ) { """ INSERT INTO "remindersLists" - ("id", "color", "title") + ("id", "color", "title", "position") VALUES - (NULL, 4889071, 'Personal') + (NULL, 4889071, 'Personal', 0) ON CONFLICT ("id") - DO UPDATE SET "color" = "excluded"."color", "title" = "excluded"."title" - RETURNING "id", "color", "title" + DO UPDATE SET "color" = "excluded"."color", "title" = "excluded"."title", "position" = "excluded"."position" + RETURNING "id", "color", "title", "position" """ } results: { """ @@ -437,22 +437,23 @@ extension SnapshotTests { ) { """ INSERT INTO "remindersLists" - ("id", "color", "title") + ("id", "color", "title", "position") VALUES - (NULL, 4889071, 'Personal') + (NULL, 4889071, 'Personal', 0) ON CONFLICT ("title") DO UPDATE SET "color" = 65280 - RETURNING "id", "color", "title" + RETURNING "id", "color", "title", "position" """ } results: { """ - ┌─────────────────────┐ - │ RemindersList( │ - │ id: 1, │ - │ color: 65280, │ - │ title: "Personal" │ - │ ) │ - └─────────────────────┘ + ┌──────────────────────┐ + │ RemindersList( │ + │ id: 1, │ + │ color: 65280, │ + │ title: "Personal", │ + │ position: 0 │ + │ ) │ + └──────────────────────┘ """ } } @@ -525,17 +526,18 @@ extension SnapshotTests { ("title") VALUES ('cruise') - RETURNING "id", "color", "title" + RETURNING "id", "color", "title", "position" """ } results: { """ - ┌───────────────────┐ - │ RemindersList( │ - │ id: 4, │ - │ color: 4889071, │ - │ title: "cruise" │ - │ ) │ - └───────────────────┘ + ┌────────────────────┐ + │ RemindersList( │ + │ id: 4, │ + │ color: 4889071, │ + │ title: "cruise", │ + │ position: 0 │ + │ ) │ + └────────────────────┘ """ } } diff --git a/Tests/StructuredQueriesTests/JSONFunctionsTests.swift b/Tests/StructuredQueriesTests/JSONFunctionsTests.swift index ddbf57ae..05ad33f3 100644 --- a/Tests/StructuredQueriesTests/JSONFunctionsTests.swift +++ b/Tests/StructuredQueriesTests/JSONFunctionsTests.swift @@ -224,7 +224,7 @@ extension SnapshotTests { .limit(1) ) { """ - SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title" AS "remindersList", json_group_array(CASE WHEN ("reminders"."id" IS NOT NULL) THEN json_object('id', json_quote("reminders"."id"), 'assignedUserID', json_quote("reminders"."assignedUserID"), 'dueDate', json_quote("reminders"."dueDate"), 'isCompleted', json(CASE "reminders"."isCompleted" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'isFlagged', json(CASE "reminders"."isFlagged" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'notes', json_quote("reminders"."notes"), 'priority', json_quote("reminders"."priority"), 'remindersListID', json_quote("reminders"."remindersListID"), 'title', json_quote("reminders"."title")) END) AS "reminders" + SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position" AS "remindersList", json_group_array(CASE WHEN ("reminders"."id" IS NOT NULL) THEN json_object('id', json_quote("reminders"."id"), 'assignedUserID', json_quote("reminders"."assignedUserID"), 'dueDate', json_quote("reminders"."dueDate"), 'isCompleted', json(CASE "reminders"."isCompleted" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'isFlagged', json(CASE "reminders"."isFlagged" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'notes', json_quote("reminders"."notes"), 'priority', json_quote("reminders"."priority"), 'remindersListID', json_quote("reminders"."remindersListID"), 'title', json_quote("reminders"."title")) END) AS "reminders" FROM "remindersLists" LEFT JOIN "reminders" ON ("remindersLists"."id" = "reminders"."remindersListID") WHERE NOT ("reminders"."isCompleted") @@ -238,7 +238,8 @@ extension SnapshotTests { │ remindersList: RemindersList( │ │ id: 1, │ │ color: 4889071, │ - │ title: "Personal" │ + │ title: "Personal", │ + │ position: 0 │ │ ), │ │ reminders: [ │ │ [0]: Reminder( │ diff --git a/Tests/StructuredQueriesTests/LiveTests.swift b/Tests/StructuredQueriesTests/LiveTests.swift index 05c98f9d..4ed9196b 100644 --- a/Tests/StructuredQueriesTests/LiveTests.swift +++ b/Tests/StructuredQueriesTests/LiveTests.swift @@ -184,32 +184,35 @@ extension SnapshotTests { .select { ($0, $1.id.count()) } ) { """ - SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", count("reminders"."id") + SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position", count("reminders"."id") FROM "remindersLists" JOIN "reminders" ON ("remindersLists"."id" = "reminders"."remindersListID") GROUP BY "remindersLists"."id" """ } results: { """ - ┌─────────────────────┬───┐ - │ RemindersList( │ 5 │ - │ id: 1, │ │ - │ color: 4889071, │ │ - │ title: "Personal" │ │ - │ ) │ │ - ├─────────────────────┼───┤ - │ RemindersList( │ 3 │ - │ id: 2, │ │ - │ color: 15567157, │ │ - │ title: "Family" │ │ - │ ) │ │ - ├─────────────────────┼───┤ - │ RemindersList( │ 2 │ - │ id: 3, │ │ - │ color: 11689427, │ │ - │ title: "Business" │ │ - │ ) │ │ - └─────────────────────┴───┘ + ┌──────────────────────┬───┐ + │ RemindersList( │ 5 │ + │ id: 1, │ │ + │ color: 4889071, │ │ + │ title: "Personal", │ │ + │ position: 0 │ │ + │ ) │ │ + ├──────────────────────┼───┤ + │ RemindersList( │ 3 │ + │ id: 2, │ │ + │ color: 15567157, │ │ + │ title: "Family", │ │ + │ position: 0 │ │ + │ ) │ │ + ├──────────────────────┼───┤ + │ RemindersList( │ 2 │ + │ id: 3, │ │ + │ color: 11689427, │ │ + │ title: "Business", │ │ + │ position: 0 │ │ + │ ) │ │ + └──────────────────────┴───┘ """ } } diff --git a/Tests/StructuredQueriesTests/SQLMacroTests.swift b/Tests/StructuredQueriesTests/SQLMacroTests.swift index c6706585..2ad29850 100644 --- a/Tests/StructuredQueriesTests/SQLMacroTests.swift +++ b/Tests/StructuredQueriesTests/SQLMacroTests.swift @@ -60,7 +60,7 @@ extension SnapshotTests { """ SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", - "remindersLists"."id", "remindersLists"."color", "remindersLists"."title" + "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position" FROM "reminders" JOIN "remindersLists" ON "reminders"."remindersListID" = "remindersLists"."id" @@ -68,19 +68,19 @@ extension SnapshotTests { """ } results: { """ - ┌────────────────────────────────────────────┬─────────────────────┐ - │ Reminder( │ RemindersList( │ - │ id: 1, │ id: 1, │ - │ assignedUserID: 1, │ color: 4889071, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ title: "Personal" │ - │ isCompleted: false, │ ) │ - │ isFlagged: false, │ │ - │ notes: "Milk, Eggs, Apples", │ │ - │ priority: nil, │ │ - │ remindersListID: 1, │ │ - │ title: "Groceries" │ │ - │ ) │ │ - └────────────────────────────────────────────┴─────────────────────┘ + ┌────────────────────────────────────────────┬──────────────────────┐ + │ Reminder( │ RemindersList( │ + │ id: 1, │ id: 1, │ + │ assignedUserID: 1, │ color: 4889071, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ title: "Personal", │ + │ isCompleted: false, │ position: 0 │ + │ isFlagged: false, │ ) │ + │ notes: "Milk, Eggs, Apples", │ │ + │ priority: nil, │ │ + │ remindersListID: 1, │ │ + │ title: "Groceries" │ │ + │ ) │ │ + └────────────────────────────────────────────┴──────────────────────┘ """ } } @@ -99,7 +99,7 @@ extension SnapshotTests { ) ) { """ - SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "remindersLists"."id", "remindersLists"."color", "remindersLists"."title" + SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position" FROM "reminders" JOIN "remindersLists" ON "reminders"."remindersListID" = "remindersLists"."id" LIMIT 1 """ } results: { @@ -120,7 +120,8 @@ extension SnapshotTests { │ list: RemindersList( │ │ id: 1, │ │ color: 4889071, │ - │ title: "Personal" │ + │ title: "Personal", │ + │ position: 0 │ │ ) │ │ ) │ └──────────────────────────────────────────────┘ diff --git a/Tests/StructuredQueriesTests/SelectTests.swift b/Tests/StructuredQueriesTests/SelectTests.swift index 03f1f404..40dc18ed 100644 --- a/Tests/StructuredQueriesTests/SelectTests.swift +++ b/Tests/StructuredQueriesTests/SelectTests.swift @@ -169,137 +169,137 @@ extension SnapshotTests { .join(RemindersList.all) { $0.remindersListID.eq($1.id) } ) { """ - SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "remindersLists"."id", "remindersLists"."color", "remindersLists"."title" + SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position" FROM "reminders" JOIN "remindersLists" ON ("reminders"."remindersListID" = "remindersLists"."id") """ } results: { #""" - ┌────────────────────────────────────────────┬─────────────────────┐ - │ Reminder( │ RemindersList( │ - │ id: 1, │ id: 1, │ - │ assignedUserID: 1, │ color: 4889071, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ title: "Personal" │ - │ isCompleted: false, │ ) │ - │ isFlagged: false, │ │ - │ notes: "Milk, Eggs, Apples", │ │ - │ priority: nil, │ │ - │ remindersListID: 1, │ │ - │ title: "Groceries" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼─────────────────────┤ - │ Reminder( │ RemindersList( │ - │ id: 2, │ id: 1, │ - │ assignedUserID: nil, │ color: 4889071, │ - │ dueDate: Date(2000-12-30T00:00:00.000Z), │ title: "Personal" │ - │ isCompleted: false, │ ) │ - │ isFlagged: true, │ │ - │ notes: "", │ │ - │ priority: nil, │ │ - │ remindersListID: 1, │ │ - │ title: "Haircut" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼─────────────────────┤ - │ Reminder( │ RemindersList( │ - │ id: 3, │ id: 1, │ - │ assignedUserID: nil, │ color: 4889071, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ title: "Personal" │ - │ isCompleted: false, │ ) │ - │ isFlagged: false, │ │ - │ notes: "Ask about diet", │ │ - │ priority: .high, │ │ - │ remindersListID: 1, │ │ - │ title: "Doctor appointment" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼─────────────────────┤ - │ Reminder( │ RemindersList( │ - │ id: 4, │ id: 1, │ - │ assignedUserID: nil, │ color: 4889071, │ - │ dueDate: Date(2000-06-25T00:00:00.000Z), │ title: "Personal" │ - │ isCompleted: true, │ ) │ - │ isFlagged: false, │ │ - │ notes: "", │ │ - │ priority: nil, │ │ - │ remindersListID: 1, │ │ - │ title: "Take a walk" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼─────────────────────┤ - │ Reminder( │ RemindersList( │ - │ id: 5, │ id: 1, │ - │ assignedUserID: nil, │ color: 4889071, │ - │ dueDate: nil, │ title: "Personal" │ - │ isCompleted: false, │ ) │ - │ isFlagged: false, │ │ - │ notes: "", │ │ - │ priority: nil, │ │ - │ remindersListID: 1, │ │ - │ title: "Buy concert tickets" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼─────────────────────┤ - │ Reminder( │ RemindersList( │ - │ id: 6, │ id: 2, │ - │ assignedUserID: nil, │ color: 15567157, │ - │ dueDate: Date(2001-01-03T00:00:00.000Z), │ title: "Family" │ - │ isCompleted: false, │ ) │ - │ isFlagged: true, │ │ - │ notes: "", │ │ - │ priority: .high, │ │ - │ remindersListID: 2, │ │ - │ title: "Pick up kids from school" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼─────────────────────┤ - │ Reminder( │ RemindersList( │ - │ id: 7, │ id: 2, │ - │ assignedUserID: nil, │ color: 15567157, │ - │ dueDate: Date(2000-12-30T00:00:00.000Z), │ title: "Family" │ - │ isCompleted: true, │ ) │ - │ isFlagged: false, │ │ - │ notes: "", │ │ - │ priority: .low, │ │ - │ remindersListID: 2, │ │ - │ title: "Get laundry" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼─────────────────────┤ - │ Reminder( │ RemindersList( │ - │ id: 8, │ id: 2, │ - │ assignedUserID: nil, │ color: 15567157, │ - │ dueDate: Date(2001-01-05T00:00:00.000Z), │ title: "Family" │ - │ isCompleted: false, │ ) │ - │ isFlagged: false, │ │ - │ notes: "", │ │ - │ priority: .high, │ │ - │ remindersListID: 2, │ │ - │ title: "Take out trash" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼─────────────────────┤ - │ Reminder( │ RemindersList( │ - │ id: 9, │ id: 3, │ - │ assignedUserID: nil, │ color: 11689427, │ - │ dueDate: Date(2001-01-03T00:00:00.000Z), │ title: "Business" │ - │ isCompleted: false, │ ) │ - │ isFlagged: false, │ │ - │ notes: """ │ │ - │ Status of tax return │ │ - │ Expenses for next year │ │ - │ Changing payroll company │ │ - │ """, │ │ - │ priority: nil, │ │ - │ remindersListID: 3, │ │ - │ title: "Call accountant" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼─────────────────────┤ - │ Reminder( │ RemindersList( │ - │ id: 10, │ id: 3, │ - │ assignedUserID: nil, │ color: 11689427, │ - │ dueDate: Date(2000-12-30T00:00:00.000Z), │ title: "Business" │ - │ isCompleted: true, │ ) │ - │ isFlagged: false, │ │ - │ notes: "", │ │ - │ priority: .medium, │ │ - │ remindersListID: 3, │ │ - │ title: "Send weekly emails" │ │ - │ ) │ │ - └────────────────────────────────────────────┴─────────────────────┘ + ┌────────────────────────────────────────────┬──────────────────────┐ + │ Reminder( │ RemindersList( │ + │ id: 1, │ id: 1, │ + │ assignedUserID: 1, │ color: 4889071, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ title: "Personal", │ + │ isCompleted: false, │ position: 0 │ + │ isFlagged: false, │ ) │ + │ notes: "Milk, Eggs, Apples", │ │ + │ priority: nil, │ │ + │ remindersListID: 1, │ │ + │ title: "Groceries" │ │ + │ ) │ │ + ├────────────────────────────────────────────┼──────────────────────┤ + │ Reminder( │ RemindersList( │ + │ id: 2, │ id: 1, │ + │ assignedUserID: nil, │ color: 4889071, │ + │ dueDate: Date(2000-12-30T00:00:00.000Z), │ title: "Personal", │ + │ isCompleted: false, │ position: 0 │ + │ isFlagged: true, │ ) │ + │ notes: "", │ │ + │ priority: nil, │ │ + │ remindersListID: 1, │ │ + │ title: "Haircut" │ │ + │ ) │ │ + ├────────────────────────────────────────────┼──────────────────────┤ + │ Reminder( │ RemindersList( │ + │ id: 3, │ id: 1, │ + │ assignedUserID: nil, │ color: 4889071, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ title: "Personal", │ + │ isCompleted: false, │ position: 0 │ + │ isFlagged: false, │ ) │ + │ notes: "Ask about diet", │ │ + │ priority: .high, │ │ + │ remindersListID: 1, │ │ + │ title: "Doctor appointment" │ │ + │ ) │ │ + ├────────────────────────────────────────────┼──────────────────────┤ + │ Reminder( │ RemindersList( │ + │ id: 4, │ id: 1, │ + │ assignedUserID: nil, │ color: 4889071, │ + │ dueDate: Date(2000-06-25T00:00:00.000Z), │ title: "Personal", │ + │ isCompleted: true, │ position: 0 │ + │ isFlagged: false, │ ) │ + │ notes: "", │ │ + │ priority: nil, │ │ + │ remindersListID: 1, │ │ + │ title: "Take a walk" │ │ + │ ) │ │ + ├────────────────────────────────────────────┼──────────────────────┤ + │ Reminder( │ RemindersList( │ + │ id: 5, │ id: 1, │ + │ assignedUserID: nil, │ color: 4889071, │ + │ dueDate: nil, │ title: "Personal", │ + │ isCompleted: false, │ position: 0 │ + │ isFlagged: false, │ ) │ + │ notes: "", │ │ + │ priority: nil, │ │ + │ remindersListID: 1, │ │ + │ title: "Buy concert tickets" │ │ + │ ) │ │ + ├────────────────────────────────────────────┼──────────────────────┤ + │ Reminder( │ RemindersList( │ + │ id: 6, │ id: 2, │ + │ assignedUserID: nil, │ color: 15567157, │ + │ dueDate: Date(2001-01-03T00:00:00.000Z), │ title: "Family", │ + │ isCompleted: false, │ position: 0 │ + │ isFlagged: true, │ ) │ + │ notes: "", │ │ + │ priority: .high, │ │ + │ remindersListID: 2, │ │ + │ title: "Pick up kids from school" │ │ + │ ) │ │ + ├────────────────────────────────────────────┼──────────────────────┤ + │ Reminder( │ RemindersList( │ + │ id: 7, │ id: 2, │ + │ assignedUserID: nil, │ color: 15567157, │ + │ dueDate: Date(2000-12-30T00:00:00.000Z), │ title: "Family", │ + │ isCompleted: true, │ position: 0 │ + │ isFlagged: false, │ ) │ + │ notes: "", │ │ + │ priority: .low, │ │ + │ remindersListID: 2, │ │ + │ title: "Get laundry" │ │ + │ ) │ │ + ├────────────────────────────────────────────┼──────────────────────┤ + │ Reminder( │ RemindersList( │ + │ id: 8, │ id: 2, │ + │ assignedUserID: nil, │ color: 15567157, │ + │ dueDate: Date(2001-01-05T00:00:00.000Z), │ title: "Family", │ + │ isCompleted: false, │ position: 0 │ + │ isFlagged: false, │ ) │ + │ notes: "", │ │ + │ priority: .high, │ │ + │ remindersListID: 2, │ │ + │ title: "Take out trash" │ │ + │ ) │ │ + ├────────────────────────────────────────────┼──────────────────────┤ + │ Reminder( │ RemindersList( │ + │ id: 9, │ id: 3, │ + │ assignedUserID: nil, │ color: 11689427, │ + │ dueDate: Date(2001-01-03T00:00:00.000Z), │ title: "Business", │ + │ isCompleted: false, │ position: 0 │ + │ isFlagged: false, │ ) │ + │ notes: """ │ │ + │ Status of tax return │ │ + │ Expenses for next year │ │ + │ Changing payroll company │ │ + │ """, │ │ + │ priority: nil, │ │ + │ remindersListID: 3, │ │ + │ title: "Call accountant" │ │ + │ ) │ │ + ├────────────────────────────────────────────┼──────────────────────┤ + │ Reminder( │ RemindersList( │ + │ id: 10, │ id: 3, │ + │ assignedUserID: nil, │ color: 11689427, │ + │ dueDate: Date(2000-12-30T00:00:00.000Z), │ title: "Business", │ + │ isCompleted: true, │ position: 0 │ + │ isFlagged: false, │ ) │ + │ notes: "", │ │ + │ priority: .medium, │ │ + │ remindersListID: 3, │ │ + │ title: "Send weekly emails" │ │ + │ ) │ │ + └────────────────────────────────────────────┴──────────────────────┘ """# } @@ -1123,50 +1123,50 @@ extension SnapshotTests { .where { $1.isHighPriority.ifnull(false) } ) { """ - SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title" + SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position", "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title" FROM "remindersLists" LEFT JOIN "reminders" ON ("remindersLists"."id" = "reminders"."remindersListID") WHERE ifnull(("reminders"."priority" = 3), 0) """ } results: { """ - ┌─────────────────────┬────────────────────────────────────────────┐ - │ RemindersList( │ Reminder( │ - │ id: 1, │ id: 3, │ - │ color: 4889071, │ assignedUserID: nil, │ - │ title: "Personal" │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ ) │ isCompleted: false, │ - │ │ isFlagged: false, │ - │ │ notes: "Ask about diet", │ - │ │ priority: .high, │ - │ │ remindersListID: 1, │ - │ │ title: "Doctor appointment" │ - │ │ ) │ - ├─────────────────────┼────────────────────────────────────────────┤ - │ RemindersList( │ Reminder( │ - │ id: 2, │ id: 6, │ - │ color: 15567157, │ assignedUserID: nil, │ - │ title: "Family" │ dueDate: Date(2001-01-03T00:00:00.000Z), │ - │ ) │ isCompleted: false, │ - │ │ isFlagged: true, │ - │ │ notes: "", │ - │ │ priority: .high, │ - │ │ remindersListID: 2, │ - │ │ title: "Pick up kids from school" │ - │ │ ) │ - ├─────────────────────┼────────────────────────────────────────────┤ - │ RemindersList( │ Reminder( │ - │ id: 2, │ id: 8, │ - │ color: 15567157, │ assignedUserID: nil, │ - │ title: "Family" │ dueDate: Date(2001-01-05T00:00:00.000Z), │ - │ ) │ isCompleted: false, │ - │ │ isFlagged: false, │ - │ │ notes: "", │ - │ │ priority: .high, │ - │ │ remindersListID: 2, │ - │ │ title: "Take out trash" │ - │ │ ) │ - └─────────────────────┴────────────────────────────────────────────┘ + ┌──────────────────────┬────────────────────────────────────────────┐ + │ RemindersList( │ Reminder( │ + │ id: 1, │ id: 3, │ + │ color: 4889071, │ assignedUserID: nil, │ + │ title: "Personal", │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ position: 0 │ isCompleted: false, │ + │ ) │ isFlagged: false, │ + │ │ notes: "Ask about diet", │ + │ │ priority: .high, │ + │ │ remindersListID: 1, │ + │ │ title: "Doctor appointment" │ + │ │ ) │ + ├──────────────────────┼────────────────────────────────────────────┤ + │ RemindersList( │ Reminder( │ + │ id: 2, │ id: 6, │ + │ color: 15567157, │ assignedUserID: nil, │ + │ title: "Family", │ dueDate: Date(2001-01-03T00:00:00.000Z), │ + │ position: 0 │ isCompleted: false, │ + │ ) │ isFlagged: true, │ + │ │ notes: "", │ + │ │ priority: .high, │ + │ │ remindersListID: 2, │ + │ │ title: "Pick up kids from school" │ + │ │ ) │ + ├──────────────────────┼────────────────────────────────────────────┤ + │ RemindersList( │ Reminder( │ + │ id: 2, │ id: 8, │ + │ color: 15567157, │ assignedUserID: nil, │ + │ title: "Family", │ dueDate: Date(2001-01-05T00:00:00.000Z), │ + │ position: 0 │ isCompleted: false, │ + │ ) │ isFlagged: false, │ + │ │ notes: "", │ + │ │ priority: .high, │ + │ │ remindersListID: 2, │ + │ │ title: "Take out trash" │ + │ │ ) │ + └──────────────────────┴────────────────────────────────────────────┘ """ } } diff --git a/Tests/StructuredQueriesTests/SelectionTests.swift b/Tests/StructuredQueriesTests/SelectionTests.swift index f02e5818..daef8585 100644 --- a/Tests/StructuredQueriesTests/SelectionTests.swift +++ b/Tests/StructuredQueriesTests/SelectionTests.swift @@ -19,7 +19,7 @@ extension SnapshotTests { } ) { """ - SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title" AS "remindersList", count("reminders"."id") AS "remindersCount" + SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position" AS "remindersList", count("reminders"."id") AS "remindersCount" FROM "remindersLists" JOIN "reminders" ON ("remindersLists"."id" = "reminders"."remindersListID") GROUP BY "remindersLists"."id" @@ -32,7 +32,8 @@ extension SnapshotTests { │ remindersList: RemindersList( │ │ id: 1, │ │ color: 4889071, │ - │ title: "Personal" │ + │ title: "Personal", │ + │ position: 0 │ │ ), │ │ remindersCount: 5 │ │ ) │ @@ -41,7 +42,8 @@ extension SnapshotTests { │ remindersList: RemindersList( │ │ id: 2, │ │ color: 15567157, │ - │ title: "Family" │ + │ title: "Family", │ + │ position: 0 │ │ ), │ │ remindersCount: 3 │ │ ) │ @@ -54,7 +56,7 @@ extension SnapshotTests { .map { RemindersListAndReminderCount.Columns(remindersList: $1, remindersCount: $0) } ) { """ - SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title" AS "remindersList", count("reminders"."id") AS "remindersCount" + SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position" AS "remindersList", count("reminders"."id") AS "remindersCount" FROM "remindersLists" JOIN "reminders" ON ("remindersLists"."id" = "reminders"."remindersListID") GROUP BY "remindersLists"."id" @@ -67,7 +69,8 @@ extension SnapshotTests { │ remindersList: RemindersList( │ │ id: 1, │ │ color: 4889071, │ - │ title: "Personal" │ + │ title: "Personal", │ + │ position: 0 │ │ ), │ │ remindersCount: 5 │ │ ) │ @@ -76,7 +79,8 @@ extension SnapshotTests { │ remindersList: RemindersList( │ │ id: 2, │ │ color: 15567157, │ - │ title: "Family" │ + │ title: "Family", │ + │ position: 0 │ │ ), │ │ remindersCount: 3 │ │ ) │ From a3de95c5f6eca051ec8581d9242573ca4a6ed8af Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 17 Jun 2025 16:08:21 -0700 Subject: [PATCH 18/32] find update remove later --- Sources/StructuredQueriesCore/PrimaryKeyed.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/StructuredQueriesCore/PrimaryKeyed.swift b/Sources/StructuredQueriesCore/PrimaryKeyed.swift index 1adfb037..67f7e264 100644 --- a/Sources/StructuredQueriesCore/PrimaryKeyed.swift +++ b/Sources/StructuredQueriesCore/PrimaryKeyed.swift @@ -81,8 +81,8 @@ extension PrimaryKeyedTable { /// /// - Parameter primaryKey: A primary key identifying a table row. /// - Returns: A `WHERE` clause. - public static func find(_ primaryKey: TableColumns.PrimaryKey.QueryOutput) -> Where { - Self.where { $0.primaryKey.eq(TableColumns.PrimaryKey(queryOutput: primaryKey)) } + public static func find(_ primaryKey: some QueryExpression) -> Where { + Self.where { $0.primaryKey.eq(primaryKey) } } } @@ -131,7 +131,7 @@ extension Select where From: PrimaryKeyedTable { /// /// - Parameter primaryKey: A primary key identifying a table row. /// - Returns: A select statement filtered by the given key. - public func find(_ primaryKey: From.TableColumns.PrimaryKey.QueryOutput) -> Self { + public func find(_ primaryKey: some QueryExpression) -> Self { self.and(From.find(primaryKey)) } } From 09a677da3fd43221cb320998246b9eba2f191c8c Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 17 Jun 2025 16:10:15 -0700 Subject: [PATCH 19/32] Revert "find update remove later" This reverts commit a3de95c5f6eca051ec8581d9242573ca4a6ed8af. --- Sources/StructuredQueriesCore/PrimaryKeyed.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/StructuredQueriesCore/PrimaryKeyed.swift b/Sources/StructuredQueriesCore/PrimaryKeyed.swift index 67f7e264..1adfb037 100644 --- a/Sources/StructuredQueriesCore/PrimaryKeyed.swift +++ b/Sources/StructuredQueriesCore/PrimaryKeyed.swift @@ -81,8 +81,8 @@ extension PrimaryKeyedTable { /// /// - Parameter primaryKey: A primary key identifying a table row. /// - Returns: A `WHERE` clause. - public static func find(_ primaryKey: some QueryExpression) -> Where { - Self.where { $0.primaryKey.eq(primaryKey) } + public static func find(_ primaryKey: TableColumns.PrimaryKey.QueryOutput) -> Where { + Self.where { $0.primaryKey.eq(TableColumns.PrimaryKey(queryOutput: primaryKey)) } } } @@ -131,7 +131,7 @@ extension Select where From: PrimaryKeyedTable { /// /// - Parameter primaryKey: A primary key identifying a table row. /// - Returns: A select statement filtered by the given key. - public func find(_ primaryKey: some QueryExpression) -> Self { + public func find(_ primaryKey: From.TableColumns.PrimaryKey.QueryOutput) -> Self { self.and(From.find(primaryKey)) } } From e94b3fafc4f0ece4e84606e3e4ecd3970f79d27b Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 17 Jun 2025 20:32:04 -0700 Subject: [PATCH 20/32] wip --- Sources/StructuredQueriesCore/Triggers.swift | 93 ++- .../CommonTableExpressionTests.swift | 22 +- .../StructuredQueriesTests/DeleteTests.swift | 35 +- .../StructuredQueriesTests/InsertTests.swift | 226 +++--- .../JSONFunctionsTests.swift | 268 +++---- Tests/StructuredQueriesTests/LiveTests.swift | 345 ++++----- .../OperatorsTests.swift | 91 +-- .../SQLMacroTests.swift | 113 +-- .../StructuredQueriesTests/SelectTests.swift | 653 +++++++++--------- .../Support/Schema.swift | 4 +- .../TriggersTests.swift | 38 + .../StructuredQueriesTests/UpdateTests.swift | 74 +- Tests/StructuredQueriesTests/WhereTests.swift | 2 +- 13 files changed, 1043 insertions(+), 921 deletions(-) diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index 578acf90..af1e34f0 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -2,9 +2,11 @@ import Foundation import IssueReporting extension Table { - /// A `CREATE TEMPORARY TRIGGER` statement. + /// A `CREATE TEMPORARY TRIGGER` statement that executes after a database event. /// - /// > Important: TODO: explain how implicit names are handled and how trigger helpers should always take file/line/column. and put in name/file/line/column parameters. + /// > Important: A name for the trigger is automatically derived from the arguments if one is not + /// > provided. If you build your own trigger helper that call this function, then your helper + /// > should also take fileID, line and column arguments and pass them to this function. /// /// - Parameters: /// - name: The trigger's name. By default a unique name is generated depending using the table, @@ -27,14 +29,63 @@ extension Table { name: name, ifNotExists: ifNotExists, operation: operation, + when: .after, fileID: fileID, line: line, column: column ) } - // TODO: write tests on these below: + /// A `CREATE TEMPORARY TRIGGER` statement that executes before a database event. + /// + /// > Important: A name for the trigger is automatically derived from the arguments if one is not + /// > provided. If you build your own trigger helper that call this function, then your helper + /// > should also take fileID, line and column arguments and pass them to this function. + /// + /// - Parameters: + /// - name: The trigger's name. By default a unique name is generated depending using the table, + /// operation, and source location. + /// - ifNotExists: Adds an `IF NOT EXISTS` clause to the `CREATE TRIGGER` statement. + /// - operation: The trigger's operation. + /// - fileID: The source `#fileID` associated with the trigger. + /// - line: The source `#line` associated with the trigger. + /// - column: The source `#column` associated with the trigger. + /// - Returns: A temporary trigger. + public static func createTemporaryTrigger( + _ name: String? = nil, + ifNotExists: Bool = false, + before operation: TemporaryTrigger.Operation, + fileID: StaticString = #fileID, + line: UInt = #line, + column: UInt = #column + ) -> TemporaryTrigger { + TemporaryTrigger( + name: name, + ifNotExists: ifNotExists, + operation: operation, + when: .before, + fileID: fileID, + line: line, + column: column + ) + } + /// A `CREATE TEMPORARY TRIGGER` statement that applies additional updates to a row that has just + /// been updated. + /// + /// > Important: A name for the trigger is automatically derived from the arguments if one is not + /// > provided. If you build your own trigger helper that call this function, then your helper + /// > should also take fileID, line and column arguments and pass them to this function. + /// + /// - Parameters: + /// - name: The trigger's name. By default a unique name is generated depending using the table, + /// operation, and source location. + /// - ifNotExists: Adds an `IF NOT EXISTS` clause to the `CREATE TRIGGER` statement. + /// - updates: The updates to apply after the row has been updated. + /// - fileID: The source `#fileID` associated with the trigger. + /// - line: The source `#line` associated with the trigger. + /// - column: The source `#column` associated with the trigger. + /// - Returns: A temporary trigger. public static func createTemporaryTrigger( _ name: String? = nil, ifNotExists: Bool = false, @@ -57,7 +108,22 @@ extension Table { ) } - // TODO: Touchable protocol with Date: Touchable, UUID: Touchable, ? + /// A `CREATE TEMPORARY TRIGGER` statement that updates a datetime column when a row has been updated. + /// been updated. + /// + /// > Important: A name for the trigger is automatically derived from the arguments if one is not + /// > provided. If you build your own trigger helper that call this function, then your helper + /// > should also take fileID, line and column arguments and pass them to this function. + /// + /// - Parameters: + /// - name: The trigger's name. By default a unique name is generated depending using the table, + /// operation, and source location. + /// - ifNotExists: Adds an `IF NOT EXISTS` clause to the `CREATE TRIGGER` statement. + /// - date: A key path to a datetime column. + /// - fileID: The source `#fileID` associated with the trigger. + /// - line: The source `#line` associated with the trigger. + /// - column: The source `#column` associated with the trigger. + /// - Returns: A temporary trigger. public static func createTemporaryTrigger( _ name: String? = nil, ifNotExists: Bool = false, @@ -77,10 +143,6 @@ extension Table { column: column ) } - - // TODO: createTemporaryTrigger(afterUpdateTouch: \.updatedAt) - // TODO: createTemporaryTrigger(afterUpdate: { $0... }, touch: { $0... = }) - // TODO: createTemporaryTrigger(afterUpdate: \.self, touch: \.updatedAt) } public struct TemporaryTrigger: Statement { @@ -88,6 +150,11 @@ public struct TemporaryTrigger: Statement { public typealias Joins = () public typealias QueryValue = () + fileprivate enum When: String { + case before = "BEFORE" + case after = "AFTER" + } + public struct Operation: QueryExpression { public typealias QueryValue = () @@ -181,14 +248,14 @@ public struct TemporaryTrigger: Statement { private let when: QueryFragment? public var queryFragment: QueryFragment { - var query: QueryFragment = "AFTER" + var query: QueryFragment = "" let statement: QueryFragment switch kind { case .insert(let begin): - query.append(" INSERT") + query.append("INSERT") statement = begin case .update(let begin, let columnNames): - query.append(" UPDATE") + query.append("UPDATE") if !columnNames.isEmpty { query.append( " OF \(columnNames.map { QueryFragment(quote: $0) }.joined(separator: ", "))" @@ -220,6 +287,7 @@ public struct TemporaryTrigger: Statement { fileprivate let name: String? fileprivate let ifNotExists: Bool fileprivate let operation: Operation + fileprivate let when: When fileprivate let fileID: StaticString fileprivate let line: UInt fileprivate let column: UInt @@ -242,7 +310,8 @@ public struct TemporaryTrigger: Statement { if ifNotExists { query.append(" IF NOT EXISTS") } - query.append("\(.newlineOrSpace)\(triggerName.indented())\(.newlineOrSpace)\(operation)") + query.append("\(.newlineOrSpace)\(triggerName.indented())") + query.append("\(.newlineOrSpace)\(raw: when.rawValue) \(operation)") return query.segments.reduce(into: QueryFragment()) { switch $1 { case .sql(let sql): diff --git a/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift b/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift index 0c0b9ec3..4434bad3 100644 --- a/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift +++ b/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift @@ -108,7 +108,7 @@ extension SnapshotTests { .select { ($1.remindersListID, $0.title, !$0.isFlagged, true) } .limit(1) } - .returning(\.self) + .returning { ($0.id, $0.title) } } ) { """ @@ -123,23 +123,13 @@ extension SnapshotTests { FROM "incompleteReminders" JOIN "reminders" ON ("incompleteReminders"."title" = "reminders"."title") LIMIT 1 - RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title" + RETURNING "id", "title" """ - } results: { + }results: { """ - ┌────────────────────────┐ - │ Reminder( │ - │ id: 11, │ - │ assignedUserID: nil, │ - │ dueDate: nil, │ - │ isCompleted: true, │ - │ isFlagged: true, │ - │ notes: "", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Groceries" │ - │ ) │ - └────────────────────────┘ + ┌────┬─────────────┐ + │ 11 │ "Groceries" │ + └────┴─────────────┘ """ } } diff --git a/Tests/StructuredQueriesTests/DeleteTests.swift b/Tests/StructuredQueriesTests/DeleteTests.swift index c006b39c..a34e49b3 100644 --- a/Tests/StructuredQueriesTests/DeleteTests.swift +++ b/Tests/StructuredQueriesTests/DeleteTests.swift @@ -47,23 +47,24 @@ extension SnapshotTests { """ DELETE FROM "reminders" WHERE ("reminders"."id" = 1) - RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title" - """ - } results: { - """ - ┌────────────────────────────────────────────┐ - │ Reminder( │ - │ id: 1, │ - │ assignedUserID: 1, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "Milk, Eggs, Apples", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Groceries" │ - │ ) │ - └────────────────────────────────────────────┘ + RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt" + """ + }results: { + """ + ┌─────────────────────────────────────────────┐ + │ Reminder( │ + │ id: 1, │ + │ assignedUserID: 1, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "Milk, Eggs, Apples", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Groceries", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + └─────────────────────────────────────────────┘ """ } assertQuery(Reminder.count()) { diff --git a/Tests/StructuredQueriesTests/InsertTests.swift b/Tests/StructuredQueriesTests/InsertTests.swift index 52b8e285..5136cabe 100644 --- a/Tests/StructuredQueriesTests/InsertTests.swift +++ b/Tests/StructuredQueriesTests/InsertTests.swift @@ -16,7 +16,7 @@ extension SnapshotTests { } onConflictDoUpdate: { $0.title += " Copy" } - .returning(\.self) + .returning(\.id) ) { """ INSERT INTO "reminders" @@ -24,35 +24,14 @@ extension SnapshotTests { VALUES (1, 'Groceries', 1, '2001-01-01 00:00:00.000', 3), (2, 'Haircut', 0, '1970-01-01 00:00:00.000', 1) ON CONFLICT DO UPDATE SET "title" = ("reminders"."title" || ' Copy') - RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title" + RETURNING "id" """ - } results: { + }results: { """ - ┌────────────────────────────────────────────┐ - │ Reminder( │ - │ id: 11, │ - │ assignedUserID: nil, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ isCompleted: true, │ - │ isFlagged: false, │ - │ notes: "", │ - │ priority: .high, │ - │ remindersListID: 1, │ - │ title: "Groceries" │ - │ ) │ - ├────────────────────────────────────────────┤ - │ Reminder( │ - │ id: 12, │ - │ assignedUserID: nil, │ - │ dueDate: Date(1970-01-01T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "", │ - │ priority: .low, │ - │ remindersListID: 2, │ - │ title: "Haircut" │ - │ ) │ - └────────────────────────────────────────────┘ + ┌────┐ + │ 11 │ + │ 12 │ + └────┘ """ } } @@ -61,30 +40,20 @@ extension SnapshotTests { assertQuery( Reminder .insert(\.remindersListID) { 1 } - .returning(\.self) + .returning(\.id) ) { """ INSERT INTO "reminders" ("remindersListID") VALUES (1) - RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title" + RETURNING "id" """ - } results: { + }results: { """ - ┌────────────────────────┐ - │ Reminder( │ - │ id: 11, │ - │ assignedUserID: nil, │ - │ dueDate: nil, │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "" │ - │ ) │ - └────────────────────────┘ + ┌────┐ + │ 11 │ + └────┘ """ } } @@ -115,12 +84,12 @@ extension SnapshotTests { ) { """ INSERT INTO "reminders" - ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title") + ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt") VALUES - (100, NULL, NULL, 0, 0, '', NULL, 1, 'Check email') + (100, NULL, NULL, 0, 0, '', NULL, 1, 'Check email', '2040-02-14 23:31:30.000') RETURNING "id" """ - } results: { + }results: { """ ┌─────┐ │ 100 │ @@ -135,12 +104,12 @@ extension SnapshotTests { ) { """ INSERT INTO "reminders" - ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title") + ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt") VALUES - (101, NULL, NULL, 0, 0, '', NULL, 1, 'Check voicemail') + (101, NULL, NULL, 0, 0, '', NULL, 1, 'Check voicemail', '2040-02-14 23:31:30.000') RETURNING "id" """ - } results: { + }results: { """ ┌─────┐ │ 101 │ @@ -156,12 +125,12 @@ extension SnapshotTests { ) { """ INSERT INTO "reminders" - ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title") + ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt") VALUES - (102, NULL, NULL, 0, 0, '', NULL, 1, 'Check mailbox'), (103, NULL, NULL, 0, 0, '', NULL, 1, 'Check Slack') + (102, NULL, NULL, 0, 0, '', NULL, 1, 'Check mailbox', '2040-02-14 23:31:30.000'), (103, NULL, NULL, 0, 0, '', NULL, 1, 'Check Slack', '2040-02-14 23:31:30.000') RETURNING "id" """ - } results: { + }results: { """ ┌─────┐ │ 102 │ @@ -177,12 +146,12 @@ extension SnapshotTests { ) { """ INSERT INTO "reminders" - ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title") + ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt") VALUES - (104, NULL, NULL, 0, 0, '', NULL, 1, 'Check pager') + (104, NULL, NULL, 0, 0, '', NULL, 1, 'Check pager', '2040-02-14 23:31:30.000') RETURNING "id" """ - } results: { + }results: { """ ┌─────┐ │ 104 │ @@ -238,12 +207,12 @@ extension SnapshotTests { ) { """ INSERT INTO "reminders" - ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title") + ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt") VALUES - (NULL, NULL, NULL, 0, 0, '', NULL, 1, 'Check email') + (NULL, NULL, NULL, 0, 0, '', NULL, 1, 'Check email', '2040-02-14 23:31:30.000') RETURNING "id" """ - } results: { + }results: { """ ┌────┐ │ 11 │ @@ -259,12 +228,12 @@ extension SnapshotTests { ) { """ INSERT INTO "reminders" - ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title") + ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt") VALUES - (NULL, NULL, NULL, 0, 0, '', NULL, 1, 'Check voicemail') + (NULL, NULL, NULL, 0, 0, '', NULL, 1, 'Check voicemail', '2040-02-14 23:31:30.000') RETURNING "id" """ - } results: { + }results: { """ ┌────┐ │ 12 │ @@ -283,12 +252,12 @@ extension SnapshotTests { ) { """ INSERT INTO "reminders" - ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title") + ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt") VALUES - (NULL, NULL, NULL, 0, 0, '', NULL, 1, 'Check mailbox'), (NULL, NULL, NULL, 0, 0, '', NULL, 1, 'Check Slack') + (NULL, NULL, NULL, 0, 0, '', NULL, 1, 'Check mailbox', '2040-02-14 23:31:30.000'), (NULL, NULL, NULL, 0, 0, '', NULL, 1, 'Check Slack', '2040-02-14 23:31:30.000') RETURNING "id" """ - } results: { + }results: { """ ┌────┐ │ 13 │ @@ -301,25 +270,26 @@ extension SnapshotTests { @Test func upsertWithID() { assertQuery(Reminder.where { $0.id == 1 }) { """ - SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title" + SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "reminders" WHERE ("reminders"."id" = 1) """ - } results: { - """ - ┌────────────────────────────────────────────┐ - │ Reminder( │ - │ id: 1, │ - │ assignedUserID: 1, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "Milk, Eggs, Apples", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Groceries" │ - │ ) │ - └────────────────────────────────────────────┘ + }results: { + """ + ┌─────────────────────────────────────────────┐ + │ Reminder( │ + │ id: 1, │ + │ assignedUserID: 1, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "Milk, Eggs, Apples", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Groceries", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + └─────────────────────────────────────────────┘ """ } assertQuery( @@ -329,28 +299,29 @@ extension SnapshotTests { ) { """ INSERT INTO "reminders" - ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title") + ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt") VALUES - (1, NULL, NULL, 0, 0, '', NULL, 1, 'Cash check') + (1, NULL, NULL, 0, 0, '', NULL, 1, 'Cash check', '2040-02-14 23:31:30.000') ON CONFLICT ("id") - DO UPDATE SET "assignedUserID" = "excluded"."assignedUserID", "dueDate" = "excluded"."dueDate", "isCompleted" = "excluded"."isCompleted", "isFlagged" = "excluded"."isFlagged", "notes" = "excluded"."notes", "priority" = "excluded"."priority", "remindersListID" = "excluded"."remindersListID", "title" = "excluded"."title" - RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title" - """ - } results: { - """ - ┌────────────────────────┐ - │ Reminder( │ - │ id: 1, │ - │ assignedUserID: nil, │ - │ dueDate: nil, │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Cash check" │ - │ ) │ - └────────────────────────┘ + DO UPDATE SET "assignedUserID" = "excluded"."assignedUserID", "dueDate" = "excluded"."dueDate", "isCompleted" = "excluded"."isCompleted", "isFlagged" = "excluded"."isFlagged", "notes" = "excluded"."notes", "priority" = "excluded"."priority", "remindersListID" = "excluded"."remindersListID", "title" = "excluded"."title", "updatedAt" = "excluded"."updatedAt" + RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt" + """ + }results: { + """ + ┌─────────────────────────────────────────────┐ + │ Reminder( │ + │ id: 1, │ + │ assignedUserID: nil, │ + │ dueDate: nil, │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Cash check", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + └─────────────────────────────────────────────┘ """ } } @@ -376,28 +347,29 @@ extension SnapshotTests { ) { """ INSERT INTO "reminders" - ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title") + ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt") VALUES - (NULL, NULL, NULL, 0, 0, '', NULL, 1, '') + (NULL, NULL, NULL, 0, 0, '', NULL, 1, '', '2040-02-14 23:31:30.000') ON CONFLICT ("id") - DO UPDATE SET "assignedUserID" = "excluded"."assignedUserID", "dueDate" = "excluded"."dueDate", "isCompleted" = "excluded"."isCompleted", "isFlagged" = "excluded"."isFlagged", "notes" = "excluded"."notes", "priority" = "excluded"."priority", "remindersListID" = "excluded"."remindersListID", "title" = "excluded"."title" - RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title" - """ - } results: { - """ - ┌────────────────────────┐ - │ Reminder( │ - │ id: 11, │ - │ assignedUserID: nil, │ - │ dueDate: nil, │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "" │ - │ ) │ - └────────────────────────┘ + DO UPDATE SET "assignedUserID" = "excluded"."assignedUserID", "dueDate" = "excluded"."dueDate", "isCompleted" = "excluded"."isCompleted", "isFlagged" = "excluded"."isFlagged", "notes" = "excluded"."notes", "priority" = "excluded"."priority", "remindersListID" = "excluded"."remindersListID", "title" = "excluded"."title", "updatedAt" = "excluded"."updatedAt" + RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt" + """ + }results: { + """ + ┌─────────────────────────────────────────────┐ + │ Reminder( │ + │ id: 11, │ + │ assignedUserID: nil, │ + │ dueDate: nil, │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + └─────────────────────────────────────────────┘ """ } } @@ -572,9 +544,9 @@ extension SnapshotTests { ) { """ INSERT INTO "reminders" - ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title") + ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt") VALUES - (NULL, NULL, NULL, 0, 0, '', NULL, 1, '') + (NULL, NULL, NULL, 0, 0, '', NULL, 1, '', '2040-02-14 23:31:30.000') ON CONFLICT ("id") WHERE NOT ("reminders"."isCompleted") DO UPDATE SET "isCompleted" = 1 @@ -596,9 +568,9 @@ extension SnapshotTests { ) { """ INSERT INTO "reminders" - ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title") + ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt") VALUES - (NULL, NULL, NULL, 0, 0, '', NULL, 1, '') + (NULL, NULL, NULL, 0, 0, '', NULL, 1, '', '2040-02-14 23:31:30.000') """ } } @@ -618,9 +590,9 @@ extension SnapshotTests { ) { """ INSERT INTO "reminders" - ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title") + ("id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt") VALUES - (NULL, NULL, NULL, 0, 0, '', NULL, 1, '') + (NULL, NULL, NULL, 0, 0, '', NULL, 1, '', '2040-02-14 23:31:30.000') """ } } diff --git a/Tests/StructuredQueriesTests/JSONFunctionsTests.swift b/Tests/StructuredQueriesTests/JSONFunctionsTests.swift index 742c43ae..e6ee2a49 100644 --- a/Tests/StructuredQueriesTests/JSONFunctionsTests.swift +++ b/Tests/StructuredQueriesTests/JSONFunctionsTests.swift @@ -142,7 +142,7 @@ extension SnapshotTests { .limit(2) ) { """ - SELECT "users"."id", "users"."name" AS "assignedUser", "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title" AS "reminder", json_group_array(CASE WHEN ("tags"."id" IS NOT NULL) THEN json_object('id', json_quote("tags"."id"), 'title', json_quote("tags"."title")) END) FILTER (WHERE ("tags"."id" IS NOT NULL)) AS "tags" + SELECT "users"."id", "users"."name" AS "assignedUser", "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" AS "reminder", json_group_array(CASE WHEN ("tags"."id" IS NOT NULL) THEN json_object('id', json_quote("tags"."id"), 'title', json_quote("tags"."title")) END) FILTER (WHERE ("tags"."id" IS NOT NULL)) AS "tags" FROM "reminders" LEFT JOIN "remindersTags" ON ("reminders"."id" = "remindersTags"."reminderID") LEFT JOIN "tags" ON ("remindersTags"."tagID" = "tags"."id") @@ -150,62 +150,64 @@ extension SnapshotTests { GROUP BY "reminders"."id" LIMIT 2 """ - } results: { + }results: { """ - ┌──────────────────────────────────────────────┐ - │ ReminderRow( │ - │ assignedUser: User( │ - │ id: 1, │ - │ name: "Blob" │ - │ ), │ - │ reminder: Reminder( │ - │ id: 1, │ - │ assignedUserID: 1, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "Milk, Eggs, Apples", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Groceries" │ - │ ), │ - │ tags: [ │ - │ [0]: Tag( │ - │ id: 3, │ - │ title: "someday" │ - │ ), │ - │ [1]: Tag( │ - │ id: 4, │ - │ title: "optional" │ - │ ) │ - │ ] │ - │ ) │ - ├──────────────────────────────────────────────┤ - │ ReminderRow( │ - │ assignedUser: nil, │ - │ reminder: Reminder( │ - │ id: 2, │ - │ assignedUserID: nil, │ - │ dueDate: Date(2000-12-30T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: true, │ - │ notes: "", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Haircut" │ - │ ), │ - │ tags: [ │ - │ [0]: Tag( │ - │ id: 3, │ - │ title: "someday" │ - │ ), │ - │ [1]: Tag( │ - │ id: 4, │ - │ title: "optional" │ - │ ) │ - │ ] │ - │ ) │ - └──────────────────────────────────────────────┘ + ┌───────────────────────────────────────────────┐ + │ ReminderRow( │ + │ assignedUser: User( │ + │ id: 1, │ + │ name: "Blob" │ + │ ), │ + │ reminder: Reminder( │ + │ id: 1, │ + │ assignedUserID: 1, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "Milk, Eggs, Apples", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Groceries", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ), │ + │ tags: [ │ + │ [0]: Tag( │ + │ id: 3, │ + │ title: "someday" │ + │ ), │ + │ [1]: Tag( │ + │ id: 4, │ + │ title: "optional" │ + │ ) │ + │ ] │ + │ ) │ + ├───────────────────────────────────────────────┤ + │ ReminderRow( │ + │ assignedUser: nil, │ + │ reminder: Reminder( │ + │ id: 2, │ + │ assignedUserID: nil, │ + │ dueDate: Date(2000-12-30T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: true, │ + │ notes: "", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Haircut", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ), │ + │ tags: [ │ + │ [0]: Tag( │ + │ id: 3, │ + │ title: "someday" │ + │ ), │ + │ [1]: Tag( │ + │ id: 4, │ + │ title: "optional" │ + │ ) │ + │ ] │ + │ ) │ + └───────────────────────────────────────────────┘ """ } } @@ -226,7 +228,7 @@ extension SnapshotTests { .limit(1) ) { """ - SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title" AS "remindersList", json_group_array(DISTINCT CASE WHEN ("milestones"."id" IS NOT NULL) THEN json_object('id', json_quote("milestones"."id"), 'remindersListID', json_quote("milestones"."remindersListID"), 'title', json_quote("milestones"."title")) END) FILTER (WHERE ("milestones"."id" IS NOT NULL)) AS "milestones", json_group_array(DISTINCT CASE WHEN ("reminders"."id" IS NOT NULL) THEN json_object('id', json_quote("reminders"."id"), 'assignedUserID', json_quote("reminders"."assignedUserID"), 'dueDate', json_quote("reminders"."dueDate"), 'isCompleted', json(CASE "reminders"."isCompleted" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'isFlagged', json(CASE "reminders"."isFlagged" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'notes', json_quote("reminders"."notes"), 'priority', json_quote("reminders"."priority"), 'remindersListID', json_quote("reminders"."remindersListID"), 'title', json_quote("reminders"."title")) END) FILTER (WHERE ("reminders"."id" IS NOT NULL)) AS "reminders" + SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position" AS "remindersList", json_group_array(DISTINCT CASE WHEN ("milestones"."id" IS NOT NULL) THEN json_object('id', json_quote("milestones"."id"), 'remindersListID', json_quote("milestones"."remindersListID"), 'title', json_quote("milestones"."title")) END) FILTER (WHERE ("milestones"."id" IS NOT NULL)) AS "milestones", json_group_array(DISTINCT CASE WHEN ("reminders"."id" IS NOT NULL) THEN json_object('id', json_quote("reminders"."id"), 'assignedUserID', json_quote("reminders"."assignedUserID"), 'dueDate', json_quote("reminders"."dueDate"), 'isCompleted', json(CASE "reminders"."isCompleted" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'isFlagged', json(CASE "reminders"."isFlagged" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'notes', json_quote("reminders"."notes"), 'priority', json_quote("reminders"."priority"), 'remindersListID', json_quote("reminders"."remindersListID"), 'title', json_quote("reminders"."title"), 'updatedAt', json_quote("reminders"."updatedAt")) END) FILTER (WHERE ("reminders"."id" IS NOT NULL)) AS "reminders" FROM "remindersLists" LEFT JOIN "milestones" ON ("remindersLists"."id" = "milestones"."remindersListID") LEFT JOIN "reminders" ON ("remindersLists"."id" = "reminders"."remindersListID") @@ -234,81 +236,85 @@ extension SnapshotTests { GROUP BY "remindersLists"."id" LIMIT 1 """ - } results: { + }results: { """ - ┌────────────────────────────────────────────────┐ - │ RemindersListRow( │ - │ remindersList: RemindersList( │ - │ id: 1, │ - │ color: 4889071, │ - │ title: "Personal", │ - │ position: 0 │ - │ ), │ - │ milestones: [ │ - │ [0]: Milestone( │ - │ id: 1, │ - │ remindersListID: 1, │ - │ title: "Phase 1" │ - │ ), │ - │ [1]: Milestone( │ - │ id: 2, │ - │ remindersListID: 1, │ - │ title: "Phase 2" │ - │ ), │ - │ [2]: Milestone( │ - │ id: 3, │ - │ remindersListID: 1, │ - │ title: "Phase 3" │ - │ ) │ - │ ], │ - │ reminders: [ │ - │ [0]: Reminder( │ - │ id: 1, │ - │ assignedUserID: 1, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "Milk, Eggs, Apples", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Groceries" │ - │ ), │ - │ [1]: Reminder( │ - │ id: 2, │ - │ assignedUserID: nil, │ - │ dueDate: Date(2000-12-30T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: true, │ - │ notes: "", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Haircut" │ - │ ), │ - │ [2]: Reminder( │ - │ id: 3, │ - │ assignedUserID: nil, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "Ask about diet", │ - │ priority: .high, │ - │ remindersListID: 1, │ - │ title: "Doctor appointment" │ - │ ), │ - │ [3]: Reminder( │ - │ id: 5, │ - │ assignedUserID: nil, │ - │ dueDate: nil, │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Buy concert tickets" │ - │ ) │ - │ ] │ - │ ) │ - └────────────────────────────────────────────────┘ + ┌─────────────────────────────────────────────────┐ + │ RemindersListRow( │ + │ remindersList: RemindersList( │ + │ id: 1, │ + │ color: 4889071, │ + │ title: "Personal", │ + │ position: 0 │ + │ ), │ + │ milestones: [ │ + │ [0]: Milestone( │ + │ id: 1, │ + │ remindersListID: 1, │ + │ title: "Phase 1" │ + │ ), │ + │ [1]: Milestone( │ + │ id: 2, │ + │ remindersListID: 1, │ + │ title: "Phase 2" │ + │ ), │ + │ [2]: Milestone( │ + │ id: 3, │ + │ remindersListID: 1, │ + │ title: "Phase 3" │ + │ ) │ + │ ], │ + │ reminders: [ │ + │ [0]: Reminder( │ + │ id: 1, │ + │ assignedUserID: 1, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "Milk, Eggs, Apples", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Groceries", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ), │ + │ [1]: Reminder( │ + │ id: 2, │ + │ assignedUserID: nil, │ + │ dueDate: Date(2000-12-30T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: true, │ + │ notes: "", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Haircut", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ), │ + │ [2]: Reminder( │ + │ id: 3, │ + │ assignedUserID: nil, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "Ask about diet", │ + │ priority: .high, │ + │ remindersListID: 1, │ + │ title: "Doctor appointment", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ), │ + │ [3]: Reminder( │ + │ id: 5, │ + │ assignedUserID: nil, │ + │ dueDate: nil, │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Buy concert tickets", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + │ ] │ + │ ) │ + └─────────────────────────────────────────────────┘ """ } } diff --git a/Tests/StructuredQueriesTests/LiveTests.swift b/Tests/StructuredQueriesTests/LiveTests.swift index 4ed9196b..fc040506 100644 --- a/Tests/StructuredQueriesTests/LiveTests.swift +++ b/Tests/StructuredQueriesTests/LiveTests.swift @@ -9,136 +9,146 @@ extension SnapshotTests { @Test func selectAll() { assertQuery(Reminder.all) { """ - SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title" + SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "reminders" """ - } results: { + }results: { #""" - ┌────────────────────────────────────────────┐ - │ Reminder( │ - │ id: 1, │ - │ assignedUserID: 1, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "Milk, Eggs, Apples", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Groceries" │ - │ ) │ - ├────────────────────────────────────────────┤ - │ Reminder( │ - │ id: 2, │ - │ assignedUserID: nil, │ - │ dueDate: Date(2000-12-30T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: true, │ - │ notes: "", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Haircut" │ - │ ) │ - ├────────────────────────────────────────────┤ - │ Reminder( │ - │ id: 3, │ - │ assignedUserID: nil, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "Ask about diet", │ - │ priority: .high, │ - │ remindersListID: 1, │ - │ title: "Doctor appointment" │ - │ ) │ - ├────────────────────────────────────────────┤ - │ Reminder( │ - │ id: 4, │ - │ assignedUserID: nil, │ - │ dueDate: Date(2000-06-25T00:00:00.000Z), │ - │ isCompleted: true, │ - │ isFlagged: false, │ - │ notes: "", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Take a walk" │ - │ ) │ - ├────────────────────────────────────────────┤ - │ Reminder( │ - │ id: 5, │ - │ assignedUserID: nil, │ - │ dueDate: nil, │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Buy concert tickets" │ - │ ) │ - ├────────────────────────────────────────────┤ - │ Reminder( │ - │ id: 6, │ - │ assignedUserID: nil, │ - │ dueDate: Date(2001-01-03T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: true, │ - │ notes: "", │ - │ priority: .high, │ - │ remindersListID: 2, │ - │ title: "Pick up kids from school" │ - │ ) │ - ├────────────────────────────────────────────┤ - │ Reminder( │ - │ id: 7, │ - │ assignedUserID: nil, │ - │ dueDate: Date(2000-12-30T00:00:00.000Z), │ - │ isCompleted: true, │ - │ isFlagged: false, │ - │ notes: "", │ - │ priority: .low, │ - │ remindersListID: 2, │ - │ title: "Get laundry" │ - │ ) │ - ├────────────────────────────────────────────┤ - │ Reminder( │ - │ id: 8, │ - │ assignedUserID: nil, │ - │ dueDate: Date(2001-01-05T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "", │ - │ priority: .high, │ - │ remindersListID: 2, │ - │ title: "Take out trash" │ - │ ) │ - ├────────────────────────────────────────────┤ - │ Reminder( │ - │ id: 9, │ - │ assignedUserID: nil, │ - │ dueDate: Date(2001-01-03T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: """ │ - │ Status of tax return │ - │ Expenses for next year │ - │ Changing payroll company │ - │ """, │ - │ priority: nil, │ - │ remindersListID: 3, │ - │ title: "Call accountant" │ - │ ) │ - ├────────────────────────────────────────────┤ - │ Reminder( │ - │ id: 10, │ - │ assignedUserID: nil, │ - │ dueDate: Date(2000-12-30T00:00:00.000Z), │ - │ isCompleted: true, │ - │ isFlagged: false, │ - │ notes: "", │ - │ priority: .medium, │ - │ remindersListID: 3, │ - │ title: "Send weekly emails" │ - │ ) │ - └────────────────────────────────────────────┘ + ┌─────────────────────────────────────────────┐ + │ Reminder( │ + │ id: 1, │ + │ assignedUserID: 1, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "Milk, Eggs, Apples", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Groceries", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + ├─────────────────────────────────────────────┤ + │ Reminder( │ + │ id: 2, │ + │ assignedUserID: nil, │ + │ dueDate: Date(2000-12-30T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: true, │ + │ notes: "", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Haircut", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + ├─────────────────────────────────────────────┤ + │ Reminder( │ + │ id: 3, │ + │ assignedUserID: nil, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "Ask about diet", │ + │ priority: .high, │ + │ remindersListID: 1, │ + │ title: "Doctor appointment", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + ├─────────────────────────────────────────────┤ + │ Reminder( │ + │ id: 4, │ + │ assignedUserID: nil, │ + │ dueDate: Date(2000-06-25T00:00:00.000Z), │ + │ isCompleted: true, │ + │ isFlagged: false, │ + │ notes: "", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Take a walk", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + ├─────────────────────────────────────────────┤ + │ Reminder( │ + │ id: 5, │ + │ assignedUserID: nil, │ + │ dueDate: nil, │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Buy concert tickets", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + ├─────────────────────────────────────────────┤ + │ Reminder( │ + │ id: 6, │ + │ assignedUserID: nil, │ + │ dueDate: Date(2001-01-03T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: true, │ + │ notes: "", │ + │ priority: .high, │ + │ remindersListID: 2, │ + │ title: "Pick up kids from school", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + ├─────────────────────────────────────────────┤ + │ Reminder( │ + │ id: 7, │ + │ assignedUserID: nil, │ + │ dueDate: Date(2000-12-30T00:00:00.000Z), │ + │ isCompleted: true, │ + │ isFlagged: false, │ + │ notes: "", │ + │ priority: .low, │ + │ remindersListID: 2, │ + │ title: "Get laundry", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + ├─────────────────────────────────────────────┤ + │ Reminder( │ + │ id: 8, │ + │ assignedUserID: nil, │ + │ dueDate: Date(2001-01-05T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "", │ + │ priority: .high, │ + │ remindersListID: 2, │ + │ title: "Take out trash", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + ├─────────────────────────────────────────────┤ + │ Reminder( │ + │ id: 9, │ + │ assignedUserID: nil, │ + │ dueDate: Date(2001-01-03T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: """ │ + │ Status of tax return │ + │ Expenses for next year │ + │ Changing payroll company │ + │ """, │ + │ priority: nil, │ + │ remindersListID: 3, │ + │ title: "Call accountant", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + ├─────────────────────────────────────────────┤ + │ Reminder( │ + │ id: 10, │ + │ assignedUserID: nil, │ + │ dueDate: Date(2000-12-30T00:00:00.000Z), │ + │ isCompleted: true, │ + │ isFlagged: false, │ + │ notes: "", │ + │ priority: .medium, │ + │ remindersListID: 3, │ + │ title: "Send weekly emails", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + └─────────────────────────────────────────────┘ """# } } @@ -226,51 +236,54 @@ extension SnapshotTests { .select { ($0, $2.title.groupConcat()) } ) { """ - SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", group_concat("tags"."title") + SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt", group_concat("tags"."title") FROM "reminders" JOIN "remindersTags" ON ("reminders"."id" = "remindersTags"."reminderID") JOIN "tags" ON ("remindersTags"."tagID" = "tags"."id") GROUP BY "reminders"."id" """ - } results: { + }results: { """ - ┌────────────────────────────────────────────┬────────────────────┐ - │ Reminder( │ "someday,optional" │ - │ id: 1, │ │ - │ assignedUserID: 1, │ │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ │ - │ isCompleted: false, │ │ - │ isFlagged: false, │ │ - │ notes: "Milk, Eggs, Apples", │ │ - │ priority: nil, │ │ - │ remindersListID: 1, │ │ - │ title: "Groceries" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼────────────────────┤ - │ Reminder( │ "someday,optional" │ - │ id: 2, │ │ - │ assignedUserID: nil, │ │ - │ dueDate: Date(2000-12-30T00:00:00.000Z), │ │ - │ isCompleted: false, │ │ - │ isFlagged: true, │ │ - │ notes: "", │ │ - │ priority: nil, │ │ - │ remindersListID: 1, │ │ - │ title: "Haircut" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼────────────────────┤ - │ Reminder( │ "car,kids" │ - │ id: 4, │ │ - │ assignedUserID: nil, │ │ - │ dueDate: Date(2000-06-25T00:00:00.000Z), │ │ - │ isCompleted: true, │ │ - │ isFlagged: false, │ │ - │ notes: "", │ │ - │ priority: nil, │ │ - │ remindersListID: 1, │ │ - │ title: "Take a walk" │ │ - │ ) │ │ - └────────────────────────────────────────────┴────────────────────┘ + ┌─────────────────────────────────────────────┬────────────────────┐ + │ Reminder( │ "someday,optional" │ + │ id: 1, │ │ + │ assignedUserID: 1, │ │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ │ + │ isCompleted: false, │ │ + │ isFlagged: false, │ │ + │ notes: "Milk, Eggs, Apples", │ │ + │ priority: nil, │ │ + │ remindersListID: 1, │ │ + │ title: "Groceries", │ │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ │ + │ ) │ │ + ├─────────────────────────────────────────────┼────────────────────┤ + │ Reminder( │ "someday,optional" │ + │ id: 2, │ │ + │ assignedUserID: nil, │ │ + │ dueDate: Date(2000-12-30T00:00:00.000Z), │ │ + │ isCompleted: false, │ │ + │ isFlagged: true, │ │ + │ notes: "", │ │ + │ priority: nil, │ │ + │ remindersListID: 1, │ │ + │ title: "Haircut", │ │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ │ + │ ) │ │ + ├─────────────────────────────────────────────┼────────────────────┤ + │ Reminder( │ "car,kids" │ + │ id: 4, │ │ + │ assignedUserID: nil, │ │ + │ dueDate: Date(2000-06-25T00:00:00.000Z), │ │ + │ isCompleted: true, │ │ + │ isFlagged: false, │ │ + │ notes: "", │ │ + │ priority: nil, │ │ + │ remindersListID: 1, │ │ + │ title: "Take a walk", │ │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ │ + │ ) │ │ + └─────────────────────────────────────────────┴────────────────────┘ """ } } diff --git a/Tests/StructuredQueriesTests/OperatorsTests.swift b/Tests/StructuredQueriesTests/OperatorsTests.swift index e21840f0..23512680 100644 --- a/Tests/StructuredQueriesTests/OperatorsTests.swift +++ b/Tests/StructuredQueriesTests/OperatorsTests.swift @@ -425,7 +425,7 @@ extension SnapshotTests { } ) { """ - SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title" + SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "reminders" WHERE ("reminders"."id" BETWEEN coalesce(( SELECT min("reminders"."id") @@ -435,45 +435,48 @@ extension SnapshotTests { FROM "reminders" ), 0) / 3)) """ - } results: { - """ - ┌────────────────────────────────────────────┐ - │ Reminder( │ - │ id: 1, │ - │ assignedUserID: 1, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "Milk, Eggs, Apples", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Groceries" │ - │ ) │ - ├────────────────────────────────────────────┤ - │ Reminder( │ - │ id: 2, │ - │ assignedUserID: nil, │ - │ dueDate: Date(2000-12-30T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: true, │ - │ notes: "", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Haircut" │ - │ ) │ - ├────────────────────────────────────────────┤ - │ Reminder( │ - │ id: 3, │ - │ assignedUserID: nil, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "Ask about diet", │ - │ priority: .high, │ - │ remindersListID: 1, │ - │ title: "Doctor appointment" │ - │ ) │ - └────────────────────────────────────────────┘ + }results: { + """ + ┌─────────────────────────────────────────────┐ + │ Reminder( │ + │ id: 1, │ + │ assignedUserID: 1, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "Milk, Eggs, Apples", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Groceries", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + ├─────────────────────────────────────────────┤ + │ Reminder( │ + │ id: 2, │ + │ assignedUserID: nil, │ + │ dueDate: Date(2000-12-30T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: true, │ + │ notes: "", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Haircut", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + ├─────────────────────────────────────────────┤ + │ Reminder( │ + │ id: 3, │ + │ assignedUserID: nil, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "Ask about diet", │ + │ priority: .high, │ + │ remindersListID: 1, │ + │ title: "Doctor appointment", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + └─────────────────────────────────────────────┘ """ } } @@ -571,12 +574,12 @@ extension SnapshotTests { assertQuery(Values(Reminder.where { $0.id == 1 }.exists())) { """ SELECT EXISTS ( - SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title" + SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "reminders" WHERE ("reminders"."id" = 1) ) """ - } results: { + }results: { """ ┌──────┐ │ true │ @@ -586,12 +589,12 @@ extension SnapshotTests { assertQuery(Values(Reminder.where { $0.id == 100 }.exists())) { """ SELECT EXISTS ( - SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title" + SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "reminders" WHERE ("reminders"."id" = 100) ) """ - } results: { + }results: { """ ┌───────┐ │ false │ diff --git a/Tests/StructuredQueriesTests/SQLMacroTests.swift b/Tests/StructuredQueriesTests/SQLMacroTests.swift index 2ad29850..d35f978e 100644 --- a/Tests/StructuredQueriesTests/SQLMacroTests.swift +++ b/Tests/StructuredQueriesTests/SQLMacroTests.swift @@ -18,26 +18,27 @@ extension SnapshotTests { ) ) { """ - SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title" + SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "reminders" ORDER BY "reminders"."id" LIMIT 1 """ - } results: { + }results: { """ - ┌────────────────────────────────────────────┐ - │ Reminder( │ - │ id: 1, │ - │ assignedUserID: 1, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "Milk, Eggs, Apples", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Groceries" │ - │ ) │ - └────────────────────────────────────────────┘ + ┌─────────────────────────────────────────────┐ + │ Reminder( │ + │ id: 1, │ + │ assignedUserID: 1, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "Milk, Eggs, Apples", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Groceries", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + └─────────────────────────────────────────────┘ """ } } @@ -59,28 +60,29 @@ extension SnapshotTests { ) { """ SELECT - "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", + "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt", "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position" FROM "reminders" JOIN "remindersLists" ON "reminders"."remindersListID" = "remindersLists"."id" LIMIT 1 """ - } results: { - """ - ┌────────────────────────────────────────────┬──────────────────────┐ - │ Reminder( │ RemindersList( │ - │ id: 1, │ id: 1, │ - │ assignedUserID: 1, │ color: 4889071, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ title: "Personal", │ - │ isCompleted: false, │ position: 0 │ - │ isFlagged: false, │ ) │ - │ notes: "Milk, Eggs, Apples", │ │ - │ priority: nil, │ │ - │ remindersListID: 1, │ │ - │ title: "Groceries" │ │ - │ ) │ │ - └────────────────────────────────────────────┴──────────────────────┘ + }results: { + """ + ┌─────────────────────────────────────────────┬──────────────────────┐ + │ Reminder( │ RemindersList( │ + │ id: 1, │ id: 1, │ + │ assignedUserID: 1, │ color: 4889071, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ title: "Personal", │ + │ isCompleted: false, │ position: 0 │ + │ isFlagged: false, │ ) │ + │ notes: "Milk, Eggs, Apples", │ │ + │ priority: nil, │ │ + │ remindersListID: 1, │ │ + │ title: "Groceries", │ │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ │ + │ ) │ │ + └─────────────────────────────────────────────┴──────────────────────┘ """ } } @@ -99,32 +101,33 @@ extension SnapshotTests { ) ) { """ - SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position" + SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt", "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position" FROM "reminders" JOIN "remindersLists" ON "reminders"."remindersListID" = "remindersLists"."id" LIMIT 1 """ - } results: { - """ - ┌──────────────────────────────────────────────┐ - │ ReminderWithList( │ - │ reminder: Reminder( │ - │ id: 1, │ - │ assignedUserID: 1, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "Milk, Eggs, Apples", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Groceries" │ - │ ), │ - │ list: RemindersList( │ - │ id: 1, │ - │ color: 4889071, │ - │ title: "Personal", │ - │ position: 0 │ - │ ) │ - │ ) │ - └──────────────────────────────────────────────┘ + }results: { + """ + ┌───────────────────────────────────────────────┐ + │ ReminderWithList( │ + │ reminder: Reminder( │ + │ id: 1, │ + │ assignedUserID: 1, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "Milk, Eggs, Apples", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Groceries", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ), │ + │ list: RemindersList( │ + │ id: 1, │ + │ color: 4889071, │ + │ title: "Personal", │ + │ position: 0 │ + │ ) │ + │ ) │ + └───────────────────────────────────────────────┘ """ } } diff --git a/Tests/StructuredQueriesTests/SelectTests.swift b/Tests/StructuredQueriesTests/SelectTests.swift index 9d25f748..52960643 100644 --- a/Tests/StructuredQueriesTests/SelectTests.swift +++ b/Tests/StructuredQueriesTests/SelectTests.swift @@ -169,137 +169,147 @@ extension SnapshotTests { .join(RemindersList.all) { $0.remindersListID.eq($1.id) } ) { """ - SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position" + SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt", "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position" FROM "reminders" JOIN "remindersLists" ON ("reminders"."remindersListID" = "remindersLists"."id") """ - } results: { + }results: { #""" - ┌────────────────────────────────────────────┬──────────────────────┐ - │ Reminder( │ RemindersList( │ - │ id: 1, │ id: 1, │ - │ assignedUserID: 1, │ color: 4889071, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ title: "Personal", │ - │ isCompleted: false, │ position: 0 │ - │ isFlagged: false, │ ) │ - │ notes: "Milk, Eggs, Apples", │ │ - │ priority: nil, │ │ - │ remindersListID: 1, │ │ - │ title: "Groceries" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼──────────────────────┤ - │ Reminder( │ RemindersList( │ - │ id: 2, │ id: 1, │ - │ assignedUserID: nil, │ color: 4889071, │ - │ dueDate: Date(2000-12-30T00:00:00.000Z), │ title: "Personal", │ - │ isCompleted: false, │ position: 0 │ - │ isFlagged: true, │ ) │ - │ notes: "", │ │ - │ priority: nil, │ │ - │ remindersListID: 1, │ │ - │ title: "Haircut" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼──────────────────────┤ - │ Reminder( │ RemindersList( │ - │ id: 3, │ id: 1, │ - │ assignedUserID: nil, │ color: 4889071, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ title: "Personal", │ - │ isCompleted: false, │ position: 0 │ - │ isFlagged: false, │ ) │ - │ notes: "Ask about diet", │ │ - │ priority: .high, │ │ - │ remindersListID: 1, │ │ - │ title: "Doctor appointment" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼──────────────────────┤ - │ Reminder( │ RemindersList( │ - │ id: 4, │ id: 1, │ - │ assignedUserID: nil, │ color: 4889071, │ - │ dueDate: Date(2000-06-25T00:00:00.000Z), │ title: "Personal", │ - │ isCompleted: true, │ position: 0 │ - │ isFlagged: false, │ ) │ - │ notes: "", │ │ - │ priority: nil, │ │ - │ remindersListID: 1, │ │ - │ title: "Take a walk" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼──────────────────────┤ - │ Reminder( │ RemindersList( │ - │ id: 5, │ id: 1, │ - │ assignedUserID: nil, │ color: 4889071, │ - │ dueDate: nil, │ title: "Personal", │ - │ isCompleted: false, │ position: 0 │ - │ isFlagged: false, │ ) │ - │ notes: "", │ │ - │ priority: nil, │ │ - │ remindersListID: 1, │ │ - │ title: "Buy concert tickets" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼──────────────────────┤ - │ Reminder( │ RemindersList( │ - │ id: 6, │ id: 2, │ - │ assignedUserID: nil, │ color: 15567157, │ - │ dueDate: Date(2001-01-03T00:00:00.000Z), │ title: "Family", │ - │ isCompleted: false, │ position: 0 │ - │ isFlagged: true, │ ) │ - │ notes: "", │ │ - │ priority: .high, │ │ - │ remindersListID: 2, │ │ - │ title: "Pick up kids from school" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼──────────────────────┤ - │ Reminder( │ RemindersList( │ - │ id: 7, │ id: 2, │ - │ assignedUserID: nil, │ color: 15567157, │ - │ dueDate: Date(2000-12-30T00:00:00.000Z), │ title: "Family", │ - │ isCompleted: true, │ position: 0 │ - │ isFlagged: false, │ ) │ - │ notes: "", │ │ - │ priority: .low, │ │ - │ remindersListID: 2, │ │ - │ title: "Get laundry" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼──────────────────────┤ - │ Reminder( │ RemindersList( │ - │ id: 8, │ id: 2, │ - │ assignedUserID: nil, │ color: 15567157, │ - │ dueDate: Date(2001-01-05T00:00:00.000Z), │ title: "Family", │ - │ isCompleted: false, │ position: 0 │ - │ isFlagged: false, │ ) │ - │ notes: "", │ │ - │ priority: .high, │ │ - │ remindersListID: 2, │ │ - │ title: "Take out trash" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼──────────────────────┤ - │ Reminder( │ RemindersList( │ - │ id: 9, │ id: 3, │ - │ assignedUserID: nil, │ color: 11689427, │ - │ dueDate: Date(2001-01-03T00:00:00.000Z), │ title: "Business", │ - │ isCompleted: false, │ position: 0 │ - │ isFlagged: false, │ ) │ - │ notes: """ │ │ - │ Status of tax return │ │ - │ Expenses for next year │ │ - │ Changing payroll company │ │ - │ """, │ │ - │ priority: nil, │ │ - │ remindersListID: 3, │ │ - │ title: "Call accountant" │ │ - │ ) │ │ - ├────────────────────────────────────────────┼──────────────────────┤ - │ Reminder( │ RemindersList( │ - │ id: 10, │ id: 3, │ - │ assignedUserID: nil, │ color: 11689427, │ - │ dueDate: Date(2000-12-30T00:00:00.000Z), │ title: "Business", │ - │ isCompleted: true, │ position: 0 │ - │ isFlagged: false, │ ) │ - │ notes: "", │ │ - │ priority: .medium, │ │ - │ remindersListID: 3, │ │ - │ title: "Send weekly emails" │ │ - │ ) │ │ - └────────────────────────────────────────────┴──────────────────────┘ + ┌─────────────────────────────────────────────┬──────────────────────┐ + │ Reminder( │ RemindersList( │ + │ id: 1, │ id: 1, │ + │ assignedUserID: 1, │ color: 4889071, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ title: "Personal", │ + │ isCompleted: false, │ position: 0 │ + │ isFlagged: false, │ ) │ + │ notes: "Milk, Eggs, Apples", │ │ + │ priority: nil, │ │ + │ remindersListID: 1, │ │ + │ title: "Groceries", │ │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ │ + │ ) │ │ + ├─────────────────────────────────────────────┼──────────────────────┤ + │ Reminder( │ RemindersList( │ + │ id: 2, │ id: 1, │ + │ assignedUserID: nil, │ color: 4889071, │ + │ dueDate: Date(2000-12-30T00:00:00.000Z), │ title: "Personal", │ + │ isCompleted: false, │ position: 0 │ + │ isFlagged: true, │ ) │ + │ notes: "", │ │ + │ priority: nil, │ │ + │ remindersListID: 1, │ │ + │ title: "Haircut", │ │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ │ + │ ) │ │ + ├─────────────────────────────────────────────┼──────────────────────┤ + │ Reminder( │ RemindersList( │ + │ id: 3, │ id: 1, │ + │ assignedUserID: nil, │ color: 4889071, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ title: "Personal", │ + │ isCompleted: false, │ position: 0 │ + │ isFlagged: false, │ ) │ + │ notes: "Ask about diet", │ │ + │ priority: .high, │ │ + │ remindersListID: 1, │ │ + │ title: "Doctor appointment", │ │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ │ + │ ) │ │ + ├─────────────────────────────────────────────┼──────────────────────┤ + │ Reminder( │ RemindersList( │ + │ id: 4, │ id: 1, │ + │ assignedUserID: nil, │ color: 4889071, │ + │ dueDate: Date(2000-06-25T00:00:00.000Z), │ title: "Personal", │ + │ isCompleted: true, │ position: 0 │ + │ isFlagged: false, │ ) │ + │ notes: "", │ │ + │ priority: nil, │ │ + │ remindersListID: 1, │ │ + │ title: "Take a walk", │ │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ │ + │ ) │ │ + ├─────────────────────────────────────────────┼──────────────────────┤ + │ Reminder( │ RemindersList( │ + │ id: 5, │ id: 1, │ + │ assignedUserID: nil, │ color: 4889071, │ + │ dueDate: nil, │ title: "Personal", │ + │ isCompleted: false, │ position: 0 │ + │ isFlagged: false, │ ) │ + │ notes: "", │ │ + │ priority: nil, │ │ + │ remindersListID: 1, │ │ + │ title: "Buy concert tickets", │ │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ │ + │ ) │ │ + ├─────────────────────────────────────────────┼──────────────────────┤ + │ Reminder( │ RemindersList( │ + │ id: 6, │ id: 2, │ + │ assignedUserID: nil, │ color: 15567157, │ + │ dueDate: Date(2001-01-03T00:00:00.000Z), │ title: "Family", │ + │ isCompleted: false, │ position: 0 │ + │ isFlagged: true, │ ) │ + │ notes: "", │ │ + │ priority: .high, │ │ + │ remindersListID: 2, │ │ + │ title: "Pick up kids from school", │ │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ │ + │ ) │ │ + ├─────────────────────────────────────────────┼──────────────────────┤ + │ Reminder( │ RemindersList( │ + │ id: 7, │ id: 2, │ + │ assignedUserID: nil, │ color: 15567157, │ + │ dueDate: Date(2000-12-30T00:00:00.000Z), │ title: "Family", │ + │ isCompleted: true, │ position: 0 │ + │ isFlagged: false, │ ) │ + │ notes: "", │ │ + │ priority: .low, │ │ + │ remindersListID: 2, │ │ + │ title: "Get laundry", │ │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ │ + │ ) │ │ + ├─────────────────────────────────────────────┼──────────────────────┤ + │ Reminder( │ RemindersList( │ + │ id: 8, │ id: 2, │ + │ assignedUserID: nil, │ color: 15567157, │ + │ dueDate: Date(2001-01-05T00:00:00.000Z), │ title: "Family", │ + │ isCompleted: false, │ position: 0 │ + │ isFlagged: false, │ ) │ + │ notes: "", │ │ + │ priority: .high, │ │ + │ remindersListID: 2, │ │ + │ title: "Take out trash", │ │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ │ + │ ) │ │ + ├─────────────────────────────────────────────┼──────────────────────┤ + │ Reminder( │ RemindersList( │ + │ id: 9, │ id: 3, │ + │ assignedUserID: nil, │ color: 11689427, │ + │ dueDate: Date(2001-01-03T00:00:00.000Z), │ title: "Business", │ + │ isCompleted: false, │ position: 0 │ + │ isFlagged: false, │ ) │ + │ notes: """ │ │ + │ Status of tax return │ │ + │ Expenses for next year │ │ + │ Changing payroll company │ │ + │ """, │ │ + │ priority: nil, │ │ + │ remindersListID: 3, │ │ + │ title: "Call accountant", │ │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ │ + │ ) │ │ + ├─────────────────────────────────────────────┼──────────────────────┤ + │ Reminder( │ RemindersList( │ + │ id: 10, │ id: 3, │ + │ assignedUserID: nil, │ color: 11689427, │ + │ dueDate: Date(2000-12-30T00:00:00.000Z), │ title: "Business", │ + │ isCompleted: true, │ position: 0 │ + │ isFlagged: false, │ ) │ + │ notes: "", │ │ + │ priority: .medium, │ │ + │ remindersListID: 3, │ │ + │ title: "Send weekly emails", │ │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ │ + │ ) │ │ + └─────────────────────────────────────────────┴──────────────────────┘ """# } @@ -357,38 +367,40 @@ extension SnapshotTests { .limit(2) ) { """ - SELECT "users"."id", "users"."name", "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title" + SELECT "users"."id", "users"."name", "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "users" RIGHT JOIN "reminders" ON ("users"."id" IS "reminders"."assignedUserID") LIMIT 2 """ - } results: { - """ - ┌────────────────┬────────────────────────────────────────────┐ - │ User( │ Reminder( │ - │ id: 1, │ id: 1, │ - │ name: "Blob" │ assignedUserID: 1, │ - │ ) │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ │ isCompleted: false, │ - │ │ isFlagged: false, │ - │ │ notes: "Milk, Eggs, Apples", │ - │ │ priority: nil, │ - │ │ remindersListID: 1, │ - │ │ title: "Groceries" │ - │ │ ) │ - ├────────────────┼────────────────────────────────────────────┤ - │ nil │ Reminder( │ - │ │ id: 2, │ - │ │ assignedUserID: nil, │ - │ │ dueDate: Date(2000-12-30T00:00:00.000Z), │ - │ │ isCompleted: false, │ - │ │ isFlagged: true, │ - │ │ notes: "", │ - │ │ priority: nil, │ - │ │ remindersListID: 1, │ - │ │ title: "Haircut" │ - │ │ ) │ - └────────────────┴────────────────────────────────────────────┘ + }results: { + """ + ┌────────────────┬─────────────────────────────────────────────┐ + │ User( │ Reminder( │ + │ id: 1, │ id: 1, │ + │ name: "Blob" │ assignedUserID: 1, │ + │ ) │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ │ isCompleted: false, │ + │ │ isFlagged: false, │ + │ │ notes: "Milk, Eggs, Apples", │ + │ │ priority: nil, │ + │ │ remindersListID: 1, │ + │ │ title: "Groceries", │ + │ │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ │ ) │ + ├────────────────┼─────────────────────────────────────────────┤ + │ nil │ Reminder( │ + │ │ id: 2, │ + │ │ assignedUserID: nil, │ + │ │ dueDate: Date(2000-12-30T00:00:00.000Z), │ + │ │ isCompleted: false, │ + │ │ isFlagged: true, │ + │ │ notes: "", │ + │ │ priority: nil, │ + │ │ remindersListID: 1, │ + │ │ title: "Haircut", │ + │ │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ │ ) │ + └────────────────┴─────────────────────────────────────────────┘ """ } @@ -399,38 +411,40 @@ extension SnapshotTests { .select { ($0, $1) } ) { """ - SELECT "users"."id", "users"."name", "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title" + SELECT "users"."id", "users"."name", "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "users" RIGHT JOIN "reminders" ON ("users"."id" IS "reminders"."assignedUserID") LIMIT 2 """ - } results: { - """ - ┌────────────────┬────────────────────────────────────────────┐ - │ User( │ Reminder( │ - │ id: 1, │ id: 1, │ - │ name: "Blob" │ assignedUserID: 1, │ - │ ) │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ │ isCompleted: false, │ - │ │ isFlagged: false, │ - │ │ notes: "Milk, Eggs, Apples", │ - │ │ priority: nil, │ - │ │ remindersListID: 1, │ - │ │ title: "Groceries" │ - │ │ ) │ - ├────────────────┼────────────────────────────────────────────┤ - │ nil │ Reminder( │ - │ │ id: 2, │ - │ │ assignedUserID: nil, │ - │ │ dueDate: Date(2000-12-30T00:00:00.000Z), │ - │ │ isCompleted: false, │ - │ │ isFlagged: true, │ - │ │ notes: "", │ - │ │ priority: nil, │ - │ │ remindersListID: 1, │ - │ │ title: "Haircut" │ - │ │ ) │ - └────────────────┴────────────────────────────────────────────┘ + }results: { + """ + ┌────────────────┬─────────────────────────────────────────────┐ + │ User( │ Reminder( │ + │ id: 1, │ id: 1, │ + │ name: "Blob" │ assignedUserID: 1, │ + │ ) │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ │ isCompleted: false, │ + │ │ isFlagged: false, │ + │ │ notes: "Milk, Eggs, Apples", │ + │ │ priority: nil, │ + │ │ remindersListID: 1, │ + │ │ title: "Groceries", │ + │ │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ │ ) │ + ├────────────────┼─────────────────────────────────────────────┤ + │ nil │ Reminder( │ + │ │ id: 2, │ + │ │ assignedUserID: nil, │ + │ │ dueDate: Date(2000-12-30T00:00:00.000Z), │ + │ │ isCompleted: false, │ + │ │ isFlagged: true, │ + │ │ notes: "", │ + │ │ priority: nil, │ + │ │ remindersListID: 1, │ + │ │ title: "Haircut", │ + │ │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ │ ) │ + └────────────────┴─────────────────────────────────────────────┘ """ } @@ -482,49 +496,52 @@ extension SnapshotTests { Reminder.where(\.isCompleted) ) { """ - SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title" + SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "reminders" WHERE "reminders"."isCompleted" """ - } results: { - """ - ┌────────────────────────────────────────────┐ - │ Reminder( │ - │ id: 4, │ - │ assignedUserID: nil, │ - │ dueDate: Date(2000-06-25T00:00:00.000Z), │ - │ isCompleted: true, │ - │ isFlagged: false, │ - │ notes: "", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Take a walk" │ - │ ) │ - ├────────────────────────────────────────────┤ - │ Reminder( │ - │ id: 7, │ - │ assignedUserID: nil, │ - │ dueDate: Date(2000-12-30T00:00:00.000Z), │ - │ isCompleted: true, │ - │ isFlagged: false, │ - │ notes: "", │ - │ priority: .low, │ - │ remindersListID: 2, │ - │ title: "Get laundry" │ - │ ) │ - ├────────────────────────────────────────────┤ - │ Reminder( │ - │ id: 10, │ - │ assignedUserID: nil, │ - │ dueDate: Date(2000-12-30T00:00:00.000Z), │ - │ isCompleted: true, │ - │ isFlagged: false, │ - │ notes: "", │ - │ priority: .medium, │ - │ remindersListID: 3, │ - │ title: "Send weekly emails" │ - │ ) │ - └────────────────────────────────────────────┘ + }results: { + """ + ┌─────────────────────────────────────────────┐ + │ Reminder( │ + │ id: 4, │ + │ assignedUserID: nil, │ + │ dueDate: Date(2000-06-25T00:00:00.000Z), │ + │ isCompleted: true, │ + │ isFlagged: false, │ + │ notes: "", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Take a walk", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + ├─────────────────────────────────────────────┤ + │ Reminder( │ + │ id: 7, │ + │ assignedUserID: nil, │ + │ dueDate: Date(2000-12-30T00:00:00.000Z), │ + │ isCompleted: true, │ + │ isFlagged: false, │ + │ notes: "", │ + │ priority: .low, │ + │ remindersListID: 2, │ + │ title: "Get laundry", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + ├─────────────────────────────────────────────┤ + │ Reminder( │ + │ id: 10, │ + │ assignedUserID: nil, │ + │ dueDate: Date(2000-12-30T00:00:00.000Z), │ + │ isCompleted: true, │ + │ isFlagged: false, │ + │ notes: "", │ + │ priority: .medium, │ + │ remindersListID: 3, │ + │ title: "Send weekly emails", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + └─────────────────────────────────────────────┘ """ } } @@ -905,25 +922,26 @@ extension SnapshotTests { } assertQuery(Reminder.limit(1).select { ($0.id, $0.title) }.map { _, _ in }) { """ - SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title" + SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "reminders" LIMIT 1 """ - } results: { - """ - ┌────────────────────────────────────────────┐ - │ Reminder( │ - │ id: 1, │ - │ assignedUserID: 1, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "Milk, Eggs, Apples", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Groceries" │ - │ ) │ - └────────────────────────────────────────────┘ + }results: { + """ + ┌─────────────────────────────────────────────┐ + │ Reminder( │ + │ id: 1, │ + │ assignedUserID: 1, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "Milk, Eggs, Apples", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Groceries", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + └─────────────────────────────────────────────┘ """ } assertQuery(Reminder.limit(1).select { ($0.id, $0.title) }.map { ($1, $0) }) { @@ -975,26 +993,27 @@ extension SnapshotTests { .limit(1) ) { """ - SELECT "r1s"."id", "r1s"."assignedUserID", "r1s"."dueDate", "r1s"."isCompleted", "r1s"."isFlagged", "r1s"."notes", "r1s"."priority", "r1s"."remindersListID", "r1s"."title", "r2s"."id", "r2s"."assignedUserID", "r2s"."dueDate", "r2s"."isCompleted", "r2s"."isFlagged", "r2s"."notes", "r2s"."priority", "r2s"."remindersListID", "r2s"."title" + SELECT "r1s"."id", "r1s"."assignedUserID", "r1s"."dueDate", "r1s"."isCompleted", "r1s"."isFlagged", "r1s"."notes", "r1s"."priority", "r1s"."remindersListID", "r1s"."title", "r1s"."updatedAt", "r2s"."id", "r2s"."assignedUserID", "r2s"."dueDate", "r2s"."isCompleted", "r2s"."isFlagged", "r2s"."notes", "r2s"."priority", "r2s"."remindersListID", "r2s"."title", "r2s"."updatedAt" FROM "reminders" AS "r1s" JOIN "reminders" AS "r2s" ON ("r1s"."id" = "r2s"."id") LIMIT 1 """ - } results: { - """ - ┌────────────────────────────────────────────┬────────────────────────────────────────────┐ - │ Reminder( │ Reminder( │ - │ id: 1, │ id: 1, │ - │ assignedUserID: 1, │ assignedUserID: 1, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ isCompleted: false, │ isCompleted: false, │ - │ isFlagged: false, │ isFlagged: false, │ - │ notes: "Milk, Eggs, Apples", │ notes: "Milk, Eggs, Apples", │ - │ priority: nil, │ priority: nil, │ - │ remindersListID: 1, │ remindersListID: 1, │ - │ title: "Groceries" │ title: "Groceries" │ - │ ) │ ) │ - └────────────────────────────────────────────┴────────────────────────────────────────────┘ + }results: { + """ + ┌─────────────────────────────────────────────┬─────────────────────────────────────────────┐ + │ Reminder( │ Reminder( │ + │ id: 1, │ id: 1, │ + │ assignedUserID: 1, │ assignedUserID: 1, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ isCompleted: false, │ isCompleted: false, │ + │ isFlagged: false, │ isFlagged: false, │ + │ notes: "Milk, Eggs, Apples", │ notes: "Milk, Eggs, Apples", │ + │ priority: nil, │ priority: nil, │ + │ remindersListID: 1, │ remindersListID: 1, │ + │ title: "Groceries", │ title: "Groceries", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ ) │ + └─────────────────────────────────────────────┴─────────────────────────────────────────────┘ """ } } @@ -1030,31 +1049,32 @@ extension SnapshotTests { .select { ($0, $1.jsonGroupArray()) } ) { """ - SELECT "r1s"."id", "r1s"."assignedUserID", "r1s"."dueDate", "r1s"."isCompleted", "r1s"."isFlagged", "r1s"."notes", "r1s"."priority", "r1s"."remindersListID", "r1s"."title", json_group_array(CASE WHEN ("r2s"."id" IS NOT NULL) THEN json_object('id', json_quote("r2s"."id"), 'assignedUserID', json_quote("r2s"."assignedUserID"), 'dueDate', json_quote("r2s"."dueDate"), 'isCompleted', json(CASE "r2s"."isCompleted" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'isFlagged', json(CASE "r2s"."isFlagged" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'notes', json_quote("r2s"."notes"), 'priority', json_quote("r2s"."priority"), 'remindersListID', json_quote("r2s"."remindersListID"), 'title', json_quote("r2s"."title")) END) + SELECT "r1s"."id", "r1s"."assignedUserID", "r1s"."dueDate", "r1s"."isCompleted", "r1s"."isFlagged", "r1s"."notes", "r1s"."priority", "r1s"."remindersListID", "r1s"."title", "r1s"."updatedAt", json_group_array(CASE WHEN ("r2s"."id" IS NOT NULL) THEN json_object('id', json_quote("r2s"."id"), 'assignedUserID', json_quote("r2s"."assignedUserID"), 'dueDate', json_quote("r2s"."dueDate"), 'isCompleted', json(CASE "r2s"."isCompleted" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'isFlagged', json(CASE "r2s"."isFlagged" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'notes', json_quote("r2s"."notes"), 'priority', json_quote("r2s"."priority"), 'remindersListID', json_quote("r2s"."remindersListID"), 'title', json_quote("r2s"."title"), 'updatedAt', json_quote("r2s"."updatedAt")) END) FROM "reminders" AS "r1s" LEFT JOIN "reminders" AS "r2s" ON ("r1s"."id" = "r2s"."id") GROUP BY "r1s"."id" LIMIT 1 """ - } results: { - """ - ┌────────────────────────────────────────────┬────────────────────────────────────────────────┐ - │ Reminder( │ [ │ - │ id: 1, │ [0]: TableAlias( │ - │ assignedUserID: 1, │ base: Reminder( │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ id: 1, │ - │ isCompleted: false, │ assignedUserID: 1, │ - │ isFlagged: false, │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ notes: "Milk, Eggs, Apples", │ isCompleted: false, │ - │ priority: nil, │ isFlagged: false, │ - │ remindersListID: 1, │ notes: "Milk, Eggs, Apples", │ - │ title: "Groceries" │ priority: nil, │ - │ ) │ remindersListID: 1, │ - │ │ title: "Groceries" │ - │ │ ) │ - │ │ ) │ - │ │ ] │ - └────────────────────────────────────────────┴────────────────────────────────────────────────┘ + }results: { + """ + ┌─────────────────────────────────────────────┬─────────────────────────────────────────────────┐ + │ Reminder( │ [ │ + │ id: 1, │ [0]: TableAlias( │ + │ assignedUserID: 1, │ base: Reminder( │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ id: 1, │ + │ isCompleted: false, │ assignedUserID: 1, │ + │ isFlagged: false, │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ notes: "Milk, Eggs, Apples", │ isCompleted: false, │ + │ priority: nil, │ isFlagged: false, │ + │ remindersListID: 1, │ notes: "Milk, Eggs, Apples", │ + │ title: "Groceries", │ priority: nil, │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ remindersListID: 1, │ + │ ) │ title: "Groceries", │ + │ │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ │ ) │ + │ │ ) │ + │ │ ] │ + └─────────────────────────────────────────────┴─────────────────────────────────────────────────┘ """ } } @@ -1123,50 +1143,53 @@ extension SnapshotTests { .where { $1.isHighPriority.ifnull(false) } ) { """ - SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position", "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title" + SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position", "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "remindersLists" LEFT JOIN "reminders" ON ("remindersLists"."id" = "reminders"."remindersListID") WHERE ifnull(("reminders"."priority" IS 3), 0) """ - } results: { - """ - ┌──────────────────────┬────────────────────────────────────────────┐ - │ RemindersList( │ Reminder( │ - │ id: 1, │ id: 3, │ - │ color: 4889071, │ assignedUserID: nil, │ - │ title: "Personal", │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ position: 0 │ isCompleted: false, │ - │ ) │ isFlagged: false, │ - │ │ notes: "Ask about diet", │ - │ │ priority: .high, │ - │ │ remindersListID: 1, │ - │ │ title: "Doctor appointment" │ - │ │ ) │ - ├──────────────────────┼────────────────────────────────────────────┤ - │ RemindersList( │ Reminder( │ - │ id: 2, │ id: 6, │ - │ color: 15567157, │ assignedUserID: nil, │ - │ title: "Family", │ dueDate: Date(2001-01-03T00:00:00.000Z), │ - │ position: 0 │ isCompleted: false, │ - │ ) │ isFlagged: true, │ - │ │ notes: "", │ - │ │ priority: .high, │ - │ │ remindersListID: 2, │ - │ │ title: "Pick up kids from school" │ - │ │ ) │ - ├──────────────────────┼────────────────────────────────────────────┤ - │ RemindersList( │ Reminder( │ - │ id: 2, │ id: 8, │ - │ color: 15567157, │ assignedUserID: nil, │ - │ title: "Family", │ dueDate: Date(2001-01-05T00:00:00.000Z), │ - │ position: 0 │ isCompleted: false, │ - │ ) │ isFlagged: false, │ - │ │ notes: "", │ - │ │ priority: .high, │ - │ │ remindersListID: 2, │ - │ │ title: "Take out trash" │ - │ │ ) │ - └──────────────────────┴────────────────────────────────────────────┘ + }results: { + """ + ┌──────────────────────┬─────────────────────────────────────────────┐ + │ RemindersList( │ Reminder( │ + │ id: 1, │ id: 3, │ + │ color: 4889071, │ assignedUserID: nil, │ + │ title: "Personal", │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ position: 0 │ isCompleted: false, │ + │ ) │ isFlagged: false, │ + │ │ notes: "Ask about diet", │ + │ │ priority: .high, │ + │ │ remindersListID: 1, │ + │ │ title: "Doctor appointment", │ + │ │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ │ ) │ + ├──────────────────────┼─────────────────────────────────────────────┤ + │ RemindersList( │ Reminder( │ + │ id: 2, │ id: 6, │ + │ color: 15567157, │ assignedUserID: nil, │ + │ title: "Family", │ dueDate: Date(2001-01-03T00:00:00.000Z), │ + │ position: 0 │ isCompleted: false, │ + │ ) │ isFlagged: true, │ + │ │ notes: "", │ + │ │ priority: .high, │ + │ │ remindersListID: 2, │ + │ │ title: "Pick up kids from school", │ + │ │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ │ ) │ + ├──────────────────────┼─────────────────────────────────────────────┤ + │ RemindersList( │ Reminder( │ + │ id: 2, │ id: 8, │ + │ color: 15567157, │ assignedUserID: nil, │ + │ title: "Family", │ dueDate: Date(2001-01-05T00:00:00.000Z), │ + │ position: 0 │ isCompleted: false, │ + │ ) │ isFlagged: false, │ + │ │ notes: "", │ + │ │ priority: .high, │ + │ │ remindersListID: 2, │ + │ │ title: "Take out trash", │ + │ │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ │ ) │ + └──────────────────────┴─────────────────────────────────────────────┘ """ } } diff --git a/Tests/StructuredQueriesTests/Support/Schema.swift b/Tests/StructuredQueriesTests/Support/Schema.swift index fb1326af..60cbd28f 100644 --- a/Tests/StructuredQueriesTests/Support/Schema.swift +++ b/Tests/StructuredQueriesTests/Support/Schema.swift @@ -28,6 +28,7 @@ struct Reminder: Codable, Equatable, Identifiable { var priority: Priority? var remindersListID: Int var title = "" + var updatedAt: Date = Date(timeIntervalSinceReferenceDate: 1234567890) static func searching(_ text: String) -> Where { Self.where { $0.title.collate(.nocase).contains(text) @@ -107,7 +108,8 @@ extension Database { "remindersListID" INTEGER NOT NULL REFERENCES "remindersLists"("id") ON DELETE CASCADE, "notes" TEXT NOT NULL DEFAULT '', "priority" INTEGER, - "title" TEXT NOT NULL DEFAULT '' + "title" TEXT NOT NULL DEFAULT '', + "updatedAt" TEXT NOT NULL DEFAULT (datetime('subsec')) ) """ ) diff --git a/Tests/StructuredQueriesTests/TriggersTests.swift b/Tests/StructuredQueriesTests/TriggersTests.swift index 3e3c3ef6..e34c0652 100644 --- a/Tests/StructuredQueriesTests/TriggersTests.swift +++ b/Tests/StructuredQueriesTests/TriggersTests.swift @@ -72,5 +72,43 @@ extension SnapshotTests { ) } } + + @Test func afterUpdateTouch() { + assertQuery( + RemindersList.createTemporaryTrigger( + afterUpdateTouch: { + $0.position += 1 + } + ) + ) { + """ + CREATE TEMPORARY TRIGGER + "after_update_on_remindersLists@StructuredQueriesTests/TriggersTests.swift:78:45" + AFTER UPDATE ON "remindersLists" + FOR EACH ROW BEGIN + UPDATE "remindersLists" + SET "position" = ("remindersLists"."position" + 1) + WHERE ("remindersLists"."rowid" = "new"."rowid"); + END + """ + } + } + + @Test func afterUpdateTouchDate() { + assertQuery( + Reminder.createTemporaryTrigger(afterUpdateTouch: \.updatedAt) + ) { + """ + CREATE TEMPORARY TRIGGER + "after_update_on_reminders@StructuredQueriesTests/TriggersTests.swift:99:40" + AFTER UPDATE ON "reminders" + FOR EACH ROW BEGIN + UPDATE "reminders" + SET "updatedAt" = datetime('subsec') + WHERE ("reminders"."rowid" = "new"."rowid"); + END + """ + } + } } } diff --git a/Tests/StructuredQueriesTests/UpdateTests.swift b/Tests/StructuredQueriesTests/UpdateTests.swift index 1fcbc3f8..9144bf54 100644 --- a/Tests/StructuredQueriesTests/UpdateTests.swift +++ b/Tests/StructuredQueriesTests/UpdateTests.swift @@ -101,25 +101,26 @@ extension SnapshotTests { ) { """ UPDATE "reminders" - SET "assignedUserID" = 1, "dueDate" = '2001-01-01 00:00:00.000', "isCompleted" = 1, "isFlagged" = 0, "notes" = 'Milk, Eggs, Apples', "priority" = NULL, "remindersListID" = 1, "title" = 'Groceries' + SET "assignedUserID" = 1, "dueDate" = '2001-01-01 00:00:00.000', "isCompleted" = 1, "isFlagged" = 0, "notes" = 'Milk, Eggs, Apples', "priority" = NULL, "remindersListID" = 1, "title" = 'Groceries', "updatedAt" = '2040-02-14 23:31:30.000' WHERE ("reminders"."id" = 1) - RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title" - """ - } results: { - """ - ┌────────────────────────────────────────────┐ - │ Reminder( │ - │ id: 1, │ - │ assignedUserID: 1, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ isCompleted: true, │ - │ isFlagged: false, │ - │ notes: "Milk, Eggs, Apples", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Groceries" │ - │ ) │ - └────────────────────────────────────────────┘ + RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt" + """ + }results: { + """ + ┌─────────────────────────────────────────────┐ + │ Reminder( │ + │ id: 1, │ + │ assignedUserID: 1, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ isCompleted: true, │ + │ isFlagged: false, │ + │ notes: "Milk, Eggs, Apples", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Groceries", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + └─────────────────────────────────────────────┘ """ } } @@ -270,23 +271,24 @@ extension SnapshotTests { UPDATE "reminders" AS "rs" SET "title" = ("rs"."title" || ' 2') WHERE ("rs"."id" = 1) - RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title" - """ - } results: { - """ - ┌────────────────────────────────────────────┐ - │ Reminder( │ - │ id: 1, │ - │ assignedUserID: 1, │ - │ dueDate: Date(2001-01-01T00:00:00.000Z), │ - │ isCompleted: false, │ - │ isFlagged: false, │ - │ notes: "Milk, Eggs, Apples", │ - │ priority: nil, │ - │ remindersListID: 1, │ - │ title: "Groceries 2" │ - │ ) │ - └────────────────────────────────────────────┘ + RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt" + """ + }results: { + """ + ┌─────────────────────────────────────────────┐ + │ Reminder( │ + │ id: 1, │ + │ assignedUserID: 1, │ + │ dueDate: Date(2001-01-01T00:00:00.000Z), │ + │ isCompleted: false, │ + │ isFlagged: false, │ + │ notes: "Milk, Eggs, Apples", │ + │ priority: nil, │ + │ remindersListID: 1, │ + │ title: "Groceries 2", │ + │ updatedAt: Date(2040-02-14T23:31:30.000Z) │ + │ ) │ + └─────────────────────────────────────────────┘ """ } } @@ -340,7 +342,7 @@ extension SnapshotTests { ) { """ UPDATE "reminders" - SET "assignedUserID" = NULL, "dueDate" = NULL, "isCompleted" = 1, "isFlagged" = 0, "notes" = '', "priority" = NULL, "remindersListID" = 1, "title" = 'Buy iPhone' + SET "assignedUserID" = NULL, "dueDate" = NULL, "isCompleted" = 1, "isFlagged" = 0, "notes" = '', "priority" = NULL, "remindersListID" = 1, "title" = 'Buy iPhone', "updatedAt" = '2040-02-14 23:31:30.000' WHERE ("reminders"."id" = 100) """ } diff --git a/Tests/StructuredQueriesTests/WhereTests.swift b/Tests/StructuredQueriesTests/WhereTests.swift index ca72bcf9..1558fe74 100644 --- a/Tests/StructuredQueriesTests/WhereTests.swift +++ b/Tests/StructuredQueriesTests/WhereTests.swift @@ -129,7 +129,7 @@ extension SnapshotTests { .where { $1.isCompleted } ) { """ - SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title" + SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position", "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "remindersLists" LEFT JOIN "reminders" ON ("remindersLists"."id" = "reminders"."remindersListID") WHERE ("remindersLists"."id" = 4) AND "reminders"."isCompleted" From e29eccc9c71d6280e01d902199c1b86703daff70 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 18 Jun 2025 11:19:33 -0700 Subject: [PATCH 21/32] wip --- Sources/StructuredQueriesCore/Triggers.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index af1e34f0..e2ee03ee 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -296,7 +296,7 @@ public struct TemporaryTrigger: Statement { /// /// - Parameter ifExists: Adds an `IF EXISTS` condition to the `DROP TRIGGER`. /// - Returns: A `DROP TRIGGER` statement for this trigger. - public func drop(ifExists: Bool = false) -> some Statement { + public func drop(ifExists: Bool = false) -> some Statement<()> { var query: QueryFragment = "DROP TRIGGER" if ifExists { query.append(" IF EXISTS") From bcac7860fcef9ca1da97d8aa96737ed90465a01d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 18 Jun 2025 12:13:50 -0700 Subject: [PATCH 22/32] wip --- Sources/StructuredQueriesCore/Triggers.swift | 6 ++-- .../TriggersTests.swift | 34 +++++++++++-------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index e2ee03ee..a21545b0 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -150,7 +150,7 @@ public struct TemporaryTrigger: Statement { public typealias Joins = () public typealias QueryValue = () - fileprivate enum When: String { + fileprivate enum When: QueryFragment { case before = "BEFORE" case after = "AFTER" } @@ -263,7 +263,7 @@ public struct TemporaryTrigger: Statement { } statement = begin case .delete(let begin): - query.append(" DELETE") + query.append("DELETE") statement = begin } query.append(" ON \(On.self)\(.newlineOrSpace)FOR EACH ROW") @@ -311,7 +311,7 @@ public struct TemporaryTrigger: Statement { query.append(" IF NOT EXISTS") } query.append("\(.newlineOrSpace)\(triggerName.indented())") - query.append("\(.newlineOrSpace)\(raw: when.rawValue) \(operation)") + query.append("\(.newlineOrSpace)\(when.rawValue) \(operation)") return query.segments.reduce(into: QueryFragment()) { switch $1 { case .sql(let sql): diff --git a/Tests/StructuredQueriesTests/TriggersTests.swift b/Tests/StructuredQueriesTests/TriggersTests.swift index e34c0652..3425ca55 100644 --- a/Tests/StructuredQueriesTests/TriggersTests.swift +++ b/Tests/StructuredQueriesTests/TriggersTests.swift @@ -8,20 +8,19 @@ import Testing extension SnapshotTests { @Suite struct TriggersTests { @Test func basics() { - assertQuery( - RemindersList.createTemporaryTrigger( - after: .insert { new in - RemindersList - .update { - $0.position = RemindersList.select { ($0.position.max() ?? -1) + 1 } - } - .where { $0.id.eq(new.id) } - } - ) - ) { + let trigger = RemindersList.createTemporaryTrigger( + after: .insert { new in + RemindersList + .update { + $0.position = RemindersList.select { ($0.position.max() ?? -1) + 1 } + } + .where { $0.id.eq(new.id) } + } + ) + assertQuery(trigger) { """ CREATE TEMPORARY TRIGGER - "after_insert_on_remindersLists@StructuredQueriesTests/TriggersTests.swift:12:45" + "after_insert_on_remindersLists@StructuredQueriesTests/TriggersTests.swift:11:57" AFTER INSERT ON "remindersLists" FOR EACH ROW BEGIN UPDATE "remindersLists" @@ -33,6 +32,11 @@ extension SnapshotTests { END """ } + assertQuery(trigger.drop()) { + """ + DROP TRIGGER "after_insert_on_remindersLists@StructuredQueriesTests/TriggersTests.swift:11:57" + """ + } } @Test func dateDiagnostic() { @@ -48,7 +52,7 @@ extension SnapshotTests { ) { """ CREATE TEMPORARY TRIGGER - "after_update_on_reminders@StructuredQueriesTests/TriggersTests.swift:41:42" + "after_update_on_reminders@StructuredQueriesTests/TriggersTests.swift:45:42" AFTER UPDATE ON "reminders" FOR EACH ROW BEGIN UPDATE "reminders" @@ -83,7 +87,7 @@ extension SnapshotTests { ) { """ CREATE TEMPORARY TRIGGER - "after_update_on_remindersLists@StructuredQueriesTests/TriggersTests.swift:78:45" + "after_update_on_remindersLists@StructuredQueriesTests/TriggersTests.swift:82:45" AFTER UPDATE ON "remindersLists" FOR EACH ROW BEGIN UPDATE "remindersLists" @@ -100,7 +104,7 @@ extension SnapshotTests { ) { """ CREATE TEMPORARY TRIGGER - "after_update_on_reminders@StructuredQueriesTests/TriggersTests.swift:99:40" + "after_update_on_reminders@StructuredQueriesTests/TriggersTests.swift:103:40" AFTER UPDATE ON "reminders" FOR EACH ROW BEGIN UPDATE "reminders" From 2d1bffe2b14a93d7d5544c77677a4df819ffc4bc Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 19 Jun 2025 15:34:26 -0700 Subject: [PATCH 23/32] more overloads --- Sources/StructuredQueriesCore/Triggers.swift | 62 ++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index a21545b0..8a6fae49 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -143,6 +143,68 @@ extension Table { column: column ) } + + public static func createTemporaryTrigger( + _ name: String? = nil, + ifNotExists: Bool = false, + afterUpdateTouch date: KeyPath>, + fileID: StaticString = #fileID, + line: UInt = #line, + column: UInt = #column + ) -> TemporaryTrigger { + Self.createTemporaryTrigger( + name, + ifNotExists: ifNotExists, + afterUpdateTouch: { + $0[dynamicMember: date] = SQLQueryExpression("datetime('subsec')") + }, + fileID: fileID, + line: line, + column: column + ) + } + + public static func createTemporaryTrigger( + _ name: String? = nil, + ifNotExists: Bool = false, + afterInsertTouch updates: (inout Updates) -> Void, + fileID: StaticString = #fileID, + line: UInt = #line, + column: UInt = #column + ) -> TemporaryTrigger { + Self.createTemporaryTrigger( + name, + ifNotExists: ifNotExists, + after: .insert { new in + Self + .where { $0.rowid.eq(new.rowid) } + .update { updates(&$0) } + }, + fileID: fileID, + line: line, + column: column + ) + } + + public static func createTemporaryTrigger( + _ name: String? = nil, + ifNotExists: Bool = false, + afterInsertTouch date: KeyPath>, + fileID: StaticString = #fileID, + line: UInt = #line, + column: UInt = #column + ) -> TemporaryTrigger { + Self.createTemporaryTrigger( + name, + ifNotExists: ifNotExists, + afterUpdateTouch: { + $0[dynamicMember: date] = SQLQueryExpression("datetime('subsec')") + }, + fileID: fileID, + line: line, + column: column + ) + } } public struct TemporaryTrigger: Statement { From 2c31653df38e96c8bc93798cedb546ac392148d9 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 19 Jun 2025 15:39:03 -0700 Subject: [PATCH 24/32] wip --- Sources/StructuredQueriesCore/Triggers.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index 8a6fae49..d3394cc5 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -197,7 +197,7 @@ extension Table { Self.createTemporaryTrigger( name, ifNotExists: ifNotExists, - afterUpdateTouch: { + afterInsertTouch: { $0[dynamicMember: date] = SQLQueryExpression("datetime('subsec')") }, fileID: fileID, From 618f8bd77e88e18e72f92feadc4ee414143e9849 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 19 Jun 2025 23:07:45 -0700 Subject: [PATCH 25/32] wip --- Sources/StructuredQueriesCore/Triggers.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index d3394cc5..6e3e54a8 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -233,8 +233,8 @@ public struct TemporaryTrigger: Statement { /// - condition: A predicate that must be satisfied to perform the given statement. /// - Returns: An `AFTER INSERT` trigger operation. public static func insert( - forEachRow perform: (New) -> some Statement, - when condition: ((New) -> any QueryExpression)? = nil + forEachRow perform: (_ new: New) -> some Statement, + when condition: ((_ new: New) -> any QueryExpression)? = nil ) -> Self { Self( kind: .insert(operation: perform(On.as(_New.self).columns).query), @@ -249,8 +249,8 @@ public struct TemporaryTrigger: Statement { /// - condition: A predicate that must be satisfied to perform the given statement. /// - Returns: An `AFTER UPDATE` trigger operation. public static func update( - forEachRow perform: (Old, New) -> some Statement, - when condition: ((Old, New) -> any QueryExpression)? = nil + forEachRow perform: (_ old: Old, _ new: New) -> some Statement, + when condition: ((_ old: Old, _ new: New) -> any QueryExpression)? = nil ) -> Self { update( of: { _ in }, @@ -268,8 +268,8 @@ public struct TemporaryTrigger: Statement { /// - Returns: An `AFTER UPDATE` trigger operation. public static func update( of columns: (On.TableColumns) -> (repeat TableColumn), - forEachRow perform: (Old, New) -> some Statement, - when condition: ((Old, New) -> any QueryExpression)? = nil + forEachRow perform: (_ old: Old, _ new: New) -> some Statement, + when condition: ((_ old: Old, _ new: New) -> any QueryExpression)? = nil ) -> Self { var columnNames: [String] = [] for column in repeat each columns(On.columns) { @@ -291,8 +291,8 @@ public struct TemporaryTrigger: Statement { /// - condition: A predicate that must be satisfied to perform the given statement. /// - Returns: An `AFTER DELETE` trigger operation. public static func delete( - forEachRow perform: (Old) -> some Statement, - when condition: ((Old) -> any QueryExpression)? = nil + forEachRow perform: (_ old: Old) -> some Statement, + when condition: ((_ old: Old) -> any QueryExpression)? = nil ) -> Self { Self( kind: .delete(operation: perform(On.as(_Old.self).columns).query), From 473615edbd7abacfc979be819d6ca1f4e103166d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 19 Jun 2025 23:09:21 -0700 Subject: [PATCH 26/32] wip --- Sources/StructuredQueriesCore/Triggers.swift | 28 +++----------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index 6e3e54a8..5ae9afd6 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -124,30 +124,10 @@ extension Table { /// - line: The source `#line` associated with the trigger. /// - column: The source `#column` associated with the trigger. /// - Returns: A temporary trigger. - public static func createTemporaryTrigger( + public static func createTemporaryTrigger>( _ name: String? = nil, ifNotExists: Bool = false, - afterUpdateTouch date: KeyPath>, - fileID: StaticString = #fileID, - line: UInt = #line, - column: UInt = #column - ) -> TemporaryTrigger { - Self.createTemporaryTrigger( - name, - ifNotExists: ifNotExists, - afterUpdateTouch: { - $0[dynamicMember: date] = SQLQueryExpression("datetime('subsec')") - }, - fileID: fileID, - line: line, - column: column - ) - } - - public static func createTemporaryTrigger( - _ name: String? = nil, - ifNotExists: Bool = false, - afterUpdateTouch date: KeyPath>, + afterUpdateTouch date: KeyPath>, fileID: StaticString = #fileID, line: UInt = #line, column: UInt = #column @@ -186,10 +166,10 @@ extension Table { ) } - public static func createTemporaryTrigger( + public static func createTemporaryTrigger>( _ name: String? = nil, ifNotExists: Bool = false, - afterInsertTouch date: KeyPath>, + afterInsertTouch date: KeyPath>, fileID: StaticString = #fileID, line: UInt = #line, column: UInt = #column From df1068b08f642f5eee074b64e817d84ebcc36cf4 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 21 Jun 2025 09:35:11 -0700 Subject: [PATCH 27/32] Support multiple statements in triggers --- Sources/StructuredQueriesCore/Triggers.swift | 43 +++++++++++++------ .../OperatorsTests.swift | 4 +- .../TriggersTests.swift | 36 ++++++++++++++++ 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index 5ae9afd6..0dfa4356 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -213,11 +213,11 @@ public struct TemporaryTrigger: Statement { /// - condition: A predicate that must be satisfied to perform the given statement. /// - Returns: An `AFTER INSERT` trigger operation. public static func insert( - forEachRow perform: (_ new: New) -> some Statement, + @MultiStatementBuilder forEachRow perform: (_ new: New) -> [any Statement], when condition: ((_ new: New) -> any QueryExpression)? = nil ) -> Self { Self( - kind: .insert(operation: perform(On.as(_New.self).columns).query), + kind: .insert(operation: perform(On.as(_New.self).columns)), when: condition?(On.as(_New.self).columns).queryFragment ) } @@ -229,7 +229,7 @@ public struct TemporaryTrigger: Statement { /// - condition: A predicate that must be satisfied to perform the given statement. /// - Returns: An `AFTER UPDATE` trigger operation. public static func update( - forEachRow perform: (_ old: Old, _ new: New) -> some Statement, + @MultiStatementBuilder forEachRow perform: (_ old: Old, _ new: New) -> [any Statement], when condition: ((_ old: Old, _ new: New) -> any QueryExpression)? = nil ) -> Self { update( @@ -248,7 +248,7 @@ public struct TemporaryTrigger: Statement { /// - Returns: An `AFTER UPDATE` trigger operation. public static func update( of columns: (On.TableColumns) -> (repeat TableColumn), - forEachRow perform: (_ old: Old, _ new: New) -> some Statement, + @MultiStatementBuilder forEachRow perform: (_ old: Old, _ new: New) -> [any Statement], when condition: ((_ old: Old, _ new: New) -> any QueryExpression)? = nil ) -> Self { var columnNames: [String] = [] @@ -257,7 +257,7 @@ public struct TemporaryTrigger: Statement { } return Self( kind: .update( - operation: perform(On.as(_Old.self).columns, On.as(_New.self).columns).query, + operation: perform(On.as(_Old.self).columns, On.as(_New.self).columns), columnNames: columnNames ), when: condition?(On.as(_Old.self).columns, On.as(_New.self).columns).queryFragment @@ -271,31 +271,32 @@ public struct TemporaryTrigger: Statement { /// - condition: A predicate that must be satisfied to perform the given statement. /// - Returns: An `AFTER DELETE` trigger operation. public static func delete( - forEachRow perform: (_ old: Old) -> some Statement, + @MultiStatementBuilder forEachRow perform: (_ old: Old) -> [any Statement], when condition: ((_ old: Old) -> any QueryExpression)? = nil ) -> Self { Self( - kind: .delete(operation: perform(On.as(_Old.self).columns).query), + kind: .delete(operation: perform(On.as(_Old.self).columns)), when: condition?(On.as(_Old.self).columns).queryFragment ) } private enum Kind { - case insert(operation: QueryFragment) - case update(operation: QueryFragment, columnNames: [String]) - case delete(operation: QueryFragment) + case insert(operation: [any Statement]) + case update(operation: [any Statement], columnNames: [String]) + case delete(operation: [any Statement]) } private let kind: Kind private let when: QueryFragment? public var queryFragment: QueryFragment { + let statementSeparator: QueryFragment = ";\(.newlineOrSpace)" var query: QueryFragment = "" let statement: QueryFragment switch kind { case .insert(let begin): query.append("INSERT") - statement = begin + statement = begin.map(\.query).joined(separator: statementSeparator) case .update(let begin, let columnNames): query.append("UPDATE") if !columnNames.isEmpty { @@ -303,10 +304,10 @@ public struct TemporaryTrigger: Statement { " OF \(columnNames.map { QueryFragment(quote: $0) }.joined(separator: ", "))" ) } - statement = begin + statement = begin.map(\.query).joined(separator: statementSeparator) case .delete(let begin): query.append("DELETE") - statement = begin + statement = begin.map(\.query).joined(separator: statementSeparator) } query.append(" ON \(On.self)\(.newlineOrSpace)FOR EACH ROW") if let when { @@ -425,3 +426,19 @@ public struct TemporaryTrigger: Statement { return "\(quote: name)" } } + +@resultBuilder +public enum MultiStatementBuilder { + public static func buildPartialBlock( + first: some Statement + ) -> [some Statement] { + [first] + } + + public static func buildPartialBlock( + accumulated: [any Statement], + next: some Statement + ) -> [any Statement] { + accumulated + [next] + } +} diff --git a/Tests/StructuredQueriesTests/OperatorsTests.swift b/Tests/StructuredQueriesTests/OperatorsTests.swift index c83e1706..0bca3d79 100644 --- a/Tests/StructuredQueriesTests/OperatorsTests.swift +++ b/Tests/StructuredQueriesTests/OperatorsTests.swift @@ -574,11 +574,11 @@ extension SnapshotTests { assertQuery(Values(Reminder.exists())) { """ SELECT EXISTS ( - SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title" + SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "reminders" ) """ - } results: { + }results: { """ ┌──────┐ │ true │ diff --git a/Tests/StructuredQueriesTests/TriggersTests.swift b/Tests/StructuredQueriesTests/TriggersTests.swift index 3425ca55..49071dce 100644 --- a/Tests/StructuredQueriesTests/TriggersTests.swift +++ b/Tests/StructuredQueriesTests/TriggersTests.swift @@ -114,5 +114,41 @@ extension SnapshotTests { """ } } + + @Test func multiStatement() { + let trigger = RemindersList.createTemporaryTrigger( + after: .insert { new in + RemindersList + .update { + $0.position = RemindersList.select { ($0.position.max() ?? -1) + 1 } + } + .where { $0.id.eq(new.id) } + RemindersList + .where { $0.position.eq(0) } + .delete() + RemindersList + .select(\.position) + } + ) + assertQuery(trigger) { + """ + CREATE TEMPORARY TRIGGER + "after_insert_on_remindersLists@StructuredQueriesTests/TriggersTests.swift:119:57" + AFTER INSERT ON "remindersLists" + FOR EACH ROW BEGIN + UPDATE "remindersLists" + SET "position" = ( + SELECT (coalesce(max("remindersLists"."position"), -1) + 1) + FROM "remindersLists" + ) + WHERE ("remindersLists"."id" = "new"."id"); + DELETE FROM "remindersLists" + WHERE ("remindersLists"."position" = 0); + SELECT "remindersLists"."position" + FROM "remindersLists"; + END + """ + } + } } } From 4ddfd2672138d1b3d258d93d7729f0d6ba8be021 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 21 Jun 2025 10:27:03 -0700 Subject: [PATCH 28/32] Reuse query fragment builder --- .../QueryFragmentBuilder.swift | 15 ++++++ Sources/StructuredQueriesCore/Triggers.swift | 54 ++++++++----------- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/Sources/StructuredQueriesCore/QueryFragmentBuilder.swift b/Sources/StructuredQueriesCore/QueryFragmentBuilder.swift index 19e7e1ed..95a1939b 100644 --- a/Sources/StructuredQueriesCore/QueryFragmentBuilder.swift +++ b/Sources/StructuredQueriesCore/QueryFragmentBuilder.swift @@ -42,3 +42,18 @@ extension QueryFragmentBuilder<()> { Array(repeat each expression) } } + +extension QueryFragmentBuilder { + public static func buildExpression( + _ expression: some Statement + ) -> [QueryFragment] { + [expression.query] + } + + public static func buildBlock( + _ first: [QueryFragment], + _ rest: [QueryFragment]... + ) -> [QueryFragment] { + first + rest.flatMap(\.self) + } +} diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index 0dfa4356..b112f782 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -213,11 +213,12 @@ public struct TemporaryTrigger: Statement { /// - condition: A predicate that must be satisfied to perform the given statement. /// - Returns: An `AFTER INSERT` trigger operation. public static func insert( - @MultiStatementBuilder forEachRow perform: (_ new: New) -> [any Statement], + @QueryFragmentBuilder + forEachRow perform: (_ new: New) -> [QueryFragment], when condition: ((_ new: New) -> any QueryExpression)? = nil ) -> Self { Self( - kind: .insert(operation: perform(On.as(_New.self).columns)), + kind: .insert(operations: perform(On.as(_New.self).columns)), when: condition?(On.as(_New.self).columns).queryFragment ) } @@ -229,7 +230,8 @@ public struct TemporaryTrigger: Statement { /// - condition: A predicate that must be satisfied to perform the given statement. /// - Returns: An `AFTER UPDATE` trigger operation. public static func update( - @MultiStatementBuilder forEachRow perform: (_ old: Old, _ new: New) -> [any Statement], + @QueryFragmentBuilder + forEachRow perform: (_ old: Old, _ new: New) -> [QueryFragment], when condition: ((_ old: Old, _ new: New) -> any QueryExpression)? = nil ) -> Self { update( @@ -248,7 +250,8 @@ public struct TemporaryTrigger: Statement { /// - Returns: An `AFTER UPDATE` trigger operation. public static func update( of columns: (On.TableColumns) -> (repeat TableColumn), - @MultiStatementBuilder forEachRow perform: (_ old: Old, _ new: New) -> [any Statement], + @QueryFragmentBuilder + forEachRow perform: (_ old: Old, _ new: New) -> [QueryFragment], when condition: ((_ old: Old, _ new: New) -> any QueryExpression)? = nil ) -> Self { var columnNames: [String] = [] @@ -257,7 +260,7 @@ public struct TemporaryTrigger: Statement { } return Self( kind: .update( - operation: perform(On.as(_Old.self).columns, On.as(_New.self).columns), + operations: perform(On.as(_Old.self).columns, On.as(_New.self).columns), columnNames: columnNames ), when: condition?(On.as(_Old.self).columns, On.as(_New.self).columns).queryFragment @@ -271,32 +274,32 @@ public struct TemporaryTrigger: Statement { /// - condition: A predicate that must be satisfied to perform the given statement. /// - Returns: An `AFTER DELETE` trigger operation. public static func delete( - @MultiStatementBuilder forEachRow perform: (_ old: Old) -> [any Statement], + @QueryFragmentBuilder + forEachRow perform: (_ old: Old) -> [QueryFragment], when condition: ((_ old: Old) -> any QueryExpression)? = nil ) -> Self { Self( - kind: .delete(operation: perform(On.as(_Old.self).columns)), + kind: .delete(operations: perform(On.as(_Old.self).columns)), when: condition?(On.as(_Old.self).columns).queryFragment ) } private enum Kind { - case insert(operation: [any Statement]) - case update(operation: [any Statement], columnNames: [String]) - case delete(operation: [any Statement]) + case insert(operations: [QueryFragment]) + case update(operations: [QueryFragment], columnNames: [String]) + case delete(operations: [QueryFragment]) } private let kind: Kind private let when: QueryFragment? public var queryFragment: QueryFragment { - let statementSeparator: QueryFragment = ";\(.newlineOrSpace)" var query: QueryFragment = "" - let statement: QueryFragment + let statements: [QueryFragment] switch kind { case .insert(let begin): query.append("INSERT") - statement = begin.map(\.query).joined(separator: statementSeparator) + statements = begin case .update(let begin, let columnNames): query.append("UPDATE") if !columnNames.isEmpty { @@ -304,17 +307,20 @@ public struct TemporaryTrigger: Statement { " OF \(columnNames.map { QueryFragment(quote: $0) }.joined(separator: ", "))" ) } - statement = begin.map(\.query).joined(separator: statementSeparator) + statements = begin case .delete(let begin): query.append("DELETE") - statement = begin.map(\.query).joined(separator: statementSeparator) + statements = begin } query.append(" ON \(On.self)\(.newlineOrSpace)FOR EACH ROW") if let when { query.append(" WHEN \(when)") } query.append(" BEGIN") - query.append("\(.newlineOrSpace)\(statement.indented());\(.newlineOrSpace)END") + for statement in statements { + query.append("\(.newlineOrSpace)\(statement.indented());") + } + query.append("\(.newlineOrSpace)END") return query } @@ -426,19 +432,3 @@ public struct TemporaryTrigger: Statement { return "\(quote: name)" } } - -@resultBuilder -public enum MultiStatementBuilder { - public static func buildPartialBlock( - first: some Statement - ) -> [some Statement] { - [first] - } - - public static func buildPartialBlock( - accumulated: [any Statement], - next: some Statement - ) -> [any Statement] { - accumulated + [next] - } -} From 8b0e6fde962658b2804857aaff7017b7b3486120 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 21 Jun 2025 10:27:31 -0700 Subject: [PATCH 29/32] wip --- Sources/StructuredQueriesCore/Triggers.swift | 2 +- .../CommonTableExpressionTests.swift | 2 +- .../StructuredQueriesTests/DeleteTests.swift | 2 +- .../StructuredQueriesTests/InsertTests.swift | 24 +++++++++---------- .../JSONFunctionsTests.swift | 4 ++-- Tests/StructuredQueriesTests/LiveTests.swift | 4 ++-- .../OperatorsTests.swift | 8 +++---- .../SQLMacroTests.swift | 6 ++--- .../StructuredQueriesTests/SelectTests.swift | 16 ++++++------- .../Support/Schema.swift | 2 +- .../StructuredQueriesTests/UpdateTests.swift | 4 ++-- 11 files changed, 37 insertions(+), 37 deletions(-) diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index b112f782..c4eefc44 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -114,7 +114,7 @@ extension Table { /// > Important: A name for the trigger is automatically derived from the arguments if one is not /// > provided. If you build your own trigger helper that call this function, then your helper /// > should also take fileID, line and column arguments and pass them to this function. - /// + /// /// - Parameters: /// - name: The trigger's name. By default a unique name is generated depending using the table, /// operation, and source location. diff --git a/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift b/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift index 4434bad3..e5f911bb 100644 --- a/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift +++ b/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift @@ -125,7 +125,7 @@ extension SnapshotTests { LIMIT 1 RETURNING "id", "title" """ - }results: { + } results: { """ ┌────┬─────────────┐ │ 11 │ "Groceries" │ diff --git a/Tests/StructuredQueriesTests/DeleteTests.swift b/Tests/StructuredQueriesTests/DeleteTests.swift index a34e49b3..05eddb93 100644 --- a/Tests/StructuredQueriesTests/DeleteTests.swift +++ b/Tests/StructuredQueriesTests/DeleteTests.swift @@ -49,7 +49,7 @@ extension SnapshotTests { WHERE ("reminders"."id" = 1) RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt" """ - }results: { + } results: { """ ┌─────────────────────────────────────────────┐ │ Reminder( │ diff --git a/Tests/StructuredQueriesTests/InsertTests.swift b/Tests/StructuredQueriesTests/InsertTests.swift index 4e510d8c..a0843eef 100644 --- a/Tests/StructuredQueriesTests/InsertTests.swift +++ b/Tests/StructuredQueriesTests/InsertTests.swift @@ -26,7 +26,7 @@ extension SnapshotTests { ON CONFLICT DO UPDATE SET "title" = ("reminders"."title" || ' Copy') RETURNING "id" """ - }results: { + } results: { """ ┌────┐ │ 11 │ @@ -49,7 +49,7 @@ extension SnapshotTests { (1) RETURNING "id" """ - }results: { + } results: { """ ┌────┐ │ 11 │ @@ -89,7 +89,7 @@ extension SnapshotTests { (100, NULL, NULL, 0, 0, '', NULL, 1, 'Check email', '2040-02-14 23:31:30.000') RETURNING "id" """ - }results: { + } results: { """ ┌─────┐ │ 100 │ @@ -109,7 +109,7 @@ extension SnapshotTests { (101, NULL, NULL, 0, 0, '', NULL, 1, 'Check voicemail', '2040-02-14 23:31:30.000') RETURNING "id" """ - }results: { + } results: { """ ┌─────┐ │ 101 │ @@ -130,7 +130,7 @@ extension SnapshotTests { (102, NULL, NULL, 0, 0, '', NULL, 1, 'Check mailbox', '2040-02-14 23:31:30.000'), (103, NULL, NULL, 0, 0, '', NULL, 1, 'Check Slack', '2040-02-14 23:31:30.000') RETURNING "id" """ - }results: { + } results: { """ ┌─────┐ │ 102 │ @@ -151,7 +151,7 @@ extension SnapshotTests { (104, NULL, NULL, 0, 0, '', NULL, 1, 'Check pager', '2040-02-14 23:31:30.000') RETURNING "id" """ - }results: { + } results: { """ ┌─────┐ │ 104 │ @@ -237,7 +237,7 @@ extension SnapshotTests { (NULL, NULL, NULL, 0, 0, '', NULL, 1, 'Check email', '2040-02-14 23:31:30.000') RETURNING "id" """ - }results: { + } results: { """ ┌────┐ │ 11 │ @@ -258,7 +258,7 @@ extension SnapshotTests { (NULL, NULL, NULL, 0, 0, '', NULL, 1, 'Check voicemail', '2040-02-14 23:31:30.000') RETURNING "id" """ - }results: { + } results: { """ ┌────┐ │ 12 │ @@ -282,7 +282,7 @@ extension SnapshotTests { (NULL, NULL, NULL, 0, 0, '', NULL, 1, 'Check mailbox', '2040-02-14 23:31:30.000'), (NULL, NULL, NULL, 0, 0, '', NULL, 1, 'Check Slack', '2040-02-14 23:31:30.000') RETURNING "id" """ - }results: { + } results: { """ ┌────┐ │ 13 │ @@ -299,7 +299,7 @@ extension SnapshotTests { FROM "reminders" WHERE ("reminders"."id" = 1) """ - }results: { + } results: { """ ┌─────────────────────────────────────────────┐ │ Reminder( │ @@ -331,7 +331,7 @@ extension SnapshotTests { DO UPDATE SET "assignedUserID" = "excluded"."assignedUserID", "dueDate" = "excluded"."dueDate", "isCompleted" = "excluded"."isCompleted", "isFlagged" = "excluded"."isFlagged", "notes" = "excluded"."notes", "priority" = "excluded"."priority", "remindersListID" = "excluded"."remindersListID", "title" = "excluded"."title", "updatedAt" = "excluded"."updatedAt" RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt" """ - }results: { + } results: { """ ┌─────────────────────────────────────────────┐ │ Reminder( │ @@ -379,7 +379,7 @@ extension SnapshotTests { DO UPDATE SET "assignedUserID" = "excluded"."assignedUserID", "dueDate" = "excluded"."dueDate", "isCompleted" = "excluded"."isCompleted", "isFlagged" = "excluded"."isFlagged", "notes" = "excluded"."notes", "priority" = "excluded"."priority", "remindersListID" = "excluded"."remindersListID", "title" = "excluded"."title", "updatedAt" = "excluded"."updatedAt" RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt" """ - }results: { + } results: { """ ┌─────────────────────────────────────────────┐ │ Reminder( │ diff --git a/Tests/StructuredQueriesTests/JSONFunctionsTests.swift b/Tests/StructuredQueriesTests/JSONFunctionsTests.swift index e6ee2a49..18e063e6 100644 --- a/Tests/StructuredQueriesTests/JSONFunctionsTests.swift +++ b/Tests/StructuredQueriesTests/JSONFunctionsTests.swift @@ -150,7 +150,7 @@ extension SnapshotTests { GROUP BY "reminders"."id" LIMIT 2 """ - }results: { + } results: { """ ┌───────────────────────────────────────────────┐ │ ReminderRow( │ @@ -236,7 +236,7 @@ extension SnapshotTests { GROUP BY "remindersLists"."id" LIMIT 1 """ - }results: { + } results: { """ ┌─────────────────────────────────────────────────┐ │ RemindersListRow( │ diff --git a/Tests/StructuredQueriesTests/LiveTests.swift b/Tests/StructuredQueriesTests/LiveTests.swift index fc040506..62d2d79e 100644 --- a/Tests/StructuredQueriesTests/LiveTests.swift +++ b/Tests/StructuredQueriesTests/LiveTests.swift @@ -12,7 +12,7 @@ extension SnapshotTests { SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "reminders" """ - }results: { + } results: { #""" ┌─────────────────────────────────────────────┐ │ Reminder( │ @@ -242,7 +242,7 @@ extension SnapshotTests { JOIN "tags" ON ("remindersTags"."tagID" = "tags"."id") GROUP BY "reminders"."id" """ - }results: { + } results: { """ ┌─────────────────────────────────────────────┬────────────────────┐ │ Reminder( │ "someday,optional" │ diff --git a/Tests/StructuredQueriesTests/OperatorsTests.swift b/Tests/StructuredQueriesTests/OperatorsTests.swift index 0bca3d79..16ed1f8a 100644 --- a/Tests/StructuredQueriesTests/OperatorsTests.swift +++ b/Tests/StructuredQueriesTests/OperatorsTests.swift @@ -435,7 +435,7 @@ extension SnapshotTests { FROM "reminders" ), 0) / 3)) """ - }results: { + } results: { """ ┌─────────────────────────────────────────────┐ │ Reminder( │ @@ -578,7 +578,7 @@ extension SnapshotTests { FROM "reminders" ) """ - }results: { + } results: { """ ┌──────┐ │ true │ @@ -593,7 +593,7 @@ extension SnapshotTests { WHERE ("reminders"."id" = 1) ) """ - }results: { + } results: { """ ┌──────┐ │ true │ @@ -608,7 +608,7 @@ extension SnapshotTests { WHERE ("reminders"."id" = 100) ) """ - }results: { + } results: { """ ┌───────┐ │ false │ diff --git a/Tests/StructuredQueriesTests/SQLMacroTests.swift b/Tests/StructuredQueriesTests/SQLMacroTests.swift index d35f978e..40fa5a0b 100644 --- a/Tests/StructuredQueriesTests/SQLMacroTests.swift +++ b/Tests/StructuredQueriesTests/SQLMacroTests.swift @@ -23,7 +23,7 @@ extension SnapshotTests { ORDER BY "reminders"."id" LIMIT 1 """ - }results: { + } results: { """ ┌─────────────────────────────────────────────┐ │ Reminder( │ @@ -67,7 +67,7 @@ extension SnapshotTests { ON "reminders"."remindersListID" = "remindersLists"."id" LIMIT 1 """ - }results: { + } results: { """ ┌─────────────────────────────────────────────┬──────────────────────┐ │ Reminder( │ RemindersList( │ @@ -104,7 +104,7 @@ extension SnapshotTests { SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt", "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position" FROM "reminders" JOIN "remindersLists" ON "reminders"."remindersListID" = "remindersLists"."id" LIMIT 1 """ - }results: { + } results: { """ ┌───────────────────────────────────────────────┐ │ ReminderWithList( │ diff --git a/Tests/StructuredQueriesTests/SelectTests.swift b/Tests/StructuredQueriesTests/SelectTests.swift index 52960643..1a7c054c 100644 --- a/Tests/StructuredQueriesTests/SelectTests.swift +++ b/Tests/StructuredQueriesTests/SelectTests.swift @@ -173,7 +173,7 @@ extension SnapshotTests { FROM "reminders" JOIN "remindersLists" ON ("reminders"."remindersListID" = "remindersLists"."id") """ - }results: { + } results: { #""" ┌─────────────────────────────────────────────┬──────────────────────┐ │ Reminder( │ RemindersList( │ @@ -372,7 +372,7 @@ extension SnapshotTests { RIGHT JOIN "reminders" ON ("users"."id" IS "reminders"."assignedUserID") LIMIT 2 """ - }results: { + } results: { """ ┌────────────────┬─────────────────────────────────────────────┐ │ User( │ Reminder( │ @@ -416,7 +416,7 @@ extension SnapshotTests { RIGHT JOIN "reminders" ON ("users"."id" IS "reminders"."assignedUserID") LIMIT 2 """ - }results: { + } results: { """ ┌────────────────┬─────────────────────────────────────────────┐ │ User( │ Reminder( │ @@ -500,7 +500,7 @@ extension SnapshotTests { FROM "reminders" WHERE "reminders"."isCompleted" """ - }results: { + } results: { """ ┌─────────────────────────────────────────────┐ │ Reminder( │ @@ -926,7 +926,7 @@ extension SnapshotTests { FROM "reminders" LIMIT 1 """ - }results: { + } results: { """ ┌─────────────────────────────────────────────┐ │ Reminder( │ @@ -998,7 +998,7 @@ extension SnapshotTests { JOIN "reminders" AS "r2s" ON ("r1s"."id" = "r2s"."id") LIMIT 1 """ - }results: { + } results: { """ ┌─────────────────────────────────────────────┬─────────────────────────────────────────────┐ │ Reminder( │ Reminder( │ @@ -1055,7 +1055,7 @@ extension SnapshotTests { GROUP BY "r1s"."id" LIMIT 1 """ - }results: { + } results: { """ ┌─────────────────────────────────────────────┬─────────────────────────────────────────────────┐ │ Reminder( │ [ │ @@ -1148,7 +1148,7 @@ extension SnapshotTests { LEFT JOIN "reminders" ON ("remindersLists"."id" = "reminders"."remindersListID") WHERE ifnull(("reminders"."priority" IS 3), 0) """ - }results: { + } results: { """ ┌──────────────────────┬─────────────────────────────────────────────┐ │ RemindersList( │ Reminder( │ diff --git a/Tests/StructuredQueriesTests/Support/Schema.swift b/Tests/StructuredQueriesTests/Support/Schema.swift index 60cbd28f..596db2f8 100644 --- a/Tests/StructuredQueriesTests/Support/Schema.swift +++ b/Tests/StructuredQueriesTests/Support/Schema.swift @@ -28,7 +28,7 @@ struct Reminder: Codable, Equatable, Identifiable { var priority: Priority? var remindersListID: Int var title = "" - var updatedAt: Date = Date(timeIntervalSinceReferenceDate: 1234567890) + var updatedAt: Date = Date(timeIntervalSinceReferenceDate: 1_234_567_890) static func searching(_ text: String) -> Where { Self.where { $0.title.collate(.nocase).contains(text) diff --git a/Tests/StructuredQueriesTests/UpdateTests.swift b/Tests/StructuredQueriesTests/UpdateTests.swift index 9144bf54..66b35c5d 100644 --- a/Tests/StructuredQueriesTests/UpdateTests.swift +++ b/Tests/StructuredQueriesTests/UpdateTests.swift @@ -105,7 +105,7 @@ extension SnapshotTests { WHERE ("reminders"."id" = 1) RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt" """ - }results: { + } results: { """ ┌─────────────────────────────────────────────┐ │ Reminder( │ @@ -273,7 +273,7 @@ extension SnapshotTests { WHERE ("rs"."id" = 1) RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt" """ - }results: { + } results: { """ ┌─────────────────────────────────────────────┐ │ Reminder( │ From c9753615a126ac9f17830ce14b39d77fc293f489 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sun, 22 Jun 2025 22:55:04 -0700 Subject: [PATCH 30/32] wip --- .../Documentation.docc/Articles/Triggers.md | 53 ++++++++----------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md b/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md index ad2e7ee1..7ebf4142 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md @@ -18,7 +18,6 @@ it is updated in the database. One can create such a trigger SQL statement using @Column { ```swift Reminder.createTemporaryTrigger( - "reminders_updatedAt", after: .update { _, _ in Reminder.update { $0.updatedAt = #sql("datetime('subsec')") @@ -29,7 +28,7 @@ it is updated in the database. One can create such a trigger SQL statement using } @Column { ```sql - CREATE TEMPORARY TRIGGER "reminders_updatedAt" + CREATE TEMPORARY TRIGGER "after_update_on_reminders@…" AFTER UPDATE ON "reminders" FOR EACH ROW BEGIN @@ -50,17 +49,16 @@ a specialized tool just for that kind of trigger, @Row { @Column { ```swift - Reminder - .createTemporaryTrigger( - afterUpdateTouch: { - $0.updatedAt = datetime('subsec') - } - ) + Reminder.createTemporaryTrigger( + afterUpdateTouch: { + $0.updatedAt = datetime('subsec') + } + ) ``` } @Column { ```sql - CREATE TEMPORARY TRIGGER "reminders_updatedAt" + CREATE TEMPORARY TRIGGER "after_update_on_reminders@…" AFTER UPDATE ON "reminders" FOR EACH ROW BEGIN @@ -79,15 +77,14 @@ comes with another specialized too just for that kind of trigger, @Row { @Column { ```swift - Reminder - .createTemporaryTrigger( - afterUpdateTouch: \.updatedAt - ) + Reminder.createTemporaryTrigger( + afterUpdateTouch: \.updatedAt + ) ``` } @Column { ```sql - CREATE TEMPORARY TRIGGER "reminders_updatedAt" + CREATE TEMPORARY TRIGGER "after_update_on_reminders@…" AFTER UPDATE ON "reminders" FOR EACH ROW BEGIN @@ -102,14 +99,13 @@ comes with another specialized too just for that kind of trigger, There are 3 kinds of triggers depending on the event being listened for in the database: inserts, updates, and deletes. For each of these kinds of triggers one can perform 4 kinds of actions: a -select, insert, update, or delete. All 12 combinations of these kinds of triggers are supported by -the library. +select, insert, update, or delete. Each action can be performed either before or after the event +being listened for executes. All 24 combinations of these kinds of triggers are supported by the +library. -> Note: Technically SQLite supports `BEFORE` triggers and `AFTER` triggers, but the documentation -> recommends against using `BEFORE` triggers as it can lead to undefined behavior. Therefore -> StructuredQueries does not expose `BEFORE` triggers in its API. If you want to go against the -> recommendations of SQLite and create a `BEFORE` trigger, you can always write a trigger as a SQL -> string (see for more info). +> Tip: SQLite generally +> [recommends against](https://sqlite.org/lang_createtrigger.html#cautions_on_the_use_of_before_triggers) +> using `BEFORE` triggers, as it can lead to undefined behavior. Here are a few examples to show you the possibilities with triggers: @@ -124,21 +120,19 @@ last list was deleted: @Column { ```swift RemindersList.createTemporaryTrigger( - "nonEmptyRemindersLists", after: .delete { _ in - RemindersList - .insert { - RemindersList.Draft(title: "Personal") - } + RemindersList.insert { + RemindersList.Draft(title: "Personal") + } } when: { _ in - !RemindersList.all.exists() + !RemindersList.exists() } ) ``` } @Column { ```sql - CREATE TEMPORARY TRIGGER "nonEmptyRemindersLists" + CREATE TEMPORARY TRIGGER "after_delete_on_remindersLists@…" AFTER DELETE ON "remindersLists" FOR EACH ROW WHEN NOT (EXISTS (SELECT * FROM "remindersLists")) BEGIN @@ -168,7 +162,6 @@ reminder is inserted into the database with the following trigger: @Column { ```swift RemindersList.createTemporaryTrigger( - "insertReminderCallback", after: .insert { new in #sql("SELECT didInsertReminder(\(new.id))") } @@ -177,7 +170,7 @@ reminder is inserted into the database with the following trigger: } @Column { ```sql - CREATE TEMPORARY TRIGGER "insertReminderCallback" + CREATE TEMPORARY TRIGGER "after_insert_on_remindersLists@" AFTER DELETE ON "reminders" FOR EACH ROW BEGIN From 0d79f074aa187e8d56ef0b2a19260d49fc2e0746 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sun, 22 Jun 2025 23:06:56 -0700 Subject: [PATCH 31/32] wip --- .../Documentation.docc/Articles/Triggers.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md b/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md index 7ebf4142..f692b6ab 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md @@ -161,7 +161,7 @@ reminder is inserted into the database with the following trigger: @Row { @Column { ```swift - RemindersList.createTemporaryTrigger( + Reminders.createTemporaryTrigger( after: .insert { new in #sql("SELECT didInsertReminder(\(new.id))") } @@ -170,8 +170,8 @@ reminder is inserted into the database with the following trigger: } @Column { ```sql - CREATE TEMPORARY TRIGGER "after_insert_on_remindersLists@" - AFTER DELETE ON "reminders" + CREATE TEMPORARY TRIGGER "after_insert_on_reminders@…" + AFTER INSERT ON "reminders" FOR EACH ROW BEGIN SELECT didInsertReminder("new"."id") From 7ab16577daed5c5564eb926637a66599bf59bee2 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 23 Jun 2025 09:28:29 -0700 Subject: [PATCH 32/32] wip --- .../Documentation.docc/Articles/Triggers.md | 13 ++++ Sources/StructuredQueriesCore/Triggers.swift | 68 +++++++++++++++++-- 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md b/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md index f692b6ab..4348d429 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Articles/Triggers.md @@ -179,3 +179,16 @@ reminder is inserted into the database with the following trigger: ``` } } + + +## Topics + +### Creating temporary triggers + +- ``Table/createTemporaryTrigger(_:ifNotExists:after:fileID:line:column:)`` +- ``Table/createTemporaryTrigger(_:ifNotExists:before:fileID:line:column:)`` + +### Touching records + +- ``Table/createTemporaryTrigger(_:ifNotExists:afterInsertTouch:fileID:line:column:)`` +- ``Table/createTemporaryTrigger(_:ifNotExists:afterUpdateTouch:fileID:line:column:)`` diff --git a/Sources/StructuredQueriesCore/Triggers.swift b/Sources/StructuredQueriesCore/Triggers.swift index c4eefc44..106720e3 100644 --- a/Sources/StructuredQueriesCore/Triggers.swift +++ b/Sources/StructuredQueriesCore/Triggers.swift @@ -4,9 +4,11 @@ import IssueReporting extension Table { /// A `CREATE TEMPORARY TRIGGER` statement that executes after a database event. /// + /// See for more information. + /// /// > Important: A name for the trigger is automatically derived from the arguments if one is not /// > provided. If you build your own trigger helper that call this function, then your helper - /// > should also take fileID, line and column arguments and pass them to this function. + /// > should also take `fileID`, `line` and `column` arguments and pass them to this function. /// /// - Parameters: /// - name: The trigger's name. By default a unique name is generated depending using the table, @@ -38,9 +40,11 @@ extension Table { /// A `CREATE TEMPORARY TRIGGER` statement that executes before a database event. /// + /// See for more information. + /// /// > Important: A name for the trigger is automatically derived from the arguments if one is not /// > provided. If you build your own trigger helper that call this function, then your helper - /// > should also take fileID, line and column arguments and pass them to this function. + /// > should also take `fileID`, `line` and `column` arguments and pass them to this function. /// /// - Parameters: /// - name: The trigger's name. By default a unique name is generated depending using the table, @@ -73,9 +77,11 @@ extension Table { /// A `CREATE TEMPORARY TRIGGER` statement that applies additional updates to a row that has just /// been updated. /// + /// See for more information. + /// /// > Important: A name for the trigger is automatically derived from the arguments if one is not /// > provided. If you build your own trigger helper that call this function, then your helper - /// > should also take fileID, line and column arguments and pass them to this function. + /// > should also take `fileID`, `line` and `column` arguments and pass them to this function. /// /// - Parameters: /// - name: The trigger's name. By default a unique name is generated depending using the table, @@ -108,12 +114,14 @@ extension Table { ) } - /// A `CREATE TEMPORARY TRIGGER` statement that updates a datetime column when a row has been updated. - /// been updated. + /// A `CREATE TEMPORARY TRIGGER` statement that updates a datetime column when a row has been + /// updated. + /// + /// See for more information. /// /// > Important: A name for the trigger is automatically derived from the arguments if one is not /// > provided. If you build your own trigger helper that call this function, then your helper - /// > should also take fileID, line and column arguments and pass them to this function. + /// > should also take `fileID`, `line` and `column` arguments and pass them to this function. /// /// - Parameters: /// - name: The trigger's name. By default a unique name is generated depending using the table, @@ -144,6 +152,24 @@ extension Table { ) } + /// A `CREATE TEMPORARY TRIGGER` statement that applies additional updates to a row that has just + /// been inserted. + /// + /// See for more information. + /// + /// > Important: A name for the trigger is automatically derived from the arguments if one is not + /// > provided. If you build your own trigger helper that call this function, then your helper + /// > should also take `fileID`, `line` and `column` arguments and pass them to this function. + /// + /// - Parameters: + /// - name: The trigger's name. By default a unique name is generated depending using the table, + /// operation, and source location. + /// - ifNotExists: Adds an `IF NOT EXISTS` clause to the `CREATE TRIGGER` statement. + /// - updates: The updates to apply after the row has been inserted. + /// - fileID: The source `#fileID` associated with the trigger. + /// - line: The source `#line` associated with the trigger. + /// - column: The source `#column` associated with the trigger. + /// - Returns: A temporary trigger. public static func createTemporaryTrigger( _ name: String? = nil, ifNotExists: Bool = false, @@ -166,6 +192,24 @@ extension Table { ) } + /// A `CREATE TEMPORARY TRIGGER` statement that updates a datetime column when a row has been + /// inserted. + /// + /// See for more information. + /// + /// > Important: A name for the trigger is automatically derived from the arguments if one is not + /// > provided. If you build your own trigger helper that call this function, then your helper + /// > should also take `fileID`, `line` and `column` arguments and pass them to this function. + /// + /// - Parameters: + /// - name: The trigger's name. By default a unique name is generated depending using the table, + /// operation, and source location. + /// - ifNotExists: Adds an `IF NOT EXISTS` clause to the `CREATE TRIGGER` statement. + /// - date: A key path to a datetime column. + /// - fileID: The source `#fileID` associated with the trigger. + /// - line: The source `#line` associated with the trigger. + /// - column: The source `#column` associated with the trigger. + /// - Returns: A temporary trigger. public static func createTemporaryTrigger>( _ name: String? = nil, ifNotExists: Bool = false, @@ -187,6 +231,13 @@ extension Table { } } +/// A `CREATE TEMPORARY TRIGGER` statement. +/// +/// This type of statement is returned from the +/// `[Table.createTemporaryTrigger]` family of +/// functions. +/// +/// To learn more, see . public struct TemporaryTrigger: Statement { public typealias From = Never public typealias Joins = () @@ -196,7 +247,10 @@ public struct TemporaryTrigger: Statement { case before = "BEFORE" case after = "AFTER" } - + + /// The database event used in a trigger. + /// + /// To learn more, see . public struct Operation: QueryExpression { public typealias QueryValue = ()