Skip to content

Commit

Permalink
Major overhaul of the entire SQLKit package (#172)
Browse files Browse the repository at this point in the history
* Bump Swift to 5.8, add Dependabot config
* Docs overhaul
* Deprecate \(raw:) in SQLQueryString with rename to explicitly "unsafe" version, numerous other SQLQueryString cleanups
* ExistentialAny compliance
* Overhaul SQLRowDecoder and SQLQueryEncoder
* NIO -> NIOCore in imports
* Fix all Sendable complaints from the compiler
* Make SQLDatabaseReportedVersion Comparable
* Deprecate SQLError and SQLErrorType, deprecate use of binds in SQLRaw.
* Improve the behavior of the async-aware implementations
* Basically redo all the tests
* Cleanup of SQLKitBenchmark
* Deprecate `SQLTriggerWhen/Event/Each/Order/Timing` in favor of better-namespaced names, add missing support for `SQLTriggerSyntax.Create.supportsDefiner`
* Add SQLBetween and SQLQualifiedTable
* Structural updates
* Add missing predicate builder method to match the secondary predicate builder API
* Misc general cleanup
* Add basic support for "INSERT ... SELECT" queries to SQLInsert, add SQLSubquery and SQLSubqueryBuilder, use them in SQLCreateTableBuilder and SQLInsertBuilder
* Make SQLLiteral.string and SQLIdentifier aware of proper escaping of their respective quoting.
* Add all/first(decodingColumn:) utilities to SQLQueryFetcher
* Add column list builders
* Be more consistent about use of `String` versus `StringProtocol` and `any SQLExpression` versus `some SQLExpression`.
* Add missing model decoding methods to SQLQueryFetcher. Further revise SQLRowDecoder and SQLQueryEncoder to provide the documented functionality correctly. Improve docs a bunch more. Add several missing "model"-handling methods to SQLInsertBuilder, SQLColumnUpdateBuilder, etc. Separate out the string handling utilities into their own file and refine the coding error handling.
* Add SQLDataType.timestamp
* Make the SQLStatement API more useful, speed up serialization very slightly, improve various other serialize(to:) methods
* Make SQLDropBehavior respect the dialect, add predicate support to SQLCreateIndex, use SQLDropBehavior in SQLDropEnum (and make it respect the dialect for IF EXISTS), correct the docs and implementation of SQLDistinct, docs and serialization improvements for the rest of the query expressions. SQLDropTrigger gets a dropBehavior property, and SQLDropEnum's name is now mutable as with other properties.
* Remove useless property on SQLAliasedColumnListBuilder, fix SQLColumnUpdateBuilder to correctly use SQLColumnAssignment
* Make SQLPredicateBuilder and SQLSecondaryPredicateBuilder as fully consistent as possible.
* Make the async version of SQLDatabase/execute a protocol requirement so non-default implementations are invoked when used through existentials
* Fix SQLSelect and SQLList serialization, fix tests
  • Loading branch information
gwynne committed Apr 13, 2024
1 parent 7d36d6c commit f1263a2
Show file tree
Hide file tree
Showing 150 changed files with 11,347 additions and 4,796 deletions.
37 changes: 37 additions & 0 deletions .github/.codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
codecov:
notify:
after_n_builds: 1
wait_for_ci: false
require_ci_to_pass: false
comment:
behavior: default
layout: diff, files
require_changes: true
coverage:
status:
patch:
default:
branches:
- ^main$
informational: true
only_pulls: false
paths:
- ^Sources.*
target: auto
project:
default:
branches:
- ^main$
informational: true
only_pulls: false
paths:
- ^Sources.*
target: auto
github_checks:
annotations: true
ignore:
- ^Sources/SQLKitBenchmark/.*
- ^Tests/.*
- ^.build/.*
slack_app: false

5 changes: 0 additions & 5 deletions .github/CONTRIBUTING.md

This file was deleted.

10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
groups:
dependencies:
patterns:
- "*"
10 changes: 5 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,21 @@ jobs:
if: ${{ !(github.event.pull_request.draft || false) }}
services:
mysql-a:
image: mysql:8
image: mysql:latest
env: { MYSQL_USER: test_username, MYSQL_PASSWORD: test_password, MYSQL_DATABASE: test_database, MYSQL_ALLOW_EMPTY_PASSWORD: true }
mysql-b:
image: mysql:8
image: mysql:latest
env: { MYSQL_USER: test_username, MYSQL_PASSWORD: test_password, MYSQL_DATABASE: test_database, MYSQL_ALLOW_EMPTY_PASSWORD: true }
psql-a:
image: postgres:16
image: postgres:latest
env: { POSTGRES_USER: test_username, POSTGRES_PASSWORD: test_password, POSTGRES_DB: test_database }
psql-b:
image: postgres:16
image: postgres:latest
env: { POSTGRES_USER: test_username, POSTGRES_PASSWORD: test_password, POSTGRES_DB: test_database }
strategy:
fail-fast: false
matrix:
swift-image: ['swift:5.9-jammy']
swift-image: ['swift:5.10-jammy']
driver:
- { sqlkit: 'sqlite-kit', fluent: 'fluent-sqlite-driver' }
- { sqlkit: 'mysql-kit', fluent: 'fluent-mysql-driver' }
Expand Down
5 changes: 2 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
/.build
/Packages
/*.xcodeproj
Package.resolved
/Package.resolved
DerivedData
.swiftpm
Tests/LinuxMain.swift
/.swiftpm
55 changes: 41 additions & 14 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.6
// swift-tools-version:5.8
import PackageDescription

let package = Package(
Expand All @@ -14,20 +14,47 @@ let package = Package(
.library(name: "SQLKitBenchmark", targets: ["SQLKitBenchmark"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.64.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"),
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.3.0"),
],
targets: [
.target(name: "SQLKit", dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "NIO", package: "swift-nio"),
]),
.target(name: "SQLKitBenchmark", dependencies: [
.target(name: "SQLKit")
]),
.testTarget(name: "SQLKitTests", dependencies: [
.target(name: "SQLKit"),
.target(name: "SQLKitBenchmark"),
]),
.target(
name: "SQLKit",
dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "Collections", package: "swift-collections"),
],
swiftSettings: swiftSettings
),
.target(
name: "SQLKitBenchmark",
dependencies: [
.target(name: "SQLKit"),
],
swiftSettings: swiftSettings
),
.testTarget(
name: "SQLKitTests",
dependencies: [
.target(name: "SQLKit"),
.target(name: "SQLKitBenchmark"),
],
swiftSettings: swiftSettings
),
]
)

var swiftSettings: [SwiftSetting] { [
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("ConciseMagicFile"),
.enableUpcomingFeature("ForwardTrailingClosures"),
.enableUpcomingFeature("ImportObjcForwardDeclarations"),
.enableUpcomingFeature("DisableOutwardActorInference"),
.enableUpcomingFeature("IsolatedDefaultValues"),
.enableUpcomingFeature("GlobalConcurrency"),
.enableUpcomingFeature("StrictConcurrency"),
.enableExperimentalFeature("StrictConcurrency=complete"),
] }
38 changes: 16 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
<p align="center">
<img src="https://user-images.githubusercontent.com/1342803/58835528-3523e400-8624-11e9-8128-4925c7c9cf08.png" height="64" alt="SQLKit">
<br>
<br>
<a href="https://docs.vapor.codes/4.0/">
<img src="http://img.shields.io/badge/read_the-docs-2196f3.svg" alt="Documentation">
</a>
<a href="https://discord.gg/vapor">
<img src="https://img.shields.io/discord/431917998102675485.svg" alt="Team Chat">
</a>
<a href="LICENSE">
<img src="http://img.shields.io/badge/license-MIT-brightgreen.svg" alt="MIT License">
</a>
<a href="https://github.com/vapor/sql-kit/actions/workflows/test.yml">
<img src="https://img.shields.io/github/actions/workflow/status/vapor/sql-kit/test.yml?event=push&&logo=github&label=tests&logoColor=%23ccc" alt="Continuous Integration">
</a>
<a href="https://swift.org">
<img src="http://img.shields.io/badge/swift-5.6-brightgreen.svg" alt="Swift 5.6">
</a>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/vapor/sql-kit/assets/1130717/b5828634-c1a1-4d91-b25d-20e033b77269">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/vapor/sql-kit/assets/1130717/f45e4b01-0579-4011-8b06-0f159e0d386f">
<img src="https://github.com/vapor/sql-kit/assets/1130717/f45e4b01-0579-4011-8b06-0f159e0d386f" height="96" alt="SQLKit">
</picture>
<br>
<br>
<a href="https://docs.vapor.codes/4.0/"><img src="https://design.vapor.codes/images/readthedocs.svg" alt="Documentation"></a>
<a href="https://discord.gg/vapor"><img src="https://design.vapor.codes/images/discordchat.svg" alt="Team Chat"></a>
<a href="LICENSE"><img src="https://design.vapor.codes/images/mitlicense.svg" alt="MIT License"></a>
<a href="https://github.com/vapor/sql-kit/actions/workflows/test.yml"><img src="https://img.shields.io/github/actions/workflow/status/vapor/sql-kit/test.yml?event=push&style=plastic&logo=github&label=tests&logoColor=%23ccc" alt="Continuous Integration"></a>
<a href="https://codecov.io/github/vapor/sql-kit"><img src="https://img.shields.io/codecov/c/github/vapor/sql-kit?style=plastic&logo=codecov&label=codecov"></a>
<a href="https://swift.org"><img src="https://design.vapor.codes/images/swift58up.svg" alt="Swift 5.8+"></a>
</p>

<br>
Expand Down Expand Up @@ -60,7 +55,7 @@ SQLKit does not deal with creating or managing database connections itself. This

### Database

Instances of `SQLDatabase` are capable of serializing and executing `SQLExpression`.
Instances of `SQLDatabase` are capable of serializing and executing `SQLExpression`s.

```swift
let db: any SQLDatabase = ...
Expand Down Expand Up @@ -282,6 +277,5 @@ SELECT * FROM "planets" WHERE "name" = $1 -- bindings: ["planet"]

The `\(bind:)` interpolation should be used for any user input to avoid SQL injection. The `\(ident:)` interpolation is used to safely specify identifiers such as table and column names.

##### ⚠️ **Important!**⚠️

Always prefer a structured query (i.e. one for which a builder or expression type exists) over raw queries. Consider writing your own `SQLExpression`s, and even your own `SQLQueryBuilder`s, rather than using raw queries, and don't hesitate to [open an issue](https://github.com/vapor/sql-kit/issues/new) to ask for additional feature support.
> [!IMPORTANT]
> Always prefer a structured query (i.e. one for which a builder or expression type exists) over raw queries. Consider writing your own `SQLExpression`s, and even your own `SQLQueryBuilder`s, rather than using raw queries, and don't hesitate to [open an issue](https://github.com/vapor/sql-kit/issues/new) to ask for additional feature support.
16 changes: 2 additions & 14 deletions Sources/SQLKit/Builders/Implementations/SQLAlterEnumBuilder.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import NIOCore

/// Builds ``SQLAlterEnum`` queries.
public final class SQLAlterEnumBuilder: SQLQueryBuilder {
/// ``SQLAlterEnum`` query being built.
public var alterEnum: SQLAlterEnum

/// See ``SQLQueryBuilder/database``.
// See `SQLQueryBuilder.database`.
public var database: any SQLDatabase

/// See ``SQLQueryBuilder/query``.
// See `SQLQueryBuilder.query`.
@inlinable
public var query: any SQLExpression {
self.alterEnum
Expand All @@ -35,16 +33,6 @@ public final class SQLAlterEnumBuilder: SQLQueryBuilder {
self.alterEnum.value = value
return self
}

/// See ``SQLQueryBuilder/run()-2zws8``.
@inlinable
public func run() -> EventLoopFuture<Void> {
guard self.database.dialect.enumSyntax == .typeName else {
self.database.logger.warning("Database does not support standalone enum types.")
return self.database.eventLoop.makeSucceededFuture(())
}
return self.database.execute(sql: self.query) { _ in }
}
}

extension SQLDatabase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,15 @@ public final class SQLAlterTableBuilder: SQLQueryBuilder {
/// ``SQLAlterTable`` query being built.
public var alterTable: SQLAlterTable

/// See ``SQLQueryBuilder/database``.
// See `SQLQueryBuilder.database`.
public var database: any SQLDatabase

/// See ``SQLQueryBuilder/query``.
// See `SQLQueryBuilder.query`.
@inlinable
public var query: any SQLExpression {
self.alterTable
}

/// The set of column alteration expressions.
@inlinable
public var columns: [any SQLExpression] {
get { self.alterTable.addColumns }
set { self.alterTable.addColumns = newValue }
}

/// Create a new ``SQLAlterTableBuilder``.
@inlinable
public init(_ alterTable: SQLAlterTable, on database: any SQLDatabase) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,78 @@
/// A builder for specifying column updates and an optional predicate to be applied to
/// rows that caused unique key conflicts during an `INSERT`.
public final class SQLConflictUpdateBuilder: SQLColumnUpdateBuilder, SQLPredicateBuilder {
/// See ``SQLColumnUpdateBuilder/values``.
public var values: [any SQLExpression]
// See `SQLColumnUpdateBuilder.values`.
public var values: [any SQLExpression] = []

/// See ``SQLPredicateBuilder/predicate``.
public var predicate: (any SQLExpression)?
// See `SQLPredicateBuilder.predicate`.
public var predicate: (any SQLExpression)? = nil

/// Create a conflict update builder.
@usableFromInline
init() {
self.values = []
self.predicate = nil
}
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. See ``SQLExcludedColumn``.
/// given in the `INSERT` query's `VALUES` list.
///
/// See ``SQLExcludedColumn`` for additional details.
@inlinable
@discardableResult
public func set(excludedValueOf columnName: String) -> Self {
self.set(excludedValueOf: SQLColumn(columnName))
}

/// Add an assignment of the given column, using the value the column was given in the
/// `INSERT` query's `VALUES` list. See ``SQLExcludedColumn``.
/// `INSERT` query's `VALUES` list.
///
/// See ``SQLExcludedColumn`` for additional details.
@inlinable
@discardableResult
public func set(excludedValueOf column: any SQLExpression) -> Self {
self.values.append(SQLColumnAssignment(settingExcludedValueFor: column))
return self
}

/// Encodes the given ``Encodable`` value to a sequence of key-value pairs and adds an assignment
/// Encodes the given `Encodable` value to a sequence of key-value pairs and adds an assignment
/// for each pair which uses the values each column was given in the original `INSERT` query's
/// `VALUES` list.
///
/// See ``SQLExcludedColumn`` and ``SQLQueryEncoder`` for additional details.
///
/// > Important: The actual values stored in the provided `model` _are not used_ by this method.
/// > The model is encoded, then the resulting values are discarded and the list of column names
/// > is used to repeatedly invoke ``set(excludedValueOf:)-zmis``. This is potentially very
/// > inefficient; a future version of the API will offer the ability to efficiently set the
/// > excluded values for all input columns in one operation.
@inlinable
@discardableResult
public func set(
excludedContentOf model: some Encodable & Sendable,
prefix: String? = nil,
keyEncodingStrategy: SQLQueryEncoder.KeyEncodingStrategy = .useDefaultKeys,
nilEncodingStrategy: SQLQueryEncoder.NilEncodingStrategy = .default,
userInfo: [CodingUserInfoKey: any Sendable] = [:]
) throws -> Self {
try self.set(
excludedContentOf: model,
with: .init(prefix: prefix, keyEncodingStrategy: keyEncodingStrategy, nilEncodingStrategy: nilEncodingStrategy, userInfo: userInfo)
)
}

/// Encodes the given `Encodable` value to a sequence of key-value pairs and adds an assignment
/// for each pair which uses the values each column was given in the original `INSERT` query's
/// `VALUES` list. See ``SQLExcludedColumn``.
/// `VALUES` list. See ``SQLExcludedColumn`` and ``SQLQueryEncoder``.
///
/// > Important: The actual values stored in the provided `model` _are not used_ by this method.
/// > The model is encoded, then the resulting values are discarded and the list of column names
/// > is used to repeatedly invoke ``set(excludedValueOf:)-zmis``. This is potentially very
/// > inefficient; a future version of the API will offer the ability to efficiently set the
/// > excluded values for all input columns in one operation.
@inlinable
@discardableResult
public func set<E>(excludedContentOf model: E) throws -> Self where E: Encodable {
try SQLQueryEncoder().encode(model).reduce(self) { $0.set(excludedValueOf: $1.0) }
public func set(
excludedContentOf model: some Encodable & Sendable,
with encoder: SQLQueryEncoder
) throws -> Self {
try encoder.encode(model).reduce(self) { $0.set(excludedValueOf: $1.0) }
}
}
Loading

0 comments on commit f1263a2

Please sign in to comment.