From 129fe2162ee9587ecb4954889c6cb28ad335ce2e Mon Sep 17 00:00:00 2001 From: Tanner Nelson Date: Mon, 20 Jun 2016 18:27:39 -0400 Subject: [PATCH] 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) + } }