From 86135acece1a5880fd628a0304b4f09b80ca3d3f Mon Sep 17 00:00:00 2001 From: Milo Hehmsoth Date: Wed, 1 Oct 2025 18:26:13 -0500 Subject: [PATCH 1/9] Add two vars for the orReplace and ifNotExists in dialect --- Sources/SQLKit/Database/SQLDialect.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Sources/SQLKit/Database/SQLDialect.swift b/Sources/SQLKit/Database/SQLDialect.swift index 41d7b030..6857ea66 100644 --- a/Sources/SQLKit/Database/SQLDialect.swift +++ b/Sources/SQLKit/Database/SQLDialect.swift @@ -368,6 +368,16 @@ public struct SQLTriggerSyntax: Sendable { public static var conditionRequiresParentheses: Self { .init(rawValue: 1 << 9) } + + /// Indicates support for `OR REPLACE` syntax. + public static var supportsOrReplace: Self { + .init(rawValue: 1 << 10) + } + + /// Indicates support for an `IF NOT EXISTS` syntax. + public static var supportsIfNotExists: Self { + .init(rawValue: 1 << 11) + } } /// Describes specific feature support for `CREATE TRIGGER` syntax. From 58be034577094c10f3bc4120c545e0d83d02c9d3 Mon Sep 17 00:00:00 2001 From: Milo Hehmsoth Date: Wed, 1 Oct 2025 18:26:39 -0500 Subject: [PATCH 2/9] Implement their support on SQLCreateTrigger --- .../Expressions/Queries/SQLCreateTrigger.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Sources/SQLKit/Expressions/Queries/SQLCreateTrigger.swift b/Sources/SQLKit/Expressions/Queries/SQLCreateTrigger.swift index b97b2711..c4490513 100644 --- a/Sources/SQLKit/Expressions/Queries/SQLCreateTrigger.swift +++ b/Sources/SQLKit/Expressions/Queries/SQLCreateTrigger.swift @@ -36,6 +36,12 @@ public struct SQLCreateTrigger: SQLExpression { /// `true` if the new trigger will be a constraint trigger, if supported. public var isConstraint: Bool + + /// Include `OR REPLACE` to the trigger syntax. + public var orReplace: Bool + + /// Whether to include `IF NOT EXISTS` syntax. + public var ifNotExists: Bool /// The ordering of the trigger's execution relative to the triggering event. /// @@ -122,6 +128,8 @@ public struct SQLCreateTrigger: SQLExpression { self.when = when self.event = event self.isConstraint = false + self.orReplace = false + self.ifNotExists = false } /// Create a new trigger creation query. @@ -162,6 +170,15 @@ public struct SQLCreateTrigger: SQLExpression { if syntax.contains(.supportsConstraints), self.isConstraint { $0.append("CONSTRAINT") } + + if self.orReplace, syntax.contains(.supportsOrReplace) { + $0.append("OR REPLACE") + } + $0.append("TRIGGER") + if self.ifNotExists, syntax.contains(.supportsIfNotExists) { + $0.append("IF NOT EXISTS") + } + if let definer = self.definer, syntax.contains(.supportsDefiner) { $0.append("DEFINER =", definer) } From 2df56f3a6acbcf27c2d1bcc34ed13de10d58fdb2 Mon Sep 17 00:00:00 2001 From: Milo Hehmsoth Date: Wed, 1 Oct 2025 18:32:29 -0500 Subject: [PATCH 3/9] Add the builders --- .../SQLCreateTriggerBuilder.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Sources/SQLKit/Builders/Implementations/SQLCreateTriggerBuilder.swift b/Sources/SQLKit/Builders/Implementations/SQLCreateTriggerBuilder.swift index c81d539d..bc948370 100644 --- a/Sources/SQLKit/Builders/Implementations/SQLCreateTriggerBuilder.swift +++ b/Sources/SQLKit/Builders/Implementations/SQLCreateTriggerBuilder.swift @@ -158,6 +158,22 @@ public final class SQLCreateTriggerBuilder: SQLQueryBuilder { self.createTrigger.orderTriggerName = otherTriggerName return self } + + + @inlinable + @discardableResult + public func orReplace(orReplace: Bool = true) -> Self { + self.createTrigger.orReplace = orReplace + return self + } + + + @inlinable + @discardableResult + public func ifNotExists(ifNotExists: Bool = true) -> Self { + self.createTrigger.ifNotExists = ifNotExists + return self + } } extension SQLDatabase { From 4f1ae823c964b0b1bb63df2cc0e21c1c07cbc756 Mon Sep 17 00:00:00 2001 From: Milo Hehmsoth Date: Wed, 1 Oct 2025 18:36:29 -0500 Subject: [PATCH 4/9] remove floating append trigger statement --- Sources/SQLKit/Expressions/Queries/SQLCreateTrigger.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SQLKit/Expressions/Queries/SQLCreateTrigger.swift b/Sources/SQLKit/Expressions/Queries/SQLCreateTrigger.swift index c4490513..235e3503 100644 --- a/Sources/SQLKit/Expressions/Queries/SQLCreateTrigger.swift +++ b/Sources/SQLKit/Expressions/Queries/SQLCreateTrigger.swift @@ -174,7 +174,7 @@ public struct SQLCreateTrigger: SQLExpression { if self.orReplace, syntax.contains(.supportsOrReplace) { $0.append("OR REPLACE") } - $0.append("TRIGGER") + if self.ifNotExists, syntax.contains(.supportsIfNotExists) { $0.append("IF NOT EXISTS") } From 5dcb1e0d2bfb33e3d52947095403497d660c16be Mon Sep 17 00:00:00 2001 From: Milo Hehmsoth Date: Wed, 1 Oct 2025 18:37:12 -0500 Subject: [PATCH 5/9] Add supportsOrReplace on the postgres trigger create test --- Tests/SQLKitTests/SQLCreateDropTriggerTests.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Tests/SQLKitTests/SQLCreateDropTriggerTests.swift b/Tests/SQLKitTests/SQLCreateDropTriggerTests.swift index c65f58ab..f1a8bd90 100644 --- a/Tests/SQLKitTests/SQLCreateDropTriggerTests.swift +++ b/Tests/SQLKitTests/SQLCreateDropTriggerTests.swift @@ -57,7 +57,7 @@ final class SQLCreateDropTriggerTests: XCTestCase { } func testPostgreSqlTriggerCreates() { - self.db._dialect.triggerSyntax = .init(create: [.supportsForEach, .postgreSQLChecks, .supportsCondition, .conditionRequiresParentheses, .supportsConstraints]) + self.db._dialect.triggerSyntax = .init(create: [.supportsForEach, .postgreSQLChecks, .supportsCondition, .conditionRequiresParentheses, .supportsConstraints, .supportsOrReplace]) XCTAssertSerialization( of: self.db.create(trigger: "foo", table: "planet", when: .after, event: .insert) .each(.row) @@ -80,6 +80,10 @@ final class SQLCreateDropTriggerTests: XCTestCase { .procedure("qwer"), is: "CREATE TRIGGER ``foo`` INSTEAD OF UPDATE ON ``planet`` FOR EACH ROW EXECUTE PROCEDURE ``qwer``" ) + XCTAssertSerialization( + of: self.db.create(trigger: "foo", table: "planet", when: .before, event: .insert) + .orReplace(), + is: "CREATE OR REPLACE TRIGGER ``foo`` BEFORE INSERT ON ``planet``") } func testPostgreSqlTriggerCreateWithColumns() { From d10b516157d8e183fa6004391abb1251853a4b08 Mon Sep 17 00:00:00 2001 From: Milo Hehmsoth Date: Wed, 1 Oct 2025 18:42:34 -0500 Subject: [PATCH 6/9] Add to SQLLite test and add dialect flag --- Tests/SQLKitTests/SQLCreateDropTriggerTests.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Tests/SQLKitTests/SQLCreateDropTriggerTests.swift b/Tests/SQLKitTests/SQLCreateDropTriggerTests.swift index f1a8bd90..6dd18275 100644 --- a/Tests/SQLKitTests/SQLCreateDropTriggerTests.swift +++ b/Tests/SQLKitTests/SQLCreateDropTriggerTests.swift @@ -47,13 +47,18 @@ final class SQLCreateDropTriggerTests: XCTestCase { } func testSqliteTriggerCreates() { - self.db._dialect.triggerSyntax = .init(create: [.supportsBody, .supportsCondition]) + self.db._dialect.triggerSyntax = .init(create: [.supportsBody, .supportsCondition, .supportsIfNotExists]) XCTAssertSerialization( of: self.db.create(trigger: "foo", table: "planet", when: .before, event: .insert) .body(self.body.map { SQLRaw($0) }) .condition("\(ident: "foo") = \(ident: "bar")" as SQLQueryString), is: "CREATE TRIGGER ``foo`` BEFORE INSERT ON ``planet`` WHEN ``foo`` = ``bar`` BEGIN \(self.body.joined(separator: " ")) END;" ) + XCTAssertSerialization( + of: self.db.create(trigger: "foo", table: "planet", when: .before, event: .insert) + .ifNotExists(), + is: "CREATE TRIGGER ``foo`` IF NOT EXISTS BEFORE INSERT ON ``planet``" + ) } func testPostgreSqlTriggerCreates() { From c545d5e8948ee49f79aa8bd5e75537eb8d0e3fd0 Mon Sep 17 00:00:00 2001 From: Milo Hehmsoth Date: Wed, 1 Oct 2025 18:42:56 -0500 Subject: [PATCH 7/9] Re-order operations so IF NOT EXISTS appears after TRIGGER --- .../SQLKit/Expressions/Queries/SQLCreateTrigger.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/SQLKit/Expressions/Queries/SQLCreateTrigger.swift b/Sources/SQLKit/Expressions/Queries/SQLCreateTrigger.swift index 235e3503..99d003f1 100644 --- a/Sources/SQLKit/Expressions/Queries/SQLCreateTrigger.swift +++ b/Sources/SQLKit/Expressions/Queries/SQLCreateTrigger.swift @@ -175,14 +175,15 @@ public struct SQLCreateTrigger: SQLExpression { $0.append("OR REPLACE") } - if self.ifNotExists, syntax.contains(.supportsIfNotExists) { - $0.append("IF NOT EXISTS") - } - if let definer = self.definer, syntax.contains(.supportsDefiner) { $0.append("DEFINER =", definer) } $0.append("TRIGGER", self.name) + + if self.ifNotExists, syntax.contains(.supportsIfNotExists) { + $0.append("IF NOT EXISTS") + } + $0.append(self.when, self.event) if let columns = self.columns, !columns.isEmpty, syntax.contains(.supportsUpdateColumns) { $0.append("OF", SQLList(columns)) From e6376e9165323ab7a3f0c0c2db8dbf7e3707f5a8 Mon Sep 17 00:00:00 2001 From: Milo Hehmsoth Date: Wed, 1 Oct 2025 18:49:26 -0500 Subject: [PATCH 8/9] Split self.name out from create trigger serializer statement so IF NOT EXISTS can appear before TRIGGER ``name`` --- Sources/SQLKit/Expressions/Queries/SQLCreateTrigger.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/SQLKit/Expressions/Queries/SQLCreateTrigger.swift b/Sources/SQLKit/Expressions/Queries/SQLCreateTrigger.swift index 99d003f1..eaed6ce4 100644 --- a/Sources/SQLKit/Expressions/Queries/SQLCreateTrigger.swift +++ b/Sources/SQLKit/Expressions/Queries/SQLCreateTrigger.swift @@ -178,12 +178,15 @@ public struct SQLCreateTrigger: SQLExpression { if let definer = self.definer, syntax.contains(.supportsDefiner) { $0.append("DEFINER =", definer) } - $0.append("TRIGGER", self.name) + + $0.append("TRIGGER") if self.ifNotExists, syntax.contains(.supportsIfNotExists) { $0.append("IF NOT EXISTS") } + $0.append(self.name) + $0.append(self.when, self.event) if let columns = self.columns, !columns.isEmpty, syntax.contains(.supportsUpdateColumns) { $0.append("OF", SQLList(columns)) From 00faacd0626d4368976b53354859191de17e4f0b Mon Sep 17 00:00:00 2001 From: Milo Hehmsoth Date: Wed, 1 Oct 2025 18:49:33 -0500 Subject: [PATCH 9/9] Fix tests --- Tests/SQLKitTests/SQLCreateDropTriggerTests.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Tests/SQLKitTests/SQLCreateDropTriggerTests.swift b/Tests/SQLKitTests/SQLCreateDropTriggerTests.swift index 6dd18275..40d18917 100644 --- a/Tests/SQLKitTests/SQLCreateDropTriggerTests.swift +++ b/Tests/SQLKitTests/SQLCreateDropTriggerTests.swift @@ -33,7 +33,7 @@ final class SQLCreateDropTriggerTests: XCTestCase { } func testMySqlTriggerCreates() { - self.db._dialect.triggerSyntax = .init(create: [.supportsBody, .supportsOrder, .supportsDefiner, .requiresForEachRow]) + self.db._dialect.triggerSyntax = .init(create: [.supportsBody, .supportsOrder, .supportsDefiner, .requiresForEachRow, .supportsIfNotExists]) let builder = self.db.create(trigger: "foo", table: "planet", when: .before, event: .insert) .body(self.body.map { SQLRaw($0) }) @@ -44,6 +44,11 @@ final class SQLCreateDropTriggerTests: XCTestCase { of: builder, is: "CREATE DEFINER = 'foo@bar' TRIGGER ``foo`` BEFORE INSERT ON ``planet`` FOR EACH ROW PRECEDES ``other`` BEGIN \(self.body.joined(separator: " ")) END;" ) + XCTAssertSerialization( + of: self.db.create(trigger: "foo", table: "planet", when: .before, event: .insert) + .ifNotExists(), + is: "CREATE TRIGGER IF NOT EXISTS ``foo`` BEFORE INSERT ON ``planet`` FOR EACH ROW" + ) } func testSqliteTriggerCreates() { @@ -57,7 +62,7 @@ final class SQLCreateDropTriggerTests: XCTestCase { XCTAssertSerialization( of: self.db.create(trigger: "foo", table: "planet", when: .before, event: .insert) .ifNotExists(), - is: "CREATE TRIGGER ``foo`` IF NOT EXISTS BEFORE INSERT ON ``planet``" + is: "CREATE TRIGGER IF NOT EXISTS ``foo`` BEFORE INSERT ON ``planet``" ) }