From ffbf0473c5ec7e90b7e1173cc576d51148337c3a Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Wed, 15 Jun 2016 16:55:14 -0400 Subject: [PATCH 01/10] file moving --- Sources/Database.swift | 32 -- Sources/Driver.swift | 24 -- Sources/Fluent+C7.swift | 3 - Sources/Fluent/Helper/oldsql.swift | 292 ++++++++++++++++++ Sources/{ => Fluent/Model}/Model.swift | 0 Sources/{ => Fluent/Model}/Value.swift | 4 +- Sources/{ => Fluent/Query}/Action.swift | 0 Sources/{ => Fluent/Query}/Comparison.swift | 0 Sources/{ => Fluent/Query}/Filter.swift | 0 Sources/{ => Fluent/Query}/Limit.swift | 0 Sources/{ => Fluent/Query}/Query.swift | 2 +- Sources/{ => Fluent/Query}/Scope.swift | 0 Sources/Fluent/SQL/GeneralSQLSerializer.swift | 55 ++++ Sources/Fluent/SQL/SQL+Builder.swift | 20 ++ Sources/Fluent/SQL/SQL+Query.swift | 18 ++ Sources/Fluent/SQL/SQL.swift | 16 + Sources/Fluent/SQL/SQLSerializer.swift | 19 ++ Sources/Fluent/Schema/Schema.swift | 34 ++ Sources/Fluent/Utilities/Fluent+C7.swift | 4 + .../Utilities}/Fluent+Polymorphic.swift | 0 .../Utilities}/Value+Polymorphic.swift | 0 Sources/Helper.swift | 21 -- Sources/PrintDriver.swift | 19 -- Sources/SQL.swift | 220 ------------- Tests/Fluent/ModelFindTests.swift | 18 +- Tests/Fluent/QueryFiltersTests.swift | 6 +- Tests/Fluent/SchemaCreateTests.swift | 25 ++ 27 files changed, 500 insertions(+), 332 deletions(-) delete mode 100644 Sources/Database.swift delete mode 100644 Sources/Driver.swift delete mode 100644 Sources/Fluent+C7.swift create mode 100644 Sources/Fluent/Helper/oldsql.swift rename Sources/{ => Fluent/Model}/Model.swift (100%) rename Sources/{ => Fluent/Model}/Value.swift (84%) rename Sources/{ => Fluent/Query}/Action.swift (100%) rename Sources/{ => Fluent/Query}/Comparison.swift (100%) rename Sources/{ => Fluent/Query}/Filter.swift (100%) rename Sources/{ => Fluent/Query}/Limit.swift (100%) rename Sources/{ => Fluent/Query}/Query.swift (98%) rename Sources/{ => Fluent/Query}/Scope.swift (100%) create mode 100644 Sources/Fluent/SQL/GeneralSQLSerializer.swift create mode 100644 Sources/Fluent/SQL/SQL+Builder.swift create mode 100644 Sources/Fluent/SQL/SQL+Query.swift create mode 100644 Sources/Fluent/SQL/SQL.swift create mode 100644 Sources/Fluent/SQL/SQLSerializer.swift create mode 100644 Sources/Fluent/Schema/Schema.swift create mode 100644 Sources/Fluent/Utilities/Fluent+C7.swift rename Sources/{ => Fluent/Utilities}/Fluent+Polymorphic.swift (100%) rename Sources/{ => Fluent/Utilities}/Value+Polymorphic.swift (100%) delete mode 100644 Sources/Helper.swift delete mode 100644 Sources/PrintDriver.swift delete mode 100644 Sources/SQL.swift create mode 100644 Tests/Fluent/SchemaCreateTests.swift diff --git a/Sources/Database.swift b/Sources/Database.swift deleted file mode 100644 index bf2b1ae8..00000000 --- a/Sources/Database.swift +++ /dev/null @@ -1,32 +0,0 @@ -/** - References a database with a single `Driver`. - Statically maps `Model`s to `Database`s. -*/ -public class Database { - /** - The `Driver` powering this database. - Responsible for executing queries. - */ - public let driver: Driver - - /** - Creates a `Database` with the supplied - `Driver`. This cannot be changed later. - */ - public init(driver: Driver) { - self.driver = driver - } - - /** - Maps `Model` names to their respective - `Database`. This allows multiple models - in the same application to use different - methods of data persistence. - */ - public static var map: [String: Database] = [:] - - /** - The default database for all `Model` types. - */ - public static var `default`: Database = Database(driver: PrintDriver()) -} \ No newline at end of file diff --git a/Sources/Driver.swift b/Sources/Driver.swift deleted file mode 100644 index 8982e571..00000000 --- a/Sources/Driver.swift +++ /dev/null @@ -1,24 +0,0 @@ -/** - A `Driver` execute queries - and returns an array of results. - It is responsible for interfacing - with the data store powering Fluent. -*/ -public protocol Driver { - /** - The string value for the - default identifier key. - - The `idKey` will be used when - `Model.find(_:)` or other find - by identifier methods are used. - */ - var idKey: String { get } - - /** - Executes a `Query` from and - returns an array of results fetched, - created, or updated by the action. - */ - func execute(_ query: Query) throws -> [[String: Value]] -} diff --git a/Sources/Fluent+C7.swift b/Sources/Fluent+C7.swift deleted file mode 100644 index a5f6722e..00000000 --- a/Sources/Fluent+C7.swift +++ /dev/null @@ -1,3 +0,0 @@ -import C7 - -public typealias StructuredData = C7.StructuredData \ No newline at end of file diff --git a/Sources/Fluent/Helper/oldsql.swift b/Sources/Fluent/Helper/oldsql.swift new file mode 100644 index 00000000..b1f96a01 --- /dev/null +++ b/Sources/Fluent/Helper/oldsql.swift @@ -0,0 +1,292 @@ +/** + A helper for creating generic + SQL statements from Fluent queries. + + Subclass this to support specific + SQL flavors, such as MySQL. +*/ + +/* +public class SQLClass { + public enum TableAction { + case create, alter + } + + public enum Action { + case select + case update + case insert + case delete + case table(TableAction) + } + + public struct Limit { + var count: Int + + } + + public enum Column { + case integer(String) + case string(String, Int) + } + + + /** + The table to query. + */ + public let table: String + public let action: Action + public let limit: Limit? + public let filters: [Filter] + public let data: [String: Value?]? + public let columns: [Column] + + public init(query: Query) { + table = query.entity + + switch query.action { + case .create: + action = .insert + case .delete: + action = .delete + case .fetch: + action = .select + case .update: + action = .update + } + + if let count = query.limit?.count { + limit = Limit(count: count) + } else { + limit = nil + } + + filters = query.filters + data = query.data + columns = [] + } + + public init(builder: Schema.Builder) { + table = builder.entity + action = .table(.create) + + columns = builder.fields.map { field in + switch field { + case .int(let name): + return .integer(name) + case .string(let name, let length): + return .string(name, length) + } + } + + limit = nil + filters = [] + data = nil + } + + public init( + table: String, + action: Action, + limit: Limit? = nil, + filters: [Filter] = [], + data: [String: Value?]? = nil, + columns: [Column] = [] + ) { + self.table = table + self.action = action + self.limit = limit + self.filters = filters + self.data = data + self.columns = [] + } + + + /** + The SQL statement string. + */ + public var statement: (query: String, values: [Value]) { + var values: [Value] = [] + + var statement = [action.sql] + + statement.append(table) + + switch action { + case .table(_): + if let columnClause = columnClause { + statement.append(columnClause) + } + case .select, .insert, .delete, .update: + if let (dataString, dataValues) = dataClause { + statement.append(dataString) + values += dataValues + } + + if let whereClause = whereClause { + statement.append("WHERE \(whereClause)") + } + + if let limit = limit where limit.count > 0 { + statement.append(limit.sql) + } + } + + let string = "\(statement.joined(separator: " "));" + return (string, values) + } + + /** + The next placeholder to use in + place of a value for parameterization. + */ + public var nextPlaceholder: String { + return "?" + } + + var columnClause: String? { + guard case .table(let action) = action else { + return nil + } + + var clause: [String] = [] + + switch action { + case .alter: + return "" // TODO + case .create: + for column in columns { + clause.append(column.sql) + } + } + + let string = clause.joined(separator: ",") + return "(" + string + ")" + } + + /** + The data clause containing + values for INSERT and UPDATE queries. + */ + var dataClause: (String, [Value])? { + guard let data = data else { + return nil + } + + switch action { + case .insert, .update: + // continue + break + default: + return nil + } + + let values: [Value] = data.values.flatMap { value in + return value + } + + let clause: String + + if case .insert = action { + let fields = data.keys.joined(separator: ", ") + + let values = data.flatMap { key, value in + return value.sql(placeholder: nextPlaceholder) + }.joined(separator: ", ") + + clause = "(\(fields)) VALUES (\(values))" + } else if case .update = action { + let updates = data.flatMap { key, value in + let string = value.sql(placeholder: nextPlaceholder) + return "\(key) = \(string)" + }.joined(separator: ", ") + + clause = "SET \(updates)" + } else { + return nil + } + + return (clause, values) + } + + /** + The where clause that filters + SELECT, UPDATE, and DELETE queries. + */ + var whereClause: (String, [Value])? { + if filters.count == 0 { + return nil + } + + var values: [Value] = [] + + for filter in filters { + switch filter { + case .compare(_, _, let compareValue): + values.append(compareValue) + case .subset(_, _, let subsetValues): + values += subsetValues + } + } + + var clause: [String] = [] + + for filter in filters { + let sql = filter.sql(placeholder: nextPlaceholder) + clause.append(sql) + } + + let string = clause.joined(separator: " AND ") + return (string, values) + } + +} + +extension Filter { + /** + Translates a filter to SQL. + */ + func sql(placeholder: String) -> String { + switch self { + case .compare(let field, let comparison, _): + return "\(field) \(comparison.sql) \(placeholder)" + case .subset(let field, let scope, let values): + let string = values.map { value in + return placeholder + }.joined(separator: ", ") + + return "\(field) \(scope.sql) (\(string))" + } + } +} + + +/** + Allows optionals to be targeted + in protocol extensions +*/ +private protocol Extractable { + associatedtype Wrapped + func extract() -> Wrapped? +} + +/** + Conforms `Optional` +*/ +extension Optional: Extractable { + private func extract() -> Wrapped? { + return self + } +} + +/** + Protocol extensions for `Value?` +*/ +extension Extractable where Wrapped == Value { + /** + Translates a `Value?` to SQL. + */ + func sql(placeholder: String) -> String { + return self.extract()?.sql(placeholder: placeholder) ?? "NULL" + } +}*/ + diff --git a/Sources/Model.swift b/Sources/Fluent/Model/Model.swift similarity index 100% rename from Sources/Model.swift rename to Sources/Fluent/Model/Model.swift diff --git a/Sources/Value.swift b/Sources/Fluent/Model/Value.swift similarity index 84% rename from Sources/Value.swift rename to Sources/Fluent/Model/Value.swift index b6f49d78..6e7a9b4b 100644 --- a/Sources/Value.swift +++ b/Sources/Fluent/Model/Value.swift @@ -2,9 +2,7 @@ A type of data that can be retrieved or stored in a database. */ -public protocol Value: CustomStringConvertible, Polymorphic { - var structuredData: StructuredData { get } -} +public protocol Value: CustomStringConvertible, StructuredDataRepresentable, Polymorphic {} extension Int: Value { public var structuredData: StructuredData { diff --git a/Sources/Action.swift b/Sources/Fluent/Query/Action.swift similarity index 100% rename from Sources/Action.swift rename to Sources/Fluent/Query/Action.swift diff --git a/Sources/Comparison.swift b/Sources/Fluent/Query/Comparison.swift similarity index 100% rename from Sources/Comparison.swift rename to Sources/Fluent/Query/Comparison.swift diff --git a/Sources/Filter.swift b/Sources/Fluent/Query/Filter.swift similarity index 100% rename from Sources/Filter.swift rename to Sources/Fluent/Query/Filter.swift diff --git a/Sources/Limit.swift b/Sources/Fluent/Query/Limit.swift similarity index 100% rename from Sources/Limit.swift rename to Sources/Fluent/Query/Limit.swift diff --git a/Sources/Query.swift b/Sources/Fluent/Query/Query.swift similarity index 98% rename from Sources/Query.swift rename to Sources/Fluent/Query/Query.swift index a0aeca5f..209b874d 100644 --- a/Sources/Query.swift +++ b/Sources/Fluent/Query/Query.swift @@ -65,7 +65,7 @@ public class Query { func run() throws -> [T] { var models: [T] = [] - let results = try database.driver.execute(self) + let results = try database.driver.query(self) for result in results { guard var model = T(serialized: result) else { diff --git a/Sources/Scope.swift b/Sources/Fluent/Query/Scope.swift similarity index 100% rename from Sources/Scope.swift rename to Sources/Fluent/Query/Scope.swift diff --git a/Sources/Fluent/SQL/GeneralSQLSerializer.swift b/Sources/Fluent/SQL/GeneralSQLSerializer.swift new file mode 100644 index 00000000..55429061 --- /dev/null +++ b/Sources/Fluent/SQL/GeneralSQLSerializer.swift @@ -0,0 +1,55 @@ +class GeneralSQLSerializer: SQLSerializer { + let sql: SQL + + required init(sql: SQL) { + self.sql = sql + } + + func serialize() -> (String, [Value]) { + switch sql { + case .table(let action, let table, let columns): + var statement: [String] = [] + + statement += makeSQL(action) + statement += makeSQL(table) + statement += makeSQL(columns) + + return ( + statement.joined(separator: " "), + [] + ) + default: + return ("", []) + } + } + + func makeSQL(_ tableAction: SQL.TableAction) -> String { + switch tableAction { + case .alter: + return "ALTER TABLE" + case .create: + return "CREATE TABLE" + } + } + + func makeSQL(_ column: SQL.Column) -> String { + switch column { + case .integer(let name): + return makeSQL(name) + " INTEGER" + case .string(let name, let length): + return makeSQL(name) + " VARCHAR(\(length))" + } + } + + func makeSQL(_ columns: [SQL.Column]) -> String { + return "(" + columns.map { makeSQL($0) }.joined(separator: ", ") + ")" + } + + func makeSQL(_ string: String) -> String { + return "`\(string)`" + } +} + +func +=(lhs: inout [String], rhs: String) { + lhs.append(rhs) +} diff --git a/Sources/Fluent/SQL/SQL+Builder.swift b/Sources/Fluent/SQL/SQL+Builder.swift new file mode 100644 index 00000000..539ef3ab --- /dev/null +++ b/Sources/Fluent/SQL/SQL+Builder.swift @@ -0,0 +1,20 @@ +extension SQL { + init(builder: Schema.Builder) { + var columns: [Column] = [] + + for field in builder.fields { + let column: Column + + switch field { + case .int(let name): + column = .integer(name) + case .string(let name, let length): + column = .string(name, length) + } + + columns.append(column) + } + + self = .table(action: .create, table: builder.entity, columns: columns) + } +} diff --git a/Sources/Fluent/SQL/SQL+Query.swift b/Sources/Fluent/SQL/SQL+Query.swift new file mode 100644 index 00000000..81be1ebe --- /dev/null +++ b/Sources/Fluent/SQL/SQL+Query.swift @@ -0,0 +1,18 @@ +extension SQL { + init(query: Query) { + switch query.action { + case .fetch: + self = .select( + table: query.entity, + filters: query.filters, + limit: query.limit?.count + ) + default: + self = .select( + table: "", + filters: [], + limit: 0 + ) + } + } +} diff --git a/Sources/Fluent/SQL/SQL.swift b/Sources/Fluent/SQL/SQL.swift new file mode 100644 index 00000000..ea13cd68 --- /dev/null +++ b/Sources/Fluent/SQL/SQL.swift @@ -0,0 +1,16 @@ +enum SQL { + enum TableAction { + case create, alter + } + + enum Column { + case integer(String) + case string(String, Int) + } + + case insert(table: String, data: [String: Value]) + case select(table: String, filters: [Filter], limit: Int?) + case update(table: String, filters: [Filter], data: [String: Value]) + case delete(table: String, filters: [Filter], limit: Int?) + case table(action: TableAction, table: String, columns: [Column]) +} diff --git a/Sources/Fluent/SQL/SQLSerializer.swift b/Sources/Fluent/SQL/SQLSerializer.swift new file mode 100644 index 00000000..9c18538c --- /dev/null +++ b/Sources/Fluent/SQL/SQLSerializer.swift @@ -0,0 +1,19 @@ +protocol SQLSerializer { + init(sql: SQL) + func serialize() -> (String, [Value]) +} + +final class SQLiteSerializer: GeneralSQLSerializer { + override func makeSQL(_ column: SQL.Column) -> String { + switch column { + case .integer(let name): + return makeSQL(name) + " INTEGER" + case .string(let name, _): + return makeSQL(name) + " TEXT" + } + } +} + +final class MySQLSerializer: GeneralSQLSerializer { + +} diff --git a/Sources/Fluent/Schema/Schema.swift b/Sources/Fluent/Schema/Schema.swift new file mode 100644 index 00000000..2098a6e7 --- /dev/null +++ b/Sources/Fluent/Schema/Schema.swift @@ -0,0 +1,34 @@ +public final class Schema { + + public static func build(_ entity: String, closure: (Builder) -> ()) throws { + let builder = Builder(entity) + closure(builder) + _ = try Database.default.driver.build(builder) + } + +} + +extension Schema { + public enum Field { + case int(String) + case string(String, Int) + } + + public final class Builder { + public let entity: String + public var fields: [Field] + + public init(_ entity: String) { + self.entity = entity + fields = [] + } + + public func int(_ name: String) { + fields.append(.int(name)) + } + + public func string(_ name: String, length: Int = 128) { + fields.append(.string(name, length)) + } + } +} diff --git a/Sources/Fluent/Utilities/Fluent+C7.swift b/Sources/Fluent/Utilities/Fluent+C7.swift new file mode 100644 index 00000000..f62d313c --- /dev/null +++ b/Sources/Fluent/Utilities/Fluent+C7.swift @@ -0,0 +1,4 @@ +import C7 + +public typealias StructuredData = C7.StructuredData +public typealias StructuredDataRepresentable = C7.StructuredDataRepresentable \ No newline at end of file diff --git a/Sources/Fluent+Polymorphic.swift b/Sources/Fluent/Utilities/Fluent+Polymorphic.swift similarity index 100% rename from Sources/Fluent+Polymorphic.swift rename to Sources/Fluent/Utilities/Fluent+Polymorphic.swift diff --git a/Sources/Value+Polymorphic.swift b/Sources/Fluent/Utilities/Value+Polymorphic.swift similarity index 100% rename from Sources/Value+Polymorphic.swift rename to Sources/Fluent/Utilities/Value+Polymorphic.swift diff --git a/Sources/Helper.swift b/Sources/Helper.swift deleted file mode 100644 index fd4025dd..00000000 --- a/Sources/Helper.swift +++ /dev/null @@ -1,21 +0,0 @@ -/** - Subclass `Helper` to provide - support translating a Fluent `Query` - to a `Driver`s native database language. -*/ -public class Helper { - - /** - The `Query` that is being - interpreted by the `Helper` - */ - var query: Query - - /** - Creates a new `Helper` with a - given `Query` - */ - public init(query: Query) { - self.query = query - } -} \ No newline at end of file diff --git a/Sources/PrintDriver.swift b/Sources/PrintDriver.swift deleted file mode 100644 index 03ec59bc..00000000 --- a/Sources/PrintDriver.swift +++ /dev/null @@ -1,19 +0,0 @@ -/** - A dummy `Driver` useful for developing. -*/ -public class PrintDriver: Driver { - public var idKey: String = "foo" - - public func execute(_ query: Query) throws -> [[String : Value]] { - let sql = SQL(query: query) - - print("Statement: \(sql.statement) Values: \(sql.values)") - - print("Table \(query.entity)") - print("Action \(query.action)") - print("Filters \(query.filters)") - print() - - return [] - } -} \ No newline at end of file diff --git a/Sources/SQL.swift b/Sources/SQL.swift deleted file mode 100644 index 024ea338..00000000 --- a/Sources/SQL.swift +++ /dev/null @@ -1,220 +0,0 @@ -/** - A helper for creating generic - SQL statements from Fluent queries. - - Subclass this to support specific - SQL flavors, such as MySQL. -*/ -public class SQL: Helper { - /** - The values to be parameterized - into the statement. - */ - public var values: [Value] - - /** - The SQL statement string. - */ - public var statement: String { - values = [] - - var statement = [query.action.sql] - statement.append(table) - - if let dataClause = dataClause { - statement.append(dataClause) - } - - if let whereClause = whereClause { - statement.append("WHERE \(whereClause)") - } - - if let limit = query.limit where limit.count > 0 { - statement.append(limit.sql) - } - - return "\(statement.joined(separator: " "));" - } - - /** - The next placeholder to use in - place of a value for parameterization. - */ - public var nextPlaceholder: String { - return "?" - } - - /** - The table to query. - */ - var table: String { - return query.entity - } - - /** - The data clause containing - values for INSERT and UPDATE queries. - */ - var dataClause: String? { - guard let data = query.data else { - return nil - } - - guard query.action == .create || query.action == .update else { - return nil - } - - values += data.values.flatMap { value in - return value - } - - var clause: String? - - if case .create = query.action { - let fields = data.keys.joined(separator: ", ") - - let values = data.flatMap { key, value in - return value.sql(placeholder: nextPlaceholder) - }.joined(separator: ", ") - - clause = "(\(fields)) VALUES (\(values))" - } else if case .update = query.action { - let updates = data.flatMap { key, value in - let string = value.sql(placeholder: nextPlaceholder) - return "\(key) = \(string)" - }.joined(separator: ", ") - - clause = "SET \(updates)" - } - - return clause - } - - /** - The where clause that filters - SELECT, UPDATE, and DELETE queries. - */ - var whereClause: String? { - if query.filters.count == 0 { - return nil - } - - for filter in query.filters { - switch filter { - case .compare(_, _, let value): - values.append(value) - case .subset(_, _, let values): - self.values += values - } - } - - var clause: [String] = [] - - for filter in query.filters { - let sql = filter.sql(placeholder: nextPlaceholder) - clause.append(sql) - } - - return clause.joined(separator: " AND ") - } - - /** - Creates a SQL helper for the - given query. - */ - public override init(query: Query) { - values = [] - super.init(query: query) - } -} - -extension Filter { - /** - Translates a filter to SQL. - */ - func sql(placeholder: String) -> String { - switch self { - case .compare(let field, let comparison, _): - return "\(field) \(comparison.sql) \(placeholder)" - case .subset(let field, let scope, let values): - let string = values.map { value in - return placeholder - }.joined(separator: ", ") - - return "\(field) \(scope.sql) (\(string))" - } - } -} - -extension Action { - /** - Translates an action to SQL. - */ - var sql: String { - switch self { - case .fetch: - return "SELECT * FROM" - case .delete: - return "DELETE FROM" - case .create: - return "INSERT INTO" - case .update: - return "UPDATE" - } - } -} - - -/** - Allows optionals to be targeted - in protocol extensions -*/ -private protocol Extractable { - associatedtype Wrapped - func extract() -> Wrapped? -} - -/** - Conforms `Optional` -*/ -extension Optional: Extractable { - private func extract() -> Wrapped? { - return self - } -} - -/** - Protocol extensions for `Value?` -*/ -extension Extractable where Wrapped == Value { - /** - Translates a `Value?` to SQL. - */ - func sql(placeholder: String) -> String { - return self.extract()?.sql(placeholder: placeholder) ?? "NULL" - } -} - -extension Value { - /** - Translates a `Value` to SQL. - */ - func sql(placeholder: String) -> String { - switch structuredData { - case .null: - return "NULL" - default: - return placeholder - } - } -} - -extension Limit { - /** - Translates a `Limit` to SQL. - */ - var sql: String { - return "LIMIT \(count)" - } -} - diff --git a/Tests/Fluent/ModelFindTests.swift b/Tests/Fluent/ModelFindTests.swift index f7ee7643..68de80e0 100644 --- a/Tests/Fluent/ModelFindTests.swift +++ b/Tests/Fluent/ModelFindTests.swift @@ -30,7 +30,7 @@ class ModelFindTests: XCTestCase { case broken } - func execute(_ query: Query) throws -> [[String: Value]] { + func query(_ query: Query) throws -> [[String: Value]] { if let filter = query.filters.first, case .compare(let key, let comparison, let value) = filter @@ -50,16 +50,18 @@ class ModelFindTests: XCTestCase { return [] } - } - static var allTests : [(String, (ModelFindTests) -> () throws -> Void)] { - return [ - ("testFindFailing", testFindFailing), - ("testFindSucceeding", testFindSucceeding), - ("testFindErroring", testFindErroring), - ] + func build(_ builder: Schema.Builder) throws { + // + } } + static let allTests = [ + ("testFindFailing", testFindFailing), + ("testFindSucceeding", testFindSucceeding), + ("testFindErroring", testFindErroring), + ] + override func setUp() { database = Database(driver: DummyDriver()) Database.default = database diff --git a/Tests/Fluent/QueryFiltersTests.swift b/Tests/Fluent/QueryFiltersTests.swift index feeb7043..ecbdddf9 100644 --- a/Tests/Fluent/QueryFiltersTests.swift +++ b/Tests/Fluent/QueryFiltersTests.swift @@ -27,9 +27,13 @@ class QueryFiltersTests: XCTestCase { case broken } - func execute(_ query: Query) throws -> [[String: Value]] { + func query(_ query: Query) throws -> [[String: Value]] { return [] } + + func build(_ builder: Schema.Builder) throws { + + } } static var allTests : [(String, (QueryFiltersTests) -> () throws -> Void)] { diff --git a/Tests/Fluent/SchemaCreateTests.swift b/Tests/Fluent/SchemaCreateTests.swift new file mode 100644 index 00000000..70996fae --- /dev/null +++ b/Tests/Fluent/SchemaCreateTests.swift @@ -0,0 +1,25 @@ +import XCTest +@testable import Fluent + +class SchemaCreateTests: XCTestCase { + static let allTests = [ + ("testCreate", testCreate), + ] + + func testCreate() throws { + let builder = Schema.Builder("users") + + builder.int("id") + builder.string("name") + builder.string("email", length: 256) + + let sql = SQL(builder: builder) + + let serializer = GeneralSQLSerializer(sql: sql) + let sqliteSerializer = SQLiteSerializer(sql: sql) + + print(serializer.serialize()) + print(sqliteSerializer.serialize()) + } + +} From 57d8c76935cfe4f0beedd498e0d65892a162d377 Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Wed, 15 Jun 2016 17:40:53 -0400 Subject: [PATCH 02/10] aliases --- Package.swift | 35 ++++++++++++++++++- Sources/Fluent/SQL/GeneralSQLSerializer.swift | 18 +++++----- Sources/Fluent/SQL/SQL+Query.swift | 6 ++++ Sources/Fluent/SQL/SQL.swift | 6 ++-- Sources/Fluent/SQL/SQLSerializer.swift | 6 +--- Sources/FluentMySQL | 1 + Sources/FluentSQLite | 1 + Sources/MySQL | 1 + Sources/SQLite | 1 + Tests/FluentMySQL | 1 + Tests/FluentSQLite | 1 + Tests/MySQL | 1 + Tests/SQLite | 1 + 13 files changed, 61 insertions(+), 18 deletions(-) create mode 120000 Sources/FluentMySQL create mode 120000 Sources/FluentSQLite create mode 120000 Sources/MySQL create mode 120000 Sources/SQLite create mode 120000 Tests/FluentMySQL create mode 120000 Tests/FluentSQLite create mode 120000 Tests/MySQL create mode 120000 Tests/SQLite diff --git a/Package.swift b/Package.swift index 07e7015d..aa29b999 100644 --- a/Package.swift +++ b/Package.swift @@ -7,6 +7,39 @@ let package = Package( .Package(url: "https://github.com/open-swift/C7.git", majorVersion: 0, minor: 9), // Syntax for easily accessing values from generic data. - .Package(url: "https://github.com/qutheory/polymorphic.git", majorVersion: 0, minor: 2) + .Package(url: "https://github.com/qutheory/polymorphic.git", majorVersion: 0, minor: 2), + + .Package(url: "https://github.com/qutheory/cmysql.git", majorVersion: 0, minor: 1), + + .Package(url: "https://github.com/qutheory/csqlite.git", majorVersion: 0, minor: 1), + + .Package(url: "https://github.com/qutheory/libc.git", majorVersion: 0, minor: 1), + ], + targets: [ + Target( + name: "FluentMySQL", + dependencies: [ + .Target(name: "MySQL"), + .Target(name: "Fluent") + ] + ), + Target( + name: "MySQL" + ), + + Target( + name: "FluentSQLite", + dependencies: [ + .Target(name: "SQLite"), + .Target(name: "Fluent") + ] + ), + Target( + name: "SQLite" + ), + + Target( + name: "Fluent" + ), ] ) diff --git a/Sources/Fluent/SQL/GeneralSQLSerializer.swift b/Sources/Fluent/SQL/GeneralSQLSerializer.swift index 55429061..366f3b93 100644 --- a/Sources/Fluent/SQL/GeneralSQLSerializer.swift +++ b/Sources/Fluent/SQL/GeneralSQLSerializer.swift @@ -1,11 +1,11 @@ -class GeneralSQLSerializer: SQLSerializer { - let sql: SQL +public class GeneralSQLSerializer: SQLSerializer { + public let sql: SQL - required init(sql: SQL) { + public required init(sql: SQL) { self.sql = sql } - func serialize() -> (String, [Value]) { + public func serialize() -> (String, [Value]) { switch sql { case .table(let action, let table, let columns): var statement: [String] = [] @@ -23,7 +23,7 @@ class GeneralSQLSerializer: SQLSerializer { } } - func makeSQL(_ tableAction: SQL.TableAction) -> String { + public func makeSQL(_ tableAction: SQL.TableAction) -> String { switch tableAction { case .alter: return "ALTER TABLE" @@ -32,7 +32,7 @@ class GeneralSQLSerializer: SQLSerializer { } } - func makeSQL(_ column: SQL.Column) -> String { + public func makeSQL(_ column: SQL.Column) -> String { switch column { case .integer(let name): return makeSQL(name) + " INTEGER" @@ -41,15 +41,15 @@ class GeneralSQLSerializer: SQLSerializer { } } - func makeSQL(_ columns: [SQL.Column]) -> String { + public func makeSQL(_ columns: [SQL.Column]) -> String { return "(" + columns.map { makeSQL($0) }.joined(separator: ", ") + ")" } - func makeSQL(_ string: String) -> String { + public func makeSQL(_ string: String) -> String { return "`\(string)`" } } -func +=(lhs: inout [String], rhs: String) { +public func +=(lhs: inout [String], rhs: String) { lhs.append(rhs) } diff --git a/Sources/Fluent/SQL/SQL+Query.swift b/Sources/Fluent/SQL/SQL+Query.swift index 81be1ebe..c6f9f76c 100644 --- a/Sources/Fluent/SQL/SQL+Query.swift +++ b/Sources/Fluent/SQL/SQL+Query.swift @@ -16,3 +16,9 @@ extension SQL { } } } + +extension Query { + public var sql: SQL { + return SQL(query: self) + } +} diff --git a/Sources/Fluent/SQL/SQL.swift b/Sources/Fluent/SQL/SQL.swift index ea13cd68..1d89e12b 100644 --- a/Sources/Fluent/SQL/SQL.swift +++ b/Sources/Fluent/SQL/SQL.swift @@ -1,9 +1,9 @@ -enum SQL { - enum TableAction { +public enum SQL { + public enum TableAction { case create, alter } - enum Column { + public enum Column { case integer(String) case string(String, Int) } diff --git a/Sources/Fluent/SQL/SQLSerializer.swift b/Sources/Fluent/SQL/SQLSerializer.swift index 9c18538c..a2f4d6b6 100644 --- a/Sources/Fluent/SQL/SQLSerializer.swift +++ b/Sources/Fluent/SQL/SQLSerializer.swift @@ -1,4 +1,4 @@ -protocol SQLSerializer { +public protocol SQLSerializer { init(sql: SQL) func serialize() -> (String, [Value]) } @@ -13,7 +13,3 @@ final class SQLiteSerializer: GeneralSQLSerializer { } } } - -final class MySQLSerializer: GeneralSQLSerializer { - -} diff --git a/Sources/FluentMySQL b/Sources/FluentMySQL new file mode 120000 index 00000000..1c464464 --- /dev/null +++ b/Sources/FluentMySQL @@ -0,0 +1 @@ +../../fluent-mysql/Sources/FluentMySQL/ \ No newline at end of file diff --git a/Sources/FluentSQLite b/Sources/FluentSQLite new file mode 120000 index 00000000..e47950df --- /dev/null +++ b/Sources/FluentSQLite @@ -0,0 +1 @@ +../../fluent-sqlite/Sources/FluentSQLite/ \ No newline at end of file diff --git a/Sources/MySQL b/Sources/MySQL new file mode 120000 index 00000000..d32a457b --- /dev/null +++ b/Sources/MySQL @@ -0,0 +1 @@ +../../mysql/Sources/MySQL/ \ No newline at end of file diff --git a/Sources/SQLite b/Sources/SQLite new file mode 120000 index 00000000..4475c0c4 --- /dev/null +++ b/Sources/SQLite @@ -0,0 +1 @@ +../../sqlite/Sources/SQLite/ \ No newline at end of file diff --git a/Tests/FluentMySQL b/Tests/FluentMySQL new file mode 120000 index 00000000..a0fe0fc5 --- /dev/null +++ b/Tests/FluentMySQL @@ -0,0 +1 @@ +../../fluent-mysql/Tests/FluentMySQL/ \ No newline at end of file diff --git a/Tests/FluentSQLite b/Tests/FluentSQLite new file mode 120000 index 00000000..caf74a6f --- /dev/null +++ b/Tests/FluentSQLite @@ -0,0 +1 @@ +../../fluent-sqlite/Tests/FluentSQLite/ \ No newline at end of file diff --git a/Tests/MySQL b/Tests/MySQL new file mode 120000 index 00000000..c16f388d --- /dev/null +++ b/Tests/MySQL @@ -0,0 +1 @@ +../../mysql/Tests/MySQL/ \ No newline at end of file diff --git a/Tests/SQLite b/Tests/SQLite new file mode 120000 index 00000000..bb9955e9 --- /dev/null +++ b/Tests/SQLite @@ -0,0 +1 @@ +../../sqlite/Tests/SQLite/ \ No newline at end of file From 791551e8f2f4b032e0b19a900a807c1914538eb8 Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Wed, 15 Jun 2016 22:46:37 -0400 Subject: [PATCH 03/10] select support --- Sources/Fluent/Model/Value.swift | 29 ++++ Sources/Fluent/Query/Query.swift | 16 +- Sources/Fluent/SQL/GeneralSQLSerializer.swift | 149 ++++++++++++++++-- Sources/Fluent/SQL/SQL+Query.swift | 21 ++- Sources/Fluent/SQL/SQLSerializer.swift | 6 +- 5 files changed, 199 insertions(+), 22 deletions(-) diff --git a/Sources/Fluent/Model/Value.swift b/Sources/Fluent/Model/Value.swift index 6e7a9b4b..a8ca4f88 100644 --- a/Sources/Fluent/Model/Value.swift +++ b/Sources/Fluent/Model/Value.swift @@ -31,3 +31,32 @@ extension Float: Value { return .double(Double(self)) } } + +extension StructuredData: Fluent.Value { + public var structuredData: StructuredData { + return self + } +} + +extension StructuredData: CustomStringConvertible { + public var description: String { + switch self { + case .array(let array): + return array.description + case .bool(let bool): + return bool.description + case .data(let data): + return data.description + case .dictionary(let dict): + return dict.description + case .double(let double): + return double.description + case .int(let int): + return int.description + case .null: + return "NULL" + case .string(let string): + return string + } + } +} diff --git a/Sources/Fluent/Query/Query.swift b/Sources/Fluent/Query/Query.swift index 209b874d..c8119304 100644 --- a/Sources/Fluent/Query/Query.swift +++ b/Sources/Fluent/Query/Query.swift @@ -21,7 +21,7 @@ public class Query { Optional data to be used during `.create` or `.updated` actions. */ - public var data: [String: Value?]? + public var data: [String: Value]? /** Optionally limit the amount of @@ -108,7 +108,7 @@ public class Query { */ public func create(_ serialized: [String: Value?]) throws -> T? { action = .create - data = serialized + data = nilToNull(serialized) return try run().first } @@ -164,7 +164,7 @@ public class Query { */ public func update(_ serialized: [String: Value?]) throws { action = .update - data = serialized + data = nilToNull(serialized) let _ = try run() // discardableResult } @@ -208,6 +208,16 @@ public class Query { return filter(field, .equals, value) } + private func nilToNull(_ serialized: [String: Value?]) -> [String: Value] { + var converted: [String: Value] = [:] + + for (key, value) in serialized { + converted[key] = value ?? StructuredData.null + } + + return converted + } + } extension Query: CustomStringConvertible { diff --git a/Sources/Fluent/SQL/GeneralSQLSerializer.swift b/Sources/Fluent/SQL/GeneralSQLSerializer.swift index 366f3b93..a94f7de7 100644 --- a/Sources/Fluent/SQL/GeneralSQLSerializer.swift +++ b/Sources/Fluent/SQL/GeneralSQLSerializer.swift @@ -10,20 +10,121 @@ public class GeneralSQLSerializer: SQLSerializer { case .table(let action, let table, let columns): var statement: [String] = [] - statement += makeSQL(action) - statement += makeSQL(table) - statement += makeSQL(columns) + statement += sql(action) + statement += sql(table) + statement += sql(columns) return ( statement.joined(separator: " "), [] ) - default: + case .insert(let table, let data): + var statement: [String] = [] + + statement += "INSERT INTO" + statement += sql(table) + statement += sql(data) + + return ( + statement.joined(separator: " "), + Array(data.values) + ) + case .select(let table, let filters, let limit): + var statement: [String] = [] + + statement += "SELECT * FROM" + statement += sql(table) + let (clause, values) = sql(filters) + statement += clause + + if let limit = limit { + statement += "LIMIT" + statement += limit.description + } + + return ( + statement.joined(separator: " "), + values + ) + case .delete(let table, let filters, let limit): + return ("", []) + case .update(let table, let filters, let data): return ("", []) } } - public func makeSQL(_ tableAction: SQL.TableAction) -> String { + public func sql(_ filters: [Filter]) -> (String, [Value]) { + var statement: [String] = [] + var values: [Value] = [] + + statement += "WHERE" + + var subStatement: [String] = [] + + for filter in filters { + let (clause, subValues) = sql(filter) + subStatement += clause + values += subValues + } + + statement += subStatement.joined(separator: "AND") + + return ( + statement.joined(separator: " "), + values + ) + } + + public func sql(_ filter: Filter) -> (String, [Value]) { + var statement: [String] = [] + var values: [Value] = [] + + switch filter { + case .compare(let key, let comparison, let value): + statement += sql(key) + statement += sql(comparison) + statement += "?" + values += value + case .subset(let key, let scope, let subValues): + statement += sql(key) + statement += sql(scope) + statement += sql(subValues) + values += subValues + } + + return ( + statement.joined(separator: " "), + values + ) + } + + public 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 "!=" + } + } + + public func sql(_ scope: Filter.Scope) -> String { + switch scope { + case .in: + return "IN" + case .notIn: + return "NOT IN" + } + } + + public func sql(_ tableAction: SQL.TableAction) -> String { switch tableAction { case .alter: return "ALTER TABLE" @@ -32,20 +133,42 @@ public class GeneralSQLSerializer: SQLSerializer { } } - public func makeSQL(_ column: SQL.Column) -> String { + public func sql(_ column: SQL.Column) -> String { switch column { case .integer(let name): - return makeSQL(name) + " INTEGER" + return sql(name) + " INTEGER" case .string(let name, let length): - return makeSQL(name) + " VARCHAR(\(length))" + return sql(name) + " VARCHAR(\(length))" } } - public func makeSQL(_ columns: [SQL.Column]) -> String { - return "(" + columns.map { makeSQL($0) }.joined(separator: ", ") + ")" + public func sql(_ data: [String: Value]) -> String { + var clause: [String] = [] + + clause += sql(Array(data.keys)) + clause += "VALUES" + clause += sql(Array(data.values)) + + return clause.joined(separator: " ") } - public func makeSQL(_ string: String) -> String { + public func sql(_ columns: [String]) -> String { + return "(" + columns.joined(separator: ",") + ")" + } + + public func sql(_ values: [Value]) -> String { + return "(" + values.map { sql($0) }.joined(separator: ",") + ")" + } + + public func sql(_ value: Value) -> String { + return "?" + } + + public func sql(_ columns: [SQL.Column]) -> String { + return "(" + columns.map { sql($0) }.joined(separator: ", ") + ")" + } + + public func sql(_ string: String) -> String { return "`\(string)`" } } @@ -53,3 +176,7 @@ public class GeneralSQLSerializer: SQLSerializer { public func +=(lhs: inout [String], rhs: String) { lhs.append(rhs) } + +public func +=(lhs: inout [Value], rhs: Value) { + lhs.append(rhs) +} diff --git a/Sources/Fluent/SQL/SQL+Query.swift b/Sources/Fluent/SQL/SQL+Query.swift index c6f9f76c..c4acbfa9 100644 --- a/Sources/Fluent/SQL/SQL+Query.swift +++ b/Sources/Fluent/SQL/SQL+Query.swift @@ -7,11 +7,22 @@ extension SQL { filters: query.filters, limit: query.limit?.count ) - default: - self = .select( - table: "", - filters: [], - limit: 0 + case .create: + self = .insert( + table: query.entity, + data: query.data ?? [:] + ) + case .delete: + self = .delete( + table: query.entity, + filters: query.filters, + limit: query.limit?.count + ) + case .update: + self = .update( + table: query.entity, + filters: query.filters, + data: query.data ?? [:] ) } } diff --git a/Sources/Fluent/SQL/SQLSerializer.swift b/Sources/Fluent/SQL/SQLSerializer.swift index a2f4d6b6..5ab17d1c 100644 --- a/Sources/Fluent/SQL/SQLSerializer.swift +++ b/Sources/Fluent/SQL/SQLSerializer.swift @@ -4,12 +4,12 @@ public protocol SQLSerializer { } final class SQLiteSerializer: GeneralSQLSerializer { - override func makeSQL(_ column: SQL.Column) -> String { + override func sql(_ column: SQL.Column) -> String { switch column { case .integer(let name): - return makeSQL(name) + " INTEGER" + return sql(name) + " INTEGER" case .string(let name, _): - return makeSQL(name) + " TEXT" + return sql(name) + " TEXT" } } } From 52a1baa70a6e27b8a6a449714df1e54a512621b5 Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Wed, 15 Jun 2016 23:20:15 -0400 Subject: [PATCH 04/10] test cases --- Sources/Fluent/Model/Value.swift | 6 ++ Sources/Fluent/SQL/GeneralSQLSerializer.swift | 100 ++++++++++++++---- Tests/Fluent/SQLSerializerTests.swift | 69 ++++++++++++ 3 files changed, 157 insertions(+), 18 deletions(-) create mode 100644 Tests/Fluent/SQLSerializerTests.swift diff --git a/Sources/Fluent/Model/Value.swift b/Sources/Fluent/Model/Value.swift index a8ca4f88..b4630f0c 100644 --- a/Sources/Fluent/Model/Value.swift +++ b/Sources/Fluent/Model/Value.swift @@ -38,6 +38,12 @@ extension StructuredData: Fluent.Value { } } +extension Bool: Value { + public var structuredData: StructuredData { + return .bool(self) + } +} + extension StructuredData: CustomStringConvertible { public var description: String { switch self { diff --git a/Sources/Fluent/SQL/GeneralSQLSerializer.swift b/Sources/Fluent/SQL/GeneralSQLSerializer.swift index a94f7de7..ec4bc663 100644 --- a/Sources/Fluent/SQL/GeneralSQLSerializer.swift +++ b/Sources/Fluent/SQL/GeneralSQLSerializer.swift @@ -23,36 +23,87 @@ public class GeneralSQLSerializer: SQLSerializer { statement += "INSERT INTO" statement += sql(table) - statement += sql(data) + let (dataClause, dataValues) = sql(data) + statement += dataClause return ( - statement.joined(separator: " "), - Array(data.values) + sql(statement), + dataValues ) case .select(let table, let filters, let limit): var statement: [String] = [] + var values: [Value] = [] statement += "SELECT * FROM" statement += sql(table) - let (clause, values) = sql(filters) - statement += clause + + if !filters.isEmpty { + let (filtersClause, filtersValues) = sql(filters) + statement += filtersClause + values += filtersValues + } if let limit = limit { - statement += "LIMIT" - statement += limit.description + statement += sql(limit: limit) } return ( - statement.joined(separator: " "), + sql(statement), values ) case .delete(let table, let filters, let limit): - return ("", []) + var statement: [String] = [] + var values: [Value] = [] + + 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): - return ("", []) + var statement: [String] = [] + + var values: [Value] = [] + + statement += "UPDATE" + statement += sql(table) + + let (dataClause, dataValues) = sql(data) + statement += dataClause + values += dataValues + + let (filterclause, filterValues) = sql(filters) + statement += filterclause + values += filterValues + + return ( + sql(statement), + values + ) } } + public func sql(limit: Int) -> String { + var statement: [String] = [] + + statement += "LIMIT" + statement += limit.description + + return statement.joined(separator: " ") + } + public func sql(_ filters: [Filter]) -> (String, [Value]) { var statement: [String] = [] var values: [Value] = [] @@ -70,7 +121,7 @@ public class GeneralSQLSerializer: SQLSerializer { statement += subStatement.joined(separator: "AND") return ( - statement.joined(separator: " "), + sql(statement), values ) } @@ -93,7 +144,7 @@ public class GeneralSQLSerializer: SQLSerializer { } return ( - statement.joined(separator: " "), + sql(statement), values ) } @@ -142,18 +193,31 @@ public class GeneralSQLSerializer: SQLSerializer { } } - public func sql(_ data: [String: Value]) -> String { + public func sql(_ data: [String: Value]) -> (String, [Value]) { var clause: [String] = [] - clause += sql(Array(data.keys)) + let values = Array(data.values) + + clause += sql(keys: Array(data.keys)) clause += "VALUES" - clause += sql(Array(data.values)) + clause += sql(values) + + return ( + sql(clause), + values + ) + } + + public func sql(_ strings: [String]) -> String { + return strings.joined(separator: " ") + } - return clause.joined(separator: " ") + public func sql(keys: [String]) -> String { + return sql(list: keys.map { sql($0) }) } - public func sql(_ columns: [String]) -> String { - return "(" + columns.joined(separator: ",") + ")" + public func sql(list: [String]) -> String { + return "(" + list.joined(separator: ",") + ")" } public func sql(_ values: [Value]) -> String { diff --git a/Tests/Fluent/SQLSerializerTests.swift b/Tests/Fluent/SQLSerializerTests.swift new file mode 100644 index 00000000..1b4f7755 --- /dev/null +++ b/Tests/Fluent/SQLSerializerTests.swift @@ -0,0 +1,69 @@ +import XCTest +@testable import Fluent + +class SQLSerializerTests: XCTestCase { + static let allTests = [ + ("testBasicSelect", testBasicSelect), + ] + + func testBasicSelect() { + let sql = SQL.select(table: "users", filters: [], limit: nil) + let (statement, values) = serialize(sql) + + XCTAssertEqual(statement, "SELECT * FROM `users`") + XCTAssert(values.isEmpty) + } + + func testRegularSelect() { + let filter = Filter.compare("age", .greaterThanOrEquals, 21) + let sql = SQL.select(table: "users", filters: [filter], limit: 5) + let (statement, values) = serialize(sql) + + XCTAssertEqual(statement, "SELECT * FROM `users` WHERE `age` >= ? LIMIT 5") + XCTAssertEqual(values.first?.int, 21) + XCTAssertEqual(values.count, 1) + } + + func testFilterCompareSelect() { + let filter = Filter.compare("name", .notEquals, "duck") + + let select = SQL.select(table: "friends", filters: [filter], limit: nil) + let (statement, values) = serialize(select) + + XCTAssertEqual(statement, "SELECT * FROM `friends` WHERE `name` != ?") + XCTAssertEqual(values.first?.string, "duck") + XCTAssertEqual(values.count, 1) + } + + func testFilterCompareUpdate() { + let filter = Filter.compare("name", .equals, "duck") + + let update = SQL.update(table: "friends", filters: [filter], data: ["not it": true]) + let (statement, values) = serialize(update) + + XCTAssertEqual(statement, "UPDATE `friends` (`not it`) VALUES (?) WHERE `name` = ?") + XCTAssertEqual(values.first?.bool, true) + XCTAssertEqual(values.last?.string, "duck") + XCTAssertEqual(values.count, 2) + } + + func testFilterCompareDelete() { + let filter = Filter.compare("name", .greaterThan, "duck") + + let delete = SQL.delete(table: "friends", filters: [filter], limit: nil) + let (statement, values) = serialize(delete) + + XCTAssertEqual(statement, "DELETE FROM `friends` WHERE `name` > ?") + XCTAssertEqual(values.first?.string, "duck") + XCTAssertEqual(values.count, 1) + } +} + +// MARK: Utilities + +extension SQLSerializerTests { + private func serialize(_ sql: SQL) -> (String, [Value]) { + let serializer = GeneralSQLSerializer(sql: sql) + return serializer.serialize() + } +} From 8fb9588a4650ef3b5deaf5295ae576464922b243 Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Thu, 16 Jun 2016 17:11:21 -0400 Subject: [PATCH 05/10] preparations + alter table sql --- Sources/Fluent/Model/Model+Preparation.swift | 60 +++++++ Sources/Fluent/Model/Model.swift | 24 ++- Sources/Fluent/Query/Action.swift | 2 +- Sources/Fluent/Query/Query.swift | 19 +-- Sources/Fluent/SQL/GeneralSQLSerializer.swift | 52 ++++-- Sources/Fluent/SQL/SQL+Builder.swift | 40 ++++- Sources/Fluent/SQL/SQL+Query.swift | 2 +- Sources/Fluent/SQL/SQL.swift | 9 +- Sources/Fluent/SQL/SQLSerializer.swift | 2 + Sources/Fluent/Schema/Schema.swift | 67 ++++++-- Tests/Fluent/PreparationTests.swift | 151 ++++++++++++++++++ 11 files changed, 380 insertions(+), 48 deletions(-) create mode 100644 Sources/Fluent/Model/Model+Preparation.swift create mode 100644 Tests/Fluent/PreparationTests.swift diff --git a/Sources/Fluent/Model/Model+Preparation.swift b/Sources/Fluent/Model/Model+Preparation.swift new file mode 100644 index 00000000..c0a5c493 --- /dev/null +++ b/Sources/Fluent/Model/Model+Preparation.swift @@ -0,0 +1,60 @@ +extension Model { + public static func prepare(database: Database) throws { + try database.create(entity) { builder in + print("Preparing \(self.dynamicType)") + + let model = self.init() + let mirror = Mirror(reflecting: model) + + for property in mirror.children { + let name = property.label ?? "" + let type = "\(property.value.dynamicType)" + + if name == database.driver.idKey { + builder.id() + } else { + if type.contains("String") { + builder.string(name) + } else if type.contains("Int") { + builder.int(name) + } + } + } + } + } + + public static func revert(database: Database) throws { + try database.delete(entity) + } + + private init() { + self.init(serialized: [:]) + } + + public func serialize() -> [String: Value?] { + var serialized: [String: Value?] = [:] + + let mirror = Mirror(reflecting: self) + for property in mirror.children { + let name = property.label ?? "" + let type = "\(property.value.dynamicType)" + + if let id = id { + serialized["id"] = id.int ?? id.string + } + + if type.contains("String") { + if let string = property.value as? String { + serialized[name] = string + } + } else if type.contains("Int") { + if let int = property.value as? Int { + serialized[name] = int + } + } + + } + + return serialized + } +} diff --git a/Sources/Fluent/Model/Model.swift b/Sources/Fluent/Model/Model.swift index 63aee4a3..28183abd 100644 --- a/Sources/Fluent/Model/Model.swift +++ b/Sources/Fluent/Model/Model.swift @@ -1,8 +1,10 @@ +import Foundation + /** Represents an entity that can be stored and retrieved from the `Database`. */ -public protocol Model: CustomStringConvertible { +public protocol Model: CustomStringConvertible, Preparation { /** The `Database` this model will use. It can be changed at any point. @@ -35,10 +37,10 @@ public protocol Model: CustomStringConvertible { func serialize() -> [String: Value?] /** - Attempts to initialize an entity + Initializes an entity from the database representation. */ - init?(serialized: [String: Value]) + init(serialized: [String: Value]) } //MARK: Defaults @@ -92,14 +94,24 @@ extension Model { public static var query: Query { return Query() } + + /** + Creates a `Query` with a first filter. + */ + @discardableResult + public static func filter(_ field: String, _ comparison: Filter.Comparison, _ value: Value) -> Query { + return query.filter(field, comparison, value) + } + + @discardableResult + public static func filter(_ field: String, _ value: Value) -> Query { + return filter(field, .equals, value) + } } //MARK: Database extension Model { - /** - Used to identify this `Model`. - */ private static var name: String { return "\(self)" } diff --git a/Sources/Fluent/Query/Action.swift b/Sources/Fluent/Query/Action.swift index 37841289..85195ffe 100644 --- a/Sources/Fluent/Query/Action.swift +++ b/Sources/Fluent/Query/Action.swift @@ -7,5 +7,5 @@ public enum Action { case fetch case delete case create - case update + case modify } diff --git a/Sources/Fluent/Query/Query.swift b/Sources/Fluent/Query/Query.swift index c8119304..59f15699 100644 --- a/Sources/Fluent/Query/Query.swift +++ b/Sources/Fluent/Query/Query.swift @@ -68,10 +68,7 @@ public class Query { let results = try database.driver.query(self) for result in results { - guard var model = T(serialized: result) else { - continue - } - + var model = T(serialized: result) model.id = result[database.driver.idKey] models.append(model) } @@ -122,7 +119,7 @@ public class Query { if let id = model.id { let _ = filter(database.driver.idKey, .equals, id) // discardableResult - try update(data) + try modify(data) } else { let new = try create(data) model.id = new?.id @@ -159,11 +156,11 @@ public class Query { //MARK: Update /** - Attempts to update model's collection with + Attempts to modify model's collection with the supplied serialized data. */ - public func update(_ serialized: [String: Value?]) throws { - action = .update + public func modify(_ serialized: [String: Value?]) throws { + action = .modify data = nilToNull(serialized) let _ = try run() // discardableResult } @@ -178,7 +175,7 @@ public class Query { Used for filtering results based on how a result's value compares to the supplied value. */ - // @discardableResult + @discardableResult public func filter(_ field: String, _ comparison: Filter.Comparison, _ value: Value) -> Self { let filter = Filter.compare(field, comparison, value) filters.append(filter) @@ -192,7 +189,7 @@ public class Query { Used for filtering results based on whether a result's value is or is not in a set. */ - // @discardableResult + @discardableResult public func filter(_ field: String, _ scope: Filter.Scope, _ set: [Value]) -> Self { let filter = Filter.subset(field, scope, set) filters.append(filter) @@ -203,7 +200,7 @@ public class Query { /** Shortcut for creating a `.equals` filter. */ - // @discardableResult + @discardableResult public func filter(_ field: String, _ value: Value) -> Self { return filter(field, .equals, value) } diff --git a/Sources/Fluent/SQL/GeneralSQLSerializer.swift b/Sources/Fluent/SQL/GeneralSQLSerializer.swift index ec4bc663..68df7210 100644 --- a/Sources/Fluent/SQL/GeneralSQLSerializer.swift +++ b/Sources/Fluent/SQL/GeneralSQLSerializer.swift @@ -7,12 +7,10 @@ public class GeneralSQLSerializer: SQLSerializer { public func serialize() -> (String, [Value]) { switch sql { - case .table(let action, let table, let columns): + case .table(let action, let table): var statement: [String] = [] - statement += sql(action) - statement += sql(table) - statement += sql(columns) + statement += sql(action, table) return ( statement.joined(separator: " "), @@ -175,21 +173,55 @@ public class GeneralSQLSerializer: SQLSerializer { } } - public func sql(_ tableAction: SQL.TableAction) -> String { + public func sql(_ tableAction: SQL.TableAction, _ table: String) -> String { switch tableAction { - case .alter: - return "ALTER TABLE" - case .create: - return "CREATE TABLE" + case .alter(let create, let delete): + var clause: [String] = [] + + clause += "ALTER TABLE" + clause += sql(table) + + for column in create { + clause += "ADD" + clause += sql(column) + } + + for name in delete { + clause += "DROP COLUMN" + clause += sql(name) + } + + 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" + clause += sql(table) + + return sql(clause) } } public func sql(_ column: SQL.Column) -> String { switch column { + case .primaryKey: + return sql("id") + " INTEGER PRIMARY KEY" case .integer(let name): return sql(name) + " INTEGER" case .string(let name, let length): - return sql(name) + " VARCHAR(\(length))" + if let length = length { + return sql(name) + " STRING(\(length))" + } else { + return sql(name) + " STRING" + } } } diff --git a/Sources/Fluent/SQL/SQL+Builder.swift b/Sources/Fluent/SQL/SQL+Builder.swift index 539ef3ab..cdbea1d5 100644 --- a/Sources/Fluent/SQL/SQL+Builder.swift +++ b/Sources/Fluent/SQL/SQL+Builder.swift @@ -1,11 +1,34 @@ extension SQL { - init(builder: Schema.Builder) { - var columns: [Column] = [] + public init(schema: Schema) { + let action: TableAction + let table: String - for field in builder.fields { - let column: Column + switch schema { + case .create(let entity, let fields): + table = entity + action = .create(columns: fields.columns) + case .modify(let entity, let create, let delete): + table = entity + action = .alter(create: create.columns, delete: delete) + case .delete(let entity): + table = entity + action = .drop + } + + self = .table(action: action, table: table) + } +} + +extension Collection where Iterator.Element == Schema.Field { + var columns: [SQL.Column] { + var columns: [SQL.Column] = [] + + for field in self { + let column: SQL.Column switch field { + case .id: + column = .primaryKey case .int(let name): column = .integer(name) case .string(let name, let length): @@ -15,6 +38,13 @@ extension SQL { columns.append(column) } - self = .table(action: .create, table: builder.entity, columns: columns) + return columns + } + +} + +extension Schema { + public var sql: SQL { + return SQL(schema: self) } } diff --git a/Sources/Fluent/SQL/SQL+Query.swift b/Sources/Fluent/SQL/SQL+Query.swift index c4acbfa9..355e0d2a 100644 --- a/Sources/Fluent/SQL/SQL+Query.swift +++ b/Sources/Fluent/SQL/SQL+Query.swift @@ -18,7 +18,7 @@ extension SQL { filters: query.filters, limit: query.limit?.count ) - case .update: + case .modify: self = .update( table: query.entity, filters: query.filters, diff --git a/Sources/Fluent/SQL/SQL.swift b/Sources/Fluent/SQL/SQL.swift index 1d89e12b..f34445a2 100644 --- a/Sources/Fluent/SQL/SQL.swift +++ b/Sources/Fluent/SQL/SQL.swift @@ -1,16 +1,19 @@ public enum SQL { public enum TableAction { - case create, alter + case create(columns: [Column]) + case alter(create: [Column], delete: [String]) + case drop } public enum Column { + case primaryKey case integer(String) - case string(String, Int) + case string(String, Int?) } case insert(table: String, data: [String: Value]) case select(table: String, filters: [Filter], limit: Int?) case update(table: String, filters: [Filter], data: [String: Value]) case delete(table: String, filters: [Filter], limit: Int?) - case table(action: TableAction, table: String, columns: [Column]) + case table(action: TableAction, table: String) } diff --git a/Sources/Fluent/SQL/SQLSerializer.swift b/Sources/Fluent/SQL/SQLSerializer.swift index 5ab17d1c..2188025c 100644 --- a/Sources/Fluent/SQL/SQLSerializer.swift +++ b/Sources/Fluent/SQL/SQLSerializer.swift @@ -6,6 +6,8 @@ public protocol SQLSerializer { final class SQLiteSerializer: GeneralSQLSerializer { override func sql(_ column: SQL.Column) -> String { switch column { + case .primaryKey: + return sql("id") + " INTEGER PRIMARY KEY" case .integer(let name): return sql(name) + " INTEGER" case .string(let name, _): diff --git a/Sources/Fluent/Schema/Schema.swift b/Sources/Fluent/Schema/Schema.swift index 2098a6e7..6e6ddcbe 100644 --- a/Sources/Fluent/Schema/Schema.swift +++ b/Sources/Fluent/Schema/Schema.swift @@ -1,20 +1,37 @@ -public final class Schema { - - public static func build(_ entity: String, closure: (Builder) -> ()) throws { - let builder = Builder(entity) - closure(builder) - _ = try Database.default.driver.build(builder) - } - +public enum Schema { + case create(entity: String, create: [Field]) + case modify(entity: String, create: [Field], delete: [String]) + case delete(entity: String) } extension Schema { public enum Field { + case id case int(String) - case string(String, Int) + case string(String, Int?) + } +} + +extension Schema { + 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) + } } - public final class Builder { + public class Creator { public let entity: String public var fields: [Field] @@ -23,12 +40,40 @@ extension Schema { fields = [] } + public func id() { + fields.append(.id) + } + public func int(_ name: String) { fields.append(.int(name)) } - public func string(_ name: String, length: Int = 128) { + public func string(_ name: String, length: Int? = nil) { fields.append(.string(name, length)) } + + public var schema: Schema { + return .create(entity: entity, create: fields) + } + } +} + + +extension Database { + public func modify(_ entity: String, closure: (Schema.Modifier) -> ()) throws { + let modifier = Schema.Modifier(entity) + closure(modifier) + _ = try driver.schema(modifier.schema) + } + + public func create(_ entity: String, closure: (Schema.Creator) -> ()) throws { + let creator = Schema.Creator(entity) + closure(creator) + _ = try driver.schema(creator.schema) + } + + public func delete(_ entity: String) throws { + let schema = Schema.delete(entity: entity) + _ = try driver.schema(schema) } } diff --git a/Tests/Fluent/PreparationTests.swift b/Tests/Fluent/PreparationTests.swift new file mode 100644 index 00000000..77de4189 --- /dev/null +++ b/Tests/Fluent/PreparationTests.swift @@ -0,0 +1,151 @@ +import XCTest +import Fluent + +class PreparationTests: XCTestCase { + static let allTests = [ + ("testManualPreparation", testManualPreparation), + ] + + func testManualPreparation() { + let driver = TestBuildDriver { builder in + XCTAssertEqual(builder.entity, "users") + guard builder.fields.count == 3 else { + XCTFail("Invalid field count") + return + } + + guard case .int(let colOneName) = builder.fields[0] else { + XCTFail("Invalid first field") + return + } + XCTAssertEqual(colOneName, "id") + + guard case .string(let colTwoName, let colTwoLength) = builder.fields[1] else { + XCTFail("Invalid second field") + return + } + XCTAssertEqual(colTwoName, "name") + XCTAssertEqual(colTwoLength, nil) + + guard case .string(let colThreeName, let colThreeLength) = builder.fields[2] else { + XCTFail("Invalid second field") + return + } + XCTAssertEqual(colThreeName, "email") + XCTAssertEqual(colThreeLength, 128) + } + + let database = Database(driver: driver) + + let preparation = TestPreparation(entity: "users") { builder in + builder.int("id") + builder.string("name") + builder.string("email", length: 128) + } + database.preparations = [preparation] + + do { + try database.prepare() + } catch { + XCTFail("Preparation failed: \(error)") + } + } + + func testModelPreparation() { + let driver = TestBuildDriver { builder in + XCTAssertEqual(builder.entity, "testmodels") + guard builder.fields.count == 3 else { + XCTFail("Invalid field count") + return + } + + guard case .int(let colOneName) = builder.fields[0] else { + XCTFail("Invalid first field") + return + } + XCTAssertEqual(colOneName, "id") + + guard case .string(let colTwoName, let colTwoLength) = builder.fields[1] else { + XCTFail("Invalid second field") + return + } + XCTAssertEqual(colTwoName, "name") + XCTAssertEqual(colTwoLength, nil) + + guard case .int(let colThreeName) = builder.fields[2] else { + XCTFail("Invalid second field") + return + } + XCTAssertEqual(colThreeName, "age") + } + + let database = Database(driver: driver) + + database.preparations = [ + TestModel() + ] + + do { + try database.prepare() + } catch { + XCTFail("Preparation failed: \(error)") + } + } +} + +// MARK: Utilities + +final class TestModel: Model { + var id: Value? + var name: String + var age: Int + + init(serialized: [String: Value]) { + id = serialized["id"] + name = serialized["name"]?.string ?? "" + age = serialized["age"]?.int ?? 0 + } +} + +class TestPreparation: Preparation { + var entity: String + var testClosure: (Schema.Builder) -> () + + init(entity: String, testClosure: (Schema.Builder) -> ()) { + self.entity = entity + self.testClosure = testClosure + } + + func up(database: Database) throws { + try database.create(entity) { builder in + self.testClosure(builder) + } + } + + func down(database: Database) throws { + try database.delete(entity) + } +} + +class TestBuildDriver: Driver { + var idKey: String = "id" + + @discardableResult + func query(_ query: Query) throws -> [[String: Value]] { return [] } + + var testClosure: (Schema.Builder) -> () + init(testClosure: (Schema.Builder) -> ()) { + self.testClosure = testClosure + } + + func build(_ builder: Schema.Builder) throws { + testClosure(builder) + } +} + +extension SQLSerializerTests { + private func serialize(_ sql: SQL) -> (String, [Value]) { + let serializer = GeneralSQLSerializer(sql: sql) + return serializer.serialize() + } +} From 8cf105f13fb88a77c706cfd177f8708500315726 Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Thu, 16 Jun 2016 18:46:49 -0400 Subject: [PATCH 06/10] schema rename --- Sources/Fluent/Model/Model+Preparation.swift | 2 - Tests/Fluent/ModelFindTests.swift | 2 +- Tests/Fluent/PreparationTests.swift | 72 ++++++++++---------- Tests/Fluent/QueryFiltersTests.swift | 2 +- Tests/Fluent/SchemaCreateTests.swift | 4 +- 5 files changed, 41 insertions(+), 41 deletions(-) diff --git a/Sources/Fluent/Model/Model+Preparation.swift b/Sources/Fluent/Model/Model+Preparation.swift index c0a5c493..fd6818c4 100644 --- a/Sources/Fluent/Model/Model+Preparation.swift +++ b/Sources/Fluent/Model/Model+Preparation.swift @@ -1,8 +1,6 @@ extension Model { public static func prepare(database: Database) throws { try database.create(entity) { builder in - print("Preparing \(self.dynamicType)") - let model = self.init() let mirror = Mirror(reflecting: model) diff --git a/Tests/Fluent/ModelFindTests.swift b/Tests/Fluent/ModelFindTests.swift index 68de80e0..97a08865 100644 --- a/Tests/Fluent/ModelFindTests.swift +++ b/Tests/Fluent/ModelFindTests.swift @@ -51,7 +51,7 @@ class ModelFindTests: XCTestCase { return [] } - func build(_ builder: Schema.Builder) throws { + func schema(_ builder: Schema) throws { // } } diff --git a/Tests/Fluent/PreparationTests.swift b/Tests/Fluent/PreparationTests.swift index 77de4189..656c60d7 100644 --- a/Tests/Fluent/PreparationTests.swift +++ b/Tests/Fluent/PreparationTests.swift @@ -7,27 +7,33 @@ class PreparationTests: XCTestCase { ] func testManualPreparation() { - let driver = TestBuildDriver { builder in - XCTAssertEqual(builder.entity, "users") - guard builder.fields.count == 3 else { + let driver = TestSchemaDriver { schema in + guard case .create(let entity, let fields) = schema else { + XCTFail("Invalid schema") + return + } + + XCTAssertEqual(entity, "users") + + guard fields.count == 3 else { XCTFail("Invalid field count") return } - guard case .int(let colOneName) = builder.fields[0] else { + guard case .int(let colOneName) = fields[0] else { XCTFail("Invalid first field") return } XCTAssertEqual(colOneName, "id") - guard case .string(let colTwoName, let colTwoLength) = builder.fields[1] else { + guard case .string(let colTwoName, let colTwoLength) = fields[1] else { XCTFail("Invalid second field") return } XCTAssertEqual(colTwoName, "name") XCTAssertEqual(colTwoLength, nil) - guard case .string(let colThreeName, let colThreeLength) = builder.fields[2] else { + guard case .string(let colThreeName, let colThreeLength) = fields[2] else { XCTFail("Invalid second field") return } @@ -37,42 +43,47 @@ class PreparationTests: XCTestCase { let database = Database(driver: driver) - let preparation = TestPreparation(entity: "users") { builder in + TestPreparation.entity = "users" + TestPreparation.testClosure = { builder in builder.int("id") builder.string("name") builder.string("email", length: 128) } - database.preparations = [preparation] do { - try database.prepare() + try database.prepare(TestPreparation) } catch { XCTFail("Preparation failed: \(error)") } } func testModelPreparation() { - let driver = TestBuildDriver { builder in - XCTAssertEqual(builder.entity, "testmodels") - guard builder.fields.count == 3 else { + let driver = TestSchemaDriver { schema in + guard case .create(let entity, let fields) = schema else { + XCTFail("Invalid schema") + return + } + + XCTAssertEqual(entity, "testmodels") + + guard fields.count == 3 else { XCTFail("Invalid field count") return } - guard case .int(let colOneName) = builder.fields[0] else { + guard case .id = fields[0] else { XCTFail("Invalid first field") return } - XCTAssertEqual(colOneName, "id") - guard case .string(let colTwoName, let colTwoLength) = builder.fields[1] else { + guard case .string(let colTwoName, let colTwoLength) = fields[1] else { XCTFail("Invalid second field") return } XCTAssertEqual(colTwoName, "name") XCTAssertEqual(colTwoLength, nil) - guard case .int(let colThreeName) = builder.fields[2] else { + guard case .int(let colThreeName) = fields[2] else { XCTFail("Invalid second field") return } @@ -81,12 +92,8 @@ class PreparationTests: XCTestCase { let database = Database(driver: driver) - database.preparations = [ - TestModel() - ] - do { - try database.prepare() + try database.prepare(TestModel) } catch { XCTFail("Preparation failed: \(error)") } @@ -108,38 +115,33 @@ final class TestModel: Model { } class TestPreparation: Preparation { - var entity: String - var testClosure: (Schema.Builder) -> () - - init(entity: String, testClosure: (Schema.Builder) -> ()) { - self.entity = entity - self.testClosure = testClosure - } + static var entity: String = "" + static var testClosure: (Schema.Creator) -> () = { _ in } - func up(database: Database) throws { + static func prepare(database: Database) throws { try database.create(entity) { builder in self.testClosure(builder) } } - func down(database: Database) throws { + static func revert(database: Database) throws { try database.delete(entity) } } -class TestBuildDriver: Driver { +class TestSchemaDriver: Driver { var idKey: String = "id" @discardableResult func query(_ query: Query) throws -> [[String: Value]] { return [] } - var testClosure: (Schema.Builder) -> () - init(testClosure: (Schema.Builder) -> ()) { + var testClosure: (Schema) -> () + init(testClosure: (Schema) -> ()) { self.testClosure = testClosure } - func build(_ builder: Schema.Builder) throws { - testClosure(builder) + func schema(_ schema: Schema) throws { + testClosure(schema) } } diff --git a/Tests/Fluent/QueryFiltersTests.swift b/Tests/Fluent/QueryFiltersTests.swift index ecbdddf9..23d5c329 100644 --- a/Tests/Fluent/QueryFiltersTests.swift +++ b/Tests/Fluent/QueryFiltersTests.swift @@ -31,7 +31,7 @@ class QueryFiltersTests: XCTestCase { return [] } - func build(_ builder: Schema.Builder) throws { + func schema(_ schema: Schema) throws { } } diff --git a/Tests/Fluent/SchemaCreateTests.swift b/Tests/Fluent/SchemaCreateTests.swift index 70996fae..3f706193 100644 --- a/Tests/Fluent/SchemaCreateTests.swift +++ b/Tests/Fluent/SchemaCreateTests.swift @@ -7,13 +7,13 @@ class SchemaCreateTests: XCTestCase { ] func testCreate() throws { - let builder = Schema.Builder("users") + let builder = Schema.Creator("users") builder.int("id") builder.string("name") builder.string("email", length: 256) - let sql = SQL(builder: builder) + let sql = builder.schema.sql let serializer = GeneralSQLSerializer(sql: sql) let sqliteSerializer = SQLiteSerializer(sql: sql) From a491695b3e911f15827fa4c1e63e284c203447a3 Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Sun, 19 Jun 2016 11:22:17 -0400 Subject: [PATCH 07/10] double column --- Sources/Fluent/SQL/GeneralSQLSerializer.swift | 10 ++++------ Sources/Fluent/SQL/SQL+Builder.swift | 4 +++- Sources/Fluent/SQL/SQL.swift | 3 ++- Sources/Fluent/SQL/SQLSerializer.swift | 2 ++ Sources/Fluent/Schema/Schema.swift | 9 +++++++-- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Sources/Fluent/SQL/GeneralSQLSerializer.swift b/Sources/Fluent/SQL/GeneralSQLSerializer.swift index 68df7210..4b74f8e0 100644 --- a/Sources/Fluent/SQL/GeneralSQLSerializer.swift +++ b/Sources/Fluent/SQL/GeneralSQLSerializer.swift @@ -216,12 +216,10 @@ public class GeneralSQLSerializer: SQLSerializer { return sql("id") + " INTEGER PRIMARY KEY" case .integer(let name): return sql(name) + " INTEGER" - case .string(let name, let length): - if let length = length { - return sql(name) + " STRING(\(length))" - } else { - return sql(name) + " STRING" - } + case .string(let name, _): + return sql(name) + " STRING" + case .double(let name, _, _): + return sql(name) + " DOUBLE" } } diff --git a/Sources/Fluent/SQL/SQL+Builder.swift b/Sources/Fluent/SQL/SQL+Builder.swift index cdbea1d5..eb742fea 100644 --- a/Sources/Fluent/SQL/SQL+Builder.swift +++ b/Sources/Fluent/SQL/SQL+Builder.swift @@ -32,7 +32,9 @@ extension Collection where Iterator.Element == Schema.Field { case .int(let name): column = .integer(name) case .string(let name, let length): - column = .string(name, length) + column = .string(name, length: length) + case .double(let name, let digits, let decimal): + column = .double(name, digits: digits, decimal: decimal) } columns.append(column) diff --git a/Sources/Fluent/SQL/SQL.swift b/Sources/Fluent/SQL/SQL.swift index f34445a2..9ec535ea 100644 --- a/Sources/Fluent/SQL/SQL.swift +++ b/Sources/Fluent/SQL/SQL.swift @@ -8,7 +8,8 @@ public enum SQL { public enum Column { case primaryKey case integer(String) - case string(String, Int?) + case string(String, length: Int?) + case double(String, digits: Int?, decimal: Int?) } case insert(table: String, data: [String: Value]) diff --git a/Sources/Fluent/SQL/SQLSerializer.swift b/Sources/Fluent/SQL/SQLSerializer.swift index 2188025c..22a7e07a 100644 --- a/Sources/Fluent/SQL/SQLSerializer.swift +++ b/Sources/Fluent/SQL/SQLSerializer.swift @@ -12,6 +12,8 @@ final class SQLiteSerializer: GeneralSQLSerializer { return sql(name) + " INTEGER" case .string(let name, _): return sql(name) + " TEXT" + case .double(let name, _, _): + return sql(name) + " DOUBLE" } } } diff --git a/Sources/Fluent/Schema/Schema.swift b/Sources/Fluent/Schema/Schema.swift index 6e6ddcbe..e001e17f 100644 --- a/Sources/Fluent/Schema/Schema.swift +++ b/Sources/Fluent/Schema/Schema.swift @@ -8,7 +8,8 @@ extension Schema { public enum Field { case id case int(String) - case string(String, Int?) + case string(String, length: Int?) + case double(String, digits: Int?, decimal: Int?) } } @@ -49,7 +50,11 @@ extension Schema { } public func string(_ name: String, length: Int? = nil) { - fields.append(.string(name, length)) + fields.append(.string(name, length: length)) + } + + public func double(_ name: String, digits: Int? = nil, decimal: Int? = nil) { + fields.append(.double(name, digits: digits, decimal: decimal)) } public var schema: Schema { From a56e3128314a1a21c7cb58390c95671cf9ba803b Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Mon, 20 Jun 2016 18:05:09 -0400 Subject: [PATCH 08/10] updates --- Package.swift | 35 +-------------- .../Preparation/Database+Preparation.swift | 44 +++++++++++++++++++ Sources/Fluent/Preparation/Migration.swift | 15 +++++++ .../Model+Preparation.swift | 18 ++++++-- Sources/Fluent/Preparation/Preparation.swift | 4 ++ .../Fluent/Preparation/PreparationError.swift | 5 +++ Sources/Fluent/Schema/Schema.swift | 8 ++-- .../Fluent/Utilities/Fluent+Polymorphic.swift | 1 + Sources/FluentMySQL | 1 - Sources/FluentSQLite | 1 - Sources/MySQL | 1 - Sources/SQLite | 1 - Tests/FluentMySQL | 1 - Tests/FluentSQLite | 1 - Tests/MySQL | 1 - Tests/SQLite | 1 - 16 files changed, 89 insertions(+), 49 deletions(-) create mode 100644 Sources/Fluent/Preparation/Database+Preparation.swift create mode 100644 Sources/Fluent/Preparation/Migration.swift rename Sources/Fluent/{Model => Preparation}/Model+Preparation.swift (74%) create mode 100644 Sources/Fluent/Preparation/Preparation.swift create mode 100644 Sources/Fluent/Preparation/PreparationError.swift delete mode 120000 Sources/FluentMySQL delete mode 120000 Sources/FluentSQLite delete mode 120000 Sources/MySQL delete mode 120000 Sources/SQLite delete mode 120000 Tests/FluentMySQL delete mode 120000 Tests/FluentSQLite delete mode 120000 Tests/MySQL delete mode 120000 Tests/SQLite diff --git a/Package.swift b/Package.swift index aa29b999..07e7015d 100644 --- a/Package.swift +++ b/Package.swift @@ -7,39 +7,6 @@ let package = Package( .Package(url: "https://github.com/open-swift/C7.git", majorVersion: 0, minor: 9), // Syntax for easily accessing values from generic data. - .Package(url: "https://github.com/qutheory/polymorphic.git", majorVersion: 0, minor: 2), - - .Package(url: "https://github.com/qutheory/cmysql.git", majorVersion: 0, minor: 1), - - .Package(url: "https://github.com/qutheory/csqlite.git", majorVersion: 0, minor: 1), - - .Package(url: "https://github.com/qutheory/libc.git", majorVersion: 0, minor: 1), - ], - targets: [ - Target( - name: "FluentMySQL", - dependencies: [ - .Target(name: "MySQL"), - .Target(name: "Fluent") - ] - ), - Target( - name: "MySQL" - ), - - Target( - name: "FluentSQLite", - dependencies: [ - .Target(name: "SQLite"), - .Target(name: "Fluent") - ] - ), - Target( - name: "SQLite" - ), - - Target( - name: "Fluent" - ), + .Package(url: "https://github.com/qutheory/polymorphic.git", majorVersion: 0, minor: 2) ] ) diff --git a/Sources/Fluent/Preparation/Database+Preparation.swift b/Sources/Fluent/Preparation/Database+Preparation.swift new file mode 100644 index 00000000..330f75d3 --- /dev/null +++ b/Sources/Fluent/Preparation/Database+Preparation.swift @@ -0,0 +1,44 @@ +extension Database { + public func prepare(_ preparations: [Preparation.Type]) throws { + for preparation in preparations { + try prepare(preparation) + } + } + + public func hasPrepared(_ preparation: Preparation.Type) throws -> Bool { + Migration.database = self + + do { + // check to see if this preparation has already run + if let _ = try Migration.filter("name", preparation.name).first() { + return true + } + } catch { + // could not fetch migrations + // try to create `.fluent` table + try Migration.prepare(database: self) + } + + return false + } + + public func prepare(_ preparation: Preparation.Type) throws { + Migration.database = self + + // set the current database on involved Models + if let model = preparation as? Model.Type { + model.database = self + } + + + if try hasPrepared(preparation) { + throw PreparationError.alreadyPrepared + } + + try preparation.prepare(database: self) + + // record that this preparation has run + var migration = Migration(name: preparation.name) + try migration.save() + } +} diff --git a/Sources/Fluent/Preparation/Migration.swift b/Sources/Fluent/Preparation/Migration.swift new file mode 100644 index 00000000..48e16e7b --- /dev/null +++ b/Sources/Fluent/Preparation/Migration.swift @@ -0,0 +1,15 @@ +final class Migration: Model { + static var entity = "fluent" + + var id: Value? + var name: String + + init(name: String) { + self.name = name + } + + init(serialized: [String: Value]) { + id = serialized["id"] + name = serialized["name"]?.string ?? "" + } +} diff --git a/Sources/Fluent/Model/Model+Preparation.swift b/Sources/Fluent/Preparation/Model+Preparation.swift similarity index 74% rename from Sources/Fluent/Model/Model+Preparation.swift rename to Sources/Fluent/Preparation/Model+Preparation.swift index fd6818c4..d3082ec0 100644 --- a/Sources/Fluent/Model/Model+Preparation.swift +++ b/Sources/Fluent/Preparation/Model+Preparation.swift @@ -1,3 +1,5 @@ +// MARK: Preparation + extension Model { public static func prepare(database: Database) throws { try database.create(entity) { builder in @@ -5,7 +7,9 @@ extension Model { let mirror = Mirror(reflecting: model) for property in mirror.children { - let name = property.label ?? "" + guard let name = property.label else { + throw PreparationError.automationFailed("Unable to unwrap property name.") + } let type = "\(property.value.dynamicType)" if name == database.driver.idKey { @@ -15,6 +19,10 @@ extension Model { builder.string(name) } else if type.contains("Int") { builder.int(name) + } else if type.contains("Double") || type.contains("Float") { + builder.double(name) + } else { + throw PreparationError.automationFailed("Unable to prepare property '\(name): \(type)', only String, Int, Double, and Float are supported.") } } } @@ -24,7 +32,11 @@ extension Model { public static func revert(database: Database) throws { try database.delete(entity) } +} + +// MARK: Automation +extension Model { private init() { self.init(serialized: [:]) } @@ -50,9 +62,9 @@ extension Model { serialized[name] = int } } - + } - + return serialized } } diff --git a/Sources/Fluent/Preparation/Preparation.swift b/Sources/Fluent/Preparation/Preparation.swift new file mode 100644 index 00000000..d24c2db0 --- /dev/null +++ b/Sources/Fluent/Preparation/Preparation.swift @@ -0,0 +1,4 @@ +public protocol Preparation { + static func prepare(database: Database) throws + static func revert(database: Database) throws +} diff --git a/Sources/Fluent/Preparation/PreparationError.swift b/Sources/Fluent/Preparation/PreparationError.swift new file mode 100644 index 00000000..761b9dd9 --- /dev/null +++ b/Sources/Fluent/Preparation/PreparationError.swift @@ -0,0 +1,5 @@ +public enum PreparationError: ErrorProtocol { + case alreadyPrepared + case revertImpossible + case automationFailed(String) +} diff --git a/Sources/Fluent/Schema/Schema.swift b/Sources/Fluent/Schema/Schema.swift index e001e17f..7b8f0c0a 100644 --- a/Sources/Fluent/Schema/Schema.swift +++ b/Sources/Fluent/Schema/Schema.swift @@ -65,15 +65,15 @@ extension Schema { extension Database { - public func modify(_ entity: String, closure: (Schema.Modifier) -> ()) throws { + public func modify(_ entity: String, closure: (Schema.Modifier) throws -> ()) throws { let modifier = Schema.Modifier(entity) - closure(modifier) + try closure(modifier) _ = try driver.schema(modifier.schema) } - public func create(_ entity: String, closure: (Schema.Creator) -> ()) throws { + public func create(_ entity: String, closure: (Schema.Creator) throws -> ()) throws { let creator = Schema.Creator(entity) - closure(creator) + try closure(creator) _ = try driver.schema(creator.schema) } diff --git a/Sources/Fluent/Utilities/Fluent+Polymorphic.swift b/Sources/Fluent/Utilities/Fluent+Polymorphic.swift index 08fe28ba..426b7d99 100644 --- a/Sources/Fluent/Utilities/Fluent+Polymorphic.swift +++ b/Sources/Fluent/Utilities/Fluent+Polymorphic.swift @@ -1 +1,2 @@ @_exported import Polymorphic +@_exported import PathIndexable diff --git a/Sources/FluentMySQL b/Sources/FluentMySQL deleted file mode 120000 index 1c464464..00000000 --- a/Sources/FluentMySQL +++ /dev/null @@ -1 +0,0 @@ -../../fluent-mysql/Sources/FluentMySQL/ \ No newline at end of file diff --git a/Sources/FluentSQLite b/Sources/FluentSQLite deleted file mode 120000 index e47950df..00000000 --- a/Sources/FluentSQLite +++ /dev/null @@ -1 +0,0 @@ -../../fluent-sqlite/Sources/FluentSQLite/ \ No newline at end of file diff --git a/Sources/MySQL b/Sources/MySQL deleted file mode 120000 index d32a457b..00000000 --- a/Sources/MySQL +++ /dev/null @@ -1 +0,0 @@ -../../mysql/Sources/MySQL/ \ No newline at end of file diff --git a/Sources/SQLite b/Sources/SQLite deleted file mode 120000 index 4475c0c4..00000000 --- a/Sources/SQLite +++ /dev/null @@ -1 +0,0 @@ -../../sqlite/Sources/SQLite/ \ No newline at end of file diff --git a/Tests/FluentMySQL b/Tests/FluentMySQL deleted file mode 120000 index a0fe0fc5..00000000 --- a/Tests/FluentMySQL +++ /dev/null @@ -1 +0,0 @@ -../../fluent-mysql/Tests/FluentMySQL/ \ No newline at end of file diff --git a/Tests/FluentSQLite b/Tests/FluentSQLite deleted file mode 120000 index caf74a6f..00000000 --- a/Tests/FluentSQLite +++ /dev/null @@ -1 +0,0 @@ -../../fluent-sqlite/Tests/FluentSQLite/ \ No newline at end of file diff --git a/Tests/MySQL b/Tests/MySQL deleted file mode 120000 index c16f388d..00000000 --- a/Tests/MySQL +++ /dev/null @@ -1 +0,0 @@ -../../mysql/Tests/MySQL/ \ No newline at end of file diff --git a/Tests/SQLite b/Tests/SQLite deleted file mode 120000 index bb9955e9..00000000 --- a/Tests/SQLite +++ /dev/null @@ -1 +0,0 @@ -../../sqlite/Tests/SQLite/ \ No newline at end of file From 129fe2162ee9587ecb4954889c6cb28ad335ce2e Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Mon, 20 Jun 2016 18:27:39 -0400 Subject: [PATCH 09/10] schema tests + docs --- Package.swift | 5 +- Sources/Fluent/Helper/oldsql.swift | 292 ------------------ .../Preparation/Database+Preparation.swift | 7 + .../Preparation/Model+Preparation.swift | 9 + Sources/Fluent/Preparation/Preparation.swift | 16 + Sources/Fluent/SQL/GeneralSQLSerializer.swift | 20 +- Sources/Fluent/SQL/SQL.swift | 5 + Sources/Fluent/SQL/SQLSerializer.swift | 18 +- Sources/Fluent/Schema/Database+Schema.swift | 30 ++ Sources/Fluent/Schema/Schema+Creator.swift | 36 +++ Sources/Fluent/Schema/Schema+Field.swift | 12 + Sources/Fluent/Schema/Schema+Modifier.swift | 24 ++ Sources/Fluent/Schema/Schema.swift | 83 +---- Tests/Fluent/SchemaCreateTests.swift | 33 +- 14 files changed, 194 insertions(+), 396 deletions(-) delete mode 100644 Sources/Fluent/Helper/oldsql.swift create mode 100644 Sources/Fluent/Schema/Database+Schema.swift create mode 100644 Sources/Fluent/Schema/Schema+Creator.swift create mode 100644 Sources/Fluent/Schema/Schema+Field.swift create mode 100644 Sources/Fluent/Schema/Schema+Modifier.swift diff --git a/Package.swift b/Package.swift index 07e7015d..81e48ef1 100644 --- a/Package.swift +++ b/Package.swift @@ -7,6 +7,9 @@ let package = Package( .Package(url: "https://github.com/open-swift/C7.git", majorVersion: 0, minor: 9), // Syntax for easily accessing values from generic data. - .Package(url: "https://github.com/qutheory/polymorphic.git", majorVersion: 0, minor: 2) + .Package(url: "https://github.com/qutheory/polymorphic.git", majorVersion: 0, minor: 2), + + // Syntax for easily indexing arrays and dictionaries. + .Package(url: "https://github.com/qutheory/path-indexable.git", majorVersion: 0, minor: 2) ] ) diff --git a/Sources/Fluent/Helper/oldsql.swift b/Sources/Fluent/Helper/oldsql.swift deleted file mode 100644 index b1f96a01..00000000 --- a/Sources/Fluent/Helper/oldsql.swift +++ /dev/null @@ -1,292 +0,0 @@ -/** - A helper for creating generic - SQL statements from Fluent queries. - - Subclass this to support specific - SQL flavors, such as MySQL. -*/ - -/* -public class SQLClass { - public enum TableAction { - case create, alter - } - - public enum Action { - case select - case update - case insert - case delete - case table(TableAction) - } - - public struct Limit { - var count: Int - - } - - public enum Column { - case integer(String) - case string(String, Int) - } - - - /** - The table to query. - */ - public let table: String - public let action: Action - public let limit: Limit? - public let filters: [Filter] - public let data: [String: Value?]? - public let columns: [Column] - - public init(query: Query) { - table = query.entity - - switch query.action { - case .create: - action = .insert - case .delete: - action = .delete - case .fetch: - action = .select - case .update: - action = .update - } - - if let count = query.limit?.count { - limit = Limit(count: count) - } else { - limit = nil - } - - filters = query.filters - data = query.data - columns = [] - } - - public init(builder: Schema.Builder) { - table = builder.entity - action = .table(.create) - - columns = builder.fields.map { field in - switch field { - case .int(let name): - return .integer(name) - case .string(let name, let length): - return .string(name, length) - } - } - - limit = nil - filters = [] - data = nil - } - - public init( - table: String, - action: Action, - limit: Limit? = nil, - filters: [Filter] = [], - data: [String: Value?]? = nil, - columns: [Column] = [] - ) { - self.table = table - self.action = action - self.limit = limit - self.filters = filters - self.data = data - self.columns = [] - } - - - /** - The SQL statement string. - */ - public var statement: (query: String, values: [Value]) { - var values: [Value] = [] - - var statement = [action.sql] - - statement.append(table) - - switch action { - case .table(_): - if let columnClause = columnClause { - statement.append(columnClause) - } - case .select, .insert, .delete, .update: - if let (dataString, dataValues) = dataClause { - statement.append(dataString) - values += dataValues - } - - if let whereClause = whereClause { - statement.append("WHERE \(whereClause)") - } - - if let limit = limit where limit.count > 0 { - statement.append(limit.sql) - } - } - - let string = "\(statement.joined(separator: " "));" - return (string, values) - } - - /** - The next placeholder to use in - place of a value for parameterization. - */ - public var nextPlaceholder: String { - return "?" - } - - var columnClause: String? { - guard case .table(let action) = action else { - return nil - } - - var clause: [String] = [] - - switch action { - case .alter: - return "" // TODO - case .create: - for column in columns { - clause.append(column.sql) - } - } - - let string = clause.joined(separator: ",") - return "(" + string + ")" - } - - /** - The data clause containing - values for INSERT and UPDATE queries. - */ - var dataClause: (String, [Value])? { - guard let data = data else { - return nil - } - - switch action { - case .insert, .update: - // continue - break - default: - return nil - } - - let values: [Value] = data.values.flatMap { value in - return value - } - - let clause: String - - if case .insert = action { - let fields = data.keys.joined(separator: ", ") - - let values = data.flatMap { key, value in - return value.sql(placeholder: nextPlaceholder) - }.joined(separator: ", ") - - clause = "(\(fields)) VALUES (\(values))" - } else if case .update = action { - let updates = data.flatMap { key, value in - let string = value.sql(placeholder: nextPlaceholder) - return "\(key) = \(string)" - }.joined(separator: ", ") - - clause = "SET \(updates)" - } else { - return nil - } - - return (clause, values) - } - - /** - The where clause that filters - SELECT, UPDATE, and DELETE queries. - */ - var whereClause: (String, [Value])? { - if filters.count == 0 { - return nil - } - - var values: [Value] = [] - - for filter in filters { - switch filter { - case .compare(_, _, let compareValue): - values.append(compareValue) - case .subset(_, _, let subsetValues): - values += subsetValues - } - } - - var clause: [String] = [] - - for filter in filters { - let sql = filter.sql(placeholder: nextPlaceholder) - clause.append(sql) - } - - let string = clause.joined(separator: " AND ") - return (string, values) - } - -} - -extension Filter { - /** - Translates a filter to SQL. - */ - func sql(placeholder: String) -> String { - switch self { - case .compare(let field, let comparison, _): - return "\(field) \(comparison.sql) \(placeholder)" - case .subset(let field, let scope, let values): - let string = values.map { value in - return placeholder - }.joined(separator: ", ") - - return "\(field) \(scope.sql) (\(string))" - } - } -} - - -/** - Allows optionals to be targeted - in protocol extensions -*/ -private protocol Extractable { - associatedtype Wrapped - func extract() -> Wrapped? -} - -/** - Conforms `Optional` -*/ -extension Optional: Extractable { - private func extract() -> Wrapped? { - return self - } -} - -/** - Protocol extensions for `Value?` -*/ -extension Extractable where Wrapped == Value { - /** - Translates a `Value?` to SQL. - */ - func sql(placeholder: String) -> String { - return self.extract()?.sql(placeholder: placeholder) ?? "NULL" - } -}*/ - diff --git a/Sources/Fluent/Preparation/Database+Preparation.swift b/Sources/Fluent/Preparation/Database+Preparation.swift index 330f75d3..f52568cb 100644 --- a/Sources/Fluent/Preparation/Database+Preparation.swift +++ b/Sources/Fluent/Preparation/Database+Preparation.swift @@ -42,3 +42,10 @@ extension Database { try migration.save() } } + +extension Preparation { + public static var name: String { + let type = "\(self.dynamicType)" + return type.components(separatedBy: ".Type").first ?? type + } +} diff --git a/Sources/Fluent/Preparation/Model+Preparation.swift b/Sources/Fluent/Preparation/Model+Preparation.swift index d3082ec0..497e37a7 100644 --- a/Sources/Fluent/Preparation/Model+Preparation.swift +++ b/Sources/Fluent/Preparation/Model+Preparation.swift @@ -1,6 +1,9 @@ // MARK: Preparation extension Model { + /** + Automates the preparation of a model. + */ public static func prepare(database: Database) throws { try database.create(entity) { builder in let model = self.init() @@ -29,6 +32,9 @@ extension Model { } } + /** + Automates reverting the model's preparation. + */ public static func revert(database: Database) throws { try database.delete(entity) } @@ -41,6 +47,9 @@ extension Model { self.init(serialized: [:]) } + /** + Automates the serialization of a model. + */ public func serialize() -> [String: Value?] { var serialized: [String: Value?] = [:] diff --git a/Sources/Fluent/Preparation/Preparation.swift b/Sources/Fluent/Preparation/Preparation.swift index d24c2db0..5dde903d 100644 --- a/Sources/Fluent/Preparation/Preparation.swift +++ b/Sources/Fluent/Preparation/Preparation.swift @@ -1,4 +1,20 @@ +/** + A preparation prepares the database for + any task that it may need to perform during runtime. +*/ public protocol Preparation { + /** + The prepare method should call any methods + it needs on the database to prepare. + */ static func prepare(database: Database) throws + + /** + The revert method should undo any actions + caused by the prepare method. + + If this is impossible, the `PreparationError.revertImpossible` + error should be thrown. + */ static func revert(database: Database) throws } diff --git a/Sources/Fluent/SQL/GeneralSQLSerializer.swift b/Sources/Fluent/SQL/GeneralSQLSerializer.swift index 4b74f8e0..e4adcb7e 100644 --- a/Sources/Fluent/SQL/GeneralSQLSerializer.swift +++ b/Sources/Fluent/SQL/GeneralSQLSerializer.swift @@ -1,3 +1,8 @@ +/** + A generic SQL serializer. + This class can be subclassed by + specific SQL serializers. +*/ public class GeneralSQLSerializer: SQLSerializer { public let sql: SQL @@ -181,16 +186,19 @@ public class GeneralSQLSerializer: SQLSerializer { clause += "ALTER TABLE" clause += sql(table) + var subclause: [String] = [] + for column in create { - clause += "ADD" - clause += sql(column) + subclause += "ADD " + sql(column) } for name in delete { - clause += "DROP COLUMN" - clause += sql(name) + subclause += "DROP " + sql(name) } + clause += sql(list: subclause) + + return sql(clause) case .create(let columns): var clause: [String] = [] @@ -247,11 +255,11 @@ public class GeneralSQLSerializer: SQLSerializer { } public func sql(list: [String]) -> String { - return "(" + list.joined(separator: ",") + ")" + return "(" + list.joined(separator: ", ") + ")" } public func sql(_ values: [Value]) -> String { - return "(" + values.map { sql($0) }.joined(separator: ",") + ")" + return "(" + values.map { sql($0) }.joined(separator: ", ") + ")" } public func sql(_ value: Value) -> String { diff --git a/Sources/Fluent/SQL/SQL.swift b/Sources/Fluent/SQL/SQL.swift index 9ec535ea..cd920f69 100644 --- a/Sources/Fluent/SQL/SQL.swift +++ b/Sources/Fluent/SQL/SQL.swift @@ -1,3 +1,8 @@ +/** + 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: [Column]) diff --git a/Sources/Fluent/SQL/SQLSerializer.swift b/Sources/Fluent/SQL/SQLSerializer.swift index 22a7e07a..4fb00a2c 100644 --- a/Sources/Fluent/SQL/SQLSerializer.swift +++ b/Sources/Fluent/SQL/SQLSerializer.swift @@ -1,19 +1,7 @@ +/** + A SQL serializer. +*/ public protocol SQLSerializer { init(sql: SQL) func serialize() -> (String, [Value]) } - -final class SQLiteSerializer: GeneralSQLSerializer { - override func sql(_ column: SQL.Column) -> String { - switch column { - case .primaryKey: - return sql("id") + " INTEGER PRIMARY KEY" - case .integer(let name): - return sql(name) + " INTEGER" - case .string(let name, _): - return sql(name) + " TEXT" - case .double(let name, _, _): - return sql(name) + " DOUBLE" - } - } -} diff --git a/Sources/Fluent/Schema/Database+Schema.swift b/Sources/Fluent/Schema/Database+Schema.swift new file mode 100644 index 00000000..76ff5f12 --- /dev/null +++ b/Sources/Fluent/Schema/Database+Schema.swift @@ -0,0 +1,30 @@ +extension Database { + /** + Modifies the schema of the database + for the given entity. + */ + public func modify(_ entity: String, closure: (Schema.Modifier) throws -> ()) throws { + let modifier = Schema.Modifier(entity) + try closure(modifier) + _ = try driver.schema(modifier.schema) + } + + /** + Creates the schema of the database + for the given entity. + */ + public func create(_ entity: String, closure: (Schema.Creator) throws -> ()) throws { + let creator = Schema.Creator(entity) + try closure(creator) + _ = try driver.schema(creator.schema) + } + + /** + Deletes the schema of the database + for the given entity. + */ + public func delete(_ entity: String) throws { + let schema = Schema.delete(entity: entity) + _ = try driver.schema(schema) + } +} diff --git a/Sources/Fluent/Schema/Schema+Creator.swift b/Sources/Fluent/Schema/Schema+Creator.swift new file mode 100644 index 00000000..c1353dfa --- /dev/null +++ b/Sources/Fluent/Schema/Schema+Creator.swift @@ -0,0 +1,36 @@ +extension Schema { + /** + Creates a Schema. + + Cannot modify or delete fields. + */ + public class Creator { + public let entity: String + public var fields: [Field] + + public init(_ entity: String) { + self.entity = entity + fields = [] + } + + public func id() { + fields.append(.id) + } + + public func int(_ name: String) { + fields.append(.int(name)) + } + + public func string(_ name: String, length: Int? = nil) { + fields.append(.string(name, length: length)) + } + + public func double(_ name: String, digits: Int? = nil, decimal: Int? = nil) { + fields.append(.double(name, digits: digits, decimal: decimal)) + } + + public var schema: Schema { + return .create(entity: entity, create: fields) + } + } +} diff --git a/Sources/Fluent/Schema/Schema+Field.swift b/Sources/Fluent/Schema/Schema+Field.swift new file mode 100644 index 00000000..669ec8c1 --- /dev/null +++ b/Sources/Fluent/Schema/Schema+Field.swift @@ -0,0 +1,12 @@ +extension Schema { + /** + Various types of fields + that can be used in a Schema. + */ + public enum Field { + case id + case int(String) + case string(String, length: Int?) + case double(String, digits: Int?, decimal: Int?) + } +} diff --git a/Sources/Fluent/Schema/Schema+Modifier.swift b/Sources/Fluent/Schema/Schema+Modifier.swift new file mode 100644 index 00000000..6d673f78 --- /dev/null +++ b/Sources/Fluent/Schema/Schema+Modifier.swift @@ -0,0 +1,24 @@ +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 index 7b8f0c0a..3cdf7bdc 100644 --- a/Sources/Fluent/Schema/Schema.swift +++ b/Sources/Fluent/Schema/Schema.swift @@ -1,84 +1,9 @@ +/** + 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) } - -extension Schema { - public enum Field { - case id - case int(String) - case string(String, length: Int?) - case double(String, digits: Int?, decimal: Int?) - } -} - -extension Schema { - 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) - } - } - - public class Creator { - public let entity: String - public var fields: [Field] - - public init(_ entity: String) { - self.entity = entity - fields = [] - } - - public func id() { - fields.append(.id) - } - - public func int(_ name: String) { - fields.append(.int(name)) - } - - public func string(_ name: String, length: Int? = nil) { - fields.append(.string(name, length: length)) - } - - public func double(_ name: String, digits: Int? = nil, decimal: Int? = nil) { - fields.append(.double(name, digits: digits, decimal: decimal)) - } - - public var schema: Schema { - return .create(entity: entity, create: fields) - } - } -} - - -extension Database { - public func modify(_ entity: String, closure: (Schema.Modifier) throws -> ()) throws { - let modifier = Schema.Modifier(entity) - try closure(modifier) - _ = try driver.schema(modifier.schema) - } - - public func create(_ entity: String, closure: (Schema.Creator) throws -> ()) throws { - let creator = Schema.Creator(entity) - try closure(creator) - _ = try driver.schema(creator.schema) - } - - public func delete(_ entity: String) throws { - let schema = Schema.delete(entity: entity) - _ = try driver.schema(schema) - } -} diff --git a/Tests/Fluent/SchemaCreateTests.swift b/Tests/Fluent/SchemaCreateTests.swift index 3f706193..1305f5ca 100644 --- a/Tests/Fluent/SchemaCreateTests.swift +++ b/Tests/Fluent/SchemaCreateTests.swift @@ -14,12 +14,39 @@ class SchemaCreateTests: XCTestCase { builder.string("email", length: 256) let sql = builder.schema.sql + let serializer = GeneralSQLSerializer(sql: sql) + + let (statement, values) = serializer.serialize() + + XCTAssertEqual(statement, "CREATE TABLE `users` (`id` INTEGER, `name` STRING, `email` STRING)") + XCTAssertEqual(values.count, 0) + } + func testModify() throws { + let builder = Schema.Modifier("users") + + 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 sqliteSerializer = SQLiteSerializer(sql: sql) - print(serializer.serialize()) - print(sqliteSerializer.serialize()) + let (statement, values) = serializer.serialize() + + XCTAssertEqual(statement, "ALTER TABLE `users` (ADD `id` INTEGER, ADD `name` STRING, ADD `email` STRING, 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 (statement, values) = serializer.serialize() + + XCTAssertEqual(statement, "DROP TABLE `users`") + XCTAssertEqual(values.count, 0) + } } From 319012fb2440c1a85c4fe8c38971d0679af1533e Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Mon, 20 Jun 2016 19:17:22 -0400 Subject: [PATCH 10/10] add database files --- .gitignore | 4 +-- Sources/Fluent/Database/Database.swift | 34 +++++++++++++++++++++++ Sources/Fluent/Database/Driver.swift | 31 +++++++++++++++++++++ Sources/Fluent/Database/PrintDriver.swift | 28 +++++++++++++++++++ Sources/Fluent/Query/Query.swift | 2 +- 5 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 Sources/Fluent/Database/Database.swift create mode 100644 Sources/Fluent/Database/Driver.swift create mode 100644 Sources/Fluent/Database/PrintDriver.swift diff --git a/.gitignore b/.gitignore index 29d0b557..cff3dd9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ Packages .build - -Database - .DS_Store *.xcodeproj + diff --git a/Sources/Fluent/Database/Database.swift b/Sources/Fluent/Database/Database.swift new file mode 100644 index 00000000..6c7b326c --- /dev/null +++ b/Sources/Fluent/Database/Database.swift @@ -0,0 +1,34 @@ +import Foundation + +/** + References a database with a single `Driver`. + Statically maps `Model`s to `Database`s. +*/ +public class Database { + /** + The `Driver` powering this database. + Responsible for executing queries. + */ + public let driver: Driver + + /** + Creates a `Database` with the supplied + `Driver`. This cannot be changed later. + */ + public init(driver: Driver) { + self.driver = driver + } + + /** + Maps `Model` names to their respective + `Database`. This allows multiple models + in the same application to use different + methods of data persistence. + */ + public static var map: [String: Database] = [:] + + /** + The default database for all `Model` types. + */ + public static var `default`: Database = Database(driver: PrintDriver()) +} diff --git a/Sources/Fluent/Database/Driver.swift b/Sources/Fluent/Database/Driver.swift new file mode 100644 index 00000000..9269724f --- /dev/null +++ b/Sources/Fluent/Database/Driver.swift @@ -0,0 +1,31 @@ +/** + A `Driver` execute queries + and returns an array of results. + It is responsible for interfacing + with the data store powering Fluent. +*/ +public protocol Driver { + /** + The string value for the + default identifier key. + + The `idKey` will be used when + `Model.find(_:)` or other find + by identifier methods are used. + */ + var idKey: String { get } + + /** + Executes a `Query` from and + returns an array of results fetched, + created, or updated by the action. + */ + @discardableResult + func query(_ query: Query) throws -> [[String: Value]] + + /** + Creates the `Schema` indicated + by the `Builder`. + */ + func schema(_ schema: Schema) throws +} diff --git a/Sources/Fluent/Database/PrintDriver.swift b/Sources/Fluent/Database/PrintDriver.swift new file mode 100644 index 00000000..62a11458 --- /dev/null +++ b/Sources/Fluent/Database/PrintDriver.swift @@ -0,0 +1,28 @@ +/** + A dummy `Driver` useful for developing. +*/ +public class PrintDriver: Driver { + public var idKey: String = "foo" + + public func query(_ query: Query) throws -> [[String : Value]] { + + let sql = SQL(query: query) + let serializer = GeneralSQLSerializer(sql: sql) + + let (statement, values) = serializer.serialize() + print("[Print driver]") + + print("Statement: \(statement) Values: \(values)") + + print("Table \(query.entity)") + print("Action \(query.action)") + print("Filters \(query.filters)") + print() + + return [] + } + + public func schema(_ schema: Schema) throws { + //let sql = SQL(builder: builder) + } +} \ No newline at end of file diff --git a/Sources/Fluent/Query/Query.swift b/Sources/Fluent/Query/Query.swift index 59f15699..50626487 100644 --- a/Sources/Fluent/Query/Query.swift +++ b/Sources/Fluent/Query/Query.swift @@ -49,7 +49,7 @@ public class Query { Creates a new `Query` with the `Model`'s database. */ - init() { + public init() { filters = [] action = .fetch database = T.database