Skip to content

6.0.0 release prep - #31

Merged
taplin merged 9 commits into
mainfrom
release-prep
Oct 13, 2026
Merged

taplin merged 9 commits into
mainfrom
release-prep

Conversation

@taplin

@taplin taplin commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Release prep for Perfect-SQLite 6.0.0, the first release since 5.0.0 (2020). It requires Perfect-CRUD from: "3.0.1". CI was already on main.

  • Dynamic SQL and the CRUD path:
    • SQL with more than one statement now throws SQLiteCRUDError. Before, everything after the first statement was silently dropped (Database.sql and Dynamic execute).
    • SQL with no statement (empty, whitespace, ;, a comment) or with an embedded NUL now throws. It used to crash or silently truncate.
    • Unsigned values above Int64.max throw instead of trapping.
    • Duplicate result column names keep the first column, including names that differ only by case, which could crash CRUD 3.0.0.
    • SQLiteDatabaseConfiguration is Sendable, so CRUD's connection pool and async methods work with it. Tests cover the pool and async round trips.
  • README:
    • Requirements (SQLite 3.29+), legacy pointer, and "Migrating from 5.x", which covers DQS off, reconcile reaching sub-tables, Linux using the system libsqlite3-dev, and the Dynamic API.
    • Corrections: the DQS/reconcile caveat, lastInsertId advice (rowid), and the threading modes.
    • Removed stale pre-5.0.0 jazzy docs/.

Testing

58/58 with warnings as errors:

  • macOS Xcode 27 (debug and release) and Xcode 26.3;
  • Linux 6.2 and 6.4;
  • iOS Simulator: 56/56 before the last 2 tests were added.

Three adversarial reviews. The final one checked that every SQL string CRUD generates is a single statement (create, reconcile rebuilds, indexes, FKs), and the pool test's determinism (40/40).

🤖 Generated with Claude Code

taplin and others added 9 commits October 6, 2026 07:22
Replaces the branch: "main" pin with the released version, so a
version-pinned consumer can resolve this package. Package.resolved is
gitignored; swift package resolve picks Perfect-CRUD 3.0.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pins the install and Perfect-CRUD snippets to from: "6.0.0" / "3.0.0",
notes the SQLite 3.29 floor, and lists the changes since 5.0.0: toolchain
and platforms, Linux on the system libsqlite3, double-quoted strings off,
reconcile reaching sub-tables (non-atomic, .shallow opt-out), reconcile
keeping FOREIGN KEYs, quoted identifiers, and the Dynamic API's
affectedRows (0) and insertedID (nil). Dynamic select works on this
connector (nextDynamicRow is implemented); checked from a scratch consumer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sqlite3_prepare_v2 stops after the first statement, so
execute(DynamicSQL(..., allowsMultipleStatements: true)) and Database.sql
ran the first statement and silently ignored the others. An
SQLExeDelegate runs exactly one statement with one set of bindings and one
row cursor, and CRUD documents DynamicSQL as a single statement whose
multi-statement guard is best-effort, so the connector now throws an
SQLiteCRUDError when another statement follows (even one that can't be
prepared yet) rather than running them in turn. Trailing whitespace, ';'
and comments are still accepted. The raw SQLite.prepare/execute API is
unchanged.

Adds DynamicAPITests: Dynamic select and execute return rows, and two
INSERTs in one execute throw with neither run. The refusal test fails
with 5 issues without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Binding .uinteger or .uinteger64 (Dynamic .uint, or a typed comparison
with a UInt) did Int(i), which traps above Int64.max. They now go through
Int64(exactly:) and throw an SQLiteCRUDError when the value doesn't fit
SQLite's signed 64-bit INTEGER.

