From 426219f89d878a3393ee6634935bdd8595826c86 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 6 Oct 2025 13:33:50 -0700 Subject: [PATCH 1/4] Temporary trigger improvements The "touch" helpers have been moved from statics on the table type to statics on the operation type so that additional customization (like `on` and `when`) can be surfaced. --- .../Documentation.docc/Articles/Triggers.md | 12 +- .../Triggers.swift | 133 ++++++++++++++++-- .../TriggersTests.swift | 12 +- 3 files changed, 134 insertions(+), 23 deletions(-) diff --git a/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Triggers.md b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Triggers.md index 2b400e11..25b77238 100644 --- a/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Triggers.md +++ b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Triggers.md @@ -45,13 +45,13 @@ 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, -``StructuredQueriesCore/Table/createTemporaryTrigger(_:ifNotExists:afterUpdateTouch:fileID:line:column:)``: +``StructuredQueriesCore/Table/createTemporaryTrigger(_:ifNotExists:afterUpdateTouch:when:fileID:line:column:)``: @Row { @Column { ```swift Reminder.createTemporaryTrigger( - afterUpdateTouch: { + after: .update: { $0.updatedAt = datetime('subsec') } ) @@ -72,14 +72,14 @@ 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, -``StructuredQueriesCore/Table/createTemporaryTrigger(_:ifNotExists:afterUpdateTouch:fileID:line:column:)``: +``StructuredQueriesCore/Table/createTemporaryTrigger(_:ifNotExists:afterUpdateTouch:when:fileID:line:column:)``: @Row { @Column { ```swift Reminder.createTemporaryTrigger( - afterUpdateTouch: \.updatedAt + after .update(touch: \.updatedAt) ) ``` } @@ -197,8 +197,8 @@ reminder is inserted into the database with the following trigger: ### Touching records -- ``StructuredQueriesCore/Table/createTemporaryTrigger(_:ifNotExists:afterInsertTouch:fileID:line:column:)`` -- ``StructuredQueriesCore/Table/createTemporaryTrigger(_:ifNotExists:afterUpdateTouch:fileID:line:column:)`` +- ``StructuredQueriesCore/Table/createTemporaryTrigger(_:ifNotExists:afterInsertTouch:when:fileID:line:column:)`` +- ``StructuredQueriesCore/Table/createTemporaryTrigger(_:ifNotExists:afterUpdateTouch:when:fileID:line:column:)`` ### Triggers diff --git a/Sources/StructuredQueriesSQLiteCore/Triggers.swift b/Sources/StructuredQueriesSQLiteCore/Triggers.swift index e34d960c..68d20185 100644 --- a/Sources/StructuredQueriesSQLiteCore/Triggers.swift +++ b/Sources/StructuredQueriesSQLiteCore/Triggers.swift @@ -124,6 +124,7 @@ extension 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. + /// - condition: A predicate that must be satisfied to perform the given statement. /// - fileID: The source `#fileID` associated with the trigger. /// - line: The source `#line` associated with the trigger. /// - column: The source `#column` associated with the trigger. @@ -132,6 +133,10 @@ extension Table { _ name: String? = nil, ifNotExists: Bool = false, afterUpdateTouch updates: (inout Updates) -> Void, + when condition: ( + (_ old: TemporaryTrigger.Operation.Old, _ new: TemporaryTrigger.Operation.New) -> + any QueryExpression + )? = nil, fileID: StaticString = #fileID, line: UInt = #line, column: UInt = #column @@ -139,11 +144,14 @@ extension Table { Self.createTemporaryTrigger( name, ifNotExists: ifNotExists, - after: .update { _, new in - Self - .where { $0.rowid.eq(new.rowid) } - .update { updates(&$0) } - }, + after: .update( + forEachRow: { _, new in + Self + .where { $0.rowid.eq(new.rowid) } + .update { updates(&$0) } + }, + when: condition + ), fileID: fileID, line: line, column: column @@ -166,6 +174,7 @@ extension Table { /// - dateColumn: A key path to a datetime column. /// - dateFunction: A database function that returns the current datetime, _e.g._, /// `#sql("datetime('subsec'))"`. + /// - condition: A predicate that must be satisfied to perform the given statement. /// - fileID: The source `#fileID` associated with the trigger. /// - line: The source `#line` associated with the trigger. /// - column: The source `#column` associated with the trigger. @@ -175,6 +184,10 @@ extension Table { ifNotExists: Bool = false, afterUpdateTouch dateColumn: KeyPath>, date dateFunction: any QueryExpression = SQLQueryExpression("datetime('subsec')"), + when condition: ( + (_ old: TemporaryTrigger.Operation.Old, _ new: TemporaryTrigger.Operation.New) -> + any QueryExpression + )? = nil, fileID: StaticString = #fileID, line: UInt = #line, column: UInt = #column @@ -185,6 +198,7 @@ extension Table { afterUpdateTouch: { $0[dynamicMember: dateColumn] = dateFunction }, + when: condition, fileID: fileID, line: line, column: column @@ -205,6 +219,7 @@ extension 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. + /// - condition: A predicate that must be satisfied to perform the given statement. /// - fileID: The source `#fileID` associated with the trigger. /// - line: The source `#line` associated with the trigger. /// - column: The source `#column` associated with the trigger. @@ -213,6 +228,8 @@ extension Table { _ name: String? = nil, ifNotExists: Bool = false, afterInsertTouch updates: (inout Updates) -> Void, + when condition: ((_ new: TemporaryTrigger.Operation.New) -> any QueryExpression)? = + nil, fileID: StaticString = #fileID, line: UInt = #line, column: UInt = #column @@ -220,11 +237,14 @@ extension Table { Self.createTemporaryTrigger( name, ifNotExists: ifNotExists, - after: .insert { new in - Self - .where { $0.rowid.eq(new.rowid) } - .update { updates(&$0) } - }, + after: .insert( + forEachRow: { new in + Self + .where { $0.rowid.eq(new.rowid) } + .update { updates(&$0) } + }, + when: condition + ), fileID: fileID, line: line, column: column @@ -247,6 +267,7 @@ extension Table { /// - dateColumn: A key path to a datetime column. /// - dateFunction: A database function that returns the current datetime, _e.g._, /// `#sql("datetime('subsec'))"`. + /// - condition: A predicate that must be satisfied to perform the given statement. /// - fileID: The source `#fileID` associated with the trigger. /// - line: The source `#line` associated with the trigger. /// - column: The source `#column` associated with the trigger. @@ -256,6 +277,8 @@ extension Table { ifNotExists: Bool = false, afterInsertTouch dateColumn: KeyPath>, date dateFunction: any QueryExpression = SQLQueryExpression("datetime('subsec')"), + when condition: ((_ new: TemporaryTrigger.Operation.New) -> any QueryExpression)? = + nil, fileID: StaticString = #fileID, line: UInt = #line, column: UInt = #column @@ -266,6 +289,7 @@ extension Table { afterInsertTouch: { $0[dynamicMember: dateColumn] = dateFunction }, + when: condition, fileID: fileID, line: line, column: column @@ -276,8 +300,8 @@ extension Table { /// A `CREATE TEMPORARY TRIGGER` statement. /// /// This type of statement is returned from the -/// `[Table.createTemporaryTrigger]` family of -/// functions. +/// `[Table.createTemporaryTrigger]` +/// family of functions. /// /// To learn more, see . public struct TemporaryTrigger: Sendable, Statement { @@ -320,6 +344,33 @@ public struct TemporaryTrigger: Sendable, Statement { ) } + @_disfavoredOverload + public static func insert( + touch updates: (inout Updates) -> Void, + when condition: ((_ new: New) -> any QueryExpression)? = nil + ) -> Self { + Self.insert( + forEachRow: { new in + On + .where { $0.rowid.eq(new.rowid) } + .update { updates(&$0) } + }, + when: condition + ) + } + + @_disfavoredOverload + public static func insert>( + touch dateColumn: KeyPath>, + date dateFunction: any QueryExpression = SQLQueryExpression("datetime('subsec')"), + when condition: ((_ new: New) -> any QueryExpression)? = nil + ) -> Self { + Self.insert( + touch: { $0[dynamicMember: dateColumn] = dateFunction }, + when: condition + ) + } + /// An `AFTER UPDATE` trigger operation. /// /// - Parameters: @@ -364,6 +415,64 @@ public struct TemporaryTrigger: Sendable, Statement { ) } + @_disfavoredOverload + public static func update( + touch updates: (inout Updates) -> Void, + when condition: ((_ old: Old, _ new: New) -> any QueryExpression)? = nil + ) -> Self { + Self.update( + forEachRow: { _, new in + On + .where { $0.rowid.eq(new.rowid) } + .update { updates(&$0) } + }, + when: condition + ) + } + + @_disfavoredOverload + public static func update>( + touch dateColumn: KeyPath>, + date dateFunction: any QueryExpression = SQLQueryExpression("datetime('subsec')"), + when condition: ((_ old: Old, _ new: New) -> any QueryExpression)? = nil + ) -> Self { + Self.update( + touch: { $0[dynamicMember: dateColumn] = dateFunction }, + when: condition + ) + } + + @_disfavoredOverload + public static func update( + of columns: (On.TableColumns) -> (repeat each Column), + touch updates: (inout Updates) -> Void, + when condition: ((_ old: Old, _ new: New) -> any QueryExpression)? = nil + ) -> Self { + Self.update( + of: columns, + forEachRow: { _, new in + On + .where { $0.rowid.eq(new.rowid) } + .update { updates(&$0) } + }, + when: condition + ) + } + + @_disfavoredOverload + public static func update>( + of columns: (On.TableColumns) -> (repeat each Column), + touch dateColumn: KeyPath>, + date dateFunction: any QueryExpression = SQLQueryExpression("datetime('subsec')"), + when condition: ((_ old: Old, _ new: New) -> any QueryExpression)? = nil + ) -> Self { + Self.update( + of: columns, + touch: { $0[dynamicMember: dateColumn] = dateFunction }, + when: condition + ) + } + /// An `AFTER DELETE` trigger operation. /// /// - Parameters: diff --git a/Tests/StructuredQueriesTests/TriggersTests.swift b/Tests/StructuredQueriesTests/TriggersTests.swift index 84a023f0..693b2e90 100644 --- a/Tests/StructuredQueriesTests/TriggersTests.swift +++ b/Tests/StructuredQueriesTests/TriggersTests.swift @@ -85,7 +85,7 @@ extension SnapshotTests { assertQuery( RemindersList.createTemporaryTrigger( "after_update_on_remindersLists", - afterUpdateTouch: { + after: .update { $0.position += 1 } ) @@ -105,7 +105,10 @@ extension SnapshotTests { @Test func afterUpdateTouchDate() { assertQuery( - Reminder.createTemporaryTrigger("after_update_on_reminders", afterUpdateTouch: \.updatedAt) + Reminder.createTemporaryTrigger( + "after_update_on_reminders", + after: .update(touch: \.updatedAt) + ) ) { """ CREATE TEMPORARY TRIGGER @@ -132,7 +135,7 @@ extension SnapshotTests { assertQuery( Episode.createTemporaryTrigger( "after_update_on_episodes", - afterUpdateTouch: \.timestamps.updatedAt + after: .update(touch: \.timestamps.updatedAt) ) ) { """ @@ -152,8 +155,7 @@ extension SnapshotTests { assertQuery( Reminder.createTemporaryTrigger( "after_update_on_reminders", - afterUpdateTouch: \.updatedAt, - date: #sql("customDate()") + after: .update(touch: \.updatedAt, date: #sql("customDate()")) ) ) { """ From 8f0fa74d13cb893e1d857950a56781bc715e49f8 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 6 Oct 2025 13:50:04 -0700 Subject: [PATCH 2/4] wip --- .../Documentation.docc/Articles/Triggers.md | 14 +- .../Internal/Deprecations.swift | 135 ++++++++++++- .../Triggers.swift | 186 ------------------ 3 files changed, 136 insertions(+), 199 deletions(-) diff --git a/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Triggers.md b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Triggers.md index 25b77238..27965586 100644 --- a/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Triggers.md +++ b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Triggers.md @@ -45,7 +45,7 @@ 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, -``StructuredQueriesCore/Table/createTemporaryTrigger(_:ifNotExists:afterUpdateTouch:when:fileID:line:column:)``: +``TemporaryTrigger/Operation/update(touch:when:)``: @Row { @Column { @@ -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, -``StructuredQueriesCore/Table/createTemporaryTrigger(_:ifNotExists:afterUpdateTouch:when:fileID:line:column:)``: +``TemporaryTrigger/Operation/update(touch:when:)``: @Row { @@ -195,11 +195,11 @@ reminder is inserted into the database with the following trigger: - ``StructuredQueriesCore/Table/createTemporaryTrigger(_:ifNotExists:before:fileID:line:column:)`` - ``StructuredQueriesCore/Table/createTemporaryTrigger(_:ifNotExists:insteadOf:fileID:line:column:)`` -### Touching records - -- ``StructuredQueriesCore/Table/createTemporaryTrigger(_:ifNotExists:afterInsertTouch:when:fileID:line:column:)`` -- ``StructuredQueriesCore/Table/createTemporaryTrigger(_:ifNotExists:afterUpdateTouch:when:fileID:line:column:)`` - ### Triggers - ``TemporaryTrigger`` + +### Deprecations + +- ``StructuredQueriesCore/Table/createTemporaryTrigger(_:ifNotExists:afterInsertTouch:fileID:line:column:)`` +- ``StructuredQueriesCore/Table/createTemporaryTrigger(_:ifNotExists:afterUpdateTouch:fileID:line:column:)`` diff --git a/Sources/StructuredQueriesSQLiteCore/Internal/Deprecations.swift b/Sources/StructuredQueriesSQLiteCore/Internal/Deprecations.swift index cb190ac0..90634617 100644 --- a/Sources/StructuredQueriesSQLiteCore/Internal/Deprecations.swift +++ b/Sources/StructuredQueriesSQLiteCore/Internal/Deprecations.swift @@ -1,11 +1,123 @@ import Foundation import StructuredQueriesCore +// NB: Deprecated after 0.22.2: + +extension Table { + @available( + *, + deprecated, + message: "Prefer 'createTemporaryTrigger(after: .update(touch:))', instead" + ) + public static func createTemporaryTrigger( + _ name: String? = nil, + ifNotExists: Bool = false, + afterUpdateTouch updates: (inout Updates) -> Void, + fileID: StaticString = #fileID, + line: UInt = #line, + column: UInt = #column + ) -> TemporaryTrigger { + Self.createTemporaryTrigger( + name, + ifNotExists: ifNotExists, + after: .update { _, new in + Self + .where { $0.rowid.eq(new.rowid) } + .update { updates(&$0) } + }, + fileID: fileID, + line: line, + column: column + ) + } + + @available( + *, + deprecated, + message: "Prefer 'createTemporaryTrigger(after: .update(touch:))', instead" + ) + public static func createTemporaryTrigger>( + _ name: String? = nil, + ifNotExists: Bool = false, + afterUpdateTouch dateColumn: KeyPath>, + date dateFunction: any QueryExpression = SQLQueryExpression("datetime('subsec')"), + fileID: StaticString = #fileID, + line: UInt = #line, + column: UInt = #column + ) -> TemporaryTrigger { + Self.createTemporaryTrigger( + name, + ifNotExists: ifNotExists, + afterUpdateTouch: { + $0[dynamicMember: dateColumn] = dateFunction + }, + fileID: fileID, + line: line, + column: column + ) + } + + @available( + *, + deprecated, + message: "Prefer 'createTemporaryTrigger(after: .insert(touch:))', instead" + ) + 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 + ) + } + + @available( + *, + deprecated, + message: "Prefer 'createTemporaryTrigger(after: .insert(touch:))', instead" + ) + public static func createTemporaryTrigger>( + _ name: String? = nil, + ifNotExists: Bool = false, + afterInsertTouch dateColumn: KeyPath>, + date dateFunction: any QueryExpression = SQLQueryExpression("datetime('subsec')"), + fileID: StaticString = #fileID, + line: UInt = #line, + column: UInt = #column + ) -> TemporaryTrigger { + Self.createTemporaryTrigger( + name, + ifNotExists: ifNotExists, + afterInsertTouch: { + $0[dynamicMember: dateColumn] = dateFunction + }, + fileID: fileID, + line: line, + column: column + ) + } +} + // NB: Deprecated after 0.5.1: extension Table { @available( - *, deprecated, message: "Use a trailing closure, instead: 'Table.insert { row }'" + *, + deprecated, + message: "Use a trailing closure, instead: 'Table.insert { row }'" ) public static func insert( or conflictResolution: ConflictResolution, @@ -16,7 +128,9 @@ extension Table { } @available( - *, deprecated, message: "Use a trailing closure, instead: 'Table.insert { rows }'" + *, + deprecated, + message: "Use a trailing closure, instead: 'Table.insert { rows }'" ) public static func insert( or conflictResolution: ConflictResolution, @@ -49,7 +163,10 @@ extension Table { @available(*, deprecated, renamed: "insert(or:_:select:onConflictDoUpdate:)") public static func insert< - V1, each V2, From, Joins + V1, + each V2, + From, + Joins >( or conflictResolution: ConflictResolution, _ columns: (TableColumns) -> (TableColumn, repeat TableColumn), @@ -62,7 +179,9 @@ extension Table { extension PrimaryKeyedTable { @available( - *, deprecated, message: "Use a trailing closure, instead: 'Table.insert { draft }'" + *, + deprecated, + message: "Use a trailing closure, instead: 'Table.insert { draft }'" ) public static func insert( or conflictResolution: ConflictResolution, @@ -73,7 +192,9 @@ extension PrimaryKeyedTable { } @available( - *, deprecated, message: "Use a trailing closure, instead: 'Table.insert { drafts }'" + *, + deprecated, + message: "Use a trailing closure, instead: 'Table.insert { drafts }'" ) public static func insert( or conflictResolution: ConflictResolution, @@ -84,7 +205,9 @@ extension PrimaryKeyedTable { } @available( - *, deprecated, message: "Use a trailing closure, instead: 'Table.upsert { draft }'" + *, + deprecated, + message: "Use a trailing closure, instead: 'Table.upsert { draft }'" ) public static func upsert( or conflictResolution: ConflictResolution, diff --git a/Sources/StructuredQueriesSQLiteCore/Triggers.swift b/Sources/StructuredQueriesSQLiteCore/Triggers.swift index 68d20185..0d476775 100644 --- a/Sources/StructuredQueriesSQLiteCore/Triggers.swift +++ b/Sources/StructuredQueriesSQLiteCore/Triggers.swift @@ -109,192 +109,6 @@ extension Table { column: column ) } - - /// 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. - /// - /// - 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. - /// - condition: A predicate that must be satisfied to perform the given statement. - /// - 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, - afterUpdateTouch updates: (inout Updates) -> Void, - when condition: ( - (_ old: TemporaryTrigger.Operation.Old, _ new: TemporaryTrigger.Operation.New) -> - any QueryExpression - )? = nil, - fileID: StaticString = #fileID, - line: UInt = #line, - column: UInt = #column - ) -> TemporaryTrigger { - Self.createTemporaryTrigger( - name, - ifNotExists: ifNotExists, - after: .update( - forEachRow: { _, new in - Self - .where { $0.rowid.eq(new.rowid) } - .update { updates(&$0) } - }, - when: condition - ), - fileID: fileID, - line: line, - column: column - ) - } - - /// 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. - /// - /// - 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. - /// - dateColumn: A key path to a datetime column. - /// - dateFunction: A database function that returns the current datetime, _e.g._, - /// `#sql("datetime('subsec'))"`. - /// - condition: A predicate that must be satisfied to perform the given statement. - /// - 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, - afterUpdateTouch dateColumn: KeyPath>, - date dateFunction: any QueryExpression = SQLQueryExpression("datetime('subsec')"), - when condition: ( - (_ old: TemporaryTrigger.Operation.Old, _ new: TemporaryTrigger.Operation.New) -> - any QueryExpression - )? = nil, - fileID: StaticString = #fileID, - line: UInt = #line, - column: UInt = #column - ) -> TemporaryTrigger { - Self.createTemporaryTrigger( - name, - ifNotExists: ifNotExists, - afterUpdateTouch: { - $0[dynamicMember: dateColumn] = dateFunction - }, - when: condition, - fileID: fileID, - line: line, - column: column - ) - } - - /// 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. - /// - condition: A predicate that must be satisfied to perform the given statement. - /// - 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, - afterInsertTouch updates: (inout Updates) -> Void, - when condition: ((_ new: TemporaryTrigger.Operation.New) -> any QueryExpression)? = - nil, - fileID: StaticString = #fileID, - line: UInt = #line, - column: UInt = #column - ) -> TemporaryTrigger { - Self.createTemporaryTrigger( - name, - ifNotExists: ifNotExists, - after: .insert( - forEachRow: { new in - Self - .where { $0.rowid.eq(new.rowid) } - .update { updates(&$0) } - }, - when: condition - ), - fileID: fileID, - line: line, - column: column - ) - } - - /// 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. - /// - dateColumn: A key path to a datetime column. - /// - dateFunction: A database function that returns the current datetime, _e.g._, - /// `#sql("datetime('subsec'))"`. - /// - condition: A predicate that must be satisfied to perform the given statement. - /// - 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, - afterInsertTouch dateColumn: KeyPath>, - date dateFunction: any QueryExpression = SQLQueryExpression("datetime('subsec')"), - when condition: ((_ new: TemporaryTrigger.Operation.New) -> any QueryExpression)? = - nil, - fileID: StaticString = #fileID, - line: UInt = #line, - column: UInt = #column - ) -> TemporaryTrigger { - Self.createTemporaryTrigger( - name, - ifNotExists: ifNotExists, - afterInsertTouch: { - $0[dynamicMember: dateColumn] = dateFunction - }, - when: condition, - fileID: fileID, - line: line, - column: column - ) - } } /// A `CREATE TEMPORARY TRIGGER` statement. From dedf015d0e73e3033d39624931d6590fcbd3c4c4 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 6 Oct 2025 13:50:59 -0700 Subject: [PATCH 3/4] wip --- .../Documentation.docc/Articles/Triggers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Triggers.md b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Triggers.md index 27965586..d978ce87 100644 --- a/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Triggers.md +++ b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Triggers.md @@ -79,7 +79,7 @@ comes with another specialized too just for that kind of trigger, @Column { ```swift Reminder.createTemporaryTrigger( - after .update(touch: \.updatedAt) + after: .update(touch: \.updatedAt) ) ``` } From 1ba48d658b7499d7c797ea7e9ec32f88a81681d0 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 6 Oct 2025 13:59:36 -0700 Subject: [PATCH 4/4] wip --- .../Triggers.swift | 60 ++++++++++++++++--- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/Sources/StructuredQueriesSQLiteCore/Triggers.swift b/Sources/StructuredQueriesSQLiteCore/Triggers.swift index 0d476775..de494837 100644 --- a/Sources/StructuredQueriesSQLiteCore/Triggers.swift +++ b/Sources/StructuredQueriesSQLiteCore/Triggers.swift @@ -141,12 +141,12 @@ public struct TemporaryTrigger: Sendable, Statement { public typealias Old = TableAlias.TableColumns public typealias New = TableAlias.TableColumns - /// An `AFTER INSERT` trigger operation. + /// An `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. + /// - Returns: An `INSERT` trigger operation. public static func insert( @QueryFragmentBuilder forEachRow perform: (_ new: New) -> [QueryFragment], @@ -158,6 +158,12 @@ public struct TemporaryTrigger: Sendable, Statement { ) } + /// An `INSERT` trigger operation that applies additional updates to the associated rows. + /// + /// - Parameters: + /// - updates: The updates to apply to associated rows. + /// - condition: A predicate that must be satisfied to perform the given statement. + /// - Returns: An `INSERT` trigger operation. @_disfavoredOverload public static func insert( touch updates: (inout Updates) -> Void, @@ -173,6 +179,14 @@ public struct TemporaryTrigger: Sendable, Statement { ) } + /// An `INSERT` trigger operation that updates a datetime column for the associated rows. + /// + /// - Parameters: + /// - dateColumn: A key path to a datetime column. + /// - dateFunction: A database function that returns the current datetime, _e.g._, + /// `#sql("datetime('subsec'))"`. + /// - condition: A predicate that must be satisfied to perform the given statement. + /// - Returns: An `UPDATE` trigger operation. @_disfavoredOverload public static func insert>( touch dateColumn: KeyPath>, @@ -185,12 +199,12 @@ public struct TemporaryTrigger: Sendable, Statement { ) } - /// An `AFTER UPDATE` trigger operation. + /// An `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. + /// - Returns: An `UPDATE` trigger operation. public static func update( @QueryFragmentBuilder forEachRow perform: (_ old: Old, _ new: New) -> [QueryFragment], @@ -203,13 +217,13 @@ public struct TemporaryTrigger: Sendable, Statement { ) } - /// An `AFTER UPDATE` trigger operation. + /// An `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. + /// - Returns: An `UPDATE` trigger operation. public static func update( of columns: (On.TableColumns) -> (repeat each Column), @QueryFragmentBuilder @@ -229,6 +243,12 @@ public struct TemporaryTrigger: Sendable, Statement { ) } + /// An `UPDATE` trigger operation that applies additional updates to the associated rows. + /// + /// - Parameters: + /// - updates: The updates to apply to associated rows. + /// - condition: A predicate that must be satisfied to perform the given statement. + /// - Returns: An `UPDATE` trigger operation. @_disfavoredOverload public static func update( touch updates: (inout Updates) -> Void, @@ -244,6 +264,14 @@ public struct TemporaryTrigger: Sendable, Statement { ) } + /// An `UPDATE` trigger operation that updates a datetime column for the associated rows. + /// + /// - Parameters: + /// - dateColumn: A key path to a datetime column. + /// - dateFunction: A database function that returns the current datetime, _e.g._, + /// `#sql("datetime('subsec'))"`. + /// - condition: A predicate that must be satisfied to perform the given statement. + /// - Returns: An `UPDATE` trigger operation. @_disfavoredOverload public static func update>( touch dateColumn: KeyPath>, @@ -256,6 +284,13 @@ public struct TemporaryTrigger: Sendable, Statement { ) } + /// An `UPDATE` trigger operation that applies additional updates to the associated rows. + /// + /// - Parameters: + /// - columns: Updated columns to scope the operation to. + /// - updates: The updates to apply to associated rows. + /// - condition: A predicate that must be satisfied to perform the given statement. + /// - Returns: An `UPDATE` trigger operation. @_disfavoredOverload public static func update( of columns: (On.TableColumns) -> (repeat each Column), @@ -273,6 +308,15 @@ public struct TemporaryTrigger: Sendable, Statement { ) } + /// An `UPDATE` trigger operation that updates a datetime column for the associated rows. + /// + /// - Parameters: + /// - columns: Updated columns to scope the operation to. + /// - dateColumn: A key path to a datetime column. + /// - dateFunction: A database function that returns the current datetime, _e.g._, + /// `#sql("datetime('subsec'))"`. + /// - condition: A predicate that must be satisfied to perform the given statement. + /// - Returns: An `UPDATE` trigger operation. @_disfavoredOverload public static func update>( of columns: (On.TableColumns) -> (repeat each Column), @@ -287,12 +331,12 @@ public struct TemporaryTrigger: Sendable, Statement { ) } - /// An `AFTER DELETE` trigger operation. + /// A `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. + /// - Returns: A `DELETE` trigger operation. public static func delete( @QueryFragmentBuilder forEachRow perform: (_ old: Old) -> [QueryFragment],