├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── MySQLStORM │ ├── Convenience.swift │ ├── DataConverters.swift │ ├── Delete.swift │ ├── Insert.swift │ ├── MySQLConnect.swift │ ├── MySQLDataType.swift │ ├── MySQLStORM.swift │ ├── SQL.swift │ ├── Select.swift │ ├── Update.swift │ ├── Upsert.swift │ └── parseRows.swift └── Tests ├── LinuxMain.swift └── MySQLStORMTests ├── MySQLStORMTests.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/screenshots 64 | 65 | Packages/ 66 | *.xcodeproj 67 | .build 68 | .packages_lin 69 | .build_lin 70 | buildlinux 71 | Package.pins 72 | Package.resolved 73 | PADockerfile_build 74 | StORMlog.txt 75 | .DS_Store 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | // Generated automatically by Perfect Assistant Application 3 | // Date: 2017-10-03 17:18:40 +0000 4 | import PackageDescription 5 | let package = Package( 6 | name: "MySQLStORM", 7 | products: [ 8 | .library(name: "MySQLStORM", targets: ["MySQLStORM"]) 9 | ], 10 | dependencies: [ 11 | .package(url: "https://github.com/PerfectlySoft/Perfect-MySQL.git", from: "3.0.0"), 12 | .package(url: "https://github.com/SwiftORM/StORM.git", from: "3.0.0"), 13 | .package(url: "https://github.com/PerfectlySoft/Perfect-Logger.git", from: "3.0.0"), 14 | ], 15 | targets: [ 16 | .target(name: "MySQLStORM", dependencies: ["PerfectMySQL", "StORM", "PerfectLogger"]) 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MySQL-StORM 2 | 3 | MySQL-StORM is the MySQL module for StORM - a Swift ORM. 4 | 5 | It aims to be easy to use, but flexible. Drawing on previous experiences, whether they be good, bad or ugly, of other ORM's, I have tried to build a system that allows you write great code without worrying about the details of how to interact with the database. 6 | 7 | Other database wrappers will be available shortly. They will all use the StORM base, and provide as much consistency between datasources as possible. 8 | 9 | StORM is built on top of [Perfect](https://github.com/PerfectlySoft/Perfect) - the most mature of the Server Side Swift platforms. 10 | 11 | ### What does it do? 12 | 13 | * Abstracts the database layer from your code. 14 | * Provides a way of adding save, delete, find to your Swift classes 15 | * Gives you access to more powerful select, insert, update, delete, and raw SQL querying. 16 | * Maps result sets to your classes 17 | 18 | 19 | ### What does it not do? 20 | 21 | Right now there are a few things missing, but the basics are there. 22 | 23 | On the "TODO" list are: 24 | 25 | * complete joins 26 | * complete having 27 | * complete group by 28 | * upsert 29 | * documentation 30 | * complete test coverage 31 | -------------------------------------------------------------------------------- /Sources/MySQLStORM/Convenience.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Convenience.swift 3 | // PostgresSTORM 4 | // 5 | // Created by Jonathan Guthrie on 2016-10-04. 6 | // 7 | // 8 | 9 | 10 | import StORM 11 | import PerfectLogger 12 | 13 | /// Convenience methods extending the main class. 14 | extension MySQLStORM { 15 | 16 | /// Deletes one row, with an id. 17 | /// Presumes first property in class is the id. 18 | public func delete() throws { 19 | let (idname, idval) = firstAsKey() 20 | do { 21 | try exec(deleteSQL(self.table(), idName: idname), params: [String(describing: idval)]) 22 | } catch { 23 | LogFile.error("Error msg: \(error)", logFile: "./StORMlog.txt") 24 | self.error = StORMError.error("\(error)") 25 | throw error 26 | } 27 | } 28 | 29 | /// Deletes one row, with the id as set. 30 | public func delete(_ id: Any) throws { 31 | let (idname, _) = firstAsKey() 32 | do { 33 | try exec(deleteSQL(self.table(), idName: idname), params: [String(describing: id)]) 34 | } catch { 35 | LogFile.error("Error msg: \(error)", logFile: "./StORMlog.txt") 36 | self.error = StORMError.error("\(error)") 37 | throw error 38 | } 39 | } 40 | 41 | /// Retrieves a single row with the supplied ID. 42 | public func get(_ id: Any) throws { 43 | let (idname, _) = firstAsKey() 44 | do { 45 | try select(whereclause: "\(idname) = ?", params: [String(describing: id)], orderby: []) 46 | } catch { 47 | LogFile.error("Error msg: \(error)", logFile: "./StORMlog.txt") 48 | self.error = StORMError.error("\(error)") 49 | throw error 50 | } 51 | } 52 | 53 | /// Retrieves a single row with the ID as set. 54 | public func get() throws { 55 | let (idname, idval) = firstAsKey() 56 | do { 57 | try select(whereclause: "\(idname) = ?", params: [String(describing: idval)], orderby: []) 58 | } catch { 59 | LogFile.error("Error msg: \(error)", logFile: "./StORMlog.txt") 60 | self.error = StORMError.error("\(error)") 61 | throw error 62 | } 63 | } 64 | 65 | /// Performs a find on mathing column name/value pairs. 66 | public func find(_ data: [(String, Any)]) throws { 67 | let (idname, _) = firstAsKey() 68 | 69 | var paramsString = [String]() 70 | var set = [String]() 71 | for i in data { 72 | paramsString.append(String(describing: i.1)) 73 | set.append("\(i.0) = ?") 74 | } 75 | 76 | do { 77 | try select(whereclause: set.joined(separator: " AND "), params: paramsString, orderby: [idname]) 78 | } catch { 79 | LogFile.error("Error msg: \(error)", logFile: "./StORMlog.txt") 80 | self.error = StORMError.error("\(error)") 81 | throw error 82 | } 83 | 84 | } 85 | 86 | /// Performs a find on mathing column name/value pairs. 87 | public func find(_ data: [String: Any]) throws { 88 | let (idname, _) = firstAsKey() 89 | 90 | var paramsString = [String]() 91 | var set = [String]() 92 | for (key, value) in data { 93 | paramsString.append(String(describing: value)) 94 | set.append("\(key) = ?") 95 | } 96 | 97 | do { 98 | try select(whereclause: set.joined(separator: " AND "), params: paramsString, orderby: [idname]) 99 | } catch { 100 | LogFile.error("Error msg: \(error)", logFile: "./StORMlog.txt") 101 | self.error = StORMError.error("\(error)") 102 | throw error 103 | } 104 | 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /Sources/MySQLStORM/DataConverters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataConverters.swift 3 | // MySQLStORM 4 | // 5 | // Created by Jonathan Guthrie on 2017-08-10. 6 | // 7 | 8 | import Foundation 9 | 10 | extension MySQLStORM { 11 | 12 | fileprivate func trim(_ str: String) -> String { 13 | return str.trimmingCharacters(in: .whitespacesAndNewlines) 14 | } 15 | 16 | public func toArrayString(_ input: Any) -> [String] { 17 | return (input as? String ?? "").split(separator: ",").map { trim(String($0)) } 18 | } 19 | public func toArrayInt(_ input: Any) -> [Int] { 20 | // Needs refinement. Should not be 0 21 | return (input as? String ?? "").split(separator: ",").map { Int(trim(String($0))) ?? 0 } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/MySQLStORM/Delete.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Delete.swift 3 | // PostgresStORM 4 | // 5 | // Created by Jonathan Guthrie on 2016-09-24. 6 | // 7 | // 8 | 9 | import PerfectLib 10 | import StORM 11 | import PerfectLogger 12 | 13 | /// Performs delete-specific functions as an extension 14 | extension MySQLStORM { 15 | 16 | func deleteSQL(_ table: String, idName: String = "id") -> String { 17 | return "DELETE FROM \(table) WHERE \(idName) = ?" 18 | } 19 | 20 | /// Deletes one row, with an id as an integer 21 | @discardableResult 22 | public func delete(_ id: Int, idName: String = "id") throws -> Bool { 23 | do { 24 | try exec(deleteSQL(self.table(), idName: idName), params: [String(id)]) 25 | } catch { 26 | LogFile.error("Error msg: \(error)", logFile: "./StORMlog.txt") 27 | self.error = StORMError.error("\(error)") 28 | throw error 29 | } 30 | return true 31 | } 32 | 33 | /// Deletes one row, with an id as a String 34 | @discardableResult 35 | public func delete(_ id: String, idName: String = "id") throws -> Bool { 36 | do { 37 | try exec(deleteSQL(self.table(), idName: idName), params: [id]) 38 | } catch { 39 | LogFile.error("Error msg: \(error)", logFile: "./StORMlog.txt") 40 | self.error = StORMError.error("\(error)") 41 | throw error 42 | } 43 | return true 44 | } 45 | 46 | /// Deletes one row, with an id as a UUID 47 | @discardableResult 48 | public func delete(_ id: UUID, idName: String = "id") throws -> Bool { 49 | do { 50 | try exec(deleteSQL(self.table(), idName: idName), params: [id.string]) 51 | } catch { 52 | LogFile.error("Error msg: \(error)", logFile: "./StORMlog.txt") 53 | self.error = StORMError.error("\(error)") 54 | throw error 55 | } 56 | return true 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Sources/MySQLStORM/Insert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Insert.swift 3 | // PostgresStORM 4 | // 5 | // Created by Jonathan Guthrie on 2016-09-24. 6 | // 7 | // 8 | 9 | import StORM 10 | import PerfectLogger 11 | 12 | /// Performs insert functions as an extension to the main class. 13 | extension MySQLStORM { 14 | 15 | /// Insert function where the suppled data is in [(String, Any)] format. 16 | @discardableResult 17 | public func insert(_ data: [(String, Any)]) throws -> Any { 18 | 19 | var keys = [String]() 20 | var vals = [String]() 21 | for i in data { 22 | keys.append(i.0) 23 | vals.append(String(describing: i.1)) 24 | } 25 | do { 26 | return try insert(cols: keys, params: vals) 27 | } catch { 28 | LogFile.error("Error msg: \(error)", logFile: "./StORMlog.txt") 29 | throw StORMError.error("\(error)") 30 | } 31 | } 32 | 33 | /// Insert function where the suppled data is in [String: Any] format. 34 | public func insert(_ data: [String: Any]) throws -> Any { 35 | 36 | var keys = [String]() 37 | var vals = [String]() 38 | for (key, value) in data { 39 | keys.append(key) 40 | vals.append(String(describing: value)) 41 | } 42 | 43 | do { 44 | return try insert(cols: keys, params: vals) 45 | } catch { 46 | LogFile.error("Error msg: \(error)", logFile: "./StORMlog.txt") 47 | throw StORMError.error("\(error)") 48 | } 49 | } 50 | 51 | 52 | 53 | /// Insert function where the suppled data is in matching arrays of columns and parameter values. 54 | public func insert(cols: [String], params: [Any]) throws -> Any { 55 | let (idname, _) = firstAsKey() 56 | do { 57 | return try insert(cols: cols, params: params, idcolumn: idname) 58 | } catch { 59 | LogFile.error("Error msg: \(error)", logFile: "./StORMlog.txt") 60 | throw StORMError.error("\(error)") 61 | } 62 | } 63 | 64 | /// Insert function where the suppled data is in matching arrays of columns and parameter values, as well as specifying the name of the id column. 65 | public func insert(cols: [String], params: [Any], idcolumn: String) throws -> Any { 66 | 67 | var paramString = [String]() 68 | var substString = [String]() 69 | for i in 0.. String 12 | } 13 | 14 | public enum MySQLDataType: StORMFieldTypeProtocol { 15 | case notDefined 16 | case bit(length: UInt8) 17 | case tinyInt(length: UInt8) 18 | case smallInt(length: UInt8) 19 | case mediumInt(length: UInt8) 20 | case int(length: UInt8) 21 | case integer(length: UInt8) 22 | case bigInt(length: UInt8) 23 | case decimal(length: UInt8, scale: UInt8) 24 | case dec(length: UInt8, scale: UInt8) 25 | case float(length: UInt8, scale: UInt8) 26 | case double(length: UInt8, scale: UInt8) 27 | case date 28 | case dateTime 29 | case timeStamp(fsp: UInt8) 30 | case time(fsp: UInt8) 31 | case year 32 | case char(length: UInt8) 33 | case varChar(length: UInt8) 34 | case binary(length: UInt8) 35 | case varBinary(length: UInt8) 36 | case tinyBlob 37 | case blob(length: UInt8) 38 | case text 39 | case mediumBlob 40 | case mediumText 41 | case longBlob 42 | case longText 43 | 44 | public func fieldType() -> String { 45 | switch self { 46 | case .notDefined: 47 | return "NOT DEFINED" 48 | case .bit(let lngt): 49 | return "BIT(\(lngt))" 50 | case .tinyInt(let lngt): 51 | return "TINYINT(\(lngt))" 52 | case .smallInt(let lngt): 53 | return "SMALLINT(\(lngt))" 54 | case .mediumInt(let lngt): 55 | return "MEDIUMINT(\(lngt))" 56 | case .int(let lngt): 57 | return "INT(\(lngt))" 58 | case .integer(let lngt): 59 | return "INTEGER(\(lngt))" 60 | case .bigInt(let lngt): 61 | return "BIGINT(\(lngt))" 62 | case .decimal(let lngt, let scale): 63 | return "DECIMAL(\(lngt),\(scale))" 64 | case .dec(let lngt, let scale): 65 | return "DEC(\(lngt),\(scale))" 66 | case .float(let lngt, let scale): 67 | return "FLOAT(\(lngt),\(scale))" 68 | case .double(let lngt, let scale): 69 | return "DOUBLE(\(lngt),\(scale))" 70 | case .date: 71 | return "DATE" 72 | case .dateTime: 73 | return "DATETIME" 74 | case .timeStamp(let fsp): 75 | return "TIMESTAMP(\(fsp))" 76 | case .time(let fsp): 77 | return "TIME(\(fsp))" 78 | case .year: 79 | return "YEAR" 80 | case .char(let lngt): 81 | return "CHAR(\(lngt))" 82 | case .varChar(let lngt): 83 | return "VARCHAR(\(lngt))" 84 | case .binary(let lngt): 85 | return "BINARY(\(lngt))" 86 | case .varBinary(let lngt): 87 | return "VARBINARY(\(lngt))" 88 | case .tinyBlob: 89 | return "TINYBLOB" 90 | case .blob(let lngt): 91 | return "BLOB(\(lngt))" 92 | case .text: 93 | return "TEXT" 94 | case .mediumBlob: 95 | return "MEDIUMBLOB" 96 | case .mediumText: 97 | return "MEDIUMTEXT" 98 | case .longBlob: 99 | return "LONGBLOB" 100 | case .longText: 101 | return "LONGTEXT" 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/MySQLStORM/MySQLStORM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MySQLStORM.swift 3 | // MySQLStORM 4 | // 5 | // Created by Jonathan Guthrie on 2016-10-03. 6 | // 7 | // 8 | 9 | import StORM 10 | import PerfectMySQL 11 | import PerfectLogger 12 | 13 | /// MySQLConnector sets the connection parameters for the PostgreSQL Server access 14 | /// Usage: 15 | /// MySQLConnector.host = "XXXXXX" 16 | /// MySQLConnector.username = "XXXXXX" 17 | /// MySQLConnector.password = "XXXXXX" 18 | /// MySQLConnector.port = 3306 19 | public struct MySQLConnector { 20 | 21 | public static var host: String = "" 22 | public static var username: String = "" 23 | public static var password: String = "" 24 | public static var database: String = "" 25 | public static var port: Int = 3306 26 | public static var charset: String = "utf8mb4" 27 | public static var method: StORMConnectionMethod = .network 28 | 29 | public static var quiet: Bool = false 30 | 31 | private init(){} 32 | } 33 | 34 | /// SuperClass that inherits from the foundation "StORM" class. 35 | /// Provides MySQLL-specific ORM functionality to child classes 36 | open class MySQLStORM: StORM, StORMProtocol { 37 | 38 | /// Holds the last statement executed 39 | public var lastStatement: MySQLStmt? 40 | 41 | /// Table that the child object relates to in the database. 42 | /// Defined as "open" as it is meant to be overridden by the child class. 43 | open func table() -> String { 44 | let m = Mirror(reflecting: self) 45 | return ("\(m.subjectType)").lowercased() 46 | } 47 | 48 | /// Public init 49 | override public init() { 50 | super.init() 51 | } 52 | 53 | /// This method can be overriden in MySQLStORM subclass to provide a different database configuration 54 | open func configuration() -> MySQLConnect { 55 | let conf = MySQLConnect(host: MySQLConnector.host, 56 | username: MySQLConnector.username, 57 | password: MySQLConnector.password, 58 | database: MySQLConnector.database, 59 | port: MySQLConnector.port, 60 | charset: MySQLConnector.charset, 61 | method: MySQLConnector.method) 62 | return conf 63 | } 64 | 65 | private func printDebug(_ statement: String, _ params: [String]) { 66 | if StORMdebug { LogFile.debug("StORM Debug: \(statement) : \(params.joined(separator: ", "))", logFile: "./StORMlog.txt") } 67 | } 68 | 69 | // Internal function which executes statements, with parameter binding 70 | // Returns raw result 71 | @discardableResult 72 | func exec(_ statement: String) throws -> MySQL.Results { 73 | 74 | let conf = self.configuration() 75 | let thisConnection = MySQLConnect( 76 | host: conf.credentials.host, 77 | username: conf.credentials.username, 78 | password: conf.credentials.password, 79 | database: conf.database, 80 | port: conf.credentials.port, 81 | charset: conf.charset, 82 | method: conf.credentials.method 83 | ) 84 | 85 | thisConnection.open() 86 | thisConnection.statement = statement 87 | printDebug(statement, []) 88 | 89 | let querySuccess = thisConnection.server.query(statement: statement) 90 | 91 | guard querySuccess else { 92 | throw StORMError.error(thisConnection.server.errorMessage()) 93 | } 94 | 95 | let result = thisConnection.server.storeResults()! 96 | thisConnection.server.close() 97 | return result 98 | } 99 | 100 | private func fieldNamesToStringArray(_ arr: [Int:String]) -> [String] { 101 | var out = [String]() 102 | for i in 0.. [StORMRow] { 156 | let conf = self.configuration() 157 | let thisConnection = MySQLConnect( 158 | host: conf.credentials.host, 159 | username: conf.credentials.username, 160 | password: conf.credentials.password, 161 | database: conf.database, 162 | port: conf.credentials.port, 163 | charset: conf.charset, 164 | method: conf.credentials.method 165 | ) 166 | 167 | thisConnection.open() 168 | thisConnection.statement = statement 169 | printDebug(statement, params) 170 | 171 | lastStatement = MySQLStmt(thisConnection.server) 172 | 173 | var res = lastStatement?.prepare(statement: statement) 174 | guard res! else { 175 | throw StORMError.error(thisConnection.server.errorMessage()) 176 | } 177 | 178 | for p in params { 179 | lastStatement?.bindParam(p) 180 | } 181 | 182 | res = lastStatement?.execute() 183 | 184 | for index in 0..Void) throws { 244 | do { 245 | if keyIsEmpty() { 246 | let setId = try insert(asData(1)) 247 | set(setId) 248 | } else { 249 | let (idname, idval) = firstAsKey() 250 | try update(data: asData(1), idName: idname, idValue: idval) 251 | } 252 | } catch { 253 | LogFile.error("Error msg: \(error)", logFile: "./StORMlog.txt") 254 | throw StORMError.error("\(error)") 255 | } 256 | } 257 | 258 | /// Unlike the save() methods, create() mandates the addition of a new document, regardless of whether an ID has been set or specified. 259 | override open func create() throws { 260 | do { 261 | try insert(asData()) 262 | } catch { 263 | LogFile.error("Error msg: \(error)", logFile: "./StORMlog.txt") 264 | throw StORMError.error("\(error)") 265 | } 266 | } 267 | 268 | /// Table Creation (alias for setup) 269 | open func setupTable(_ str: String = "") throws { 270 | try setup(str) 271 | } 272 | 273 | // 274 | open subscript(key: String) -> MySQLDataType { 275 | get { 276 | return .notDefined 277 | } 278 | } 279 | 280 | /// Table Creation 281 | /// Requires the connection to be configured, as well as a valid "table" property to have been set in the class 282 | /// - Parameter str: create statement 283 | /// - Parameter defaultTypes: by default it is true so MySQLStORM decides for the type conversion. To be able to provide your own, you need to override the `subscript(key: String) -> String` 284 | open func setup(_ str: String = "", _ useDefaults: Bool? = true) throws { 285 | LogFile.info("Running setup: \(table())", logFile: "./StORMlog.txt") 286 | var createStatement = str 287 | if str.count == 0 { 288 | var opt = [String]() 289 | var keyName = "" 290 | for child in Mirror(reflecting: self).children { 291 | guard let key = child.label else { 292 | continue 293 | } 294 | var verbage = "" 295 | if !key.hasPrefix("internal_") && !key.hasPrefix("_") { 296 | verbage = "`\(key)` " 297 | if let defaultTypes = useDefaults, defaultTypes == false { 298 | let field: MySQLDataType = self[key] 299 | verbage += field.fieldType() 300 | } else { 301 | if child.value is Int && opt.count == 0 { 302 | verbage += "int" 303 | } else if child.value is Int { 304 | verbage += "int" 305 | } else if child.value is Bool { 306 | verbage += "int" // MySQL has no bool type 307 | } else if child.value is Double { 308 | verbage += "float" 309 | } else if child.value is UInt || child.value is UInt8 || child.value is UInt16 || child.value is UInt32 || child.value is UInt64 { 310 | verbage += "blob" 311 | } else { 312 | verbage += "text" 313 | } 314 | } 315 | if opt.count == 0 { 316 | if child.value is Int { 317 | verbage = "`\(key)` int NOT NULL AUTO_INCREMENT" 318 | } else { 319 | verbage = "`\(key)` varchar(255) NOT NULL" 320 | } 321 | keyName = key 322 | } 323 | opt.append(verbage) 324 | } 325 | } 326 | let keyComponent = ", PRIMARY KEY (`\(keyName)`)" 327 | 328 | createStatement = "CREATE TABLE IF NOT EXISTS \(table()) (\(opt.joined(separator: ", "))\(keyComponent));" 329 | if StORMdebug { LogFile.info("createStatement: \(createStatement)", logFile: "./StORMlog.txt") } 330 | } 331 | do { 332 | try sql(createStatement, params: []) 333 | } catch { 334 | LogFile.error("Error msg: \(error)", logFile: "./StORMlog.txt") 335 | throw StORMError.error("\(error)") 336 | } 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /Sources/MySQLStORM/SQL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SQL.swift 3 | // PostgresStORM 4 | // 5 | // Created by Jonathan Guthrie on 2016-09-24. 6 | // 7 | // 8 | 9 | import StORM 10 | import PerfectMySQL 11 | import PerfectLogger 12 | 13 | /// An extension to the main class providing SQL statement functions 14 | extension MySQLStORM { 15 | 16 | /// Execute Raw SQL (with parameter binding) 17 | public func sql(_ statement: String, params: [String]) throws { 18 | do { 19 | try exec(statement, params: params) 20 | } catch { 21 | if !MySQLConnector.quiet { 22 | LogFile.error("Error msg: \(error)", logFile: "./StORMlog.txt") 23 | } 24 | self.error = StORMError.error("\(error)") 25 | throw error 26 | } 27 | } 28 | 29 | /// Execute Raw SQL (with parameter binding) 30 | /// Returns [StORMRow] (discardable) 31 | @discardableResult 32 | public func sqlRows(_ statement: String, params: [String]) throws -> [StORMRow] { 33 | do { 34 | return try execRows(statement, params: params) 35 | } catch { 36 | if !MySQLConnector.quiet { 37 | LogFile.error("Error msg: \(error)", logFile: "./StORMlog.txt") 38 | } 39 | self.error = StORMError.error("\(error)") 40 | throw error 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Sources/MySQLStORM/Select.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Select.swift 3 | // PostgresStORM 4 | // 5 | // Created by Jonathan Guthrie on 2016-09-24. 6 | // 7 | // 8 | 9 | import StORM 10 | import PerfectLogger 11 | 12 | /// Provides select functions as an extension to the main class. 13 | extension MySQLStORM { 14 | 15 | /// Retrieves all rows in the table, only limited by the cursor (9,999,999 rows) 16 | public func findAll() throws { 17 | do { 18 | let cursor = StORMCursor(limit: 9999999,offset: 0) 19 | try select( 20 | columns: [], 21 | whereclause: "1", 22 | params: [], 23 | orderby: [], 24 | cursor: cursor 25 | ) 26 | } catch { 27 | throw StORMError.error("\(error)") 28 | } 29 | } 30 | /// Select function with specific where clause. 31 | /// Parameterized statements are used, so all params should be passed in using the [Any] params array. 32 | /// The whereclause should be specified in the following format: "name = ? AND email LIKE ?" 33 | /// An orderby array can be specified in a String array like ["name DESC","email ASC"] 34 | /// A StORMCursor can be supplied, otherwise the default values are used. 35 | /// Note that the joins, having and groupBy functionality is unimplemented at this time. 36 | /// The select function will populate the object with the results of the query. 37 | public func select( 38 | whereclause: String, 39 | params: [Any], 40 | orderby: [String], 41 | cursor: StORMCursor = StORMCursor(), 42 | joins: [StORMDataSourceJoin] = [], 43 | having: [String] = [], 44 | groupBy: [String] = [] 45 | ) throws { 46 | do { 47 | try select(columns: [], whereclause: whereclause, params: params, orderby: orderby, cursor: cursor, joins: joins, having: having, groupBy: groupBy) 48 | } catch { 49 | throw StORMError.error("\(error)") 50 | } 51 | } 52 | 53 | /// Select function with specific where clause, and spefified columns to return. 54 | /// Parameterized statements are used, so all params should be passed in using the [Any] params array. 55 | /// The whereclause should be specified in the following format: "name = ? AND email LIKE ?" 56 | /// An orderby array can be specified in a String array like ["name DESC","email ASC"] 57 | /// A StORMCursor can be supplied, otherwise the default values are used. 58 | /// Note that the joins, having and groupBy functionality is unimplemented at this time. 59 | /// The select function will populate the object with the results of the query. 60 | public func select( 61 | columns: [String], 62 | whereclause: String, 63 | params: [Any], 64 | orderby: [String], 65 | cursor: StORMCursor = StORMCursor(), 66 | joins: [StORMDataSourceJoin] = [], 67 | having: [String] = [], 68 | groupBy: [String] = [] 69 | ) throws { 70 | 71 | let clauseCount = "COUNT(*) AS counter" 72 | var clauseSelectList = "*" 73 | var clauseWhere = "" 74 | var clauseOrder = "" 75 | 76 | if columns.count > 0 { 77 | clauseSelectList = columns.joined(separator: ",") 78 | } else { 79 | var keys = [String]() 80 | for i in cols() { 81 | keys.append(i.0) 82 | } 83 | clauseSelectList = keys.joined(separator: ",") 84 | } 85 | if whereclause.count > 0 { 86 | clauseWhere = " WHERE \(whereclause)" 87 | } 88 | 89 | var paramsString = [String]() 90 | for i in 0.. 0 { 94 | clauseOrder = " ORDER BY \(orderby.joined(separator: ", "))" 95 | } 96 | do { 97 | 98 | // JOIN 99 | var joinClause = "" 100 | for join in joins { 101 | switch join.direction { 102 | case .INNER: 103 | joinClause += " INNER JOIN \(join.table) ON \(join.onCondition) " 104 | default: () 105 | } 106 | } 107 | 108 | // SELECT ASSEMBLE 109 | var str = "SELECT \(clauseSelectList) FROM \(table()) \(joinClause) \(clauseWhere) \(clauseOrder)" 110 | 111 | // SELECT COUNT(*) ASSEMBLE 112 | let strCount = "SELECT \(clauseCount) FROM \(table()) \(joinClause) \(clauseWhere)" 113 | 114 | 115 | // TODO: having, groupby 116 | 117 | 118 | // LIMIT & OFFSET 119 | if cursor.limit > 0 { 120 | let limit = " LIMIT \(cursor.limit)" 121 | str += limit 122 | // strCount += limit 123 | } 124 | if cursor.offset > 0 { 125 | let offset = " OFFSET \(cursor.offset)" 126 | str += offset 127 | // strCount += offset 128 | } 129 | 130 | // get the number of records 131 | let getCount = try execRows(strCount, params: paramsString) 132 | let numrecords = getCount[0].data["counter"] ?? 0 133 | 134 | results.cursorData = StORMCursor( 135 | limit: cursor.limit, 136 | offset: cursor.offset, 137 | totalRecords: Int(numrecords as! Int64)) 138 | 139 | // save results into ResultSet 140 | results.rows = try execRows(str, params: paramsString) 141 | 142 | // if just one row returned, act like a "GET" 143 | if results.cursorData.totalRecords == 1 { makeRow() } 144 | 145 | //return results 146 | } catch { 147 | LogFile.error("Error msg: \(error)", logFile: "./StORMlog.txt") 148 | self.error = StORMError.error("\(error)") 149 | throw error 150 | } 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /Sources/MySQLStORM/Update.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Update.swift 3 | // PostgresStORM 4 | // 5 | // Created by Jonathan Guthrie on 2016-09-24. 6 | // 7 | // 8 | 9 | import StORM 10 | import PerfectLogger 11 | 12 | /// Extends the main class with update functions. 13 | extension MySQLStORM { 14 | 15 | /// Updates the row with the specified data. 16 | /// This is an alternative to the save() function. 17 | /// Specify matching arrays of columns and parameters, as well as the id name and value. 18 | @discardableResult 19 | public func update(cols: [String], params: [Any], idName: String, idValue: Any) throws -> Bool { 20 | 21 | var paramsString = [String]() 22 | var set = [String]() 23 | for i in 0.. Bool { 47 | 48 | var keys = [String]() 49 | var vals = [String]() 50 | for i in 0..= conflictkeys.count { 29 | upsertString.append("\(String(describing: cols[i])) = ?") 30 | 31 | } 32 | 33 | if !conflictkeys.contains(cols[i]) { 34 | upsertParamsString.append(String(describing: params[i])) 35 | } 36 | 37 | } 38 | 39 | paramsString.append(contentsOf: upsertParamsString) 40 | 41 | let str = "INSERT INTO \(self.table()) (\(cols.joined(separator: ","))) VALUES(\(substString.joined(separator: ","))) ON DUPLICATE KEY UPDATE \(upsertString.joined(separator: ","))" 42 | 43 | do { 44 | try exec(str, params: paramsString) 45 | } catch { 46 | LogFile.error("Error msg: \(error)", logFile: "./StORMlog.txt") 47 | self.error = StORMError.error("\(error)") 48 | throw error 49 | } 50 | 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Sources/MySQLStORM/parseRows.swift: -------------------------------------------------------------------------------- 1 | // 2 | // parseRows.swift 3 | // PostgresSTORM 4 | // 5 | // Created by Jonathan Guthrie on 2016-10-06. 6 | // 7 | // 8 | 9 | import StORM 10 | import PerfectMySQL 11 | import PerfectLib 12 | import Foundation 13 | 14 | /// Supplies the parseRows method extending the main class. 15 | extension MySQLStORM { 16 | 17 | /// parseRows takes the [String:Any] result and returns an array of StormRows 18 | public func parseRows(_ result: MySQLStmt.Results, resultSet: StORMResultSet) -> [StORMRow] { 19 | var resultRows = [StORMRow]() 20 | _ = result.forEachRow { row in 21 | var params = [String: Any]() 22 | 23 | for f in 0.. [String:Any] { 49 | var val = [String: Any]() 50 | 51 | do { 52 | val = try data.jsonDecode() as? [String:Any] ?? [String: Any]() 53 | } catch { 54 | print("invalid JSON") 55 | } 56 | 57 | return val 58 | } 59 | 60 | // decodes a TEXT field to JSON str array 61 | public func fromJSONtoStringArray(_ data: String) -> [String] { 62 | var val = [String]() 63 | 64 | do { 65 | val = try data.jsonDecode() as? [String] ?? [String]() 66 | } catch { 67 | print("invalid JSON") 68 | } 69 | 70 | return val 71 | } 72 | 73 | } 74 | 75 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import MySQLStORMTests 3 | 4 | XCTMain([ 5 | testCase(MySQLStORMTests.allTests), 6 | ]) 7 | -------------------------------------------------------------------------------- /Tests/MySQLStORMTests/MySQLStORMTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import PerfectLib 3 | import Foundation 4 | import StORM 5 | import MySQLStORM 6 | @testable import MySQLStORM 7 | 8 | 9 | class User: MySQLStORM { 10 | // NOTE: First param in class should be the ID. 11 | var id : Int = 0 12 | var firstname : String = "" 13 | var lastname : String = "" 14 | var email : String = "" 15 | 16 | 17 | override open func table() -> String { 18 | return "usertests" 19 | } 20 | 21 | override func to(_ this: StORMRow) { 22 | id = Int(this.data["id"] as! Int32) 23 | if let f = this.data["firstname"] { 24 | firstname = f as! String 25 | } 26 | if let f = this.data["lastname"] { 27 | lastname = f as! String 28 | } 29 | if let f = this.data["email"] { 30 | email = f as! String 31 | } 32 | } 33 | 34 | func rows() -> [User] { 35 | var rows = [User]() 36 | for i in 0.. 0, "Object not saved (new)") 104 | } 105 | 106 | /* ============================================================================================= 107 | SELECT via SQL 108 | ============================================================================================= */ 109 | func testSQLSelect() { 110 | // StORMdebug = true 111 | let n = User() 112 | n.firstname = "X" 113 | n.lastname = "Y" 114 | 115 | do { 116 | try n.save {id in n.id = id as! Int } 117 | } catch { 118 | XCTFail(String(describing: error)) 119 | } 120 | 121 | 122 | let obj = User() 123 | 124 | do { 125 | try obj.sql("SELECT firstname FROM \(obj.table()) WHERE id = ?", params: ["\(n.id)"]) 126 | } catch { 127 | XCTFail(String(describing: error)) 128 | } 129 | 130 | } 131 | 132 | 133 | /* ============================================================================================= 134 | Save - Update 135 | ============================================================================================= */ 136 | func testSaveUpdate() { 137 | let obj = User() 138 | obj.firstname = "X" 139 | obj.lastname = "Y" 140 | 141 | do { 142 | try obj.save {id in obj.id = id as! Int } 143 | } catch { 144 | XCTFail(String(describing: error)) 145 | } 146 | 147 | obj.firstname = "A" 148 | obj.lastname = "B" 149 | do { 150 | try obj.save() 151 | } catch { 152 | XCTFail(String(describing: error)) 153 | } 154 | print("testSaveUpdate 1: \(obj.errorMsg)") 155 | XCTAssert(obj.id > 0, "Object not saved (update)") 156 | } 157 | 158 | /* ============================================================================================= 159 | Save - Create 160 | ============================================================================================= */ 161 | func testSaveCreate() { 162 | let obj = User() 163 | 164 | do { 165 | obj.id = 10001 166 | obj.firstname = "Mister" 167 | obj.lastname = "PotatoHead" 168 | obj.email = "potato@example.com" 169 | try obj.create() 170 | } catch { 171 | print("testSaveCreate 1: \(obj.errorMsg)") 172 | XCTFail(String(describing: error)) 173 | } 174 | XCTAssert(obj.id == 10001, "Object not saved (create)") 175 | 176 | // cleanup 177 | let objCleanup = User() 178 | objCleanup.id = 10001 179 | do { 180 | try objCleanup.delete() 181 | } catch { 182 | print("testSaveCreate: \(error)") 183 | XCTFail(String(describing: error)) 184 | } 185 | 186 | } 187 | 188 | /* ============================================================================================= 189 | Get (with id) 190 | ============================================================================================= */ 191 | func testGetByPassingID() { 192 | let obj = User() 193 | //obj.connection = connect // Use if object was instantiated without connection 194 | obj.firstname = "X testGetByPassingID" 195 | obj.lastname = "Y testGetByPassingID" 196 | 197 | do { 198 | try obj.save {id in obj.id = id as! Int } 199 | } catch { 200 | XCTFail(String(describing: error)) 201 | } 202 | 203 | let obj2 = User() 204 | 205 | do { 206 | try obj2.get(obj.id) 207 | } catch { 208 | XCTFail(String(describing: error)) 209 | } 210 | XCTAssert(obj.id == obj2.id, "Object not the same (id)") 211 | XCTAssert(obj.firstname == obj2.firstname, "Object not the same (firstname)") 212 | XCTAssert(obj.lastname == obj2.lastname, "Object not the same (lastname)") 213 | } 214 | 215 | 216 | /* ============================================================================================= 217 | Get (by id set) 218 | ============================================================================================= */ 219 | func testGetByID() { 220 | let obj = User() 221 | obj.firstname = "testGetByID" 222 | obj.lastname = "testGetByID" 223 | 224 | do { 225 | try obj.save {id in obj.id = id as! Int } 226 | } catch { 227 | XCTFail(String(describing: error)) 228 | } 229 | 230 | let obj2 = User() 231 | obj2.id = obj.id 232 | 233 | do { 234 | try obj2.get() 235 | } catch { 236 | XCTFail(String(describing: error)) 237 | } 238 | XCTAssert(obj.id == obj2.id, "Object not the same (id)") 239 | XCTAssert(obj.firstname == obj2.firstname, "Object not the same (firstname)") 240 | XCTAssert(obj.lastname == obj2.lastname, "Object not the same (lastname)") 241 | } 242 | 243 | /* ============================================================================================= 244 | Get (with id) - integer too large 245 | ============================================================================================= */ 246 | func testGetByPassingIDtooLarge() { 247 | let obj = User() 248 | 249 | do { 250 | try obj.get(874682634789) 251 | XCTAssert(obj.results.cursorData.totalRecords == 0, "Object should have found no rows") 252 | } catch { 253 | XCTFail(error as! String) 254 | } 255 | } 256 | 257 | /* ============================================================================================= 258 | Get (with id) - no record 259 | // test get where id does not exist (id) 260 | ============================================================================================= */ 261 | func testGetByPassingIDnoRecord() { 262 | let obj = User() 263 | 264 | do { 265 | try obj.get(1111111) 266 | XCTAssert(obj.results.cursorData.totalRecords == 0, "Object should have found no rows") 267 | } catch { 268 | XCTFail(error as! String) 269 | } 270 | } 271 | 272 | 273 | 274 | 275 | // test get where id does not exist () 276 | /* ============================================================================================= 277 | Get (preset id) - no record 278 | // test get where id does not exist (id) 279 | ============================================================================================= */ 280 | func testGetBySettingIDnoRecord() { 281 | let obj = User() 282 | obj.id = 1111111 283 | do { 284 | try obj.get() 285 | XCTAssert(obj.results.cursorData.totalRecords == 0, "Object should have found no rows") 286 | } catch { 287 | XCTFail(error as! String) 288 | } 289 | } 290 | 291 | 292 | /* ============================================================================================= 293 | Returning DELETE statement to verify correct form 294 | // deleteSQL 295 | ============================================================================================= */ 296 | func testCheckDeleteSQL() { 297 | let obj = User() 298 | XCTAssert(obj.deleteSQL("test", idName: "testid") == "DELETE FROM test WHERE testid = ?", "DeleteSQL statement is not correct") 299 | 300 | } 301 | 302 | 303 | 304 | // delete(id: Int, idName: String = "id") 305 | // delete() 306 | 307 | 308 | /* ============================================================================================= 309 | Find 310 | ============================================================================================= */ 311 | func testFind() { 312 | let obj = User() 313 | 314 | do { 315 | try obj.find([("firstname", "Joe")]) 316 | //print("Find Record: \(obj.id), \(obj.firstname), \(obj.lastname), \(obj.email)") 317 | } catch { 318 | XCTFail("Find error: \(obj.error.string())") 319 | } 320 | } 321 | 322 | 323 | /* ============================================================================================= 324 | Count test 325 | Thanks @dindarm for finding the count bug! 326 | ============================================================================================= */ 327 | func testCount() { 328 | 329 | let obj1 = User() 330 | obj1.firstname = "COUNTTEST" 331 | obj1.lastname = "Y" 332 | 333 | let obj2 = User() 334 | obj2.firstname = "COUNTTEST" 335 | obj2.lastname = "Z" 336 | 337 | let objCount = User() 338 | do { 339 | try obj1.save() 340 | try obj2.save() 341 | try objCount.find(["firstname":"COUNTTEST"]) 342 | 343 | try obj1.delete() 344 | try obj2.delete() 345 | } catch { 346 | XCTFail(String(describing: error)) 347 | } 348 | 349 | 350 | print(objCount.results.cursorData.totalRecords) 351 | XCTAssert(objCount.results.cursorData.totalRecords == 2, "THERE SHOULD BE TWO ROWS") 352 | } 353 | 354 | 355 | /* ============================================================================================= 356 | SELECT via SQL 357 | ============================================================================================= */ 358 | func testUTF8() { 359 | // Thanks to @chip 360 | let n = User() 361 | n.firstname = "Jägermeister" 362 | n.lastname = "Jägermeister" 363 | 364 | do { 365 | try n.save {id in n.id = id as! Int } 366 | } catch { 367 | XCTFail(String(describing: error)) 368 | } 369 | 370 | 371 | let obj = User() 372 | 373 | do { 374 | try obj.get(n.id) 375 | XCTAssert(obj.firstname == "Jägermeister", "UTF8 assertion failed (Jägermeister), \(obj.firstname)") 376 | } catch { 377 | XCTFail(String(describing: error)) 378 | } 379 | 380 | } 381 | 382 | static var allTests : [(String, (MySQLStORMTests) -> () throws -> Void)] { 383 | return [ 384 | ("testSaveNew", testSaveNew), 385 | ("testSaveUpdate", testSaveUpdate), 386 | ("testSaveCreate", testSaveCreate), 387 | ("testGetByPassingID", testGetByPassingID), 388 | ("testGetByID", testGetByID), 389 | ("testGetByPassingIDtooLarge", testGetByPassingIDtooLarge), 390 | ("testGetByPassingIDnoRecord", testGetByPassingIDnoRecord), 391 | ("testGetBySettingIDnoRecord", testGetBySettingIDnoRecord), 392 | ("testCheckDeleteSQL", testCheckDeleteSQL), 393 | ("testFind", testFind), 394 | ("testUTF8", testUTF8) 395 | ] 396 | } 397 | 398 | } 399 | -------------------------------------------------------------------------------- /Tests/MySQLStORMTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCTestManifests.swift 3 | // PostgresSTORM 4 | // 5 | // Created by Jonathan Guthrie on 2016-10-06. 6 | // 7 | // 8 | 9 | import XCTest 10 | 11 | #if !os(OSX) 12 | public func allTests() -> [XCTestCaseEntry] { 13 | return [ 14 | testCase(MySQLStORMTests.allTests) 15 | ] 16 | } 17 | #endif 18 | --------------------------------------------------------------------------------