Repository navigation
The first release since 3.5.1 (December 2019). It is the Swift 6 rewrite of the package on top of Perfect-CRUD 3, and it fixes a run of correctness bugs found since: nested transactions, statements MySQL can't prepare, binary and non-UTF-8 values that came back truncated, table creation order for related tables, .reconcileTable data loss, identifier quoting, and building against MariaDB Connector/C. It is a major version: some raw API results change type and several behaviours change. Read "Breaking and behaviour changes" before upgrading.
Requirements
- Swift tools 6.2 (
// swift-tools-version: 6.2); 3.5.1 declared 4.0. - Platforms: macOS 12+ and Linux. No iOS (the client library isn't available there).
- The MySQL client library, found through
pkg-configasmysqlclient: Oracle's libmysqlclient 8.0 or newer (Homebrewmysql-clienton macOS, keg-only, so setPKG_CONFIG_PATH;libmysqlclient-devon Linux), or MariaDB Connector/C through Debian/Ubuntu'slibmariadb-dev-compat. The separatePerfect-mysqlclient/Perfect-mysqlclient-Linuxpackages are no longer used. - Perfect-CRUD 3.0.1 or newer (3.0.1 fixes four crashes in 3.0.0).
.package(url: "https://github.com/PerfectlySoft/Perfect-MySQL.git", from: "4.0.0")This release adds a GitHub Actions workflow for every push and pull request: Linux (Swift 6.2 and 6.4 with libmysqlclient, plus Swift 6.4 with MariaDB Connector/C, debug and release) against a MySQL 8.4 service container with the live tests required, in non-UTC time zones, and macOS (Xcode 26.0.1 and 26.6, debug and release) with the server-independent tests. Warnings are errors everywhere.
Highlights
- Swift 6 (#60). Builds in the Swift 6 language mode.
MySQL,MySQLStmt, bothResultsclasses andMySQLDatabaseConfigurationare@unchecked Sendable(still one task at a time per connection);MySQLOpt,MySQLServerOptandMySQLCRUDErrorareSendable. The client library is wrapped by an inline system-library target instead of a separate package. - Perfect-CRUD 3 support (#60), including its Dynamic API:
Database.select(DynamicQuery),mutate(DynamicMutation)(with affected-row counts and inserted IDs) andexecute(DynamicSQL)work on MySQL. - Nested transactions (#61). Perfect-CRUD 3's nested
transaction {}usesSAVEPOINT, which MySQL can't prepare; these statements now run directly. - Exact bytes from query results (#65).
MySQL.ResultsgainsnextBytes(),forEachRowBytes(callback:)andfieldIsBinary(at:). - TLS modes with MariaDB Connector/C (#67).
setOption(.MYSQL_OPT_SSL_MODE, …)works when built against Connector/C, mapped onto its own options (see the limitations below). - Related tables are created in dependency order (#68), so
create(Parent.self)works when a sub-table has a@ForeignKeyback to the parent.
Breaking and behaviour changes
- Toolchain, platforms and the client library (#60). Swift 6.2+, macOS 12+. Upgrade: install the client library and
pkg-config(on macOS,brew install mysql-client pkg-configand exportPKG_CONFIG_PATH=$(brew --prefix mysql-client)/lib/pkgconfig), and drop any direct dependency onPerfect-mysqlclient. - Perfect-CRUD 3 (#60). Its own breaking changes apply (for example,
Expressionis nowCRUDExpression). Upgrade: follow the Perfect-CRUD 3.0.0 release notes. - Binary values from
MySQLStmtare[UInt8](#64, #65). BINARY, VARBINARY, BIT and GEOMETRY columns, binary expressions (CAST(x AS BINARY),CHAR(n)withoutUSING,x'..'literals,UNHEX,FROM_BASE64,UUID_TO_BIN,INET6_ATON,CONCATwith a binary operand) and every string column afterSET NAMES binaryused to come back asString, usually truncated or empty. JSON and binary collations such asutf8mb4_binstay text. Upgrade: where you cast a rawMySQLStmtvalue toString, cast to[UInt8]and useString(decoding:as: UTF8.self), orCAST(… AS CHAR)in SQL. Through Perfect-CRUD nothing changes forString,UUID,Date,URLor Codable properties over binary columns, and[UInt8]/Dataproperties over VARBINARY now decode instead of reading as empty. AStringproperty over a BLOB column now decodes the text instead of"". - Text is no longer truncated at invalid UTF-8 (#65).
MySQL.Results.next()andMySQLStmt's text path stopped at the first invalid byte (SELECT X'61FF62'returned"a"); they now return the whole value with U+FFFD for each invalid sequence. Upgrade: usenextBytes()where you need the exact bytes;fieldIsBinary(at:)says which columns are binary. - Statements MySQL can't prepare run directly (#61).
BEGIN,START,COMMIT,ROLLBACK,SAVEPOINT,RELEASE,LOCK,UNLOCKandUSEare matched on their first keyword, ignoring case and leading whitespace, and anything else the server refuses to prepare (error 1295) falls back to direct execution. UppercaseCREATE/DROP/ALTERstill run directly; lowercase DDL is still prepared, so it can take bindings. If such a statement returns rows (CHECK TABLE,XA RECOVER), it now throws instead of silently returning nothing. Upgrade: run row-returning admin statements withMySQL.query(statement:)andstoreResults(). setOptionis stricter (#67).MYSQL_OPT_SSL_MODEaccepts only 1...5 (libmysqlclient accepted 0 and 6), and an integer option that doesn't fit aUInt32, such as -1, returnsfalseinstead of trapping. Upgrade: check the return value.- Options survive a failed connect (#67).
connect()always passesCLIENT_REMEMBER_OPTIONS; both client libraries otherwise reset every option after a failed attempt, so a retry used to run without them (including the SSL mode). Upgrade: nothing, unless you relied on a retry dropping options. - SSL modes on MariaDB Connector/C (#67). REQUIRED is enforced only after authenticating: Connector/C falls back to plaintext with a non-TLS server, so
connect()then closes the connection and fails with error 2026, by which point an attacker able to tamper with the connection could have captured the authentication exchange (or the password, if the server asks formysql_clear_password). REQUIRED also turns offMYSQL_OPT_RECONNECT. VERIFY_CA also checks the host name (Connector/C 3.4 skips the host-name check, and withoutMYSQL_OPT_SSL_CAthe CA check, on local connections). DISABLED still uses TLS if anyMYSQL_OPT_SSL_*file or cipher option is set. With libmysqlclient nothing changes. Upgrade: on Connector/C use VERIFY_IDENTITY withMYSQL_OPT_SSL_CA, which fails before authenticating. - Table creation order (#68).
create()emits each table after the tables it references within the set (sub-tables used to come first, so a child's FOREIGN KEY to the parent failed); with.dropTable, all of them are dropped first in reverse order. A child's FOREIGN KEY clause no longer leaks into the parent's CREATE TABLE. Upgrade: nothing; mutually referencing tables still can't be created withcreate(). .reconcileTable(#68, #69). It no longer drops and re-adds existing columns whose names contain capital letters (they lost their data, or the DROP failed for a@ForeignKeycolumn). A@ForeignKeycolumn it adds gets its FOREIGN KEY constraint in the sameALTER TABLE, so the ALTER now fails if existing rows violate it. A@ForeignKeycolumn removed from the model has its constraint dropped with it (this failed with error 1828 and left the schema half-reconciled). Upgrade: clean up orphaned rows before reconciling a model that gains a foreign key; reconcile is still not atomic.- Dates are read as UTC (#72, #73). Perfect-CRUD has always written
Datevalues as UTC text, but 3.5.1 parsed them in the process's local time zone. Outside UTC, dates came back shifted by the UTC offset, andselect()silently stopped (dropping that row and every later one) when a row's UTC time fell in a local daylight-saving gap (for example2026-03-08 02:30:00under America/Denver), or when the column heldDATETIME(6)fractions or aDATE. Dates are now parsed as UTC, fractions andDATEvalues are read, and the zero date reads as nil for an optionalDate(a non-optional one is a decoding error, and Perfect-CRUD'sselect()stops at such a row, dropping it and every later one). Year 0 is accepted, soDate.distantPast(0000-12-30) round-trips, and aDateMySQL can't store now throws when bound: only non-finite dates used to crash, while years outside 0–9999 were written and then rejected by the server. The error comes frominsert,update,count()andsql; in aselect()whosewherecompares against such aDate, Perfect-CRUD catches it, logs it and returns no rows ([]). Dates also use the proleptic Gregorian calendar, as the server does (Foundation's calendar put dates before 1582-10-15 10 or more days off and rejected 1582-10-05…14), and fractions stay inside their second, so9999-12-31 23:59:59.999999no longer becomes year 10000 (written back as9999-12-31 23:59:59; dates are written in whole seconds, truncated). Upgrade: rows written through Perfect-CRUD on or after 1582-10-15 read back correctly with no migration. Dates before 1582-10-15 that 3.x wrote (it used Foundation's Julian-before-1582 calendar both ways, and storedDate.distantPastas0001-01-01) now read back 2 or more days different. Otherwise only apps that compensated for the old shift by hand, and local-time values written outside Perfect-CRUD (NOW()/CURRENT_TIMESTAMPon a server whose time zone isn't UTC,TIMESTAMPcolumns read in a non-UTC sessiontime_zone), now shift; run those connections in UTC (for exampleSET time_zone = '+00:00') if you need them to line up. The connector doesn't set it for you. - CALL and multiple result sets (#72, #73). A
CALLthrough Perfect-CRUD now returns the procedure's first result set (it read every column as missing), and the remaining result sets are discarded, so the connection doesn't fail later statements with "Commands out of sync". An error the procedure raises after its first result set (a failing INSERT,SIGNAL) now throws from typeddb.sql(_:_:)and Dynamic calls instead of being dropped; untypeddb.sql("CALL …")reads only the first result, so it can only log such an error. Upgrade: if a procedure returns several result sets you need, read them withMySQLStmtdirectly. - Counts and insert IDs (#72, #73).
MySQL.numberAffectedRows()returns -1 after a failed query or beforestoreResults(), as the C API does, andlastInsertId()returns an ID aboveInt64.maxas its bit pattern; both trapped. Through Perfect-CRUD's Dynamic API, a statement that returns rows reports their count asaffectedRowsandinsertedIDnil; an insert ID aboveInt64.maxis reported as nil. Upgrade: useUInt64(bitPattern:)for BIGINT UNSIGNED auto-increment IDs fromlastInsertId(). - Errors while reading rows throw (#72, #73). A query that fails partway through its rows used to return the rows read so far (none, in practice) through Perfect-CRUD; it now throws
MySQLCRUDError. Upgrade: nothing, unless you relied on the empty result. - DECIMAL columns decode into
Double/Float(#72, #73). They read as 0.0; text that doesn't parse now throws. Upgrade: nothing. - Duplicate column names (#72, #73). When a result has several columns with the same name, a Codable model or a Dynamic row now reads the first one; Dynamic rows also treat names that differ only by case as the same name. Upgrade: alias columns (
c.id AS child_id) when you need both. listTables/listDatabaseswildcards (#72, #73; security). The wildcard could end the string literal and, withCLIENT_MULTI_STATEMENTS, run SQL: on a MariaDB Connector/C build with any'(Connector/C'smysql_list_tablesdoesn't escape at all); with libmysqlclient under big5, cp932, gbk, gb18030 or sjis (its escaping is byte-by-byte, so a non-ASCII byte can swallow the backslash); and with either library after a stored routine changedsql_mode, which leaves the client'sNO_BACKSLASH_ESCAPESbit stale. The query is now built so it doesn't depend on that bit: a wildcard with'has each'doubled and nothing else changed; any other wildcard is escaped by the client library (mysql_real_escape_string_quote/mysql_real_escape_string); and these wildcards are refused (the result is[]): any containing both'and\, and under those five charsets any with a non-ASCII byte and a'or\. Ordinary wildcards work in bothNO_BACKSLASH_ESCAPESmodes. Upgrade: a wildcard combining'and\now returns[]; set the charset withMYSQL_SET_CHARSET_NAMErather thanSET NAMES.- Identifier quoting (#69, #71).
quote(identifier:)doubles every backtick, per Unicode scalar, so table and field names passed through Perfect-CRUD's Dynamic API can't close the quoted identifier and inject SQL. This assumes a utf8mb4 connection, which thedatabase:host:initializers set. Upgrade: if you buildMySQLDatabaseConfiguration(connection:)from a connection using gbk, big5 or gb18030, don't pass untrusted identifiers through it.
Fixes
- #61: Nested transactions,
LOCK/UNLOCK TABLES,START TRANSACTIONandUSEwork through Perfect-CRUD (from #56). The package builds against MariaDB Connector/C (themy_boolshim from the Swift 6 rewrite redefined Connector/C's own typedef).MySQLStmt.fieldNames()no longer leaks aMYSQL_RESper prepared CRUD statement. AMySQLStmtkeeps itsMySQLconnection alive. - #62: Cached result metadata is refreshed per result set (Connector/C crashed with SIGBUS after
nextResult()), andresults()on aCALLno longer crashes without a priorfieldNames(). - #63, #66: Three tests that failed against current servers (a Perfect-CRUD cache-key collision, since fixed in Perfect-CRUD, and a test that ignored the port).
- #64: BINARY/VARBINARY values are bytes instead of truncated strings.
- #65: No truncation at invalid UTF-8; byte access for
MySQL.Results; BIT and GEOMETRY are bytes. Decoding a 3 MB value went from about 1.2 s to about 3 ms in release. - #67:
MYSQL_OPT_SSL_MODEwith Connector/C; options kept across a failed connect; out-of-range integer options no longer trap. - #68: Parent tables are created before sub-tables that reference them;
.reconcileTablekeeps mixed-case columns and adds FK constraints for new columns. - #69:
.reconcileTabledrops a removed@ForeignKeycolumn's constraint; backticks are escaped in identifiers. - #70: The live tests skip unless
MYSQL_TEST_PORTnames a test server and the host isn'tlocalhost(they could otherwise drop databases on a local server on 3306). - #71: Backticks are escaped per Unicode scalar, closing an identifier injection through the Dynamic API.
- #72, #73:
DynamicMutationthrough a Connector/C build trapped ("Not enough bits to represent the passed value"), because the affected-row count was read after the statement was reset, which Connector/C reports as -1. The count and inserted ID are now read right after execution. Reproduced on Linux withlibmariadb-dev-compatbefore the fix; a regression test covers it. - #72, #73:
Dateparsing in UTC, with fractions,DATEvalues and zero dates (see above). - #72, #73:
numberAffectedRows()andlastInsertId()no longer trap on the C API's -1 sentinel or IDs aboveInt64.max. - #72, #73:
CALLreturns its rows and leftover result sets are drained, also when a caller stops reading early, so a connection (including a pooled one) isn't left out of sync. - #72, #73: Dynamic statements that return rows report the row count and no stale insert ID (on Connector/C the insert ID is the connection's).
- #72, #73: DECIMAL columns decode into
DoubleandFloatproperties. - #72, #73: a statement that fails while its rows are being read (for example a scalar subquery returning more than one row, error 1242) crashed with SIGSEGV on Connector/C and silently returned no rows with libmysqlclient. Through Perfect-CRUD it now throws; raw
MySQLStmt.Resultsstops and sets the newfailed,errorCodeanderrorMessageproperties, andforEachRowreturns false. - #72, #73: parameter memory leaked on every execution: the bind array of every parameterized statement, and the buffer of every blob parameter (
[UInt8],[Int8],bindParam(_:length:), so everyData/[UInt8]value Perfect-CRUD binds).leaks --atExitover 300 executions of a 3-blob INSERT went from 891 leaks (37,984 bytes) to 0. Preparing aMySQLStmtagain with binds pending leaked and then trapped. All fixed. - #72, #73: reusing a
MySQLStmt(execute, read results, reset, execute again) corrupted the heap: the client library kept pointers into freed result buffers. AResultsnow detaches them when it's released, if the statement is still bound to it, so reassigning a results variable and walking aCALL's result sets also work. Re-executing a preparedCALLon Connector/C read freed metadata; the cached metadata is now refetched after each execution.execute()afterreset()without binding every parameter read freed parameter buffers; it now returns false with error 2031 (CR_PARAMS_NOT_BOUND). That re-execution wasn't only reachable through directMySQLStmtuse: callingnext()again on a Perfect-CRUDSelectIteratorthat had already finished re-executed its statement the same way. Binding more values than a statement has parameters wrote past the bind array and crashed, also throughdb.sql(_:bindings:)with too many bindings; the extra values are now refused with error 2034 (CR_INVALID_PARAMETER_NO), andexecute()refuses untilreset(). - #72, #73: an error raised by a
CALLafter its first result set is reported (see above). - #72, #73:
listTables/listDatabasesSQL injection through the wildcard, including after a stored routine changessql_mode(see above); wildcards keep working underNO_BACKSLASH_ESCAPES. - #72, #73:
[Int8]properties holding negative values read back instead of crashing, and with Perfect-CRUD 3.0.1 (now required) they round-trip through CRUD. - #72, #73: dates use the proleptic Gregorian calendar and keep fractions inside their second (see above).
- #72, #73: a Dynamic
selectorexecutewhose result had column names differing only by case (SELECT 1 AS a, 2 AS A, or a join returningp.Nameandc.name) crashed with "Duplicate values for key", because Perfect-CRUD 3.0.0'sDynamicRowlowercases its keys. Dynamic rows now keep the first column of each name, ignoring case. Columns with exactly the same name (p.id, c.id) also resolve to the first column, for Codable decoding too; before, which one was kept was arbitrary.
Upgrade notes
- Depend on
from: "4.0.0", build with Swift 6.2 or newer, and raise your deployment target to macOS 12. - Install the client library and
pkg-configand make surepkg-config --cflags mysqlclientworks (on macOS, setPKG_CONFIG_PATHfor Homebrew's keg-onlymysql-client). Remove anyPerfect-mysqlclientdependency. - Apply Perfect-CRUD 3.0.0's upgrade notes.
- Search for raw
MySQLStmtreads of binary columns or binary expressions and switch them to[UInt8]. - If you built against MariaDB Connector/C and use
MYSQL_OPT_SSL_MODE, prefer VERIFY_IDENTITY with a CA over REQUIRED. - Before reconciling a model that gains a
@ForeignKey, make sure existing rows satisfy it. - If you run outside UTC and corrected Perfect-CRUD dates by hand, remove the correction; if you store local-time values written by the server, consider
SET time_zone = '+00:00'on the connection.
Known limitations: dates are written with whole seconds (fractions are read, not written). Only the first result set of a multi-result CALL is returned through Perfect-CRUD, and untyped db.sql("CALL …") can't report an error raised after it (it's logged). Perfect-CRUD's select() stops at a row that fails to decode, silently dropping it and every later row, instead of throwing. Running the test suite against a MariaDB server fails two DirectStatementTests that assume MySQL behaviour (MariaDB can prepare XA RECOVER, and types SELECT 2 + 2 differently).
Legacy
The pre-Swift-6 code is preserved on the legacy branch, which points at the merge of #52, one whitespace-only commit before the 3.5.1 tag. Stay on 3.5.1 if you can't move to Swift 6.2 yet.