Skip to content

feat(mysql): ALTER column changes via table rebuild; DML/function forms self-migrating apps use - #59

Merged
luthermonson merged 2 commits into
mainfrom
feat/alter-dml-compat
Oct 27, 2026
Merged

luthermonson merged 2 commits into
mainfrom
feat/alter-dml-compat

Conversation

@luthermonson

Copy link
Copy Markdown
Contributor

I found these gaps by replaying Sendy 7.1.6's includes/update.php against MySQL 8.4.11 and litewire/Turso (through ePHPm), then diffing the resulting schemas. I also replayed a sweep of the app's ~1,000 SQL statement shapes. Before this PR, the updater hit 30 failed statements on litewire. After it, the schema diff shows one difference: a unique index whose guard needs GET_LOCK, which ePHPm wires through #642.

ALTER TABLE

  • Column changes go through SQLite's table rebuild. This covers MODIFY / CHANGE / ALTER [COLUMN] c {SET|DROP} {DEFAULT|NOT NULL} and ADD ... FIRST|AFTER c. ADD ... FIRST|AFTER matters because MySQL column order shows up in SHOW COLUMNS and SELECT *.
    • Translation returns a new TranslateResult::RebuildTable(ColumnRebuild).
    • The session reads the table's CREATE, indexes and triggers from sqlite_master, then runs create → copy → drop → rename → recreate in one transaction. Inside a client transaction it uses a savepoint instead.
    • What carries over: data, the inline PRIMARY KEY of a redefined column, constraints, indexes and triggers. Renamed columns are followed in all of them. Type changes re-apply affinity.
    • NOT NULL over existing NULL values fails the statement and leaves the table unchanged.
    • The pure planner is ColumnRebuild::statements in litewire_translate::alter, unit-tested.
  • A multi-operation ALTER is split into one statement per operation. Parenthesized ADD (a ..., b ...) and USING BTREE|HASH are handled.

DML

  • INSERT IGNORE → INSERT OR IGNORE.
  • UPDATE/DELETE ... [ORDER BY] LIMIT n → a rowid IN (subquery) form.
  • DELETE t1 FROM t t1 JOIN t t2 ... (the dedup idiom) works.
  • UPDATE a JOIN b|(derived) ON ... SET a.x = ... → UPDATE ... FROM.
  • UPDATE a, b SET a.x=.., b.y=.. is split into one statement per table. This is only done when each value reads its own table; otherwise it errors.
  • || / && are now logical OR / AND. They were string concatenation, so SHOW COLUMNS ... WHERE Field='a' || Field='b' matched nothing. The updater then re-added columns that already existed.

Functions

  • New: IF, FROM_UNIXTIME, DATE_SUB/DATE_ADD/d ± INTERVAL n unit, FIND_IN_SET, SUBSTRING_INDEX (counts ±1), and SHOW TABLE STATUS.
  • Fix: UNIX_TIMESTAMP(x) ignored its argument and returned text, which sorts after every integer. As a result WHERE t < UNIX_TIMESTAMP() matched every row.

Tests and docs

  • Tests: tests/mysql_migration_forms.rs runs on rusqlite and Turso, plus unit tests for each rewrite.
  • Docs: docs/sql-translation.md and the CHANGELOG are updated.

…nction forms self-migrating apps use

Found by replaying Sendy 7.1.6's updater against MySQL 8.4 and litewire and
diffing the resulting schemas: 30 failed updater statements before; one
schema difference after, which needs GET_LOCK in the embedding server.

- MODIFY / CHANGE / ALTER COLUMN {SET|DROP} {DEFAULT|NOT NULL} and ADD ...
  FIRST|AFTER: SQLite's table-rebuild procedure. Translation returns a
  TranslateResult::RebuildTable plan; the session reads the definition,
  indexes and triggers from sqlite_master and runs create/copy/drop/rename
  in one transaction (savepoint inside a client transaction). Data, inline
  PRIMARY KEY, constraints, indexes and triggers carry over; renames follow.
- Multi-operation ALTER split per operation; ADD (a ..., b ...); USING BTREE.
- INSERT IGNORE -> INSERT OR IGNORE; UPDATE/DELETE ... LIMIT via rowid
  subquery; DELETE t1 FROM t t1 JOIN ...; UPDATE ... JOIN -> UPDATE FROM;
  UPDATE a, b SET ... split per table when each value reads its own table.
- || and && are logical OR/AND (were string concat: a probe ORing column
  names matched nothing and the migration re-added existing columns).
- IF, FROM_UNIXTIME, DATE_SUB/DATE_ADD/INTERVAL, FIND_IN_SET,
  SUBSTRING_INDEX(+-1); SHOW TABLE STATUS.
- UNIX_TIMESTAMP(x) ignored its argument and returned text (which sorts
  after every integer); it now returns an integer for the given time.
…ensrv fork 146de689; zstd ignores)

RUSTSEC-2026-0385/0386/0410 fixed by lockfile bumps (tokio 1.54.1,
ctutils 0.4.6, zstd-sys 2.1.2). The opensrv-mysql patch moves to the
ePHPm fork's HEAD 146de689 (mysql_common without default features), as
ePHPm already uses. zstd 0.13 stays via mysql_common's binlog decoder,
which the server never runs: ignored with justification.
@luthermonson
luthermonson merged commit b8884c7 into main Oct 27, 2026
5 checks passed
@luthermonson
luthermonson deleted the feat/alter-dml-compat branch October 27, 2026 22:33
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