RTC bug PR-5.4-dev Pending

User tests: Successful: Unsuccessful:

avatar genr8r
genr8r
4 Jun 2026

Hi all,

Hit this on a production Joomla 5.4.6 site while trying to clean up a stuck check-in state. Tracing it back showed the bug isn't environment-specific, it's a predicate mismatch in CheckinModel that any site can hit once a row has been edited and released via the standard Table API.

Pull Request resolves # none (bug not previously reported on the issue tracker).

  • I read the Generative AI policy and my contribution is either not created with the help of AI or is compatible with the policy and GNU/GPL 2 or later.

Summary of Changes

CheckinModel::checkin() and CheckinModel::getItems() both use WHERE checked_out IS NOT NULL for nullable checked_out columns. By Joomla's own convention, checked_out = 0 means "not locked": Joomla\CMS\Table\Table::checkIn() explicitly sets $this->checked_out = 0 to release a lock, and Table::isCheckedOut() tests > 0. The IS NOT NULL predicate matches 0, so every row that has been edited and properly released via the API gets counted as needing check-in.

This PR replaces the IS NOT NULL clause with > 0 in both methods. > 0 is correct for nullable columns too because NULL > 0 evaluates to NULL, which WHERE treats as false. The change is two identical hunks; the if/else branches on column nullability become redundant and collapse to a single line.

