From 7f39483d13d4d99d64bdd701c907a1e93cee7f0b Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Wed, 15 Mar 2017 17:29:44 +0100 Subject: [PATCH 01/11] raw first steps --- Sources/Fluent/Database/Database.swift | 8 +- Sources/Fluent/Database/Driver.swift | 7 +- Sources/Fluent/Database/Executor.swift | 6 +- .../Fluent/Entity/KeyNamingConvention.swift | 2 + Sources/Fluent/Pivot/Pivot.swift | 2 + Sources/Fluent/Preparation/Migration.swift | 1 - Sources/Fluent/Query/Action.swift | 68 +++ Sources/Fluent/Query/Comparison.swift | 46 -- Sources/Fluent/Query/Filter.swift | 163 ------ Sources/Fluent/Query/Filter/Comparison.swift | 104 ++++ Sources/Fluent/Query/Filter/Filter.swift | 37 ++ .../Fluent/Query/Filter/Query+Filter.swift | 76 +++ Sources/Fluent/Query/Filter/Query+Group.swift | 29 + Sources/Fluent/Query/{ => Filter}/Scope.swift | 0 Sources/Fluent/Query/Group.swift | 21 - Sources/Fluent/Query/Log.swift | 13 +- Sources/Fluent/Query/Query.swift | 6 + Sources/Fluent/Query/Raw.swift | 60 ++ Sources/Fluent/SQL/GeneralSQLSerializer.swift | 532 ----------------- Sources/Fluent/SQL/SQL+Query.swift | 43 -- Sources/Fluent/SQL/SQL+Schema.swift | 26 - Sources/Fluent/SQL/SQL.swift | 17 - Sources/Fluent/SQL/SQLQuerySerializer.swift | 550 ++++++++++++++++++ Sources/Fluent/SQL/SQLSerializer.swift | 5 +- Sources/Fluent/SQLite/SQLiteDriver.swift | 10 +- Sources/Fluent/SQLite/SQLiteSerializer.swift | 4 +- Sources/Fluent/Schema/Builder.swift | 170 ++++++ Sources/Fluent/Schema/Creator.swift | 6 + Sources/Fluent/Schema/Database+Schema.swift | 51 +- Sources/Fluent/Schema/Field.swift | 144 +++++ Sources/Fluent/Schema/Modifier.swift | 19 + Sources/Fluent/Schema/Schema+Creator.swift | 175 ------ Sources/Fluent/Schema/Schema+Field.swift | 65 --- Sources/Fluent/Schema/Schema+Modifier.swift | 21 - Sources/Fluent/Schema/Schema.swift | 7 - .../FluentTester/Tester+InsertAndFind.swift | 2 +- Sources/FluentTester/Tester+Paginate.swift | 2 +- .../Tester+PivotsAndRelations.swift | 32 +- Sources/FluentTester/Tester+Schema.swift | 2 +- Sources/FluentTester/Tester+Timestamps.swift | 2 +- Tests/FluentTests/JoinTests.swift | 79 +-- Tests/FluentTests/ModelFindTests.swift | 6 +- Tests/FluentTests/ModelTests.swift | 41 +- Tests/FluentTests/PivotTests.swift | 4 +- Tests/FluentTests/PreparationTests.swift | 27 +- Tests/FluentTests/RelationTests.swift | 11 +- Tests/FluentTests/SQLSerializerTests.swift | 157 +++-- Tests/FluentTests/SchemaCreateTests.swift | 58 +- Tests/FluentTests/Utilities/Compound.swift | 2 + Tests/FluentTests/Utilities/Dummy.swift | 6 +- .../Utilities/LastQueryDriver.swift | 21 +- 51 files changed, 1509 insertions(+), 1437 deletions(-) delete mode 100644 Sources/Fluent/Query/Comparison.swift delete mode 100644 Sources/Fluent/Query/Filter.swift create mode 100644 Sources/Fluent/Query/Filter/Comparison.swift create mode 100644 Sources/Fluent/Query/Filter/Filter.swift create mode 100644 Sources/Fluent/Query/Filter/Query+Filter.swift create mode 100644 Sources/Fluent/Query/Filter/Query+Group.swift rename Sources/Fluent/Query/{ => Filter}/Scope.swift (100%) delete mode 100644 Sources/Fluent/Query/Group.swift create mode 100644 Sources/Fluent/Query/Raw.swift delete mode 100644 Sources/Fluent/SQL/GeneralSQLSerializer.swift delete mode 100644 Sources/Fluent/SQL/SQL+Query.swift delete mode 100644 Sources/Fluent/SQL/SQL+Schema.swift delete mode 100644 Sources/Fluent/SQL/SQL.swift create mode 100644 Sources/Fluent/SQL/SQLQuerySerializer.swift create mode 100644 Sources/Fluent/Schema/Builder.swift create mode 100644 Sources/Fluent/Schema/Creator.swift create mode 100644 Sources/Fluent/Schema/Field.swift create mode 100644 Sources/Fluent/Schema/Modifier.swift delete mode 100644 Sources/Fluent/Schema/Schema+Creator.swift delete mode 100644 Sources/Fluent/Schema/Schema+Field.swift delete mode 100644 Sources/Fluent/Schema/Schema+Modifier.swift delete mode 100644 Sources/Fluent/Schema/Schema.swift diff --git a/Sources/Fluent/Database/Database.swift b/Sources/Fluent/Database/Database.swift index b8dd2ecf..a961bfcd 100644 --- a/Sources/Fluent/Database/Database.swift +++ b/Sources/Fluent/Database/Database.swift @@ -79,13 +79,7 @@ extension Database { return try threadConnectionPool.connection().query(query) } - /// Seeee Executor protocol. - public func schema(_ schema: Schema) throws { - log?(Log(schema)) - try threadConnectionPool.connection().schema(schema) - } - - /// Seeee Executor protocol. + /// See Executor protocol. @discardableResult public func raw(_ raw: String, _ values: [Node]) throws -> Node { log?(Log(raw: raw)) diff --git a/Sources/Fluent/Database/Driver.swift b/Sources/Fluent/Database/Driver.swift index cbacf33b..b4f55dd1 100644 --- a/Sources/Fluent/Database/Driver.swift +++ b/Sources/Fluent/Database/Driver.swift @@ -44,15 +44,10 @@ public protocol Driver: Executor { extension Driver { /// See Executor protocol. @discardableResult - public func query(_ query: Query) throws -> Node { + public func query(_ query: Query) throws -> Node { return try makeConnection().query(query) } - /// See Executor protocol. - public func schema(_ schema: Schema) throws { - return try makeConnection().schema(schema) - } - /// See Executor protocol. @discardableResult public func raw(_ raw: String, _ values: [Node]) throws -> Node { diff --git a/Sources/Fluent/Database/Executor.swift b/Sources/Fluent/Database/Executor.swift index a567d2b8..f3932c51 100644 --- a/Sources/Fluent/Database/Executor.swift +++ b/Sources/Fluent/Database/Executor.swift @@ -15,11 +15,7 @@ public protocol Executor { /// returns an array of results fetched, /// created, or updated by the action. @discardableResult - func query(_ query: Query) throws -> Node - - /// Creates the `Schema` indicated - /// by the `Builder`. - func schema(_ schema: Schema) throws + func query(_ query: Query) throws -> Node /// Drivers that support raw querying /// accept string queries and parameterized values. diff --git a/Sources/Fluent/Entity/KeyNamingConvention.swift b/Sources/Fluent/Entity/KeyNamingConvention.swift index 938add49..2fa30863 100644 --- a/Sources/Fluent/Entity/KeyNamingConvention.swift +++ b/Sources/Fluent/Entity/KeyNamingConvention.swift @@ -6,6 +6,8 @@ public enum KeyNamingConvention { case camelCase } +// MARK: Convert PascalCase to snake and camel + extension String { internal func snake_case() -> String { let characters = Array(self.characters) diff --git a/Sources/Fluent/Pivot/Pivot.swift b/Sources/Fluent/Pivot/Pivot.swift index 4cbe4f12..34f9011d 100644 --- a/Sources/Fluent/Pivot/Pivot.swift +++ b/Sources/Fluent/Pivot/Pivot.swift @@ -69,7 +69,9 @@ public final class Pivot< try row.set(Right.foreignIdKey, rightId) return row } +} +extension Pivot: Preparation { public static func prepare(_ database: Database) throws { try database.create(self) { builder in builder.id(for: self) diff --git a/Sources/Fluent/Preparation/Migration.swift b/Sources/Fluent/Preparation/Migration.swift index 3209d5e7..adcb78f1 100644 --- a/Sources/Fluent/Preparation/Migration.swift +++ b/Sources/Fluent/Preparation/Migration.swift @@ -14,7 +14,6 @@ final class Migration: Entity { func makeRow() throws -> Row { var row = Row() - try row.set(idKey, id) try row.set("name", name) return row } diff --git a/Sources/Fluent/Query/Action.swift b/Sources/Fluent/Query/Action.swift index c616b598..f2c5c3b7 100644 --- a/Sources/Fluent/Query/Action.swift +++ b/Sources/Fluent/Query/Action.swift @@ -7,4 +7,72 @@ public enum Action { case delete case create case modify + case schema(Schema) +} + +public enum Schema { + case create([Field]) + case modify(add: [Field], remove: [Field]) + case delete +} + +extension Action: Equatable { + public static func ==(lhs: Action, rhs: Action) -> Bool { + switch lhs { + case .fetch: + switch rhs { + case .fetch: return true + default: return false + } + case .count: + switch rhs { + case .count: return true + default: return false + } + case .delete: + switch rhs { + case .delete: return true + default: return false + } + case .create: + switch rhs { + case .create: return true + default: return false + } + case .modify: + switch rhs { + case .modify: return true + default: return false + } + case .schema(let a): + switch rhs { + case .schema(let b): return a == b + default: return false + } + + } + } +} + +extension Schema: Equatable { + public static func ==(lhs: Schema, rhs: Schema) -> Bool { + switch lhs { + case .create(let a): + switch rhs { + case .create(let b): return a == b + default: return false + } + case .modify(let addA, let removeA): + switch rhs { + case .modify(let addB, let removeB): + return addA == addB && removeA == removeB + default: return false + } + case .delete: + switch rhs { + case .delete: return true + default: return false + } + } + } } diff --git a/Sources/Fluent/Query/Comparison.swift b/Sources/Fluent/Query/Comparison.swift deleted file mode 100644 index a6a37c01..00000000 --- a/Sources/Fluent/Query/Comparison.swift +++ /dev/null @@ -1,46 +0,0 @@ -extension Filter { - /// Describes the various operators for - /// comparing values. - public enum Comparison { - case equals - case greaterThan - case lessThan - case greaterThanOrEquals - case lessThanOrEquals - case notEquals - case hasSuffix - case hasPrefix - case contains - } - -} - -func == (lhs: String, rhs: NodeRepresentable) throws -> Filter.Method { - let node = try rhs.makeNode(in: rowContext) - return .compare(lhs, .equals, node) -} - -func > (lhs: String, rhs: NodeRepresentable) throws -> Filter.Method { - let node = try rhs.makeNode(in: rowContext) - return .compare(lhs, .greaterThan, node) -} - -func < (lhs: String, rhs: NodeRepresentable) throws -> Filter.Method { - let node = try rhs.makeNode(in: rowContext) - return .compare(lhs, .lessThan, node) -} - -func >= (lhs: String, rhs: NodeRepresentable) throws -> Filter.Method { - let node = try rhs.makeNode(in: rowContext) - return .compare(lhs, .greaterThanOrEquals, node) -} - -func <= (lhs: String, rhs: NodeRepresentable) throws -> Filter.Method { - let node = try rhs.makeNode(in: rowContext) - return .compare(lhs, .lessThanOrEquals, node) -} - -func != (lhs: String, rhs: NodeRepresentable) throws -> Filter.Method { - let node = try rhs.makeNode(in: rowContext) - return .compare(lhs, .notEquals, node) -} diff --git a/Sources/Fluent/Query/Filter.swift b/Sources/Fluent/Query/Filter.swift deleted file mode 100644 index 4b9e2660..00000000 --- a/Sources/Fluent/Query/Filter.swift +++ /dev/null @@ -1,163 +0,0 @@ -/// Defines a `Filter` that can be -/// added on fetch, delete, and update -/// operations to limit the set of -/// data affected. -public struct Filter { - public enum Relation { - case and, or - } - - public enum Method { - case compare(String, Comparison, Node) - case subset(String, Scope, [Node]) - case group(Relation, [Filter]) - case raw(command: String, values: [Node]) - } - - public init(_ entity: Entity.Type, _ method: Method) { - self.entity = entity - self.method = method - } - - public var entity: Entity.Type - public var method: Method -} - -extension Filter: CustomStringConvertible { - public var description: String { - switch method { - case .compare(let field, let comparison, let value): - return "(\(entity)) \(field) \(comparison) \(value)" - case .subset(let field, let scope, let values): - let valueDescriptions = values.map { $0.string ?? "" } - return "(\(entity)) \(field) \(scope) \(valueDescriptions)" - case .group(let relation, let filters): - return filters.map { $0.description }.joined(separator: "\(relation)") - case .raw(command: let query, values: let values): - return "\(query) \(values)" - } - } -} - -extension QueryRepresentable { - @discardableResult - public func filter( - _ entity: T.Type, - _ field: String, - _ comparison: Filter.Comparison, - _ value: NodeRepresentable - ) throws -> Query { - let query = try makeQuery() - let filter = Filter(entity, .compare(field, comparison, try value.makeNode(in: query.context))) - query.filters.append(filter) - return query - } - - @discardableResult - public func filter( - _ entity: T.Type, - _ field: String, - _ scope: Filter.Scope, - _ set: [NodeRepresentable] - ) throws -> Query { - let query = try makeQuery() - let filter = Filter(T.self, .subset(field, scope, try set.map({ try $0.makeNode(in: query.context) }))) - query.filters.append(filter) - return query - } - - @discardableResult - public func filter( - _ entity: T.Type, - _ field: String, - _ value: NodeRepresentable - ) throws -> Query { - return try makeQuery().filter(entity, field, .equals, value) - } - - //MARK: Filter - - /// Adds a `.compare` filter to the query's - /// filters. - /// - /// Used for filtering results based on how - /// a result's value compares to the supplied value. - @discardableResult - public func filter( - _ field: String, - _ comparison: Filter.Comparison, - _ value: NodeRepresentable - ) throws -> Query { - return try makeQuery().filter(E.self, field, comparison, value) - } - - /// Adds a `.subset` filter to the query's - /// filters. - /// - /// Used for filtering results based on whether - /// a result's value is or is not in a set. - @discardableResult - public func filter( - _ field: String, - _ scope: Filter.Scope, - _ set: [NodeRepresentable] - ) throws -> Query { - return try makeQuery().filter(E.self, field, scope, set) - } - - - /// Shortcut for creating a `.equals` filter. - @discardableResult - public func filter( - _ field: String, - _ value: NodeRepresentable - ) throws -> Query { - return try makeQuery().filter(E.self, field, .equals, value) - } - - /// Shortcut for creating a `.contains` filter. - @discardableResult - public func filter( - _ field: String, - contains value: NodeRepresentable - ) throws -> Query { - return try filter(E.self, field, .contains, value) - } -} - -extension QueryRepresentable { - @discardableResult - public func filter( - _ entity: T.Type, - _ value: Filter.Method - ) throws -> Query { - let query = try makeQuery() - let filter = Filter(T.self, value) - query.filters.append(filter) - return query - } - - /// Used to accept more freehand queries - @discardableResult - public func filter( - _ value: Filter.Method - ) throws -> Query { - let query = try makeQuery() - let filter = Filter(E.self, value) - query.filters.append(filter) - return query - } -} - -extension QueryRepresentable { - /// Shortcut for creating a `.raw` filter. - @discardableResult - public func raw(command: String, values: [NodeRepresentable] = []) throws -> Query { - let query = try makeQuery() - - let values = try values.map { try $0.makeNode(in: query.context) } - let filter = Filter(E.self, .raw(command: command, values: values)) - query.filters.append(filter) - return query - } -} diff --git a/Sources/Fluent/Query/Filter/Comparison.swift b/Sources/Fluent/Query/Filter/Comparison.swift new file mode 100644 index 00000000..9a396c06 --- /dev/null +++ b/Sources/Fluent/Query/Filter/Comparison.swift @@ -0,0 +1,104 @@ +extension Filter { + /// Describes the various operators for + /// comparing values. + public enum Comparison { + case equals + case greaterThan + case lessThan + case greaterThanOrEquals + case lessThanOrEquals + case notEquals + case hasSuffix + case hasPrefix + case contains + case custom(String) + } + +} + +func == (lhs: String, rhs: NodeRepresentable) throws -> Filter.Method { + let node = try rhs.makeNode(in: rowContext) + return .compare(lhs, .equals, node) +} + +func > (lhs: String, rhs: NodeRepresentable) throws -> Filter.Method { + let node = try rhs.makeNode(in: rowContext) + return .compare(lhs, .greaterThan, node) +} + +func < (lhs: String, rhs: NodeRepresentable) throws -> Filter.Method { + let node = try rhs.makeNode(in: rowContext) + return .compare(lhs, .lessThan, node) +} + +func >= (lhs: String, rhs: NodeRepresentable) throws -> Filter.Method { + let node = try rhs.makeNode(in: rowContext) + return .compare(lhs, .greaterThanOrEquals, node) +} + +func <= (lhs: String, rhs: NodeRepresentable) throws -> Filter.Method { + let node = try rhs.makeNode(in: rowContext) + return .compare(lhs, .lessThanOrEquals, node) +} + +func != (lhs: String, rhs: NodeRepresentable) throws -> Filter.Method { + let node = try rhs.makeNode(in: rowContext) + return .compare(lhs, .notEquals, node) +} + +extension Filter.Comparison: Equatable { + public static func ==(lhs: Filter.Comparison, rhs: Filter.Comparison) -> Bool { + switch lhs { + case .equals: + switch rhs { + case .equals: return true + default: return false + } + case .greaterThan: + switch rhs { + case .greaterThan: return true + default: return false + } + case .lessThan: + switch rhs { + case .lessThan: return true + default: return false + } + case .greaterThanOrEquals: + switch rhs { + case .greaterThanOrEquals: return true + default: return false + } + case .lessThanOrEquals: + switch rhs { + case .lessThanOrEquals: return true + default: return false + } + case .notEquals: + switch rhs { + case .notEquals: return true + default: return false + } + case .hasSuffix: + switch rhs { + case .hasSuffix: return true + default: return false + } + case .hasPrefix: + switch rhs { + case .hasPrefix: return true + default: return false + } + case .contains: + switch rhs { + case .contains: return true + default: return false + } + case .custom(let a): + switch rhs { + case .custom(let b): return a == b + default: return false + } + } + } +} diff --git a/Sources/Fluent/Query/Filter/Filter.swift b/Sources/Fluent/Query/Filter/Filter.swift new file mode 100644 index 00000000..4d155d23 --- /dev/null +++ b/Sources/Fluent/Query/Filter/Filter.swift @@ -0,0 +1,37 @@ +/// Defines a `Filter` that can be +/// added on fetch, delete, and update +/// operations to limit the set of +/// data affected. +public struct Filter { + public enum Relation { + case and, or + } + + public enum Method { + case compare(String, Comparison, Node) + case subset(String, Scope, [Node]) + case group(Relation, [Filter]) + } + + public init(_ entity: Entity.Type, _ method: Method) { + self.entity = entity + self.method = method + } + + public var entity: Entity.Type + public var method: Method +} + +extension Filter: CustomStringConvertible { + public var description: String { + switch method { + case .compare(let field, let comparison, let value): + return "(\(entity)) \(field) \(comparison) \(value)" + case .subset(let field, let scope, let values): + let valueDescriptions = values.map { $0.string ?? "" } + return "(\(entity)) \(field) \(scope) \(valueDescriptions)" + case .group(let relation, let filters): + return filters.map { $0.description }.joined(separator: "\(relation)") + } + } +} diff --git a/Sources/Fluent/Query/Filter/Query+Filter.swift b/Sources/Fluent/Query/Filter/Query+Filter.swift new file mode 100644 index 00000000..5d6b623d --- /dev/null +++ b/Sources/Fluent/Query/Filter/Query+Filter.swift @@ -0,0 +1,76 @@ +extension QueryRepresentable { + /// Manually create and append filter + @discardableResult + public func filter( + _ filter: Filter + ) throws -> Query { + let query = try makeQuery() + query.filters.append(filter) + return query + } + + /// Filter entity with field, comparison, and value. + @discardableResult + public func filter( + _ entity: T.Type, + _ field: String, + _ comparison: Filter.Comparison, + _ value: NodeRepresentable + ) throws -> Query { + let query = try makeQuery() + let value = try value.makeNode(in: query.context) + let filter = Filter(entity, .compare(field, comparison, value)) + return try query.filter(filter) + } + + /// Filter entity where field equals value + @discardableResult + public func filter( + _ entity: T.Type, + _ field: String, + _ value: NodeRepresentable + ) throws -> Query { + return try makeQuery() + .filter(entity, field, .equals, value) + } + + /// Filter self with field, comparison, and value. + @discardableResult + public func filter( + _ field: String, + _ comparison: Filter.Comparison, + _ value: NodeRepresentable + ) throws -> Query { + return try makeQuery() + .filter(E.self, field, comparison, value) + } + + /// Filter self where field equals value. + @discardableResult + public func filter( + _ field: String, + _ value: NodeRepresentable + ) throws -> Query { + return try makeQuery() + .filter(field, .equals, value) + } + + /// Entity operator filter queries + @discardableResult + public func filter( + _ entity: T.Type, + _ value: Filter.Method + ) throws -> Query { + let filter = Filter(T.self, value) + return try makeQuery().filter(filter) + } + + /// Self operator filter queries + @discardableResult + public func filter( + _ value: Filter.Method + ) throws -> Query { + let filter = Filter(E.self, value) + return try makeQuery().filter(filter) + } +} diff --git a/Sources/Fluent/Query/Filter/Query+Group.swift b/Sources/Fluent/Query/Filter/Query+Group.swift new file mode 100644 index 00000000..2d6109d5 --- /dev/null +++ b/Sources/Fluent/Query/Filter/Query+Group.swift @@ -0,0 +1,29 @@ +public typealias QueryClosure = (Query) throws -> () + +extension QueryRepresentable { + /// Grouped filter closure with specified relation. + public func group( + _ relation: Filter.Relation, + _ closure: QueryClosure + ) throws -> Query { + let main = try makeQuery() + + let sub = Query(main.database) + try closure(sub) + + let group = Filter(E.self, .group(relation, sub.filters)) + try filter(group) + + return main + } + + /// Grouped `and` filter subquery + public func and(_ closure: QueryClosure) throws -> Query { + return try group(.and, closure) + } + + /// Grouped `or` filter subquery + public func or(_ closure: QueryClosure) throws -> Query { + return try group(.or, closure) + } +} diff --git a/Sources/Fluent/Query/Scope.swift b/Sources/Fluent/Query/Filter/Scope.swift similarity index 100% rename from Sources/Fluent/Query/Scope.swift rename to Sources/Fluent/Query/Filter/Scope.swift diff --git a/Sources/Fluent/Query/Group.swift b/Sources/Fluent/Query/Group.swift deleted file mode 100644 index 7975bdb8..00000000 --- a/Sources/Fluent/Query/Group.swift +++ /dev/null @@ -1,21 +0,0 @@ -extension QueryRepresentable { - public func and(_ closure: (Query) throws -> ()) throws -> Query { - return try group(.and, closure) - } - - public func or(_ closure: (Query) throws -> ()) throws -> Query { - return try group(.or, closure) - } - - public func group(_ relation: Filter.Relation, _ closure: (Query) throws -> ()) throws -> Query { - let main = try makeQuery() - - let sub = Query(main.database) - try closure(sub) - - let group = Filter(E.self, .group(relation, sub.filters)) - main.filters.append(group) - - return main - } -} diff --git a/Sources/Fluent/Query/Log.swift b/Sources/Fluent/Query/Log.swift index c2aa6317..6f863734 100644 --- a/Sources/Fluent/Query/Log.swift +++ b/Sources/Fluent/Query/Log.swift @@ -14,7 +14,7 @@ public struct Log { } /// Create a log from raw sql and values. - init(sql: String, values: [Node]) { + init(sql: String, values: [Node] = []) { var log = sql if values.count > 0 { let valuesString = values.map({ $0.string ?? "" }).joined(separator: ", ") @@ -25,15 +25,8 @@ public struct Log { } /// Create a log from a Query - init(_ query: Query) { - let serializer = GeneralSQLSerializer(sql: query.sql) - let (sql, values) = serializer.serialize() - self.init(sql: sql, values: values) - } - - /// Create a log from a Schema query - init(_ schema: Schema) { - let serializer = GeneralSQLSerializer(sql: schema.sql) + init(_ query: Query) { + let serializer = GeneralSQLSerializer(query) let (sql, values) = serializer.serialize() self.init(sql: sql, values: values) } diff --git a/Sources/Fluent/Query/Query.swift b/Sources/Fluent/Query/Query.swift index e8b3bb93..beb7c141 100644 --- a/Sources/Fluent/Query/Query.swift +++ b/Sources/Fluent/Query/Query.swift @@ -20,6 +20,11 @@ public final class Query { /// be applied to the results. public var sorts: [Sort] + /// An array of custom, raw query + /// fragments that must be specially supported + /// by the underlying driver + public var raws: [Raw] + /// An array of joins: other entities /// that will be queried during this query's /// execution. @@ -39,6 +44,7 @@ public final class Query { self.database = database joins = [] sorts = [] + raws = [] } /// Performs the Query returning the raw diff --git a/Sources/Fluent/Query/Raw.swift b/Sources/Fluent/Query/Raw.swift new file mode 100644 index 00000000..99809b72 --- /dev/null +++ b/Sources/Fluent/Query/Raw.swift @@ -0,0 +1,60 @@ +public enum Raw { + case filter(String, [Node]) + case join(String) + case limit(String) + case sort(String) +} + +extension Raw { + public init( + filter: String, + values: [NodeRepresentable] = [] + ) throws { + let values = try values.map { nr in + return try nr.makeNode(in: nil) + } + self = .filter(filter, values) + } +} + +extension Sequence where Iterator.Element == Raw { + /// All raw filters and values + public var filters: [(String, [Node])] { + return flatMap { raw in + guard case .filter(let string, let values) = raw else { + return nil + } + return (string, values) + } + } + + /// All raw joins + public var joins: [String] { + return flatMap { raw in + guard case .join(let string) = raw else { + return nil + } + return string + } + } + + /// All raw limits + public var limits: [String] { + return flatMap { raw in + guard case .limit(let string) = raw else { + return nil + } + return string + } + } + + /// All raw sorts + public var sorts: [String] { + return flatMap { raw in + guard case .sort(let string) = raw else { + return nil + } + return string + } + } +} diff --git a/Sources/Fluent/SQL/GeneralSQLSerializer.swift b/Sources/Fluent/SQL/GeneralSQLSerializer.swift deleted file mode 100644 index 8d23f039..00000000 --- a/Sources/Fluent/SQL/GeneralSQLSerializer.swift +++ /dev/null @@ -1,532 +0,0 @@ -/// A generic SQL serializer. -/// This class can be subclassed by -/// specific SQL serializers. -open class GeneralSQLSerializer: SQLSerializer { - public let sql: SQL - - public required init(sql: SQL) { - self.sql = sql - } - - open func serialize() -> (String, [Node]) { - switch sql { - case .table(let action, let table): - var statement: [String] = [] - - statement += sql(action, table) - - return ( - statement.joined(separator: " "), - [] - ) - case .insert(let table, let data): - var statement: [String] = [] - - statement += "INSERT INTO" - statement += sql(table) - - let values: [Node] - if let (dataClause, dataValues) = sql(data) { - statement += dataClause - values = dataValues - } else { - values = [] - } - - return ( - sql(statement), - values - ) - case .select(let table, let filters, let unions, let orders, let limit): - var statement: [String] = [] - var values: [Node] = [] - - let tableSQL = sql(table) - statement += "SELECT \(tableSQL).* FROM" - statement += tableSQL - - if !unions.isEmpty { - statement += sql(unions) - } - - if !filters.isEmpty { - let (filtersClause, filtersValues) = sql(filters) - statement += filtersClause - values += filtersValues - } - - if !orders.isEmpty { - statement += sql(orders) - } - - if let limit = limit { - statement += sql(limit: limit) - } - - return ( - sql(statement), - values - ) - case .count(let table, let filters, let unions): - var statement: [String] = [] - var values: [Node] = [] - - statement += "SELECT COUNT(*) as _fluent_count FROM" - statement += sql(table) - - if !unions.isEmpty { - statement += sql(unions) - } - - if !filters.isEmpty { - let (filtersClause, filtersValues) = sql(filters) - statement += filtersClause - values += filtersValues - } - - return ( - sql(statement), - values - ) - case .delete(let table, let filters, let limit): - var statement: [String] = [] - var values: [Node] = [] - - statement += "DELETE FROM" - statement += sql(table) - - if !filters.isEmpty { - let (filtersClause, filtersValues) = sql(filters) - statement += filtersClause - values += filtersValues - } - - if let limit = limit { - statement += sql(limit: limit) - } - - return ( - sql(statement), - values - ) - case .update(let table, let filters, let data): - var statement: [String] = [] - - var values: [Node] = [] - - statement += "UPDATE" - statement += sql(table) - statement += "SET" - - if let data = data, let obj = data.typeObject { - let (dataClause, dataValues) = sql(update: obj) - statement += dataClause - values += dataValues - } - - let (filterclause, filterValues) = sql(filters) - statement += filterclause - values += filterValues - - return ( - sql(statement), - values - ) - } - } - - open func sql(limit: Limit) -> String { - var statement: [String] = [] - - statement += "LIMIT" - statement += "\(limit.offset), \(limit.count)" - - return statement.joined(separator: " ") - } - - - open func sql(_ filters: [Filter]) -> (String, [Node]) { - var statement: [String] = [] - - statement += "WHERE" - - let (clause, values) = sql(filters, relation: .and) - - statement += clause - - return ( - sql(statement), - values - ) - } - - open func sql(_ filters: [Filter], relation: Filter.Relation) -> (String, [Node]) { - var statement: [String] = [] - var values: [Node] = [] - - - var subStatement: [String] = [] - - for filter in filters { - let (clause, subValues) = sql(filter) - subStatement += clause - values += subValues - } - - statement += subStatement.joined(separator: " \(sql(relation)) ") - - return ( - sql(statement), - values - ) - } - - open func sql(_ relation: Filter.Relation) -> String { - let word: String - switch relation { - case .and: - word = "AND" - case .or: - word = "OR" - } - return word - } - - open func sql(_ filter: Filter) -> (String, [Node]) { - var statement: [String] = [] - var values: [Node] = [] - - switch filter.method { - case .compare(let key, let comparison, let value): - // `.null` needs special handling in the case of `.equals` or `.notEquals`. - if comparison == .equals && value == .null { - statement += "\(sql(filter.entity.entity)).\(sql(key)) IS NULL" - } - else if comparison == .notEquals && value == .null { - statement += "\(sql(filter.entity.entity)).\(sql(key)) IS NOT NULL" - } - else { - statement += "\(sql(filter.entity.entity)).\(sql(key))" - statement += sql(comparison) - statement += "?" - - /** - `.like` comparison operator requires additional - processing of `value` - */ - switch comparison { - case .hasPrefix: - values += sql(hasPrefix: value) - case .hasSuffix: - values += sql(hasSuffix: value) - case .contains: - values += sql(contains: value) - default: - values += value - } - } - case .subset(let key, let scope, let subValues): - statement += "\(sql(filter.entity.entity)).\(sql(key))" - statement += sql(scope) - statement += sql(subValues) - values += subValues - case .group(let relation, let filters): - let (clause, subvals) = sql(filters, relation: relation) - statement += "(\(clause))" - values += subvals - case .raw(command: let command, values: let subvalues): - statement += command - values += subvalues - } - - return ( - sql(statement), - values - ) - } - - open func sql(_ sort: Sort) -> String { - var clause: [String] = [] - - clause += sql(sort.entity.entity) + "." + sql(sort.field) - - switch sort.direction { - case .ascending: - clause += "ASC" - case .descending: - clause += "DESC" - } - - return sql(clause) - } - - open func sql(_ sorts: [Sort]) -> String { - var clause: [String] = [] - - clause += "ORDER BY" - - clause += sorts.map { sort in - return sql(sort) - }.joined(separator: ", ") - - return sql(clause) - } - - open func sql(_ comparison: Filter.Comparison) -> String { - switch comparison { - case .equals: - return "=" - case .greaterThan: - return ">" - case .greaterThanOrEquals: - return ">=" - case .lessThan: - return "<" - case .lessThanOrEquals: - return "<=" - case .notEquals: - return "!=" - case .hasSuffix: - fallthrough - case .hasPrefix: - fallthrough - case .contains: - return "LIKE" - } - } - - open func sql(hasPrefix value: Node) -> Node { - guard let string = value.string else { - return value - } - - return .string("\(string)%") - } - - open func sql(hasSuffix value: Node) -> Node { - guard let string = value.string else { - return value - } - - return .string("%\(string)") - } - - open func sql(contains value: Node) -> Node { - guard let string = value.string else { - return value - } - - return .string("%\(string)%") - } - - open func sql(_ scope: Filter.Scope) -> String { - switch scope { - case .in: - return "IN" - case .notIn: - return "NOT IN" - } - } - - open func sql(_ tableAction: SQL.TableAction, _ table: String) -> String { - switch tableAction { - case .alter(let create, let delete): - var clause: [String] = [] - - clause += "ALTER TABLE" - clause += sql(table) - - var subclause: [String] = [] - - for column in create { - subclause += "ADD " + sql(column) - } - - for name in delete { - subclause += "DROP " + sql(name) - } - - clause += subclause.joined(separator: ", ") - - return sql(clause) - case .create(let columns): - var clause: [String] = [] - - clause += "CREATE TABLE" - clause += sql(table) - clause += sql(columns) - - return sql(clause) - case .drop: - var clause: [String] = [] - - clause += "DROP TABLE IF EXISTS" - clause += sql(table) - - return sql(clause) - } - } - - open func sql(_ column: Schema.Field) -> String { - var clause: [String] = [] - - clause += sql(column.name) - clause += sql(column.type, primaryKey: column.primaryKey) - - if !column.optional { - clause += "NOT NULL" - } - - if column.unique { - clause += "UNIQUE" - } - - if let d = column.default { - let dc: String - - switch d.wrapped { - case .number(let n): - dc = "'" + n.description + "'" - case .null: - dc = "NULL" - case .bool(let b): - dc = b ? "TRUE" : "FALSE" - default: - dc = "'" + (d.string ?? "") + "'" - } - - clause += "DEFAULT \(dc)" - } - - return clause.joined(separator: " ") - } - - - open func sql(_ type: Schema.Field.DataType, primaryKey: Bool) -> String { - switch type { - case .id(let type): - let typeString: String - switch type { - case .int: - typeString = "INTEGER" - case .uuid: - typeString = "STRING" - case .custom(let dataType): - typeString = dataType - } - if primaryKey { - return typeString + " PRIMARY KEY" - } else { - return typeString - } - case .int: - return "INTEGER" - case .string(_): - return "STRING" - case .double: - return "DOUBLE" - case .bool: - return "BOOL" - case .bytes: - return "BLOB" - case .date: - return "TIMESTAMP" - case .custom(let type): - return type - } - } - - open func sql(_ data: Node?) -> (String, [Node])? { - guard let node = data else { - return nil - } - - guard let dict = node.typeObject else { - return nil - } - - var clause: [String] = [] - - let values = Array(dict.values) - - clause += sql(keys: Array(dict.keys)) - clause += "VALUES" - clause += sql(values) - - return ( - sql(clause), - values - ) - } - - open func sql(_ joins: [Join]) -> String { - var clause: [String] = [] - - for join in joins { - clause += sql(join) - } - - return sql(clause) - } - - open func sql(_ join: Join) -> String { - var clause: [String] = [] - - clause += "JOIN" - clause += sql(join.joined.entity) - clause += "ON" - - clause += "\(sql(join.base.entity)).\(sql(join.baseKey))" - clause += "=" - clause += "\(sql(join.joined.entity)).\(sql(join.joinedKey))" - - return sql(clause) - } - - open func sql(update data: [String: Node]) -> (String, [Node]) { - return ( - data.map(sql).joined(separator: ", "), - Array(data.values) - ) - } - - open func sql(key: String, value: Node) -> String { - return sql(key) + " = " + sql(value) - } - - open func sql(_ strings: [String]) -> String { - return strings.joined(separator: " ") - } - - open func sql(keys: [String]) -> String { - return sql(list: keys.map { sql($0) }) - } - - open func sql(list: [String]) -> String { - return "(" + list.joined(separator: ", ") + ")" - } - - open func sql(_ values: [Node]) -> String { - return "(" + values.map { sql($0) }.joined(separator: ", ") + ")" - } - - open func sql(_ value: Node) -> String { - return "?" - } - - open func sql(_ columns: [Schema.Field]) -> String { - return "(" + columns.map { sql($0) }.joined(separator: ", ") + ")" - } - - open func sql(_ string: String) -> String { - return "`\(string)`" - } -} - -public func +=(lhs: inout [String], rhs: String) { - lhs.append(rhs) -} - -public func +=(lhs: inout [Node], rhs: Node) { - lhs.append(rhs) -} diff --git a/Sources/Fluent/SQL/SQL+Query.swift b/Sources/Fluent/SQL/SQL+Query.swift deleted file mode 100644 index f68c3024..00000000 --- a/Sources/Fluent/SQL/SQL+Query.swift +++ /dev/null @@ -1,43 +0,0 @@ -extension SQL { - init(query: Query) { - switch query.action { - case .fetch: - self = .select( - table: T.entity, - filters: query.filters, - joins: query.joins, - orders: query.sorts, - limit: query.limit - ) - case .count: - self = .count( - table: T.entity, - filters: query.filters, - joins: query.joins - ) - case .create: - self = .insert( - table: T.entity, - data: query.data - ) - case .delete: - self = .delete( - table: T.entity, - filters: query.filters, - limit: query.limit - ) - case .modify: - self = .update( - table: T.entity, - filters: query.filters, - data: query.data - ) - } - } -} - -extension Query { - public var sql: SQL { - return SQL(query: self) - } -} diff --git a/Sources/Fluent/SQL/SQL+Schema.swift b/Sources/Fluent/SQL/SQL+Schema.swift deleted file mode 100644 index c5157f6f..00000000 --- a/Sources/Fluent/SQL/SQL+Schema.swift +++ /dev/null @@ -1,26 +0,0 @@ -extension SQL { - public init(schema: Schema) { - let action: TableAction - let table: String - - switch schema { - case .create(let entity, let fields): - table = entity - action = .create(columns: fields) - case .modify(let entity, let fields, let delete): - table = entity - action = .alter(create: fields, delete: delete) - case .delete(let entity): - table = entity - action = .drop - } - - self = .table(action: action, table: table) - } -} - -extension Schema { - public var sql: SQL { - return SQL(schema: self) - } -} diff --git a/Sources/Fluent/SQL/SQL.swift b/Sources/Fluent/SQL/SQL.swift deleted file mode 100644 index a3364987..00000000 --- a/Sources/Fluent/SQL/SQL.swift +++ /dev/null @@ -1,17 +0,0 @@ -/// Represents a SQL query that -/// can act as an intermediary between -/// Fluent data structures and serializers. -public enum SQL { - public enum TableAction { - case create(columns: [Schema.Field]) - case alter(create: [Schema.Field], delete: [String]) - case drop - } - - case insert(table: String, data: Node?) - case select(table: String, filters: [Filter], joins: [Join], orders: [Sort], limit: Limit?) - case count(table: String, filters: [Filter], joins: [Join]) - case update(table: String, filters: [Filter], data: Node?) - case delete(table: String, filters: [Filter], limit: Limit?) - case table(action: TableAction, table: String) -} diff --git a/Sources/Fluent/SQL/SQLQuerySerializer.swift b/Sources/Fluent/SQL/SQLQuerySerializer.swift new file mode 100644 index 00000000..32abd023 --- /dev/null +++ b/Sources/Fluent/SQL/SQLQuerySerializer.swift @@ -0,0 +1,550 @@ +/// Serializers a Query into general SQL +open class GeneralSQLSerializer: SQLSerializer { + public let query: Query + public required init(_ query: Query) { + self.query = query + } + + open func serialize() -> (String, [Node]) { + switch query.action { + case .create: + return insert() + case .fetch: + return select() + case .count: + return count() + case .delete: + return delete() + case .modify: + return modify() + case .schema(let schema): + switch schema { + case .create(let fields): + return create(fields) + case .modify(let add, let remove): + return alter(add: add, drop: remove) + case .delete: + return drop() + } + } + } + + // MARK: Data + + open func insert() -> (String, [Node]) { + var statement: [String] = [] + + statement += "INSERT INTO" + statement += escape(E.entity) + + let values: [Node] + + if let dict = query.data?.typeObject { + values = Array(dict.values) + let k = Array(dict.keys) + + statement += keys(k) + statement += "VALUES" + statement += placeholders(values) + } else { + values = [] + } + + return ( + concatenate(statement), + values + ) + } + + open func select() -> (String, [Node]) { + var statement: [String] = [] + var values: [Node] = [] + + let table = escape(E.entity) + statement += "SELECT \(table).* FROM" + statement += table + + if !query.joins.isEmpty { + statement += joins(query.joins) + } + statement += query.raws.joins + + if !query.filters.isEmpty { + let (filtersClause, filtersValues) = filters(query.filters) + statement += filtersClause + values += filtersValues + } + + if !query.sorts.isEmpty { + statement += sorts(query.sorts) + } + + if let l = query.limit { + statement += limit(l) + } + + return ( + concatenate(statement), + values + ) + } + + open func count() -> (String, [Node]) { + var fragments: [String] = [] + var values: [Node] = [] + + fragments += "SELECT COUNT(*) as _fluent_count FROM" + fragments += escape(E.entity) + + if !query.joins.isEmpty { + fragments += joins(query.joins) + } + + if !query.filters.isEmpty { + let (filtersClause, filtersValues) = filters(query.filters) + fragments += filtersClause + values += filtersValues + } + + return ( + concatenate(fragments), + values + ) + } + + open func delete() -> (String, [Node]) { + var fragments: [String] = [] + var values: [Node] = [] + + fragments += "DELETE FROM" + fragments += escape(E.entity) + + if !query.filters.isEmpty { + let (filtersClause, filtersValues) = filters(query.filters) + fragments += filtersClause + values += filtersValues + } + + if let l = query.limit { + fragments += limit(l) + } + + return ( + concatenate(fragments), + values + ) + } + + open func modify() -> (String, [Node]) { + var statement: [String] = [] + + var values: [Node] = [] + + statement += "UPDATE" + statement += escape(E.entity) + statement += "SET" + + if let data = query.data, let obj = data.typeObject { + var fragments: [String] = [] + + obj.forEach { (key, value) in + fragments += escape(key) + " = " + placeholder(value) + } + + statement += fragments.joined(separator: ", ") + values += Array(obj.values) + } + + let (filterclause, filterValues) = filters(query.filters) + statement += filterclause + values += filterValues + + return ( + concatenate(statement), + values + ) + } + + // MARK: Schema + + + open func create(_ add: [Field]) -> (String, [Node]) { + var statement: [String] = [] + + statement += "CREATE TABLE" + statement += escape(E.entity) + statement += columns(add) + + return ( + concatenate(statement), + [] + ) + } + + open func alter(add: [Field], drop: [Field]) -> (String, [Node]) { + var statement: [String] = [] + + statement += "ALTER TABLE" + statement += escape(E.entity) + + var subclause: [String] = [] + + for field in add { + subclause += "ADD " + column(field) + } + + for field in drop { + subclause += "DROP " + escape(field.name) + } + + statement += subclause.joined(separator: ", ") + + return ( + concatenate(statement), + [] + ) + } + + open func drop() -> (String, [Node]) { + var statement: [String] = [] + + statement += "DROP TABLE IF EXISTS" + statement += escape(E.entity) + + return ( + concatenate(statement), + [] + ) + } + + open func columns(_ fields: [Field]) -> String { + let string = fields.map { field in + return column(field) + }.joined(separator: ", ") + + return "(\(string))" + } + + open func column(_ field: Field) -> String { + var clause: [String] = [] + + clause += escape(field.name) + clause += type(field.type, primaryKey: field.primaryKey) + + if !field.optional { + clause += "NOT NULL" + } + + if field.unique { + clause += "UNIQUE" + } + + if let d = field.default { + let dc: String + + switch d.wrapped { + case .number(let n): + dc = "'" + n.description + "'" + case .null: + dc = "NULL" + case .bool(let b): + dc = b ? "TRUE" : "FALSE" + default: + dc = "'" + (d.string ?? "") + "'" + } + + clause += "DEFAULT \(dc)" + } + + return clause.joined(separator: " ") + } + + + open func type(_ type: Field.DataType, primaryKey: Bool) -> String { + switch type { + case .id(let type): + let typeString: String + switch type { + case .int: + typeString = "INTEGER" + case .uuid: + typeString = "STRING" + case .custom(let dataType): + typeString = dataType + } + if primaryKey { + return typeString + " PRIMARY KEY" + } else { + return typeString + } + case .int: + return "INTEGER" + case .string(_): + return "STRING" + case .double: + return "DOUBLE" + case .bool: + return "BOOL" + case .bytes: + return "BLOB" + case .date: + return "TIMESTAMP" + case .custom(let type): + return type + } + } + + // MARK: Query Types + + open func limit(_ limit: Limit) -> String { + var statement: [String] = [] + + statement += "LIMIT" + statement += "\(limit.offset), \(limit.count)" + + return statement.joined(separator: " ") + } + + + open func filters(_ f: [Filter]) -> (String, [Node]) { + var fragments: [String] = [] + + fragments += "WHERE" + + let (clause, values) = filters(f, .and) + + fragments += clause + + return ( + concatenate(fragments), + values + ) + } + + open func filters(_ filters: [Filter], _ r: Filter.Relation) -> (String, [Node]) { + var fragments: [String] = [] + var values: [Node] = [] + + + var subFragments: [String] = [] + + for f in filters { + let (clause, subValues) = filter(f) + subFragments += clause + values += subValues + } + + fragments += subFragments.joined(separator: " \(relation(r)) ") + + return ( + concatenate(fragments), + values + ) + } + + open func relation(_ relation: Filter.Relation) -> String { + let word: String + switch relation { + case .and: + word = "AND" + case .or: + word = "OR" + } + return word + } + + open func filter(_ filter: Filter) -> (String, [Node]) { + var statement: [String] = [] + var values: [Node] = [] + + switch filter.method { + case .compare(let key, let c, let value): + // `.null` needs special handling in the case of `.equals` or `.notEquals`. + if c == .equals && value == .null { + statement += escape(filter.entity.entity) + "." + escape(key) + " IS NULL" + } + else if c == .notEquals && value == .null { + statement += escape(filter.entity.entity) + "." + escape(key) + " IS NOT NULL" + } + else { + statement += escape(filter.entity.entity) + "." + escape(key) + statement += comparison(c) + statement += "?" + + /// `.like` comparison operator requires additional + /// processing of `value` + switch c { + case .hasPrefix: + values += hasPrefix(value) + case .hasSuffix: + values += hasSuffix(value) + case .contains: + values += contains(value) + default: + values += value + } + } + case .subset(let key, let s, let subValues): + statement += escape(filter.entity.entity) + "." + escape(key) + statement += scope(s) + statement += placeholders(subValues) + values += subValues + case .group(let relation, let f): + let (clause, subvals) = filters(f, relation) + statement += "(\(clause))" + values += subvals + } + + return ( + concatenate(statement), + values + ) + } + + open func sorts(_ sorts: [Sort]) -> String { + var clause: [String] = [] + + clause += "ORDER BY" + + clause += sorts + .map(sort) + .joined(separator: ", ") + + return clause.joined(separator: " ") + } + + open func sort(_ sort: Sort) -> String { + var clause: [String] = [] + + clause += escape(sort.entity.entity) + "." + escape(sort.field) + + switch sort.direction { + case .ascending: + clause += "ASC" + case .descending: + clause += "DESC" + } + + return clause.joined(separator: " ") + } + + open func comparison(_ comparison: Filter.Comparison) -> String { + switch comparison { + case .equals: + return "=" + case .greaterThan: + return ">" + case .greaterThanOrEquals: + return ">=" + case .lessThan: + return "<" + case .lessThanOrEquals: + return "<=" + case .notEquals: + return "!=" + case .hasSuffix: + fallthrough + case .hasPrefix: + fallthrough + case .contains: + return "LIKE" + case .custom(let string): + return string + } + } + + open func hasPrefix(_ value: Node) -> Node { + guard let string = value.string else { + return value + } + + return .string("\(string)%") + } + + open func hasSuffix(_ value: Node) -> Node { + guard let string = value.string else { + return value + } + + return .string("%\(string)") + } + + open func contains(_ value: Node) -> Node { + guard let string = value.string else { + return value + } + + return .string("%\(string)%") + } + + open func scope(_ scope: Filter.Scope) -> String { + switch scope { + case .in: + return "IN" + case .notIn: + return "NOT IN" + } + } + + open func joins(_ joins: [Join]) -> String { + var fragments: [String] = [] + + for j in joins { + fragments += join(j) + } + + return concatenate(fragments) + } + + open func join(_ join: Join) -> String { + var fragments: [String] = [] + + fragments += "JOIN" + fragments += escape(join.joined.entity) + fragments += "ON" + + fragments += "\(escape(join.base.entity)).\(escape(join.baseKey))" + fragments += "=" + fragments += "\(escape(join.joined.entity)).\(escape(join.joinedKey))" + + return concatenate(fragments) + } + + // MARK: Convenience + + open func concatenate(_ fragments: [String]) -> String { + return fragments.joined(separator: " ") + } + + open func keys(_ keys: [String]) -> String { + return list(keys.map { escape($0) }) + } + + open func list(_ list: [String]) -> String { + let string = list.joined(separator: ", ") + return "(\(string))" + } + + open func placeholders(_ values: [Node]) -> String { + let string = values.map { value in + return placeholder(value) + }.joined(separator: ", ") + return "(\(string))" + } + + open func placeholder(_ value: Node) -> String { + return "?" + } + + open func escape(_ string: String) -> String { + return "`\(string)`" + } +} + +public func +=(lhs: inout [String], rhs: String) { + lhs.append(rhs) +} + +public func +=(lhs: inout [Node], rhs: Node) { + lhs.append(rhs) +} diff --git a/Sources/Fluent/SQL/SQLSerializer.swift b/Sources/Fluent/SQL/SQLSerializer.swift index cc473779..fce80bc6 100644 --- a/Sources/Fluent/SQL/SQLSerializer.swift +++ b/Sources/Fluent/SQL/SQLSerializer.swift @@ -1,5 +1,6 @@ /// A SQL serializer. public protocol SQLSerializer { - init(sql: SQL) - func serialize() -> (String, [Node]) + associatedtype E: Entity + init(_ query: Query) + func serialize() throws -> (String, [Node]) } diff --git a/Sources/Fluent/SQLite/SQLiteDriver.swift b/Sources/Fluent/SQLite/SQLiteDriver.swift index cb73eb48..801877e0 100755 --- a/Sources/Fluent/SQLite/SQLiteDriver.swift +++ b/Sources/Fluent/SQLite/SQLiteDriver.swift @@ -52,8 +52,8 @@ extension SQLiteDriverProtocol { /// Executes the query. @discardableResult - public func query(_ query: Query) throws -> Node { - let serializer = SQLiteSerializer(sql: query.sql) + public func query(_ query: Query) throws -> Node { + let serializer = SQLiteSerializer(query) let (statement, values) = serializer.serialize() let results = try database.execute(statement) { statement in try self.bind(statement: statement, to: values) @@ -66,12 +66,6 @@ extension SQLiteDriverProtocol { } } - public func schema(_ schema: Schema) throws { - let serializer = SQLiteSerializer(sql: schema.sql) - let (statement, values) = serializer.serialize() - try _ = raw(statement, values) - } - /// Executes a raw query with an /// optional array of paramterized /// values and returns the results. diff --git a/Sources/Fluent/SQLite/SQLiteSerializer.swift b/Sources/Fluent/SQLite/SQLiteSerializer.swift index fd0b4005..4b0300e2 100755 --- a/Sources/Fluent/SQLite/SQLiteSerializer.swift +++ b/Sources/Fluent/SQLite/SQLiteSerializer.swift @@ -1,9 +1,9 @@ import SQLite /// SQLite-specific overrides for the GeneralSQLSerializer -public class SQLiteSerializer: GeneralSQLSerializer { +public class SQLiteSerializer: GeneralSQLSerializer { /// Serializes a SQLite data type. - public override func sql(_ type: Schema.Field.DataType, primaryKey: Bool) -> String { + public override func type(_ type: Field.DataType, primaryKey: Bool) -> String { // SQLite has a design where any data type that does not contain `TEXT`, // `CLOB`, or `CHAR` will be treated with `NUMERIC` affinity. // All SQLite `STRING` fields should instead be declared with `TEXT`. diff --git a/Sources/Fluent/Schema/Builder.swift b/Sources/Fluent/Schema/Builder.swift new file mode 100644 index 00000000..15ecef7d --- /dev/null +++ b/Sources/Fluent/Schema/Builder.swift @@ -0,0 +1,170 @@ +public protocol Builder: class { + var fields: [Field] { get set } +} + +extension Builder { + public func id(for entityType: E.Type) { + let field = Field( + name: E.idKey, + type: .id(type: E.idType), + primaryKey: true + ) + fields.append(field) + } + + public func foreignId(for entityType: E.Type) { + let field = Field( + name: E.foreignIdKey, + type: .id(type: E.idType) + ) + fields.append(field) + } + + public func int( + _ name: String, + optional: Bool = false, + unique: Bool = false, + default: NodeRepresentable? = nil + ) { + let field = Field( + name: name, + type: .int, + optional: optional, + unique: unique, + default: `default` + ) + fields.append(field) + } + + public func string( + _ name: String, + length: Int? = nil, + optional: Bool = false, + unique: Bool = false, + default: NodeRepresentable? = nil + ) { + let field = Field( + name: name, + type: .string(length: length), + optional: optional, + unique: unique, + default: `default` + ) + fields.append(field) + } + + public func double( + _ name: String, + optional: Bool = false, + unique: Bool = false, + default: NodeRepresentable? = nil + ) { + let field = Field( + name: name, + type: .double, + optional: optional, + unique: unique, + default: `default` + ) + fields.append(field) + } + + public func bool( + _ name: String, + optional: Bool = false, + unique: Bool = false, + default: NodeRepresentable? = nil + ) { + let field = Field( + name: name, + type: .bool, + optional: optional, + unique: unique, + default: `default` + ) + fields.append(field) + } + + public func bytes( + _ name: String, + optional: Bool = false, + unique: Bool = false, + default: NodeRepresentable? = nil + ) { + let field = Field( + name: name, + type: .bytes, + optional: optional, + unique: unique, + default: `default` + ) + fields.append(field) + } + + public func date( + _ name: String, + optional: Bool = false, + unique: Bool = false, + default: NodeRepresentable? = nil + ) { + let field = Field( + name: name, + type: .date, + optional: optional, + unique: unique, + default: `default` + ) + fields.append(field) + } + + public func custom( + _ name: String, + type: String, + optional: Bool = false, + unique: Bool = false, + default: NodeRepresentable? = nil + ) { + let field = Field( + name: name, + type: .custom(type: type), + optional: optional, + unique: unique, + default: `default` + ) + fields.append(field) + } + + // MARK: Relations + + public func parent( + _ entity: E.Type = E.self, + optional: Bool = false, + unique: Bool = false, + default: NodeRepresentable? = nil + ) { + parent( + idKey: E.idKey, + idType: E.idType, + optional: optional, + unique: unique, + default: `default` + ) + } + + public func parent( + idKey: String, + idType: IdentifierType, + optional: Bool = false, + unique: Bool = false, + default: NodeRepresentable? = nil + ) { + let field = Field( + name: idKey, + type: .id(type: idType), + optional: optional, + unique: unique, + default: `default` + ) + fields.append(field) + } +} diff --git a/Sources/Fluent/Schema/Creator.swift b/Sources/Fluent/Schema/Creator.swift new file mode 100644 index 00000000..895a365e --- /dev/null +++ b/Sources/Fluent/Schema/Creator.swift @@ -0,0 +1,6 @@ +public final class Creator: Builder { + public var fields: [Field] + public init() { + fields = [] + } +} diff --git a/Sources/Fluent/Schema/Database+Schema.swift b/Sources/Fluent/Schema/Database+Schema.swift index 0e8083bf..598c9c4e 100644 --- a/Sources/Fluent/Schema/Database+Schema.swift +++ b/Sources/Fluent/Schema/Database+Schema.swift @@ -1,35 +1,10 @@ -extension Database { - /// Modifies the schema of the database - /// for the given entity. - public func modify(custom entity: String, closure: (Schema.Modifier) throws -> ()) throws { - let modifier = Schema.Modifier(entity) - try closure(modifier) - _ = try schema(modifier.schema) - } - - /// Creates the schema of the database - /// for the given entity. - public func create(custom entity: String, closure: (Schema.Creator) throws -> ()) throws { - let creator = Schema.Creator(entity) - try closure(creator) - _ = try schema(creator.schema) - } - - /// Deletes the schema of the database - /// for the given entity. - public func delete(custom entity: String) throws { - let schema = Schema.delete(entity: entity) - _ = try self.schema(schema) - } -} - extension Database { /// Creates the schema of the database /// for the given entity. - public func create(_ e: E.Type, closure: (Schema.Creator) throws -> ()) throws { + public func create(_ e: E.Type, closure: (Creator) throws -> ()) throws { if e.database == nil { e.database = self } - let creator = Schema.Creator(e.entity) + let creator = Creator() // add timestamps if E.usesTimestamps { @@ -38,17 +13,26 @@ extension Database { } try closure(creator) - _ = try schema(creator.schema) + + let query = Query(self) + query.action = .schema(.create(creator.fields)) + try self.query(query) } /// Modifies the schema of the database /// for the given entity. - public func modify(_ e: E.Type, closure: (Schema.Modifier) throws -> ()) throws { + public func modify(_ e: E.Type, closure: (Modifier) throws -> ()) throws { if e.database == nil { e.database = self } - let modifier = Schema.Modifier(e.entity) + let modifier = Modifier() try closure(modifier) - _ = try schema(modifier.schema) + + let query = Query(self) + query.action = .schema(.modify( + add: modifier.fields, + remove: modifier.delete + )) + try self.query(query) } /// Deletes the schema of the database @@ -56,7 +40,8 @@ extension Database { public func delete(_ e: E.Type) throws { if e.database == nil { e.database = self } - let schema = Schema.delete(entity: e.entity) - _ = try self.schema(schema) + let query = Query(self) + query.action = .schema(.delete) + try self.query(query) } } diff --git a/Sources/Fluent/Schema/Field.swift b/Sources/Fluent/Schema/Field.swift new file mode 100644 index 00000000..67fd66d9 --- /dev/null +++ b/Sources/Fluent/Schema/Field.swift @@ -0,0 +1,144 @@ + +/// Various types of fields +/// that can be used in a Schema. +public struct Field { + public let name: String + public let type: DataType + public let optional: Bool + public let unique: Bool + public let `default`: Node? + public let primaryKey: Bool + + public enum DataType { + case id(type: IdentifierType) + case int + case string(length: Int?) + case double + case bool + case bytes + case date + case custom(type: String) + } + + public init( + name: String, + type: DataType, + optional: Bool = false, + unique: Bool = false, + default: Node? = nil, + primaryKey: Bool = false + ) { + self.name = name + self.type = type + self.optional = optional + self.unique = unique + self.default = `default` + self.primaryKey = primaryKey + } + + public init( + name: String, + type: DataType, + optional: Bool = false, + unique: Bool = false, + default: NodeRepresentable? = nil, + primaryKey: Bool = false + ) { + let node: Node? + + if let d = `default` { + node = try? d.makeNode(in: rowContext) + } else { + node = nil + } + + self.init( + name: name, + type: type, + optional: optional, + unique: unique, + default: node, + primaryKey: primaryKey + ) + } +} + +extension Field: Equatable { + public static func ==(lhs: Field, rhs: Field) -> Bool { + return lhs.name == rhs.name + && lhs.type == rhs.type + && lhs.optional == rhs.optional + && lhs.unique == rhs.unique + && lhs.default == rhs.default + && lhs.primaryKey == rhs.primaryKey + } +} + +extension Field.DataType: Equatable { + public static func ==(lhs: Field.DataType, rhs: Field.DataType) -> Bool { + switch lhs { + case .id(let a): + switch rhs { + case .id(let b): return a == b + default: return false + } + case .int: + switch rhs { + case .int: return true + default: return false + } + case .string: + switch rhs { + case .string: return true + default: return false + } + case .double: + switch rhs { + case .double: return true + default: return false + } + case .bool: + switch rhs { + case .bool: return true + default: return false + } + case .bytes: + switch rhs { + case .bytes: return true + default: return false + } + case .date: + switch rhs { + case .date: return true + default: return false + } + case .custom(let a): + switch rhs { + case .custom(let b): return a == b + default: return false + } + } + } +} + +extension IdentifierType: Equatable { + public static func ==(lhs: IdentifierType, rhs: IdentifierType) -> Bool { + switch lhs { + case .int: + switch rhs { + case .int: return true + default: return false + } + case .uuid: + switch rhs { + case .uuid: return true + default: return false + } + case .custom(let a): + switch rhs { + case .custom(let b): return a == b + default: return false + } + } + } +} diff --git a/Sources/Fluent/Schema/Modifier.swift b/Sources/Fluent/Schema/Modifier.swift new file mode 100644 index 00000000..09e2eeb9 --- /dev/null +++ b/Sources/Fluent/Schema/Modifier.swift @@ -0,0 +1,19 @@ +/// Modifies a schema. A subclass of Creator. +/// Can modify or delete fields. +public final class Modifier: Builder { + public var fields: [Field] + public var delete: [Field] + + public init() { + fields = [] + delete = [] + } + + public func delete(_ name: String) { + let field = Field( + name: name, + type: .custom(type: "delete") + ) + delete.append(field) + } +} diff --git a/Sources/Fluent/Schema/Schema+Creator.swift b/Sources/Fluent/Schema/Schema+Creator.swift deleted file mode 100644 index 62f8e66e..00000000 --- a/Sources/Fluent/Schema/Schema+Creator.swift +++ /dev/null @@ -1,175 +0,0 @@ -extension Schema { - /// Creates schema. - /// Cannot delete or modify fields. - public class Creator { - public let entity: String - public var fields: [Field] - - public init(_ entity: String) { - self.entity = entity - fields = [] - } - - public func id(for entityType: E.Type) { - fields += Field( - name: E.idKey, - type: .id(type: E.idType), - primaryKey: true - ) - } - - public func foreignId(for entityType: E.Type) { - fields += Field( - name: E.foreignIdKey, - type: .id(type: E.idType) - ) - } - - public func int( - _ name: String, - optional: Bool = false, - unique: Bool = false, - default: NodeRepresentable? = nil - ) { - fields += Field( - name: name, - type: .int, - optional: optional, - unique: unique, - default: `default` - ) - } - - public func string( - _ name: String, - length: Int? = nil, - optional: Bool = false, - unique: Bool = false, - default: NodeRepresentable? = nil - ) { - fields += Field( - name: name, - type: .string(length: length), - optional: optional, - unique: unique, - default: `default` - ) - } - - public func double( - _ name: String, - optional: Bool = false, - unique: Bool = false, - default: NodeRepresentable? = nil - ) { - fields += Field( - name: name, - type: .double, - optional: optional, - unique: unique, - default: `default` - ) - } - - public func bool( - _ name: String, - optional: Bool = false, - unique: Bool = false, - default: NodeRepresentable? = nil - ) { - fields += Field( - name: name, - type: .bool, - optional: optional, - unique: unique, - default: `default` - ) - } - - public func bytes( - _ name: String, - optional: Bool = false, - unique: Bool = false, - default: NodeRepresentable? = nil - ) { - fields += Field( - name: name, - type: .bytes, - optional: optional, - unique: unique, - default: `default` - ) - } - - public func date( - _ name: String, - optional: Bool = false, - unique: Bool = false, - default: NodeRepresentable? = nil - ) { - fields += Field( - name: name, - type: .date, - optional: optional, - unique: unique, - default: `default` - ) - } - - public func custom( - _ name: String, - type: String, - optional: Bool = false, - unique: Bool = false, - default: NodeRepresentable? = nil - ) { - fields += Field( - name: name, - type: .custom(type: type), - optional: optional, - unique: unique, - default: `default` - ) - } - - public var schema: Schema { - return .create(entity: entity, create: fields) - } - - // MARK: Relations - public func parent( - _ entity: E.Type = E.self, - optional: Bool = false, - unique: Bool = false, - default: NodeRepresentable? = nil - ) { - parent( - idKey: E.idKey, - idType: E.idType, - optional: optional, - unique: unique, - default: `default` - ) - } - - public func parent( - idKey: String, - idType: IdentifierType, - optional: Bool = false, - unique: Bool = false, - default: NodeRepresentable? = nil - ) { - fields += Field( - name: idKey, - type: .id(type: idType), - optional: optional, - unique: unique, - default: `default` - ) - } - } -} - -func +=(lhs: inout [Schema.Field], rhs: Schema.Field) { - lhs.append(rhs) -} diff --git a/Sources/Fluent/Schema/Schema+Field.swift b/Sources/Fluent/Schema/Schema+Field.swift deleted file mode 100644 index 046202bc..00000000 --- a/Sources/Fluent/Schema/Schema+Field.swift +++ /dev/null @@ -1,65 +0,0 @@ -extension Schema { - /// Various types of fields - /// that can be used in a Schema. - public struct Field { - public let name: String - public let type: DataType - public let optional: Bool - public let unique: Bool - public let `default`: Node? - public let primaryKey: Bool - - public enum DataType { - case id(type: IdentifierType) - case int - case string(length: Int?) - case double - case bool - case bytes - case date - case custom(type: String) - } - - public init( - name: String, - type: DataType, - optional: Bool = false, - unique: Bool = false, - default: Node? = nil, - primaryKey: Bool = false - ) { - self.name = name - self.type = type - self.optional = optional - self.unique = unique - self.default = `default` - self.primaryKey = primaryKey - } - - public init( - name: String, - type: DataType, - optional: Bool = false, - unique: Bool = false, - default: NodeRepresentable? = nil, - primaryKey: Bool = false - ) { - let node: Node? - - if let d = `default` { - node = try? d.makeNode(in: rowContext) - } else { - node = nil - } - - self.init( - name: name, - type: type, - optional: optional, - unique: unique, - default: node, - primaryKey: primaryKey - ) - } - } -} diff --git a/Sources/Fluent/Schema/Schema+Modifier.swift b/Sources/Fluent/Schema/Schema+Modifier.swift deleted file mode 100644 index 67b67907..00000000 --- a/Sources/Fluent/Schema/Schema+Modifier.swift +++ /dev/null @@ -1,21 +0,0 @@ -extension Schema { - /// Modifies a schema. A subclass of Creator. - /// Can modify or delete fields. - public class Modifier: Creator { - public var delete: [String] - - public override init(_ entity: String) { - delete = [] - super.init(entity) - } - - public func delete(_ name: String) { - delete.append(name) - - } - - public override var schema: Schema { - return .modify(entity: entity, create: fields, delete: delete) - } - } -} diff --git a/Sources/Fluent/Schema/Schema.swift b/Sources/Fluent/Schema/Schema.swift deleted file mode 100644 index 1d42ca99..00000000 --- a/Sources/Fluent/Schema/Schema.swift +++ /dev/null @@ -1,7 +0,0 @@ -/// Represents an action on the -/// Schema of a collection. -public enum Schema { - case create(entity: String, create: [Field]) - case modify(entity: String, create: [Field], delete: [String]) - case delete(entity: String) -} diff --git a/Sources/FluentTester/Tester+InsertAndFind.swift b/Sources/FluentTester/Tester+InsertAndFind.swift index 7b549d78..17b5a786 100644 --- a/Sources/FluentTester/Tester+InsertAndFind.swift +++ b/Sources/FluentTester/Tester+InsertAndFind.swift @@ -3,7 +3,7 @@ extension Tester { Atom.database = database try Atom.prepare(database) defer { - try? Atom.revert(database) + try! Atom.revert(database) } let hydrogen = Atom(id: nil, name: "Hydrogen", protons: 1, weight: 1.007) diff --git a/Sources/FluentTester/Tester+Paginate.swift b/Sources/FluentTester/Tester+Paginate.swift index 68ec2f40..44d20698 100644 --- a/Sources/FluentTester/Tester+Paginate.swift +++ b/Sources/FluentTester/Tester+Paginate.swift @@ -3,7 +3,7 @@ extension Tester { Compound.database = database try Compound.prepare(database) defer { - try? Compound.revert(database) + try! Compound.revert(database) } let ethanol = Compound(name: "Ethanol") diff --git a/Sources/FluentTester/Tester+PivotsAndRelations.swift b/Sources/FluentTester/Tester+PivotsAndRelations.swift index ed683567..780976df 100644 --- a/Sources/FluentTester/Tester+PivotsAndRelations.swift +++ b/Sources/FluentTester/Tester+PivotsAndRelations.swift @@ -1,30 +1,30 @@ extension Tester { - func setup() { + func setup() throws { Atom.database = database - try! Atom.prepare(database) + try Atom.prepare(database) Compound.database = database - try! Compound.prepare(database) + try Compound.prepare(database) Student.database = database - try! Student.prepare(database) + try Student.prepare(database) Pivot.database = database - try! Pivot.prepare(database) + try Pivot.prepare(database) Pivot, Student>.database = database - try! Pivot, Student>.prepare(database) + try Pivot, Student>.prepare(database) } - func teardown() { - try! Atom.revert(database) - try! Compound.revert(database) - try! Student.revert(database) + func teardown() throws { + try Atom.revert(database) + try Compound.revert(database) + try Student.revert(database) - try! Pivot.revert(database) - try! Pivot, Student>.revert(database) + try Pivot.revert(database) + try Pivot, Student>.revert(database) } public func testPivotsAndRelations() throws { - setup() - defer { teardown() } + try setup() + defer { try! teardown() } let hydrogen = Atom(id: nil, name: "Hydrogen", protons: 1, weight: 1.007) try hydrogen.save() @@ -61,8 +61,8 @@ extension Tester { public func testDoublePivot() throws { - setup() - defer { teardown() } + try setup() + defer { try? teardown() } let hydrogen = Atom(id: nil, name: "Hydrogen", protons: 1, weight: 1.007) try hydrogen.save() diff --git a/Sources/FluentTester/Tester+Schema.swift b/Sources/FluentTester/Tester+Schema.swift index e664a823..14dfe504 100644 --- a/Sources/FluentTester/Tester+Schema.swift +++ b/Sources/FluentTester/Tester+Schema.swift @@ -3,7 +3,7 @@ extension Tester { Student.database = database try Student.prepare(database) defer { - try? Student.revert(database) + try! Student.revert(database) } let bob = Student( diff --git a/Sources/FluentTester/Tester+Timestamps.swift b/Sources/FluentTester/Tester+Timestamps.swift index 2c70ff3c..a0ea20f4 100644 --- a/Sources/FluentTester/Tester+Timestamps.swift +++ b/Sources/FluentTester/Tester+Timestamps.swift @@ -5,7 +5,7 @@ extension Tester { Compound.database = database try Compound.prepare(database) defer { - try? Compound.revert(database) + try! Compound.revert(database) } let ethanol = Compound(name: "Ethanol") diff --git a/Tests/FluentTests/JoinTests.swift b/Tests/FluentTests/JoinTests.swift index 08c6b294..7e6e171e 100644 --- a/Tests/FluentTests/JoinTests.swift +++ b/Tests/FluentTests/JoinTests.swift @@ -4,8 +4,6 @@ import XCTest class JoinTests: XCTestCase { static let allTests = [ ("testBasic", testBasic), - ("testCustom", testCustom), - ("testSQL", testSQL), ("testSQLFilters", testSQLFilters), ("testSiblings", testSiblings) ] @@ -26,65 +24,8 @@ class JoinTests: XCTestCase { let query = try Query(db).join(Compound.self) try lqd.query(query) - if let sql = lqd.lastQuery { - switch sql { - case .select(let table, let filters, let joins, let orders, let limit): - XCTAssertEqual(table, "atoms") - XCTAssertEqual(filters.count, 0) - XCTAssertEqual(orders.count, 0) - XCTAssertEqual(joins.count, 1) - if let join = joins.first { - XCTAssert(join.base == Atom.self) - XCTAssertEqual(join.joined.foreignIdKey, "compound_\(lqd.idKey)") - XCTAssert(join.joined == Compound.self) - XCTAssertEqual(join.joined.idKey, lqd.idKey) - } - XCTAssert(limit == nil) - default: - XCTFail("Invalid SQL type.") - } - } else { - XCTFail("No last query.") - } - } - - func testCustom() throws { - let query = try Query(db).join(Compound.self) - try lqd.query(query) - - - if let sql = lqd.lastQuery { - switch sql { - case .select(let table, let filters, let joins, let orders, let limit): - XCTAssertEqual(table, "atoms") - XCTAssertEqual(filters.count, 0) - XCTAssertEqual(orders.count, 0) - XCTAssertEqual(joins.count, 1) - if let join = joins.first { - XCTAssert(join.base == Atom.self) - XCTAssertEqual(join.joined.idKey, Compound.idKey) - XCTAssert(join.joined == Compound.self) - XCTAssertEqual(join.joined.foreignIdKey, Compound.foreignIdKey) - } - XCTAssert(limit == nil) - default: - XCTFail("Invalid SQL type.") - } - } else { - XCTFail("No last query.") - } - } - - func testSQL() throws { - let query = try Query(db).join(Compound.self) - try lqd.query(query) - - - if let sql = lqd.lastQuery { - let serializer = GeneralSQLSerializer(sql: sql) - let (statement, values) = serializer.serialize() - XCTAssertEqual(statement, "SELECT `atoms`.* FROM `atoms` JOIN `compounds` ON `atoms`.`#id` = `compounds`.`atom_#id`") - XCTAssertEqual(values.count, 0) + if let (sql, _) = lqd.lastQuery { + XCTAssertEqual(sql, "SELECT `atoms`.* FROM `atoms` JOIN `compounds` ON `atoms`.`#id` = `compounds`.`atom_#id`") } else { XCTFail("No last query.") } @@ -98,14 +39,8 @@ class JoinTests: XCTestCase { try lqd.query(query) - - if let sql = lqd.lastQuery { - let serializer = GeneralSQLSerializer(sql: sql) - let (statement, values) = serializer.serialize() - XCTAssertEqual( - statement, - "SELECT `atoms`.* FROM `atoms` JOIN `compounds` ON `atoms`.`#id` = `compounds`.`atom_#id` WHERE `atoms`.`protons` > ? AND `compounds`.`atoms` < ?" - ) + if let (sql, values) = lqd.lastQuery { + XCTAssertEqual(sql, "SELECT `atoms`.* FROM `atoms` JOIN `compounds` ON `atoms`.`#id` = `compounds`.`atom_#id` WHERE `atoms`.`protons` > ? AND `compounds`.`atoms` < ?") if values.count == 2 { XCTAssertEqual(values[0].int, 5) XCTAssertEqual(values[1].int, 128) @@ -126,11 +61,9 @@ class JoinTests: XCTestCase { _ = try atom.compounds.all() } - if let sql = lqd.lastQuery { - let serializer = GeneralSQLSerializer(sql: sql) - let (statement, values) = serializer.serialize() + if let (sql, values) = lqd.lastQuery { XCTAssertEqual( - statement, + sql, "SELECT `compounds`.* FROM `compounds` JOIN `atom_compound` ON `compounds`.`#id` = `atom_compound`.`compound_#id` WHERE `atom_compound`.`atom_#id` = ?" ) XCTAssertEqual(values.count, 1) diff --git a/Tests/FluentTests/ModelFindTests.swift b/Tests/FluentTests/ModelFindTests.swift index f93dba7b..21d30b35 100644 --- a/Tests/FluentTests/ModelFindTests.swift +++ b/Tests/FluentTests/ModelFindTests.swift @@ -48,7 +48,7 @@ class ModelFindTests: XCTestCase { self.driver = driver } - func query(_ query: Query) throws -> Node { + func query(_ query: Query) throws -> Node { if let filter = query.filters.first, case .compare(let key, let comparison, let value) = filter.method, @@ -69,10 +69,6 @@ class ModelFindTests: XCTestCase { return .array([]) } - func schema(_ builder: Schema) throws { - // - } - func raw(_ raw: String, _ values: [Node]) throws -> Node { return .null } diff --git a/Tests/FluentTests/ModelTests.swift b/Tests/FluentTests/ModelTests.swift index dd87f426..3ca098d5 100644 --- a/Tests/FluentTests/ModelTests.swift +++ b/Tests/FluentTests/ModelTests.swift @@ -25,14 +25,12 @@ class ModelTests: XCTestCase { XCTAssertTrue(atom.exists, "Model should exist after saving.") - let (sql, _) = GeneralSQLSerializer(sql: lqd.lastQuery!).serialize() + let (sql, _) = lqd.lastQuery! print(sql) atom.name = "bob" try atom.save() - let (_, _) = GeneralSQLSerializer(sql: lqd.lastQuery!).serialize() - try atom.delete() } @@ -41,17 +39,18 @@ class ModelTests: XCTestCase { let thing = StringIdentifiedThing() thing.id = "derp" - try! thing.save() - let saveQ = GeneralSQLSerializer(sql: lqd.lastQuery!).serialize() - XCTAssertEqual(saveQ.0, "INSERT INTO `string_identified_things` (`#id`) VALUES (?)") - XCTAssertEqual(saveQ.1, ["derp"]) + try thing.save() + if let (sql, values) = lqd.lastQuery { + XCTAssertEqual(sql, "INSERT INTO `string_identified_things` (`#id`) VALUES (?)") + XCTAssertEqual(values, ["derp"]) + } XCTAssertTrue(thing.exists) - _ = try! StringIdentifiedThing.find("derp") - let findQ = GeneralSQLSerializer(sql: lqd.lastQuery!).serialize() - - XCTAssertEqual(findQ.0, "SELECT `string_identified_things`.* FROM `string_identified_things` WHERE `string_identified_things`.`#id` = ? LIMIT 0, 1") - XCTAssertEqual(findQ.1, ["derp"]) + _ = try StringIdentifiedThing.find("derp") + if let (sql, values) = lqd.lastQuery { + XCTAssertEqual(sql, "SELECT `string_identified_things`.* FROM `string_identified_things` WHERE `string_identified_things`.`#id` = ? LIMIT 0, 1") + XCTAssertEqual(values, ["derp"]) + } } func testCustomIdentifiedThings() throws { @@ -60,18 +59,18 @@ class ModelTests: XCTestCase { let thing = CustomIdentifiedThing() thing.id = 123 - try! thing.save() - let saveQ = GeneralSQLSerializer(sql: lqd.lastQuery!).serialize() - XCTAssertEqual(saveQ.0, "INSERT INTO `custom_identified_things` (`#id`) VALUES (?)") - XCTAssertEqual(saveQ.1, [123]) + try thing.save() + if let (sql, values) = lqd.lastQuery { + XCTAssertEqual(sql, "INSERT INTO `custom_identified_things` (`#id`) VALUES (?)") + XCTAssertEqual(values, [123]) + + } XCTAssertTrue(thing.exists) _ = try CustomIdentifiedThing.find(123) - if let sql = lqd.lastQuery { - let findQ = GeneralSQLSerializer(sql: sql).serialize() - - XCTAssertEqual(findQ.0, "SELECT `custom_identified_things`.* FROM `custom_identified_things` WHERE `custom_identified_things`.`#id` = ? LIMIT 0, 1") - XCTAssertEqual(findQ.1, [123]) + if let (sql, values) = lqd.lastQuery { + XCTAssertEqual(sql, "SELECT `custom_identified_things`.* FROM `custom_identified_things` WHERE `custom_identified_things`.`#id` = ? LIMIT 0, 1") + XCTAssertEqual(values, [123]) } else { XCTFail("No last query") } diff --git a/Tests/FluentTests/PivotTests.swift b/Tests/FluentTests/PivotTests.swift index 61e1ae11..40f5142e 100644 --- a/Tests/FluentTests/PivotTests.swift +++ b/Tests/FluentTests/PivotTests.swift @@ -23,13 +23,11 @@ class PivotTests: XCTestCase { try atom.compounds.add(compound) - guard let query = lqd.lastQuery else { + guard let (sql, _) = lqd.lastQuery else { XCTFail("No query recorded") return } - let (sql, _) = GeneralSQLSerializer(sql: query).serialize() - XCTAssertEqual( sql, "INSERT INTO `atom_compound` (`\(Atom.foreignIdKey)`, `\(Compound.foreignIdKey)`) VALUES (?, ?)" diff --git a/Tests/FluentTests/PreparationTests.swift b/Tests/FluentTests/PreparationTests.swift index 893b31db..3a6d78d1 100644 --- a/Tests/FluentTests/PreparationTests.swift +++ b/Tests/FluentTests/PreparationTests.swift @@ -8,13 +8,11 @@ class PreparationTests: XCTestCase { func testManualPreparation() { let driver = TestSchemaDriver { schema in - guard case .create(let entity, let fields) = schema else { + guard case .create(let fields) = schema else { XCTFail("Invalid schema") return } - XCTAssertEqual(entity, "users") - guard fields.count == 3 else { XCTFail("Invalid field count") return @@ -43,7 +41,6 @@ class PreparationTests: XCTestCase { let database = Database(driver) - TestPreparation.entity = "users" TestPreparation.testClosure = { builder in builder.int("id") builder.string("name") @@ -59,13 +56,11 @@ class PreparationTests: XCTestCase { func testStringIdentifiedModelPreparation() { let driver = TestSchemaDriver { schema in - guard case .create(let entity, let fields) = schema else { + guard case .create(let fields) = schema else { XCTFail("Invalid schema") return } - XCTAssertEqual(entity, "string_identified_things") - guard fields.count == 1 else { XCTFail("Invalid field count") return @@ -93,13 +88,11 @@ class PreparationTests: XCTestCase { func testModelPreparation() { let driver = TestSchemaDriver { schema in - guard case .create(let entity, let fields) = schema else { + guard case .create(let fields) = schema else { XCTFail("Invalid schema") return } - XCTAssertEqual(entity, "test_models") - guard fields.count == 3 else { XCTFail("Invalid field count") return @@ -168,17 +161,16 @@ final class TestModel: Entity { } class TestPreparation: Preparation { - static var entity: String = "" - static var testClosure: (Schema.Creator) -> () = { _ in } + static var testClosure: (Creator) -> () = { _ in } static func prepare(_ database: Database) throws { - try database.create(custom: entity) { builder in + try database.create(Atom.self) { builder in self.testClosure(builder) } } static func revert(_ database: Database) throws { - try database.delete(custom: entity) + try database.delete(Atom.self) } } @@ -217,10 +209,3 @@ struct TestSchemaConnection: Connection { func raw(_ raw: String, _ values: [Node]) throws -> Node { return .null } } - -extension SQLSerializerTests { - private func serialize(_ sql: SQL) -> (String, [Node]) { - let serializer = GeneralSQLSerializer(sql: sql) - return serializer.serialize() - } -} diff --git a/Tests/FluentTests/RelationTests.swift b/Tests/FluentTests/RelationTests.swift index 71cb893a..09b6c528 100644 --- a/Tests/FluentTests/RelationTests.swift +++ b/Tests/FluentTests/RelationTests.swift @@ -11,7 +11,14 @@ class RelationTests: XCTestCase { var memory: MemoryDriver! var database: Database! - let ents = [Atom.self, Proton.self, Nucleus.self, Group.self] as [(Entity & Preparation).Type] + let ents = [ + Atom.self, + Proton.self, + Nucleus.self, + Group.self, + Compound.self, + Pivot.self + ] as [(Entity & Preparation).Type] override func setUp() { memory = try! MemoryDriver() @@ -73,7 +80,7 @@ class RelationTests: XCTestCase { do { let query = try hydrogen.children(type: Nucleus.self).makeQuery() - let (sql, _) = GeneralSQLSerializer(sql: query.sql).serialize() + let (sql, _) = GeneralSQLSerializer(query).serialize() print(sql) } catch { print(error) diff --git a/Tests/FluentTests/SQLSerializerTests.swift b/Tests/FluentTests/SQLSerializerTests.swift index 7cbd31f0..447062e8 100644 --- a/Tests/FluentTests/SQLSerializerTests.swift +++ b/Tests/FluentTests/SQLSerializerTests.swift @@ -7,32 +7,41 @@ class SQLSerializerTests: XCTestCase { ("testRegularSelect", testRegularSelect), ("testOffsetSelect", testOffsetSelect), ("testFilterCompareSelect", testFilterCompareSelect), - ("testFilterLikeSelect", testFilterLikeSelect), - ("testBasicCount", testBasicCount), - ("testRegularCount", testRegularCount), - ("testFilterCompareCount", testFilterCompareCount), - ("testFilterLikeCount", testFilterLikeCount), - ("testFilterEqualsNullSelect", testFilterEqualsNullSelect), - ("testFilterNotEqualsNullSelect", testFilterNotEqualsNullSelect), - ("testFilterCompareUpdate", testFilterCompareUpdate), - ("testFilterCompareDelete", testFilterCompareDelete), - ("testFilterGroup", testFilterGroup), - ("testSort", testSort), - ("testSortMultiple", testSortMultiple), +// ("testFilterLikeSelect", testFilterLikeSelect), +// ("testBasicCount", testBasicCount), +// ("testRegularCount", testRegularCount), +// ("testFilterCompareCount", testFilterCompareCount), +// ("testFilterLikeCount", testFilterLikeCount), +// ("testFilterEqualsNullSelect", testFilterEqualsNullSelect), +// ("testFilterNotEqualsNullSelect", testFilterNotEqualsNullSelect), +// ("testFilterCompareUpdate", testFilterCompareUpdate), +// ("testFilterCompareDelete", testFilterCompareDelete), +// ("testFilterGroup", testFilterGroup), +// ("testSort", testSort), +// ("testSortMultiple", testSortMultiple), ] + var db: Database! + + override func setUp() { + let lqd = LastQueryDriver() + db = Database(lqd) + } + func testBasicSelect() { - let sql = SQL.select(table: "users", filters: [], joins: [], orders: [], limit: nil) - let (statement, values) = serialize(sql) + let query = Query(db) + let (statement, values) = serialize(query) - XCTAssertEqual(statement, "SELECT `users`.* FROM `users`") + XCTAssertEqual(statement, "SELECT `atoms`.* FROM `atoms`") XCTAssert(values.isEmpty) } func testRegularSelect() { let filter = Filter(User.self, .compare("age", .greaterThanOrEquals, 21)) - let sql = SQL.select(table: "users", filters: [filter], joins: [], orders: [], limit: Limit(count: 5)) - let (statement, values) = serialize(sql) + let query = Query(db) + query.filters.append(filter) + query.limit = Limit(count: 5) + let (statement, values) = serialize(query) XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`age` >= ? LIMIT 0, 5") XCTAssertEqual(values.first?.int, 21) @@ -41,38 +50,42 @@ class SQLSerializerTests: XCTestCase { func testOffsetSelect() { let filter = Filter(User.self, .compare("age", .greaterThanOrEquals, 21)) - let sql = SQL.select(table: "users", filters: [filter], joins: [], orders: [], limit: Limit(count: 5, offset: 15)) - let (statement, _) = serialize(sql) + let query = Query(db) + query.filters.append(filter) + query.limit = Limit(count: 5, offset: 15) + let (statement, values) = serialize(query) XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`age` >= ? LIMIT 15, 5") + XCTAssertEqual(values.count, 1) } func testFilterCompareSelect() { let filter = Filter(User.self, .compare("name", .notEquals, "duck")) + let query = Query(db) + query.filters.append(filter) + let (statement, values) = serialize(query) - let select = SQL.select(table: "friends", filters: [filter], joins: [], orders: [], limit: nil) - let (statement, values) = serialize(select) - - XCTAssertEqual(statement, "SELECT `friends`.* FROM `friends` WHERE `users`.`name` != ?") + XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`name` != ?") XCTAssertEqual(values.first?.string, "duck") XCTAssertEqual(values.count, 1) } func testFilterLikeSelect() { let filter = Filter(User.self, .compare("name", .hasPrefix, "duc")) + let query = Query(db) + query.filters.append(filter) + let (statement, values) = serialize(query) - let select = SQL.select(table: "friends", filters: [filter], joins: [], orders: [], limit: nil) - let (statement, values) = serialize(select) - - XCTAssertEqual(statement, "SELECT `friends`.* FROM `friends` WHERE `users`.`name` LIKE ?") + XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`name` LIKE ?") XCTAssertEqual(values.first?.string, "duc%") XCTAssertEqual(values.count, 1) } - + func testBasicCount() { - let sql = SQL.count(table: "users", filters: [], joins: []) - let (statement, values) = serialize(sql) + let query = Query(db) + query.action = .count + let (statement, values) = serialize(query) XCTAssertEqual(statement, "SELECT COUNT(*) as _fluent_count FROM `users`") XCTAssert(values.isEmpty) @@ -80,8 +93,10 @@ class SQLSerializerTests: XCTestCase { func testRegularCount() { let filter = Filter(User.self, .compare("age", .greaterThanOrEquals, 21)) - let sql = SQL.count(table: "users", filters: [filter], joins: []) - let (statement, values) = serialize(sql) + let query = Query(db) + query.action = .count + query.filters.append(filter) + let (statement, values) = serialize(query) XCTAssertEqual(statement, "SELECT COUNT(*) as _fluent_count FROM `users` WHERE `users`.`age` >= ?") XCTAssertEqual(values.first?.int, 21) @@ -90,52 +105,57 @@ class SQLSerializerTests: XCTestCase { func testFilterCompareCount() { let filter = Filter(User.self, .compare("name", .notEquals, "duck")) + let query = Query(db) + query.action = .count + query.filters.append(filter) + let (statement, values) = serialize(query) - let select = SQL.count(table: "friends", filters: [filter], joins: []) - let (statement, values) = serialize(select) - - XCTAssertEqual(statement, "SELECT COUNT(*) as _fluent_count FROM `friends` WHERE `users`.`name` != ?") + XCTAssertEqual(statement, "SELECT COUNT(*) as _fluent_count FROM `users` WHERE `users`.`name` != ?") XCTAssertEqual(values.first?.string, "duck") XCTAssertEqual(values.count, 1) } func testFilterLikeCount() { let filter = Filter(User.self, .compare("name", .hasPrefix, "duc")) + let query = Query(db) + query.action = .count + query.filters.append(filter) + let (statement, values) = serialize(query) - let select = SQL.count(table: "friends", filters: [filter], joins: []) - let (statement, values) = serialize(select) - - XCTAssertEqual(statement, "SELECT COUNT(*) as _fluent_count FROM `friends` WHERE `users`.`name` LIKE ?") + XCTAssertEqual(statement, "SELECT COUNT(*) as _fluent_count FROM `users` WHERE `users`.`name` LIKE ?") XCTAssertEqual(values.first?.string, "duc%") XCTAssertEqual(values.count, 1) } func testFilterEqualsNullSelect() { let filter = Filter(User.self, .compare("name", .equals, Node.null)) - - let select = SQL.select(table: "friends", filters: [filter], joins: [], orders: [], limit: nil) - let (statement, values) = serialize(select) + let query = Query(db) + query.filters.append(filter) + let (statement, values) = serialize(query) - XCTAssertEqual(statement, "SELECT `friends`.* FROM `friends` WHERE `users`.`name` IS NULL") + XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`name` IS NULL") XCTAssertEqual(values.count, 0) } func testFilterNotEqualsNullSelect() { let filter = Filter(User.self, .compare("name", .notEquals, Node.null)) - - let select = SQL.select(table: "friends", filters: [filter], joins: [], orders: [], limit: nil) - let (statement, values) = serialize(select) + let query = Query(db) + query.filters.append(filter) + let (statement, values) = serialize(query) - XCTAssertEqual(statement, "SELECT `friends`.* FROM `friends` WHERE `users`.`name` IS NOT NULL") + XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`name` IS NOT NULL") XCTAssertEqual(values.count, 0) } func testFilterCompareUpdate() { let filter = Filter(User.self, .compare("name", .equals, "duck")) + let query = Query(db) + query.filters.append(filter) + query.data = ["not it": true] + query.action = .modify + let (statement, values) = serialize(query) - let update = SQL.update(table: "friends", filters: [filter], data: ["not it": true]) - let (statement, values) = serialize(update) - XCTAssertEqual(statement, "UPDATE `friends` SET `not it` = ? WHERE `users`.`name` = ?") + XCTAssertEqual(statement, "UPDATE `users` SET `not it` = ? WHERE `users`.`name` = ?") XCTAssertEqual(values.first?.bool, true) XCTAssertEqual(values.last?.string, "duck") XCTAssertEqual(values.count, 2) @@ -143,11 +163,13 @@ class SQLSerializerTests: XCTestCase { func testFilterCompareDelete() { let filter = Filter(User.self, .compare("name", .greaterThan, .string("duck"))) + let query = Query(db) + query.filters.append(filter) + query.action = .delete + let (statement, values) = serialize(query) - let delete = SQL.delete(table: "friends", filters: [filter], limit: nil) - let (statement, values) = serialize(delete) - XCTAssertEqual(statement, "DELETE FROM `friends` WHERE `users`.`name` > ?") + XCTAssertEqual(statement, "DELETE FROM `users` WHERE `users`.`name` > ?") XCTAssertEqual(values.first?.string, "duck") XCTAssertEqual(values.count, 1) } @@ -159,8 +181,11 @@ class SQLSerializerTests: XCTestCase { let four = Filter(User.self, .compare("4", .equals, .string("4"))) let group = Filter(User.self, .group(.or, [two, three])) - let select = SQL.select(table: "users", filters: [one, group, four], joins: [], orders: [], limit: nil) - let (statement, values) = serialize(select) + let query = Query(db) + query.filters.append(one) + query.filters.append(group) + query.filters.append(four) + let (statement, values) = serialize(query) XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`1` = ? AND (`users`.`2` = ? OR `users`.`3` = ?) AND `users`.`4` = ?") XCTAssertEqual(values.count, 4) @@ -169,8 +194,12 @@ class SQLSerializerTests: XCTestCase { func testSort() throws { let adult = Filter(User.self, .compare("age", .greaterThan, 17)) let name = Sort(User.self, "name", .ascending) - let select = SQL.select(table: "users", filters: [adult], joins: [], orders: [name], limit: nil) - let (statement, values) = serialize(select) + + let query = Query(db) + query.filters.append(adult) + query.sorts.append(name) + let (statement, values) = serialize(query) + XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`age` > ? ORDER BY `users`.`name` ASC") XCTAssertEqual(values.count, 1) } @@ -179,8 +208,12 @@ class SQLSerializerTests: XCTestCase { let adult = Filter(User.self, .compare("age", .greaterThan, 17)) let name = Sort(User.self, "name", .ascending) let email = Sort(User.self, "email", .descending) - let select = SQL.select(table: "users", filters: [adult], joins: [], orders: [name, email], limit: nil) - let (statement, values) = serialize(select) + + let query = Query(db) + query.filters.append(adult) + query.sorts += [name, email] + let (statement, values) = serialize(query) + XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`age` > ? ORDER BY `users`.`name` ASC, `users`.`email` DESC") XCTAssertEqual(values.count, 1) } @@ -189,8 +222,8 @@ class SQLSerializerTests: XCTestCase { // MARK: Utilities extension SQLSerializerTests { - fileprivate func serialize(_ sql: SQL) -> (String, [Node]) { - let serializer = GeneralSQLSerializer(sql: sql) + func serialize(_ query: Query) -> (String, [Node]) { + let serializer = GeneralSQLSerializer(query) return serializer.serialize() } } diff --git a/Tests/FluentTests/SchemaCreateTests.swift b/Tests/FluentTests/SchemaCreateTests.swift index 9cbcbf6d..03e078d0 100644 --- a/Tests/FluentTests/SchemaCreateTests.swift +++ b/Tests/FluentTests/SchemaCreateTests.swift @@ -9,31 +9,40 @@ class SchemaCreateTests: XCTestCase { ("testDelete", testDelete), ] + var db: Database! + + override func setUp() { + let lqd = LastQueryDriver() + db = Database(lqd) + } + func testCreate() throws { - let builder = Schema.Creator("users") + let builder = Creator() builder.int("id") builder.string("name") builder.string("email", length: 256) builder.custom("profile", type: "JSON") - let sql = builder.schema.sql - let serializer = GeneralSQLSerializer(sql: sql) + let query = Query(db) + query.action = .schema(.create(builder.fields)) + let serializer = GeneralSQLSerializer(query) let (statement, values) = serializer.serialize() - XCTAssertEqual(statement, "CREATE TABLE `users` (`id` INTEGER NOT NULL, `name` STRING NOT NULL, `email` STRING NOT NULL, `profile` JSON NOT NULL)") + XCTAssertEqual(statement, "CREATE TABLE `atoms` (`id` INTEGER NOT NULL, `name` STRING NOT NULL, `email` STRING NOT NULL, `profile` JSON NOT NULL)") XCTAssertEqual(values.count, 0) } func testStringIdentifiedEntity() throws { - let builder = Schema.Creator(StringIdentifiedThing.entity) + let builder = Creator() builder.id(for: StringIdentifiedThing.self) - - let sql = builder.schema.sql - let serializer = GeneralSQLSerializer(sql: sql) + + let query = Query(db) + query.action = .schema(.create(builder.fields)) + let serializer = GeneralSQLSerializer(query) let (statement, values) = serializer.serialize() @@ -43,12 +52,13 @@ class SchemaCreateTests: XCTestCase { func testCustomIdentifiedEntity() throws { - let builder = Schema.Creator(CustomIdentifiedThing.entity) + let builder = Creator() builder.id(for: CustomIdentifiedThing.self) - let sql = builder.schema.sql - let serializer = GeneralSQLSerializer(sql: sql) + let query = Query(db) + query.action = .schema(.create(builder.fields)) + let serializer = GeneralSQLSerializer(query) let (statement, values) = serializer.serialize() @@ -57,44 +67,46 @@ class SchemaCreateTests: XCTestCase { } func testStringDefault() throws { - let builder = Schema.Creator("table") + let builder = Creator() builder.string("string", default: "default") - let sql = builder.schema.sql - let serializer = GeneralSQLSerializer(sql: sql) + let query = Query(db) + query.action = .schema(.create(builder.fields)) + let serializer = GeneralSQLSerializer(query) let (statement, values) = serializer.serialize() - XCTAssertEqual(statement, "CREATE TABLE `table` (`string` STRING NOT NULL DEFAULT 'default')") + XCTAssertEqual(statement, "CREATE TABLE `atoms` (`string` STRING NOT NULL DEFAULT 'default')") XCTAssertEqual(values.count, 0) } func testModify() throws { - let builder = Schema.Modifier("users") + let builder = Modifier() builder.int("id") builder.string("name") builder.string("email", length: 256) builder.delete("age") - let sql = builder.schema.sql - let serializer = GeneralSQLSerializer(sql: sql) + let query = Query(db) + query.action = .schema(.modify(add: builder.fields, remove: builder.delete)) + let serializer = GeneralSQLSerializer(query) let (statement, values) = serializer.serialize() - XCTAssertEqual(statement, "ALTER TABLE `users` ADD `id` INTEGER NOT NULL, ADD `name` STRING NOT NULL, ADD `email` STRING NOT NULL, DROP `age`") + XCTAssertEqual(statement, "ALTER TABLE `atoms` ADD `id` INTEGER NOT NULL, ADD `name` STRING NOT NULL, ADD `email` STRING NOT NULL, DROP `age`") XCTAssertEqual(values.count, 0) } func testDelete() throws { - let schema = Schema.delete(entity: "users") - let sql = schema.sql - let serializer = GeneralSQLSerializer(sql: sql) + let query = Query(db) + query.action = .schema(.delete) + let serializer = GeneralSQLSerializer(query) let (statement, values) = serializer.serialize() - XCTAssertEqual(statement, "DROP TABLE IF EXISTS `users`") + XCTAssertEqual(statement, "DROP TABLE IF EXISTS `atoms`") XCTAssertEqual(values.count, 0) } } diff --git a/Tests/FluentTests/Utilities/Compound.swift b/Tests/FluentTests/Utilities/Compound.swift index a2e0c488..ae2cbc55 100644 --- a/Tests/FluentTests/Utilities/Compound.swift +++ b/Tests/FluentTests/Utilities/Compound.swift @@ -19,7 +19,9 @@ final class Compound: Entity { try row.set("name", name) return row } +} +extension Compound: Preparation { static func prepare(_ database: Fluent.Database) throws { try database.create(self) { builder in builder.id(for: self) diff --git a/Tests/FluentTests/Utilities/Dummy.swift b/Tests/FluentTests/Utilities/Dummy.swift index 3b377d4d..1ab9dea1 100644 --- a/Tests/FluentTests/Utilities/Dummy.swift +++ b/Tests/FluentTests/Utilities/Dummy.swift @@ -31,7 +31,7 @@ class DummyDriver: Driver { class DummyConnection: Connection { public var closed: Bool = false - func query(_ query: Query) throws -> Node { + func query(_ query: Query) throws -> Node { if query.action == .count { return 0 } @@ -39,10 +39,6 @@ class DummyConnection: Connection { return .array([]) } - func schema(_ schema: Schema) throws { - - } - func raw(_ raw: String, _ values: [Node]) throws -> Node { return .null } diff --git a/Tests/FluentTests/Utilities/LastQueryDriver.swift b/Tests/FluentTests/Utilities/LastQueryDriver.swift index 64789dff..ace6a0c5 100644 --- a/Tests/FluentTests/Utilities/LastQueryDriver.swift +++ b/Tests/FluentTests/Utilities/LastQueryDriver.swift @@ -5,8 +5,7 @@ class LastQueryDriver: Driver { var idType: IdentifierType = .int var idKey: String = "#id" - var lastQuery: SQL? - var lastSchema: Schema? + var lastQuery: (String, [Node])? var lastRaw: (String, [Node])? func makeConnection() throws -> Connection { @@ -24,16 +23,14 @@ struct LastQueryConnection: Connection { } @discardableResult - func query(_ query: Query) throws -> Node { - let sql = query.sql - driver.lastQuery = sql - return Node.array([ - Node.object([T.idKey: 5]) - ]) - } - - func schema(_ schema: Schema) throws { - driver.lastSchema = schema + func query(_ query: Query) throws -> Node { + let serializer = GeneralSQLSerializer(query) + driver.lastQuery = serializer.serialize() + return try Node(node: [ + [ + E.idKey: 5 + ] + ], in: nil) } func raw(_ raw: String, _ values: [Node]) throws -> Node { From ab921ecaa247f232bfc01770626e85fd6c2715e2 Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Wed, 15 Mar 2017 18:09:55 +0100 Subject: [PATCH 02/11] raw or wrapped --- Sources/Fluent/Query/Filter/Filter.swift | 2 +- .../Fluent/Query/Filter/Query+Filter.swift | 2 +- Sources/Fluent/Query/Query.swift | 8 +- Sources/Fluent/Query/QueryRepresentable.swift | 2 +- Sources/Fluent/Query/Raw.swift | 80 ++++++----------- Sources/Fluent/SQL/SQLQuerySerializer.swift | 14 ++- Tests/FluentTests/ModelFindTests.swift | 3 +- Tests/FluentTests/QueryFiltersTests.swift | 13 ++- Tests/FluentTests/SQLSerializerTests.swift | 90 ++++++++++--------- 9 files changed, 102 insertions(+), 112 deletions(-) diff --git a/Sources/Fluent/Query/Filter/Filter.swift b/Sources/Fluent/Query/Filter/Filter.swift index 4d155d23..2db9a866 100644 --- a/Sources/Fluent/Query/Filter/Filter.swift +++ b/Sources/Fluent/Query/Filter/Filter.swift @@ -10,7 +10,7 @@ public struct Filter { public enum Method { case compare(String, Comparison, Node) case subset(String, Scope, [Node]) - case group(Relation, [Filter]) + case group(Relation, [RawOr]) } public init(_ entity: Entity.Type, _ method: Method) { diff --git a/Sources/Fluent/Query/Filter/Query+Filter.swift b/Sources/Fluent/Query/Filter/Query+Filter.swift index 5d6b623d..968e65df 100644 --- a/Sources/Fluent/Query/Filter/Query+Filter.swift +++ b/Sources/Fluent/Query/Filter/Query+Filter.swift @@ -5,7 +5,7 @@ extension QueryRepresentable { _ filter: Filter ) throws -> Query { let query = try makeQuery() - query.filters.append(filter) + query.filters.append(.some(filter)) return query } diff --git a/Sources/Fluent/Query/Query.swift b/Sources/Fluent/Query/Query.swift index beb7c141..15cacea0 100644 --- a/Sources/Fluent/Query/Query.swift +++ b/Sources/Fluent/Query/Query.swift @@ -6,7 +6,7 @@ public final class Query { /// An array of filters to apply ///during the query's action. - public var filters: [Filter] + public var filters: [RawOr] /// Optional data to be used during ///`.create` or `.updated` actions. @@ -20,11 +20,6 @@ public final class Query { /// be applied to the results. public var sorts: [Sort] - /// An array of custom, raw query - /// fragments that must be specially supported - /// by the underlying driver - public var raws: [Raw] - /// An array of joins: other entities /// that will be queried during this query's /// execution. @@ -44,7 +39,6 @@ public final class Query { self.database = database joins = [] sorts = [] - raws = [] } /// Performs the Query returning the raw diff --git a/Sources/Fluent/Query/QueryRepresentable.swift b/Sources/Fluent/Query/QueryRepresentable.swift index f8b16370..1c0c7d92 100644 --- a/Sources/Fluent/Query/QueryRepresentable.swift +++ b/Sources/Fluent/Query/QueryRepresentable.swift @@ -164,7 +164,7 @@ extension QueryRepresentable { ) ) - query.filters.append(filter) + query.filters.append(.some(filter)) try model.willDelete() try query.raw() diff --git a/Sources/Fluent/Query/Raw.swift b/Sources/Fluent/Query/Raw.swift index 99809b72..9bae98c1 100644 --- a/Sources/Fluent/Query/Raw.swift +++ b/Sources/Fluent/Query/Raw.swift @@ -1,60 +1,38 @@ -public enum Raw { - case filter(String, [Node]) - case join(String) - case limit(String) - case sort(String) +public enum RawOr { + case raw(String, [Node]) + case some(Wrapped) } -extension Raw { - public init( - filter: String, - values: [NodeRepresentable] = [] - ) throws { - let values = try values.map { nr in - return try nr.makeNode(in: nil) - } - self = .filter(filter, values) - } -} - -extension Sequence where Iterator.Element == Raw { - /// All raw filters and values - public var filters: [(String, [Node])] { - return flatMap { raw in - guard case .filter(let string, let values) = raw else { - return nil - } - return (string, values) - } +extension QueryRepresentable { + @discardableResult + public func filter( + raw string: String, + _ values: [Node] = [] + ) throws -> Query { + let query = try makeQuery() + query.filters.append(.raw(string, values)) + return query } - /// All raw joins - public var joins: [String] { - return flatMap { raw in - guard case .join(let string) = raw else { - return nil - } - return string - } - } - - /// All raw limits - public var limits: [String] { - return flatMap { raw in - guard case .limit(let string) = raw else { - return nil - } - return string - } + @discardableResult + public func filter( + raw string: String, + _ values: [NodeRepresentable] + ) throws -> Query { + let query = try makeQuery() + let values = try values.map { try $0.makeNode(in: query.context) } + query.filters.append(.raw(string, values)) + return query } +} - /// All raw sorts - public var sorts: [String] { - return flatMap { raw in - guard case .sort(let string) = raw else { - return nil - } - return string +extension RawOr: CustomStringConvertible { + public var description: String { + switch self { + case .raw(let string, let values): + return "[raw] \(string) \(values)" + case .some(let wrapped): + return "\(wrapped)" } } } diff --git a/Sources/Fluent/SQL/SQLQuerySerializer.swift b/Sources/Fluent/SQL/SQLQuerySerializer.swift index 32abd023..542cad9f 100644 --- a/Sources/Fluent/SQL/SQLQuerySerializer.swift +++ b/Sources/Fluent/SQL/SQLQuerySerializer.swift @@ -67,7 +67,6 @@ open class GeneralSQLSerializer: SQLSerializer { if !query.joins.isEmpty { statement += joins(query.joins) } - statement += query.raws.joins if !query.filters.isEmpty { let (filtersClause, filtersValues) = filters(query.filters) @@ -306,7 +305,7 @@ open class GeneralSQLSerializer: SQLSerializer { } - open func filters(_ f: [Filter]) -> (String, [Node]) { + open func filters(_ f: [RawOr]) -> (String, [Node]) { var fragments: [String] = [] fragments += "WHERE" @@ -321,7 +320,7 @@ open class GeneralSQLSerializer: SQLSerializer { ) } - open func filters(_ filters: [Filter], _ r: Filter.Relation) -> (String, [Node]) { + open func filters(_ filters: [RawOr], _ r: Filter.Relation) -> (String, [Node]) { var fragments: [String] = [] var values: [Node] = [] @@ -353,6 +352,15 @@ open class GeneralSQLSerializer: SQLSerializer { return word } + open func filter(_ f: RawOr) -> (String, [Node]) { + switch f { + case .raw(let string, let values): + return (string, values) + case .some(let f): + return filter(f) + } + } + open func filter(_ filter: Filter) -> (String, [Node]) { var statement: [String] = [] var values: [Node] = [] diff --git a/Tests/FluentTests/ModelFindTests.swift b/Tests/FluentTests/ModelFindTests.swift index 21d30b35..2e682f11 100644 --- a/Tests/FluentTests/ModelFindTests.swift +++ b/Tests/FluentTests/ModelFindTests.swift @@ -50,7 +50,8 @@ class ModelFindTests: XCTestCase { func query(_ query: Query) throws -> Node { if - let filter = query.filters.first, + let rawOrFilter = query.filters.first, + case .some(let filter) = rawOrFilter, case .compare(let key, let comparison, let value) = filter.method, query.action == .fetch && query.filters.count == 1 && diff --git a/Tests/FluentTests/QueryFiltersTests.swift b/Tests/FluentTests/QueryFiltersTests.swift index 801d4357..f6e23400 100644 --- a/Tests/FluentTests/QueryFiltersTests.swift +++ b/Tests/FluentTests/QueryFiltersTests.swift @@ -29,7 +29,11 @@ class QueryFiltersTests: XCTestCase { func testBasicQuery() throws { let query = try DummyModel.query().filter("name", "Vapor") - guard let filter = query.filters.first, query.filters.count == 1 else { + guard + let rawOrFilter = query.filters.first, + query.filters.count == 1, + case .some(let filter) = rawOrFilter + else { XCTFail("Should be one filter") return } @@ -48,9 +52,10 @@ class QueryFiltersTests: XCTestCase { let query = try DummyModel.query().filter("name", .hasPrefix, "Vap") guard - let filter = query.filters.first, - query.filters.count == 1 else - { + let rawOrFilter = query.filters.first, + query.filters.count == 1, + case .some(let filter) = rawOrFilter + else { XCTFail("Should be one filter") return } diff --git a/Tests/FluentTests/SQLSerializerTests.swift b/Tests/FluentTests/SQLSerializerTests.swift index 447062e8..85be5e78 100644 --- a/Tests/FluentTests/SQLSerializerTests.swift +++ b/Tests/FluentTests/SQLSerializerTests.swift @@ -7,18 +7,18 @@ class SQLSerializerTests: XCTestCase { ("testRegularSelect", testRegularSelect), ("testOffsetSelect", testOffsetSelect), ("testFilterCompareSelect", testFilterCompareSelect), -// ("testFilterLikeSelect", testFilterLikeSelect), -// ("testBasicCount", testBasicCount), -// ("testRegularCount", testRegularCount), -// ("testFilterCompareCount", testFilterCompareCount), -// ("testFilterLikeCount", testFilterLikeCount), -// ("testFilterEqualsNullSelect", testFilterEqualsNullSelect), -// ("testFilterNotEqualsNullSelect", testFilterNotEqualsNullSelect), -// ("testFilterCompareUpdate", testFilterCompareUpdate), -// ("testFilterCompareDelete", testFilterCompareDelete), -// ("testFilterGroup", testFilterGroup), -// ("testSort", testSort), -// ("testSortMultiple", testSortMultiple), + ("testFilterLikeSelect", testFilterLikeSelect), + ("testBasicCount", testBasicCount), + ("testRegularCount", testRegularCount), + ("testFilterCompareCount", testFilterCompareCount), + ("testFilterLikeCount", testFilterLikeCount), + ("testFilterEqualsNullSelect", testFilterEqualsNullSelect), + ("testFilterNotEqualsNullSelect", testFilterNotEqualsNullSelect), + ("testFilterCompareUpdate", testFilterCompareUpdate), + ("testFilterCompareDelete", testFilterCompareDelete), + ("testFilterGroup", testFilterGroup), + ("testSort", testSort), + ("testSortMultiple", testSortMultiple), ] var db: Database! @@ -39,7 +39,7 @@ class SQLSerializerTests: XCTestCase { func testRegularSelect() { let filter = Filter(User.self, .compare("age", .greaterThanOrEquals, 21)) let query = Query(db) - query.filters.append(filter) + query.filters.append(.some(filter)) query.limit = Limit(count: 5) let (statement, values) = serialize(query) @@ -51,7 +51,7 @@ class SQLSerializerTests: XCTestCase { func testOffsetSelect() { let filter = Filter(User.self, .compare("age", .greaterThanOrEquals, 21)) let query = Query(db) - query.filters.append(filter) + query.filters.append(.some(filter)) query.limit = Limit(count: 5, offset: 15) let (statement, values) = serialize(query) @@ -62,7 +62,7 @@ class SQLSerializerTests: XCTestCase { func testFilterCompareSelect() { let filter = Filter(User.self, .compare("name", .notEquals, "duck")) let query = Query(db) - query.filters.append(filter) + query.filters.append(.some(filter)) let (statement, values) = serialize(query) XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`name` != ?") @@ -73,7 +73,7 @@ class SQLSerializerTests: XCTestCase { func testFilterLikeSelect() { let filter = Filter(User.self, .compare("name", .hasPrefix, "duc")) let query = Query(db) - query.filters.append(filter) + query.filters.append(.some(filter)) let (statement, values) = serialize(query) XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`name` LIKE ?") @@ -91,11 +91,10 @@ class SQLSerializerTests: XCTestCase { XCTAssert(values.isEmpty) } - func testRegularCount() { - let filter = Filter(User.self, .compare("age", .greaterThanOrEquals, 21)) + func testRegularCount() throws { let query = Query(db) query.action = .count - query.filters.append(filter) + try query.filter("age", .greaterThanOrEquals, 21) let (statement, values) = serialize(query) XCTAssertEqual(statement, "SELECT COUNT(*) as _fluent_count FROM `users` WHERE `users`.`age` >= ?") @@ -103,11 +102,10 @@ class SQLSerializerTests: XCTestCase { XCTAssertEqual(values.count, 1) } - func testFilterCompareCount() { - let filter = Filter(User.self, .compare("name", .notEquals, "duck")) + func testFilterCompareCount() throws { let query = Query(db) query.action = .count - query.filters.append(filter) + try query.filter("name", .notEquals, "duck") let (statement, values) = serialize(query) XCTAssertEqual(statement, "SELECT COUNT(*) as _fluent_count FROM `users` WHERE `users`.`name` != ?") @@ -115,11 +113,10 @@ class SQLSerializerTests: XCTestCase { XCTAssertEqual(values.count, 1) } - func testFilterLikeCount() { - let filter = Filter(User.self, .compare("name", .hasPrefix, "duc")) + func testFilterLikeCount() throws { let query = Query(db) query.action = .count - query.filters.append(filter) + try query.filter("name", .hasPrefix, "duc") let (statement, values) = serialize(query) XCTAssertEqual(statement, "SELECT COUNT(*) as _fluent_count FROM `users` WHERE `users`.`name` LIKE ?") @@ -127,30 +124,27 @@ class SQLSerializerTests: XCTestCase { XCTAssertEqual(values.count, 1) } - func testFilterEqualsNullSelect() { - let filter = Filter(User.self, .compare("name", .equals, Node.null)) + func testFilterEqualsNullSelect() throws { let query = Query(db) - query.filters.append(filter) + try query.filter("name", .equals, Node.null) let (statement, values) = serialize(query) XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`name` IS NULL") XCTAssertEqual(values.count, 0) } - func testFilterNotEqualsNullSelect() { - let filter = Filter(User.self, .compare("name", .notEquals, Node.null)) + func testFilterNotEqualsNullSelect() throws { let query = Query(db) - query.filters.append(filter) + try query.filter("name", .notEquals, Node.null) let (statement, values) = serialize(query) XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`name` IS NOT NULL") XCTAssertEqual(values.count, 0) } - func testFilterCompareUpdate() { - let filter = Filter(User.self, .compare("name", .equals, "duck")) + func testFilterCompareUpdate() throws { let query = Query(db) - query.filters.append(filter) + try query.filter("name", "duck") query.data = ["not it": true] query.action = .modify let (statement, values) = serialize(query) @@ -161,10 +155,9 @@ class SQLSerializerTests: XCTestCase { XCTAssertEqual(values.count, 2) } - func testFilterCompareDelete() { - let filter = Filter(User.self, .compare("name", .greaterThan, .string("duck"))) + func testFilterCompareDelete() throws { let query = Query(db) - query.filters.append(filter) + try query.filter("name", .greaterThan, "duck") query.action = .delete let (statement, values) = serialize(query) @@ -179,12 +172,12 @@ class SQLSerializerTests: XCTestCase { let two = Filter(User.self, .compare("2", .equals, .string("2"))) let three = Filter(User.self, .compare("3", .equals, .string("3"))) let four = Filter(User.self, .compare("4", .equals, .string("4"))) - let group = Filter(User.self, .group(.or, [two, three])) + let group = Filter(User.self, .group(.or, [.some(two), .some(three)])) let query = Query(db) - query.filters.append(one) - query.filters.append(group) - query.filters.append(four) + try query.filter(one) + try query.filter(group) + try query.filter(four) let (statement, values) = serialize(query) XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`1` = ? AND (`users`.`2` = ? OR `users`.`3` = ?) AND `users`.`4` = ?") @@ -196,7 +189,7 @@ class SQLSerializerTests: XCTestCase { let name = Sort(User.self, "name", .ascending) let query = Query(db) - query.filters.append(adult) + try query.filter(adult) query.sorts.append(name) let (statement, values) = serialize(query) @@ -210,13 +203,24 @@ class SQLSerializerTests: XCTestCase { let email = Sort(User.self, "email", .descending) let query = Query(db) - query.filters.append(adult) + try query.filter(adult) query.sorts += [name, email] let (statement, values) = serialize(query) XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`age` > ? ORDER BY `users`.`name` ASC, `users`.`email` DESC") XCTAssertEqual(values.count, 1) } + + func testRawFilter() throws { + let query = Query(db) + try query.filter("name", "bob") + try query.filter(raw: "aGe ~~ ?", [22]) + + let (statement, values) = serialize(query) + + XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`name` = ? AND aGe ~~ ?") + XCTAssertEqual(values.count, 2) + } } // MARK: Utilities From 5b8dadb42e9af8e188f43c0a73c6ce350e16246a Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Wed, 15 Mar 2017 21:42:51 +0100 Subject: [PATCH 03/11] raw joins --- Sources/Fluent/Query/Join.swift | 2 +- Sources/Fluent/Query/Query.swift | 2 +- Sources/Fluent/Query/Raw.swift | 15 +++++++++++++++ Sources/Fluent/SQL/SQLQuerySerializer.swift | 11 ++++++++++- Tests/FluentTests/SQLSerializerTests.swift | 13 +++++++++++++ 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/Sources/Fluent/Query/Join.swift b/Sources/Fluent/Query/Join.swift index 4e2e78f1..664da7d9 100644 --- a/Sources/Fluent/Query/Join.swift +++ b/Sources/Fluent/Query/Join.swift @@ -74,7 +74,7 @@ extension QueryRepresentable { @discardableResult public func join(_ join: Join) throws -> Query { let query = try makeQuery() - query.joins.append(join) + query.joins.append(.some(join)) return query } } diff --git a/Sources/Fluent/Query/Query.swift b/Sources/Fluent/Query/Query.swift index 8daf81bd..42414f23 100644 --- a/Sources/Fluent/Query/Query.swift +++ b/Sources/Fluent/Query/Query.swift @@ -23,7 +23,7 @@ public final class Query { /// An array of joins: other entities /// that will be queried during this query's /// execution. - public var joins: [Join] + public var joins: [RawOr] private(set) lazy var context: RowContext = { let context = RowContext() diff --git a/Sources/Fluent/Query/Raw.swift b/Sources/Fluent/Query/Raw.swift index 9bae98c1..ae8c540e 100644 --- a/Sources/Fluent/Query/Raw.swift +++ b/Sources/Fluent/Query/Raw.swift @@ -3,6 +3,8 @@ public enum RawOr { case some(Wrapped) } +// MARK: Filter + extension QueryRepresentable { @discardableResult public func filter( @@ -26,6 +28,19 @@ extension QueryRepresentable { } } +// MARK: Join + +extension QueryRepresentable { + @discardableResult + public func join( + raw string: String + ) throws -> Query { + let query = try makeQuery() + query.joins.append(.raw(string, [])) + return query + } +} + extension RawOr: CustomStringConvertible { public var description: String { switch self { diff --git a/Sources/Fluent/SQL/SQLQuerySerializer.swift b/Sources/Fluent/SQL/SQLQuerySerializer.swift index 542cad9f..184bfa39 100644 --- a/Sources/Fluent/SQL/SQLQuerySerializer.swift +++ b/Sources/Fluent/SQL/SQLQuerySerializer.swift @@ -494,7 +494,7 @@ open class GeneralSQLSerializer: SQLSerializer { } } - open func joins(_ joins: [Join]) -> String { + open func joins(_ joins: [RawOr]) -> String { var fragments: [String] = [] for j in joins { @@ -504,6 +504,15 @@ open class GeneralSQLSerializer: SQLSerializer { return concatenate(fragments) } + open func join(_ rawOrJoin: RawOr) -> String { + switch rawOrJoin { + case .raw(let string, _): + return string + case .some(let j): + return join(j) + } + } + open func join(_ join: Join) -> String { var fragments: [String] = [] diff --git a/Tests/FluentTests/SQLSerializerTests.swift b/Tests/FluentTests/SQLSerializerTests.swift index 85be5e78..e69b3cc5 100644 --- a/Tests/FluentTests/SQLSerializerTests.swift +++ b/Tests/FluentTests/SQLSerializerTests.swift @@ -221,6 +221,19 @@ class SQLSerializerTests: XCTestCase { XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`name` = ? AND aGe ~~ ?") XCTAssertEqual(values.count, 2) } + + func testRawJoinsAndFilters() throws { + let query = Query(db) + try query.join(Atom.self) + try query.filter(Atom.self, "size", 42) + try query.filter(raw: "`foo`.aGe ~~ ?", [22]) + try query.join(raw: "JOIN `foo` ON `users`.BAR !~ `foo`.🚀") + + let (statement, values) = serialize(query) + + XCTAssertEqual(statement, "SELECT `users`.* FROM `users` JOIN `atoms` ON `users`.`id` = `atoms`.`user_id` JOIN `foo` ON `users`.BAR !~ `foo`.🚀 WHERE `atoms`.`size` = ? AND `foo`.aGe ~~ ?") + XCTAssertEqual(values.count, 2) + } } // MARK: Utilities From c02e5366725e576ada3c6f0e7577c5d28565bd66 Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Wed, 15 Mar 2017 22:00:57 +0100 Subject: [PATCH 04/11] raw finishing touches --- Sources/Fluent/Query/Raw.swift | 23 +++++++++++++++++++ Sources/FluentTester/Tester+SoftDelete.swift | 4 ---- Tests/FluentTests/ModelFindTests.swift | 3 +-- Tests/FluentTests/QueryFiltersTests.swift | 10 ++++---- Tests/FluentTests/SQLSerializerTests.swift | 24 ++++++++------------ 5 files changed, 37 insertions(+), 27 deletions(-) diff --git a/Sources/Fluent/Query/Raw.swift b/Sources/Fluent/Query/Raw.swift index ae8c540e..98d06d7c 100644 --- a/Sources/Fluent/Query/Raw.swift +++ b/Sources/Fluent/Query/Raw.swift @@ -3,6 +3,17 @@ public enum RawOr { case some(Wrapped) } +extension RawOr { + var wrapped: Wrapped? { + switch self { + case .some(let wrapped): + return wrapped + case .raw: + return nil + } + } +} + // MARK: Filter extension QueryRepresentable { @@ -28,6 +39,12 @@ extension QueryRepresentable { } } +extension Array where Element == RawOr { + public mutating func append(_ filter: Filter) { + append(.some(filter)) + } +} + // MARK: Join extension QueryRepresentable { @@ -41,6 +58,12 @@ extension QueryRepresentable { } } +extension Array where Element == RawOr { + public mutating func append(_ join: Join) { + append(.some(join)) + } +} + extension RawOr: CustomStringConvertible { public var description: String { switch self { diff --git a/Sources/FluentTester/Tester+SoftDelete.swift b/Sources/FluentTester/Tester+SoftDelete.swift index 0faa0cc0..39e88547 100644 --- a/Sources/FluentTester/Tester+SoftDelete.swift +++ b/Sources/FluentTester/Tester+SoftDelete.swift @@ -8,10 +8,6 @@ extension Tester { try? Compound.revert(database) } - database.log = { query in - print(query) - } - let ethanol = Compound(name: "Ethanol") let hcl = Compound(name: "Hydrochloric Acid") let methanol = Compound(name: "Methanol") diff --git a/Tests/FluentTests/ModelFindTests.swift b/Tests/FluentTests/ModelFindTests.swift index 2e682f11..4eebbf4b 100644 --- a/Tests/FluentTests/ModelFindTests.swift +++ b/Tests/FluentTests/ModelFindTests.swift @@ -50,8 +50,7 @@ class ModelFindTests: XCTestCase { func query(_ query: Query) throws -> Node { if - let rawOrFilter = query.filters.first, - case .some(let filter) = rawOrFilter, + let filter = query.filters.first?.wrapped, case .compare(let key, let comparison, let value) = filter.method, query.action == .fetch && query.filters.count == 1 && diff --git a/Tests/FluentTests/QueryFiltersTests.swift b/Tests/FluentTests/QueryFiltersTests.swift index f6e23400..2341bd22 100644 --- a/Tests/FluentTests/QueryFiltersTests.swift +++ b/Tests/FluentTests/QueryFiltersTests.swift @@ -30,9 +30,8 @@ class QueryFiltersTests: XCTestCase { let query = try DummyModel.query().filter("name", "Vapor") guard - let rawOrFilter = query.filters.first, - query.filters.count == 1, - case .some(let filter) = rawOrFilter + let filter = query.filters.first?.wrapped, + query.filters.count == 1 else { XCTFail("Should be one filter") return @@ -52,9 +51,8 @@ class QueryFiltersTests: XCTestCase { let query = try DummyModel.query().filter("name", .hasPrefix, "Vap") guard - let rawOrFilter = query.filters.first, - query.filters.count == 1, - case .some(let filter) = rawOrFilter + let filter = query.filters.first?.wrapped, + query.filters.count == 1 else { XCTFail("Should be one filter") return diff --git a/Tests/FluentTests/SQLSerializerTests.swift b/Tests/FluentTests/SQLSerializerTests.swift index e69b3cc5..ced5efa0 100644 --- a/Tests/FluentTests/SQLSerializerTests.swift +++ b/Tests/FluentTests/SQLSerializerTests.swift @@ -39,7 +39,7 @@ class SQLSerializerTests: XCTestCase { func testRegularSelect() { let filter = Filter(User.self, .compare("age", .greaterThanOrEquals, 21)) let query = Query(db) - query.filters.append(.some(filter)) + query.filters.append(filter) query.limit = Limit(count: 5) let (statement, values) = serialize(query) @@ -51,7 +51,7 @@ class SQLSerializerTests: XCTestCase { func testOffsetSelect() { let filter = Filter(User.self, .compare("age", .greaterThanOrEquals, 21)) let query = Query(db) - query.filters.append(.some(filter)) + query.filters.append(filter) query.limit = Limit(count: 5, offset: 15) let (statement, values) = serialize(query) @@ -62,7 +62,7 @@ class SQLSerializerTests: XCTestCase { func testFilterCompareSelect() { let filter = Filter(User.self, .compare("name", .notEquals, "duck")) let query = Query(db) - query.filters.append(.some(filter)) + query.filters.append(filter) let (statement, values) = serialize(query) XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`name` != ?") @@ -73,7 +73,7 @@ class SQLSerializerTests: XCTestCase { func testFilterLikeSelect() { let filter = Filter(User.self, .compare("name", .hasPrefix, "duc")) let query = Query(db) - query.filters.append(.some(filter)) + query.filters.append(filter) let (statement, values) = serialize(query) XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`name` LIKE ?") @@ -168,16 +168,10 @@ class SQLSerializerTests: XCTestCase { } func testFilterGroup() throws { - let one = Filter(User.self, .compare("1", .equals, .string("1"))) - let two = Filter(User.self, .compare("2", .equals, .string("2"))) - let three = Filter(User.self, .compare("3", .equals, .string("3"))) - let four = Filter(User.self, .compare("4", .equals, .string("4"))) - let group = Filter(User.self, .group(.or, [.some(two), .some(three)])) - let query = Query(db) - try query.filter(one) - try query.filter(group) - try query.filter(four) + try query.filter("1", 1) + try query.or { try $0.filter("2", 2).filter("3", 3) } + try query.filter("4", 4) let (statement, values) = serialize(query) XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`1` = ? AND (`users`.`2` = ? OR `users`.`3` = ?) AND `users`.`4` = ?") @@ -223,7 +217,7 @@ class SQLSerializerTests: XCTestCase { } func testRawJoinsAndFilters() throws { - let query = Query(db) + let query = Query(db) try query.join(Atom.self) try query.filter(Atom.self, "size", 42) try query.filter(raw: "`foo`.aGe ~~ ?", [22]) @@ -231,7 +225,7 @@ class SQLSerializerTests: XCTestCase { let (statement, values) = serialize(query) - XCTAssertEqual(statement, "SELECT `users`.* FROM `users` JOIN `atoms` ON `users`.`id` = `atoms`.`user_id` JOIN `foo` ON `users`.BAR !~ `foo`.🚀 WHERE `atoms`.`size` = ? AND `foo`.aGe ~~ ?") + XCTAssertEqual(statement, "SELECT `compounds`.* FROM `compounds` JOIN `atoms` ON `compounds`.`id` = `atoms`.`compound_id` JOIN `foo` ON `users`.BAR !~ `foo`.🚀 WHERE `atoms`.`size` = ? AND `foo`.aGe ~~ ?") XCTAssertEqual(values.count, 2) } } From fb975e2c7cfff56f6419caf5a97cfe79857882cf Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Wed, 15 Mar 2017 22:10:14 +0100 Subject: [PATCH 05/11] compiler speed --- Sources/Fluent/SQL/SQLQuerySerializer.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Fluent/SQL/SQLQuerySerializer.swift b/Sources/Fluent/SQL/SQLQuerySerializer.swift index 184bfa39..fea4213f 100644 --- a/Sources/Fluent/SQL/SQLQuerySerializer.swift +++ b/Sources/Fluent/SQL/SQLQuerySerializer.swift @@ -243,13 +243,13 @@ open class GeneralSQLSerializer: SQLSerializer { switch d.wrapped { case .number(let n): - dc = "'" + n.description + "'" + dc = "'\(n.description)'" case .null: dc = "NULL" case .bool(let b): dc = b ? "TRUE" : "FALSE" default: - dc = "'" + (d.string ?? "") + "'" + dc = "'\((d.string ?? ""))'" } clause += "DEFAULT \(dc)" From 696aa6ab764f30cbc3c58cec716241e9061f8f59 Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Wed, 15 Mar 2017 23:45:23 +0100 Subject: [PATCH 06/11] create positions + datetime --- Sources/Fluent/SQL/SQLQuerySerializer.swift | 2 +- Sources/Fluent/Schema/Database+Schema.swift | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/Fluent/SQL/SQLQuerySerializer.swift b/Sources/Fluent/SQL/SQLQuerySerializer.swift index fea4213f..ee841497 100644 --- a/Sources/Fluent/SQL/SQLQuerySerializer.swift +++ b/Sources/Fluent/SQL/SQLQuerySerializer.swift @@ -287,7 +287,7 @@ open class GeneralSQLSerializer: SQLSerializer { case .bytes: return "BLOB" case .date: - return "TIMESTAMP" + return "DATETIME" case .custom(let type): return type } diff --git a/Sources/Fluent/Schema/Database+Schema.swift b/Sources/Fluent/Schema/Database+Schema.swift index 3357cff0..c03f14cf 100644 --- a/Sources/Fluent/Schema/Database+Schema.swift +++ b/Sources/Fluent/Schema/Database+Schema.swift @@ -5,6 +5,7 @@ extension Database { if e.database == nil { e.database = self } let creator = Creator() + try closure(creator) // add timestamps if let T = E.self as? Timestampable.Type { @@ -17,8 +18,6 @@ extension Database { creator.date(S.deletedAtKey, optional: true) } - try closure(creator) - let query = Query(self) query.action = .schema(.create(creator.fields)) try self.query(query) From d7f0d3504984a086590679b327c73e0d36e2fc30 Mon Sep 17 00:00:00 2001 From: tanner0101 Date: Wed, 22 Mar 2017 12:47:50 +0100 Subject: [PATCH 07/11] simplify equatable --- Sources/Fluent/Query/Action.swift | 69 ++++++------------- Sources/Fluent/Query/Filter/Comparison.swift | 66 ++++-------------- Tests/FluentTests/RawTests.swift | 2 + .../Utilities/LastQueryDriver.swift | 2 +- 4 files changed, 38 insertions(+), 101 deletions(-) diff --git a/Sources/Fluent/Query/Action.swift b/Sources/Fluent/Query/Action.swift index f2c5c3b7..99697b32 100644 --- a/Sources/Fluent/Query/Action.swift +++ b/Sources/Fluent/Query/Action.swift @@ -18,61 +18,32 @@ public enum Schema { extension Action: Equatable { public static func ==(lhs: Action, rhs: Action) -> Bool { - switch lhs { - case .fetch: - switch rhs { - case .fetch: return true - default: return false - } - case .count: - switch rhs { - case .count: return true - default: return false - } - case .delete: - switch rhs { - case .delete: return true - default: return false - } - case .create: - switch rhs { - case .create: return true - default: return false - } - case .modify: - switch rhs { - case .modify: return true - default: return false - } - case .schema(let a): - switch rhs { - case .schema(let b): return a == b - default: return false - } - + switch (lhs, rhs) { + case (.fetch, .fetch), + (.count, .count), + (.delete, .delete), + (.create, .create), + (.modify, .modify): + return true + case (.schema(let a), .schema(let b)): + return a == b + default: + return false } } } extension Schema: Equatable { public static func ==(lhs: Schema, rhs: Schema) -> Bool { - switch lhs { - case .create(let a): - switch rhs { - case .create(let b): return a == b - default: return false - } - case .modify(let addA, let removeA): - switch rhs { - case .modify(let addB, let removeB): - return addA == addB && removeA == removeB - default: return false - } - case .delete: - switch rhs { - case .delete: return true - default: return false - } + switch (lhs, rhs) { + case (.create(let a), .create(let b)): + return a == b + case (.modify(let addA, let removeA), .modify(let addB, let removeB)): + return addA == addB && removeA == removeB + case (.delete, .delete): + return true + default: + return false } } } diff --git a/Sources/Fluent/Query/Filter/Comparison.swift b/Sources/Fluent/Query/Filter/Comparison.swift index 9a396c06..c40d1288 100644 --- a/Sources/Fluent/Query/Filter/Comparison.swift +++ b/Sources/Fluent/Query/Filter/Comparison.swift @@ -48,57 +48,21 @@ func != (lhs: String, rhs: NodeRepresentable) throws -> Filter.Method { extension Filter.Comparison: Equatable { public static func ==(lhs: Filter.Comparison, rhs: Filter.Comparison) -> Bool { - switch lhs { - case .equals: - switch rhs { - case .equals: return true - default: return false - } - case .greaterThan: - switch rhs { - case .greaterThan: return true - default: return false - } - case .lessThan: - switch rhs { - case .lessThan: return true - default: return false - } - case .greaterThanOrEquals: - switch rhs { - case .greaterThanOrEquals: return true - default: return false - } - case .lessThanOrEquals: - switch rhs { - case .lessThanOrEquals: return true - default: return false - } - case .notEquals: - switch rhs { - case .notEquals: return true - default: return false - } - case .hasSuffix: - switch rhs { - case .hasSuffix: return true - default: return false - } - case .hasPrefix: - switch rhs { - case .hasPrefix: return true - default: return false - } - case .contains: - switch rhs { - case .contains: return true - default: return false - } - case .custom(let a): - switch rhs { - case .custom(let b): return a == b - default: return false - } + switch (lhs, rhs) { + case (.equals, .equals), + (.greaterThan, .greaterThan), + (.lessThan, .lessThan), + (.greaterThanOrEquals, .greaterThanOrEquals), + (.lessThanOrEquals, .lessThanOrEquals), + (.notEquals, .notEquals), + (.hasSuffix, .hasSuffix), + (.hasPrefix, .hasPrefix), + (.contains, .contains): + return true + case (.custom(let a), .custom(let b)): + return a == b + default: + return false } } } diff --git a/Tests/FluentTests/RawTests.swift b/Tests/FluentTests/RawTests.swift index 73681c1e..45c4cb72 100644 --- a/Tests/FluentTests/RawTests.swift +++ b/Tests/FluentTests/RawTests.swift @@ -19,6 +19,8 @@ class RawTests: XCTestCase { override func setUp(){ lqd = LastQueryDriver() db = Database(lqd) + Compound.database = db + Atom.database = db } func testBasic() throws { diff --git a/Tests/FluentTests/Utilities/LastQueryDriver.swift b/Tests/FluentTests/Utilities/LastQueryDriver.swift index ace6a0c5..d41df097 100644 --- a/Tests/FluentTests/Utilities/LastQueryDriver.swift +++ b/Tests/FluentTests/Utilities/LastQueryDriver.swift @@ -3,7 +3,7 @@ import Fluent class LastQueryDriver: Driver { var keyNamingConvention: KeyNamingConvention = .snake_case var idType: IdentifierType = .int - var idKey: String = "#id" + let idKey: String = "#id" var lastQuery: (String, [Node])? var lastRaw: (String, [Node])? From d242ffc2b0e78694bbfbf1c3ae5f95643be5e6e5 Mon Sep 17 00:00:00 2001 From: tanner0101 Date: Wed, 22 Mar 2017 13:05:17 +0100 Subject: [PATCH 08/11] raw fields --- Sources/Fluent/Query/Action.swift | 4 +- Sources/Fluent/Query/Raw.swift | 2 +- Sources/Fluent/SQL/GeneralSQLSerializer.swift | 32 +++++--- Sources/Fluent/Schema/Builder.swift | 32 +++++--- Sources/Fluent/Schema/Creator.swift | 2 +- Sources/Fluent/Schema/Field.swift | 79 ++++++------------- Sources/Fluent/Schema/Modifier.swift | 6 +- Tests/FluentTests/PreparationTests.swift | 24 +++--- 8 files changed, 85 insertions(+), 96 deletions(-) diff --git a/Sources/Fluent/Query/Action.swift b/Sources/Fluent/Query/Action.swift index 99697b32..53816084 100644 --- a/Sources/Fluent/Query/Action.swift +++ b/Sources/Fluent/Query/Action.swift @@ -11,8 +11,8 @@ public enum Action { } public enum Schema { - case create([Field]) - case modify(add: [Field], remove: [Field]) + case create([RawOr]) + case modify(add: [RawOr], remove: [RawOr]) case delete } diff --git a/Sources/Fluent/Query/Raw.swift b/Sources/Fluent/Query/Raw.swift index 641e22f1..31802f6e 100644 --- a/Sources/Fluent/Query/Raw.swift +++ b/Sources/Fluent/Query/Raw.swift @@ -4,7 +4,7 @@ public enum RawOr { } extension RawOr { - var wrapped: Wrapped? { + public var wrapped: Wrapped? { switch self { case .some(let wrapped): return wrapped diff --git a/Sources/Fluent/SQL/GeneralSQLSerializer.swift b/Sources/Fluent/SQL/GeneralSQLSerializer.swift index ec6b7e84..09908266 100644 --- a/Sources/Fluent/SQL/GeneralSQLSerializer.swift +++ b/Sources/Fluent/SQL/GeneralSQLSerializer.swift @@ -198,7 +198,7 @@ open class GeneralSQLSerializer: SQLSerializer { // MARK: Schema - open func create(_ add: [Field]) -> (String, [Node]) { + open func create(_ add: [RawOr]) -> (String, [Node]) { var statement: [String] = [] statement += "CREATE TABLE" @@ -211,7 +211,7 @@ open class GeneralSQLSerializer: SQLSerializer { ) } - open func alter(add: [Field], drop: [Field]) -> (String, [Node]) { + open func alter(add: [RawOr], drop: [RawOr]) -> (String, [Node]) { var statement: [String] = [] statement += "ALTER TABLE" @@ -224,7 +224,14 @@ open class GeneralSQLSerializer: SQLSerializer { } for field in drop { - subclause += "DROP " + escape(field.name) + let name: String + switch field { + case .raw(let raw, _): + name = raw + case .some(let some): + name = some.name + } + subclause += "DROP " + escape(name) } statement += subclause.joined(separator: ", ") @@ -247,14 +254,21 @@ open class GeneralSQLSerializer: SQLSerializer { ) } - open func columns(_ fields: [Field]) -> String { - let string = fields.map { field in - return column(field) - }.joined(separator: ", ") - - return "(\(string))" + open func columns(_ fields: [RawOr]) -> String { + let parsed: [String] = fields.map(column) + + return "(" + parsed.joined(separator: ", ") + ")" } + open func column(_ field: RawOr) -> String { + switch field { + case .raw(let raw, _): + return raw + case .some(let some): + return column(some) + } + } + open func column(_ field: Field) -> String { var clause: [String] = [] diff --git a/Sources/Fluent/Schema/Builder.swift b/Sources/Fluent/Schema/Builder.swift index 15ecef7d..76c102f4 100644 --- a/Sources/Fluent/Schema/Builder.swift +++ b/Sources/Fluent/Schema/Builder.swift @@ -1,15 +1,19 @@ public protocol Builder: class { - var fields: [Field] { get set } + var fields: [RawOr] { get set } } extension Builder { + public func addField(_ field: Field) { + fields.append(.some(field)) + } + public func id(for entityType: E.Type) { let field = Field( name: E.idKey, type: .id(type: E.idType), primaryKey: true ) - fields.append(field) + addField(field) } public func foreignId(for entityType: E.Type) { @@ -17,7 +21,7 @@ extension Builder { name: E.foreignIdKey, type: .id(type: E.idType) ) - fields.append(field) + addField(field) } public func int( @@ -33,7 +37,7 @@ extension Builder { unique: unique, default: `default` ) - fields.append(field) + addField(field) } public func string( @@ -50,7 +54,7 @@ extension Builder { unique: unique, default: `default` ) - fields.append(field) + addField(field) } public func double( @@ -66,7 +70,7 @@ extension Builder { unique: unique, default: `default` ) - fields.append(field) + addField(field) } public func bool( @@ -82,7 +86,7 @@ extension Builder { unique: unique, default: `default` ) - fields.append(field) + addField(field) } public func bytes( @@ -98,7 +102,7 @@ extension Builder { unique: unique, default: `default` ) - fields.append(field) + addField(field) } public func date( @@ -114,7 +118,7 @@ extension Builder { unique: unique, default: `default` ) - fields.append(field) + addField(field) } public func custom( @@ -131,7 +135,7 @@ extension Builder { unique: unique, default: `default` ) - fields.append(field) + addField(field) } // MARK: Relations @@ -165,6 +169,12 @@ extension Builder { unique: unique, default: `default` ) - fields.append(field) + addField(field) + } + + // MARK: Raw + + public func raw(_ string: String) { + fields.append(.raw(string, [])) } } diff --git a/Sources/Fluent/Schema/Creator.swift b/Sources/Fluent/Schema/Creator.swift index 895a365e..de3a18a3 100644 --- a/Sources/Fluent/Schema/Creator.swift +++ b/Sources/Fluent/Schema/Creator.swift @@ -1,5 +1,5 @@ public final class Creator: Builder { - public var fields: [Field] + public var fields: [RawOr] public init() { fields = [] } diff --git a/Sources/Fluent/Schema/Field.swift b/Sources/Fluent/Schema/Field.swift index 67fd66d9..5d053c2c 100644 --- a/Sources/Fluent/Schema/Field.swift +++ b/Sources/Fluent/Schema/Field.swift @@ -76,69 +76,34 @@ extension Field: Equatable { extension Field.DataType: Equatable { public static func ==(lhs: Field.DataType, rhs: Field.DataType) -> Bool { - switch lhs { - case .id(let a): - switch rhs { - case .id(let b): return a == b - default: return false - } - case .int: - switch rhs { - case .int: return true - default: return false - } - case .string: - switch rhs { - case .string: return true - default: return false - } - case .double: - switch rhs { - case .double: return true - default: return false - } - case .bool: - switch rhs { - case .bool: return true - default: return false - } - case .bytes: - switch rhs { - case .bytes: return true - default: return false - } - case .date: - switch rhs { - case .date: return true - default: return false - } - case .custom(let a): - switch rhs { - case .custom(let b): return a == b - default: return false - } + switch (lhs, rhs) { + case (.id(let a), .id(let b)): + return a == b + case (.int, .int), + (.string, .string), + (.double, .double), + (.bool, .bool), + (.bytes, .bytes), + (.date, .date): + return true + case (.custom(let a), .custom(let b)): + return a == b + default: + return false } } } extension IdentifierType: Equatable { public static func ==(lhs: IdentifierType, rhs: IdentifierType) -> Bool { - switch lhs { - case .int: - switch rhs { - case .int: return true - default: return false - } - case .uuid: - switch rhs { - case .uuid: return true - default: return false - } - case .custom(let a): - switch rhs { - case .custom(let b): return a == b - default: return false - } + switch (lhs, rhs) { + case (.int, .int), + (.uuid, .uuid): + return true + case (.custom(let a), .custom(let b)): + return a == b + default: + return false } } } diff --git a/Sources/Fluent/Schema/Modifier.swift b/Sources/Fluent/Schema/Modifier.swift index 09e2eeb9..7149edb4 100644 --- a/Sources/Fluent/Schema/Modifier.swift +++ b/Sources/Fluent/Schema/Modifier.swift @@ -1,8 +1,8 @@ /// Modifies a schema. A subclass of Creator. /// Can modify or delete fields. public final class Modifier: Builder { - public var fields: [Field] - public var delete: [Field] + public var fields: [RawOr] + public var delete: [RawOr] public init() { fields = [] @@ -14,6 +14,6 @@ public final class Modifier: Builder { name: name, type: .custom(type: "delete") ) - delete.append(field) + delete.append(.some(field)) } } diff --git a/Tests/FluentTests/PreparationTests.swift b/Tests/FluentTests/PreparationTests.swift index a03f12d3..9106436e 100644 --- a/Tests/FluentTests/PreparationTests.swift +++ b/Tests/FluentTests/PreparationTests.swift @@ -18,24 +18,24 @@ class PreparationTests: XCTestCase { return } - guard case .int = fields[0].type else { + guard case .int = fields[0].wrapped!.type else { XCTFail("Invalid first field") return } - XCTAssertEqual(fields[0].name, "id") + XCTAssertEqual(fields[0].wrapped?.name, "id") - guard case .string(let colTwoLength) = fields[1].type else { + guard case .string(let colTwoLength) = fields[1].wrapped!.type else { XCTFail("Invalid second field") return } - XCTAssertEqual(fields[1].name, "name") + XCTAssertEqual(fields[1].wrapped?.name, "name") XCTAssertEqual(colTwoLength, nil) - guard case .string(let colThreeLength) = fields[2].type else { + guard case .string(let colThreeLength) = fields[2].wrapped!.type else { XCTFail("Invalid second field") return } - XCTAssertEqual(fields[2].name, "email") + XCTAssertEqual(fields[2].wrapped?.name, "email") XCTAssertEqual(colThreeLength, 128) } @@ -66,7 +66,7 @@ class PreparationTests: XCTestCase { return } - guard case .id(let keyType) = fields[0].type else { + guard case .id(let keyType) = fields[0].wrapped!.type else { XCTFail("Invalid first field \(fields[0])") return } @@ -98,23 +98,23 @@ class PreparationTests: XCTestCase { return } - guard case .id = fields[0].type else { + guard case .id = fields[0].wrapped!.type else { XCTFail("Invalid first field") return } - guard case .string(let colTwoLength) = fields[1].type else { + guard case .string(let colTwoLength) = fields[1].wrapped!.type else { XCTFail("Invalid second field") return } - XCTAssertEqual(fields[1].name, "name") + XCTAssertEqual(fields[1].wrapped!.name, "name") XCTAssertEqual(colTwoLength, nil) - guard case .int = fields[2].type else { + guard case .int = fields[2].wrapped!.type else { XCTFail("Invalid second field") return } - XCTAssertEqual(fields[2].name, "age") + XCTAssertEqual(fields[2].wrapped?.name, "age") } let database = Database(driver) From fd6c0230309888943356e21dfee70cfcb8010086 Mon Sep 17 00:00:00 2001 From: tanner0101 Date: Wed, 22 Mar 2017 13:25:26 +0100 Subject: [PATCH 09/11] fix array stuff --- Tests/FluentTests/RawTests.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Tests/FluentTests/RawTests.swift b/Tests/FluentTests/RawTests.swift index 45c4cb72..5a45e794 100644 --- a/Tests/FluentTests/RawTests.swift +++ b/Tests/FluentTests/RawTests.swift @@ -63,11 +63,10 @@ class RawTests: XCTestCase { func testRawSet() throws { let query = Query(db) try query.set(raw: "id", equals: "UUID()") - query.data[.some("name")] = RawOr.some("vapor") query.action = .modify let (statement, values) = serialize(query) - XCTAssertEqual(statement, "UPDATE `compounds` SET id = UUID(), `name` = ?") - XCTAssertEqual(values.count, 1) + XCTAssertEqual(statement, "UPDATE `compounds` SET id = UUID()") + XCTAssertEqual(values.count, 0) } func testRawGet() throws { From c052194ec5dc3259ed32ad71d42d6d5a44fd01c8 Mon Sep 17 00:00:00 2001 From: tanner0101 Date: Wed, 22 Mar 2017 13:42:53 +0100 Subject: [PATCH 10/11] sql delete join support --- Sources/Fluent/Query/QueryRepresentable.swift | 5 --- Sources/Fluent/SQL/GeneralSQLSerializer.swift | 36 ++++++++++++------- Tests/FluentTests/SQLSerializerTests.swift | 11 ++++++ 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/Sources/Fluent/Query/QueryRepresentable.swift b/Sources/Fluent/Query/QueryRepresentable.swift index 075749fc..7ca66b9b 100644 --- a/Sources/Fluent/Query/QueryRepresentable.swift +++ b/Sources/Fluent/Query/QueryRepresentable.swift @@ -201,11 +201,6 @@ extension QueryRepresentable { /// in the model's collection. public func delete() throws { let query = try makeQuery() - - guard query.joins.count == 0 else { - throw QueryError.notSupported("Cannot perform delete on queries that contain joins. Delete the entities directly instead.") - } - query.action = .delete try query.raw() } diff --git a/Sources/Fluent/SQL/GeneralSQLSerializer.swift b/Sources/Fluent/SQL/GeneralSQLSerializer.swift index 09908266..5d5babdc 100644 --- a/Sources/Fluent/SQL/GeneralSQLSerializer.swift +++ b/Sources/Fluent/SQL/GeneralSQLSerializer.swift @@ -102,47 +102,59 @@ open class GeneralSQLSerializer: SQLSerializer { } open func count() -> (String, [Node]) { - var fragments: [String] = [] + var statement: [String] = [] var values: [Node] = [] - fragments += "SELECT COUNT(*) as _fluent_count FROM" - fragments += escape(E.entity) + statement += "SELECT COUNT(*) as _fluent_count FROM" + statement += escape(E.entity) if !query.joins.isEmpty { - fragments += joins(query.joins) + statement += joins(query.joins) } if !query.filters.isEmpty { let (filtersClause, filtersValues) = filters(query.filters) - fragments += filtersClause + statement += filtersClause values += filtersValues } return ( - concatenate(fragments), + concatenate(statement), values ) } open func delete() -> (String, [Node]) { - var fragments: [String] = [] + var statement: [String] = [] var values: [Node] = [] - fragments += "DELETE FROM" - fragments += escape(E.entity) + statement += "DELETE" + if !query.joins.isEmpty { + statement += escape(E.entity) + } + statement += "FROM" + statement += escape(E.entity) + + if !query.joins.isEmpty { + statement += joins(query.joins) + } if !query.filters.isEmpty { let (filtersClause, filtersValues) = filters(query.filters) - fragments += filtersClause + statement += filtersClause values += filtersValues } + + if !query.sorts.isEmpty { + statement += sorts(query.sorts) + } if let l = query.limits.first { - fragments += limit(l) + statement += limit(l) } return ( - concatenate(fragments), + concatenate(statement), values ) } diff --git a/Tests/FluentTests/SQLSerializerTests.swift b/Tests/FluentTests/SQLSerializerTests.swift index 6dac5690..f19e3051 100644 --- a/Tests/FluentTests/SQLSerializerTests.swift +++ b/Tests/FluentTests/SQLSerializerTests.swift @@ -205,4 +205,15 @@ class SQLSerializerTests: XCTestCase { XCTAssertEqual(statement, "SELECT `users`.* FROM `users` WHERE `users`.`age` > ? ORDER BY `users`.`name` ASC, `users`.`email` DESC") XCTAssertEqual(values.count, 1) } + + func testJoinedDelete() throws { + let query = Query(db) + try query.join(Atom.self) + try query.filter(Atom.self, "name", "Hydrogen") + try query.delete() + + let (statement, values) = serialize(query) + XCTAssertEqual(statement, "DELETE `compounds` FROM `compounds` JOIN `atoms` ON `compounds`.`id` = `atoms`.`compound_id` WHERE `atoms`.`name` = ?") + XCTAssertEqual(values.count, 1) + } } From 3365a70fbc053e8fe98656c3c8491b33a1205c00 Mon Sep 17 00:00:00 2001 From: tanner0101 Date: Wed, 22 Mar 2017 13:55:21 +0100 Subject: [PATCH 11/11] add raw note --- Sources/Fluent/Database/Executor.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/Fluent/Database/Executor.swift b/Sources/Fluent/Database/Executor.swift index f3932c51..675b641f 100644 --- a/Sources/Fluent/Database/Executor.swift +++ b/Sources/Fluent/Database/Executor.swift @@ -22,6 +22,10 @@ public protocol Executor { /// /// This allows Fluent extensions to be written that /// can support custom querying behavior. + /// + /// - note: Passing parameterized values as a `[Node]` array + /// instead of interpolating them into the raw string + /// can help prevent SQL injection. @discardableResult func raw(_ raw: String, _ values: [Node]) throws -> Node }