[3.0] Stops drafts from silently discarding every change after the first save - #9339
Merged
jdarwood007 merged 2 commits intoAug 2, 2026
Merged
Conversation
Loading an existing draft assigned poster_time to $this->type, so the type of every loaded draft became a unix timestamp and poster_time stayed 0. The next save then wrote that timestamp back into user_drafts.type, which is a tinyint. On PostgreSQL that is a smallint, so the whole UPDATE fails with "smallint out of range": the draft keeps its old body, and because saveToDatabase() sets $context['draft_saved'] before looking at anything, save() still reports success. Autosaving an existing draft therefore threw the changes away without a word. It also meant $this->type never equalled 1 for a loaded PM draft, so the autosave response filled in $context['id_draft'] instead of $context['id_pm_draft'], and the five second autosave throttle in save() never fired, since it tests a poster_time that was always 0. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
saveToDatabase() returned the new draft ID in $context['id_pm_draft'] for PM drafts, which is how 2.1 named it. Everything in 3.0 - PM::compose2(), DraftPM::prepare(), the hidden field Editor.php builds - uses id_draft for both kinds of draft, so that value went nowhere. It only stayed unnoticed because $this->type never actually held 1: the previous commit was what made the PM branch of that expression reachable. Without this, saving a new PM draft would leave id_draft at 0 in the form and the next save would create a second draft instead of updating the first. Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
This was referenced Aug 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Editing an existing draft never saves. The first save creates the row, and every save after that is thrown away — no error, no message, and
Draft::save()still reports success.The cause is a copy-paste slip in
Draft::__construct(), from 5520b18 ("Ensure values have the right type when loading drafts"):read()doesSELECT *, soposter_timearrives right aftertypeand overwrites it. Every loaded draft therefore ends up with a unix timestamp in$this->type, and$this->poster_timestays 0.The next save writes that timestamp straight back into
user_drafts.type, which the schema declares as atinyint. On PostgreSQL that is asmallint:The whole
UPDATEfails, so the draft keeps its old body.saveToDatabase()setsUtils::$context['draft_saved'] = truebefore looking at anything, sosave()returnstrueanyway, and the PostgreSQL layer does not raise on a failed query, so nothing reaches the error log either. MySQL does not fail the statement in the same way, but it stores a nonsensetypeand the draft stops matching theAND type = {int:type}filter inread(), so it disappears from the editor.Two knock-on effects go with it, and both are fixed by the same line:
save()tests$this->poster_time, which was always 0, so it never fired;$this->typenever equalled 1 for a PM draft.That second one needs the follow-up commit.
saveToDatabase()returns the draft ID in$context['id' . ($this->type === 1 ? '_pm' : '') . '_draft'], which is how 2.1 named it. Nothing in 3.0 readsid_pm_draft—PM::compose2(),DraftPM::prepare()and the hidden fieldEditor.phpbuilds all useid_draftfor both kinds of draft. The brokentypewas the only reason that expression never took the_pmbranch, so fixing the first bug on its own would have left PM drafts withid_draft=0in the form and created a new draft on every save. The dead key is dropped.Testing
Verified against the Docker environment on PostgreSQL 17, before and after, in the same session:
poster_timeupdated,typestill 0id_draft=7typestill 1, no duplicate rowvendor/bin/phpunit— 108 tests, 157 assertions, OK.Not covered here: the PostgreSQL API's
query()swallows failures entirely (@pg_query()with no error path), which is why this was invisible for a year and a half, andsaveToDatabase()reports success without checking anything. Both are worth their own look; neither is needed to fix this.Found while splitting #7933, but this is server-side only and touches no theme file — it applies equally to the current default theme and the new one.
Issues References (Fixes|Related|Closes)
Related to #7933, #9337