Skip to content

Commit 4af9641

Browse files
authored
Merge branch 'master' into master
2 parents 97537e0 + be8566f commit 4af9641

File tree

7 files changed

+177
-17
lines changed

7 files changed

+177
-17
lines changed

Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ let package = Package(
3131
.library(name: "PerfectMySQL", targets: ["PerfectMySQL"])
3232
],
3333
dependencies: [
34-
.package(url: "https://github.com/PerfectlySoft/Perfect-CRUD.git", from: "1.0.0"),
34+
.package(url: "https://github.com/PerfectlySoft/Perfect-CRUD.git", from: "1.2.2"),
3535
.package(url: "https://github.com/PerfectlySoft/\(clientPackage).git", from: "2.0.0"),
3636
],
3737
targets: [

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ To install Home Brew:
5858
To install MySQL:
5959

6060
```
61-
brew install mysql
61+
brew install mysql@5.7
6262
```
6363

6464
Unfortunately, at this point in time you will need to edit the mysqlclient.pc file located here:

Sources/PerfectMySQL/MySQL.swift

+16-1
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,16 @@ public final class MySQL {
227227
return MYSQL_OPT_CAN_HANDLE_EXPIRED_PASSWORDS
228228
}
229229
}
230-
230+
231+
func exposedOptionToMySQLServerOption(_ o: MySQLServerOpt) -> enum_mysql_set_option {
232+
switch o {
233+
case MySQLServerOpt.MYSQL_OPTION_MULTI_STATEMENTS_ON:
234+
return MYSQL_OPTION_MULTI_STATEMENTS_ON
235+
case MySQLServerOpt.MYSQL_OPTION_MULTI_STATEMENTS_OFF:
236+
return MYSQL_OPTION_MULTI_STATEMENTS_OFF
237+
}
238+
}
239+
231240
/// Sets connect options for connect()
232241
@discardableResult
233242
public func setOption(_ option: MySQLOpt) -> Bool {
@@ -258,6 +267,12 @@ public final class MySQL {
258267
return b
259268
}
260269

270+
/// Sets server option (must be set after connect() is called)
271+
@discardableResult
272+
public func setServerOption(_ option: MySQLServerOpt) -> Bool {
273+
return mysql_set_server_option(mysqlPtr, exposedOptionToMySQLServerOption(option)) == 0
274+
}
275+
261276
/// Class used to manage and interact with result sets
262277
public final class Results: IteratorProtocol {
263278
var ptr: UnsafeMutablePointer<MYSQL_RES>

Sources/PerfectMySQL/MySQLCRUD.swift

+37-4
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,16 @@ class MySQLCRUDRowReader<K : CodingKey>: KeyedDecodingContainerProtocol {
5858
switch a {
5959
case let i as Int64:
6060
return Int(i)
61+
case let i as Int32:
62+
return Int(i)
63+
case let i as Int16:
64+
return Int(i)
65+
case let i as Int8:
66+
return Int(i)
6167
case let i as Int:
6268
return i
6369
default:
64-
throw MySQLCRUDError("Could not convert \(String(describing: a)) into an Int.")
70+
throw MySQLCRUDError("Could not convert \(String(describing: a)) into an Int for key: \(key.stringValue)")
6571
}
6672
}
6773
func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 {
@@ -81,10 +87,16 @@ class MySQLCRUDRowReader<K : CodingKey>: KeyedDecodingContainerProtocol {
8187
switch a {
8288
case let i as UInt64:
8389
return UInt(i)
90+
case let i as UInt32:
91+
return UInt(i)
92+
case let i as UInt16:
93+
return UInt(i)
94+
case let i as UInt8:
95+
return UInt(i)
8496
case let i as UInt:
8597
return i
8698
default:
87-
throw MySQLCRUDError("Could not convert \(String(describing: a)) into an UInt.")
99+
throw MySQLCRUDError("Could not convert \(String(describing: a)) into an UInt for key: \(key.stringValue)")
88100
}
89101
}
90102
func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 {
@@ -133,6 +145,11 @@ class MySQLCRUDRowReader<K : CodingKey>: KeyedDecodingContainerProtocol {
133145
throw CRUDDecoderError("Invalid Date string \(String(describing: val)).")
134146
}
135147
return date as! T
148+
case .url:
149+
guard let str = val as? String, let url = URL(string: str) else {
150+
throw CRUDDecoderError("Invalid URL string \(String(describing: val)).")
151+
}
152+
return url as! T
136153
case .codable:
137154
guard let data = (val as? String)?.data(using: .utf8) else {
138155
throw CRUDDecoderError("Unsupported type: \(type) for key: \(key.stringValue)")
@@ -180,6 +197,10 @@ class MySQLGenDelegate: SQLGenDelegate {
180197
database = db
181198
}
182199

200+
func getEmptyInsertSnippet() -> String {
201+
return "() VALUES ()"
202+
}
203+
183204
func getBinding(for expr: Expression) throws -> String {
184205
bindings.append(("?", expr))
185206
return "?"
@@ -314,6 +335,8 @@ class MySQLGenDelegate: SQLGenDelegate {
314335
typeName = "varchar(36)"
315336
case .date:
316337
typeName = "datetime"
338+
case .url:
339+
typeName = "longtext"
317340
case .codable:
318341
typeName = "json"
319342
}
@@ -416,6 +439,8 @@ class MySQLStmtExeDelegate: SQLExeDelegate {
416439
statement.bindParam(b ? 1 : 0)
417440
case .date(let d):
418441
statement.bindParam(d.mysqlFormatted())
442+
case .url(let u):
443+
statement.bindParam(u.absoluteString)
419444
case .uuid(let u):
420445
statement.bindParam(u.uuidString)
421446
case .null:
@@ -521,5 +546,13 @@ extension Date {
521546
}
522547
}
523548

524-
525-
549+
public extension Insert {
550+
func lastInsertId() throws -> UInt64? {
551+
let exeDelegate = try databaseConfiguration.sqlExeDelegate(forSQL: "SELECT LAST_INSERT_ID()")
552+
guard try exeDelegate.hasNext(), let next: KeyedDecodingContainer<ColumnKey> = try exeDelegate.next() else {
553+
throw CRUDSQLGenError("Did not get return value from statement \"SELECT LAST_INSERT_ID()\".")
554+
}
555+
let value = try next.decode(UInt64.self, forKey: ColumnKey(stringValue: "LAST_INSERT_ID()")!)
556+
return value
557+
}
558+
}

Sources/PerfectMySQL/MySQLStmt.swift

+6-2
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public final class MySQLStmt {
100100
MYSQL_TYPE_MEDIUM_BLOB,
101101
MYSQL_TYPE_LONG_BLOB,
102102
MYSQL_TYPE_BLOB:
103-
if (field.pointee.flags & UInt32(BINARY_FLAG)) != 0 {
103+
if field.pointee.charsetnr == 63 /* binary */ {
104104
return .bytes
105105
}
106106
fallthrough
@@ -582,7 +582,7 @@ public final class MySQLStmt {
582582
MYSQL_TYPE_MEDIUM_BLOB,
583583
MYSQL_TYPE_LONG_BLOB,
584584
MYSQL_TYPE_BLOB:
585-
if ( (field.pointee.flags & UInt32(BINARY_FLAG)) != 0) {
585+
if field.pointee.charsetnr == 63 /* binary */ {
586586
return .bytes(type)
587587
}
588588
fallthrough
@@ -761,7 +761,11 @@ public final class MySQLStmt {
761761
}
762762

763763
private func bind() {
764+
// empty buffer shared by .bytes, .string, .date, .null types
764765
let scratch = UnsafeMutableRawPointer(UnsafeMutablePointer<Int8>.allocate(capacity: 0))
766+
defer {
767+
scratch.deallocate()
768+
}
765769
for i in 0..<numFields {
766770
guard let field = mysql_fetch_field_direct(meta, UInt32(i)) else {
767771
continue

Sources/PerfectMySQL/PerfectMySQL.swift

+4
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,8 @@ public enum MySQLOpt {
112112
MYSQL_OPT_CAN_HANDLE_EXPIRED_PASSWORDS
113113
}
114114

115+
/// enum for mysql server options
116+
public enum MySQLServerOpt {
117+
case MYSQL_OPTION_MULTI_STATEMENTS_ON, MYSQL_OPTION_MULTI_STATEMENTS_OFF
118+
}
115119

Tests/PerfectMySQLTests/PerfectMySQLTests.swift

+112-8
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ import XCTest
2222
import PerfectCRUD
2323

2424
let testDBRowCount = 5
25+
#if os(macOS)
2526
let testHost = "127.0.0.1"
27+
#else
28+
let testHost = "host.docker.internal"
29+
#endif
2630
let testUser = "root"
2731
let testPassword = ""
2832
let testDB = "test"
@@ -280,8 +284,7 @@ class PerfectMySQLTests: XCTestCase {
280284
func testQueryStmt2() {
281285
let mysql = rawMySQL
282286
XCTAssert(mysql.query(statement: "DROP TABLE IF EXISTS all_data_types"))
283-
284-
let qres = mysql.query(statement: "CREATE TABLE `all_data_types` (`varchar` VARCHAR( 32 ),\n`tinyint` TINYINT,\n`text` TEXT,\n`date` DATE,\n`smallint` SMALLINT,\n`mediumint` MEDIUMINT,\n`int` INT,\n`bigint` BIGINT,\n`ubigint` BIGINT UNSIGNED,\n`float` FLOAT( 10, 2 ),\n`double` DOUBLE,\n`decimal` DECIMAL( 10, 2 ),\n`datetime` DATETIME,\n`timestamp` TIMESTAMP,\n`time` TIME,\n`year` YEAR,\n`char` CHAR( 10 ),\n`tinyblob` TINYBLOB,\n`tinytext` TINYTEXT,\n`blob` BLOB,\n`mediumblob` MEDIUMBLOB,\n`mediumtext` MEDIUMTEXT,\n`longblob` LONGBLOB,\n`longtext` LONGTEXT,\n`enum` ENUM( '1', '2', '3' ),\n`set` SET( '1', '2', '3' ),\n`bool` BOOL,\n`binary` BINARY( 20 ),\n`varbinary` VARBINARY( 20 ) ) ENGINE = MYISAM")
287+
let qres = mysql.query(statement: "CREATE TABLE `all_data_types` (`varchar` VARCHAR( 22 ),\n`tinyint` TINYINT,\n`text` TEXT,\n`date` DATE,\n`smallint` SMALLINT,\n`mediumint` MEDIUMINT,\n`int` INT,\n`bigint` BIGINT,\n`ubigint` BIGINT UNSIGNED,\n`float` FLOAT( 10, 2 ),\n`double` DOUBLE,\n`decimal` DECIMAL( 10, 2 ),\n`datetime` DATETIME,\n`timestamp` TIMESTAMP,\n`time` TIME,\n`year` YEAR,\n`char` CHAR( 10 ),\n`tinyblob` TINYBLOB,\n`tinytext` TINYTEXT,\n`blob` BLOB,\n`mediumblob` MEDIUMBLOB,\n`mediumtext` MEDIUMTEXT,\n`longblob` LONGBLOB,\n`longtext` LONGTEXT,\n`enum` ENUM( '1', '2', '3' ),\n`set` SET( '1', '2', '3' ),\n`bool` BOOL,\n`binary` BINARY( 20 ),\n`varbinary` VARBINARY( 20 ) ) ENGINE = MYISAM")
285288
XCTAssert(qres == true, mysql.errorMessage())
286289

287290
for _ in 1...2 {
@@ -290,7 +293,7 @@ class PerfectMySQLTests: XCTestCase {
290293
XCTAssert(prepRes, stmt1.errorMessage())
291294
XCTAssert(stmt1.paramCount() == 29)
292295

293-
stmt1.bindParam("varchar 20 string 👻")
296+
stmt1.bindParam("varchar ’22’ string 👻")
294297
stmt1.bindParam(1)
295298
stmt1.bindParam("text string")
296299
stmt1.bindParam("2015-10-21")
@@ -341,7 +344,7 @@ class PerfectMySQLTests: XCTestCase {
341344
let ok = results.forEachRow {
342345
e in
343346

344-
XCTAssertEqual(e[0] as? String, "varchar 20 string 👻")
347+
XCTAssertEqual(e[0] as? String, "varchar ’22’ string 👻")
345348
XCTAssertEqual(e[1] as? Int8, 1)
346349
XCTAssertEqual(e[2] as? String, "text string")
347350
XCTAssertEqual(e[3] as? String, "2015-10-21")
@@ -1016,12 +1019,13 @@ class PerfectMySQLTests: XCTestCase {
10161019
do {
10171020
let db = try getTestDB()
10181021
let t1 = db.table(TestTable1.self)
1019-
let newOne = TestTable1(id: 2000, name: "New One", integer: 40)
1022+
let newOne = TestTable1(id: 2000, name: "New ` One", integer: 40)
10201023
try t1.insert(newOne)
10211024
let j1 = t1.where(\TestTable1.id == newOne.id)
10221025
let j2 = try j1.select().map {$0}
10231026
XCTAssertEqual(try j1.count(), 1)
10241027
XCTAssertEqual(j2[0].id, 2000)
1028+
XCTAssertEqual(j2[0].name, "New ` One")
10251029
} catch {
10261030
XCTFail("\(error)")
10271031
}
@@ -1671,12 +1675,109 @@ class PerfectMySQLTests: XCTestCase {
16711675
XCTFail("\(error)")
16721676
}
16731677
}
1678+
1679+
func testIntConversion() {
1680+
do {
1681+
let db = try getTestDB()
1682+
struct IntTest: Codable {
1683+
let id: Int
1684+
}
1685+
try db.sql("CREATE TABLE IntTest(id tinyint PRIMARY KEY)")
1686+
let table = db.table(IntTest.self)
1687+
let inserted = IntTest(id: 1)
1688+
try table.insert(inserted)
1689+
guard let selected = try db.table(IntTest.self).where(\IntTest.id == 1).first() else {
1690+
return XCTFail("Unable to find IntTest.")
1691+
}
1692+
XCTAssertEqual(selected.id, inserted.id)
1693+
} catch {
1694+
XCTFail("\(error)")
1695+
}
1696+
}
16741697

16751698
func testBespokeSQL() {
16761699
do {
16771700
let db = try getTestDB()
1678-
let r = try db.sql("SELECT * FROM \(TestTable1.CRUDTableName) WHERE id = 2", TestTable1.self)
1679-
XCTAssertEqual(r.count, 1)
1701+
do {
1702+
let r = try db.sql("SELECT * FROM \(TestTable1.CRUDTableName) WHERE id = 2", TestTable1.self)
1703+
XCTAssertEqual(r.count, 1)
1704+
}
1705+
do {
1706+
let r = try db.sql("SELECT * FROM \(TestTable1.CRUDTableName)", TestTable1.self)
1707+
XCTAssertEqual(r.count, 5)
1708+
}
1709+
} catch {
1710+
XCTFail("\(error)")
1711+
}
1712+
}
1713+
1714+
func testURL() {
1715+
do {
1716+
let db = try getTestDB()
1717+
struct TableWithURL: Codable {
1718+
let id: Int
1719+
let url: URL
1720+
}
1721+
try db.create(TableWithURL.self)
1722+
let t1 = db.table(TableWithURL.self)
1723+
let newOne = TableWithURL(id: 2000, url: URL(string: "http://localhost/")!)
1724+
try t1.insert(newOne)
1725+
let j1 = t1.where(\TableWithURL.id == newOne.id)
1726+
let j2 = try j1.select().map {$0}
1727+
XCTAssertEqual(try j1.count(), 1)
1728+
XCTAssertEqual(j2[0].id, 2000)
1729+
XCTAssertEqual(j2[0].url.absoluteString, "http://localhost/")
1730+
} catch {
1731+
XCTFail("\(error)")
1732+
}
1733+
}
1734+
1735+
func testLastInsertId() {
1736+
do {
1737+
let db = try getTestDB()
1738+
struct ReturningItem: Codable, Equatable {
1739+
let id: UInt64?
1740+
var def: Int?
1741+
init(id: UInt64, def: Int? = nil) {
1742+
self.id = id
1743+
self.def = def
1744+
}
1745+
}
1746+
try db.sql("DROP TABLE IF EXISTS \(ReturningItem.CRUDTableName)")
1747+
try db.sql("CREATE TABLE \(ReturningItem.CRUDTableName) (id INT PRIMARY KEY AUTO_INCREMENT, def INT DEFAULT 42)")
1748+
let table = db.table(ReturningItem.self)
1749+
let id = try table
1750+
.insert(ReturningItem(id: 0, def: 0),
1751+
ignoreKeys: \ReturningItem.id)//, \ReturningItem.def)
1752+
.lastInsertId()
1753+
XCTAssertEqual(id, 1)
1754+
1755+
} catch {
1756+
XCTFail("\(error)")
1757+
}
1758+
}
1759+
1760+
func testEmptyInsert() {
1761+
do {
1762+
let db = try getTestDB()
1763+
struct ReturningItem: Codable, Equatable {
1764+
let id: Int?
1765+
var def: Int?
1766+
init(id: Int, def: Int? = nil) {
1767+
self.id = id
1768+
self.def = def
1769+
}
1770+
}
1771+
try db.sql("DROP TABLE IF EXISTS \(ReturningItem.CRUDTableName)")
1772+
try db.sql("CREATE TABLE \(ReturningItem.CRUDTableName) (id INT PRIMARY KEY AUTO_INCREMENT, def INT DEFAULT 42)")
1773+
let table = db.table(ReturningItem.self)
1774+
1775+
let id = try table
1776+
.insert(ReturningItem(id: 0, def: 0),
1777+
ignoreKeys: \ReturningItem.id, \ReturningItem.def)
1778+
.lastInsertId()
1779+
XCTAssertEqual(id, 1)
1780+
16801781
} catch {
16811782
XCTFail("\(error)")
16821783
}
@@ -1729,7 +1830,10 @@ class PerfectMySQLTests: XCTestCase {
17291830
("testBadDecoding", testBadDecoding),
17301831
("testAllPrimTypes1", testAllPrimTypes1),
17311832
("testAllPrimTypes2", testAllPrimTypes2),
1732-
("testBespokeSQL", testBespokeSQL)
1833+
("testBespokeSQL", testBespokeSQL),
1834+
("testURL", testURL),
1835+
("testLastInsertId", testLastInsertId),
1836+
("testEmptyInsert", testEmptyInsert)
17331837
]
17341838
}
17351839

0 commit comments

Comments
 (0)