From a03ea468929d0cf1868cce8f48edfb5e6db17348 Mon Sep 17 00:00:00 2001 From: AnthonyAmanse Date: Fri, 22 Jun 2018 11:42:36 -0700 Subject: [PATCH] Add more options in findAll This commit adds in options: * ORDER BY clause for sorting * OFFSET and LIMIT for retrieving a portion of the rows This builds the appropriate SQL queries for the options listed above. This commit also adds in initial tests for the new options. Signed-off-by: AnthonyAmanse --- Sources/SwiftKueryORM/Model.swift | 92 +++++++++++----- Tests/SwiftKueryORMTests/CommonUtils.swift | 17 ++- Tests/SwiftKueryORMTests/TestFind.swift | 117 +++++++++++++++++++++ 3 files changed, 198 insertions(+), 28 deletions(-) diff --git a/Sources/SwiftKueryORM/Model.swift b/Sources/SwiftKueryORM/Model.swift index cd951ad..2f1ccd9 100644 --- a/Sources/SwiftKueryORM/Model.swift +++ b/Sources/SwiftKueryORM/Model.swift @@ -73,15 +73,15 @@ public protocol Model: Codable { /// Call to find all the models in the database that accepts a completion /// handler. The callback is passed an array of models or an error - static func findAll(using db: Database?, _ onCompletion: @escaping ([Self]?, RequestError?) -> Void) + static func findAll(using db: Database?, order: Order..., offset: Int?, limit: Int?, _ onCompletion: @escaping ([Self]?, RequestError?) -> Void) /// Call to find all the models in the database that accepts a completion /// handler. The callback is passed an array of tuples (id, model) or an error - static func findAll(using db: Database?, _ onCompletion: @escaping ([(I, Self)]?, RequestError?) -> Void) + static func findAll(using db: Database?, order: Order..., offset: Int?, limit: Int?, _ onCompletion: @escaping ([(I, Self)]?, RequestError?) -> Void) /// Call to find all the models in the database that accepts a completion /// handler. The callback is passed a dictionary [id: model] or an error - static func findAll(using db: Database?, _ onCompletion: @escaping ([I: Self]?, RequestError?) -> Void) + static func findAll(using db: Database?, order: Order..., offset: Int?, limit: Int?, _ onCompletion: @escaping ([I: Self]?, RequestError?) -> Void) /// Call to find all the models in the database matching the QueryParams that accepts a completion /// handler. The callback is passed an array of models or an error @@ -895,25 +895,32 @@ public extension Model { } /** - This function constructs an array of OrderBy from the QueryParameters values + This function constructs an array of OrderBy from the QueryParameters or the Order values */ - private static func getOrderBy(values: [String: Any], table: Table) -> [OrderBy] { + private static func getOrderBy(values: [String: Any]? = nil, order: [Order]? = nil, table: Table) -> [OrderBy] { var orderByArray: [OrderBy] = [] - for (_, value) in values { - if let orderValue = value as? Ordering { - let columnDictionary = table.columns.reduce(into: [String: Column]()) { dict, value in - dict[value.name] = value - } - let orders = orderValue.getValues() - for order in orders where columnDictionary[order.value] != nil { - let column = columnDictionary[order.value]! - if case .asc(_) = order { - orderByArray.append(.ASC(column)) - } else { - orderByArray.append(.DESC(column)) - } - } - } + var orders: [Order] = [] + let columnDictionary = table.columns.reduce(into: [String: Column]()) { dict, value in + dict[value.name] = value + } + + if let order = order { + orders = order + } else if let values = values { + for (_, value) in values { + if let orderValue = value as? Ordering { + orders = orderValue.getValues() + } + } + } + + for order in orders where columnDictionary[order.value] != nil { + let column = columnDictionary[order.value]! + if case .asc(_) = order { + orderByArray.append(.ASC(column)) + } else { + orderByArray.append(.DESC(column)) + } } return orderByArray @@ -967,7 +974,7 @@ public extension Model { /// - static func findAll(using db: Database? = nil, _ onCompletion: @escaping ([Self]?, RequestError?) -> Void) { + static func findAll(using db: Database? = nil, order: Order..., offset: Int? = nil, limit: Int? = nil, _ onCompletion: @escaping ([Self]?, RequestError?) -> Void) { var table: Table do { table = try Self.getTable() @@ -975,15 +982,26 @@ public extension Model { onCompletion(nil, Self.convertError(error)) return } + let orderBy: [OrderBy] = Self.getOrderBy(order: order, table: table) + + var query = Select(from: table) + if orderBy.count > 0 { + query = query.order(by: orderBy) + } + if let offset = offset { + query = query.offset(offset) + } + if let limit = limit { + query = query.limit(to: limit) + } - let query = Select(from: table) Self.executeQuery(query: query, using: db, onCompletion) } /// Find all the models /// - Parameter using: Optional Database to use /// - Returns: An array of tuples (id, model) - static func findAll(using db: Database? = nil, _ onCompletion: @escaping ([(I, Self)]?, RequestError?) -> Void) { + static func findAll(using db: Database? = nil, order: Order..., offset: Int? = nil, limit: Int? = nil, _ onCompletion: @escaping ([(I, Self)]?, RequestError?) -> Void) { var table: Table do { table = try Self.getTable() @@ -991,13 +1009,24 @@ public extension Model { onCompletion(nil, Self.convertError(error)) return } + let orderBy: [OrderBy] = Self.getOrderBy(order: order, table: table) + + var query = Select(from: table) + if orderBy.count > 0 { + query = query.order(by: orderBy) + } + if let offset = offset { + query = query.offset(offset) + } + if let limit = limit { + query = query.limit(to: limit) + } - let query = Select(from: table) Self.executeQuery(query: query, using: db, onCompletion) } /// :nodoc: - static func findAll(using db: Database? = nil, _ onCompletion: @escaping ([I: Self]?, RequestError?) -> Void) { + static func findAll(using db: Database? = nil, order: Order..., offset: Int? = nil, limit: Int? = nil, _ onCompletion: @escaping ([I: Self]?, RequestError?) -> Void) { var table: Table do { table = try Self.getTable() @@ -1005,8 +1034,19 @@ public extension Model { onCompletion(nil, Self.convertError(error)) return } + let orderBy: [OrderBy] = Self.getOrderBy(order: order, table: table) + + var query = Select(from: table) + if orderBy.count > 0 { + query = query.order(by: orderBy) + } + if let offset = offset { + query = query.offset(offset) + } + if let limit = limit { + query = query.limit(to: limit) + } - let query = Select(from: table) Self.executeQuery(query: query, using: db) { (tuples: [(I, Self)]?, error: RequestError?) in if let error = error { onCompletion(nil, error) diff --git a/Tests/SwiftKueryORMTests/CommonUtils.swift b/Tests/SwiftKueryORMTests/CommonUtils.swift index d1e399c..b434ccb 100644 --- a/Tests/SwiftKueryORMTests/CommonUtils.swift +++ b/Tests/SwiftKueryORMTests/CommonUtils.swift @@ -35,6 +35,8 @@ class TestConnection: Connection { case returnEmpty case returnOneRow case returnThreeRows + case returnThreeRowsSortedAscending + case returnThreeRowsSortedDescending case returnError case returnValue } @@ -100,6 +102,10 @@ class TestConnection: Connection { onCompletion(.resultSet(ResultSet(TestResultFetcher(numberOfRows: 1)))) case .returnThreeRows: onCompletion(.resultSet(ResultSet(TestResultFetcher(numberOfRows: 3)))) + case .returnThreeRowsSortedAscending: + onCompletion(.resultSet(ResultSet(TestResultFetcher(numberOfRows: 3, sortedByAge: "ascending")))) + case .returnThreeRowsSortedDescending: + onCompletion(.resultSet(ResultSet(TestResultFetcher(numberOfRows: 3, sortedByAge: "descending")))) case .returnError: onCompletion(.error(QueryError.noResult("Error in query execution."))) case .returnValue: @@ -136,12 +142,19 @@ class TestConnection: Connection { class TestResultFetcher: ResultFetcher { let numberOfRows: Int - let rows = [[1, "Joe", Int32(38)], [2, "Adam", Int32(28)], [3, "Chris", Int32(36)]] + var rows = [[1, "Joe", Int32(38)], [2, "Adam", Int32(28)], [3, "Chris", Int32(36)]] let titles = ["id", "name", "age"] var fetched = 0 - init(numberOfRows: Int) { + init(numberOfRows: Int, sortedByAge: String? = nil) { self.numberOfRows = numberOfRows + if let sortedByAge = sortedByAge { + if sortedByAge == "descending" { + rows.sort {($0[2] as! Int32) > ($1[2] as! Int32)} + } else if sortedByAge == "ascending" { + rows.sort {($0[2] as! Int32) < ($1[2] as! Int32)} + } + } } func fetchNext() -> [Any?]? { diff --git a/Tests/SwiftKueryORMTests/TestFind.swift b/Tests/SwiftKueryORMTests/TestFind.swift index 12617ea..62a4424 100644 --- a/Tests/SwiftKueryORMTests/TestFind.swift +++ b/Tests/SwiftKueryORMTests/TestFind.swift @@ -10,6 +10,10 @@ class TestFind: XCTestCase { ("testFind", testFind), ("testFindAll", testFindAll), ("testFindAllMatching", testFindAllMatching), + ("testFindAllLimit", testFindAllLimit), + ("testFindAllLimitAndOffset", testFindAllLimitAndOffset), + ("testFindAllOrderByDescending", testFindAllOrderByDescending), + ("testFindAllOrderByAscending", testFindAllOrderByAscending), ] } @@ -113,4 +117,117 @@ class TestFind: XCTestCase { } }) } + + + /** + Testing that the correct SQL Query is created to retrieve a specific model. + Testing that the model can be retrieved + */ + func testFindAllLimit() { + let connection: TestConnection = createConnection(.returnOneRow) + Database.default = Database(single: connection) + performTest(asyncTasks: { expectation in + Person.findAll(limit: 1) { array, error in + XCTAssertNil(error, "Find Failed: \(String(describing: error))") + XCTAssertNotNil(connection.query, "Find Failed: Query is nil") + if let query = connection.query { + let expectedQuery = "SELECT * FROM \"People\" LIMIT 1" + let resultQuery = connection.descriptionOf(query: query) + XCTAssertEqual(resultQuery, expectedQuery, "Find Failed: Invalid query") + } + XCTAssertNotNil(array, "Find Failed: No array of models returned") + if let array = array { + XCTAssertEqual(array.count, 1, "Find Failed: \(String(describing: array.count)) is not equal to 1") + } + expectation.fulfill() + } + }) + } + + /** + Testing that the correct SQL Query is created to retrieve a specific model. + Testing that the model can be retrieved + */ + func testFindAllLimitAndOffset() { + let connection: TestConnection = createConnection(.returnOneRow) + Database.default = Database(single: connection) + performTest(asyncTasks: { expectation in + Person.findAll(offset: 2, limit: 1) { array, error in + XCTAssertNil(error, "Find Failed: \(String(describing: error))") + XCTAssertNotNil(connection.query, "Find Failed: Query is nil") + if let query = connection.query { + let expectedQuery = "SELECT * FROM \"People\" LIMIT 1 OFFSET 2" + let resultQuery = connection.descriptionOf(query: query) + XCTAssertEqual(resultQuery, expectedQuery, "Find Failed: Invalid query") + } + XCTAssertNotNil(array, "Find Failed: No array of models returned") + if let array = array { + XCTAssertEqual(array.count, 1, "Find Failed: \(String(describing: array.count)) is not equal to 1") + } + expectation.fulfill() + } + }) + } + + /** + Testing that the correct SQL Query is created to retrieve a specific model. + Testing that correct amount of models are retrieved + Testing that models are sorted by age in descending order + */ + func testFindAllOrderByDescending() { + let connection: TestConnection = createConnection(.returnThreeRowsSortedDescending) + Database.default = Database(single: connection) + performTest(asyncTasks: { expectation in + Person.findAll(order: Order.desc("age")) { array, error in + XCTAssertNil(error, "Find Failed: \(String(describing: error))") + XCTAssertNotNil(connection.query, "Find Failed: Query is nil") + if let query = connection.query { + let expectedQuery = "SELECT * FROM \"People\" ORDER BY \"People\".\"age\" DESC" + let resultQuery = connection.descriptionOf(query: query) + XCTAssertEqual(resultQuery, expectedQuery, "Find Failed: Invalid query") + } + XCTAssertNotNil(array, "Find Failed: No array of models returned") + if let array = array { + for (index, person) in array.enumerated() { + if index + 1 < array.count { + XCTAssertGreaterThanOrEqual(person.age, array[index + 1].age, "Find Failed: Age of person: \(String(describing: person.age)) is not greater than or equal to age of next person: \(String(describing: array[index + 1].age))") + } + } + XCTAssertEqual(array.count, 3, "Find Failed: \(String(describing: array.count)) is not equal to 3") + } + expectation.fulfill() + } + }) + } + + /** + Testing that the correct SQL Query is created to retrieve a specific model. + Testing that correct amount of models are retrieved + Testing that models are sorted by age in ascending order + */ + func testFindAllOrderByAscending() { + let connection: TestConnection = createConnection(.returnThreeRowsSortedAscending) + Database.default = Database(single: connection) + performTest(asyncTasks: { expectation in + Person.findAll(order: Order.asc("age")) { array, error in + XCTAssertNil(error, "Find Failed: \(String(describing: error))") + XCTAssertNotNil(connection.query, "Find Failed: Query is nil") + if let query = connection.query { + let expectedQuery = "SELECT * FROM \"People\" ORDER BY \"People\".\"age\" ASC" + let resultQuery = connection.descriptionOf(query: query) + XCTAssertEqual(resultQuery, expectedQuery, "Find Failed: Invalid query") + } + XCTAssertNotNil(array, "Find Failed: No array of models returned") + if let array = array { + for (index, person) in array.enumerated() { + if index + 1 < array.count { + XCTAssertLessThanOrEqual(person.age, array[index + 1].age, "Find Failed: Age of person: \(String(describing: person.age)) is not less than or equal to age of next person: \(String(describing: array[index + 1].age))") + } + } + XCTAssertEqual(array.count, 3, "Find Failed: \(String(describing: array.count)) is not equal to 3") + } + expectation.fulfill() + } + }) + } }