Repository: novi/mysql-swift Branch: master Commit: 07574370c94c Files: 42 Total size: 133.4 KB Directory structure: gitextract_zidf83u4/ ├── .circleci/ │ └── config.yml ├── .gitignore ├── LICENSE.md ├── Package.swift ├── Package@swift-5.swift ├── README.md ├── Sources/ │ ├── MySQL/ │ │ ├── AutoincrementID.swift │ │ ├── Blob.swift │ │ ├── Connection.swift │ │ ├── ConnectionPool.swift │ │ ├── Date.swift │ │ ├── Error.swift │ │ ├── IDType.swift │ │ ├── Query.swift │ │ ├── QueryParameter-Data.swift │ │ ├── QueryParameterType.swift │ │ ├── RawRepresentableParameter.swift │ │ ├── Result-SQLRawStringDecodable.swift │ │ ├── Result.swift │ │ ├── Sync.swift │ │ └── Transaction.swift │ ├── SQLFormatter/ │ │ ├── Error.swift │ │ └── QueryFormatter.swift │ └── cmysql/ │ ├── macos.pc │ ├── module.modulemap │ └── shim.h └── Tests/ ├── LinuxMain.swift ├── MySQLTests/ │ ├── BlobTests.swift │ ├── ConnectionPoolTests.swift │ ├── ConnectionTests.swift │ ├── DateTests.swift │ ├── EscapeTests.swift │ ├── Model.swift │ ├── MySQLConnection.swift │ ├── QueryDecimalTypeTests.swift │ ├── QueryFormatterTests.swift │ ├── QueryParameterTests.swift │ ├── QueryTests.swift │ ├── QueryURLTypeTests.swift │ └── XCTestManifests.swift └── SQLFormatterTests/ ├── SQLFormatterTests.swift └── XCTestManifests.swift ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/config.yml ================================================ version: 2 jobs: mac-swift5.3: macos: xcode: "12.3.0" steps: - checkout - run: brew tap novi/tap && brew install novi/tap/cmysqlmariadb - run: mkdir -p /usr/local/etc/my.cnf.d /usr/local/var/mysql #workaround - run: sh -c "mysqld_safe --skip-grant-tables & sleep 5" - run: mysql -u root -e "create database IF NOT EXISTS test;" - run: swift build - run: swift test mac-swift5.3-mysql: macos: xcode: "12.3.0" steps: - checkout - run: brew tap novi/tap && brew install novi/tap/cmysql - run: mkdir -p /usr/local/var/mysql/data && echo "datadir=/usr/local/var/mysql/data" >> /usr/local/etc/my.cnf && mysqld --initialize-insecure - run: mysql.server start - run: mysql -u root -e "create database IF NOT EXISTS test;" - run: swift build - run: swift test linux-swift5.0: docker: - image: yusukeito/swift-basic:swift5.0 - image: mariadb:10.3 environment: MYSQL_USER: root MYSQL_DATABASE: "test" MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_ROOT_HOST: "%" DATABASE_HOST: "%" steps: - checkout - run: sleep 10 #wait for mysql - run: swift build - run: swift test linux-swift5.1: docker: - image: yusukeito/swift-basic:swift5.1-cgrpc - image: mariadb:10.3 environment: MYSQL_USER: root MYSQL_DATABASE: "test" MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_ROOT_HOST: "%" DATABASE_HOST: "%" steps: - checkout - run: sleep 10 #wait for mysql - run: swift build - run: swift test linux-swift5.2: docker: - image: yusukeito/swift-basic:swift5.2-grpc-1 - image: mariadb:10.3 environment: MYSQL_USER: root MYSQL_DATABASE: "test" MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_ROOT_HOST: "%" DATABASE_HOST: "%" steps: - checkout - run: sleep 10 #wait for mysql - run: swift build - run: swift test linux-swift5.3: docker: - image: yusukeito/swift-basic:swift5.3-grpc-1 - image: mariadb:10.3 environment: MYSQL_USER: root MYSQL_DATABASE: "test" MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_ROOT_HOST: "%" DATABASE_HOST: "%" steps: - checkout - run: sleep 10 #wait for mysql - run: swift build - run: swift test workflows: version: 2 build_and_test: jobs: - mac-swift5.3 - mac-swift5.3-mysql - linux-swift5.0 - linux-swift5.1 - linux-swift5.2 - linux-swift5.3 ================================================ FILE: .gitignore ================================================ # OS X Finder .DS_Store # Xcode per-user config *.mode1 *.mode1v3 *.mode2v3 *.perspective *.perspectivev3 *.pbxuser #*.xcworkspace xcuserdata # Build products build/ *.o *.LinkFileList *.hmap # Automatic backup files *~.nib/ *.swp *~ *.dat *.dep docs/ Cartfile.resolved Carthage/ .build/ Constants.swift /*.xcodeproj .swiftpm/ ================================================ FILE: LICENSE.md ================================================ Copyright (c) 2015-18 Yusuke Ito Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Package.swift ================================================ // swift-tools-version:4.0 import PackageDescription let package = Package( name: "MySQL", products: [ .library(name: "MySQL", targets: ["MySQL"]) ], dependencies: [ .package(url: "https://github.com/novi/cmysql.git", from: "2.0.0"), ], targets: [ .target( name: "SQLFormatter" ), .target( name: "MySQL", dependencies: [ "SQLFormatter", ] ), .testTarget( name: "MySQLTests", dependencies: [ "MySQL" ] ), .testTarget( name: "SQLFormatterTests", dependencies: [ "MySQL" ] ) ], swiftLanguageVersions: [4] ) ================================================ FILE: Package@swift-5.swift ================================================ // swift-tools-version:5.0 import PackageDescription let package = Package( name: "mysql-swift", products: [ .library(name: "MySQL", targets: ["MySQL"]) ], targets: [ .systemLibrary( name: "CMySQL", path: "Sources/cmysql", pkgConfig: "cmysql", providers: [ .brew(["cmysql"]), .apt(["libmysqlclient-dev"]) ] ), .target( name: "SQLFormatter" ), .target( name: "MySQL", dependencies: [ "CMySQL", "SQLFormatter", ] ), .testTarget( name: "MySQLTests", dependencies: [ "MySQL" ] ), .testTarget( name: "SQLFormatterTests", dependencies: [ "MySQL" ] ) ] ) ================================================ FILE: README.md ================================================ mysql-swift =========== **This library is obsolete and not maintained. Use [MySQLNIO](https://github.com/vapor/mysql-nio) instead.** ![Platform Linux, macOS](https://img.shields.io/badge/Platforms-Linux%2C%20macOS-lightgray.svg) [![CircleCI](https://circleci.com/gh/novi/mysql-swift.svg?style=svg)](https://circleci.com/gh/novi/mysql-swift) MySQL client library for Swift. This is inspired by Node.js' [mysql](https://github.com/mysqljs/mysql). * Based on libmysqlclient * Raw SQL query * Simple query formatting and escaping (same as Node's) * Mapping queried results to `Codable` structs or classes _Note:_ No asynchronous I/O support currently. It depends libmysqlclient. ```swift // Declare a model struct User: Codable, QueryParameter { let id: Int let userName: String let age: Int? let status: Status let createdAt: Date enum Status: String, Codable { case created = "created" case verified = "verified" } private enum CodingKeys: String, CodingKey { case id case userName = "user_name" case age case status = "status" case createdAt = "created_at" } } // Selecting let nameParam = "some one" let ids: [QueryParameter] = [1, 2, 3, 4, 5, 6] let optionalInt: Int? = nil let rows: [User] = try conn.query("SELECT id,user_name,status,status,created_at FROM `user` WHERE (age > ? OR age is ?) OR name = ? OR id IN (?)", [50, optionalInt, nameParam, QueryArray(ids)] ]) // Inserting let age: Int? = 26 let user = User(id: 0, userName: "novi", age: age, status: .created, createdAt: Date()) let status = try conn.query("INSERT INTO `user` SET ?", [user]) as QueryStatus let newId = status.insertedId // Updating let tableName = "user" let defaultAge = 30 try conn.query("UPDATE ?? SET age = ? WHERE age is NULL;", [tableName, defaultAge]) ``` # Requirements * Swift 5.0 or later * MariaDB or MySQL Connector/C (libmysqlclient) 2.2.3 or later ## macOS Install pkg-config `.pc` file in [cmysql](https://github.com/vapor-community/cmysql) or [cmysql-mariadb](https://github.com/novi/cmysql-mariadb/tree/mariadb). ```sh # cmysql $ brew tap novi/tap $ brew install novi/tap/cmysql # cmysql-mariadb $ brew tap novi/tap $ brew install novi/tap/cmysqlmariadb ``` ## Ubuntu * Install `libmariadbclient` * Follow [Setting up MariaDB Repositories](https://downloads.mariadb.org/mariadb/repositories/#mirror=yamagata-university) and set up your repository. ```sh $ sudo apt-get install libmariadbclient-dev ``` # Installation ## Swift Package Manager * Add `mysql-swift` to `Package.swift` of your project. ```swift // swift-tools-version:5.2 import PackageDescription let package = Package( ..., dependencies: [ .package(url: "https://github.com/novi/mysql-swift.git", .upToNextMajor(from: "0.9.0")) ], targets: [ .target( name: "YourAppOrLibrary", dependencies: [ // add a dependency .product(name: "MySQL", package: "mysql-swift") ] ) ] ) ``` # Usage ## Connection & Querying 1. Create a pool with options (hostname, port, password,...). 2. Use `ConnectionPool.execute()`. It automatically get and release a connection. ```swift let option = Option(host: "your.mysql.host"...) // Define and create your option type let pool = ConnectionPool(option: option) // Create a pool with the option let rows: [User] = try pool.execute { conn in // The connection `conn` is held in this block try conn.query("SELECT * FROM users;") // And it returns result to outside execute block } ``` ## Transaction ```swift let wholeStaus: QueryStatus = try pool.transaction { conn in let status = try conn.query("INSERT INTO users SET ?;", [user]) as QueryStatus // Create a user let userId = status.insertedId // the user's id try conn.query("UPDATE info SET some_value = ? WHERE some_key = 'latest_user_id' ", [userId]) // Store user's id that we have created the above } wholeStaus.affectedRows == 1 // true ``` # License MIT ================================================ FILE: Sources/MySQL/AutoincrementID.swift ================================================ // // AutoincrementID.swift // MySQL // // Created by Yusuke Ito on 6/27/16. // Copyright © 2016 Yusuke Ito. All rights reserved. // import SQLFormatter public enum AutoincrementID { case noID case ID(I) public var id: I { switch self { case .noID: fatalError("\(self) has no ID") case .ID(let id): return id } } } extension AutoincrementID: Equatable { static public func ==(lhs: AutoincrementID, rhs: AutoincrementID) -> Bool { switch (lhs, rhs) { case (.noID, .noID): return true case (.ID(let lhs), .ID(let rhs) ): return lhs.id == rhs.id default: return false } } } extension AutoincrementID: CustomStringConvertible { // where I: CustomStringConvertible // is not supported yet public var description: String { switch self { case .noID: return "noID" case .ID(let id): if let idVal = id as? CustomStringConvertible { return idVal.description } else { return "\(id)" } } } } extension AutoincrementID { public init(_ id: I) { self = .ID(id) } } /* extension AutoincrementID where I: SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> AutoincrementID { return AutoincrementID(try I.fromSQLValue(string: string)) } }*/ extension AutoincrementID: QueryParameter { public func queryParameter(option: QueryParameterOption) throws -> QueryParameterType { switch self { case .noID: return "" case .ID(let id): return try id.queryParameter(option: option) } } public var omitOnQueryParameter: Bool { switch self { case .noID: return true case .ID: return false } } } /// MARK: Codable support extension AutoincrementID: Decodable where I: Decodable { public init(from decoder: Decoder) throws { self = .ID(try I(from: decoder)) } } extension AutoincrementID: Encodable where I: Encodable { public func encode(to encoder: Encoder) throws { switch self { case .noID: break // nothing to encode case .ID(let id): try id.encode(to: encoder) } } } ================================================ FILE: Sources/MySQL/Blob.swift ================================================ // // Blob.swift // MySQL // // Created by Yusuke Ito on 4/22/16. // Copyright © 2016 Yusuke Ito. All rights reserved. // import Foundation import SQLFormatter extension Data: SQLRawStringDecodable { public static func fromSQLValue(string: String) throws -> Data { fatalError("logic error, construct via init(:)") } } fileprivate let HexTable: [Character] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"] extension Data: QueryParameterType { public func escaped() -> String { var buffer = [Character](["x", "'"]) buffer.reserveCapacity( self.count * 2 + 3 ) // 3 stands for "x''", 2 stands for 2 characters per a byte data for byte in self { buffer.append(HexTable[Int((byte >> 4) & 0x0f)]) buffer.append(HexTable[Int(byte & 0x0f)]) } buffer.append("'") return String(buffer) } public func escapedForID() -> String? { return nil // Data can not be used for ID(?? placeholder). } } extension Data: QueryParameter { public func queryParameter(option: QueryParameterOption) throws -> QueryParameterType { return self } } internal struct Blob: QueryParameter { let data: Data let dataType: QueryCustomDataParameterDataType public func queryParameter(option: QueryParameterOption) throws -> QueryParameterType { return self } } extension Blob: QueryParameterType { public func escaped() -> String { switch dataType { case .blob: return data.escaped() case .json: return "CONVERT(" + data.escaped() + " using utf8mb4)" } } public func escapedForID() -> String? { return nil // Data can not be used for ID(?? placeholder). } } ================================================ FILE: Sources/MySQL/Connection.swift ================================================ // // Database.swift // MySQL // // Created by ito on 2015/10/24. // Copyright © 2015年 Yusuke Ito. All rights reserved. // import CMySQL import CoreFoundation import Foundation internal struct MySQLUtil { internal static func getMySQLError(_ mysql: UnsafeMutablePointer) -> String { guard let strPtr = mysql_error(mysql) else { return "unknown error. could not get error with `mysql_error()`." } guard let errorString = String(validatingUTF8: strPtr) else { return "unknown error. could not get error as string." } return errorString } } public protocol ConnectionOption { var host: String { get } var port: Int { get } var user: String { get } var password: String { get } var database: String { get } var timeZone: TimeZone { get } var encoding: Connection.Encoding { get } var timeout: Int { get } var reconnect: Bool { get } var omitDetailsOnError: Bool { get } } fileprivate let defaultTimeZone = TimeZone(identifier: "UTC")! public extension ConnectionOption { // Provide default options var timeZone: TimeZone { return defaultTimeZone } var encoding: Connection.Encoding { return .UTF8MB4 } var timeout: Int { return 10 } var reconnect: Bool { return true } var omitDetailsOnError: Bool { return true } } extension Connection { public enum Encoding: String { case UTF8 = "utf8" case UTF8MB4 = "utf8mb4" } } public enum ConnectionError: Error { case connectionError(String) case connectionPoolGetConnectionTimeoutError } public final class Connection { internal var isInUse: Bool = false private var mysql_: UnsafeMutablePointer? internal let pool: ConnectionPool public let option: ConnectionOption internal init(option: ConnectionOption, pool: ConnectionPool) { self.option = option self.pool = pool self.mysql_ = nil } internal func release() { pool.releaseConnection(self) } internal static func setReconnect(_ reconnect: Bool, mysql: UnsafeMutablePointer) { let reconnectPtr = UnsafeMutablePointer.allocate(capacity: 1) reconnectPtr.pointee = reconnect == false ? 0 : 1 mysql_options(mysql, MYSQL_OPT_RECONNECT, reconnectPtr) reconnectPtr.deallocate() } func setReconnect(_ reconnect: Bool) { if let mysql = mysql { Connection.setReconnect(reconnect, mysql: mysql) } } internal func connect() throws -> UnsafeMutablePointer { dispose() guard let mysql = mysql_init(nil) else { fatalError("mysql_init() failed.") } do { let timeoutPtr = UnsafeMutablePointer.allocate(capacity: 1) timeoutPtr.pointee = option.timeout mysql_options(mysql, MYSQL_OPT_CONNECT_TIMEOUT, timeoutPtr) timeoutPtr.deallocate() } Connection.setReconnect(option.reconnect, mysql: mysql) if mysql_real_connect(mysql, option.host, option.user, option.password, option.database, UInt32(option.port), nil, 0) == nil { // error throw ConnectionError.connectionError(MySQLUtil.getMySQLError(mysql)) } mysql_set_character_set(mysql, option.encoding.rawValue) self.mysql_ = mysql return mysql } internal func connectIfNeeded() throws -> UnsafeMutablePointer { guard let mysql = self.mysql_ else { return try connect() } return mysql } private var mysql: UnsafeMutablePointer? { guard mysql_ != nil else { return nil } return mysql_ } internal func ping() -> Bool { _ = try? connectIfNeeded() guard let mysql = mysql else { return false } return mysql_ping(mysql) == 0 } private func dispose() { guard let mysql = mysql else { return } mysql_close(mysql) self.mysql_ = nil } deinit { dispose() } } ================================================ FILE: Sources/MySQL/ConnectionPool.swift ================================================ // // ConnectionPool.swift // MySQL // // Created by ito on 12/24/15. // Copyright © 2015 Yusuke Ito. All rights reserved. // import Dispatch #if os(Linux) import Glibc #endif import CMySQL fileprivate var LibraryInitialized: Atomic = Atomic(false) fileprivate func InitializeMySQLLibrary() { LibraryInitialized.syncWriting { guard $0 == false else { return } if mysql_server_init(0, nil, nil) != 0 { // mysql_library_init fatalError("could not initialize MySQL library with `mysql_server_init`.") } $0 = true } } extension Array where Element == Connection { mutating func preparedNewConnection(option: ConnectionOption, pool: ConnectionPool) -> Connection { let newConn = Connection(option: option, pool: pool) _ = try? newConn.connect() append(newConn) return newConn } func getUsableConnection() -> Connection? { for c in self { if c.isInUse == false && c.ping() { c.isInUse = true return c } } return nil } internal var inUseConnections: Int { var count: Int = 0 for c in self { if c.isInUse { count += 1 } } return count } } final public class ConnectionPool: CustomStringConvertible { private var initialConnections_: Atomic = Atomic(1) public var initialConnections: Int { get { return initialConnections_.sync { $0 } } set { initialConnections_.syncWriting { $0 = newValue } pool.syncWriting { while $0.count < newValue { _ = $0.preparedNewConnection(option: self.option, pool: self) } } } } public var maxConnections: Int { get { return maxConnections_.sync { $0 } } set { maxConnections_.syncWriting { $0 = newValue } } } private var maxConnections_: Atomic = Atomic(10) internal private(set) var pool: Atomic<[Connection]> = Atomic([]) @available(*, deprecated, renamed: "option") public var options: ConnectionOption { return option } public let option: ConnectionOption @available(*, deprecated, renamed: "init(option:)") public convenience init(options: ConnectionOption) { self.init(option: options) } public init(option: ConnectionOption) { self.option = option InitializeMySQLLibrary() for _ in 0.. = Atomic(60) internal func getConnection() throws -> Connection { var connection: Connection? = pool.syncWriting { if let conn = $0.getUsableConnection() { return conn } if $0.count < maxConnections { let conn = $0.preparedNewConnection(option: option, pool: self) conn.isInUse = true return conn } return nil } if let conn = connection { return conn } let tickInMs = 50 // ms var timeoutCount = (timeoutForGetConnection*1000)/tickInMs while timeoutCount > 0 { usleep(useconds_t(1000*tickInMs)) connection = pool.sync { $0.getUsableConnection() } if connection != nil { break } timeoutCount -= 1 } guard let conn = connection else { throw ConnectionError.connectionPoolGetConnectionTimeoutError } return conn } internal func releaseConnection(_ conn: Connection) { pool.sync { _ in conn.isInUse = false } } public var description: String { let inUseConnections = pool.sync { $0.inUseConnections } return "connections:\n\tinitial:\(initialConnections), max:\(maxConnections), in-use:\(inUseConnections)" } } extension ConnectionPool { public func execute( _ block: (_ conn: Connection) throws -> T ) throws -> T { let conn = try getConnection() defer { releaseConnection(conn) } return try block(conn) } } ================================================ FILE: Sources/MySQL/Date.swift ================================================ // // Date.swift // MySQL // // Created by ito on 12/16/15. // Copyright © 2015 Yusuke Ito. All rights reserved. // import Foundation import SQLFormatter internal final class SQLDateCalendar { private static var calendars: Atomic<[TimeZone:Calendar]> = Atomic([:]) internal static func calendar(forTimezone timeZone: TimeZone, _ block: (_ calendar: Calendar) -> T) -> T { return calendars.syncWriting { if let calendar = $0[timeZone] { return block(calendar) } var calendar = Calendar(identifier: .gregorian) calendar.timeZone = timeZone $0[timeZone] = calendar return block(calendar) } } } fileprivate func pad(num: Int, digits: Int = 2) -> String { var str = String(num) if num < 0 { return str } while str.count < digits { str = "0" + str } return str } extension Date { internal init(sqlDate: String, timeZone: TimeZone) throws { switch sqlDate.count { case 19: let chars: [Character] = Array(sqlDate) if let year = Int(String(chars[0...3])), let month = Int(String(chars[5...6])), let day = Int(String(chars[8...9])), let hour = Int(String(chars[11...12])), let minute = Int(String(chars[14...15])), let second = Int(String(chars[17...18])), year > 0 && day > 0 && month > 0 { var comps = DateComponents() comps.year = year comps.month = month comps.day = day comps.hour = hour comps.minute = minute comps.second = second let parsedDate: Date? = SQLDateCalendar.calendar(forTimezone: timeZone) { calendar in calendar.date(from :comps) } if let date = parsedDate { self = date return } } default: break } throw QueryError.SQLDateStringError(sqlDate) } } extension Date: QueryParameter { public func queryParameter(option: QueryParameterOption) -> QueryParameterType { let comp: DateComponents = SQLDateCalendar.calendar(forTimezone: option.timeZone) { calendar in calendar.dateComponents([ .year, .month, .day, .hour, .minute, .second], from: self) } // YYYY-MM-DD HH:MM:SS return EscapedQueryParameter( "'\(pad(num: comp.year ?? 0, digits: 4))-\(pad(num: comp.month ?? 0))-\(pad(num: comp.day ?? 0)) \(pad(num: comp.hour ?? 0)):\(pad(num: comp.minute ?? 0)):\(pad(num: comp.second ?? 0))'" ) } } fileprivate func nanosecondsToString(_ nanosec: Int) -> String { let nanosecSecond = Double(nanosec % 1_000_000_000)/1_000_000_000.0 var nanosecStr = String(format: "%.6f", nanosecSecond) nanosecStr.removeFirst() return String(nanosecStr) } extension DateComponents: QueryParameter { public func queryParameter(option: QueryParameterOption) throws -> QueryParameterType { if let year = self.year, let month = self.month, let day = self.day, let hour = self.hour, let minute = self.minute, let second = self.second { var string = "'\(pad(num: year, digits: 4))-\(pad(num: month))-\(pad(num: day)) \(pad(num: hour)):\(pad(num: minute)):\(pad(num: second))" if let nanosec = self.nanosecond { string += nanosecondsToString(nanosec) } return EscapedQueryParameter(string + "'") } if let hour = self.hour, let minute = self.minute, let second = self.second { var string = "'\(pad(num: hour)):\(pad(num: minute)):\(pad(num: second))" if let nanosec = self.nanosecond { string += nanosecondsToString(nanosec) } return EscapedQueryParameter(string + "'") } if let year = self.year { return EscapedQueryParameter("'\(pad(num: year))'") } throw QueryParameterError.dateComponentsError(self.description) } } fileprivate let DateTimeRegex: NSRegularExpression = { return try! NSRegularExpression(pattern: "^(\\d{4})-(\\d{2})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2})\\.?(\\d*)$", options: []) }() fileprivate let TimeRegex: NSRegularExpression = { return try! NSRegularExpression(pattern: "^(\\-?\\d{1,3}):(\\d{2}):(\\d{2})\\.?(\\d*)$", options: []) }() fileprivate let DateRegex: NSRegularExpression = { return try! NSRegularExpression(pattern: "^(\\d{4})-(\\d{2})-(\\d{2})$", options: []) }() fileprivate func stringToNanoseconds(_ string: S) -> Int? { guard string.count > 0 else { return nil } guard let doubleValue = Double("0.\(string)") else { return nil } return Int(doubleValue * 1_000_000_000.0) } extension DateComponents: SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> DateComponents { if string.count == 4 { // YEAR type return DateComponents(year: Int(string)) } let wholeRange = NSRange(string.startIndex..= 19, let match = DateTimeRegex.firstMatch(in: string, options: [], range: wholeRange) { let year = Int(string[Range(match.range(at: 1), in: string)!]) let month = Int(string[Range(match.range(at: 2), in: string)!]) let day = Int(string[Range(match.range(at: 3), in: string)!]) let hour = Int(string[Range(match.range(at: 4), in: string)!]) let minute = Int(string[Range(match.range(at: 5), in: string)!]) let second = Int(string[Range(match.range(at: 6), in: string)!]) let nanosecond = String(string[Range(match.range(at: 7), in: string)!]) return DateComponents( year: year, month: month, day: day, hour: hour, minute: minute, second: second, nanosecond: stringToNanoseconds(nanosecond) ) } // "1:23:45" if string.count >= 7, let match = TimeRegex.firstMatch(in: string, options: [], range: wholeRange) { let hour = Int(string[Range(match.range(at: 1), in: string)!]) let minute = Int(string[Range(match.range(at: 2), in: string)!]) let second = Int(string[Range(match.range(at: 3), in: string)!]) let nanosecond = string[Range(match.range(at: 4), in: string)!] return DateComponents( hour: hour, minute: minute, second: second, nanosecond: stringToNanoseconds(nanosecond) ) } // "2000-01-23" if string.count == 10, let match = DateRegex.firstMatch(in: string, options: [], range: wholeRange) { let year = Int(string[Range(match.range(at: 1), in: string)!]) let month = Int(string[Range(match.range(at: 2), in: string)!]) let day = Int(string[Range(match.range(at: 3), in: string)!]) return DateComponents( year: year, month: month, day: day ) } throw QueryError.SQLDateStringError(string) } } ================================================ FILE: Sources/MySQL/Error.swift ================================================ // // Error.swift // MySQL // // Created by Yusuke Ito on 12/14/15. // Copyright © 2015 Yusuke Ito. All rights reserved. // public enum QueryError: Error { case queryExecutionError(message: String, query: String) case resultFetchError(message: String, query: String) case resultNoFieldError(query: String) case resultRowFetchError(query: String) case resultFieldFetchError(query: String) case resultParseError(message: String, result: String) case resultCastError(actualValue: String, expectedType: String, forField: String) case resultDecodeError(rawSQLValue: String, forType: Any) case resultDecodeErrorMessage(message: String) case SQLDateStringError(String) case SQLRawStringDecodeError(error: Error, actualValue: String, expectedType: String, forField: String) case missingField(String) } public enum QueryParameterError: Error { case dateComponentsError(String) } ================================================ FILE: Sources/MySQL/IDType.swift ================================================ // // IDType.swift // MySQL // // Created by Yusuke Ito on 6/27/16. // Copyright © 2016 Yusuke Ito. All rights reserved. // import SQLFormatter public protocol IDType: QueryParameter, Hashable, Codable { associatedtype T: QueryParameter, Hashable, Codable var id: T { get } init(_ id: T) } public extension IDType { func queryParameter(option: QueryParameterOption) throws -> QueryParameterType { return try id.queryParameter(option: option) } #if swift(>=4.2) func hash(into hasher: inout Hasher) { hasher.combine(id) } #else var hashValue: Int { return id.hashValue } #endif } extension IDType where Self: SQLRawStringDecodable, Self.T: SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> Self { return Self(try T.fromSQLValue(string: string)) } } extension IDType { public static func ==(lhs: Self, rhs: Self) -> Bool { return lhs.id == rhs.id } } // MARK: Codable type extension IDType { public init(from decoder: Decoder) throws { if T.self == Int.self { self.init(try decoder.singleValueContainer().decode(Int.self) as! T) } else if T.self == Int64.self { self.init(try decoder.singleValueContainer().decode(Int64.self) as! T) } else if T.self == UInt.self { self.init(try decoder.singleValueContainer().decode(UInt.self) as! T) } else if T.self == UInt64.self { self.init(try decoder.singleValueContainer().decode(UInt64.self) as! T) } else if T.self == String.self { self.init(try decoder.singleValueContainer().decode(String.self) as! T) } else { fatalError("`init(from:)` of \(Self.self) is not implemented") } } public func encode(to encoder: Encoder) throws { if T.self == Int.self { var container = encoder.singleValueContainer() try container.encode(id as! Int) } else if T.self == Int64.self { var container = encoder.singleValueContainer() try container.encode(id as! Int64) } else if T.self == UInt.self { var container = encoder.singleValueContainer() try container.encode(id as! UInt) } else if T.self == UInt64.self { var container = encoder.singleValueContainer() try container.encode(id as! UInt64) } else if T.self == String.self { var container = encoder.singleValueContainer() try container.encode(id as! String) } else { fatalError("`encode(to:)` of \(Self.self) is not implemented") } } } // TODO: this implementation does not work in release build, Swift 4.1 /* extension IDType { public init(from decoder: Decoder) throws { fatalError("`init(from:)` of \(Self.self) is not implemented") } public func encode(to encoder: Encoder) throws { fatalError("`encode(to:)` of \(Self.self) is not implemented") } } extension IDType where T == Int { public init(from decoder: Decoder) throws { self.init(try decoder.singleValueContainer().decode(Int.self)) } public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(id) } } extension IDType where T == Int64 { public init(from decoder: Decoder) throws { self.init(try decoder.singleValueContainer().decode(Int64.self)) } public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(id) } } extension IDType where T == UInt { public init(from decoder: Decoder) throws { self.init(try decoder.singleValueContainer().decode(UInt.self)) } public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(id) } } extension IDType where T == UInt64 { public init(from decoder: Decoder) throws { self.init(try decoder.singleValueContainer().decode(UInt64.self)) } public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(id) } } extension IDType where T == String { public init(from decoder: Decoder) throws { self.init(try decoder.singleValueContainer().decode(String.self)) } public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(id) } } */ ================================================ FILE: Sources/MySQL/Query.swift ================================================ // // Connection.swift // MySQL // // Created by ito on 2015/10/24. // Copyright © 2015年 Yusuke Ito. All rights reserved. // import CMySQL import SQLFormatter import Foundation public struct QueryStatus: CustomStringConvertible { public let affectedRows: UInt64? public let insertedID: UInt64 init(mysql: UnsafeMutablePointer) { self.insertedID = mysql_insert_id(mysql) let arows = mysql_affected_rows(mysql) if arows == (~0) { self.affectedRows = nil // error or select statement } else { self.affectedRows = arows } } public var description: String { return "insertedID:\(insertedID), affectedRows:" + (affectedRows != nil ? ("\(affectedRows!)") : "nil") } } internal extension String { func subString(max: Int) -> String { guard let r = index(startIndex, offsetBy: max, limitedBy: endIndex) else { return self } return String(self[startIndex.. EmptyRowResult { return EmptyRowResult() } } internal struct Field { let name: String let type: enum_field_types init?(f: MYSQL_FIELD) { if f.name == nil { return nil } guard let fs = String(validatingUTF8: f.name) else { return nil } self.name = fs self.type = f.type } var isDate: Bool { return type == MYSQL_TYPE_DATE || type == MYSQL_TYPE_DATETIME || type == MYSQL_TYPE_TIME || type == MYSQL_TYPE_TIMESTAMP } } internal enum FieldValue { case null case binary(Data) case date(dateString: String, timezone: TimeZone) static func makeBinary(ptr: UnsafeMutablePointer, length: UInt) -> FieldValue { let data = Data(bytes: UnsafeRawPointer(ptr), count: Int(length)) return FieldValue.binary(data) } func string() throws -> String { switch self { case .null: throw QueryError.resultParseError(message: "the field is not string.", result: "null") case .date(let string, _): return string case .binary(let data): guard let string = String(data: data, encoding: .utf8) else { throw QueryError.resultParseError(message: "invalid utf8 string bytes.", result: "") } return string } } } fileprivate func query(query formattedQuery: String, option: QueryParameterOption) throws -> ([T], QueryStatus) { let (rows, status) = try self.query(query: formattedQuery, option: option) return try (rows.map({ try T(from: QueryRowResultDecoder(row: $0))}), status) } fileprivate func query(query formattedQuery: String, option: QueryParameterOption) throws -> ([QueryRowResult], QueryStatus) { let mysql = try connectIfNeeded() func queryPrefix() -> String { if self.option.omitDetailsOnError { return "" } return formattedQuery.subString(max: 1000) } guard mysql_real_query(mysql, formattedQuery, UInt(formattedQuery.utf8.count)) == 0 else { throw QueryError.queryExecutionError(message: MySQLUtil.getMySQLError(mysql), query: queryPrefix()) } let status = QueryStatus(mysql: mysql) let res = mysql_use_result(mysql) guard res != nil else { if mysql_field_count(mysql) == 0 { // actual no result return ([], status) } throw QueryError.resultFetchError(message: MySQLUtil.getMySQLError(mysql), query: queryPrefix()) } defer { mysql_free_result(res) } let fieldCount = Int(mysql_num_fields(res)) guard fieldCount > 0 else { throw QueryError.resultNoFieldError(query: queryPrefix()) } // fetch field info guard let fieldDef = mysql_fetch_fields(res) else { throw QueryError.resultFieldFetchError(query: queryPrefix()) } var fields:[Field] = [] for i in 0.. [QueryParameterType] { return try params.map { try $0.queryParameter(option: option) } } public func query(_ query: String, _ params: [QueryParameter] = []) throws -> ([R], QueryStatus) { let option = QueryParameterDefaultOption( timeZone: self.option.timeZone ) let queryString = try QueryFormatter.format(query: query, parameters: type(of: self).buildParameters(params, option: option)) return try self.query(query: queryString, option: option) } public func query(_ query: String, _ params: [QueryParameter] = [], option: QueryParameterOption) throws -> ([R], QueryStatus) { let queryString = try QueryFormatter.format(query: query, parameters: type(of: self).buildParameters(params, option: option)) return try self.query(query: queryString, option: option) } public func query(_ query: String, _ params: [QueryParameter] = []) throws -> [R] { let (rows, _) = try self.query(query, params) as ([R], QueryStatus) return rows } public func query(_ query: String, _ params: [QueryParameter] = [], option: QueryParameterOption) throws -> [R] { let (rows, _) = try self.query(query, params, option: option) as ([R], QueryStatus) return rows } public func query(_ query: String, _ params: [QueryParameter] = []) throws -> QueryStatus { let (_, status) = try self.query(query, params) as ([EmptyRowResult], QueryStatus) return status } public func query(_ query: String, _ params: [QueryParameter] = [], option: QueryParameterOption) throws -> QueryStatus { let (_, status) = try self.query(query, params, option: option) as ([EmptyRowResult], QueryStatus) return status } } ================================================ FILE: Sources/MySQL/QueryParameter-Data.swift ================================================ // // QueryParameter-Data.swift // MySQL // // Created by Yusuke Ito on 4/29/18. // import Foundation public protocol QueryRowResultCustomData { static func decode(fromRowData data: Data) throws -> Self } public enum QueryCustomDataParameterDataType { case blob case json } public protocol QueryCustomDataParameter { func encodeForQueryParameter() throws -> Data var queryParameterDataType: QueryCustomDataParameterDataType { get } } public extension QueryCustomDataParameter { var queryParameterDataType: QueryCustomDataParameterDataType { return .blob } } ================================================ FILE: Sources/MySQL/QueryParameterType.swift ================================================ // // QueryParameterType.swift // MySQL // // Created by Yusuke Ito on 12/28/15. // Copyright © 2015 Yusuke Ito. All rights reserved. // import SQLFormatter import Foundation public protocol QueryParameter { func queryParameter(option: QueryParameterOption) throws -> QueryParameterType var omitOnQueryParameter: Bool { get } } public extension QueryParameter { var omitOnQueryParameter: Bool { return false } } public protocol QueryParameterOption { var timeZone: TimeZone { get } } internal struct QueryParameterNull: QueryParameter { private init() { } static let null = QueryParameterNull() func queryParameter(option: QueryParameterOption) -> QueryParameterType { return EscapedQueryParameter( "NULL" ) } } @available(*, renamed: "QueryParameterDictionary") typealias QueryDictionary = QueryParameterDictionary public struct QueryParameterDictionary: QueryParameter { private let dict: [String: QueryParameter?] public init(_ dict: [String: QueryParameter?]) { self.dict = dict } public func queryParameter(option: QueryParameterOption) throws -> QueryParameterType { var keyVals: [String] = [] for (k, v) in dict { if v == nil || v?.omitOnQueryParameter == false { keyVals.append("\(SQLString.escapeForID(k)) = \(try (v ?? QueryParameterNull.null).queryParameter(option: option).escaped())") } } return EscapedQueryParameter( keyVals.joined(separator: ", ") ) } } fileprivate protocol QueryParameterArrayType: QueryParameter { } @available(*, renamed: "QueryParameterArray") typealias QueryArray = QueryParameterArray public struct QueryParameterArray: QueryParameter, QueryParameterArrayType { private let arr: [QueryParameter?] public init(_ arr: [QueryParameter?]) { self.arr = arr } public init(_ arr: [QueryParameter]) { self.arr = arr.map { Optional($0) } } public func queryParameter(option: QueryParameterOption) throws -> QueryParameterType { return EscapedQueryParameter( try arr.filter({ val in if let valid = val { return valid.omitOnQueryParameter == false } return true }).map({ if let val = $0 as? QueryParameterArrayType { return "(" + (try val.queryParameter(option: option).escaped()) + ")" } return try ($0 ?? QueryParameterNull.null).queryParameter(option: option).escaped() }).joined(separator: ", ") ) } } extension Optional: QueryParameter where Wrapped: QueryParameter { public func queryParameter(option: QueryParameterOption) throws -> QueryParameterType { switch self { case .none: return QueryParameterNull.null.queryParameter(option: option) case .some(let value): return try value.queryParameter(option: option) } } public var omitOnQueryParameter: Bool { switch self { case .none: return false case .some(let value): return value.omitOnQueryParameter } } } internal struct EscapedQueryParameter: QueryParameterType { private let value: String private let idParameter: String? init(_ val: String, idParameter: String? = nil) { self.value = val self.idParameter = idParameter } func escaped() -> String { return value } func escapedForID() -> String? { return idParameter } } extension String: QueryParameterType { public func escaped() -> String { return SQLString.escape(self) } public func escapedForID() -> String? { return SQLString.escapeForID(self) } } extension String: QueryParameter { public func queryParameter(option: QueryParameterOption) -> QueryParameterType { return self } } extension Int: QueryParameter { public func queryParameter(option: QueryParameterOption) -> QueryParameterType { return EscapedQueryParameter( String(self) ) } } extension UInt: QueryParameter { public func queryParameter(option: QueryParameterOption) -> QueryParameterType { return EscapedQueryParameter( String(self) ) } } extension Int64: QueryParameter { public func queryParameter(option: QueryParameterOption) -> QueryParameterType { return EscapedQueryParameter( String(self) ) } } extension Int32: QueryParameter { public func queryParameter(option: QueryParameterOption) -> QueryParameterType { return EscapedQueryParameter( String(self) ) } } extension Int16: QueryParameter { public func queryParameter(option: QueryParameterOption) -> QueryParameterType { return EscapedQueryParameter( String(self) ) } } extension Int8: QueryParameter { public func queryParameter(option: QueryParameterOption) -> QueryParameterType { return EscapedQueryParameter( String(self) ) } } extension UInt64: QueryParameter { public func queryParameter(option: QueryParameterOption) -> QueryParameterType { return EscapedQueryParameter( String(self) ) } } extension UInt32: QueryParameter { public func queryParameter(option: QueryParameterOption) -> QueryParameterType { return EscapedQueryParameter( String(self) ) } } extension UInt16: QueryParameter { public func queryParameter(option: QueryParameterOption) -> QueryParameterType { return EscapedQueryParameter( String(self) ) } } extension UInt8: QueryParameter { public func queryParameter(option: QueryParameterOption) -> QueryParameterType { return EscapedQueryParameter( String(self) ) } } extension Double: QueryParameter { public func queryParameter(option: QueryParameterOption) -> QueryParameterType { return EscapedQueryParameter( String(self) ) } } extension Float: QueryParameter { public func queryParameter(option: QueryParameterOption) -> QueryParameterType { return EscapedQueryParameter( String(self) ) } } extension Bool: QueryParameter { public func queryParameter(option: QueryParameterOption) -> QueryParameterType { return EscapedQueryParameter( self ? "true" : "false" ) } } extension Decimal: QueryParameter { public func queryParameter(option: QueryParameterOption) -> QueryParameterType { return EscapedQueryParameter( String(describing: self) ) } } /// MARK: Codable support fileprivate final class QueryParameterEncoder: Encoder { let codingPath = [CodingKey]() let userInfo = [CodingUserInfoKey : Any]() var dict: [String: QueryParameter?] = [:] var singleValue: QueryParameter? = nil enum StorageType { case single case dictionary } var storageType: StorageType = .dictionary func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { return KeyedEncodingContainer(QueryParameterKeyedEncodingContainer(encoder: self)) } func unkeyedContainer() -> UnkeyedEncodingContainer { fatalError("not supported unkeyedContainer in QueryParameter") } func singleValueContainer() -> SingleValueEncodingContainer { self.storageType = .single return QueryParameterSingleValueEncodingContainer(encoder: self) } } fileprivate struct QueryParameterSingleValueEncodingContainer: SingleValueEncodingContainer { let codingPath = [CodingKey]() var encoder: QueryParameterEncoder mutating func encodeNil() throws { encoder.singleValue = nil } mutating func encode(_ value: Bool) throws { encoder.singleValue = value } mutating func encode(_ value: Int) throws { encoder.singleValue = value } mutating func encode(_ value: Int8) throws { encoder.singleValue = value } mutating func encode(_ value: Int16) throws { encoder.singleValue = value } mutating func encode(_ value: Int32) throws { encoder.singleValue = value } mutating func encode(_ value: Int64) throws { encoder.singleValue = value } mutating func encode(_ value: UInt) throws { encoder.singleValue = value } mutating func encode(_ value: UInt8) throws { encoder.singleValue = value } mutating func encode(_ value: UInt16) throws { encoder.singleValue = value } mutating func encode(_ value: UInt32) throws { encoder.singleValue = value } mutating func encode(_ value: UInt64) throws { encoder.singleValue = value } mutating func encode(_ value: Float) throws { encoder.singleValue = value } mutating func encode(_ value: Double) throws { encoder.singleValue = value } mutating func encode(_ value: String) throws { encoder.singleValue = value } mutating func encode(_ value: T) throws where T : Encodable { encoder.singleValue = value as? QueryParameter } } fileprivate struct QueryParameterKeyedEncodingContainer : KeyedEncodingContainerProtocol { fileprivate let codingPath = [CodingKey]() fileprivate let encoder: QueryParameterEncoder mutating func encodeNil(forKey key: Key) throws { encoder.dict[key.stringValue] = nil } mutating func encode(_ value: Bool, forKey key: Key) throws { encoder.dict[key.stringValue] = value } mutating func encode(_ value: Int, forKey key: Key) throws { encoder.dict[key.stringValue] = value } mutating func encode(_ value: Int8, forKey key: Key) throws { encoder.dict[key.stringValue] = value } mutating func encode(_ value: Int16, forKey key: Key) throws { encoder.dict[key.stringValue] = value } mutating func encode(_ value: Int32, forKey key: Key) throws { encoder.dict[key.stringValue] = value } mutating func encode(_ value: Int64, forKey key: Key) throws { encoder.dict[key.stringValue] = value } mutating func encode(_ value: UInt, forKey key: Key) throws { encoder.dict[key.stringValue] = value } mutating func encode(_ value: UInt8, forKey key: Key) throws { encoder.dict[key.stringValue] = value } mutating func encode(_ value: UInt16, forKey key: Key) throws { encoder.dict[key.stringValue] = value } mutating func encode(_ value: UInt32, forKey key: Key) throws { encoder.dict[key.stringValue] = value } mutating func encode(_ value: UInt64, forKey key: Key) throws { encoder.dict[key.stringValue] = value } mutating func encode(_ value: Float, forKey key: Key) throws { encoder.dict[key.stringValue] = value } mutating func encode(_ value: Double, forKey key: Key) throws { encoder.dict[key.stringValue] = value } mutating func encode(_ value: String, forKey key: Key) throws { encoder.dict[key.stringValue] = value } mutating func encode(_ value: T, forKey key: Key) throws where T : Encodable { if T.self == Date.self { encoder.dict[key.stringValue] = value as! Date } else if T.self == DateComponents.self { encoder.dict[key.stringValue] = value as! DateComponents } else if T.self == Data.self { encoder.dict[key.stringValue] = value as! Data } else if T.self == URL.self { // encode absoluteString same as JSONEncoder // https://github.com/apple/swift/blob/master/stdlib/public/SDK/Foundation/JSONEncoder.swift encoder.dict[key.stringValue] = (value as! URL).absoluteString } else if T.self == Decimal.self { encoder.dict[key.stringValue] = (value as! Decimal).description } else if let custom = value as? QueryCustomDataParameter { if let param = value as? QueryParameter { if !param.omitOnQueryParameter { let data = try custom.encodeForQueryParameter() encoder.dict[key.stringValue] = Blob(data: data, dataType: custom.queryParameterDataType) } } else { let data = try custom.encodeForQueryParameter() encoder.dict[key.stringValue] = Blob(data: data, dataType: custom.queryParameterDataType) } } else { let singleValueEncoder = QueryParameterEncoder() try value.encode(to: singleValueEncoder) if let param = value as? QueryParameter { if !param.omitOnQueryParameter { encoder.dict[key.stringValue] = singleValueEncoder.singleValue } } else { encoder.dict[key.stringValue] = singleValueEncoder.singleValue } } //fatalError("not supported type \(T.self)") } mutating func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey { fatalError("nestedContainer in query parameter is not supported.") } mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { fatalError("nestedUnkeyedContainer in query parameter is not supported.") } mutating func superEncoder() -> Encoder { fatalError("superEncoder in query parameter is not supported.") } mutating func superEncoder(forKey key: Key) -> Encoder { fatalError("superEncoder(forKey:) in query parameter is not supported.") } } extension Encodable where Self: QueryParameter { public func queryParameter(option: QueryParameterOption) throws -> QueryParameterType { let encoder = QueryParameterEncoder() try self.encode(to: encoder) switch encoder.storageType { case .dictionary: return try QueryParameterDictionary(encoder.dict).queryParameter(option: option) case .single: return try (encoder.singleValue ?? QueryParameterNull.null).queryParameter(option: option) } } } ================================================ FILE: Sources/MySQL/RawRepresentableParameter.swift ================================================ // // RawRepresentableParameter.swift // MySQL // // Created by Yusuke Ito on 4/21/16. // Copyright © 2016 Yusuke Ito. All rights reserved. // import SQLFormatter import Foundation public protocol QueryRawRepresentableParameter: RawRepresentable, QueryParameter { } extension QueryRawRepresentableParameter where RawValue: QueryParameter { public func queryParameter(option: QueryParameterOption) throws -> QueryParameterType { return try rawValue.queryParameter(option: option) } } /* extension RawRepresentable where RawValue: QueryParameter { public func queryParameter(option: QueryParameterOption) throws -> QueryParameterType { return try rawValue.queryParameter(option: option) } }*/ ================================================ FILE: Sources/MySQL/Result-SQLRawStringDecodable.swift ================================================ // // ResultTypes.swift // MySQL // // Created by Yusuke Ito on 12/28/15. // Copyright © 2015 Yusuke Ito. All rights reserved. // import Foundation extension Int: SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> Int { guard let val = Int(string) else { throw QueryError.resultDecodeError(rawSQLValue: string, forType: self) } return val } } extension UInt: SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> UInt { guard let val = UInt(string) else { throw QueryError.resultDecodeError(rawSQLValue: string, forType: self) } return val } } extension Int64: SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> Int64 { guard let val = Int64(string) else { throw QueryError.resultDecodeError(rawSQLValue: string, forType: self) } return val } } extension Int32: SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> Int32 { guard let val = Int32(string) else { throw QueryError.resultDecodeError(rawSQLValue: string, forType: self) } return val } } extension Int16: SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> Int16 { guard let val = Int16(string) else { throw QueryError.resultDecodeError(rawSQLValue: string, forType: self) } return val } } extension Int8: SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> Int8 { guard let val = Int8(string) else { throw QueryError.resultDecodeError(rawSQLValue: string, forType: self) } return val } } extension UInt64: SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> UInt64 { guard let val = UInt64(string) else { throw QueryError.resultDecodeError(rawSQLValue: string, forType: self) } return val } } extension UInt32: SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> UInt32 { guard let val = UInt32(string) else { throw QueryError.resultDecodeError(rawSQLValue: string, forType: self) } return val } } extension UInt16: SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> UInt16 { guard let val = UInt16(string) else { throw QueryError.resultDecodeError(rawSQLValue: string, forType: self) } return val } } extension UInt8: SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> UInt8 { guard let val = UInt8(string) else { throw QueryError.resultDecodeError(rawSQLValue: string, forType: self) } return val } } extension Float: SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> Float { guard let val = Float(string) else { throw QueryError.resultDecodeError(rawSQLValue: string, forType: self) } return val } } extension Double: SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> Double { guard let val = Double(string) else { throw QueryError.resultDecodeError(rawSQLValue: string, forType: self) } return val } } extension String: SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> String { return string } } extension Bool: SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> Bool { guard let val = Int(string) else { throw QueryError.resultDecodeError(rawSQLValue: string, forType: self) } return Bool(val == 0 ? false : true ) } } extension Decimal: SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> Decimal { guard let val = Decimal(string: string) else { throw QueryError.resultDecodeError(rawSQLValue: string, forType: self) } return val } } extension Date: SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> Date { fatalError("invalid constructor (use init instead)") } } ================================================ FILE: Sources/MySQL/Result.swift ================================================ // // Result.swift // MySQL // // Created by ito on 12/10/15. // Copyright © 2015 Yusuke Ito. All rights reserved. // import Foundation internal protocol SQLRawStringDecodable { static func fromSQLValue(string: String) throws -> Self } internal struct QueryRowResult { private let fields: [Connection.Field] private let fieldValues: [Connection.FieldValue] internal let columnMap: [String: Connection.FieldValue] // the key is field name init(fields: [Connection.Field], fieldValues: [Connection.FieldValue]) { self.fields = fields self.fieldValues = fieldValues var map:[String: Connection.FieldValue] = [:] for i in 0.. Bool { guard let fieldValue = columnMap[field] else { return false } switch fieldValue { case .null: return true case .binary, .date: return false } } private func castOrFail(_ obj: String, field: String) throws -> T { //print("casting val \(obj) to \(T.self)") do { return try T.fromSQLValue(string: obj) } catch { throw QueryError.SQLRawStringDecodeError(error: error, actualValue: obj, expectedType: "\(T.self)", forField: field) } } private func getValue(fieldValue: Connection.FieldValue, field: String) throws -> T { switch fieldValue { case .null: throw QueryError.resultCastError(actualValue: "NULL", expectedType: "\(T.self)", forField: field) case .date(let string, let timezone): if T.self == Date.self { return try Date(sqlDate: string, timeZone: timezone) as! T } else if T.self == DateComponents.self { return try DateComponents.fromSQLValue(string: string) as! T } throw QueryError.resultCastError(actualValue: "\(string)", expectedType: "\(T.self)", forField: field) case .binary(let data): //print("T is \(T.self)") if let bin = data as? T { return bin } return try castOrFail(fieldValue.string(), field: field) } } func getValue(forField field: String) throws -> T { guard let fieldValue = columnMap[field] else { throw QueryError.missingField(field) } return try getValue(fieldValue: fieldValue, field: field) } } internal struct QueryRowResultDecoder : Decoder { let codingPath = [CodingKey]() let userInfo = [CodingUserInfoKey : Any]() let row: QueryRowResult public func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { return KeyedDecodingContainer(RowKeyedDecodingContainer(decoder: self)) } public func unkeyedContainer() throws -> UnkeyedDecodingContainer { throw QueryError.resultDecodeErrorMessage(message: "Decoder unkeyedContainer not implemented") } public func singleValueContainer() throws -> SingleValueDecodingContainer { throw QueryError.resultDecodeErrorMessage(message: "Decoder singleValueContainer not implemented") } } fileprivate struct SQLStringDecoder: Decoder { let codingPath = [CodingKey]() let userInfo = [CodingUserInfoKey : Any]() let sqlString: String struct SingleValue: SingleValueDecodingContainer { let codingPath = [CodingKey]() let sqlString: String func decodeNil() -> Bool { fatalError() } func decode(_ type: Bool.Type) throws -> Bool { fatalError() } func decode(_ type: Int.Type) throws -> Int { return try Int.fromSQLValue(string: sqlString) } func decode(_ type: Int8.Type) throws -> Int8 { return try Int8.fromSQLValue(string: sqlString) } func decode(_ type: Int16.Type) throws -> Int16 { return try Int16.fromSQLValue(string: sqlString) } func decode(_ type: Int32.Type) throws -> Int32 { return try Int32.fromSQLValue(string: sqlString) } func decode(_ type: Int64.Type) throws -> Int64 { return try Int64.fromSQLValue(string: sqlString) } func decode(_ type: UInt.Type) throws -> UInt { return try UInt.fromSQLValue(string: sqlString) } func decode(_ type: UInt8.Type) throws -> UInt8 { return try UInt8.fromSQLValue(string: sqlString) } func decode(_ type: UInt16.Type) throws -> UInt16 { return try UInt16.fromSQLValue(string: sqlString) } func decode(_ type: UInt32.Type) throws -> UInt32 { return try UInt32.fromSQLValue(string: sqlString) } func decode(_ type: UInt64.Type) throws -> UInt64 { return try UInt64.fromSQLValue(string: sqlString) } func decode(_ type: Float.Type) throws -> Float { fatalError() } func decode(_ type: Double.Type) throws -> Double { fatalError() } func decode(_ type: String.Type) throws -> String { return sqlString } func decode(_ type: T.Type) throws -> T where T : Decodable { fatalError() } } func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey { throw QueryError.resultDecodeErrorMessage(message: "RawTypeDecoder container(keyedBy:) not implemented, you could implement `QueryRowResultCustomData` ") } func unkeyedContainer() throws -> UnkeyedDecodingContainer { throw QueryError.resultDecodeErrorMessage(message: "RawTypeDecoder unkeyedContainer not implemented") } func singleValueContainer() throws -> SingleValueDecodingContainer { return SingleValue(sqlString: sqlString) } } fileprivate struct RowKeyedDecodingContainer : KeyedDecodingContainerProtocol { typealias Key = K let decoder : QueryRowResultDecoder let allKeys = [Key]() let codingPath = [CodingKey]() func decodeNil(forKey key: K) throws -> Bool { return false } func contains(_ key: K) -> Bool { return decoder.row.columnMap[key.stringValue] != nil && !decoder.row.isNull(forField: key.stringValue) } func decode(_ type: Bool.Type, forKey key: K) throws -> Bool { return try decoder.row.getValue(forField: key.stringValue) } func decode(_ type: Int.Type, forKey key: K) throws -> Int { return try decoder.row.getValue(forField: key.stringValue) } func decode(_ type: Int8.Type, forKey key: K) throws -> Int8 { return try decoder.row.getValue(forField: key.stringValue) } func decode(_ type: Int16.Type, forKey key: K) throws -> Int16 { return try decoder.row.getValue(forField: key.stringValue) } func decode(_ type: Int32.Type, forKey key: K) throws -> Int32 { return try decoder.row.getValue(forField: key.stringValue) } func decode(_ type: Int64.Type, forKey key: K) throws -> Int64 { return try decoder.row.getValue(forField: key.stringValue) } func decode(_ type: UInt.Type, forKey key: K) throws -> UInt { return try decoder.row.getValue(forField: key.stringValue) } func decode(_ type: UInt8.Type, forKey key: K) throws -> UInt8 { return try decoder.row.getValue(forField: key.stringValue) } func decode(_ type: UInt16.Type, forKey key: K) throws -> UInt16 { return try decoder.row.getValue(forField: key.stringValue) } func decode(_ type: UInt32.Type, forKey key: K) throws -> UInt32 { return try decoder.row.getValue(forField: key.stringValue) } func decode(_ type: UInt64.Type, forKey key: K) throws -> UInt64 { return try decoder.row.getValue(forField: key.stringValue) } func decode(_ type: Float.Type, forKey key: K) throws -> Float { return try decoder.row.getValue(forField: key.stringValue) } func decode(_ type: Double.Type, forKey key: K) throws -> Double { return try decoder.row.getValue(forField: key.stringValue) } func decode(_ type: String.Type, forKey key: K) throws -> String { return try decoder.row.getValue(forField: key.stringValue) as String } func decode(_ t: T.Type, forKey key: K) throws -> T where T : Decodable { if t == Data.self { return try decoder.row.getValue(forField: key.stringValue) as Data as! T } else if t == Date.self { return try decoder.row.getValue(forField: key.stringValue) as Date as! T } else if t == DateComponents.self { return try decoder.row.getValue(forField: key.stringValue) as DateComponents as! T } else if t == URL.self { let urlString = try decoder.row.getValue(forField: key.stringValue) as String guard let url = URL(string: urlString) else { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Invalid URL string.")) } return url as! T } else if t == Decimal.self { return try decoder.row.getValue(forField: key.stringValue) as Decimal as! T } else if let customType = t as? QueryRowResultCustomData.Type { let data = try decoder.row.getValue(forField: key.stringValue) as Data return try customType.decode(fromRowData: data) as! T } guard let columnValue = decoder.row.columnMap[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: [key], debugDescription: "")) } let d = SQLStringDecoder(sqlString: try columnValue.string()) return try T(from: d) } func nestedContainer(keyedBy type: NestedKey.Type, forKey key: K) throws -> KeyedDecodingContainer { throw QueryError.resultDecodeErrorMessage(message: "KeyedDecodingContainer nestedContainer not implemented") } func nestedUnkeyedContainer(forKey key: K) throws -> UnkeyedDecodingContainer { throw QueryError.resultDecodeErrorMessage(message: "KeyedDecodingContainer nestedContainer not implemented") } func superDecoder() throws -> Decoder { throw QueryError.resultDecodeErrorMessage(message: "KeyedDecodingContainer superDecoder not implemented") } func superDecoder(forKey key: K) throws -> Decoder { throw QueryError.resultDecodeErrorMessage(message: "KeyedDecodingContainer superDecoder(forKey) not implemented") } } ================================================ FILE: Sources/MySQL/Sync.swift ================================================ // // Sync.swift // MySQL // // Created by Yusuke Ito on 1/12/16. // Copyright © 2016 Yusuke Ito. All rights reserved. // #if os(Linux) import Glibc #elseif os(OSX) import Darwin.C #endif fileprivate final class Mutex { private var mutex = pthread_mutex_t() init() { pthread_mutex_init(&mutex, nil) } func lock() { pthread_mutex_lock(&mutex) } func unlock() { pthread_mutex_unlock(&mutex) } deinit { pthread_mutex_destroy(&mutex) } } internal struct Atomic { private var value: T private let mutex = Mutex() init(_ value: T) { self.value = value } mutating func syncWriting( _ block: (inout T) throws -> R) rethrows -> R { mutex.lock() defer { mutex.unlock() } let result = try block(&value) return result } func sync( _ block: (T) throws -> R) rethrows -> R { mutex.lock() defer { mutex.unlock() } let result = try block(value) return result } } ================================================ FILE: Sources/MySQL/Transaction.swift ================================================ // // Connection-Transaction.swift // MySQL // // Created by ito on 12/24/15. // Copyright © 2015 Yusuke Ito. All rights reserved. // extension Connection { func beginTransaction() throws { _ = try query("START TRANSACTION;") } func commit() throws { _ = try query("COMMIT;") } func rollback() throws { _ = try query("ROLLBACK;") } } extension ConnectionPool { public func transaction( _ block: (_ conn: Connection) throws -> T ) throws -> T { let conn = try getConnection() defer { if option.reconnect { conn.setReconnect(true) } releaseConnection(conn) } // disable reconnect option of MySQL while transaction conn.setReconnect(false) try conn.beginTransaction() do { let result = try block(conn) try conn.commit() return result } catch { do { try conn.rollback() } catch { print("error while `ROLLBACK`.", error) } throw error } } } ================================================ FILE: Sources/SQLFormatter/Error.swift ================================================ // // Error.swift // SQLFormatter // // Created by Yusuke Ito on 4/5/16. // Copyright © 2016 Yusuke Ito. All rights reserved. // public enum QueryFormatError: Error { case placeholderCountMismatch(query: String) case parameterIDTypeError(givenValue: String, query: String) } ================================================ FILE: Sources/SQLFormatter/QueryFormatter.swift ================================================ // // Query.swift // MySQL // // Created by Yusuke Ito on 12/14/15. // Copyright © 2015 Yusuke Ito. All rights reserved. // import Foundation public protocol QueryParameterType { func escaped() -> String func escapedForID() -> String? // returns nil if not supported for query id parameter } public struct SQLString { public static func escapeForID(_ str: String) -> String { var step1 = "" for c in str { switch c { case "`": step1 += "``" default: step1.append(c) } } var out = "" for c in step1 { switch c { case ".": out += "`.`" default: out.append(c) } } return "`\(out)`" } public static func escape(_ str: String) -> String { var out = "'" for c in str.unicodeScalars { switch c { case "\0": out += "\\0" case "\n": out += "\\n" case "\r": out += "\\r" case "\u{8}": out += "\\b" case "\t": out += "\\t" case "\\": out += "\\\\" case "'": out += "\\'" case "\"": out += "\\\"" case "\u{1A}": out += "\\Z" default: out.append(Character(c)) } } out.append("'") return out } public static func escapeForLike(_ str: String, escapingWith: Character = "\\") -> String { var out = "" for c in str.unicodeScalars { switch c { case "%", "_": out.append(escapingWith) default: break } out.append(Character(c)) } return out } } public struct QueryFormatter { public static func format(query: String, parameters: [QueryParameterType]) throws -> String { var placeHolderCount = 0 var formatted = query + "" var valArgs: [QueryParameterType] = [] var scanRange = formatted.startIndex.. if let r1 = r1, let r2 = r2 { r = r1.lowerBound <= r2.lowerBound ? r1 : r2 } else if let rr = r1 ?? r2 { r = rr } else { break } switch formatted[r] { case "??": if placeHolderCount >= parameters.count { throw QueryFormatError.placeholderCountMismatch(query: query) } guard let escapedVal = parameters[placeHolderCount].escapedForID() else { throw QueryFormatError.parameterIDTypeError(givenValue: "\(parameters[placeHolderCount])", query: query) } formatted.replaceSubrange(r, with: escapedVal) scanRange = r.upperBound..= parameters.count { throw QueryFormatError.placeholderCountMismatch(query: query) } valArgs.append(parameters[placeHolderCount]) scanRange = r.upperBound..= parameters.count { break } } //print(formatted, valArgs) placeHolderCount = 0 var formattedChars = Array(formatted) var index = 0 while index < formattedChars.count { if formattedChars[index] == "?" { if placeHolderCount >= valArgs.count { throw QueryFormatError.placeholderCountMismatch(query: query) } let val = valArgs[placeHolderCount] formattedChars.remove(at: index) let valStr = val.escaped() formattedChars.insert(contentsOf: valStr, at: index) index += valStr.count - 1 placeHolderCount += 1 } else { index += 1 } } return String(formattedChars) } } ================================================ FILE: Sources/cmysql/macos.pc ================================================ prefix=/usr/local/opt/mysql exec_prefix=${prefix} libdir=${exec_prefix}/lib includedir=${prefix}/include Name: MySQL Description: MySQL client library Version: 2.0 Cflags: -I${includedir} Libs: -L${libdir} -lmysqlclient ================================================ FILE: Sources/cmysql/module.modulemap ================================================ module CMySQL [system] { header "shim.h" link "mysqlclient" export * } ================================================ FILE: Sources/cmysql/shim.h ================================================ #ifndef __CMYSQL_SHIM_H__ #define __CMYSQL_SHIM_H__ #include #if LIBMYSQL_VERSION_ID >= 80000 typedef int my_bool; #endif #endif ================================================ FILE: Tests/LinuxMain.swift ================================================ import XCTest import MySQLTests import SQLFormatterTests var tests = [XCTestCaseEntry]() tests += MySQLTests.__allTests() tests += SQLFormatterTests.__allTests() XCTMain(tests) ================================================ FILE: Tests/MySQLTests/BlobTests.swift ================================================ // // SQLTypeTests.swift // MySQL // // Created by Yusuke Ito on 4/21/16. // Copyright © 2016 Yusuke Ito. All rights reserved. // import XCTest @testable import MySQL import Foundation extension Row { fileprivate struct BlobTextRow: Codable, QueryParameter { let id: AutoincrementID let text1: String let binary1: Data } fileprivate struct JSONDataUser: Codable, Equatable, QueryCustomDataParameter, QueryRowResultCustomData { func encodeForQueryParameter() throws -> Data { let encoder = JSONEncoder() return try encoder.encode(self) } var queryParameterDataType: QueryCustomDataParameterDataType { return .json } static func decode(fromRowData data: Data) throws -> Row.JSONDataUser { let decoder = JSONDecoder() return try decoder.decode(self, from: data) } // this type decoded from and encoded to Data, like JSON, Protobuf... let name: String } fileprivate struct JSONColumnUser: Codable, QueryParameter, Equatable { let userName: String let jsonValue_blob: JSONDataUser let jsonValue_json: JSONDataUser } } final class BlobQueryTests: XCTestCase, QueryTestType { var constants: TestConstantsType! var pool: ConnectionPool! override func setUp() { super.setUp() prepare() try! createBlobTable() } func createBinaryBlobTable() throws { try dropTestTable() let conn = try pool.getConnection() let query = "CREATE TABLE `\(constants.tableName)` (" + "`id` int(11) unsigned NOT NULL AUTO_INCREMENT," + "`text1` mediumtext NOT NULL," + "`binary1` mediumblob NOT NULL," + "PRIMARY KEY (`id`)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;" _ = try conn.query(query) } func createBlobTable() throws { try dropTestTable() let conn = try pool.getConnection() let query = "CREATE TABLE `\(constants.tableName)` (" + "`id` int(11) unsigned NOT NULL AUTO_INCREMENT," + "`text1` mediumtext NOT NULL," + "`binary1` mediumblob NOT NULL," + "PRIMARY KEY (`id`)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;" _ = try conn.query(query) } func testInsertForCombinedUnicodeCharacter() throws { let str = "'゙ and áäèëî , ¥" let obj = Row.BlobTextRow(id: .noID, text1: str, binary1: Data() ) let status: QueryStatus = try pool.execute { conn in try conn.query("INSERT INTO ?? SET ? ", [constants.tableName, obj]) } XCTAssertEqual(status.insertedID, 1) } func testBlobAndTextOnBinCollation() throws { try createBinaryBlobTable() let testBinary: [UInt8] = Array( (UInt8(0)...UInt8(255)) ) let obj = Row.BlobTextRow(id: .noID, text1: "", binary1: Data(testBinary) ) let status: QueryStatus = try pool.execute { conn in try conn.query("INSERT INTO ?? SET ? ", [constants.tableName, obj]) } XCTAssertEqual(status.insertedID, 1) let rows: [Row.BlobTextRow] = try pool.execute{ conn in try conn.query("SELECT * FROM ??", [constants.tableName]) } XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0].binary1.count, 256) XCTAssertEqual(rows[0].binary1, Data(testBinary)) print(rows[0].binary1, testBinary) } func testEscapeBlob() throws { do { let testBinary: [UInt8] = [0, 0x1, 0x9, 0x10, 0x1f, 0x99, 0xff, 0x00, 0x0a] let str = try Data(testBinary).queryParameter(option: queryOption).escaped() XCTAssertEqual(str, "x'000109101f99ff000a'") } } private func createJSONValueTable() throws { try dropTestTable() let conn = try pool.getConnection() let query = """ CREATE TABLE `\(constants.tableName)` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `userName` mediumtext NOT NULL, `jsonValue_blob` mediumblob NOT NULL, `jsonValue_json` json NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; """ _ = try conn.query(query) } func testJSONColumnValue() throws { try createJSONValueTable() let jsonValue = Row.JSONDataUser(name: "name in json value") let user = Row.JSONColumnUser(userName: "john", jsonValue_blob: jsonValue, jsonValue_json: jsonValue) let status: QueryStatus = try pool.execute { conn in try conn.query("INSERT INTO ?? SET ? ", [constants.tableName, user]) } XCTAssertEqual(status.insertedID, 1) let rows: [Row.JSONColumnUser] = try pool.execute{ conn in try conn.query("SELECT * FROM ??", [constants.tableName]) } XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0], user) } } ================================================ FILE: Tests/MySQLTests/ConnectionPoolTests.swift ================================================ // // ConnectionPoolTests.swift // MySQL // // Created by ito on 12/24/15. // Copyright © 2015 Yusuke Ito. All rights reserved. // import Foundation import Dispatch import XCTest @testable import MySQL final class ConnectionPoolTests: XCTestCase, MySQLTestType { var constants: TestConstantsType! var pool: ConnectionPool! override func setUp() { super.setUp() prepare() } func testGetConnection() throws { let initialConnection = 1 XCTAssertEqual(pool.pool.sync { $0.count }, initialConnection) XCTAssertEqual(pool.pool.sync { $0.inUseConnections }, 0) var connections: [Connection] = [] for _ in 0.. = .ID(UserID(333)) XCTAssertEqual(try userID.queryParameter(option: queryOption).escaped(), "333") let arr: [QueryParameter] = [strVal, userID] XCTAssertEqual(try QueryParameterArray(arr).queryParameter(option: queryOption).escaped(), "\'Sup\\\'er\', 333") } do { let stringID: AutoincrementID = .ID(SomeStringID("id-555@")) XCTAssertEqual(try stringID.queryParameter(option: queryOption).escaped(), "\'id-555@\'") let arr: [QueryParameter] = [strVal, stringID] XCTAssertEqual(try QueryParameterArray(arr).queryParameter(option: queryOption).escaped(), "\'Sup\\\'er\', \'id-555@\'") } do { let noID: AutoincrementID = .noID XCTAssertEqual(try noID.queryParameter(option: queryOption).escaped(), "\'\'") let arr: [QueryParameter] = [strVal, noID] XCTAssertEqual(try QueryParameterArray(arr).queryParameter(option: queryOption).escaped(), "\'Sup\\\'er\'") } } func testDictionary() { let strVal: String = "Sup'er" let strValOptional: String? = "Sup'er Super" let strValOptionalNone: String? = nil let dict = QueryParameterDictionary([ "string": strVal, "stringOptional": strValOptional, "stringNone" : strValOptionalNone ]) let expectedResult = Set(arrayLiteral: "`string` = 'Sup\\'er'", "`stringOptional` = 'Sup\\'er Super'", "`stringNone` = NULL") let escaped = try! dict.queryParameter(option: queryOption).escaped() XCTAssertEqual(Set(escaped.split(separator: ",").map(String.init).map({ $0.trimmingCharacters(in: .whitespaces) })), expectedResult) } } ================================================ FILE: Tests/MySQLTests/Model.swift ================================================ // // Model.swift // MySQL // // Created by ito on 12/20/15. // Copyright © 2015 Yusuke Ito. All rights reserved. // import MySQL import Foundation struct UserID: IDType { let id: Int init(_ id: Int) { self.id = id } } struct BlobTextID: IDType { let id: Int init(_ id: Int) { self.id = id } } struct SomeStringID: IDType { let id: String init(_ id: String) { self.id = id } } final class Row { private init() { } } ================================================ FILE: Tests/MySQLTests/MySQLConnection.swift ================================================ // // MySQLTests.swift // MySQLTests // // Created by ito on 2015/10/24. // Copyright © 2015年 Yusuke Ito. All rights reserved. // import XCTest import MySQL import Foundation /* struct TestConstants: TestConstantsType { let host: String = "" let port: Int = 3306 let user: String = "" let password: String = "" let database: String = "test" let tableName: String = "unit_test_db_3894" // Unit test creates a table let encoding: Connection.Encoding = .UTF8MB4 let timeZone: Connection.TimeZone = Connection.TimeZone(GMTOffset: 60 * 60 * 9) // JST } */ struct DummyConstants: TestConstantsType { let host: String = "127.0.0.1" let port: Int = 3306 let user: String = "root" let password: String = "" let database: String = "test" let tableName: String = "unit_test_db_3894" let encoding: Connection.Encoding = .UTF8MB4 let timeZone: TimeZone = TimeZone(abbreviation: "JST")! // JST let reconnect: Bool = true } protocol TestConstantsType: ConnectionOption { var tableName: String { get } } protocol MySQLTestType: class { var constants: TestConstantsType! { get set } var pool: ConnectionPool! { get set } } extension MySQLTestType { func prepare() { self.constants = DummyConstants() // !!! Replace with your MySQL connection !!! self.pool = ConnectionPool(option: constants) XCTAssertEqual(constants.timeZone, TimeZone(abbreviation: "JST"), "test MySQL's timezone should be JST") } } ================================================ FILE: Tests/MySQLTests/QueryDecimalTypeTests.swift ================================================ // // QueryDecimalTypeTests.swift // MySQLTests // // Created by Yusuke Ito on 5/1/18. // import XCTest @testable import MySQL import Foundation extension Row { fileprivate struct DecimalRow: Codable, QueryParameter, Equatable { let valueDoubleColumn: Decimal let valueTextColumn: Decimal private enum CodingKeys: String, CodingKey { case valueDoubleColumn = "value_double_col" case valueTextColumn = "value_text_col" } } } final class QueryDecimalTypeTests: XCTestCase, QueryTestType { var constants: TestConstantsType! var pool: ConnectionPool! override func setUp() { super.setUp() prepare() try! createDecimalTestTable() } private func createDecimalTestTable() throws { try dropTestTable() let conn = try pool.getConnection() let query = """ CREATE TABLE `\(constants.tableName)` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `value_double_col` DOUBLE NOT NULL DEFAULT 0, `value_text_col` MEDIUMTEXT, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; """ _ = try conn.query(query) } func testDecimalType() throws { let value = Decimal(string: "1.549")! let row = Row.DecimalRow(valueDoubleColumn: value, valueTextColumn: value) try pool.execute { conn in _ = try conn.query("INSERT INTO ?? SET ? ", [constants.tableName, row]) } let rows: [Row.DecimalRow] = try pool.execute { try $0.query("SELECT * FROM ?? ORDER BY id ASC", [constants.tableName]) } XCTAssertEqual(rows[0], row) } func testDecimalType_largerValue() throws { let value = Decimal(string: "1.23e100")! let row = Row.DecimalRow(valueDoubleColumn: value, valueTextColumn: value) try pool.execute { conn in _ = try conn.query("INSERT INTO ?? SET ? ", [constants.tableName, row]) } let rows: [Row.DecimalRow] = try pool.execute { try $0.query("SELECT * FROM ?? ORDER BY id ASC", [constants.tableName]) } XCTAssertEqual(rows[0], row) } } ================================================ FILE: Tests/MySQLTests/QueryFormatterTests.swift ================================================ // // QueryFormatterTests.swift // MySQL // // Created by ito on 12/20/15. // Copyright © 2015 Yusuke Ito. All rights reserved. // import XCTest @testable import MySQL import SQLFormatter final class QueryFormatterTests: XCTestCase { fileprivate enum TableName: String, QueryRawRepresentableParameter { case user = "user" } func testBasicFormatting() throws { let params: (String, TableName, String, Int, String, Int?) = ( "i.d", TableName.user, "id", 1, "user's", nil ) let args: [QueryParameter] = [ params.0, params.1, params.2, params.3, params.4, params.5, ] let formatted = try QueryFormatter.format(query: "SELECT name,??,id FROM ?? WHERE ?? = ? OR name = ? OR age is ?;", parameters: Connection.buildParameters(args, option: queryOption) ) XCTAssertEqual(formatted, "SELECT name,`i`.`d`,id FROM `user` WHERE `id` = 1 OR name = 'user\\'s' OR age is NULL;") } func testLikeEscape() { XCTAssertEqual(SQLString.escapeForLike("ap%ple_"), "ap\\%ple\\_") XCTAssertEqual(SQLString.escapeForLike("ap\\%ple_"), "ap\\\\%ple\\_") XCTAssertEqual(SQLString.escapeForLike("ap%ple_", escapingWith: "$"), "ap$%ple$_") XCTAssertEqual(SQLString.escapeForLike("ap\\%ple_", escapingWith: "$"), "ap\\$%ple$_") } func testPlaceholder() throws { let params: [QueryParameter] = ["name", "message??", "col", "hello??", "hello?"] let formatted = try QueryFormatter.format(query: "SELECT ??, ?, ??, ?, ?", parameters: Connection.buildParameters(params, option: queryOption)) XCTAssertEqual(formatted, "SELECT `name`, 'message??', `col`, 'hello??', 'hello?'") } func testStringUtil() { let someString = "abcdefghijklmn12345" XCTAssertEqual(someString.subString(max: 10), "abcdefghij") XCTAssertEqual(someString.subString(max: 1000), someString) XCTAssertEqual("".subString(max: 10), "") } } ================================================ FILE: Tests/MySQLTests/QueryParameterTests.swift ================================================ // // QueryParameterTests.swift // MySQL // // Created by Yusuke Ito on 4/21/16. // Copyright © 2016 Yusuke Ito. All rights reserved. // import XCTest import MySQL import SQLFormatter // the URL as QueryParameter should be extension URL: QueryParameter { public func queryParameter(option: QueryParameterOption) throws -> QueryParameterType { return self.absoluteString.queryParameter(option: option) } } extension QueryParameterTests { static var allTests : [(String, (QueryParameterTests) -> () throws -> Void)] { return [ ("testIDType", testIDType), ("testIDTypeInContainer", testIDTypeInContainer), ("testEnumType", testEnumType), ("testAutoincrementType", testAutoincrementType), ("testDateComponentsType", testDateComponentsType), ("testDataAndURLType", testDataAndURLType), ("testDecimalType", testDecimalType), ("testCodableIDType", testCodableIDType), ("testCodableIDType_AutoincrementNoID", testCodableIDType_AutoincrementNoID) ] } } final class QueryParameterTests: XCTestCase { private struct IDInt: IDType { let id: Int init(_ id: Int) { self.id = id } } private struct IDString: IDType { let id: String init(_ id: String) { self.id = id } } private struct ModelWithIDType_StringAutoincrement: Encodable, QueryParameter { let idStringAutoincrement: AutoincrementID } private struct ModelWithIDType_IntAutoincrement: Encodable, QueryParameter { let idIntAutoincrement: AutoincrementID } private enum SomeEnumParameter: String, QueryRawRepresentableParameter { case first = "first 1" case second = "second' 2" } private enum SomeEnumCodable: String, Codable, QueryParameter { case first = "first 1" case second = "second' 2" } // https://developer.apple.com/documentation/swift/optionset private struct ShippingOptions: OptionSet, QueryRawRepresentableParameter { let rawValue: Int static let nextDay = ShippingOptions(rawValue: 1 << 0) static let secondDay = ShippingOptions(rawValue: 1 << 1) static let priority = ShippingOptions(rawValue: 1 << 2) static let standard = ShippingOptions(rawValue: 1 << 3) static let express: ShippingOptions = [.nextDay, .secondDay] static let all: ShippingOptions = [.express, .priority, .standard] } private struct ModelWithData: Encodable, QueryParameter { let data: Data } private struct ModelWithDate: Encodable, QueryParameter { let date: Date } private struct ModelWithDateComponents: Encodable, QueryParameter { let dateComponents: DateComponents } private struct ModelWithURL: Encodable, QueryParameter { let url: URL } private struct ModelWithDecimal: Encodable, QueryParameter { let value: Decimal } func testIDType() throws { let idInt: QueryParameter = IDInt(1234) XCTAssertEqual(try idInt.queryParameter(option: queryOption).escaped(), "1234") //let id: SomeID = try SomeID.fromSQLValue(string: "5678") //XCTAssertEqual(id.id, 5678) let idString: QueryParameter = IDString("123abc") XCTAssertEqual(try idString.queryParameter(option: queryOption).escaped(), "'123abc'") let idIntAutoincrement: QueryParameter = AutoincrementID(IDInt(1234)) XCTAssertEqual(try idIntAutoincrement.queryParameter(option: queryOption).escaped(), "1234") let idStringAutoincrement: QueryParameter = AutoincrementID(IDString("123abc")) XCTAssertEqual(try idStringAutoincrement.queryParameter(option: queryOption).escaped(), "'123abc'") } func testIDTypeInContainer() throws { do { let param: QueryParameter = ModelWithIDType_IntAutoincrement(idIntAutoincrement: .ID(IDInt(1234))) XCTAssertEqual(try param.queryParameter(option: queryOption).escaped(), "`idIntAutoincrement` = 1234") } do { let param: QueryParameter = ModelWithIDType_StringAutoincrement(idStringAutoincrement: .ID(IDString("123abc"))) XCTAssertEqual(try param.queryParameter(option: queryOption).escaped(), "`idStringAutoincrement` = '123abc'") } } func testEnumType() throws { do { let someVal: QueryParameter = SomeEnumParameter.second let escaped = "second' 2".escaped() XCTAssertEqual(try someVal.queryParameter(option: queryOption).escaped() , escaped) } do { let someVal: QueryParameter = SomeEnumCodable.second let escaped = "second' 2".escaped() XCTAssertEqual(try someVal.queryParameter(option: queryOption).escaped() , escaped) } do { let someOption: QueryParameter = ShippingOptions.all XCTAssertEqual(try someOption.queryParameter(option: queryOption).escaped() , "\(ShippingOptions.all.rawValue)") } } func testAutoincrementType() throws { let userID: AutoincrementID = .ID(UserID(333)) XCTAssertEqual(userID, AutoincrementID.ID(UserID(333))) let someStringID: AutoincrementID = .ID(SomeStringID("id678@")) XCTAssertEqual(someStringID, AutoincrementID.ID(SomeStringID("id678@"))) let noID: AutoincrementID = .noID XCTAssertEqual(noID, AutoincrementID.noID) } func testDateComponentsType() throws { do { let compsEmpty = DateComponents() let model = ModelWithDateComponents(dateComponents: compsEmpty) let _ = try model.queryParameter(option: queryOption).escaped() XCTFail("this should be throws an error") } catch { // OK } do { // MySQL YEAR type var comps = DateComponents() comps.year = 2155 let model = ModelWithDateComponents(dateComponents: comps) let queryString = try model.queryParameter(option: queryOption).escaped() XCTAssertEqual(queryString, "`dateComponents` = '2155'") } do { // MySQL TIME type var comps = DateComponents() comps.hour = -838 comps.minute = 59 comps.second = 59 let model = ModelWithDateComponents(dateComponents: comps) let queryString = try model.queryParameter(option: queryOption).escaped() XCTAssertEqual(queryString, "`dateComponents` = '-838:59:59'") } do { // MySQL TIME type // with nanosecond var comps = DateComponents() comps.hour = -838 comps.minute = 59 comps.second = 59 comps.nanosecond = 1234567 let model = ModelWithDateComponents(dateComponents: comps) let queryString = try model.queryParameter(option: queryOption).escaped() XCTAssertEqual(queryString, "`dateComponents` = '-838:59:59.001235'") } do { // MySQL DATETIME, TIMESTAMP type var comps = DateComponents() comps.year = 9999 comps.month = 12 comps.day = 31 comps.hour = 23 comps.minute = 59 comps.second = 59 let model = ModelWithDateComponents(dateComponents: comps) let queryString = try model.queryParameter(option: queryOption).escaped() XCTAssertEqual(queryString, "`dateComponents` = '9999-12-31 23:59:59'") } do { // MySQL DATETIME, TIMESTAMP type // with nanosecond var comps = DateComponents() comps.year = 9999 comps.month = 12 comps.day = 31 comps.hour = 23 comps.minute = 59 comps.second = 59 comps.nanosecond = 1234567 let model = ModelWithDateComponents(dateComponents: comps) let queryString = try model.queryParameter(option: queryOption).escaped() XCTAssertEqual(queryString, "`dateComponents` = '9999-12-31 23:59:59.001235'") } } func testDataAndURLType() throws { do { let dataModel = ModelWithData(data: Data([0x12, 0x34, 0x56, 0xff, 0x00])) let queryString = try dataModel.queryParameter(option: queryOption).escaped() XCTAssertEqual(queryString, "`data` = x'123456ff00'") } do { let urlModel = ModelWithURL(url: URL(string: "https://apple.com/iphone")!) let queryString = try urlModel.queryParameter(option: queryOption).escaped() XCTAssertEqual(queryString, "`url` = 'https://apple.com/iphone'") } do { let param: QueryParameter = URL(string: "https://apple.com/iphone")! let queryString = try param.queryParameter(option: queryOption).escaped() XCTAssertEqual(queryString, "'https://apple.com/iphone'") } } func testDecimalType() throws { let decimalModel = ModelWithDecimal(value: Decimal(1.2345e100)) let queryString = try decimalModel.queryParameter(option: queryOption).escaped() XCTAssertEqual(queryString, "`value` = '12345000000000010240000000000000000000000000000000000000000000000000000000000000000000000000000000000'") } private enum UserType: String, Codable { case user = "user" case admin = "admin" } private struct CodableModel: Codable, QueryParameter { let id: UserID let name: String let userType: UserType } private struct CodableModelWithAutoincrement: Codable, QueryParameter { let id: AutoincrementID let name: String let userType: UserType } func testCodableIDType() throws { let expectedResult = Set(arrayLiteral: "`id` = 123", "`name` = 'test4456'", "`userType` = 'user'") do { let parameter: QueryParameter = CodableModel(id: UserID(123), name: "test4456", userType: .user) let result = try parameter.queryParameter(option: queryOption).escaped() XCTAssertEqual(Set(result.split(separator: ",").map(String.init).map({ $0.trimmingCharacters(in: .whitespaces) })), expectedResult) } do { let parameter: QueryParameter = CodableModelWithAutoincrement(id: AutoincrementID(UserID(123)), name: "test4456", userType: .user) let result = try parameter.queryParameter(option: queryOption).escaped() XCTAssertEqual(Set(result.split(separator: ",").map(String.init).map({ $0.trimmingCharacters(in: .whitespaces) })), expectedResult) } } func testCodableIDType_AutoincrementNoID() throws { let expectedResult = Set(arrayLiteral: "`name` = 'test4456'", "`userType` = 'user'") let parameter: QueryParameter = CodableModelWithAutoincrement(id: .noID, name: "test4456", userType: .user) let result = try parameter.queryParameter(option: queryOption).escaped() XCTAssertEqual(Set(result.split(separator: ",").map(String.init).map({ $0.trimmingCharacters(in: .whitespaces) })), expectedResult) } } ================================================ FILE: Tests/MySQLTests/QueryTests.swift ================================================ // // QueryTests.swift // MySQL // // Created by ito on 12/20/15. // Copyright © 2015 Yusuke Ito. All rights reserved. // import XCTest @testable import MySQL import Foundation protocol QueryTestType: MySQLTestType { func dropTestTable() throws } extension QueryTestType { func dropTestTable() throws { let conn = try pool.getConnection() _ = try conn.query("DROP TABLE IF EXISTS \(constants.tableName)") } } extension Row { fileprivate struct SimpleUser: Codable, Equatable { let id: UInt let name: String let age: Int } fileprivate enum UserType: String, Codable { case user = "user" case admin = "admin" } fileprivate struct User: Codable, QueryParameter, Equatable { let id: AutoincrementID let name: String let age: Int let createdAt: Date let createdAtDateComponentsOptional: DateComponents? let nameOptional: String? let ageOptional: Int? let createdAtOptional: Date? let done: Bool let doneOptional: Bool? let userType: UserType private enum CodingKeys: String, CodingKey { case id case name case age case createdAt = "created_at" case createdAtDateComponentsOptional = "created_at_datecomponents_Optional" case nameOptional = "name_Optional" case ageOptional = "age_Optional" case createdAtOptional = "created_at_Optional" case done case doneOptional = "done_Optional" case userType = "user_type" } } } final class QueryTests: XCTestCase, QueryTestType { var constants: TestConstantsType! var pool: ConnectionPool! override func setUp() { super.setUp() prepare() try! createTestTable() } func createTestTable() throws { try dropTestTable() let conn = try pool.getConnection() let query = """ CREATE TABLE `\(constants.tableName)` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL DEFAULT '', `age` int(11) NOT NULL, `created_at` datetime NOT NULL DEFAULT '2001-01-01 00:00:00', `created_at_datecomponents_Optional` datetime(6) DEFAULT NULL, `name_Optional` varchar(50) DEFAULT NULL, `age_Optional` int(11) DEFAULT NULL, `created_at_Optional` datetime DEFAULT NULL, `done` tinyint(1) NOT NULL DEFAULT 0, `done_Optional` tinyint(1) DEFAULT NULL, `user_type` varchar(255) NOT NULL DEFAULT '', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; """ _ = try conn.query(query) } private var someDate: Date { return try! Date(sqlDate: "2015-12-27 16:54:00", timeZone: pool.option.timeZone) } private var anotherDate: Date { return Date(timeIntervalSinceReferenceDate: 60*60*24*67) } func testInsertRowCodable() throws { typealias User = Row.User let name = "name 's" let age = 25 let dateComponents = DateComponents(year: 2012, month: 3, day: 4, hour: 5, minute: 6, second: 7, nanosecond: 890_000_000) let userNil = User(id: .noID, name: name, age: age, createdAt: someDate, createdAtDateComponentsOptional: dateComponents, nameOptional: nil, ageOptional: nil, createdAtOptional: nil, done: false, doneOptional: nil, userType: .user) let status: QueryStatus = try pool.execute { conn in try conn.query("INSERT INTO ?? SET ? ", [constants.tableName, userNil]) } XCTAssertEqual(status.insertedID, 1) let userFill = User(id: .ID(UserID(134)), name: name, age: age, createdAt: someDate, createdAtDateComponentsOptional: dateComponents, nameOptional: "fuga", ageOptional: 50, createdAtOptional: anotherDate, done: true, doneOptional: false, userType: .admin) let status2: QueryStatus = try pool.execute { conn in try conn.query("INSERT INTO ?? SET ? ", [constants.tableName, userFill]) } XCTAssertEqual(status2.insertedID, 134) let rows:[User] = try pool.execute { conn in try conn.query("SELECT id,name,age,created_at,created_at_datecomponents_Optional,name_Optional,age_Optional,created_at_Optional,done,done_Optional,user_type FROM ?? ORDER BY id ASC", [constants.tableName]) } XCTAssertEqual(rows.count, 2) // first row XCTAssertEqual(rows[0].id.id, UserID(Int(status.insertedID))) XCTAssertEqual(rows[0].name, name) XCTAssertEqual(rows[0].age, age) XCTAssertEqual(rows[0].createdAt, someDate) XCTAssertNil(rows[0].nameOptional) XCTAssertNil(rows[0].ageOptional) XCTAssertNil(rows[0].createdAtOptional) XCTAssertFalse(rows[0].done) XCTAssertNil(rows[0].doneOptional) XCTAssertEqual(rows[0].userType, .user) XCTAssertEqual(rows[1], userFill) } func testTransaction() throws { let user = Row.User(id: .noID, name: "name", age: 11, createdAt: someDate, createdAtDateComponentsOptional: nil, nameOptional: nil, ageOptional: nil, createdAtOptional: nil, done: false, doneOptional: nil, userType: .user) let status: QueryStatus = try pool.transaction { conn in try conn.query("INSERT INTO ?? SET ? ", [constants.tableName, user]) } XCTAssertEqual(status.insertedID, 1) } func testEmojiInserting() throws { typealias User = Row.User let now = Date() let user = User(id: .noID, name: "日本語123🍣🍺あいう", age: 123, createdAt: now, createdAtDateComponentsOptional: nil, nameOptional: nil, ageOptional: nil, createdAtOptional: nil, done: false, doneOptional: nil, userType: .user) let status: QueryStatus = try pool.execute { conn in try conn.query("INSERT INTO ?? SET ? ", [constants.tableName, user]) } let rows: [User] = try pool.execute{ conn in try conn.query("SELECT id,name,age,created_at,name_Optional,age_Optional,created_at_Optional,done,done_Optional,user_type FROM ?? WHERE id = ?", [constants.tableName, status.insertedID]) } XCTAssertEqual(rows.count, 1) XCTAssertEqual(rows[0].name, user.name) XCTAssertEqual(rows[0].age, user.age) } func testBulkInsert() throws { //let now = Date() let users = (1...3).map({ row in Row.SimpleUser(id: UInt(10+row), name: "name\(row)", age: row) }) let usersParam: [QueryParameterArray] = users.map { user in QueryParameterArray([user.id, user.name, user.age]) } _ = try pool.execute { conn in try conn.query("INSERT INTO ??(id,name,age) VALUES ? ", [constants.tableName, QueryParameterArray(usersParam)]) } let fetchedUsers: [Row.SimpleUser] = try pool.execute { conn in try conn.query("SELECT id,name,age FROM ?? ORDER BY id DESC", [constants.tableName]) } XCTAssertEqual(fetchedUsers.count, 3) for index in (0..<3) { XCTAssertEqual(fetchedUsers[index], users.reversed()[index]) } } } ================================================ FILE: Tests/MySQLTests/QueryURLTypeTests.swift ================================================ // // QueryURLTypeTests.swift // MySQLTests // // Created by Yusuke Ito on 5/1/18. // import XCTest @testable import MySQL import Foundation extension Row { fileprivate struct URLRow: Codable, QueryParameter, Equatable { let url: URL let urlOptional: URL? private enum CodingKeys: String, CodingKey { case url = "url" case urlOptional = "url_Optional" } } } final class QueryURLTypeTests: XCTestCase, QueryTestType { var constants: TestConstantsType! var pool: ConnectionPool! override func setUp() { super.setUp() prepare() try! createURLTestTable() } private func createURLTestTable() throws { try dropTestTable() let conn = try pool.getConnection() let query = """ CREATE TABLE `\(constants.tableName)` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `url` mediumtext NOT NULL, `url_Optional` mediumtext, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; """ _ = try conn.query(query) } func testURLType() throws { let urlRow1 = Row.URLRow(url: URL(string: "https://apple.com/iphone")!, urlOptional: nil) let urlRow2 = Row.URLRow(url: URL(string: "https://apple.com/iphone")!, urlOptional: URL(string: "https://apple.com/ipad")!) try pool.execute { conn in _ = try conn.query("INSERT INTO ?? SET ? ", [constants.tableName, urlRow1]) _ = try conn.query("INSERT INTO ?? SET ? ", [constants.tableName, urlRow2]) } let rows: [Row.URLRow] = try pool.execute { try $0.query("SELECT * FROM ?? ORDER BY id ASC", [constants.tableName]) } XCTAssertEqual(rows[0], urlRow1) XCTAssertEqual(rows[1], urlRow2) } func testURLInvalid() throws { try pool.execute { conn in _ = try conn.query("INSERT INTO ?? SET `url` = ''", [constants.tableName]) } do { let _: [Row.URLRow] = try pool.execute { try $0.query("SELECT * FROM ?? ORDER BY id ASC", [constants.tableName]) } } catch let error as DecodingError { switch error { case .dataCorrupted(let context): print(context) // expected error default: XCTFail("unexpected error \(error)") } } catch { XCTFail("unexpected error \(error)") } } } ================================================ FILE: Tests/MySQLTests/XCTestManifests.swift ================================================ import XCTest extension BlobQueryTests { static let __allTests = [ ("testBlobAndTextOnBinCollation", testBlobAndTextOnBinCollation), ("testEscapeBlob", testEscapeBlob), ("testInsertForCombinedUnicodeCharacter", testInsertForCombinedUnicodeCharacter), ("testJSONColumnValue", testJSONColumnValue), ] } extension ConnectionPoolTests { static let __allTests = [ ("testExecutionBlock", testExecutionBlock), ("testGetConnection", testGetConnection), ("testThreadingConnectionPool", testThreadingConnectionPool), ] } extension ConnectionTests { static let __allTests = [ ("testConnect2", testConnect2), ("testConnect", testConnect), ("testDefaultConnectionOption", testDefaultConnectionOption), ] } extension DateTests { static let __allTests = [ ("testDateComponents", testDateComponents), ("testSQLCalendar", testSQLCalendar), ("testSQLDate", testSQLDate), ] } extension EscapeTests { static let __allTests = [ ("testArrayType", testArrayType), ("testAutoincrement", testAutoincrement), ("testBasicTypes", testBasicTypes), ("testDictionary", testDictionary), ("testNestedArray", testNestedArray), ("testStringEscape", testStringEscape), ] } extension QueryDecimalTypeTests { static let __allTests = [ ("testDecimalType_largerValue", testDecimalType_largerValue), ("testDecimalType", testDecimalType), ] } extension QueryFormatterTests { static let __allTests = [ ("testBasicFormatting", testBasicFormatting), ("testLikeEscape", testLikeEscape), ("testPlaceholder", testPlaceholder), ("testStringUtil", testStringUtil), ] } extension QueryParameterTests { static let __allTests = [ ("testAutoincrementType", testAutoincrementType), ("testCodableIDType_AutoincrementNoID", testCodableIDType_AutoincrementNoID), ("testCodableIDType", testCodableIDType), ("testDataAndURLType", testDataAndURLType), ("testDateComponentsType", testDateComponentsType), ("testDecimalType", testDecimalType), ("testEnumType", testEnumType), ("testIDType", testIDType), ("testIDTypeInContainer", testIDTypeInContainer), ] } extension QueryTests { static let __allTests = [ ("testBulkInsert", testBulkInsert), ("testEmojiInserting", testEmojiInserting), ("testInsertRowCodable", testInsertRowCodable), ("testTransaction", testTransaction), ] } extension QueryURLTypeTests { static let __allTests = [ ("testURLInvalid", testURLInvalid), ("testURLType", testURLType), ] } #if !os(macOS) public func __allTests() -> [XCTestCaseEntry] { return [ testCase(BlobQueryTests.__allTests), testCase(ConnectionPoolTests.__allTests), testCase(ConnectionTests.__allTests), testCase(DateTests.__allTests), testCase(EscapeTests.__allTests), testCase(QueryDecimalTypeTests.__allTests), testCase(QueryFormatterTests.__allTests), testCase(QueryParameterTests.__allTests), testCase(QueryTests.__allTests), testCase(QueryURLTypeTests.__allTests), ] } #endif ================================================ FILE: Tests/SQLFormatterTests/SQLFormatterTests.swift ================================================ // // SQLFormatterTests.swift // SQLFormatterTests // // Created by Yusuke Ito on 4/5/16. // Copyright © 2016 Yusuke Ito. All rights reserved. // import XCTest @testable import SQLFormatter class SQLFormattingTests: XCTestCase { func testDummy() throws { XCTAssertTrue(true) } } ================================================ FILE: Tests/SQLFormatterTests/XCTestManifests.swift ================================================ import XCTest extension SQLFormattingTests { static let __allTests = [ ("testDummy", testDummy), ] } #if !os(macOS) public func __allTests() -> [XCTestCaseEntry] { return [ testCase(SQLFormattingTests.__allTests), ] } #endif