Testing Instructions

  1. Pick any table that has a nullable checked_out column (e.g. #__modules, #__menu, #__ats_cannedreplies if you have Akeeba ATS installed, or any third-party table that defaults checked_out to 0).
  2. Set up the "released-via-API" state on one row via SQL: UPDATE #__modules SET checked_out = 0, checked_out_time = NULL WHERE id = <some-id>;. (You can also produce this state naturally by editing the row in the admin and saving it, which causes Joomla to call Table::checkIn() and store 0.)
  3. Go to System → Maintenance → Global Check-in.

Without this PR: the table appears in the list with a count of 1, ticking it and clicking Check-in shows a "success" banner with the raw key text COM_CHECKIN_N_ITEMS_CHECKED_IN, and the row stays in the same state.

With this PR: the table does not appear in the list. To verify the working path still works, set checked_out = 1 (or any value > 0) on a row; the table will appear, ticking and clicking Check-in shows "Item checked in." and clears the lock.

I also confirmed multi-row counts and pluralization still render correctly by locking three rows with checked_out > 0 and checking them in (banner shows "3 items checked in.").

Actual result BEFORE applying this Pull Request

Global Check-in lists tables with a count of N when N rows have checked_out IS NOT NULL, including rows where checked_out = 0 (which are not locked, by Joomla's own convention). Clicking Check-in on such a table runs an UPDATE that matches the same rows but sets checked_out = DEFAULT (already 0) and checked_out_time = NULL (already NULL), so getAffectedRows() returns 0. Text::plural('COM_CHECKIN_N_ITEMS_CHECKED_IN', 0) has no matching _0 variant and no base fallback in com_checkin.ini, so the success banner renders the raw key string.

Expected result AFTER applying this Pull Request

Tables only appear in Global Check-in when at least one row has checked_out > 0 (i.e. is actually locked by a user). Checking those tables in clears the lock and the success banner reads "Item checked in." or "N items checked in." as appropriate. Tables whose rows are all in the released-via-API state (checked_out = 0 or NULL) no longer appear at all, which is the correct behavior because there is nothing to check in.

Link to documentations

Please select:

  • Documentation link for guide.joomla.org:

  • No documentation changes for guide.joomla.org needed

  • Pull Request link for manual.joomla.org:

  • No documentation changes for manual.joomla.org needed

Environment where reproduced

  • Joomla: 5.4.6 (production site where bug surfaced, then again on a clean 5.4.6 install for fix verification)
  • PHP: 8.1.34
  • Database: MySQL 8.0 / MariaDB 10.6 (both reproduce, query semantics identical)

Related

  • Issue: none filed (bug not previously on the tracker)
  • Upstream status at time of fix: still-broken on 5.4-dev @ 83d4bf7afe. Identical lines are present on 6.1-dev, 6.2-dev, and 7.0-dev; happy to forward-port or leave that to maintainer up-merge per project convention.

Thanks for taking the time to review. Happy to adjust anything you'd like changed.

avatar genr8r genr8r - open - 4 Jun 2026
avatar genr8r genr8r - change - 4 Jun 2026
Status New Pending
avatar joomla-cms-bot joomla-cms-bot - change - 4 Jun 2026
Category Administration com_checkin
avatar richard67
richard67 - comment - 6 Jun 2026

This PR replaces the IS NOT NULL clause with > 0 in both methods. > 0 is correct for nullable columns too because NULL > 0 evaluates to NULL, which WHERE treats as false.

@genr8r Can we be sure that it works this way on all supported databases (MySQL, MariaDB and PostgreSQL)?

And even if it does, it is hardly understandable for a code reader who is not an SQL expert.

For better readability and to make sure it always works like we want, I would suggest:

$query->where($db->quoteName('checked_out') . ' IS NOT NULL');
$query->where($db->quoteName('checked_out') . ' > 0');

That will result in

WHERE `checked_out` IS NOT NULL AND `checked_out` > 0

That works the same way, and I don't really expect a performance issue with the additional IS NOT NULL condition.

That's just an idea or suggestion, not a change request by me.

What do you think about it?

avatar genr8r
genr8r - comment - 9 Jun 2026

@richard67 Thanks for the careful read.

On portability: this is ANSI SQL three-valued logic. NULL > 0 evaluates to UNKNOWN, and WHERE treats UNKNOWN as not-true, so the row is filtered. MySQL, MariaDB, and PostgreSQL all behave this way consistently (SQL-92 behavior, predates all three).

On the underlying convention in core: the "both NULL and 0 mean released" semantic is set by libraries/src/Table/Table.php on 5.4-dev @ 83d4bf7afe:

  • Table::checkIn() writes either NULL or 0 depending on $_supportNullValue (Table.php#L1286-L1291):

    $nullID = $this->_supportNullValue ? 'NULL' : '0';
    ...
    ->set($db->quoteName($checkedOutField) . ' = ' . $nullID)
  • Table::isCheckedOut() treats both as "not locked" via PHP truthy test (Table.php#L1450):

    if (!$against || ($against == $with)) {
        return false;
    }

    !0 and !null both short-circuit to "not checked out".

So both states (NULL and 0) genuinely occur on this column after a clean release, and need to be filtered out either way. checked_out > 0 does that in one predicate; IS NOT NULL AND > 0 does it in two with identical SQL semantics.

On the readability of the two-condition form: my hesitation is that the redundant predicate reads as authorial uncertainty. A reader who doesn't know the NULL semantics sees both conditions and asks "why both?", which is the same knowledge gap the suggestion was meant to close. It moves the confusion rather than removing it.

If reader comprehension is the underlying concern, I'd happily add a short inline comment instead:

// checked_out > 0 also excludes NULL rows (NULL > 0 is UNKNOWN, treated as false by WHERE),
// matching Table::checkIn() which writes either NULL or 0 to release a lock.
$query->where($db->quoteName('checked_out') . ' > 0');

That keeps the predicate clean while making both the NULL behavior and the cross-reference explicit. Let me know which way you'd prefer.

avatar genr8r genr8r - change - 9 Jun 2026
Labels Added: PR-5.4-dev
avatar richard67
richard67 - comment - 10 Jun 2026

@genr8r A comment would also be ok for me … but if it is more than one line we use a certain code style for that and not multiple // one line comments.

avatar richard67 richard67 - change - 10 Jun 2026
Title
[AI] [5.4] Fix CheckinModel listing rows with checked_out=0 as needing check-in
[5.4] [AI] Fix CheckinModel listing rows with checked_out=0 as needing check-in
avatar richard67 richard67 - edited - 10 Jun 2026
avatar genr8r
genr8r - comment - 10 Jun 2026

@richard67 Good point on the style. Switched to the block form and pushed it:

/*
 * checked_out > 0 excludes both release states Table::checkIn() can write:
 * NULL (when $_supportNullValue) and 0 (otherwise).
 * NULL > 0 evaluates to UNKNOWN, which WHERE treats as false.
 */
$query->where($db->quoteName('checked_out') . ' > 0');
avatar QuyTon QuyTon - test_item - 26 Jun 2026 - Tested successfully
avatar QuyTon
QuyTon - comment - 26 Jun 2026

I have tested this item ✅ successfully on 6714a20

Tested successfully but did not encounter the Text::plural('COM_CHECKIN_N_ITEMS_CHECKED_IN', 0) issue before the PR.


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/47886.

avatar QuyTon
QuyTon - comment - 26 Jun 2026

I have tested this item ✅ successfully on 6714a20

Tested successfully but did not encounter the Text::plural('COM_CHECKIN_N_ITEMS_CHECKED_IN', 0) issue before the PR.


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/47886.

avatar MacJoom
MacJoom - comment - 10 Jul 2026

Please reduce the comment to a one-liner: // Both NULL and 0 represent records that are not checked out.

avatar ThomasFinnern ThomasFinnern - test_item - 10 Jul 2026 - Tested successfully
avatar ThomasFinnern
ThomasFinnern - comment - 10 Jul 2026

I have tested this item ✅ successfully on 6714a20

I changed the admin module data in phpMyAdmin with a checked_out of '0' and checked_out_time 'null' (by flag)
Maintenance: Global Check-in showed the module for checkin before apply of PR and did not display it after
Additional tested article state checked out and it appeared without the "admin module" checkout state


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/47886.

avatar ThomasFinnern
ThomasFinnern - comment - 10 Jul 2026

I have tested this item ✅ successfully on 6714a20

I changed the admin module data in phpMyAdmin with a checked_out of '0' and checked_out_time 'null' (by flag)
Maintenance: Global Check-in showed the module for checkin before apply of PR and did not display it after
Additional tested article state checked out and it appeared without the "admin module" checkout state


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/47886.

avatar genr8r
genr8r - comment - 10 Jul 2026

@MacJoom Done, pushed in 9c956c1. Both occurrences now use your wording verbatim:

// Both NULL and 0 represent records that are not checked out.
$query->where($db->quoteName('checked_out') . ' > 0');

Comment-only change, no logic difference from the commit @QuyTon and @ThomasFinnern tested.

avatar MacJoom MacJoom - alter_testresult - 10 Jul 2026 - Thomas Finnern: Tested successfully
avatar MacJoom MacJoom - alter_testresult - 10 Jul 2026 - Quy Ton: Tested successfully
avatar MacJoom
MacJoom - comment - 10 Jul 2026

RTC

avatar richard67 richard67 - change - 10 Jul 2026
The description was changed
Status Pending Ready to Commit
avatar richard67
richard67 - comment - 10 Jul 2026

RTC


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/47886.

avatar richard67 richard67 - edited - 10 Jul 2026
avatar richard67
richard67 - comment - 10 Jul 2026

RTC


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/47886.

avatar alikon
alikon - comment - 11 Jul 2026

then the same change should be made here

$query = $db->getQuery(true)
->update($db->quoteName($tn))
->set($db->quoteName('checked_out') . ' = DEFAULT')
->set($db->quoteName('checked_out_time') . ' = NULL');
if ($fields['checked_out']->Null === 'YES') {
$query->where($db->quoteName('checked_out') . ' IS NOT NULL');
} else {
$query->where($db->quoteName('checked_out') . ' > 0');
}

and also i'm afraid that we need a better explanation i don't know where is better in code comments or in some manuals
something like :

// Both NULL and 0 represent records that are not checked out.
// NULL > 0 evaluates to NULL/false in SQL; 0 > 0 is false.
// Only user IDs (positive integers) indicate active locks.

avatar genr8r
genr8r - comment - 11 Jul 2026

@alikon Good catch on the task plugin. plugins/task/globalcheckin/src/Extension/Globalcheckin.php has the identical if/else on column nullability, so the same predicate mismatch is there. Happy to fold that into this PR so both call sites stay consistent. (Impact there is milder in practice: with the default delay > 0, the checked_out_time < $delayTime clause already filters checked_out = 0 rows because their time is NULL, so it only bites at delay = 0 or on rows with a stale non-NULL checked_out_time. Same defect regardless.)

On the comment, I need a steer before I touch it. The three-line version you've quoted is essentially what was in 6714a20; @MacJoom asked me on 07-10 to reduce it to the single line, I did that in 9c956c1, and the RTC followed. I don't want to revert one maintainer's request on another's say-so and start a ping-pong.

@MacJoom @richard67 @alikon, could you settle on one form? I'll implement whichever you agree on and apply it identically at all three sites (both CheckinModel methods plus the task plugin).

If the fuller explanation wins, note that @richard67 flagged earlier that multi-line inline comments should use the block style rather than stacked // lines, which matches core (ApplicationModel, InstallModel, et al.). So it would look like:

/*
 * Both NULL and 0 represent records that are not checked out.
 * NULL > 0 evaluates to UNKNOWN, which WHERE treats as false, and 0 > 0 is false.
 * Only a positive user ID indicates an active lock.
 */
$query->where($db->quoteName('checked_out') . ' > 0');

I've used "UNKNOWN, which WHERE treats as false" rather than "NULL/false" since that's the precise three-valued-logic behavior, but I'm not attached to the wording.

Heads up that adding the plugin fix will drop the RTC label and invalidate the two successful tests from @QuyTon and @ThomasFinnern, so it goes back through the test queue. I think that's the right trade rather than shipping the fix at only two of the three call sites.

avatar richard67
richard67 - comment - 11 Jul 2026

On the comment, I need a steer before I touch it. The three-line version you've quoted is essentially what was in 6714a20; @MacJoom asked me on 07-10 to reduce it to the single line, I did that in 9c956c1, and the RTC followed. I don't want to revert one maintainer's request on another's say-so and start a ping-pong.

@MacJoom @richard67 @alikon, could you settle on one form? I'll implement whichever you agree on and apply it identically at all three sites (both CheckinModel methods plus the task plugin).

I am ok with the short comment, but I can also live with the long comment. So @MacJoom and @alikon have to fight it out, or we get more opinions.

Heads up that adding the plugin fix will drop the RTC label and invalidate the two successful tests from @QuyTon and @ThomasFinnern, so it goes back through the test queue. I think that's the right trade rather than shipping the fix at only two of the three call sites.

I agree. Would be good to fix it all at once in the same way. It would also need to add a test case for the global checking task plugin to the testing instructions.

avatar MacJoom
MacJoom - comment - 11 Jul 2026

If the field is well documented in the manual (which i can not confirm now) then we don't need a comment in the code at all. so one line should be enough. Imagine all decisions in the code be documented with a block...

avatar richard67
richard67 - comment - 11 Jul 2026

How about the following one-liner?

// The following expression evaluates to false for values <= 0 as well as for NULL values

@genr8r @MacJoom @alikon Opinions please.

avatar genr8r genr8r - change - 12 Jul 2026
Labels Added: RTC
avatar alikon
alikon - comment - 14 Jul 2026

for the comment i'm ok with the one-liner
it took me sometimes to understand when reading the code but probably that's me 🥲

avatar QuyTon QuyTon - change - 14 Jul 2026
Status Ready to Commit Pending
avatar genr8r genr8r - change - 14 Jul 2026
Labels Removed: RTC
avatar joomla-cms-bot joomla-cms-bot - change - 14 Jul 2026
Category Administration com_checkin Administration com_checkin Front End Plugins
avatar genr8r genr8r - change - 14 Jul 2026
Title
[5.4] [AI] Fix CheckinModel listing rows with checked_out=0 as needing check-in
[5.4] [AI] Fix Global Check-in treating checked_out=0 rows as needing check-in
avatar genr8r genr8r - edited - 14 Jul 2026
avatar genr8r
genr8r - comment - 14 Jul 2026

@richard67 @MacJoom @alikon Sounds like consensus, so I've pushed it in 7d8fe7f.

richard67's one-liner is now at all three call sites:

// The following expression evaluates to false for values <= 0 as well as for NULL values
$query->where($db->quoteName('checked_out') . ' > 0');

I also folded in the task plugin fix @alikon spotted. plugins/task/globalcheckin/src/Extension/Globalcheckin.php had the identical if/else on column nullability and now uses the same predicate.

As flagged, adding the plugin drops RTC and invalidates the successful tests from @QuyTon and @ThomasFinnern, since this is no longer a comment-only change. Apologies for the extra round. Testing instructions in the description now include a task plugin case, per @richard67's request. Note that on the plugin side the difference is a no-op write either way, so it reads as a correctness fix rather than a visible behavior change; the steps spell that out so nobody burns time hunting for a symptom that isn't there.

I've also retitled the PR, since "CheckinModel" no longer covers the full scope.

avatar alikon alikon - test_item - 16 Jul 2026 - Tested successfully
avatar alikon
alikon - comment - 16 Jul 2026

I have tested this item ✅ successfully on 7d8fe7f


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/47886.

avatar alikon
alikon - comment - 16 Jul 2026

I have tested this item ✅ successfully on 7d8fe7f


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/47886.

avatar ThomasFinnern ThomasFinnern - test_item - 16 Jul 2026 - Tested successfully
avatar ThomasFinnern
ThomasFinnern - comment - 16 Jul 2026

I have tested this item ✅ successfully on 7d8fe7f

First tests like above,
Scheduled Tasks:
checked_out = 1, checked_out_time = '2026-07-16 15:51:22' ==> null, null
checked_out = 0, checked_out_time = '2026-07-16 15:59:36' ==> 0, 2026-07-16 15:59:36


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/47886.
avatar ThomasFinnern
ThomasFinnern - comment - 16 Jul 2026

I have tested this item ✅ successfully on 7d8fe7f

First tests like above,
Scheduled Tasks:
checked_out = 1, checked_out_time = '2026-07-16 15:51:22' ==> null, null
checked_out = 0, checked_out_time = '2026-07-16 15:59:36' ==> 0, 2026-07-16 15:59:36


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/47886.
avatar richard67 richard67 - change - 16 Jul 2026
Status Pending Ready to Commit
avatar richard67
richard67 - comment - 16 Jul 2026

RTC


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/47886.

avatar richard67
richard67 - comment - 16 Jul 2026

RTC


This comment was created with the J!Tracker Application at issues.joomla.org/tracker/joomla-cms/47886.

avatar richard67 richard67 - edited - 16 Jul 2026
avatar muhme muhme - change - 17 Jul 2026
Labels Added: RTC
avatar muhme
muhme - comment - 17 Jul 2026

✅ Final test before merge with JBT and MariaDB

  • Seen content table is listed in Global Check-in after hacking article's checked_out with 0 before PR
  • Applied PR with Patch Tester
    • content table is no more listed in Global Check-in for an article with checked_out = 0
    • content table is listed in Global Check-in for an article being edited
    • Tested banners and guidedtours as two more tables:
      • Before PR with checked_out = 0 they are listed in Global Check-in
      • After PR they are no more listed in Global Check-in
      • If a banner or a guided tours is being edited, they are listed Global Check-in
    • New installed Joomla with PostgreSQL
      • It looks like PostgreSQL does not have the problem, even with checked_out = 0 for an article entry, the content table is not listed in Global Check-in
      • Applied PR with Patch Tester
        • content table is not listed in Global Check-in for an article with checked_out = 0
        • content table is listed in Global Check-in for an article being edited
        • Using Global Check-in removes an edited article check-out
  • Not tested task as it is already tested and is the identical source code change
avatar muhme
muhme - comment - 17 Jul 2026

Just something to consider regarding this global change: the following tables contain a checked_out column:

  • banners
  • banner_clients
  • categories
  • contact_details
  • content
  • extensions
  • fields
  • fields_groups
  • finder_filters
  • guidedtours
  • guidedtour_steps
  • menu
  • modules
  • newsfeeds
  • scheduler_tasks
  • tags
  • update_sites
  • user_notes
  • workflows
  • workflow_stages
avatar muhme
muhme - comment - 17 Jul 2026

⚠️ Question: There are other places where checked_out is compared against NULL. Should these places also be changed?

  • Searched with find . -name \*.php | xargs grep -n "'checked_out'"| grep NULL
    • libraries/src/Table/MenuType.php:222
    • administrator/components/com_banners/src/Helper/BannersHelper.php:59
    • administrator/components/com_templates/src/Model/StyleModel.php:473
    • administrator/components/com_templates/src/Model/StyleModel.php:493
avatar muhme
muhme - comment - 17 Jul 2026

⚠️ Question: There are other places where checked_out is compared against NULL. Should these places also be changed?

avatar richard67
richard67 - comment - 17 Jul 2026

⚠️ Question: There are other places where checked_out is compared against NULL. Should these places also be changed?

* Searched with `find . -name \*.php | xargs grep -n "'checked_out'"| grep NULL`
  
  * libraries/src/Table/MenuType.php:222
  * administrator/components/com_banners/src/Helper/BannersHelper.php:59
  * administrator/components/com_templates/src/Model/StyleModel.php:473
  * administrator/components/com_templates/src/Model/StyleModel.php:493

These other places handle core table written by core only, so checking for NULL only is sufficient. For the global checkout handled in this PR here we have to handle also tables of 3rd party extensions, that's why we cannot rely on NULLbeing used.

avatar muhme muhme - change - 17 Jul 2026
Status Ready to Commit Fixed in Code Base
Closed_Date 0000-00-00 00:00:00 2026-07-17 17:37:03
Closed_By muhme
Labels Added: bug
avatar muhme muhme - close - 17 Jul 2026
avatar muhme muhme - merge - 17 Jul 2026
avatar muhme
muhme - comment - 17 Jul 2026

Thank you very much @genr8r for your contribution. Thanks to @richard67, @alikon and @MacJoom for supporting. Thank you to @ThomasFinnern, @QuyTon and @alikon for testing.

Add a Comment

Login with GitHub to post a comment