diff --git a/.github/.codecov.yml b/.github/.codecov.yml
index 220b77a6..ab69a043 100644
--- a/.github/.codecov.yml
+++ b/.github/.codecov.yml
@@ -34,4 +34,3 @@ ignore:
- ^Tests/.*
- ^.build/.*
slack_app: false
-
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 998a0ebe..9f8d71ff 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -8,3 +8,11 @@ updates:
dependencies:
patterns:
- "*"
+ - package-ecosystem: "swift"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ groups:
+ dependencies:
+ patterns:
+ - "*"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 334a2f5c..1af6d106 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -5,13 +5,11 @@ concurrency:
on:
pull_request: { types: [opened, reopened, synchronize, ready_for_review] }
push: { branches: [ main ] }
-env:
- LOG_LEVEL: info
-permissions:
- contents: read
jobs:
unit-tests:
+ permissions:
+ contents: read
uses: vapor/ci/.github/workflows/run-unit-tests.yml@main
secrets: inherit
with:
@@ -20,6 +18,8 @@ jobs:
pure-fluent-integration-test:
if: ${{ !(github.event.pull_request.draft || false) }}
+ permissions:
+ contents: read
runs-on: ubuntu-latest
container: swift:6.2-noble
steps:
@@ -36,19 +36,21 @@ jobs:
integration-tests:
if: ${{ !(github.event.pull_request.draft || false) }}
+ permissions:
+ contents: read
services:
mysql-a:
image: mysql:latest
- env: { MYSQL_USER: test_username, MYSQL_PASSWORD: test_password, MYSQL_DATABASE: test_database, MYSQL_ALLOW_EMPTY_PASSWORD: true }
+ env: &common_mysql_env { MYSQL_USER: test_username, MYSQL_PASSWORD: test_password, MYSQL_DATABASE: test_database, MYSQL_ALLOW_EMPTY_PASSWORD: true }
mysql-b:
image: mysql:latest
- env: { MYSQL_USER: test_username, MYSQL_PASSWORD: test_password, MYSQL_DATABASE: test_database, MYSQL_ALLOW_EMPTY_PASSWORD: true }
+ env: *common_mysql_env
psql-a:
image: postgres:latest
- env: { POSTGRES_USER: test_username, POSTGRES_PASSWORD: test_password, POSTGRES_DB: test_database }
+ env: &common_psql_env { POSTGRES_USER: test_username, POSTGRES_PASSWORD: test_password, POSTGRES_DB: test_database }
psql-b:
image: postgres:latest
- env: { POSTGRES_USER: test_username, POSTGRES_PASSWORD: test_password, POSTGRES_DB: test_database }
+ env: *common_psql_env
strategy:
fail-fast: false
matrix:
@@ -90,3 +92,10 @@ jobs:
run: |
swift package --package-path "${FLUENT_DRIVER}" edit --path sql-kit sql-kit
swift test --package-path "${FLUENT_DRIVER}" --sanitize=thread
+
+ submit-dependencies:
+ permissions:
+ contents: write
+ if: ${{ github.event_name == 'push' }}
+ uses: vapor/ci/.github/workflows/submit-deps.yml@main
+ secrets: inherit
diff --git a/Sources/SQLKit/Builders/Implementations/SQLConflictUpdateBuilder.swift b/Sources/SQLKit/Builders/Implementations/SQLConflictUpdateBuilder.swift
index 3ed7dada..e1924bb6 100644
--- a/Sources/SQLKit/Builders/Implementations/SQLConflictUpdateBuilder.swift
+++ b/Sources/SQLKit/Builders/Implementations/SQLConflictUpdateBuilder.swift
@@ -8,8 +8,8 @@ public final class SQLConflictUpdateBuilder: SQLColumnUpdateBuilder, SQLPredicat
public var predicate: (any SQLExpression)? = nil
/// Create a conflict update builder.
- @usableFromInline
- init() {}
+ @inlinable
+ public init() {}
/// Add an assignment of the column with the given name, using the value the column was
/// given in the `INSERT` query's `VALUES` list.
diff --git a/Sources/SQLKit/Builders/Prototypes/SQLCommonTableExpressionBuilder.swift b/Sources/SQLKit/Builders/Prototypes/SQLCommonTableExpressionBuilder.swift
index 2b8de270..78f25244 100644
--- a/Sources/SQLKit/Builders/Prototypes/SQLCommonTableExpressionBuilder.swift
+++ b/Sources/SQLKit/Builders/Prototypes/SQLCommonTableExpressionBuilder.swift
@@ -284,7 +284,7 @@ extension SQLCommonTableExpressionBuilder {
/// This is the common "funnel" method invoked by all other methods provided by
/// ``SQLCommonTableExpressionBuilder``. Most users will not need to call this method directly.
///
- /// See ``with(_:columns:as:)-28k4r`` and ``with(recursive:columns:as:)-6yef`` for usage examples.
+ /// See ``SQLCommonTableExpressionBuilder/with(_:columns:as:)-28k4r`` and ``SQLCommonTableExpressionBuilder/with(recursive:columns:as:)-6yef`` for usage examples.
///
/// > Warning: As with ``SQLCommonTableExpression``, ``SQLCommonTableExpressionBuilder`` does _NOT_ validate
/// > that a recursive CTE's query takes the proper form, nor that a non-recursive CTE's query is not
diff --git a/Sources/SQLKit/Builders/Prototypes/SQLCommonUnionBuilder.swift b/Sources/SQLKit/Builders/Prototypes/SQLCommonUnionBuilder.swift
index 05d5d2db..4c2fd8ef 100644
--- a/Sources/SQLKit/Builders/Prototypes/SQLCommonUnionBuilder.swift
+++ b/Sources/SQLKit/Builders/Prototypes/SQLCommonUnionBuilder.swift
@@ -77,55 +77,55 @@ extension SQLCommonUnionBuilder {
return self
}
- /// Call ``union(distinct:)-15xs8`` with a query generated by a builder.
+ /// Call ``SQLCommonUnionBuilder/union(distinct:)-15xs8`` with a query generated by a builder.
@inlinable
public func union(distinct predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> Self {
try self.union(distinct: predicate(SQLSubqueryBuilder()).select)
}
- /// Call ``union(all:)-56f28`` with a query generated by a builder.
+ /// Call ``SQLCommonUnionBuilder/union(all:)-56f28`` with a query generated by a builder.
@inlinable
public func union(all predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> Self {
try self.union(all: predicate(SQLSubqueryBuilder()).select)
}
- /// Alias ``union(distinct:)-921p6`` so it acts as the "default".
+ /// Alias ``SQLCommonUnionBuilder/union(distinct:)-921p6`` so it acts as the "default".
@inlinable
public func union(_ predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> Self {
try self.union(distinct: predicate)
}
- /// Call ``intersect(distinct:)-161s9`` with a query generated by a builder.
+ /// Call ``SQLCommonUnionBuilder/intersect(distinct:)-161s9`` with a query generated by a builder.
@inlinable
public func intersect(distinct predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> Self {
try self.intersect(distinct: predicate(SQLSubqueryBuilder()).select)
}
- /// Call ``intersect(all:)-1wiow`` with a query generated by a builder.
+ /// Call ``SQLCommonUnionBuilder/intersect(all:)-1wiow`` with a query generated by a builder.
@inlinable
public func intersect(all predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> Self {
try self.intersect(all: predicate(SQLSubqueryBuilder()).select)
}
- /// Alias ``intersect(distinct:)-8f71m`` so it acts as the "default".
+ /// Alias ``SQLCommonUnionBuilder/intersect(distinct:)-8f71m`` so it acts as the "default".
@inlinable
public func intersect(_ predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> Self {
try self.intersect(distinct: predicate)
}
- /// Call ``except(distinct:)-2ygq0`` with a query generated by a builder.
+ /// Call ``SQLCommonUnionBuilder/except(distinct:)-2ygq0`` with a query generated by a builder.
@inlinable
public func except(distinct predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> Self {
try self.except(distinct: predicate(SQLSubqueryBuilder()).select)
}
- /// Call ``except(all:)-5exbl`` with a query generated by a builder.
+ /// Call ``SQLCommonUnionBuilder/except(all:)-5exbl`` with a query generated by a builder.
@inlinable
public func except(all predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> Self {
try self.except(all: predicate(SQLSubqueryBuilder()).select)
}
- /// Alias ``except(distinct:)-62w7q`` so it acts as the "default".
+ /// Alias ``SQLCommonUnionBuilder/except(distinct:)-62w7q`` so it acts as the "default".
@inlinable
public func except(_ predicate: (any SQLSubqueryClauseBuilder) throws -> any SQLSubqueryClauseBuilder) rethrows -> Self {
try self.except(distinct: predicate)
diff --git a/Sources/SQLKit/Docs.docc/BasicUsage.md b/Sources/SQLKit/Docs.docc/BasicUsage.md
index e7ccf19b..e7dace3d 100644
--- a/Sources/SQLKit/Docs.docc/BasicUsage.md
+++ b/Sources/SQLKit/Docs.docc/BasicUsage.md
@@ -206,7 +206,7 @@ WHERE "name" <> NULL AND ("name" = ?1 OR "name" = ?2) -- bindings: ["Milky Way",
### Insert
-The ``SQLDatabase/insert(into:)-67oqt`` and ``SQLDatabase/insert(into:)-5n3gh`` methods create an `INSERT` query builder:
+The ``SQLDatabase/insert(into:)-(String)`` and ``SQLDatabase/insert(into:)-(SQLExpression)`` methods create an `INSERT` query builder:
```swift
try await db.insert(into: "galaxies")
@@ -236,7 +236,7 @@ This code generates the same SQL as would `builder.columns("name").values("Milky
### Update
-The ``SQLDatabase/update(_:)-2tf1c`` and ``SQLDatabase/update(_:)-80964`` methods create an `UPDATE` query builder:
+The ``SQLDatabase/update(_:)-(String)`` and ``SQLDatabase/update(_:)-(SQLExpression)`` methods create an `UPDATE` query builder:
```swift
try await db.update("planets")
@@ -255,7 +255,7 @@ The update builder supports the same `where()` and `orWhere()` methods as the se
### Delete
-The ``SQLDatabase/delete(from:)-3tx4f`` and ``SQLDatabase/delete(from:)-4bqlu`` methods create a `DELETE` query builder:
+The ``SQLDatabase/delete(from:)-(String)`` and ``SQLDatabase/delete(from:)-(SQLExpression)`` methods create a `DELETE` query builder:
```swift
try await db.delete(from: "planets")
diff --git a/Sources/SQLKit/Docs.docc/Resources/vapor-sqlkit-logo.svg b/Sources/SQLKit/Docs.docc/Resources/vapor-sqlkit-logo.svg
index 053016d0..0916a41d 100644
--- a/Sources/SQLKit/Docs.docc/Resources/vapor-sqlkit-logo.svg
+++ b/Sources/SQLKit/Docs.docc/Resources/vapor-sqlkit-logo.svg
@@ -1,22 +1,47 @@
-
diff --git a/Sources/SQLKit/Docs.docc/SQLDatabase+ExtensionDocs.md b/Sources/SQLKit/Docs.docc/SQLDatabase+ExtensionDocs.md
index 888ef775..34e0f199 100644
--- a/Sources/SQLKit/Docs.docc/SQLDatabase+ExtensionDocs.md
+++ b/Sources/SQLKit/Docs.docc/SQLDatabase+ExtensionDocs.md
@@ -17,40 +17,41 @@
### DML queries
-- ``SQLDatabase/delete(from:)-3tx4f``
-- ``SQLDatabase/delete(from:)-4bqlu``
-- ``SQLDatabase/insert(into:)-67oqt``
-- ``SQLDatabase/insert(into:)-5n3gh``
+- ``SQLDatabase/delete(from:)-(String)``
+- ``SQLDatabase/delete(from:)-(SQLExpression)``
+- ``SQLDatabase/insert(into:)-(String)``
+- ``SQLDatabase/insert(into:)-(SQLExpression)``
- ``SQLDatabase/select()``
- ``SQLDatabase/union(_:)``
-- ``SQLDatabase/update(_:)-2tf1c``
-- ``SQLDatabase/update(_:)-80964``
+- ``SQLDatabase/update(_:)-(String)``
+- ``SQLDatabase/update(_:)-(SQLExpression)``
### DDL queries
-- ``SQLDatabase/alter(table:)-42uao``
-- ``SQLDatabase/alter(table:)-68pbr``
-- ``SQLDatabase/create(table:)-czz4``
-- ``SQLDatabase/create(table:)-2wdmn``
-- ``SQLDatabase/drop(table:)-938qt``
-- ``SQLDatabase/drop(table:)-7k2ai``
-
-- ``SQLDatabase/alter(enum:)-66oin``
-- ``SQLDatabase/alter(enum:)-7nb5b``
-- ``SQLDatabase/create(enum:)-81hl4``
-- ``SQLDatabase/create(enum:)-70oeh``
-- ``SQLDatabase/drop(enum:)-5leu1``
-- ``SQLDatabase/drop(enum:)-3jgv``
-
-- ``SQLDatabase/create(index:)-7yh28``
-- ``SQLDatabase/create(index:)-1iuey``
-- ``SQLDatabase/drop(index:)-62i2j``
-- ``SQLDatabase/drop(index:)-19tfk``
-
-- ``SQLDatabase/create(trigger:table:when:event:)-6ntdo``
-- ``SQLDatabase/create(trigger:table:when:event:)-9upcb``
-- ``SQLDatabase/drop(trigger:)-53mq6``
-- ``SQLDatabase/drop(trigger:)-5sfa8``
+- ``SQLDatabase/alter(table:)-(String)``
+- ``SQLDatabase/alter(table:)-(SQLIdentifier)``
+- ``SQLDatabase/alter(table:)-(SQLExpression)``
+- ``SQLDatabase/create(table:)-(String)``
+- ``SQLDatabase/create(table:)-(SQLExpression)``
+- ``SQLDatabase/drop(table:)-(String)``
+- ``SQLDatabase/drop(table:)-(SQLExpression)``
+
+- ``SQLDatabase/alter(enum:)-(String)``
+- ``SQLDatabase/alter(enum:)-(SQLExpression)``
+- ``SQLDatabase/create(enum:)-(String)``
+- ``SQLDatabase/create(enum:)-(SQLExpression)``
+- ``SQLDatabase/drop(enum:)-(String)``
+- ``SQLDatabase/drop(enum:)-(SQLExpression)``
+
+- ``SQLDatabase/create(index:)-(String)``
+- ``SQLDatabase/create(index:)-(SQLExpression)``
+- ``SQLDatabase/drop(index:)-(String)``
+- ``SQLDatabase/drop(index:)-(SQLExpression)``
+
+- ``SQLDatabase/create(trigger:table:when:event:)-(String,_,_,_)``
+- ``SQLDatabase/create(trigger:table:when:event:)-(SQLExpression,_,_,_)``
+- ``SQLDatabase/drop(trigger:)-(String)``
+- ``SQLDatabase/drop(trigger:)-(SQLExpression)``
### Raw queries
@@ -63,4 +64,4 @@
### Legacy query interface
-- ``SQLDatabase/execute(sql:_:)-90wi9``
+- ``SQLDatabase/execute(sql:_:)->_``
diff --git a/Sources/SQLKit/Docs.docc/SQLQueryFetcher+ExtensionDocs.md b/Sources/SQLKit/Docs.docc/SQLQueryFetcher+ExtensionDocs.md
index 589f326e..94696311 100644
--- a/Sources/SQLKit/Docs.docc/SQLQueryFetcher+ExtensionDocs.md
+++ b/Sources/SQLKit/Docs.docc/SQLQueryFetcher+ExtensionDocs.md
@@ -4,40 +4,40 @@
### Getting Rows
-- ``SQLQueryFetcher/run(_:)-40swz``
-- ``SQLQueryFetcher/run(decoding:_:)-476q1``
-- ``SQLQueryFetcher/run(decoding:prefix:keyDecodingStrategy:userInfo:_:)-583ot``
-- ``SQLQueryFetcher/run(decoding:with:_:)-8y7ux``
+- ``SQLQueryFetcher/run(_:)->()``
+- ``SQLQueryFetcher/run(decoding:_:)->()``
+- ``SQLQueryFetcher/run(decoding:prefix:keyDecodingStrategy:userInfo:_:)->()``
+- ``SQLQueryFetcher/run(decoding:with:_:)->()``
### Getting All Rows
-- ``SQLQueryFetcher/all()-8yci1``
-- ``SQLQueryFetcher/all(decoding:)-5dt2x``
-- ``SQLQueryFetcher/all(decoding:prefix:keyDecodingStrategy:userInfo:)-5u1nz``
-- ``SQLQueryFetcher/all(decoding:with:)-6n5ox``
-- ``SQLQueryFetcher/all(decodingColumn:as:)-7x9bs``
+- ``SQLQueryFetcher/all(decoding:with:)->[D]``
+- ``SQLQueryFetcher/all(decoding:)->[D]``
+- ``SQLQueryFetcher/all(decoding:prefix:keyDecodingStrategy:userInfo:)->[D]``
+- ``SQLQueryFetcher/all(decoding:with:)->[D]``
+- ``SQLQueryFetcher/all(decodingColumn:as:)->[D]``
### Getting One Row
-- ``SQLQueryFetcher/first()-99pqx``
-- ``SQLQueryFetcher/first(decoding:)-63noi``
-- ``SQLQueryFetcher/first(decoding:prefix:keyDecodingStrategy:userInfo:)-2str1``
-- ``SQLQueryFetcher/first(decoding:with:)-58l9p``
-- ``SQLQueryFetcher/first(decodingColumn:as:)-1bcz6``
+- ``SQLQueryFetcher/first()->EventLoopFuture<(SQLRow)?>``
+- ``SQLQueryFetcher/first(decoding:)->D?``
+- ``SQLQueryFetcher/first(decoding:prefix:keyDecodingStrategy:userInfo:)->D?``
+- ``SQLQueryFetcher/first(decoding:with:)->D?``
+- ``SQLQueryFetcher/first(decodingColumn:as:)->D?``
### Legacy `EventLoopFuture` Interfaces
-- ``SQLQueryFetcher/run(_:)-542bs``
-- ``SQLQueryFetcher/run(decoding:_:)-6z89k``
-- ``SQLQueryFetcher/run(decoding:prefix:keyDecodingStrategy:userInfo:_:)-2cp56``
-- ``SQLQueryFetcher/run(decoding:with:_:)-4tte7``
-- ``SQLQueryFetcher/all()-5j67e``
-- ``SQLQueryFetcher/all(decoding:)-6q02f``
-- ``SQLQueryFetcher/all(decoding:prefix:keyDecodingStrategy:userInfo:)-91za9``
-- ``SQLQueryFetcher/all(decoding:with:)-5fc4b``
-- ``SQLQueryFetcher/all(decodingColumn:as:)-197ym``
-- ``SQLQueryFetcher/first()-7o93q``
-- ``SQLQueryFetcher/first(decoding:)-6gqh3``
-- ``SQLQueryFetcher/first(decoding:prefix:keyDecodingStrategy:userInfo:)-4hfrz``
-- ``SQLQueryFetcher/first(decoding:with:)-1n97m``
-- ``SQLQueryFetcher/first(decodingColumn:as:)-4965m``
+- ``SQLQueryFetcher/run(_:)->_``
+- ``SQLQueryFetcher/run(decoding:_:)->_``
+- ``SQLQueryFetcher/run(decoding:prefix:keyDecodingStrategy:userInfo:_:)->_``
+- ``SQLQueryFetcher/run(decoding:with:_:)->_``
+- ``SQLQueryFetcher/all()->EventLoopFuture<[SQLRow]>``
+- ``SQLQueryFetcher/all(decoding:)->EventLoopFuture<[D]>``
+- ``SQLQueryFetcher/all(decoding:prefix:keyDecodingStrategy:userInfo:)->EventLoopFuture<[D]>``
+- ``SQLQueryFetcher/all(decoding:with:)->EventLoopFuture<[D]>``
+- ``SQLQueryFetcher/all(decodingColumn:as:)->EventLoopFuture<[D]>``
+- ``SQLQueryFetcher/first()->EventLoopFuture<(SQLRow)?>``
+- ``SQLQueryFetcher/first(decoding:)->EventLoopFuture``
+- ``SQLQueryFetcher/first(decoding:prefix:keyDecodingStrategy:userInfo:)->EventLoopFuture``
+- ``SQLQueryFetcher/first(decoding:with:)->EventLoopFuture``
+- ``SQLQueryFetcher/first(decodingColumn:as:)->EventLoopFuture``
diff --git a/Sources/SQLKit/Docs.docc/theme-settings.json b/Sources/SQLKit/Docs.docc/theme-settings.json
index 4dce41b4..67e6b5da 100644
--- a/Sources/SQLKit/Docs.docc/theme-settings.json
+++ b/Sources/SQLKit/Docs.docc/theme-settings.json
@@ -1,6 +1,6 @@
{
"theme": {
- "aside": { "border-radius": "16px", "border-style": "double", "border-width": "3px" },
+ "aside": { "border-radius": "16px", "border-width": "3px", "border-style": "double" },
"border-radius": "0",
"button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
"code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
diff --git a/Sources/SQLKit/Expressions/Clauses/SQLConflictResolutionStrategy.swift b/Sources/SQLKit/Expressions/Clauses/SQLConflictResolutionStrategy.swift
index efeb35ab..5c8f85ef 100644
--- a/Sources/SQLKit/Expressions/Clauses/SQLConflictResolutionStrategy.swift
+++ b/Sources/SQLKit/Expressions/Clauses/SQLConflictResolutionStrategy.swift
@@ -88,7 +88,7 @@ public struct SQLConflictResolutionStrategy: SQLExpression {
$0.append(SQLGroupExpression(self.targetColumns))
}
$0.append("DO UPDATE SET", SQLList(assignments))
- if let predicate = predicate { $0.append("WHERE", predicate) }
+ if let predicate { $0.append("WHERE", predicate) }
case (.mysqlLike, .noAction):
break
case (.mysqlLike, .update(let assignments, _)):
diff --git a/Sources/SQLKit/Expressions/Queries/SQLInsert.swift b/Sources/SQLKit/Expressions/Queries/SQLInsert.swift
index 220f6ab9..7ae05044 100644
--- a/Sources/SQLKit/Expressions/Queries/SQLInsert.swift
+++ b/Sources/SQLKit/Expressions/Queries/SQLInsert.swift
@@ -52,10 +52,21 @@ public struct SQLInsert: SQLExpression {
/// is generated.
public var valueQuery: (any SQLExpression)? = nil
- /// If not `nil`, a strategy for resolving conflicts created by violations of applicable constraints.
+ /// The actual conflict resolution strategy serialized by this query. This property is provided to enable easier use
+ /// of database-specific conflict resolution syntax, as changing ``conflictStrategy`` to have the more generic type
+ /// would be API-breaking.
+ public var genericConflictStrategy: (any SQLExpression)? = nil
+
+ /// If not `nil`, a database-agnostic strategy for resolving conflicts created by violations of applicable constraints.
+ ///
+ /// If ``genericConflictStrategy`` is set to an expression which is not castable to ``SQLConflictResolutionStrategy``,
+ /// this property will be `nil`. Setting this property unconditionally overwrites ``genericConflictStrategy``.
///
- /// See ``SQLConflictResolutionStrategy``.
- public var conflictStrategy: SQLConflictResolutionStrategy? = nil
+ /// See ``SQLConflictResolutionStrategy`` and ``genericConflictStrategy``.
+ public var conflictStrategy: SQLConflictResolutionStrategy? {
+ get { self.genericConflictStrategy as? SQLConflictResolutionStrategy }
+ set { self.genericConflictStrategy = newValue }
+ }
/// An optional ``SQLReturning`` clause specifying data to return from the inserted rows.
///
@@ -75,7 +86,7 @@ public struct SQLInsert: SQLExpression {
serializer.statement {
$0.append(self.tableExpressionGroup)
$0.append("INSERT")
- $0.append(self.conflictStrategy?.queryModifier(for: $0))
+ $0.append(self.conflictStrategy?.queryModifier(for: $0)) // will be `nil` if genericConflictStrategy is in use
$0.append("INTO", self.table)
$0.append(SQLGroupExpression(self.columns))
if !self.values.isEmpty {
@@ -83,8 +94,9 @@ public struct SQLInsert: SQLExpression {
} else if let subquery = self.valueQuery {
$0.append(subquery)
}
- $0.append(self.conflictStrategy)
+ $0.append(self.genericConflictStrategy)
$0.append(self.returning)
}
}
}
+
diff --git a/Tests/SQLKitTests/SQLInsertUpsertTests.swift b/Tests/SQLKitTests/SQLInsertUpsertTests.swift
index 4935c0f6..6a1f3d69 100644
--- a/Tests/SQLKitTests/SQLInsertUpsertTests.swift
+++ b/Tests/SQLKitTests/SQLInsertUpsertTests.swift
@@ -204,4 +204,64 @@ final class SQLInsertUpsertTests: XCTestCase {
is: "INSERT INTO ``jumpgates`` (``id``, ``serial_number``, ``star_id``, ``last_known_status``) VALUES (DEFALLT, &1, &2, &3) ON CONFLICT (``id``) DO UPDATE SET ``last_known_status`` = &4 WHERE ``last_known_status`` <> &5"
)
}
+
+ func testGenericNonstandardUpsert() {
+ let cols = ["id", "serial_number", "star_id", "last_known_status"]
+ let vals = { (s: String) -> [any SQLExpression] in [SQLLiteral.default, SQLBind(UUID()), SQLBind(1), SQLBind(s)] }
+
+ db._dialect.upsertSyntax = .standard
+
+ XCTAssertSerialization(
+ of: self.db.insert(into: "jumpgates")
+ .columns(cols).values(vals("Vorlon pinching"))
+ .ignoringConflicts(withThing: SQLIdentifier("serial_number")),
+ is: "INSERT INTO ``jumpgates`` (``id``, ``serial_number``, ``star_id``, ``last_known_status``) VALUES (DEFALLT, &1, &2, &3) ON CONFLICT ON CONSTRAINT ``serial_number`` DO NOTHING"
+ )
+ XCTAssertSerialization(
+ of: self.db.insert(into: "jumpgates")
+ .columns(cols).values(vals("slashfic writing"))
+ .onConflict(withThing: SQLIdentifier("serial_number")) { $0
+ .set("last_known_status", to: "tachyon antitelephone dialing the").set(excludedValueOf: "star_id")
+ },
+ is: "INSERT INTO ``jumpgates`` (``id``, ``serial_number``, ``star_id``, ``last_known_status``) VALUES (DEFALLT, &1, &2, &3) ON CONFLICT ON CONSTRAINT ``serial_number`` DO UPDATE SET ``last_known_status`` = &4, ``star_id`` = EXCLUDED.``star_id``"
+ )
+ XCTAssertSerialization(
+ of: self.db.insert(into: "jumpgates")
+ .columns(cols).values(vals("protection racket payoff"))
+ .onConflict(withThing: SQLIdentifier("id")) { $0
+ .set("last_known_status", to: "insurance fraud planning")
+ .where("last_known_status", .notEqual, "evidence disposal")
+ },
+ is: "INSERT INTO ``jumpgates`` (``id``, ``serial_number``, ``star_id``, ``last_known_status``) VALUES (DEFALLT, &1, &2, &3) ON CONFLICT ON CONSTRAINT ``id`` DO UPDATE SET ``last_known_status`` = &4 WHERE ``last_known_status`` <> &5"
+ )
+ }
+}
+
+extension SQLInsertBuilder {
+ struct SQLCustomConflictResolutionStrategy: SQLExpression {
+ var thing: any SQLExpression, action: SQLConflictAction
+ func serialize(to serializer: inout SQLSerializer) {
+ serializer.statement {
+ $0.append("ON CONFLICT ON CONSTRAINT", self.thing)
+ switch self.action {
+ case .noAction: $0.append("DO NOTHING")
+ case .update(let assignments, let predicate):
+ $0.append("DO UPDATE SET", SQLList(assignments))
+ if let predicate { $0.append("WHERE", predicate) }
+ }
+ }
+ }
+ }
+ @discardableResult
+ func ignoringConflicts(withThing thing: any SQLExpression) -> Self {
+ self.insert.genericConflictStrategy = SQLCustomConflictResolutionStrategy(thing: thing, action: .noAction)
+ return self
+ }
+ @discardableResult
+ func onConflict(withThing thing: any SQLExpression, `do` updatePredicate: (SQLConflictUpdateBuilder) throws -> SQLConflictUpdateBuilder) rethrows -> Self {
+ let conflictBuilder = SQLConflictUpdateBuilder()
+ _ = try updatePredicate(conflictBuilder)
+ self.insert.genericConflictStrategy = SQLCustomConflictResolutionStrategy(thing: thing, action: .update(assignments: conflictBuilder.values, predicate: conflictBuilder.predicate))
+ return self
+ }
}