In 2009 I collected a wishlist while using MySQL for web applications. Some requests were small syntax conveniences; others reached into trigger semantics, storage engines, and query execution. The original wishes come first below, in the form in which I wanted to use them. Each is followed by what changed in MySQL 8.4 and whether the requested capability is now available.
The labels are deliberately strict. ✓ Available means that MySQL now implements the requested capability directly. ◑ Partially available means that one part exists or a modern feature solves only the common case. ✗ Still unavailable means that the original operation still has no direct equivalent.
AFTER triggers
What I wanted. I wanted an AFTER INSERT trigger to receive the generated AUTO_INCREMENT value through NEW.id. I also wanted that trigger to update other rows in the same table, for example to link the inserted row into a list maintained by neighboring rows.
What changed: ◑ Partially available. An AFTER INSERT trigger can read the generated identifier through NEW.id. The important timing distinction is documented explicitly: in a BEFORE trigger, NEW.id for an AUTO_INCREMENT column is still 0; after the row has been inserted, NEW.id contains the generated identifier.
The second half of the wish remains unavailable. A stored function or trigger cannot modify a table already used by the statement that activated it. An AFTER trigger therefore cannot update neighboring rows in its own table to maintain a linked list. That invariant still has to be maintained by the calling transaction, usually with explicit locking and ordinary DML.
Disabling triggers
What I wanted. I wanted to suppress triggers for one deliberate statement with an option such as SQL_NO_TRIGGER, without dropping the trigger or adding bypass logic to every trigger body.
What changed: ✗ Still unavailable. MySQL 8.4 has no per-statement trigger suppression switch comparable to the proposed SQL_NO_TRIGGER, and it has no ALTER TRIGGER ... ENABLE or DISABLE operation. Bulk maintenance must either tolerate the triggers, make their bodies conditional on application-controlled state, or drop and recreate them. A session variable used as a bypass is only a convention inside custom trigger code, not a server-enforced privilege boundary.
Duplicate-key handlers
What I wanted. I wanted a duplicate-key handler that could react as its own trigger event and apply custom conflict logic, rather than being limited to the assignments in INSERT ... ON DUPLICATE KEY UPDATE.
What changed: ✗ Still unavailable. INSERT ... ON DUPLICATE KEY UPDATE reacts to a conflict in a UNIQUE index or primary key. Triggers react to INSERT, UPDATE, or DELETE events; there is no separate duplicate-key trigger event, and a non-unique index does not define a conflict. Application-specific matching still requires an explicit transaction or stored procedure.
Trigger DDL
What I wanted. I wanted idempotent and editable trigger definitions: CREATE TRIGGER IF NOT EXISTS, CREATE OR REPLACE TRIGGER, and an ALTER TRIGGER operation. Renaming or moving a table should preserve its triggers without manual reconstruction.
What changed: ◑ Partially available. MySQL 8.4 accepts CREATE TRIGGER IF NOT EXISTS, and a same-schema table rename keeps the triggers associated with that table. Moving a table with triggers into another schema still fails, however, and a rename is not a general rewrite of object names embedded in trigger bodies.
The original lifecycle requests remain incomplete. There is no CREATE OR REPLACE TRIGGER and no general ALTER TRIGGER; changing a definition still means dropping and recreating it. Multiple triggers may now share the same timing and event, with PRECEDES and FOLLOWS controlling their order, which is a useful later addition but not a replacement operation.
Virtual columns
What I wanted. I wanted virtual columns in both directions: readable values derived from other columns, and a write-only input that could be supplied to INSERT or UPDATE and consumed by a trigger without becoming stored row data.
What changed: ◑ Partially available. MySQL now has generated columns. A VIRTUAL generated column is evaluated when rows are read and occupies no storage, while a STORED generated column is materialized during writes. This cleanly covers values derived from other columns:
CREATE TABLE rectangle (
width DECIMAL(10, 2),
height DECIMAL(10, 2),
area DECIMAL(20, 4) AS (width * height) VIRTUAL
); It does not implement the proposed write-only payload column. When a generated column is named in INSERT, REPLACE, or UPDATE, the only permitted explicit value is DEFAULT. Triggers also cannot refer to generated columns through NEW or OLD. Data that exists only to influence a write therefore belongs in a stored procedure parameter, a real column, or the application transaction.
MERGE tables
What I wanted. I wanted a MERGE table over InnoDB or otherwise heterogeneous child tables, with a virtual value for choosing the destination table and one global AUTO_INCREMENT sequence across all children.
What changed: ✗ Still unavailable. The MERGE engine still combines only identical MyISAM tables. Its insertion target remains a fixed policy: FIRST, LAST, or no inserts. It neither accepts InnoDB children nor offers a virtual routing value or a global AUTO_INCREMENT sequence across its children.
Native InnoDB partitioning is the modern answer when one logical table should divide its rows among partitions. It preserves one table definition and one key space, but it is not a heterogeneous MERGE table and cannot route rows into arbitrary existing tables.
Syntax simplification
What I wanted. I wanted TRUNCATE TABLE to accept several tables in one statement. For generated multi-row inserts, I also wanted adjacent parenthesized rows to be accepted without forcing application code to place commas between them.
What changed: ✗ Still unavailable. TRUNCATE TABLE still accepts one table, so truncating several tables requires several statements. Multi-row INSERT is supported, but commas between row constructors remain mandatory:
INSERT INTO t1 (a, b)
VALUES (1, 1), (2, 1), (3, 1); This is not merely parser ceremony: each parenthesized expression is a row constructor in a list. Client libraries should generate the separators or use batch APIs rather than assembling the final comma conditionally by hand.
DML with result sets
What I wanted. I wanted UPDATE and DELETE to return selected old or new column values through a RETURNING result set, instead of issuing a separate query or assigning values as a side effect inside a predicate.
What changed: ✗ Still unavailable. MySQL 8.4 does not support RETURNING for UPDATE or DELETE, whether the desired rows are the old values or the new values. The affected-row count is returned, but not an arbitrary result set.
The reliable pattern is a transaction: select the rows with an appropriate lock, apply the mutation, and commit. That makes the intended concurrency behavior visible. Smuggling a selected value into a user variable inside a predicate depends on expression evaluation and turns a false or zero value into a control-flow hazard; it is not an equivalent result channel.
Views with user variables
What I wanted. I wanted views to read caller-supplied user variables as parameters and to assign selected values to variables such as @last_value, making a view behave more like a reusable parameterized query.
What changed: ✗ Still unavailable. A view definition cannot refer to user variables or stored-program parameters. This excludes both assigning @last_value from a view and treating @parameter as an argument supplied by the caller.
A view is a persistent relational expression, not a parameterized routine. Put caller-specific values in the query that selects from the view, or use a stored procedure when a named server-side operation genuinely needs parameters and multiple statements.
Column assignment and swapping
What I wanted. I wanted to calculate a value once and assign it to several columns with chained syntax such as SET a := x := value. I also wanted a direct, type-independent way to swap two column values without a temporary variable or integer-only XOR trick.
What changed: ◑ Partially available. MySQL still has no chained column-assignment syntax such as SET a := x := value. A common table expression or one-row derived table can nevertheless name a long value once and assign it to multiple targets. The exact SQL depends on whether one or several tables are updated.
Single-table assignments are evaluated from left to right. Consequently, SET a = b, b = a does not swap values: the second assignment sees the already updated a. The historical XOR sequence works only for non-NULL integers and is too narrow for a general swap. Preserve the old values explicitly in the application or in a materialized derived row when the operation must support arbitrary column types. For multiple-table updates, MySQL does not guarantee assignment order at all.
Table value constructors
What I wanted. I wanted a compact expression such as LIST(1, 2, 3) that turns literal values into rows and can be queried, joined, or combined like a small temporary table.
What changed: ✓ Available. The old LIST(1, 2, 3) idea now has a standard-shaped counterpart. VALUES is a standalone table value constructor and can participate in joins and set operations:
VALUES ROW(1), ROW(2), ROW(3), ROW(4); The generated column is named column_0. For an arithmetic sequence rather than a literal list, a recursive common table expression can generate rows without spelling out every value.
Date and time
What I wanted. I wanted a dedicated RFC 822 date formatter and Unix timestamps that extend before 1970 as negative values instead of stopping at the Unix epoch.
What changed: ✗ Still unavailable. There is still no dedicated RFC 822 formatter; DATE_FORMAT() remains the direct built-in approach, with the time-zone offset and English day and month names handled deliberately by the session or application.
UNIX_TIMESTAMP() also retains Unix-epoch range semantics. On modern 64-bit builds its upper range extends far beyond 2038, but a date before 1970 is outside the accepted argument range and returns 0, not a negative number. TO_SECONDS() can represent earlier dates relative to year zero, but that is a different epoch and not a drop-in Unix timestamp.
Query optimization
What I wanted. I wanted MySQL to coalesce identical concurrent reads so that one running query could feed all waiting clients. I also wanted a global LIMIT over UNION to stop executing later branches as soon as enough rows had been collected.
What changed: ✗ Still unavailable. MySQL does not coalesce identical in-flight reads so that one execution feeds every waiting client. The old query cache was removed in MySQL 8.0, and it was a completed-result cache rather than the requested coordination mechanism. Applications that need request coalescing or caching must provide it outside the server.
A global LIMIT over UNION is likewise not a promise that later query blocks will be skipped once an earlier block produced enough rows. Duplicate elimination, ordering, and optimizer choices can require additional work, and without an ORDER BY the identity of the limited rows is not deterministic. When the real rule is “use old rows only if new rows are insufficient,” express the two phases explicitly in application logic or in SQL that limits each source and defines a deterministic final order.
The retrospective is mixed: table value constructors arrived cleanly, and generated columns solve a substantial class of old trigger workarounds. Most requests that alter trigger isolation, DML result channels, or storage-engine boundaries remain absent because they change deeper execution guarantees rather than syntax alone.
References
- [TriggerSyntax]Oracle, MySQL 8.4 Reference Manual: Trigger Syntax and Examples.
- [StoredRestrictions]Oracle, MySQL 8.4 Reference Manual: Restrictions on Stored Programs.
- [GeneratedColumns]Oracle, MySQL 8.4 Reference Manual: CREATE TABLE and Generated Columns.
- [Merge]Oracle, MySQL 8.4 Reference Manual: The MERGE Storage Engine.
- [Update]Oracle, MySQL 8.4 Reference Manual: UPDATE Statement.
- [Values]Oracle, MySQL 8.4 Reference Manual: VALUES Statement.
- [Views]Oracle, MySQL 8.4 Reference Manual: CREATE VIEW Statement.
- [DateTime]Oracle, MySQL 8.4 Reference Manual: Date and Time Functions.