Skip to content

Add support for ALTER COLUMN with SET DEFAULT and DROP DEFAULT - #555

Merged
JanJakes merged 3 commits into
trunkfrom
alter-column-defaults
Oct 23, 2026
Merged

JanJakes merged 3 commits into
trunkfrom
alter-column-defaults

Conversation

@JanJakes

@JanJakes JanJakes commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Previously, ALTER TABLE ... ALTER COLUMN ... SET DEFAULT and DROP DEFAULT succeeded but left the old defaults in place. This PR makes new inserts and schema metadata use the changed defaults, while preserving existing row values and unrelated column attributes.

For example, after setting an integer column's default from 1 to 5 and dropping a string column's default of 'x', an insert that omits both columns now uses 5 and NULL.

The change also handles the related error and compatibility cases:

  • Rejects NULL defaults on NOT NULL columns and preserves automatic ID generation.
  • Applies ALTER COLUMN changes in order with the other ALTER TABLE actions, reports missing tables and columns correctly, and rolls back failed changes.
  • Covers permanent and temporary tables, quoted names, literal and expression defaults, inserted values, and schema metadata with regression tests.

SET DEFAULT (expr) on an ON UPDATE CURRENT_TIMESTAMP column stops its automatic update, because the driver doesn't create the update trigger for columns with an expression default. Fixing that detection alone would let the trigger overwrite explicitly written timestamps on many more tables, so it's fixed together with the trigger in #554.

Keeping a nullable column without a default after DROP DEFAULT is tracked in #556.

The existing PDO coding-standard exemption also covers the plugin's symlinked copy of the driver.

Fixes #553.

Summary by CodeRabbit

  • New Features
    • Added support for changing or removing column defaults with ALTER TABLE, including literal and expression defaults. Updated defaults appear in column metadata and apply to subsequent inserts.
  • Bug Fixes
    • Improved handling and validation of signed numeric and BIT defaults.
    • Missing tables and columns now return more accurate MySQL-compatible errors.
    • Improved recognition of ON UPDATE CURRENT_TIMESTAMP column settings.

The plugin exposes MySQL on SQLite through a symlink. Apply the existing driver exemption there as well, since this code implements the database layer and cannot use wpdb.
@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a1ec678f-91af-4a94-a6bb-cb785e0ce161
📥 Commits

Reviewing files that changed from the base of the PR and between 874ffb4 and cd96b9e.

📒 Files selected for processing: 2
  • packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-builder.php
  • packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

📝 Walkthrough

The change adds ALTER COLUMN ... SET DEFAULT and DROP DEFAULT handling, including default normalization and validation. It also adds MySQL-compatible errors for missing tables and columns, expands test coverage, and updates a PHPCS exclusion.

Changes: ALTER COLUMN defaults

Layer / File(s) Summary
ALTER error contracts and target checks
packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-exception.php, packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php, packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php, packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Metadata_Tests.php
Adds an information-schema exception for missing columns and maps it to MySQL-compatible error details. ALTER processing now reports a missing target table. Tests cover missing table and column cases and correct the target table in a constraint test.
Default recording and normalization
packages/mysql-on-sqlite/src/sqlite/class-wp-sqlite-information-schema-builder.php, packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php, packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php
ALTER actions record or clear column defaults. Literal and expression defaults, nullability, auto-increment behavior, BIT values, and ON UPDATE CURRENT_TIMESTAMP metadata are handled and tested. Tests also cover temporary tables, combined actions, and rollback after invalid changes.

PHPCS exclusion

Layer / File(s) Summary
Restricted-class rule exclusion
phpcs.xml.dist
Adds the SQLite database integration directory to the exclusion for the WordPress.DB.RestrictedClasses.mysql__PDO rule.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant execute_alter_table_statement
  participant record_alter_table
  participant InformationSchema
  Client->>execute_alter_table_statement: Submit ALTER TABLE
  execute_alter_table_statement->>record_alter_table: Process ALTER actions
  record_alter_table->>InformationSchema: Record column default metadata
  InformationSchema-->>execute_alter_table_statement: Return updated schema metadata
Loading

Possibly related issues

Suggested reviewers: brandonpayton

Merge Risk: 🟡 Moderate · up to cd96b

Setting an expression default can stop automatic timestamp updates while the schema still reports them. Resolve or explicitly accept that limitation before merging.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 55.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 40 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely identifies the primary change: support for ALTER COLUMN with SET DEFAULT and DROP DEFAULT. It matches the main implementation and test coverage.
Linked Issues check Passed The PR satisfies the coding requirements in directly linked issue #553. record_alter_table() now records SET DEFAULT and DROP DEFAULT changes in information-schema metadata. Table rebuilds there…
Out of Scope Changes check Passed The changes remain within the stated PR scope. Trigger detection, missing-table and missing-column errors, exception conversion, and BIT default handling support the described compatibility and error …
Full details: Linked Issues check

Explanation

The PR satisfies the coding requirements in directly linked issue #553. record_alter_table() now records SET DEFAULT and DROP DEFAULT changes in information-schema metadata. Table rebuilds therefore use the new defaults for later inserts and preserve existing values. The tests cover permanent and temporary tables, metadata, literals, expressions, quoted names, ordered actions, invalid defaults, missing objects, rollback, and automatic ID generation.

Full details: Out of Scope Changes check

Explanation

The changes remain within the stated PR scope. Trigger detection, missing-table and missing-column errors, exception conversion, and BIT default handling support the described compatibility and error cases. The added tests verify the requested behavior. The PDO exclusion covers the symlinked driver named in the PR description. No unrelated change is demonstrated.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

🔧 PHPStan (2.3.0)

PHPStan reported 1 non-file analysis error(s); analysis is incomplete

🔧 PHPMD (3.0.0)
packages/mysql-on-sqlite/tests/WP_MySQL_On_SQLite_Tests.php

PHPMD could not process this file (exit code 255): 0/1 [░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20971520 bytes) in phar:///usr/bin/phpmd/vendor/pdepend/pdepend/src/Util/Cache/Driver/FileCacheDriver.php on line 183

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@JanJakes
JanJakes force-pushed the alter-column-defaults branch from 44be48b to 0e17bbd Compare October 22, 2026 09:45
@JanJakes JanJakes changed the title Apply ALTER COLUMN default changes Add support for ALTER COLUMN with SET DEFAULT and DROP DEFAULT Oct 22, 2026
@JanJakes
JanJakes force-pushed the alter-column-defaults branch from 0e17bbd to f17b3bf Compare October 22, 2026 12:26
@JanJakes
JanJakes marked this pull request as ready for review October 22, 2026 12:27
@JanJakes
JanJakes force-pushed the alter-column-defaults branch from f17b3bf to 0ce3c18 Compare October 22, 2026 12:43
@JanJakes
JanJakes force-pushed the alter-column-defaults branch from 0ce3c18 to 874ffb4 Compare October 22, 2026 20:08
Record SET DEFAULT and DROP DEFAULT using shared default handling, in
order with the other ALTER TABLE actions. Preserve unrelated column
metadata and reject missing columns.

Reject NULL defaults on NOT NULL columns and keep accepted AUTO_INCREMENT
defaults unset. Cover inserted values, schema metadata, temporary tables,
quoted names, expressions, and rollback on failure.

Fixes #553
@JanJakes
JanJakes force-pushed the alter-column-defaults branch from 874ffb4 to b585b07 Compare October 22, 2026 20:33

@brandonpayton brandonpayton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm Codex, posting this local review feedback at @brandonpayton's request through their authenticated GitHub account.

I have two correctness concerns and one compatibility concern, detailed inline. The existing MySQL-on-SQLite suite passes (1,135 tests; 15 skipped, 2 incomplete). The timestamp regression needs a behavioral assertion beyond checking metadata and inserted defaults. The default-validation gap predates this PR and could be addressed in a separate follow-up.

if ( str_contains( $extra, 'auto_increment' ) ) {
throw WP_SQLite_Information_Schema_Exception::invalid_default_value( $column_name );
}
$extra = trim( 'DEFAULT_GENERATED ' . $extra );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex review feedback, requested by @brandonpayton.

Expression defaults can disable ON UPDATE CURRENT_TIMESTAMP. The rebuild checks whether EXTRA exactly equals on update CURRENT_TIMESTAMP. Adding DEFAULT_GENERATED causes it to skip recreating the trigger, leaving timestamps stale even though the reported schema retains ON UPDATE.

Suggested fix: Use str_contains() for that attribute check in get_sqlite_create_table_statement(). Add a regression test that updates an existing row while the expression default is active, covering permanent and temporary tables.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I found this during self-review but decided to extract it to #554. The reason is that doing only the str_contains fix amplifies another issue with the timestamp trigger (it always overrides explicit values), and so I think it's better to fix both together as a follow-up.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prepared a fix in #560.

Comment on lines +1044 to +1046
$default = $literal
? $this->get_literal_default( $literal, $column['DATA_TYPE'], $column_name )
: $this->get_default_value( $node, $column['DATA_TYPE'], $column_name );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex review feedback, requested by @brandonpayton.

Invalid literal defaults can be accepted and break later inserts. For example, SET DEFAULT 'invalid' succeeds on an INT column, then inserts omitting that column fail. This is an existing validation gap exposed by the new ALTER support.

Suggested fix: Add shared validation and normalization using the literal AST and complete column metadata. Reuse it across CREATE and ALTER operations, raise the existing invalid-default exception, and test rollback. This broader work could be a separate follow-up.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. This is fairly complex because it requires extensive type conversion and validation in PHP. It’s already tracked in #551 (and pre-existing for all existing ways to set column defaults). I guess I should address it soon in a follow-up, because particular instances of this issue keep coming up.

Comment on lines +1063 to +1064
if ( str_contains( $extra, 'auto_increment' ) ) {
$default = null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex review feedback, requested by @brandonpayton.

Defaults on AUTO_INCREMENT columns are accepted but discarded. The comments and tests acknowledge this divergence, but successful execution gives callers the impression that the requested behavior was applied.

Suggested fix: Either preserve the additional metadata needed to emulate the behavior or explicitly reject unsupported changes. I'd avoid locking silent acceptance into the tests.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a MySQL bug that I don’t think is worth reproducing, but I like the idea of explicitly rejecting these cases. MySQL silently applies the default without exposing it in SHOW CREATE TABLE or INFORMATION_SCHEMA, so even a schema export/import loses it (looks like a bug indeed).

@JanJakes JanJakes Oct 23, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are known MySQL bugs indeed: #111702, #80222

Added the explicit rejection in: cd96b9e

@brandonpayton

Copy link
Copy Markdown
Member

I had Codex do an initial review and discuss its findings. They seemed worth discussing so I had them posted here.

Reject ALTER COLUMN SET DEFAULT and DROP DEFAULT instead of silently
discarding them. MySQL hides these defaults from schema metadata, so
matching its behavior would require separate hidden state.

Verify rejection, rollback, and subsequent inserts for regular and
temporary tables.

#555 (comment)
@JanJakes

Copy link
Copy Markdown
Member Author

@brandonpayton Thanks! I replied to all the findings. Two are tracked as follow-up work, and I made the suggested fix for one of them. I'll go ahead and merge this to continue working on some related issues.

@JanJakes
JanJakes merged commit 503958e into trunk Oct 23, 2026
25 checks passed
@JanJakes
JanJakes deleted the alter-column-defaults branch October 23, 2026 09:10
JanJakes added a commit that referenced this pull request Oct 23, 2026
## Summary

A numeric zero default on a temporal column was stored verbatim and
emitted as a quoted string:

```sql
CREATE TABLE t (created_at TIMESTAMP NOT NULL DEFAULT 0);
SHOW CREATE TABLE t;
-- `created_at` timestamp NOT NULL DEFAULT '0'                    (MySQL: '0000-00-00 00:00:00')
```

MySQL rejects `DEFAULT '0'` in strict SQL mode (`ERROR 1067: Invalid
default value`), so dumps of these tables don't import. The H5P plugin
creates its tables with this pattern, so exports of sites that use it
fail.

## Changes

Zero defaults of temporal columns now get MySQL's zero values, both in
new tables and in existing databases:

- **Normalize on write.** `CREATE TABLE` and `ALTER TABLE` store zero
defaults of `DATE`, `TIME`, `DATETIME`, `TIMESTAMP`, and `YEAR` columns
as MySQL zero values, including fractional seconds precision
(`DATETIME(3)` → `'0000-00-00 00:00:00.000'`). This covers all numeric
forms, such as `0`, `-0`, `0.0`, `0e0`, and `FALSE`, and it fixes `SHOW
CREATE TABLE`, `INFORMATION_SCHEMA.COLUMNS`, and the default that new
rows receive.
- **Normalize `ALTER COLUMN ... SET DEFAULT`.** Defaults set with `SET
DEFAULT`, added in #555, are normalized the same way. This includes a
parenthesized literal, such as `SET DEFAULT (0)`, which MySQL also
stores as a literal.
- **Normalize quoted zeros, except for `YEAR`.** Quoted zeros, such as
`DEFAULT '0'`, are normalized the same way, as older driver versions
stored them the same as numbers. For `YEAR` columns, MySQL reads the
strings `'0'` and `'00'` as the year 2000, so they stay as written.
- **Fix `DATETIME_PRECISION`.** `INFORMATION_SCHEMA.COLUMNS` reported
`0` for all `TIME(n)`, `DATETIME(n)`, and `TIMESTAMP(n)` columns. It now
reports the precision of the column type.
- **Repair existing databases.** For databases last configured by driver
3.1.1 or older, the configuration corrects the stored
`DATETIME_PRECISION` values, and it changes zero defaults of temporal
columns, such as `0`, to the zero values with `ALTER TABLE ... ALTER
COLUMN ... SET DEFAULT`. This also recreates the tables, so that new
rows get the zero value. This includes tables created by the legacy
SQLite driver. The repair runs once, within the configuration
transaction, so it either fully succeeds or changes nothing.
- **Repair existing rows.** Rows that got such a zero default from older
driver versions still store it, such as `'0'`. The repair replaces it
with the zero value, so that dumps of these rows import into MySQL in
strict SQL mode. The update doesn't run the table's triggers, such as
the emulation of `ON UPDATE CURRENT_TIMESTAMP`.
- **Report why a table rebuild fails.** `ANALYZE`, `CHECK`, `OPTIMIZE`,
and `REPAIR TABLE` reported every general SQLite error as a missing
table, so a failed table rebuild could say `Table '…' doesn't exist` for
a table that exists. They now report only SQLite's "no such table" error
that way, and other errors as SQLite returns them.

Values match MySQL 8 output for the same default in `CREATE TABLE` or
`ALTER COLUMN ... SET DEFAULT`, in `STRICT_TRANS_TABLES` mode:

| Column | `COLUMN_DEFAULT` |
| --- | --- |
| `TIMESTAMP` / `DATETIME DEFAULT 0` | `0000-00-00 00:00:00` |
| `DATE DEFAULT 0` | `0000-00-00` |
| `TIME DEFAULT 0` | `00:00:00` |
| `YEAR DEFAULT 0` | `0000` |
| `DATETIME(3) DEFAULT 0` | `0000-00-00 00:00:00.000` |
| `TIMESTAMP(6) DEFAULT 0` | `0000-00-00 00:00:00.000000` |
| `TIME(2) DEFAULT 0` | `00:00:00.00` |

## Scope

- **Non-zero temporal defaults**, such as `DEFAULT 20200102`, are still
stored verbatim. The existing `@TODO` covers them.
- **`YEAR` string zeros**, such as `'0'`, stay as written. MySQL reads
them as the year 2000, but the driver also stores an inserted `'0'` as
`0000`, so both need a separate fix, tracked in #545. Older driver
versions stored `YEAR DEFAULT 0` and `YEAR DEFAULT '0'` the same way, so
the repair can't tell them apart and leaves them unchanged.
- **Recreated tables reset their `AUTO_INCREMENT` counter** to the
highest existing ID. This is an existing problem of `ALTER TABLE` and
`OPTIMIZE TABLE`, tracked in #544.
- **Recreated legacy tables lose `ON UPDATE CURRENT_TIMESTAMP`.** The
legacy SQLite driver implemented it with triggers, which a table rebuild
drops without replacing them. This is an existing problem of `ALTER
TABLE` and `OPTIMIZE TABLE`, tracked in #547. The repair also rebuilds
legacy tables that have a temporal zero default, so they lose it as
well.

## Testing

End to end, the `SHOW CREATE TABLE` output for H5P's `wp_h5p_contents`
and `wp_h5p_contents_user_data` imports into MySQL with
`sql_mode='STRICT_TRANS_TABLES'`. Before the change, it fails with
`ERROR 1067`. See the [verification on a WordPress Studio
site](#542 (comment)).

---

AI assistance: Claude (Anthropic) via the Pi coding agent, with an
initial candidate from a Homeboy cook. Claude traced the failure,
reworked and tested the implementation, verified it against MySQL, and
drafted this PR at Chris Huber's direction.

Fixes #541.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Bug Fixes**
* Temporal columns with numeric-zero defaults now use MySQL-compatible
zero dates or times, including fractional seconds.
* Temporal defaults are consistently represented in table definitions,
metadata, and inserted or altered rows.
* Existing databases are normalized when reopened, including temporal
precision corrections for older driver versions.
  * Non-numeric defaults and string `YEAR` zero values remain unchanged.
* Foreign-key relationships and active transactions remain intact during
reconfiguration.
* Failed table repairs now roll back cleanly and preserve the underlying
error details.
* Administrative errors distinguish missing tables from other database
errors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Jan Jakeš <jan@jakes.pro>
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.

ALTER COLUMN ignores SET DEFAULT and DROP DEFAULT

2 participants