From 1e0239616134dd45fa4894bfe83750d196e3a83d Mon Sep 17 00:00:00 2001 From: Tanner Date: Fri, 24 Apr 2020 10:48:02 -0400 Subject: [PATCH] 3.0.0 GM (#103) * fix maintainers link * fix maintainers link * docblock / file cleanup * implement SQLJoinBuilder, fixes #77 * Add KeyDecodingStrategy to SQLRow Decode (#82) * Add serialize support for LIKE and NOT LIKE Binary Operators * added JoinBuilder to SelectBuilder * fix documentation * removed old changed * removed change from other PR * fixed documentation * fixed table name in tests * remove test join comment * readd like and not like * removed SQLJoinBinaryExpression and SQLTableIdentifier. Updated indentation. * removed SQLJoinBinaryExpression and SQLTableIdentifier, updated indentation. * made join default to inner * added custom key decoding strategy to SQLRowDecoder * removed code from separate PR * Removed Code from separate PR * remove more code for separate PR * removed more code for separate pr * removed code for a separate pr * all new updates * add test * added array decoding * added minimum iOS version * Added options to SQLRowDecoder. Made KeyDecodingStrategy an enum inside SQLRowDecoder. * remove left over debuting code * added a custom keyDecodingStrategy test * changed keyPrefix back to prefix, added comments * removed a _convertFromSnakeCase implementation in favour of the implementation to Foundation's JSONEncoder.swift * make SQLRowDecoder init explicit Co-authored-by: {rnantes} <{reid.nantes@gmail.com}> * added key encoding strategy (#101) Co-authored-by: {rnantes} <{reid.nantes@gmail.com}> * updates * where(...) overloads * select section * select + insert * add .insert() * update * where group * update * delete * raw * updates * remove dead code Co-authored-by: Reid Nantes Co-authored-by: {rnantes} <{reid.nantes@gmail.com}> --- CONTRIBUTING.md => .github/CONTRIBUTING.md | 2 +- README.md | 291 +++++++++++++- .../Builders/SQLAlterTableBuilder.swift | 11 +- .../Builders/SQLCreateEnumBuilder.swift | 6 +- .../Builders/SQLCreateIndexBuilder.swift | 14 +- .../Builders/SQLCreateTableBuilder.swift | 29 +- .../SQLKit/Builders/SQLDeleteBuilder.swift | 15 +- .../SQLKit/Builders/SQLDropEnumBuilder.swift | 4 +- .../SQLKit/Builders/SQLDropTableBuilder.swift | 6 +- .../Builders/SQLDropTriggerBuilder.swift | 2 +- .../SQLKit/Builders/SQLInsertBuilder.swift | 46 ++- Sources/SQLKit/Builders/SQLJoinBuilder.swift | 82 ++++ .../SQLKit/Builders/SQLPredicateBuilder.swift | 48 ++- Sources/SQLKit/Builders/SQLQueryFetcher.swift | 1 - Sources/SQLKit/Builders/SQLRawBuilder.swift | 4 +- .../SQLKit/Builders/SQLSelectBuilder.swift | 373 ++++++++---------- .../SQLKit/Builders/SQLUpdateBuilder.swift | 13 +- Sources/SQLKit/Query/SQLAlterTable.swift | 2 +- Sources/SQLKit/Query/SQLBind.swift | 6 + .../SQLKit/Query/SQLColumnIdentifier.swift | 2 +- Sources/SQLKit/Query/SQLDistinct.swift | 2 +- Sources/SQLKit/Query/SQLIdentifier.swift | 6 + Sources/SQLKit/Query/SQLJoin.swift | 6 +- Sources/SQLKit/SQLDatabase.swift | 2 +- Sources/SQLKit/SQLQueryEncoder.swift | 101 ++++- Sources/SQLKit/SQLRow.swift | 13 +- Sources/SQLKit/SQLRowDecoder.swift | 134 ++++++- Sources/SQLKit/SQLSerializer.swift | 34 -- Sources/SQLKit/SQLStatement.swift | 33 ++ .../SQLBenchmark+Codeable.swift | 47 +++ .../SQLBenchmark+Planets.swift | 56 +-- Tests/SQLKitTests/SQLKitTests.swift | 106 ++++- 32 files changed, 1047 insertions(+), 450 deletions(-) rename CONTRIBUTING.md => .github/CONTRIBUTING.md (67%) create mode 100644 Sources/SQLKit/Builders/SQLJoinBuilder.swift create mode 100644 Sources/SQLKit/SQLStatement.swift create mode 100644 Sources/SQLKitBenchmark/SQLBenchmark+Codeable.swift diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 67% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md index 0ccd373f..a02934c3 100644 --- a/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,4 +2,4 @@ - [@gwynne](https://github.com/gwynne) -See the [Vapor maintainers doc](https://github.com/vapor/vapor/blob/master/Docs/maintainers.md) for more information. +See the [Vapor maintainers doc](https://github.com/vapor/vapor/blob/master/.github/maintainers.md) for more information. diff --git a/README.md b/README.md index 74e95dd0..ff506b35 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,271 @@ -

- SQLKit -
-
- - Documentation - - - Team Chat - - - MIT License - - - Continuous Integration - - - Swift 5.2 - -

+SQLKit +
+ + Documentation + + + Team Chat + + + MIT License + + + Continuous Integration + + + Swift 5.2 + +
+
+ +Build SQL queries in Swift. Extensible, protocol-based design that supports DQL, DML, and DDL. + +### Major Releases + +The table below shows a list of PostgresNIO major releases alongside their compatible NIO and Swift versions. + +|Version|NIO|Swift|SPM| +|-|-|-|-| +|3.0|2.0+|5.2+|`from: "3.0.0"`| +|2.0|1.0+|4.0+|`from: "2.0.0"`| +|1.0|n/a|4.0+|`from: "1.0.0"`| + +Use the SPM string to easily include the dependendency in your `Package.swift` file. + +```swift +.package(url: "https://github.com/vapor/sql-kit.git", from: ...) +``` + +### Supported Platforms + +PostgresNIO supports the following platforms: + +- Ubuntu 16.04+ +- macOS 10.15+ + +## Overview + +SQLKit is an API for building and serializing SQL queries in Swift. SQLKit attempts to abstract away SQL dialect inconsistencies where possible allowing you to write queries that can run on multiple database flavors. Where abstraction is not possible, SQLKit provides powerful APIs for custom or dynamic behavior. + +### Supported Databases + +These database packages are built on SQLKit: + +- [vapor/postgres-kit](https://github.com/vapor/postgres-kit): PostgreSQL +- [vapor/mysql-kit](https://github.com/vapor/mysql-kit): MySQL and MariaDB +- [vapor/sqlite-kit](https://github.com/vapor/sqlite-kit): SQLite + +### Configuration + +SQLKit does not deal with creating or managing database connections itself. This package is focused entirely around building and serializing SQL queries. To connect to your SQL database, refer to your specific database package's documentation. Once you are connected to your database and have an instance of `SQLDatabase`, you are ready to continue. + +### Database + +Instances of `SQLDatabase` are capable of serializing and executing `SQLExpression`. + +```swift +let db: SQLDatabase = ... +db.execute(sql: SQLExpression, onRow: (SQLRow) -> ()) +``` + +`SQLExpression` is a protocol that represents a SQL query string and optional bind values. It can represent an entire SQL query or just a fragment. + +SQLKit provides `SQLExpression`s for common queries like `SELECT`, `UPDATE`, `INSERT`, `DELETE`, `CREATE TABLE`, and more. + +```swift +var select = SQLSelect() +select.columns = [...] +select.tables = [...] +select.predicate = ... +``` + +`SQLDatabase` can be used to create fluent query builders for most of these query types. + +```swift +let planets = try db.select() + .column("*") + .from("planets") + .where("name", .equal, "Earth") + .all().wait() +``` + +You can execute a query builder by calling `run()`. + +### Rows + +For query builders that support returning results, like `select()`, there are additional methods for handling the database output. + +- `all()`: Returns an array of rows. +- `first()`: Returns an optional row. +- `run(_:)`: Accepts a closure that handles rows as they are returned. + +Each of these methods returns `SQLRow` which has methods for access column values. + +```swift +let row: SQLRow +let name = try row.decode(column: "name", as: String.self) +print(name) // String +``` + +### Codable + +`SQLRow` also supports decoding `Codable` models directly from a row. + +```swift +struct Planet: Codable { + var name: String +} + +let planet = try row.decode(model: Planet.self) +``` + +Query builders that support returning results have convenience methods for automatically decoding models. + +```swift +let planets = try db.select() + ... + .all(decoding: Planet.self).wait() +``` + +## Select + +The `select()` method creates a `SELECT` query builder. + +```swift +let planets = try db.select() + .columns("id", "name") + .from("planets") + .where("name", .equal, "Earth") + .all().wait() +``` + +This code would generate the following SQL: + +```sql +SELECT id, name FROM planets WHERE name = ? +``` + +Notice that `Encodable` values are automatically bound as parameters instead of being serialized directly to the query. + +The select builder has the following methods. + +- `columns` +- `from` +- `where` (`orWhere`) +- `limit` +- `offset` +- `groupBy` +- `having` (`orHaving`) +- `distinct` +- `for` (`lockingClause`) +- `join` + +By default, query components like `where` will be joined by `AND`. Methods prefixed with `or` exist for joining by `OR`. + +```swift +builder.where("name", .equal, "Earth").orWhere("name", .equal, "Mars") +``` + +This code would generate the following SQL: + +```sql +name = ? OR name = ? +``` + +`where` also supports creating grouped clauses. + +```swift +builder.where("name", .notEqual, SQLLiteral.null).where { + $0.where("name", .equal, SQLBind("Milky Way")) + .orWhere("name", .equal, SQLBind("Andromeda")) +} +``` + +This code generates the following SQL: + +```sql +name != NULL AND (name == ? OR name == ?) +``` + +## Insert + +The `insert(into:)` method creates an `INSERT` query builder. + +```swift +try db.insert(into: "galaxies") + .columns("id", "name") + .values(SQLLiteral.default, SQLBind("Milky Way")) + .values(SQLLiteral.default, SQLBind("Andromeda")) + .run().wait() +``` + +This code would generate the following SQL: + +```sql +INSERT INTO galaxies (id, name) VALUES (DEFAULT, ?) (DEFAULT, ?) +``` + +The insert builder also has a method for encoding a `Codable` type as a set of values. + +```swift +struct Galaxy: Codable { + var name: String +} + +try builder.model(Galaxy(name: "Milky Way")) +``` + +## Update + +The `update(_:)` method creates an `UPDATE` query builder. + +```swift +try db.update("planets") + .where("name", .equal, "Jpuiter") + .set("name", to: "Jupiter") + .run().wait() +``` + +This code generates the following SQL: + +```sql +UPDATE planets SET name = ? WHERE name = ? +``` + +The update builder supports the same `where` and `orWhere` methods as the select builder. + +## Delete + +The `delete(from:)` method creates a `DELETE` query builder. + +```swift +try db.delete(from: "planets") + .where("name", .equal, "Jupiter") + .run().wait() +``` + +This code generates the following SQL: + +```sql +DELETE FROM planets WHERE name = ? +``` + +The delete builder supports the same `where` and `orWhere` methods as the select builder. + +## Raw + +The `raw(_:)` method allows for passing custom SQL query strings with support for parameterized binds. + +```swift +let table = "planets" +let planets = try db.raw("SELECT * FROM \(table) WHERE name = \(bind: planet)") + .all().wait() +``` + +This code generates the following SQL: + +```sql +SELECT * FROM planets WHERE name = ? +``` + +The `\(bind:)` interpolation should be used for any user input to avoid SQL injection. diff --git a/Sources/SQLKit/Builders/SQLAlterTableBuilder.swift b/Sources/SQLKit/Builders/SQLAlterTableBuilder.swift index 064d4e63..5132b093 100644 --- a/Sources/SQLKit/Builders/SQLAlterTableBuilder.swift +++ b/Sources/SQLKit/Builders/SQLAlterTableBuilder.swift @@ -2,25 +2,18 @@ public final class SQLAlterTableBuilder: SQLQueryBuilder { /// `SQLAlterTable` query being built. public var alterTable: SQLAlterTable - /// See `SQLQueryBuilder`. public var database: SQLDatabase - /// See `SQLQueryBuilder`. public var query: SQLExpression { return self.alterTable } - - /// See `SQLColumnBuilder`. + public var columns: [SQLExpression] { get { return alterTable.addColumns } set { alterTable.addColumns = newValue } } /// Creates a new `SQLAlterTableBuilder`. - /// - /// - parameters: - /// - alterTable: Alter table query. - /// - connection: Connection to perform query on. public init(_ alterTable: SQLAlterTable, on database: SQLDatabase) { self.alterTable = alterTable self.database = database @@ -172,7 +165,7 @@ public final class SQLAlterTableBuilder: SQLQueryBuilder { extension SQLDatabase { /// Creates a new `SQLAlterTableBuilder`. /// - /// conn.alter(table: "planets")... + /// db.alter(table: "planets")... /// /// - parameters: /// - table: Table to alter. diff --git a/Sources/SQLKit/Builders/SQLCreateEnumBuilder.swift b/Sources/SQLKit/Builders/SQLCreateEnumBuilder.swift index 4e01c9fc..43ee7fe5 100644 --- a/Sources/SQLKit/Builders/SQLCreateEnumBuilder.swift +++ b/Sources/SQLKit/Builders/SQLCreateEnumBuilder.swift @@ -3,7 +3,7 @@ extension SQLDatabase { /// Creates a new `SQLCreateEnumBuilder`. /// - /// conn.create(enum: "meal", cases: "breakfast", "lunch", "dinner")... + /// db.create(enum: "meal", cases: "breakfast", "lunch", "dinner")... /// /// - parameters: /// - name: Name of ENUM type to create. @@ -15,7 +15,7 @@ extension SQLDatabase { /// Creates a new `SQLCreateEnumBuilder`. /// - /// conn.create(enum: SQLIdentifier("meal"), cases: "breakfast", "lunch", "dinner")... + /// db.create(enum: SQLIdentifier("meal"), cases: "breakfast", "lunch", "dinner")... /// /// - parameters: /// - name: Name of ENUM type to create. @@ -28,7 +28,7 @@ extension SQLDatabase { /// Builds `SQLCreateEnum` queries. /// -/// conn.create(enum: "meal", cases: "breakfast", "lunch", "dinner") +/// db.create(enum: "meal", cases: "breakfast", "lunch", "dinner") /// .run() /// /// See `SQLColumnBuilder` and `SQLQueryBuilder` for more information. diff --git a/Sources/SQLKit/Builders/SQLCreateIndexBuilder.swift b/Sources/SQLKit/Builders/SQLCreateIndexBuilder.swift index 1f7803dd..94716750 100644 --- a/Sources/SQLKit/Builders/SQLCreateIndexBuilder.swift +++ b/Sources/SQLKit/Builders/SQLCreateIndexBuilder.swift @@ -1,6 +1,6 @@ /// Builds `SQLCreateIndex` queries. /// -/// conn.create(index: "planet_name_unique").on("planet").column("name").unique().run() +/// db.create(index: "planet_name_unique").on("planet").column("name").unique().run() /// /// See `SQLCreateIndex`. public final class SQLCreateIndexBuilder: SQLQueryBuilder { @@ -23,7 +23,7 @@ public final class SQLCreateIndexBuilder: SQLQueryBuilder { /// Creates a new `SQLCreateIndexBuilder`. /// - /// conn.create(index: "foo").on("planets")... + /// db.create(index: "foo").on("planets")... /// /// - parameters: /// - table: Table to create index on. @@ -34,7 +34,7 @@ public final class SQLCreateIndexBuilder: SQLQueryBuilder { /// Creates a new `SQLCreateIndexBuilder`. /// - /// conn.create(index: "foo").on("planets")... + /// db.create(index: "foo").on("planets")... /// /// - parameters: /// - table: Table to create index on. @@ -46,7 +46,7 @@ public final class SQLCreateIndexBuilder: SQLQueryBuilder { /// Creates a new `SQLCreateIndexBuilder`. /// - /// conn.create(index: "foo").column("name")... + /// db.create(index: "foo").column("name")... /// /// - parameters: /// - column: Column to create index on. @@ -57,7 +57,7 @@ public final class SQLCreateIndexBuilder: SQLQueryBuilder { /// Creates a new `SQLCreateIndexBuilder`. /// - /// conn.create(index: "foo").column("name")... + /// db.create(index: "foo").column("name")... /// /// - parameters: /// - column: Column to create index on. @@ -79,7 +79,7 @@ public final class SQLCreateIndexBuilder: SQLQueryBuilder { extension SQLDatabase { /// Creates a new `SQLCreateIndexBuilder`. /// - /// conn.create(index: "foo")... + /// db.create(index: "foo")... /// /// - parameters: /// - name: Name for this index. @@ -92,7 +92,7 @@ extension SQLDatabase { /// Creates a new `SQLCreateIndexBuilder`. /// - /// conn.create(index: "foo")... + /// db.create(index: "foo")... /// /// - parameters: /// - name: Name for this index. diff --git a/Sources/SQLKit/Builders/SQLCreateTableBuilder.swift b/Sources/SQLKit/Builders/SQLCreateTableBuilder.swift index c27316f5..09ab3d16 100644 --- a/Sources/SQLKit/Builders/SQLCreateTableBuilder.swift +++ b/Sources/SQLKit/Builders/SQLCreateTableBuilder.swift @@ -1,6 +1,6 @@ /// Builds `SQLCreateTable` queries. /// -/// conn.create(table: Planet.self).ifNotExists() +/// db.create(table: Planet.self).ifNotExists() /// .column(for: \Planet.id, .primaryKey) /// .column(for: \Planet.galaxyID, .references(\Galaxy.id)) /// .run() @@ -10,15 +10,12 @@ public final class SQLCreateTableBuilder: SQLQueryBuilder { /// `CreateTable` query being built. public var createTable: SQLCreateTable - /// See `SQLQueryBuilder`. public var database: SQLDatabase - - /// See `SQLQueryBuilder`. + public var query: SQLExpression { return self.createTable } - - /// See `SQLColumnBuilder`. + public var columns: [SQLExpression] { get { return createTable.columns } set { createTable.columns = newValue } @@ -125,8 +122,8 @@ extension SQLCreateTableBuilder { /// - constraintName: An optional name to give the constraint. public func primaryKey(_ columns: [String], named constraintName: String? = nil) -> Self { return primaryKey( - columns.map(SQLIdentifier.init), - named: constraintName.map(SQLIdentifier.init) + columns.map(SQLIdentifier.init(_:)), + named: constraintName.map(SQLIdentifier.init(_:)) ) } @@ -161,8 +158,8 @@ extension SQLCreateTableBuilder { /// - constraintName: An optional name to give the constraint. public func unique(_ columns: [String], named constraintName: String? = nil) -> Self { return unique( - columns.map(SQLIdentifier.init), - named: constraintName.map(SQLIdentifier.init) + columns.map(SQLIdentifier.init(_:)), + named: constraintName.map(SQLIdentifier.init(_:)) ) } @@ -189,7 +186,7 @@ extension SQLCreateTableBuilder { public func check(_ expression: SQLExpression, named constraintName: String? = nil) -> Self { return self.check( expression, - named: constraintName.map(SQLIdentifier.init) + named: constraintName.map(SQLIdentifier.init(_:)) ) } @@ -226,12 +223,12 @@ extension SQLCreateTableBuilder { named constraintName: String? = nil ) -> Self { return self.foreignKey( - columns.map(SQLIdentifier.init), + columns.map(SQLIdentifier.init(_:)), references: SQLIdentifier(foreignTable), - foreignColumns.map(SQLIdentifier.init), + foreignColumns.map(SQLIdentifier.init(_:)), onDelete: onDelete, onUpdate: onUpdate, - named: constraintName.map(SQLIdentifier.init) + named: constraintName.map(SQLIdentifier.init(_:)) ) } @@ -275,7 +272,7 @@ extension SQLCreateTableBuilder { extension SQLDatabase { /// Creates a new `SQLCreateTableBuilder`. /// - /// conn.create(table: "planets")... + /// db.create(table: "planets")... /// /// - parameters: /// - table: Table to create. @@ -286,7 +283,7 @@ extension SQLDatabase { /// Creates a new `SQLCreateTableBuilder`. /// - /// conn.create(table: SQLIdentifier("planets"))... + /// db.create(table: SQLIdentifier("planets"))... /// /// - parameters: /// - table: Table to create. diff --git a/Sources/SQLKit/Builders/SQLDeleteBuilder.swift b/Sources/SQLKit/Builders/SQLDeleteBuilder.swift index 56246d55..17125c2b 100644 --- a/Sources/SQLKit/Builders/SQLDeleteBuilder.swift +++ b/Sources/SQLKit/Builders/SQLDeleteBuilder.swift @@ -1,22 +1,19 @@ /// Builds `SQLDelete` queries. /// -/// conn.delete(from: Planet.self) +/// db.delete(from: Planet.self) /// .where(\.name != "Earth").run() /// /// See `SQLQueryBuilder` and `SQLPredicateBuilder` for more information. public final class SQLDeleteBuilder: SQLQueryBuilder, SQLPredicateBuilder { /// `Delete` query being built. public var delete: SQLDelete - - /// See `SQLQueryBuilder`. + public var database: SQLDatabase - - /// See `SQLQueryBuilder`. + public var query: SQLExpression { return self.delete } - - /// See `SQLWhereBuilder`. + public var predicate: SQLExpression? { get { return self.delete.predicate } set { self.delete.predicate = newValue } @@ -34,7 +31,7 @@ public final class SQLDeleteBuilder: SQLQueryBuilder, SQLPredicateBuilder { extension SQLDatabase { /// Creates a new `SQLDeleteBuilder`. /// - /// conn.delete(from: Planet.self)... + /// db.delete(from: "planets")... /// /// - parameters: /// - table: Table to delete from. @@ -45,8 +42,6 @@ extension SQLDatabase { /// Creates a new `SQLDeleteBuilder`. /// - /// conn.delete(from: "planets")... - /// /// - parameters: /// - table: Table to delete from. /// - returns: Newly created `SQLDeleteBuilder`. diff --git a/Sources/SQLKit/Builders/SQLDropEnumBuilder.swift b/Sources/SQLKit/Builders/SQLDropEnumBuilder.swift index 00dbf260..e310cf8a 100644 --- a/Sources/SQLKit/Builders/SQLDropEnumBuilder.swift +++ b/Sources/SQLKit/Builders/SQLDropEnumBuilder.swift @@ -24,17 +24,15 @@ extension SQLDatabase { /// Builds `SQLDropEnumBuilder` queries. /// -/// conn.drop(type: "meal").run() +/// db.drop(type: "meal").run() /// /// See `SQLQueryBuilder` for more information. public final class SQLDropEnumBuilder: SQLQueryBuilder { /// `DropType` query being built. public var dropEnum: SQLDropEnum - /// See `SQLQueryBuilder`. public var database: SQLDatabase - /// See `SQLQueryBuilder`. public var query: SQLExpression { return self.dropEnum } diff --git a/Sources/SQLKit/Builders/SQLDropTableBuilder.swift b/Sources/SQLKit/Builders/SQLDropTableBuilder.swift index 716e8756..30d2d967 100644 --- a/Sources/SQLKit/Builders/SQLDropTableBuilder.swift +++ b/Sources/SQLKit/Builders/SQLDropTableBuilder.swift @@ -1,6 +1,6 @@ /// Builds `SQLDropTable` queries. /// -/// conn.drop(table: Planet.self).run() +/// db.drop(table: Planet.self).run() /// /// See `SQLQueryBuilder` for more information. public final class SQLDropTableBuilder: SQLQueryBuilder { @@ -56,7 +56,7 @@ public final class SQLDropTableBuilder: SQLQueryBuilder { extension SQLDatabase { /// Creates a new `SQLDropTable` builder. /// - /// conn.drop(table: "planets").run() + /// db.drop(table: "planets").run() /// public func drop(table: String) -> SQLDropTableBuilder { return self.drop(table: SQLIdentifier(table)) @@ -64,7 +64,7 @@ extension SQLDatabase { /// Creates a new `SQLDropTable` builder. /// - /// conn.drop(table: "planets").run() + /// db.drop(table: "planets").run() /// public func drop(table: SQLExpression) -> SQLDropTableBuilder { return .init(.init(table: table), on: self) diff --git a/Sources/SQLKit/Builders/SQLDropTriggerBuilder.swift b/Sources/SQLKit/Builders/SQLDropTriggerBuilder.swift index 6e450a81..2314cb16 100644 --- a/Sources/SQLKit/Builders/SQLDropTriggerBuilder.swift +++ b/Sources/SQLKit/Builders/SQLDropTriggerBuilder.swift @@ -1,6 +1,6 @@ /// Builds `SQLDropTrigger` query /// -/// conn.drop() +/// db.drop() /// /// See `SQLQueryBuilder` for more information. extension SQLDatabase { diff --git a/Sources/SQLKit/Builders/SQLInsertBuilder.swift b/Sources/SQLKit/Builders/SQLInsertBuilder.swift index cbff26e1..9136b955 100644 --- a/Sources/SQLKit/Builders/SQLInsertBuilder.swift +++ b/Sources/SQLKit/Builders/SQLInsertBuilder.swift @@ -1,6 +1,6 @@ /// Builds `SQLInsert` queries. /// -/// conn.insert(into: "planets"") +/// db.insert(into: "planets"") /// .value(earth).run() /// /// See `SQLQueryBuilder` for more information. @@ -25,35 +25,45 @@ public final class SQLInsertBuilder: SQLQueryBuilder { /// Adds a single encodable value to be inserted. Equivalent to calling `values(_:)` /// with single-element array. /// - /// conn.insert(into: Planet.self) + /// db.insert(into: Planet.self) /// .value(earth).run() /// /// - parameters: /// - value: `Encodable` value to insert. /// - returns: Self for chaining. - public func model(_ model: E) throws -> Self - where E: Encodable - { - let row = try SQLQueryEncoder().encode(model) - if self.insert.columns.isEmpty { - self.insert.columns += row.map { $0.0 }.map { SQLColumn($0, table: nil) } - } else { - assert( - self.insert.columns.count == row.count, - "Column count (\(self.insert.columns.count)) did not equal value count (\(row.count)): \(model)." - ) + public func model(_ model: E, prefix: String? = nil, keyEncodingStrategy: SQLQueryEncoder.KeyEncodingStrategy = .useDefaultKeys) throws -> Self + where E: Encodable { + return try models([model], prefix: prefix, keyEncodingStrategy: keyEncodingStrategy) + } + + public func models(_ models: [E], prefix: String? = nil, keyEncodingStrategy: SQLQueryEncoder.KeyEncodingStrategy = .useDefaultKeys) throws -> Self where E: Encodable { + var encoder = SQLQueryEncoder() + encoder.keyEncodingStrategy = keyEncodingStrategy + encoder.prefix = prefix + + for model in models { + let row = try encoder.encode(model) + if self.insert.columns.isEmpty { + self.insert.columns += row.map { $0.0 }.map { SQLColumn($0, table: nil) } + } else { + assert( + self.insert.columns.count == row.count, + "Column count (\(self.insert.columns.count)) did not equal value count (\(row.count)): \(model)." + ) + } + self.insert.values.append(.init(row.map { $0.1 })) } - self.insert.values.append(.init(row.map { $0.1 })) + return self } public func columns(_ columns: String...) -> Self { - self.insert.columns = columns.map(SQLIdentifier.init) + self.insert.columns = columns.map(SQLIdentifier.init(_:)) return self } public func columns(_ columns: [String]) -> Self { - self.insert.columns = columns.map(SQLIdentifier.init) + self.insert.columns = columns.map(SQLIdentifier.init(_:)) return self } @@ -95,7 +105,7 @@ public final class SQLInsertBuilder: SQLQueryBuilder { extension SQLDatabase { /// Creates a new `SQLInsertBuilder`. /// - /// conn.insert(into: "planets")... + /// db.insert(into: "planets")... /// /// - parameters: /// - table: Table to insert into. @@ -106,7 +116,7 @@ extension SQLDatabase { /// Creates a new `SQLInsertBuilder`. /// - /// conn.insert(into: "planets")... + /// db.insert(into: "planets")... /// /// - parameters: /// - table: Table to insert into. diff --git a/Sources/SQLKit/Builders/SQLJoinBuilder.swift b/Sources/SQLKit/Builders/SQLJoinBuilder.swift new file mode 100644 index 00000000..20e26917 --- /dev/null +++ b/Sources/SQLKit/Builders/SQLJoinBuilder.swift @@ -0,0 +1,82 @@ +public protocol SQLJoinBuilder: class { + var joins: [SQLExpression] { get set } +} + +/// Joins +extension SQLJoinBuilder { + /// Include the given table in the list of those used by the query, + /// performing an explicit join using the given method and condition(s). + /// Tables are joined left to right, in the same order as invocations of + /// `from()` and `join()`. The table specifier is a string assumed to be a + /// valid SQL identifier. The condition is a strings assumed to be valid + /// (semi-))arbitrary SQL. The join method is any `SQLJoinMethod`. + /// + /// - Parameters: + /// - table: The name of the table to join. + /// - method: The join method to use. + /// - expression: A string containing a join condition. + public func join(_ table: String, method: SQLJoinMethod = .inner, on expression: String) -> Self { + self.join(SQLIdentifier(table), method: method, on: SQLRaw(expression)) + } + + /// Include the given table in the list of those used by the query, + /// performing an explicit join using the given method and condition(s). + /// Tables are joined left to right, in the same order as invocations of + /// `from()` and `join()`. The table specifier, condition, and join method + /// may be arbitrary expressions. + /// + /// - Parameters: + /// - table: An expression identifying the table to join. + /// - method: An expression providing the join method to use. + /// - expression: An expression used as the join condition. + public func join(_ table: SQLExpression, method: SQLExpression = SQLJoinMethod.inner, on expression: SQLExpression) -> Self { + self.joins.append(SQLJoin(method: method, table: table, expression: expression)) + return self + } + + /// Include the given table in the list of those used by the query, + /// performing an explicit join using the given method and condition(s). + /// Tables are joined left to right, in the same order as invocations of + /// `from()` and `join()`. The table specifier and join method may be + /// arbitrary expressions. The condition is a triplet of inputs representing + /// a binary expression. + /// + /// - Parameters: + /// - table: An expression identifying the table to join. + /// - method: An expression providing the join method to use. + /// - left: The left side of a binary expression used as a join condition. + /// - op: The operator in a binary expression used as a join condition. + /// - right: The right side of a binary expression used as a join condition. + public func join( + _ table: SQLExpression, + method: SQLExpression = SQLJoinMethod.inner, + on left: SQLExpression, + _ op: SQLBinaryOperator, + _ right: SQLExpression + ) -> Self { + self.join(table, method: method, on: SQLBinaryExpression(left: left, op: op, right: right)) + } + + /// Include the given table in the list of those used by the query, + /// performing an explicit join using the given method and a list of column + /// names to be used as shorthand join conditions. Tables are joined left to + /// right, in the same order as invocations of `from()` and `join()`. The + /// table specifier, column list, and join method may be arbitrary + /// expressions. + /// + /// - Parameters: + /// - table: An expression identifying the table to join. + /// - method: An expression providing the join method to use. + /// - column: An expression giving a list of columns to match between + /// the joined tables. + public func join(_ table: SQLExpression, method: SQLExpression = SQLJoinMethod.inner, using columns: SQLExpression) -> Self { + // TODO TODO TODO: Figure out a nice way to make `SQLJoin` aware of the + // `USING()` syntax; this method is hacky and doesn't respect + // differences between database drivers. + self.joins.append(SQLList([ + method, SQLRaw("JOIN"), table, + SQLRaw("USING ("), columns, SQLRaw(")") + ], separator: SQLRaw(" "))) + return self + } +} diff --git a/Sources/SQLKit/Builders/SQLPredicateBuilder.swift b/Sources/SQLKit/Builders/SQLPredicateBuilder.swift index 0b78e7d8..51e3e918 100644 --- a/Sources/SQLKit/Builders/SQLPredicateBuilder.swift +++ b/Sources/SQLKit/Builders/SQLPredicateBuilder.swift @@ -25,8 +25,8 @@ extension SQLPredicateBuilder { /// - lhs: Left-hand side column name. /// - op: Binary operator to use for comparison. /// - rhs: Right-hand side column name. - public func `where`(_ lhs: String, _ op: SQLBinaryOperator, column rhs: String) -> Self { - return self.where(SQLIdentifier(lhs), op, SQLIdentifier(rhs)) + public func `where`(_ lhs: SQLIdentifier, _ op: SQLBinaryOperator, column rhs: SQLIdentifier) -> Self { + self.where(lhs, op, rhs) } /// Adds a column to encodable comparison to this builder's `WHERE` clause by `AND`ing. @@ -42,39 +42,59 @@ extension SQLPredicateBuilder { /// - op: Binary operator to use for comparison. /// - rhs: Encodable value. /// - returns: Self for chaining. - public func `where`(_ lhs: String, _ op: SQLBinaryOperator, _ rhs: Encodable) -> Self { - return self.where(SQLIdentifier(lhs), op, SQLBind(rhs)) + public func `where`(_ lhs: SQLIdentifier, _ op: SQLBinaryOperator, _ rhs: E) -> Self + where E: Encodable + { + return self.where(lhs, op, SQLBind(rhs)) + } + + /// Adds a column to encodable comparison to this builder's `WHERE` clause by `AND`ing. + /// + /// builder.where("name", .in, ["Earth", "Mars"]) + /// + /// The encodable value supplied will be bound to the query as a parameter. + /// + /// SELECT * FROM planets WHERE name = ? // Earth + /// + /// - parameters: + /// - lhs: Left-hand side column name. + /// - op: Binary operator to use for comparison. + /// - rhs: Encodable value. + /// - returns: Self for chaining. + public func `where`(_ lhs: SQLIdentifier, _ op: SQLBinaryOperator, _ rhs: [E]) -> Self + where E: Encodable + { + self.where(lhs, op, SQLBind.group(rhs)) } /// Adds a column to expression comparison to the `WHERE` clause by `AND`ing. /// - /// builder.where("name", .equal, .value("Earth")) + /// builder.where(SQLIdentifier("name"), .equal, SQLBind("Earth")) /// /// - parameters: /// - lhs: Left-hand side column name. /// - op: Binary operator to use for comparison. /// - rhs: Right-hand side expression. - public func `where`(_ lhs: String, _ op: SQLBinaryOperator, _ rhs: SQLExpression) -> Self { - return self.where(SQLIdentifier(lhs), op, rhs) + public func `where`(_ lhs: SQLIdentifier, _ op: SQLBinaryOperator, _ rhs: SQLExpression) -> Self { + self.where(lhs, op as SQLExpression, rhs) } - /// Adds an expression to expression comparison to the `WHERE` clause by `AND`ing. + /// Adds a column to expression comparison to the `WHERE` clause by `AND`ing. /// - /// builder.where("name", .equal, .value("Earth")) + /// builder.where(SQLIdentifier("name"), .equal, SQLBind("Earth")) /// /// - parameters: - /// - lhs: Left-hand side expression. + /// - lhs: Left-hand side column name. /// - op: Binary operator to use for comparison. /// - rhs: Right-hand side expression. - /// - returns: Self for chaining. public func `where`(_ lhs: SQLExpression, _ op: SQLBinaryOperator, _ rhs: SQLExpression) -> Self { - return self.where(SQLBinaryExpression(left: lhs, op: op, right: rhs)) + self.where(lhs, op as SQLExpression, rhs) } /// Adds an expression to expression comparison, with an arbitrary /// expression as operator, to the `WHERE` clause by `AND`ing. /// - /// builder.where("name", .equal, .value("Earth")) + /// builder.where(SQLIdentifier("name"), SQLBinaryOperator.equal, SQLBind("Earth")) /// /// - parameters: /// - lhs: Left-hand side expression. @@ -82,7 +102,7 @@ extension SQLPredicateBuilder { /// - rhs: Right-hand side expression. /// - returns: Self for chaining. public func `where`(_ lhs: SQLExpression, _ op: SQLExpression, _ rhs: SQLExpression) -> Self { - return self.where(SQLBinaryExpression(left: lhs, op: op, right: rhs)) + self.where(SQLBinaryExpression(left: lhs, op: op, right: rhs)) } /// Adds an expression to the `WHERE` clause by `AND`ing. diff --git a/Sources/SQLKit/Builders/SQLQueryFetcher.swift b/Sources/SQLKit/Builders/SQLQueryFetcher.swift index 9674d845..b7117da4 100644 --- a/Sources/SQLKit/Builders/SQLQueryFetcher.swift +++ b/Sources/SQLKit/Builders/SQLQueryFetcher.swift @@ -9,7 +9,6 @@ public protocol SQLQueryFetcher: SQLQueryBuilder { } extension SQLQueryFetcher { // MARK: First - public func first(decoding: D.Type) -> EventLoopFuture where D: Decodable { diff --git a/Sources/SQLKit/Builders/SQLRawBuilder.swift b/Sources/SQLKit/Builders/SQLRawBuilder.swift index f0d61851..d5e7d9a0 100644 --- a/Sources/SQLKit/Builders/SQLRawBuilder.swift +++ b/Sources/SQLKit/Builders/SQLRawBuilder.swift @@ -1,6 +1,6 @@ /// Builds raw SQL queries. /// -/// conn.raw("SELECT * FROM planets WHERE name = \(bind: "Earth")") +/// db.raw("SELECT * FROM planets WHERE name = \(bind: "Earth")") /// .all(decoding: Planet.self) /// public final class SQLRawBuilder: SQLQueryBuilder, SQLQueryFetcher { @@ -27,7 +27,7 @@ public final class SQLRawBuilder: SQLQueryBuilder, SQLQueryFetcher { extension SQLDatabase { /// Creates a new `SQLRawBuilder`. /// - /// conn.raw("SELECT * FROM ...")... + /// db.raw("SELECT * FROM ...")... /// /// - parameters: /// - sql: The SQL query string. diff --git a/Sources/SQLKit/Builders/SQLSelectBuilder.swift b/Sources/SQLKit/Builders/SQLSelectBuilder.swift index be0ca269..6a36e709 100644 --- a/Sources/SQLKit/Builders/SQLSelectBuilder.swift +++ b/Sources/SQLKit/Builders/SQLSelectBuilder.swift @@ -1,142 +1,52 @@ -public final class SQLSelectBuilder: SQLQueryFetcher, SQLQueryBuilder, SQLPredicateBuilder { +extension SQLDatabase { + /// Creates a new `SQLSelectBuilder`. + /// + /// db.select() + /// .column("*") + /// .from("planets"") + /// .where("name", .equal, SQLBind("Earth")) + /// .all() + /// + public func select() -> SQLSelectBuilder { + return .init(on: self) + } +} + + +public final class SQLSelectBuilder: SQLQueryFetcher, SQLQueryBuilder { public var query: SQLExpression { return self.select } - public var predicate: SQLExpression? { - get { return self.select.predicate } - set { self.select.predicate = newValue } - } - public var select: SQLSelect public var database: SQLDatabase - public init(on database: SQLDatabase) { self.select = .init() self.database = database } - - public func limit(_ limit: Int) -> Self { - self.select.limit = limit - return self - } - - public func offset(_ offset: Int) -> Self { - self.select.offset = offset - return self - } - - /// Adds a `GROUP BY` clause to the select statement. - /// - /// - parameters: - /// - expression: `SQLExpression` to group by. - /// - returns: Self for chaining. - public func groupBy(_ column: String) -> Self { - return self.groupBy(SQLColumn(column)) - } - - /// Adds a `GROUP BY` clause to the select statement. - /// - /// - parameters: - /// - expression: `SQLExpression` to group by. - /// - returns: Self for chaining. - public func groupBy(_ expression: SQLExpression) -> Self { - self.select.groupBy.append(expression) - return self - } - - /// Adds an `ORDER BY` clause to the select statement. - /// - /// - parameters: - /// - expression: `SQLExpression` to order by. - /// - returns: Self for chaining. - public func orderBy(_ column: String, _ direction: SQLDirection = .ascending) -> Self { - return self.orderBy(SQLColumn(column), direction) - } - - - /// Adds an `ORDER BY` clause to the select statement. - /// - /// - parameters: - /// - expression: `SQLExpression` to order by. - /// - returns: Self for chaining. - public func orderBy(_ expression: SQLExpression, _ direction: SQLExpression) -> Self { - return self.orderBy(SQLOrderBy(expression: expression, direction: direction)) - } - - /// Adds an `ORDER BY` clause to the select statement. - /// - /// - parameters: - /// - expression: `SQLExpression` to order by. - /// - returns: Self for chaining. - public func orderBy(_ expression: SQLExpression) -> Self { - select.orderBy.append(expression) - return self - } - - /// Adds a locking expression to this `SELECT` statement. - /// - /// db.select()...for(.update) - /// - /// Also called locking reads, the `SELECT ... FOR UPDATE` syntax - /// will lock all selected rows for the duration of the current transaction. - /// How the rows are locked depends on the specific expression supplied. - /// - /// - parameters: - /// - lockingClause: Locking clause type. - /// - returns: Self for chaining. - public func `for`(_ lockingClause: SQLLockingClause) -> Self { - return self.lockingClause(lockingClause) - } - - /// Adds a locking expression to this `SELECT` statement. - /// - /// db.select()...lockingClause(...) - /// - /// Also called locking reads, the `SELECT ... FOR UPDATE` syntax - /// will lock all selected rows for the duration of the current transaction. - /// How the rows are locked depends on the specific expression supplied. - /// - /// - note: This method allows for any `SQLExpression` conforming - /// type to be passed as the locking clause. - /// - /// - parameters: - /// - lockingClause: Locking clause type. - /// - returns: Self for chaining. - public func lockingClause(_ lockingClause: SQLExpression) -> Self { - self.select.lockingClause = lockingClause - return self - } - - /// Adds a `LIMIT` clause to the select statement. - /// - /// builder.limit(5) - /// - /// - parameters: - /// - max: Optional maximum limit. - /// If `nil`, existing limit will be removed. - /// - returns: Self for chaining. - public func limit(_ max: Int?) -> Self { - self.select.limit = max - return self +} + +// MARK: Joins + +extension SQLSelectBuilder: SQLJoinBuilder { + public var joins: [SQLExpression] { + get { self.select.joins } + set { self.select.joins = newValue } } - - /// Adds a `OFFSET` clause to the select statement. - /// - /// builder.offset(5) - /// - /// - parameters: - /// - max: Optional offset. - /// If `nil`, existing offset will be removed. - /// - returns: Self for chaining. - public func offset(_ n: Int?) -> Self { - self.select.offset = n - return self +} + +// MARK: Predicate + +extension SQLSelectBuilder: SQLPredicateBuilder { + public var predicate: SQLExpression? { + get { return self.select.predicate } + set { self.select.predicate = newValue } } } -/// DISINCT +// MARK: Distinct + extension SQLSelectBuilder { /// Adds a DISTINCT clause to the select statement. /// @@ -172,9 +82,9 @@ extension SQLSelectBuilder { } } -/// Column list +// MARK: Columns + extension SQLSelectBuilder { - /// Specify a column to be part of the result set of the query. The column /// is a string assumed to be a valid SQL identifier and is not qualified. /// The string "*" (a single asterisk) is recognized and replaced by @@ -249,9 +159,9 @@ extension SQLSelectBuilder { } -/// FROM -extension SQLSelectBuilder { +// MARK: From +extension SQLSelectBuilder { /// Include the given table in the list of those used by the query, without /// performing an explicit join. The table specifier is a string assumed to /// be a valid SQL identifier. @@ -296,88 +206,8 @@ extension SQLSelectBuilder { } -/// Joins -extension SQLSelectBuilder { - - /// Include the given table in the list of those used by the query, - /// performing an explicit join using the given method and condition(s). - /// Tables are joined left to right, in the same order as invocations of - /// `from()` and `join()`. The table specifier is a string assumed to be a - /// valid SQL identifier. The condition is a strings assumed to be valid - /// (semi-))arbitrary SQL. The join method is any `SQLJoinMethod`. - /// - /// - Parameters: - /// - table: The name of the table to join. - /// - method: The join method to use. - /// - expression: A string containing a join condition. - public func join(_ table: String, method: SQLJoinMethod = .inner, on expression: String) -> Self { - return self.join(SQLIdentifier(table), method: method, on: SQLRaw(expression)) - } - - /// Include the given table in the list of those used by the query, - /// performing an explicit join using the given method and condition(s). - /// Tables are joined left to right, in the same order as invocations of - /// `from()` and `join()`. The table specifier, condition, and join method - /// may be arbitrary expressions. - /// - /// - Parameters: - /// - table: An expression identifying the table to join. - /// - method: An expression providing the join method to use. - /// - expression: An expression used as the join condition. - public func join(_ table: SQLExpression, method: SQLExpression = SQLJoinMethod.inner, on expression: SQLExpression) -> Self { - self.select.joins.append(SQLJoin(method: method, table: table, expression: expression)) - return self - } - - /// Include the given table in the list of those used by the query, - /// performing an explicit join using the given method and condition(s). - /// Tables are joined left to right, in the same order as invocations of - /// `from()` and `join()`. The table specifier and join method may be - /// arbitrary expressions. The condition is a triplet of inputs representing - /// a binary expression. - /// - /// - Parameters: - /// - table: An expression identifying the table to join. - /// - method: An expression providing the join method to use. - /// - left: The left side of a binary expression used as a join condition. - /// - op: The operator in a binary expression used as a join condition. - /// - right: The right side of a binary expression used as a join condition. - public func join( - _ table: SQLExpression, - method: SQLExpression = SQLJoinMethod.inner, - on left: SQLExpression, - _ op: SQLBinaryOperator, - _ right: SQLExpression - ) -> Self { - return self.join(table, method: method, on: SQLBinaryExpression(left: left, op: op, right: right)) - } - - /// Include the given table in the list of those used by the query, - /// performing an explicit join using the given method and a list of column - /// names to be used as shorthand join conditions. Tables are joined left to - /// right, in the same order as invocations of `from()` and `join()`. The - /// table specifier, column list, and join method may be arbitrary - /// expressions. - /// - /// - Parameters: - /// - table: An expression identifying the table to join. - /// - method: An expression providing the join method to use. - /// - column: An expression giving a list of columns to match between - /// the joined tables. - public func join(_ table: SQLExpression, method: SQLExpression = SQLJoinMethod.inner, using columns: SQLExpression) -> Self { - // TODO TODO TODO: Figure out a nice way to make `SQLJoin` aware of the - // `USING()` syntax; this method is hacky and doesn't respect - // differences between database drivers. - self.select.joins.append(SQLList([ - method, SQLRaw("JOIN"), table, - SQLRaw("USING ("), columns, SQLRaw(")") - ], separator: SQLRaw(" "))) - return self - } - -} +// MARK: Having -/// HAVING extension SQLSelectBuilder { /// Adds a column to column comparison to this builder's `HAVING` clause by `AND`ing. /// @@ -532,18 +362,123 @@ extension SQLSelectBuilder { } } -// MARK: Connection +// MARK: Limit / Offset -extension SQLDatabase { - /// Creates a new `SQLSelectBuilder`. +extension SQLSelectBuilder { + /// Adds a `LIMIT` clause to the select statement. /// - /// conn.select() - /// .column("*") - /// .from("planets"") - /// .where("name", .equal, SQLBind("Earth")) - /// .all() + /// builder.limit(5) /// - public func select() -> SQLSelectBuilder { - return .init(on: self) + /// - parameters: + /// - max: Optional maximum limit. + /// If `nil`, existing limit will be removed. + /// - returns: Self for chaining. + public func limit(_ max: Int?) -> Self { + self.select.limit = max + return self + } + + /// Adds a `OFFSET` clause to the select statement. + /// + /// builder.offset(5) + /// + /// - parameters: + /// - max: Optional offset. + /// If `nil`, existing offset will be removed. + /// - returns: Self for chaining. + public func offset(_ n: Int?) -> Self { + self.select.offset = n + return self + } +} + +// MARK: Group By + +extension SQLSelectBuilder { + /// Adds a `GROUP BY` clause to the select statement. + /// + /// - parameters: + /// - expression: `SQLExpression` to group by. + /// - returns: Self for chaining. + public func groupBy(_ column: String) -> Self { + return self.groupBy(SQLColumn(column)) + } + + /// Adds a `GROUP BY` clause to the select statement. + /// + /// - parameters: + /// - expression: `SQLExpression` to group by. + /// - returns: Self for chaining. + public func groupBy(_ expression: SQLExpression) -> Self { + self.select.groupBy.append(expression) + return self + } + + /// Adds an `ORDER BY` clause to the select statement. + /// + /// - parameters: + /// - expression: `SQLExpression` to order by. + /// - returns: Self for chaining. + public func orderBy(_ column: String, _ direction: SQLDirection = .ascending) -> Self { + return self.orderBy(SQLColumn(column), direction) + } + + + /// Adds an `ORDER BY` clause to the select statement. + /// + /// - parameters: + /// - expression: `SQLExpression` to order by. + /// - returns: Self for chaining. + public func orderBy(_ expression: SQLExpression, _ direction: SQLExpression) -> Self { + return self.orderBy(SQLOrderBy(expression: expression, direction: direction)) + } + + /// Adds an `ORDER BY` clause to the select statement. + /// + /// - parameters: + /// - expression: `SQLExpression` to order by. + /// - returns: Self for chaining. + public func orderBy(_ expression: SQLExpression) -> Self { + select.orderBy.append(expression) + return self + } +} + + +// MARK: Locking + +extension SQLSelectBuilder { + /// Adds a locking expression to this `SELECT` statement. + /// + /// db.select()...for(.update) + /// + /// Also called locking reads, the `SELECT ... FOR UPDATE` syntax + /// will lock all selected rows for the duration of the current transaction. + /// How the rows are locked depends on the specific expression supplied. + /// + /// - parameters: + /// - lockingClause: Locking clause type. + /// - returns: Self for chaining. + public func `for`(_ lockingClause: SQLLockingClause) -> Self { + return self.lockingClause(lockingClause) + } + + /// Adds a locking expression to this `SELECT` statement. + /// + /// db.select()...lockingClause(...) + /// + /// Also called locking reads, the `SELECT ... FOR UPDATE` syntax + /// will lock all selected rows for the duration of the current transaction. + /// How the rows are locked depends on the specific expression supplied. + /// + /// - note: This method allows for any `SQLExpression` conforming + /// type to be passed as the locking clause. + /// + /// - parameters: + /// - lockingClause: Locking clause type. + /// - returns: Self for chaining. + public func lockingClause(_ lockingClause: SQLExpression) -> Self { + self.select.lockingClause = lockingClause + return self } } diff --git a/Sources/SQLKit/Builders/SQLUpdateBuilder.swift b/Sources/SQLKit/Builders/SQLUpdateBuilder.swift index 43aef3e3..b152b0d9 100644 --- a/Sources/SQLKit/Builders/SQLUpdateBuilder.swift +++ b/Sources/SQLKit/Builders/SQLUpdateBuilder.swift @@ -1,6 +1,6 @@ /// Builds `SQLUpdate` queries. /// -/// conn.update(Planet.self) +/// db.update(Planet.self) /// .set(\Planet.name == "Earth") /// .where(\Planet.name == "Eatrh") /// .run() @@ -10,15 +10,12 @@ public final class SQLUpdateBuilder: SQLQueryBuilder, SQLPredicateBuilder { /// `Update` query being built. public var update: SQLUpdate - /// See `SQLQueryBuilder`. public var database: SQLDatabase - - /// See `SQLQueryBuilder`. + public var query: SQLExpression { return self.update } - - /// See `SQLWhereBuilder`. + public var predicate: SQLExpression? { get { return self.update.predicate } set { self.update.predicate = newValue } @@ -56,7 +53,7 @@ public final class SQLUpdateBuilder: SQLQueryBuilder, SQLPredicateBuilder { extension SQLDatabase { /// Creates a new `SQLUpdateBuilder`. /// - /// conn.update("planets")... + /// db.update("planets")... /// /// - parameters: /// - table: Table to update. @@ -67,7 +64,7 @@ extension SQLDatabase { /// Creates a new `SQLUpdateBuilder`. /// - /// conn.update("planets")... + /// db.update("planets")... /// /// - parameters: /// - table: Table to update. diff --git a/Sources/SQLKit/Query/SQLAlterTable.swift b/Sources/SQLKit/Query/SQLAlterTable.swift index af3046aa..7764c28e 100644 --- a/Sources/SQLKit/Query/SQLAlterTable.swift +++ b/Sources/SQLKit/Query/SQLAlterTable.swift @@ -1,6 +1,6 @@ /// `ALTER TABLE` query. /// -/// conn.alter(table: Planet.self) +/// db.alter(table: Planet.self) /// .column(for: \.name) /// .run() /// diff --git a/Sources/SQLKit/Query/SQLBind.swift b/Sources/SQLKit/Query/SQLBind.swift index ec51bdb6..a0d1d153 100644 --- a/Sources/SQLKit/Query/SQLBind.swift +++ b/Sources/SQLKit/Query/SQLBind.swift @@ -10,3 +10,9 @@ public struct SQLBind: SQLExpression { serializer.write(bind: self.encodable) } } + +extension SQLBind { + public static func group(_ items: [Encodable]) -> SQLExpression { + SQLGroupExpression(items.map(SQLBind.init)) + } +} diff --git a/Sources/SQLKit/Query/SQLColumnIdentifier.swift b/Sources/SQLKit/Query/SQLColumnIdentifier.swift index 619fb220..f682c4ba 100644 --- a/Sources/SQLKit/Query/SQLColumnIdentifier.swift +++ b/Sources/SQLKit/Query/SQLColumnIdentifier.swift @@ -3,7 +3,7 @@ public struct SQLColumn: SQLExpression { public var table: SQLExpression? public init(_ name: String, table: String? = nil) { - self.init(SQLIdentifier(name), table: table.flatMap(SQLIdentifier.init)) + self.init(SQLIdentifier(name), table: table.flatMap(SQLIdentifier.init(_:))) } public init(_ name: SQLExpression, table: SQLExpression? = nil) { diff --git a/Sources/SQLKit/Query/SQLDistinct.swift b/Sources/SQLKit/Query/SQLDistinct.swift index dc1a0e69..321334e8 100644 --- a/Sources/SQLKit/Query/SQLDistinct.swift +++ b/Sources/SQLKit/Query/SQLDistinct.swift @@ -2,7 +2,7 @@ public struct SQLDistinct: SQLExpression { public let args: [SQLExpression] public init(_ args: String...) { - self.args = args.map(SQLIdentifier.init) + self.args = args.map(SQLIdentifier.init(_:)) } public init(_ args: SQLExpression...) { diff --git a/Sources/SQLKit/Query/SQLIdentifier.swift b/Sources/SQLKit/Query/SQLIdentifier.swift index f2002fab..edddea61 100644 --- a/Sources/SQLKit/Query/SQLIdentifier.swift +++ b/Sources/SQLKit/Query/SQLIdentifier.swift @@ -14,3 +14,9 @@ public struct SQLIdentifier: SQLExpression { serializer.dialect.identifierQuote.serialize(to: &serializer) } } + +extension SQLIdentifier: ExpressibleByStringLiteral { + public init(stringLiteral value: StringLiteralType) { + self.init(value) + } +} diff --git a/Sources/SQLKit/Query/SQLJoin.swift b/Sources/SQLKit/Query/SQLJoin.swift index a9925a74..3b0c7f5b 100644 --- a/Sources/SQLKit/Query/SQLJoin.swift +++ b/Sources/SQLKit/Query/SQLJoin.swift @@ -1,18 +1,18 @@ ///// `JOIN` clause. public struct SQLJoin: SQLExpression { public var method: SQLExpression - + public var table: SQLExpression public var expression: SQLExpression - + /// Creates a new `SQLJoin`. public init(method: SQLExpression, table: SQLExpression, expression: SQLExpression) { self.method = method self.table = table self.expression = expression } - + public func serialize(to serializer: inout SQLSerializer) { self.method.serialize(to: &serializer) serializer.write(" JOIN ") diff --git a/Sources/SQLKit/SQLDatabase.swift b/Sources/SQLKit/SQLDatabase.swift index 036734f3..d9fc0e26 100644 --- a/Sources/SQLKit/SQLDatabase.swift +++ b/Sources/SQLKit/SQLDatabase.swift @@ -17,7 +17,7 @@ extension SQLDatabase { } extension SQLDatabase { - public func with(_ logger: Logger) -> SQLDatabase { + public func logging(to logger: Logger) -> SQLDatabase { CustomLoggerSQLDatabase(database: self, logger: logger) } } diff --git a/Sources/SQLKit/SQLQueryEncoder.swift b/Sources/SQLKit/SQLQueryEncoder.swift index e05524ff..5bb2cc5a 100644 --- a/Sources/SQLKit/SQLQueryEncoder.swift +++ b/Sources/SQLKit/SQLQueryEncoder.swift @@ -1,16 +1,39 @@ public struct SQLQueryEncoder { + public var prefix: String? = nil + public var keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys + public init() { } public func encode(_ encodable: E) throws -> [(String, SQLExpression)] where E: Encodable { - let encoder = _Encoder() + let encoder = _Encoder(options: options) try encodable.encode(to: encoder) return encoder.row } + + public enum KeyEncodingStrategy { + /// A key encoding strategy that doesn't change key names during encoding. + case useDefaultKeys + /// A key encoding strategy that converts camel-case keys to snake-case keys. + case convertToSnakeCase + case custom(([CodingKey]) -> CodingKey) + } + + fileprivate struct _Options { + let prefix: String? + let keyEncodingStrategy: KeyEncodingStrategy + } + + /// The options set on the top-level decoder. + fileprivate var options: _Options { + return _Options(prefix: prefix, keyEncodingStrategy: keyEncodingStrategy) + } } private final class _Encoder: Encoder { + fileprivate let options: SQLQueryEncoder._Options + var codingPath: [CodingKey] { return [] } @@ -21,8 +44,9 @@ private final class _Encoder: Encoder { var row: [(String, SQLExpression)] - init() { + init(options: SQLQueryEncoder._Options) { self.row = [] + self.options = options } func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { @@ -40,15 +64,33 @@ private final class _Encoder: Encoder { self.encoder = encoder } + func column(for key: Key) -> String { + var encodedKey = key.stringValue + switch self.encoder.options.keyEncodingStrategy { + case .useDefaultKeys: + break + case .convertToSnakeCase: + encodedKey = _convertToSnakeCase(encodedKey) + case .custom(let customKeyEncodingFunc): + encodedKey = customKeyEncodingFunc([key]).stringValue + } + + if let prefix = self.encoder.options.prefix { + return prefix + encodedKey + } else { + return encodedKey + } + } + mutating func encodeNil(forKey key: Key) throws { - self.encoder.row.append((key.stringValue, SQLLiteral.null)) + self.encoder.row.append((self.column(for: key), SQLLiteral.null)) } mutating func encode(_ value: T, forKey key: Key) throws where T : Encodable { if let value = value as? SQLExpression { - self.encoder.row.append((key.stringValue, value)) + self.encoder.row.append((self.column(for: key), value)) } else { - self.encoder.row.append((key.stringValue, SQLBind(value))) + self.encoder.row.append((self.column(for: key), SQLBind(value))) } } @@ -77,3 +119,52 @@ private final class _Encoder: Encoder { fatalError() } } + +private extension _Encoder { + /// This is a custom implementation which does not require Foundation as opposed to the one at which needs CharacterSet from Foundation https://github.com/apple/swift/blob/master/stdlib/public/Darwin/Foundation/JSONEncoder.swift + /// + /// Provide a custom conversion to the key in the encoded JSON from the keys specified by the encoded types. + /// The full path to the current encoding position is provided for context (in case you need to locate this key within the payload). The returned key is used in place of the last component in the coding path before encoding. + /// If the result of the conversion is a duplicate key, then only one value will be present in the result. + static func _convertToSnakeCase(_ stringKey: String) -> String { + guard !stringKey.isEmpty else { return stringKey } + + enum Status { + case uppercase + case lowercase + case number + } + + var status = Status.lowercase + var snakeCasedString = "" + var i = stringKey.startIndex + while i < stringKey.endIndex { + let nextIndex = stringKey.index(i, offsetBy: 1) + + if stringKey[i].isUppercase { + switch status { + case .uppercase: + if nextIndex < stringKey.endIndex { + if stringKey[nextIndex].isLowercase { + snakeCasedString.append("_") + } + } + case .lowercase, + .number: + if i != stringKey.startIndex { + snakeCasedString.append("_") + } + } + status = .uppercase + snakeCasedString.append(stringKey[i].lowercased()) + } else { + status = .lowercase + snakeCasedString.append(stringKey[i]) + } + + i = nextIndex + } + + return snakeCasedString + } +} diff --git a/Sources/SQLKit/SQLRow.swift b/Sources/SQLKit/SQLRow.swift index 36ab454b..cc7515cf 100644 --- a/Sources/SQLKit/SQLRow.swift +++ b/Sources/SQLKit/SQLRow.swift @@ -7,10 +7,19 @@ public protocol SQLRow { } extension SQLRow { - public func decode(model type: D.Type, prefix: String? = nil) throws -> D + public func decode(model type: D.Type, prefix: String? = nil, keyDecodingStrategy: SQLRowDecoder.KeyDecodingStrategy = .useDefaultKeys) throws -> D where D: Decodable { - try SQLRowDecoder().decode(D.self, from: self, prefix: prefix) + var rowDecoder = SQLRowDecoder() + rowDecoder.prefix = prefix + rowDecoder.keyDecodingStrategy = keyDecodingStrategy + return try rowDecoder.decode(D.self, from: self) + } + + public func decode(model type: D.Type, with rowDecoder: SQLRowDecoder) throws -> D + where D: Decodable + { + return try rowDecoder.decode(D.self, from: self) } /// This method exists to enable the compiler to perform type inference on diff --git a/Sources/SQLKit/SQLRowDecoder.swift b/Sources/SQLKit/SQLRowDecoder.swift index 8793e4d3..4dbd98af 100644 --- a/Sources/SQLKit/SQLRowDecoder.swift +++ b/Sources/SQLKit/SQLRowDecoder.swift @@ -1,8 +1,35 @@ -struct SQLRowDecoder { - func decode(_ type: T.Type, from row: SQLRow, prefix: String? = nil) throws -> T +import Foundation + +public struct SQLRowDecoder { + public var prefix: String? + public var keyDecodingStrategy: KeyDecodingStrategy + + public init(prefix: String? = nil, keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys) { + self.prefix = prefix + self.keyDecodingStrategy = keyDecodingStrategy + } + + func decode(_ type: T.Type, from row: SQLRow) throws -> T where T: Decodable { - return try T.init(from: _Decoder(prefix: prefix, row: row)) + return try T.init(from: _Decoder(row: row, options: options)) + } + + public enum KeyDecodingStrategy { + case useDefaultKeys + // converts rows in snake_case to from coding keys in camelCase to + case convertFromSnakeCase + case custom(([CodingKey]) -> CodingKey) + } + + fileprivate struct _Options { + let prefix: String? + let keyDecodingStrategy: KeyDecodingStrategy + } + + /// The options set on the top-level decoder. + fileprivate var options: _Options { + return _Options(prefix: prefix, keyDecodingStrategy: keyDecodingStrategy) } enum _Error: Error { @@ -12,17 +39,23 @@ struct SQLRowDecoder { } struct _Decoder: Decoder { - let prefix: String? + fileprivate let options: SQLRowDecoder._Options let row: SQLRow var codingPath: [CodingKey] = [] var userInfo: [CodingUserInfoKey : Any] { [:] } + fileprivate init(row: SQLRow, codingPath: [CodingKey] = [], options: _Options) { + self.options = options + self.row = row + self.codingPath = codingPath + } + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key: CodingKey { - .init(_KeyedDecoder(prefix: self.prefix, row: self.row, codingPath: self.codingPath)) + .init(_KeyedDecoder(referencing: self, row: self.row, codingPath: self.codingPath)) } func unkeyedContainer() throws -> UnkeyedDecodingContainer { @@ -37,7 +70,8 @@ struct SQLRowDecoder { struct _KeyedDecoder: KeyedDecodingContainerProtocol where Key: CodingKey { - let prefix: String? + /// A reference to the decoder we're reading from. + private let decoder: _Decoder let row: SQLRow var codingPath: [CodingKey] = [] var allKeys: [Key] { @@ -46,11 +80,26 @@ struct SQLRowDecoder { } } + fileprivate init(referencing decoder: _Decoder, row: SQLRow, codingPath: [CodingKey] = []) { + self.decoder = decoder + self.row = row + } + func column(for key: Key) -> String { - if let prefix = self.prefix { - return prefix + key.stringValue + var decodedKey = key.stringValue + switch self.decoder.options.keyDecodingStrategy { + case .useDefaultKeys: + break + case .convertFromSnakeCase: + decodedKey = _convertFromSnakeCase(decodedKey) + case .custom(let customKeyDecodingFunc): + decodedKey = customKeyDecodingFunc([key]).stringValue + } + + if let prefix = self.decoder.options.prefix { + return prefix + decodedKey } else { - return key.stringValue + return decodedKey } } @@ -82,7 +131,7 @@ struct SQLRowDecoder { } func superDecoder() throws -> Decoder { - _Decoder(prefix: self.prefix, row: self.row, codingPath: self.codingPath) + _Decoder(row: self.row, codingPath: self.codingPath, options: self.decoder.options) } func superDecoder(forKey key: Key) throws -> Decoder { @@ -90,3 +139,68 @@ struct SQLRowDecoder { } } } + +fileprivate extension SQLRowDecoder { + /// This is an implementation is taken from from Swift's JSON KeyDecodingStrategy + /// https://github.com/apple/swift/blob/master/stdlib/public/Darwin/Foundation/JSONEncoder.swift + + // Convert from "snake_case_keys" to "camelCaseKeys" before attempting to match a key with the one specified by each type. + /// + /// The conversion to upper case uses `Locale.system`, also known as the ICU "root" locale. This means the result is consistent regardless of the current user's locale and language preferences. + /// + /// Converting from snake case to camel case: + /// 1. Capitalizes the word starting after each `_` + /// 2. Removes all `_` + /// 3. Preserves starting and ending `_` (as these are often used to indicate private variables or other metadata). + /// For example, `one_two_three` becomes `oneTwoThree`. `_one_two_three_` becomes `_oneTwoThree_`. + /// + /// - Note: Using a key decoding strategy has a nominal performance cost, as each string key has to be inspected for the `_` character. + static func _convertFromSnakeCase(_ stringKey: String) -> String { + guard !stringKey.isEmpty else { return stringKey } + + var words : [Range] = [] + // The general idea of this algorithm is to split words on transition from lower to upper case, then on transition of >1 upper case characters to lowercase + // + // myProperty -> my_property + // myURLProperty -> my_url_property + // + // We assume, per Swift naming conventions, that the first character of the key is lowercase. + var wordStart = stringKey.startIndex + var searchRange = stringKey.index(after: wordStart)..1 capital letters. Turn those into a word, stopping at the capital before the lower case character. + let beforeLowerIndex = stringKey.index(before: lowerCaseRange.lowerBound) + words.append(upperCaseRange.lowerBound.. ()) { - var sql = SQLStatement(parts: [], database: self.database) - closure(&sql) - sql.serialize(to: &self) - } -} - -public struct SQLStatement: SQLExpression { - public var parts: [SQLExpression] - let database: SQLDatabase - - public var dialect: SQLDialect { - self.database.dialect - } - - public mutating func append(_ raw: String) { - self.append(SQLRaw(raw)) - } - - public mutating func append(_ part: SQLExpression) { - self.parts.append(part) - } - - public func serialize(to serializer: inout SQLSerializer) { - for (i, part) in parts.enumerated() { - if i != 0 { - serializer.write(" ") - } - part.serialize(to: &serializer) - } - } -} diff --git a/Sources/SQLKit/SQLStatement.swift b/Sources/SQLKit/SQLStatement.swift new file mode 100644 index 00000000..3b62e0f3 --- /dev/null +++ b/Sources/SQLKit/SQLStatement.swift @@ -0,0 +1,33 @@ +extension SQLSerializer { + public mutating func statement(_ closure: (inout SQLStatement) -> ()) { + var sql = SQLStatement(parts: [], database: self.database) + closure(&sql) + sql.serialize(to: &self) + } +} + +public struct SQLStatement: SQLExpression { + public var parts: [SQLExpression] + let database: SQLDatabase + + public var dialect: SQLDialect { + self.database.dialect + } + + public mutating func append(_ raw: String) { + self.append(SQLRaw(raw)) + } + + public mutating func append(_ part: SQLExpression) { + self.parts.append(part) + } + + public func serialize(to serializer: inout SQLSerializer) { + for (i, part) in parts.enumerated() { + if i != 0 { + serializer.write(" ") + } + part.serialize(to: &serializer) + } + } +} diff --git a/Sources/SQLKitBenchmark/SQLBenchmark+Codeable.swift b/Sources/SQLKitBenchmark/SQLBenchmark+Codeable.swift new file mode 100644 index 00000000..72ac4319 --- /dev/null +++ b/Sources/SQLKitBenchmark/SQLBenchmark+Codeable.swift @@ -0,0 +1,47 @@ +import Foundation + +import SQLKit + +extension SQLBenchmarker { + public func testCodable() throws { + try self.db.drop(table: "planets") + .ifExists() + .run().wait() + try self.db.drop(table: "galaxies") + .ifExists() + .run().wait() + try self.db.create(table: "galaxies") + .column("id", type: .bigint, .primaryKey) + .column("name", type: .text) + .run().wait() + try self.db.create(table: "planets") + .ifNotExists() + .column("id", type: .bigint, .primaryKey) + .column("name", type: .bigint, .primaryKey) + .column("is_inhabited", type: .smallint, .notNull) + .column("galaxyID", type: .bigint, .references("galaxies", "id")) + .run().wait() + + // insert + let galaxy = Galaxy(name: "milky way") + try self.db.insert(into: "galaxies").model(galaxy).run().wait() + + // insert with keyEncodingStrategy + let earth = Planet(name: "Earth", isInhabited: true) + let mars = Planet(name: "Mars", isInhabited: false) + try self.db.insert(into: "planets") + .models([earth, mars], keyEncodingStrategy: .convertToSnakeCase) + .run().wait() + } +} + +fileprivate struct Planet: Codable { + let id: Int? = nil + let name: String + let isInhabited: Bool +} + +fileprivate struct Galaxy: Codable { + let id: Int? = nil + let name: String +} diff --git a/Sources/SQLKitBenchmark/SQLBenchmark+Planets.swift b/Sources/SQLKitBenchmark/SQLBenchmark+Planets.swift index 98ae8161..c0a64ea9 100644 --- a/Sources/SQLKitBenchmark/SQLBenchmark+Planets.swift +++ b/Sources/SQLKitBenchmark/SQLBenchmark+Planets.swift @@ -26,6 +26,7 @@ extension SQLBenchmarker { .column("id") .unique() .run().wait() + // INSERT INTO "galaxies" ("id", "name") VALUES (DEFAULT, $1) try self.db.insert(into: "galaxies") .columns("id", "name") @@ -98,58 +99,3 @@ extension SQLBenchmarker { } } } - -//import XCTest -// -//extension SQLBenchmarker { -// internal func testPlanets() throws { -// -// -// -// -// -// -// -// -// -// -// -// try self.db.select() -// .column(.coalesce(.sum("id"), 0), as: "id_sum") -// .from("planets") -// .where("galaxyID", .equal, 5_000_000) -// .run().wait() -// -// try self.db.update("planets") -// .where("name", .equal, .bind("Jpuiter")) -// .set(["name": "Jupiter"]) -// .run().wait() -// -// let selectC = try self.db.select() -// .column(.all) -// .from("planets") -// .join("galaxyID", to: .column(name: "id", table: "galaxies")) -// .all().wait().map { -// try ( -// $0.decode(Galaxy.self, table: "galaxies"), -// $0.decode(Planet.self, table: "planets") -// ) -// } -// XCTAssertEqual(selectC.count, 6) -// -// try self.db.update("galaxies") -// .set("name", to: .bind("Milky Way 2")) -// .where("name", .equal, .bind("Milky Way")) -// .run().wait() -// -// try self.db.delete(from: "galaxies") -// .where("name", .equal, .bind("Milky Way")) -// .run().wait() -// -// let b = try self.db.select() -// .column(.count(.all), as: "c") -// .from("galaxies") -// .all().wait() -// XCTAssertEqual(b.count, 1) -// } -//} diff --git a/Tests/SQLKitTests/SQLKitTests.swift b/Tests/SQLKitTests/SQLKitTests.swift index 4eb1b425..451fbbe8 100644 --- a/Tests/SQLKitTests/SQLKitTests.swift +++ b/Tests/SQLKitTests/SQLKitTests.swift @@ -14,6 +14,29 @@ final class SQLKitTests: XCTestCase { let benchmarker = SQLBenchmarker(on: db) try benchmarker.run() } + + func testSelect_whereIn() throws { + try db.select().column("*") + .from("planets") + .where("name", .in, ["Earth", "Mars"]) + .run().wait() + XCTAssertEqual(db.results[0], "SELECT * FROM `planets` WHERE `name` IN (?, ?)") + } + + func testUpdate() throws { + try db.update("planets") + .where("name", .equal, "Jpuiter") + .set("name", to: "Jupiter") + .run().wait() + XCTAssertEqual(db.results[0], "UPDATE `planets` SET `name` = ? WHERE `name` = ?") + } + + func testDelete() throws { + try db.delete(from: "planets") + .where("name", .equal, "Jupiter") + .run().wait() + XCTAssertEqual(db.results[0], "DELETE FROM `planets` WHERE `name` = ?") + } func testLockingClause_forUpdate() throws { try db.select().column("*") @@ -467,6 +490,15 @@ CREATE TABLE `planets`(`id` BIGINT, `name` TEXT, `diameter` INTEGER, `galaxy_nam let foo: Int let bar: Double? let baz: String + let waldoFred: Int? + } + + struct FooWithForeignKey: Codable { + let id: UUID + let foo: Int + let bar: Double? + let baz: String + let waldoFredID: Int } do { @@ -474,26 +506,96 @@ CREATE TABLE `planets`(`id` BIGINT, `name` TEXT, `diameter` INTEGER, `galaxy_nam "id": UUID(), "foo": 42, "bar": Double?.none as Any, - "baz": "vapor" + "baz": "vapor", + "waldoFred": 2015 ]) let foo = try row.decode(model: Foo.self) XCTAssertEqual(foo.foo, 42) XCTAssertEqual(foo.bar, nil) XCTAssertEqual(foo.baz, "vapor") + XCTAssertEqual(foo.waldoFred, 2015) + } catch { + XCTFail("Could not decode row \(error)") } do { let row = TestRow(data: [ "foos_id": UUID(), "foos_foo": 42, "foos_bar": Double?.none as Any, - "foos_baz": "vapor" + "foos_baz": "vapor", + "foos_waldoFred": 2015 ]) let foo = try row.decode(model: Foo.self, prefix: "foos_") XCTAssertEqual(foo.foo, 42) XCTAssertEqual(foo.bar, nil) XCTAssertEqual(foo.baz, "vapor") + XCTAssertEqual(foo.waldoFred, 2015) + } catch { + XCTFail("Could not decode row with prefix \(error)") + } + do { + let row = TestRow(data: [ + "id": UUID(), + "foo": 42, + "bar": Double?.none as Any, + "baz": "vapor", + "waldo_fred": 2015 + ]) + + let foo = try row.decode(model: Foo.self, keyDecodingStrategy: .convertFromSnakeCase) + XCTAssertEqual(foo.foo, 42) + XCTAssertEqual(foo.bar, nil) + XCTAssertEqual(foo.baz, "vapor") + XCTAssertEqual(foo.waldoFred, 2015) + } catch { + XCTFail("Could not decode row with keyDecodingStrategy \(error)") + } + do { + let row = TestRow(data: [ + "id": UUID(), + "foo": 42, + "bar": Double?.none as Any, + "baz": "vapor", + "waldoFredID": 2015 + ]) + + /// An implementation of CodingKey that's useful for combining and transforming keys as strings. + struct AnyKey: CodingKey { + var stringValue: String + var intValue: Int? + + init?(stringValue: String) { + self.stringValue = stringValue + self.intValue = nil + } + + init?(intValue: Int) { + self.stringValue = String(intValue) + self.intValue = intValue + } + } + + func decodeIdToID(_ keys: [CodingKey]) -> CodingKey { + let key = keys.last! + let keyString = key.stringValue + if keyString.hasSuffix("Id") { + var transformedKeyStringValue = keyString + transformedKeyStringValue.removeLast(2) + transformedKeyStringValue.append("ID") + return AnyKey(stringValue: transformedKeyStringValue)! + } + return key + } + + let foo = try row.decode(model: FooWithForeignKey.self, keyDecodingStrategy: .custom(decodeIdToID)) + XCTAssertEqual(foo.foo, 42) + XCTAssertEqual(foo.bar, nil) + XCTAssertEqual(foo.baz, "vapor") + XCTAssertEqual(foo.waldoFredID, 2015) + } catch { + XCTFail("Could not decode row with keyDecodingStrategy \(error)") } } }