Repository navigation
Release prep for 4.0.0: Perfect-CRUD 3.0.0 pin, CI, Dynamic API, UTC dates and fixes - #26
Merged
Merged
Conversation
- Perfect-CRUD is now `from: "3.0.0"` instead of `branch: "main"`. - The test target no longer passes `-Xlinker -search_dylibs_first`. It was a belt-and-braces guard for the old `MariaDB` product name, whose libMariaDB.a in the build products directory shadowed `-lmariadb` on a case-insensitive file system. The rename to PerfectMariaDB removed the cause: renamed back in a scratch copy, the test build still fails with undefined mysql_* symbols; with the current name and no flag, the test binary links /opt/homebrew/opt/mariadb-connector-c/lib/libmariadb.3.dylib (otool -L), with mysql-client 9.6 also installed. The manifest now has no unsafe flags at all. - Swift 6.4 warned that a `try` covered no throwing call in a chained `db.sql(...)[0].c` expression in SubTableCreateOrderTests; split it. - ListingWildcardTests failed against MySQL 8.4: under NO_BACKSLASH_ESCAPES MySQL treats `\` as an ordinary character in LIKE (checked with the mysql client), while MariaDB keeps it as the escape. The test now expects the server's behaviour, and the listTables/listDatabases docs say so. - verifyingModesRejectASelfSignedServerWithoutCA failed against MariaDB 11.8 over a non-loopback address: Connector/C 3.4 verifies a MariaDB 11.4+ server's self-signed certificate through a fingerprint derived from the password (MDEV-31855). With a passwordless user it is rejected as the test expected. The test now expects a connection in the fingerprint case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Connector/C 3.3 (Ubuntu 24.04's libmariadb-dev) has no password-based fingerprint verification, so against MariaDB with a password it still rejects a self-signed certificate in the verifying modes. Checked on Linux (Swift 6.2, Connector/C 3.3) against MariaDB 11.8 over a non-loopback address, and on macOS (Connector/C 3.4.9) with and without a password. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- .github/workflows/ci.yml, modeled on Perfect-CRUD's: Linux (Swift 6.2 and 6.4 x debug/release in swift:X-noble, libmariadb-dev) running the live tests against MariaDB 11.8 and MySQL 8.4 service containers, including the foreign-key tests; macOS 26 (Xcode 26.0.1 and 26.6 x debug/release) with Homebrew's mariadb-connector-c and no server. Warnings are errors everywhere. Not run on GitHub yet. - macOS needs PKG_CONFIG_PATH for the keg-only mariadb-connector-c with Swift 6.2/6.3: Xcode 26.3's SwiftPM failed with "'mysql.h' file not found" without it, while Swift 6.4's finds it through the brew provider. CI sets it; the README says so. - README: install snippet `from: "4.0.0"` with Perfect-CRUD `from: "3.0.0"`, a Requirements section (Linux is now built and tested), Connector/C 3.4's default certificate verification, a "Migrating from 3.x" section, one Testing section instead of two (with the TLS test variables). - README.zh_CN: same install snippet and current build notes, replacing instructions for Ubuntu 16.04 and hand-written .pc files. - docker-compose.yml publishes MariaDB on 127.0.0.1:3308, not 3306, matching the README's test example and keeping the drop-and-recreate tests away from a real server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The connector implemented none of SQLExeDelegate's Dynamic hooks, so `select(_: DynamicQuery)` and any `execute(_: DynamicSQL)` that returned rows threw "Dynamic rows are not supported by this connector", and `mutate`/`execute` reported 0 affected rows and no inserted ID. New DynamicAPITests fail on the previous commit in four of five tests. - nextDynamicRow() maps each column to a DynamicValue: signed integers to .int, unsigned to .uint, FLOAT/DOUBLE to .double, binary to .bytes, and the rest (DECIMAL, dates, text) to .string; NULL to .null. - Column names are lowercased and only the first of the columns whose names differ only by case, or not at all, is kept (`SELECT 1 AS a, 2 AS A`, `p.Name, c.name` in a join). Perfect-CRUD 3.0.0's DynamicRow.init lowercases the keys with Dictionary(uniqueKeysWithValues:) and would trap on them; this matches the first-wins rule planned for CRUD 3.0.1. - affectedRowCount() and lastInsertedID() are captured right after executing a statement that returns no rows (the statement is reset after its results are read). Out-of-range values become 0 / nil rather than trapping. Checked by the tests, not changed: unsigned DynamicValues already bind with is_unsigned (UInt64.max and Int64.min round-trip), and multi-statement DynamicSQL either fails as a whole (prepared path, or unprepared without CLIENT_MULTI_STATEMENTS; nothing runs) or runs every statement and reports an error from any of them (unprepared with CLIENT_MULTI_STATEMENTS). Run against MariaDB 11.8 and MySQL 8.4 on macOS (Swift 6.4, debug). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checked with Homebrew's Connector/C 3.4.9 against MariaDB 11.8 started with --skip-ssl on 127.0.0.1: with no MYSQL_OPT_SSL_MODE the connection fails with "SSL is required, but the server does not support it"; DISABLED and PREFERRED connect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MySQLStmtExeDelegate built its [name: index] column map by iterating a [Int: String] dictionary, so with duplicate column names (`a.id, b.id`, or `SELECT 1 AS id, 2 AS id`) which column a Codable property read was arbitrary. It now walks the columns in result order and keeps the first, as Perfect-MySQL 5b19235 does and as the Dynamic rows already did. New StatementResultTests.duplicateColumnNamesDecodeTheFirst (20 decodes of a four-column result with two ids and two names). Without the fix it fails in some iterations against MariaDB 11.8; with it, it passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MySQLStmt.Results.init ignored mysql_stmt_store_result's return value, and fetchRow() then called mysql_stmt_fetch on the failed statement, which crashed inside libmariadb (SIGSEGV). Reproduced with `SELECT id, (SELECT x FROM b WHERE b.k = a.id) FROM a` where one row's subquery returns two rows (error 1242 partway through the result), through Database.sql(_:_:) against MariaDB 11.8. - Results records a failed store and never fetches after it; a fetch returning 1 is an error, not the end of the rows. New public `failed`, `errorCode` and `errorMessage` on MySQLStmt.Results. libmariadb reports the store error on the connection, not the statement (mysql_stmt_errno stays 0), so the error is captured from whichever has one. - MySQLStmtExeDelegate.hasNext() throws "Error reading results. <message>" instead of reporting the end of the rows. - Results zero-initializes its binds before binding, so deinit never reads an uninitialized bind. New StatementResultTests.errorWhileReadingRowsThrows: Codable sql(), DynamicSQL and the raw MySQLStmt API, then a query on the same connection. Before the fix the test process dies with signal 11; with it, the full suite passes against MariaDB 11.8 and MySQL 8.4 (macOS, Swift 6.4, debug). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MySQLStmtExeDelegate built its column maps in init, before execute(). A
CALL has no result metadata until it runs, so every column read as
missing: db.sql("CALL p()", T.self) threw "Could not convert nil into an
Int", and Dynamic rows came back empty ([[:]]) with no error. MariaDB's
INSERT ... RETURNING did the same, and reported 0 affected rows.
Same change as Perfect-MySQL 9f3056b and 11541d0:
- The maps are built after each execute(). A dynamic column index outside
the row now throws "Missing dynamic row value" instead of being skipped.
- When the rows run out, and in deinit if a caller stopped early, the
delegate frees the current result and stores/frees every remaining one
(MySQLStmt.discardRemainingResults) before resetting, so a CALL's
trailing status result can't leave the connection out of sync. Only the
first result set of a multi-result CALL is read.
- A statement that returns rows (SELECT, CALL, INSERT ... RETURNING)
reports the number of rows stored as its affected rows, and no insert
ID. Statements without rows still report the count and insert ID read
right after execute(). DynamicAPITests' SELECT expectation changes from
0 to the row count accordingly.
New StatementResultTests: callReturnsRowsAndLeavesTheConnectionUsable
(Codable CALL with a bound argument, Dynamic CALL, a two-set procedure,
CALLs without reading rows inside a transaction, a statement after each)
and insertReturningReturnsRows (MariaDB only; returns on MySQL). Before
the fix both fail ("Could not convert nil into an Int for key: id",
empty dynamic rows, 0 affected rows); with it the full suite passes
against MariaDB 11.8 and MySQL 8.4 (macOS, Swift 6.4, debug).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Perfect-CRUD writes Date values as UTC text ("yyyy-MM-dd HH:mm:ss" with a
GMT formatter), but Date(fromMysqlFormatted:) parsed them in
TimeZone.current. Outside UTC that meant every date came back shifted by
the local offset (+7h in Denver), a UTC wall time inside the local DST
spring-forward gap didn't parse, and DATETIME(6) fractions and DATE values
never parsed in any zone. A failed parse threw, and Perfect-CRUD's
SelectIterator swallows the error, so those rows silently vanished.
Ported from Perfect-MySQL 705e9db so both connectors behave the same: the
parser reads YYYY-MM-DD and YYYY-MM-DD HH:MM:SS (space or T) with up to
six fractional digits, always as UTC through a fixed gregorian/UTC
calendar, and rejects out-of-range parts; the writer gets an explicit UTC
calendar. decodeNil reports nil for a zero date ('0000-00-00...') in a
date-typed column, so an optional Date reads nil; a non-optional Date
throws a "zero date ... use an optional property" error (Perfect-CRUD's
select() still drops that row).
README "Migrating from 3.x" describes the change. CI now runs every job
with TZ=America/Denver (tzdata added to the Linux packages).
New DateTests (Perfect-MySQL's, adapted to this package's live-test
setup): parser cases, a CRUD round trip including the Denver gap date,
DATETIME(6), DATE, NULL and zero dates. Under TZ=America/Denver the
previous commit fails the parser cases and drops the gap row; TZ=UTC hides
the round-trip bug, since TimeZone.current can't be changed in-process.
With the fix the full suite passes under TZ=America/Denver against
MariaDB 11.8 and MySQL 8.4 (macOS, Swift 6.4, debug).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ment MySQLStmt.clearBinds() freed paramBinds only when paramBindsOffset > 0, but reset() (through resetBinds) zeroes the offset, so a statement that was bound, executed and reset leaked its MYSQL_BIND array in deinit. Perfect-CRUD resets every statement after reading it. clearBinds() now frees the array whenever there is one, and prepare() clears the old array before allocating one for the new statement, so re-preparing doesn't leak either. Measured with `leaks` on a probe that prepares, binds two parameters, executes, reads and resets 500 statements (MallocStackLogging=1, macOS, Connector/C 3.4.9, MariaDB 11.8): - before: "500 leaks for 120000 total leaked bytes", every one a ROOT LEAK allocated in MySQLStmt.prepare(statement:); - after: "0 leaks for 0 total leaked bytes". New StatementResultTests.statementsRebindAfterResetAndReprepare covers reusing one statement 50 times and re-preparing it with more parameters (a test can't run `leaks` itself). Full suite passes under TZ=America/Denver against MariaDB 11.8 and MySQL 8.4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rding - The Dynamic API section lists its limits: an inserted ID above Int64.max is nil; an UPDATE that changes nothing reports 0 affected rows; row-returning statements report their row count; TINYINT(1) reads as .int and YEAR as .string; statements run unprepared report 0 affected rows; only the first result set of a multi-result CALL is visible. - "Migrating from 3.x" lists the new MySQLOpt.MYSQL_OPT_SSL_MODE case as breaking for exhaustive switches. - PKG_CONFIG_PATH: only Swift 6.2 (Xcode 26.3) was seen to need it, so the README, README.zh_CN and the CI comment no longer claim Swift 6.3 does. - README.zh_CN's Package snippet adds the Perfect-CRUD dependency and product. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It always returned true, so a raw caller couldn't tell a result cut short by an error (see 0e39522) from a complete one. Same as Perfect-MySQL dda4fec. errorWhileReadingRowsThrows now also checks forEachRow on the failing query; the full suite passes under TZ=America/Denver against MariaDB 11.8 (debug) and MySQL 8.4 (release). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MySQLStmt hands DECIMAL values over as text, and the row reader's decode(Double.self) / decode(Float.self) only accepted a Double / Float, so a Double or Float property over a DECIMAL column silently read 0.0. Both now also accept the other floating-point type and parse decimal text; text that doesn't parse throws instead of reading 0. A missing column still reads 0, as before. Ported from Perfect-MySQL f2c011c so the two connectors stay identical. New StatementResultTests.decimalColumnsDecodeIntoFloatingPoint (Perfect- MySQL's test): Double, Float and Double? properties over DECIMAL columns, including negative, fractional and NULL values. On the previous commit, against MariaDB 11.8, all three expectations fail ([0.0, 0.0], [0.0, 0.0], [nil, 0.0]); with the fix the full suite passes under TZ=America/Denver against MariaDB 11.8 and MySQL 8.4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
discardRemainingResults() stopped at the first mysql_stmt_next_result()
that didn't return 0, so when a procedure's later statement failed (a
duplicate key, error 1242, SIGNAL) the error was dropped and the CALL
reported success with its first set's rows, through typed db.sql(_:_:)
and DynamicSQL alike. Found by the final verification.
discardRemainingResults() now returns the first error a later result
reports: from mysql_stmt_next_result() > 0, or from a failing
mysql_stmt_store_result (that result is freed and draining continues).
hasNext() throws it as MySQLCRUDError after reset(), so the connection is
reusable. deinit (a caller that stopped early) can only log it through
CRUDLogging. MySQLStmt.currentError() (the statement's error, else the
connection's) is shared with Results' failure capture. Same code as
Perfect-MySQL 354664c.
Limit, documented in README "Migrating from 3.x" with the other CALL
notes: Perfect-CRUD's untyped db.sql("CALL p()") calls hasNext() once, so
it can't report such an error.
New StatementResultTests.errorAfterACallsFirstResultSetThrows: three
procedures (duplicate key, 1242, SIGNAL) through typed sql and
DynamicSQL, the untyped path, a query on the same connection after each,
and a two-set CALL that succeeds. Before the fix all six error
expectations fail against MariaDB 11.8; with it the full suite passes
under TZ=America/Denver against MariaDB 11.8 and MySQL 8.4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bindParam([UInt8]), bindParam([Int8]) and bindParam(_:length:) allocate a buffer for the value and bind it as MYSQL_TYPE_LONG_BLOB, but resetBinds() skipped that type, so every blob parameter leaked its whole value, including every Data / [UInt8] / [Int8] that Perfect-CRUD binds for an insert, update or where. resetBinds() now frees it (NULL stays a no-op). Same change as the verifier's prototype for Perfect-MySQL. Measured with `leaks` on a probe that prepares, binds three blob parameters ([UInt8] x32, [Int8] x16, 8 bytes via length:), executes, reads and resets 300 times (MallocStackLogging=1, macOS, Connector/C 3.4.9, MariaDB 11.8): - before: "895 leaks for 28672 total leaked bytes", the roots allocated in UnsafeMutableRawBufferPointer.allocate (the blob buffers); - after: "0 leaks for 0 total leaked bytes". d13f4eb fixed only the MYSQL_BIND array; its "now none" was about that array, not these buffers. New StatementResultTests.blobParametersSurviveResetAndReuse: one statement reused 50 times with blobs of every size from 0 to 49 bytes, checking the values come back (a test can't run `leaks`). Full suite passes under TZ=America/Denver against MariaDB 11.8 and MySQL 8.4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
'9999-12-31 23:59:59.999999' (a common "forever" value in DATETIME(6)) parsed as 10000-01-01 00:00:00: the fraction was added as a Double, and a Date (a Double count of seconds since 2001) can't hold six fractional digits near year 9999. mysqlFormatted() then wrote '10000-01-01 00:00:00', which the server rejects. The writer's DateFormatter also rounded, so '1969-12-31 23:59:59.999999' was written back as '1970-01-01 00:00:00'. Found by the final verification. Ported from Perfect-MySQL ce82e24; the date code is identical to its (indentation aside). The parser carries the fraction as whole microseconds and builds the Date with Date(mysqlSeconds:microseconds:), which keeps it inside its second; the writer floors to whole seconds. New DateTests.fractionsStayInsideTheirSecond (Perfect-MySQL's). On the previous commit it fails with 4 issues; with the fix the full suite passes under TZ=America/Denver against MariaDB 11.8 and MySQL 8.4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Date parsing and formatting went through Foundation's .gregorian calendar, which switches to the Julian calendar before 1582-10-15. So the valid dates 1582-10-05...14 didn't parse (decode threw "Invalid Date string", and Perfect-CRUD's select() dropped the row), and earlier dates became instants 10 or more days away from what the server's proleptic calendar means. Found by the final verification. Ported from Perfect-MySQL 1bffe69: reader and writer compute days from and to 1970-01-01 with Howard Hinnant's days_from_civil / civil_from_days, with explicit range checks; DateFormatter and Calendar are no longer used, so neither direction depends on the process locale, calendar or time zone. The date code is now identical to Perfect-MySQL's (checked by diffing the two blocks with whitespace removed). New tests, Perfect-MySQL's (the live one adapted to this package's setup): DateTests.datesUseTheProlepticGregorianCalendar (Unix times for 0001-01-01 ... 9999-12-31, the leap rule, every day from 1600 to 2000 round-tripping) and DateTests.oldDatesMatchTheServersCalendar (dates written through Perfect-CRUD read back with DATE_FORMAT as the same text). On the previous commit the first fails with 5 issues and the live one traps on a nil 1582-10-10; with the fix the full suite passes under TZ=America/Denver against MariaDB 11.8 and MySQL 8.4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Perfect-MySQL 376a1c3 found that reusing a MySQLStmt (execute, read results, reset, execute again) corrupted the heap with libmysqlclient: mysql_stmt_bind_result keeps a copy of the MYSQL_BINDs, with their buffer / length / is_null pointers, until the next bind_result, and Results.deinit freed those buffers before the next execute wrote through the copy. Results.deinit now rebinds the statement to MYSQL_TYPE_NULL binds with no pointers before freeing its buffers. Same code as Perfect-MySQL; Results.init already zero-initialised its binds here. Not reproduced with Connector/C: a 300-iteration reuse probe (int, double, string and blob parameters, each row checked) runs clean before and after this change, with the system allocator and under Guard Malloc. This is ported so the shared code stays identical and so it doesn't depend on Connector/C not writing through the stale copy. New StatementResultTests.aStatementCanBeReusedAfterReadingResults (Perfect-MySQL's test; LENGTH() is compared as any integer, since it's an INT on MariaDB and a BIGINT on MySQL). Passes before and after here; the full suite passes under TZ=America/Denver against MariaDB 11.8 and MySQL 8.4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The row reader decoded a blob into [Int8] with `.map { Int8($0) }`, which
traps ("Not enough bits to represent the passed value") on any byte
>= 0x80, so reading back a negative [Int8] value crashed the process. It
now uses Int8(bitPattern:). Perfect-MySQL has the same line and is fixed
identically.
New StatementResultTests.signedByteArraysRoundTrip: rows holding
X'FF80007F01' and X'' read into an [Int8] property as [-1, -128, 0, 127, 1]
and []. The rows are written with SQL, because Perfect-CRUD 3.0.0 itself
traps encoding a negative [Int8] (`UInt8($0)` in CodingBindings; fixed in
Perfect-CRUD 3.0.1), so a write through CRUD needs 3.0.1. On the previous
commit the test traps; with the fix the full suite passes under
TZ=America/Denver against MariaDB 11.8 and MySQL 8.4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On x86_64, libmariadb 3.3 (Ubuntu 24.04's libmariadb3 1:10.11.14-0ubuntu0.24.04.1) escapes the UTF-8 bytes of "中'" (E4 B8 AD 27) under big5 as E4 B8 AD 5C 27; the server reads AD 5C as one big5 character, so the quote ends the string literal. With CLIENT_MULTI_STATEMENTS, ListingWildcardTests.wildcardCannotRunAnotherStatement saw "中'; DROP TABLE wild_victim; -- " drop the table through both listTables and listDatabases: reproduced in swift:6.4-noble --arch amd64 against MariaDB 11.8 (2 issues), and it turned all four Linux CI jobs of PR #26 red. arm64 escapes it as E4 B8 5C AD 5C 27, which is safe. listNames now refuses (returns [], the existing failure value, so the API doesn't change) a wildcard that has a byte >= 0x80 and a `'` or `\` while the connection charset is big5, cp932, gbk, gb18030 or sjis, the charsets where 0x5C can be a multibyte trail byte. SHOW ... LIKE stays: information_schema would change case sensitivity and depend on lower_case_table_names. The guard and listNames are the same code as Perfect-MySQL 1d17314 (handle name aside), and the doc comments on listTables / listDatabases say what's refused. Tests: - wildcardCannotRunAnotherStatement now covers utf8mb4, latin1, gbk, big5, cp932, sjis and gb18030, with two more payloads (`中\'` and a halfwidth katakana before the quote). gbk and utf8mb4 keep their expectations: the DROP never runs and the list is empty. - New unsafeWildcardsAreRefusedOnlyUnderTrailByteCharsets checks the guard's exact conditions, and that `it's` / `it%` still list under big5. MariaDB 11.8 has no gb18030, so the client stays on utf8mb4 there and nothing is refused. ListingWildcardTests pass on Linux --arch amd64 (debug and release) against MariaDB 11.8 and MySQL 8.4; the full suite passes on macOS under TZ=America/Denver against both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng from 3.x"
"Migrating from 3.x" gains the behaviour changes it was missing: errors
while reading rows throw and MySQLStmt.Results has failed / errorCode /
errorMessage; DECIMAL decodes into Double and Float; dates use the
proleptic Gregorian calendar and keep fractions inside their second;
listTables / listDatabases refuse wildcards escaping can't make safe under
big5, cp932, gbk, gb18030 and sjis; [Int8] reads negative bytes. The CALL
item (rows readable; later errors thrown except through untyped
db.sql("CALL ...")) was added with the CALL-error fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
30bdf04 made Results.deinit rebind the statement to pointer-free binds before freeing its buffers. Two regressions, found by the pre-push verification (F1): - It detached whatever the statement was bound to, including a newer Results. `r = stmt.results()` builds the new Results before the old one is released, so the old one's deinit unbound the new one and every row read 0 / "". Walking a CALL's result sets with one variable failed the same way. - It allocated numFields binds, but mysql_stmt_bind_result copies the statement's *current* field count. When a Results outlived nextResult() to a wider set, or a re-prepare with more columns, that read past the allocation (SIGSEGV under Guard Malloc). Not reachable through Perfect-CRUD; reachable through the public MySQLStmt / Results API. Ported byte-identical from Perfect-MySQL e7e401a: the statement records which Results it's bound to (resultOwner, set in bind()); deinit detaches only if it's still the owner, with Int(stmt.fieldCount()) binds, skips it when that's 0, and clears the owner. bind() no longer calls mysql_stmt_bind_result for a result with no columns. Tests (StatementResultTests, Perfect-MySQL's, adapted to this package's setup; integers compared as any integer type): - reassigningAResultsVariableKeepsTheNewOneBound: on the previous commit 150 issues against both servers. - olderResultsDontDisturbLaterResultSets: on the previous commit it crashes the test process. The verifier's reassign and outlive probes under Guard Malloc (banner checked), against MariaDB 11.8 and MySQL 8.4: clean, 0 failures. Full suite passes under TZ=America/Denver against both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-executing one prepared CALL statement through the raw API read freed memory: MySQLStmt caches mysql_stmt_result_metadata, its field pointers point into storage that libmariadb replaces when it executes the statement again, and Results.bind read them. Found by the pre-push verification (P2). Ported byte-identical from Perfect-MySQL bc8700b: execute() frees the cached metadata after every successful execution, so it's fetched afresh (Results.init and fieldNames() already refresh it); fieldInfo(index:) refreshes it too, and rejects negative indexes. Checks: - A `callrepeat` probe (one prepared CALL executed 20 times with different arguments, reading fieldInfo and every row), under Guard Malloc with the banner checked: SIGSEGV (exit 139) against both MariaDB 11.8 and MySQL 8.4 on the previous commit; clean, 0 failures, with this one. The reassign / outlive / reuse probes stay clean. - New StatementResultTests.aPreparedCallCanBeExecutedRepeatedly (Perfect- MySQL's, adapted): the same loop plus a statement afterwards. It passes with the system allocator before and after; the bug shows under Guard Malloc. Full suite passes under TZ=America/Denver against both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After reset() the parameter buffers are freed, but the client library keeps the previous execution's MYSQL_BINDs; execute() only rebound when something had been bound, so calling it again without rebinding sent freed memory (a string parameter came back as garbage; SIGSEGV under Guard Malloc). Binding fewer values than the statement has parameters left zeroed binds for the rest. Predates this release; b22a5f0 (freeing blob buffers on reset) extended the window to blobs. Found by the pre-push verification (F2). Ported byte-identical from Perfect-MySQL 75b1359: execute() returns false unless the number of bound values equals paramCount(); errorCode() / errorMessage() then report 2031 CR_PARAMS_NOT_BOUND "No data supplied for parameters in prepared statement" until the next prepare() or execute(). Statements without parameters are unaffected, and Perfect-CRUD always binds every parameter. Checks: - New StatementResultTests.executingWithoutBindingEveryParameterIsRefused (Perfect-MySQL's, adapted): on the previous commit 3 issues. - The verifier's reset-execute probe under Guard Malloc (banner checked): SIGSEGV (exit 139) against MariaDB 11.8 and MySQL 8.4 before; clean after. Full suite passes under TZ=America/Denver against both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
071bd83 switched dates to the proleptic Gregorian calendar, where Date.distantPast is 0000-12-30. The writer emitted that and the servers stored it, but the reader rejected year < 1. Perfect-CRUD's SelectIterator ends iteration at the first row that fails to decode, so every row after it vanished silently. And mysqlFormatted() trapped in Int(_:) on a non-finite Date (F4). Found by the pre-push verification. Ported byte-identical from Perfect-MySQL 7221f26 (the date code is again identical to Perfect-MySQL's, checked by diffing with whitespace removed): - The reader accepts year 0 (month and day 0, the zero date, are still rejected). Year 0 is a leap year, as in MySQL. - mysqlFormatted() throws instead of trapping on a Date it can't represent: non-finite, or outside years 0...9999. The .date binding passes the error up, so the insert or update throws. README: the Dates bullet now matches Perfect-MySQL's: dates before 1582-10-15 written by 3.x read back 2 or more days different, year 0 and the thrown error are documented, and "select() skips the row" is corrected: it stops there and drops every later row. Tests (DateTests, Perfect-MySQL's): sentinelDatesRoundTrip and sentinelDatesDontEndASelect (live, adapted); the existing date tests now `try` mysqlFormatted(). A `dates` probe (ids 0...5 with distantPast at 1 and distantFuture at 3, through Perfect-CRUD, TZ=America/Denver) reads back only id 0 on the previous commit and all six with this one, against MariaDB 11.8 and MySQL 8.4. Full suite passes against both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e stale listNames escaped the whole wildcard with mysql_real_escape_string, which picks backslash-escaping or quote-doubling from the NO_BACKSLASH_ESCAPES bit in the server's last status. After a stored routine (procedure or function) changes sql_mode, both MariaDB 11.8 and MySQL 8.4 leave that bit as the routine set it while the session's sql_mode is restored, and it stays stale across later statements. So the client escaped for the wrong mode and, with CLIENT_MULTI_STATEMENTS, `x'; DROP TABLE victim; -- ` (session in NBE, routine clears it) or `x\'; DROP TABLE victim; -- ` (routine sets NBE) ran the DROP. Found by the pre-push verification (F3): its stale-nbe probe injected 6 times and its wildcard probe twice per server, on macOS (Connector/C 3.4.9) and Linux amd64 (libmariadb 3.3). A `'` is no longer left to the library: when the wildcard has one, the pattern is copied with each `'` doubled (`''` is a quote inside a literal in either mode, and 0x27 is never part of a multibyte character), and a wildcard that also has a `\` is refused (returns []), since its escaping depends on the mode. A wildcard without `'` is escaped by mysql_real_escape_string as before; escaping it for the wrong mode can't end the literal. The big5/cp932/gbk/gb18030/sjis guard stays. Doc comments on listTables / listDatabases say what's refused. This goes beyond Perfect-MySQL 7420e31, which only made escaping work under NO_BACKSLASH_ESCAPES with libmysqlclient (on libmariadb, mysql_real_escape_string already did, so that change has no counterpart here) and still trusts the mode bit; Perfect-MySQL needs this change too. Tests (ListingWildcardTests): - wildcardsWorkUnderNoBackslashEscapes (Perfect-MySQL's, adapted; utf8mb4 and big5): vic%, a name with `'` and the schema match under NBE, three payloads stay inert. Passes before and after here. - wildcardsStaySafeWhenARoutineLeavesTheModeBitStale: three routine / payload scenarios. On the previous commit 4 issues (two DROPs ran) on both servers; passes with the fix. The stale-nbe and wildcard probes under Guard Malloc (banner checked): 0 failures on both servers. Full suite passes under TZ=America/Denver. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ute() - The listTables / listDatabases item no longer says the wildcard is simply "escaped": a `'` is doubled by hand, the rest is escaped by the client library, and wildcards with both `'` and `\`, or with a non-ASCII byte and `'`/`\` under big5, cp932, gbk, gb18030 or sjis, are refused. Wildcards keep working under NO_BACKSLASH_ESCAPES. - New "Statements can be reused safely" item: replaced or outliving Results, re-executing a prepared CALL, execute() refusing (2031) unless every parameter is bound, and the parameter leaks. The "select() stops, not skips" correction went in with 364364c. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Perfect-MySQL landed the same stale-NO_BACKSLASH_ESCAPES fix as 0b63465 (5c9c5fd), on top of its perfect_mysql_escape_string_quote shim (7420e31). To keep the shared code identical: - mariadb.h gains perfect_mysql_escape_string_quote with Perfect-MySQL's signature. This package only builds against Connector/C, which has no _quote variant, so it calls mysql_real_escape_string, which already doubles quotes under NO_BACKSLASH_ESCAPES. - listNames and its comment are now Perfect-MySQL's, apart from the connection handle (self.ptr! here, mysqlPtr there); checked by diffing with whitespace removed. Behaviour is the same as 0b63465. - The listTables / listDatabases doc comments use Perfect-MySQL's wording for what's refused. Test: Perfect-MySQL's aStaleNoBackslashEscapesBitCannotEndTheLiteral, adapted (utf8mb4, latin1, big5; four stale-bit setups including a stored function and a later DO 1; `x'`, `x\'` and a trailing `\` payloads; the connection stays in sync and vic% / it_s still match). Full suite passes under TZ=America/Denver against MariaDB 11.8 and MySQL 8.4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Perfect-CRUD 3.0.1 is released, and Perfect-MySQL raises its pin to it too. 3.0.1 encodes a negative [Int8] without trapping (3.0.0's CodingBindings used UInt8($0)) and makes DynamicRow keep the first of case-only duplicate keys instead of trapping. - Package.swift, the README / README.zh_CN install snippets and the README requirements line: `from: "3.0.1"`. Package.resolved (not committed) resolves 3.0.1. - StatementResultTests.signedByteArraysRoundTrip now also writes every Int8 value from -128 to 127 through Perfect-CRUD and reads it back with a `where` on the id; the README [Int8] item says 3.0.1 writes them. Full suite passes under TZ=America/Denver against MariaDB 11.8 and MySQL 8.4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every bindParam wrote `paramBinds?[paramBindsOffset]` with no bound, so
binding more values than the prepared statement has parameters wrote past
the bind array, and resetBinds() then freed whatever was there. Reachable
through Perfect-CRUD: `db.sql("SELECT ? AS x", bindings: [3 values],
X.self)` crashed. Pre-existing (main, 3.x and Perfect-MySQL have the same
code). Found by the re-verification (M1).
- prepare() records the parameter count (paramBindsCapacity). Each
bindParam first calls reserveParamBind(): past the count it sets error
2034 (CR_INVALID_PARAMETER_NO, "Invalid parameter number"), counts the
value without allocating or storing anything, and returns. execute()
then refuses with 2034 (too many) or 2031 (too few) until reset() and a
correct rebind.
- resetBinds() frees only the array's own entries (min(offset, count))
and zeroes paramBindsOffset unconditionally, so a stray bindParam on a
statement without parameters no longer leaves it refused with 2031 until
re-prepared (I1).
- prepare() clears the binds before checking the result, so after a
failed prepare execute() reports the library's error instead of 2031
(I2).
Written so it ports byte-for-byte to Perfect-MySQL's identical MySQLStmt.
Checks:
- New StatementResultTests.bindingTooManyParametersIsRefused: three
values for one parameter through Perfect-CRUD (throws, then a correct
query works), four through raw MySQLStmt (2034, refused, then reset and
rebind works), a stray value on a parameterless statement, and execute()
after a failed re-prepare. On the previous commit the test process
crashes at the first step.
- An `overbind` probe (CRUD and raw, 50 rounds): on the previous commit
exit 134 with the system allocator and SIGSEGV (139) under Guard Malloc
against MariaDB 11.8 and MySQL 8.4; with the fix 0 failures under Guard
Malloc (banner checked), and the reset-execute / reuse / reassign /
callrepeat probes stay clean. Full suite passes under
TZ=America/Denver against both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Dates item said such a Date "throws when bound". That holds for insert, update, count() and sql; in a select() whose `where` compares against it, Perfect-CRUD's Select.makeIterator catches the binding error, logs it and yields no rows (re-verification L1, confirmed by its `dates` probe). Say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three low items from the review of 4d0020c, before the Perfect-MySQL port: - L1: after the public clearBinds() on a prepared statement there's no bind array, so every value is refused, yet the offset can still reach paramCount(); execute() then failed with error 0 and an empty message. Its guard now also requires paramBindsOffset <= paramBindsCapacity, and reports 2031 when too few values were bound, otherwise 2034 (values that weren't stored). - L2: a 2034 from binding too many values survived reset() and a correct rebind (errorCode() kept reporting it). resetBinds() now clears the wrapper's error. - L3: README "Statements can be reused safely" says that binding too many values, which crashed, is refused, that bindParam sets errorCode() to 2034, and that execute() refuses until reset(). The prepare() comment now says that after a failed prepare execute() fails with the library's error (2014 "Commands out of sync" on MariaDB Connector/C), not 2031. bindingTooManyParametersIsRefused gains assertions for L1 (2034 after clearBinds() and a bind) and L2 (errorCode() 0 after reset() and after a correct rebind); on the previous commit those 3 fail. Full suite on Xcode 27 debug against MariaDB 11.8 under TZ=America/Denver: 124 pass, 2 skipped. The overbind, reset-execute and reuse probes under Guard Malloc (banner checked) against MariaDB: 0 failures. Same code for Perfect-MySQL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release prep for Perfect-MariaDB 4.0.0, the first release since 3.0.4 (2020). The tag is
4.0.0with novprefix, matching 3.0.0–3.0.4. The tag and GitHub release follow after this merges and CI is green.Release mechanics
from: "3.0.0"instead ofbranch: "main".unsafeFlagslinker setting (8a7ebec). It was a leftover from the oldMariaDBproduct name, and SwiftPM rejects unsafe flags in version-resolved packages..github/workflows/ci.yml):New and fixed
executereturn rows, affected-row counts and insert IDs. Before, any Dynamic query that returned rows threw.MySQLStmt.Results.forEachRowreturns false when reading failed (d42da2a);Double/Float; before, it read as 0.0 (b48903d).Tested (2026-10-13, macOS, clean export of this branch, warnings as errors,
TZ=America/Denver): 106/106 pass against MariaDB 11.8 (debug and release) and against MySQL 8.4. Two TLS tests skip because they need a non-TLS server or a non-loopback host. On 2026-10-06 it also passed on Xcode 26.3 and on Linux with Swift 6.2/6.4.Commits
🤖 Generated with Claude Code