Typed UInt/UInt64 properties on insert/update never reach this path:
CRUD 3.0.0's encoder converts them with Int(value) itself and still traps
there above Int.max. That needs a Perfect-CRUD fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With two result columns of the same name (SELECT a.id, b.id, or 1 AS x,
2 AS x) the column map kept the last one, so Dynamic rows and the typed
reader returned the second column's value. The first now wins, as in
SQLite's own name lookup. Dynamic rows also fold names case-insensitively
(first wins), matching DynamicRow's lowercased keys: SELECT 1 AS a, 2 AS A
used to crash in CRUD 3.0.0's DynamicRow.init with duplicate keys.

Tests cover execute with a join, exact and case-only duplicates, a
Dynamic select with duplicate fields, and the typed reader. Without the
fix they record 2 issues and then trap on the case-only duplicate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…docs/

- DQS: a "..." string in a CHECK clause or partial-index WHERE makes every
  reconcile that removes columns fail (ALTER TABLE ... RENAME re-parses the
  schema); reconcile such databases with doubleQuotedStrings: true.
  Reproduced for both on macOS; DEFAULT values were unaffected.
- Reconcile: the FK check also counts violations that already existed;
  the in-transaction refusal applies with foreign_keys on and includes
  self-referencing tables; a rebuild drops triggers as well as indexes.
- Dynamic API: new through Perfect-CRUD 3 (5.0.0 had none), so drop the
  before/after comparison. lastInsertRowID()/Insert.lastInsertId() return
  the rowid, which isn't the key on CRUD's INT PRIMARY KEY tables.
- Migrating: one statement per call, unsigned overflow throws, first
  duplicate column wins, columnText repairs invalid UTF-8 (5.0.0 gave "").
- Linux: libsqlite3-dev and pkg-config, as CI installs.
- Delete docs/ (pre-5.0.0 jazzy output for the Swift 3 API, not served by
  GitHub Pages) and its .jazzy.yaml (Swift 3 toolchain); nothing else
  referenced them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Perfect-CRUD 3.0.0 requires C: Sendable for DatabaseConnectionPool,
transactionAsync, sqlAsync, insertAsync, firstAsync and fetchAll(), so
none of them compiled with SQLite. The configuration stores a String and
the SQLite connection, a class around a raw sqlite3* with no locking
(already @unchecked Sendable), so the conformance is @unchecked, with a
comment on when it's safe: one task at a time per connection. The async
methods and the pool move a connection between threads without sharing
it, which SQLite allows in multi-thread mode (the Apple SDKs:
sqlite3_threadsafe() == 2, checked on macOS 27 with SQLite 3.54) and
serialized mode (Ubuntu noble's 3.45.1: 1). Connections aren't opened
with SQLITE_OPEN_FULLMUTEX, so concurrent use of one connection is a data
race on Apple platforms; the README says to give each task its own
connection.

AsyncPoolTests: an insertAsync / transactionAsync (commit and rollback) /
fetchAll / firstAsync / sqlAsync round trip, and a 3-connection pool
serving 12 concurrent inserts plus manual acquire/release. Without the
conformance the test target fails to compile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Empty, whitespace-only, ";"-only and comment-only SQL prepares without
error but gives no statement, and SQLiteExeDelegate then crashed on the
nil statement ("Unexpectedly found nil") through db.sql(_:) and
Dynamic execute. This is pre-existing (5.0.0 behaves the same), found by
the final verification. prepareSingle now throws SQLiteCRUDError for
it. Its old "Empty SQL statement" guard could never fire, because
utf8CString always has a NUL.

SQL with an embedded NUL ran up to the NUL and silently ignored the
rest; it now throws.

The empty-SQL test crashes without the fix, and the NUL test records an
issue. 58/58 on Xcode 27 (debug and release), Xcode 26.3 (release) and
Linux 6.2 and 6.4 (release), all with -warnings-as-errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3.0.1 fixes four crashes in 3.0.0 reachable from public API, including
a Codable UInt/UInt64 above Int.max trapping in CRUD's encoder (now
bound as unsigned). Tests pass against 3.0.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@taplin
taplin merged commit fd3b8ed into main Oct 13, 2026
18 checks passed
@taplin
taplin deleted the release-prep branch October 13, 2026 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant