Skip to content

fix(plugin-mysql): read a nullable column's DEFAULT NULL back as NULL on MySQL and MariaDB - #3067

Merged
datlechin merged 1 commit into
mainfrom
fix/mysql-null-column-default
Sep 23, 2026
Merged

datlechin merged 1 commit into
mainfrom
fix/mysql-null-column-default

Conversation

@datlechin

Copy link
Copy Markdown
Member

Fixes #3058

Root cause

The MySQL plugin decoded a catalog default without looking at the column's nullability. MySQL and MariaDB both report SQL NULL for a nullable column's default, whether it was declared DEFAULT NULL or declared with no default at all. MySQL's manual says those are the same thing: an omitted default on a nullable column is an explicit DEFAULT NULL. The decoder mapped every SQL NULL to "no default", which the Default cell paints as the italic Empty. Saving NULL worked. The read threw it away.

The same function carried a second shipped defect (0.73.0, #2690). On MariaDB 10.2.7 and later, fetchColumns read SHOW FULL COLUMNS but applied INFORMATION_SCHEMA's quoting rules to it. SHOW FULL COLUMNS quotes nothing, so DEFAULT 'abc' was read as the SQL abc. The next edit of that column then failed or changed the default:

  • a comment edit, a reorder or a nullability change failed with ERROR 1054, or with ERROR 1064 for '' and dates
  • the string 'NULL' silently became SQL NULL

Fix

Read side (MySQLCatalogDefault.swift, MySQLPluginDriver+Schema.swift)

  • A catalog default now travels with the form of the read that produced it: .bare (SHOW COLUMNS everywhere, INFORMATION_SCHEMA on MySQL) or .quoted (INFORMATION_SCHEMA on MariaDB 10.2.7+). The old quotesLiterals flag described one read and was applied to another.
  • mysqlColumnDefault reads SQL NULL as NULL on a nullable column and as no default on a NOT NULL one. A generated or AUTO_INCREMENT column has no default.
  • MariaDB 10.2.7+ takes its defaults from the per-table INFORMATION_SCHEMA read that already ran for generation expressions. SHOW FULL COLUMNS stays the primary read, because proxies answer it truthfully and answer the catalog with nothing.
  • SHOW CREATE TABLE supplies the defaults nothing else states exactly:
    • on a MariaDB whose catalog did not answer (before 10.2.7, or blind or refused), where uuid() and 'uuid()' read alike
    • for a MySQL expression default holding non-ASCII text, which the catalog encodes twice. The catalog's backslash escaping is undone exactly, so every other expression default costs no extra read.
  • The per-table read and the whole-schema read used by Compare & Sync and database-wide Copy apply the same rules. The whole-schema read runs SHOW CREATE TABLE only for tables that hold such a default.
  • The SHOW CREATE TABLE column parser moved from OceanBaseColumnDefaults to MySQLCreateTableScanner. It still reads one definition per line, because OceanBase prints SET and ENUM members with unbalanced quotes. A line that opens a quoted column name without closing it now runs on into the next, so a line break inside a name cannot hand the rest of it to another column as a default.

Write side (MySQLColumnDefinitionSQL.swift)

  • NULL is written bare on every type. On TEXT, BLOB, JSON and GEOMETRY it was written DEFAULT (NULL), which MySQL 8 stores as an expression default and MySQL 5.7 rejects.

App

  • EditableColumnDefinition.setNullable(_:) clears a NULL default when a column becomes NOT NULL. Otherwise, now that nullable columns read as NULL, turning Nullable off would fail almost everywhere with ERROR 1067. Setting Primary Key and the Create Table auto-increment promotion go through it too.
  • A column the user changes so that it refuses NULL and defaults to NULL is reported by the structure editor and the Create Table sheet before anything runs. A column that already held that pair is not reported (SQLite and DuckDB accept it).
  • TableRows.serverAssignsValue does not count a NULL default. Add Row keeps sending NULL rather than leaving nullable MySQL columns to the server.
  • Cross-engine copy no longer carries a NULL default. SQL Server would turn it into a named constraint per column, and a later DROP COLUMN would fail on each one.
  • Schema compare treats a NULL default on a nullable column as no default. MySQL DEFAULT NULL against PostgreSQL's nothing is not a difference.

Probe script

  • scripts/check-mysql-column-defaults.sh [host] [port] [user] asserts, against a live server, every catalog shape the decoder hard-codes. It gates each fixture on the server version.

Measured

MySQL 8.4.11 MariaDB 12.3.3 / 13.0.2
nullable, no clause, SHOW FULL COLUMNS SQL NULL SQL NULL
nullable DEFAULT NULL, SHOW FULL COLUMNS SQL NULL SQL NULL
nullable DEFAULT NULL, INFORMATION_SCHEMA SQL NULL text NULL
DEFAULT 'abc', SHOW FULL COLUMNS abc abc (unquoted)
DEFAULT 'abc', INFORMATION_SCHEMA abc 'abc'
DEFAULT (concat('日','x')), catalog escaped, 日 as æ\u{97}¥ not applicable
NOT NULL DEFAULT NULL ERROR 1067 ERROR 1067

Before and after, from a harness that runs the plugin's real fetchColumns and generateModifyColumnSQL against live servers. It re-runs a MODIFY for every column with the default exactly as read, then reads the table again:

origin/main this branch
MySQL 8.4, Name after MODIFY … NULL DEFAULT NULL reads back nil (Empty) reads back NULL
MySQL 8.4, MODIFY of an expression default holding it's ERROR 1064 round-trips
MySQL 8.4, expression default holding 日 and é not applicable round-trips byte for byte
MariaDB 13, MODIFY of 'abc', '', '2020-01-01' defaults ERROR 1054, 1064, 1064 round-trip
MariaDB 13, string default 'NULL' silently becomes SQL NULL kept
MariaDB 13, SHOW CREATE TABLE against INFORMATION_SCHEMA decoding not applicable all 13 columns agree
whole-schema read against per-table read, both servers not applicable all 13 columns agree

Tests

  • MySQLColumnDefinitionSQLTests:
    • the byte-exact server strings above, for MySQL bare, MariaDB quoted and the MariaDB SHOW FULL COLUMNS fallback
    • MySQL SHOW CREATE TABLE expression defaults, non-ASCII included
    • the MariaDB SHOW CREATE TABLE fallback
    • the unescape and its non-ASCII guard, the parser against a line break in a quoted name, and bare NULL on every type
  • StructureNullDefaultTests:
    • Nullable and Primary Key clear a NULL default
    • both edit orders are refused; an untouched column and an already-existing pair are not
    • undo restores both fields
  • CreateTableDraftBuilderTests, RowOperationsManagerTests, StructureDiffEngineTests and CrossEngineStructureTranslatorTests for the app-side changes.
  • ColumnDefaultRoundTripTests.mysqlTextColumnParenthesisesOnce asserted that the menu's NULL is written DEFAULT (NULL) on a TEXT column. That was the defect, measured above, so the assertion now expects bare NULL.

There is no UI test. The flow needs a live MySQL or MariaDB server, and the UI test runner has none.

Before / After

No screenshots. Two other sessions were running TableProUITests on this machine, and a second TablePro process fails their XCUITest host. The reporter's recording shows the before. After: the same Name column reads NULL in the Default cell and keeps it after Cmd+S and Cmd+R.

Known limits

  • A MySQL nullable column whose default was removed with ALTER COLUMN … DROP DEFAULT reads as NULL. The catalog reports it exactly like DEFAULT NULL, and for TEXT so does SHOW CREATE TABLE. TablePro cannot create that state: a MODIFY without a DEFAULT clause gives DEFAULT NULL.
  • Turning Nullable off and back on leaves the NULL default cleared and the column staged, the same way clearing Generated drops the expression.

@mintlify

mintlify Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 23, 2026, 3:35 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@datlechin
datlechin force-pushed the fix/mysql-null-column-default branch from 5520511 to 9f1c9ad Compare September 23, 2026 15:35
@datlechin
datlechin merged commit 0515b3b into main Sep 23, 2026
8 checks passed
@datlechin
datlechin deleted the fix/mysql-null-column-default branch September 23, 2026 19:19

This branch was successfully deployed

1 active deployment
staging - docs — 9f1c9ad4 Deployed Sep 23, 2026 by mintlify[bot]
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.

Can't set default value of column to Null via query or via Inspector. It remains as empty.

1 participant