Skip to main content

Database migrations (the updates/ system)

Audience: developers & AI agents · Scope: changing the database schema/data safely · Last reviewed: 2026-06-18

TL;DR: Never hand-edit the schema. Put an idempotent PHP script at docroot/components/com_configbox/data/customization/updates/<version>.php. It runs automatically on the next page load, in version order, and the last-applied version is tracked in the DB. Use the KenedoDatabase wrapper from KenedoPlatform::getDb().

How it runs

  • On each request, observers/System.php calls ConfigboxUpdateHelper::applyUpdates() (docroot/components/com_configbox/helpers/update.php).
  • It scans two folders for *.php files, sorts them with version_compare, and require()s every file whose version is greater than the last-applied version:
    • core: helpers/updates/ → tracked by latest_update_version
    • custom (yours): data/customization/updates/ → tracked by latest_customization_update_version
  • Version state is stored in e5xae_configbox_system_vars (columns `key`/`value`).
  • Failure behavior: if a script throws, failed_update_detected is set to '1' and all further updates are blocked until an admin clears that flag. So scripts must be defensive.
  • Concurrency guard / stale marker: while updates run, a zero-byte marker file docroot/tmp/cb_update_in_progress exists; if the process dies hard (kill, OOM, timeout) the marker survives and applyUpdates() silently skips all migrations on every request — no flag, no log entry. Symptom: latest_customization_update_version stays behind the newest file in updates/ and pages fail with "unknown column" errors. Fix: delete the marker file (safe when no update is actually running) and reload a page.
# Check current state (read-only)
mysql -uroot -p bc_master -e "SELECT \`key\`,\`value\` FROM e5xae_configbox_system_vars \
WHERE \`key\` IN ('latest_customization_update_version','latest_update_version','failed_update_detected');"

Writing a migration

  • File name is the version: data/customization/updates/0.5.46.php (next number after the highest existing file).
  • Start with defined('CB_VALID_ENTRY') or die();.
  • Make it idempotent — guard every change with an existence check, because it may re-run after a cleared failure, and because the same script ships to staging/production.
  • Use the helper checks in ConfigboxUpdateHelper (e.g. tableFieldExists(), tableExists(), keyExists()).

Example — add a column (from updates/0.5.65.php)

<?php
defined('CB_VALID_ENTRY') or die();

if (ConfigboxUpdateHelper::tableFieldExists('#__configbox_external_user_quotes', 'pipedrive_stage_id') == false) {
$db = KenedoPlatform::getDb();
$db->setQuery("ALTER TABLE `#__configbox_external_user_quotes` ADD `pipedrive_stage_id` INT DEFAULT NULL");
$db->query();
}

Example — drop columns (from updates/0.5.87.php)

Dropping is irreversible — no later script can bring the data back, and there are no transactions. Guard each drop the same way, and ship any code change the drop requires in the same commit. For the quotes table that means removing the matching BcQuote property, because insertObject() turns declared properties into columns — see the quote data model.

foreach ($columns as $column) {
if (ConfigboxUpdateHelper::tableFieldExists($table, $column) == false) {
continue;
}
$db->setQuery("ALTER TABLE ".$table." DROP COLUMN ".$db->getQuoted($column));
$db->query();
}

Example — insert a row

For a non-trivial insert (e.g. creating a Joomla menu item, which also touches the nested set), see the fully worked, idempotent script in sef-links.md (updates/0.5.46.php).

The KenedoDatabase API

KenedoPlatform::getDb() returns a KenedoDatabase wrapper — not Joomla's JDatabaseDriver. This trips people up. Available:

NeedUseNotes
Run a querysetQuery($sql) then query()query() throws on error
Single valueloadResult()
One row as objectloadObject()returns a KenedoObject
ListsloadObjectList(), loadAssocList(), loadResultList()
Insert an objectinsertObject('#__table', $obj, 'id')builds + escapes the INSERT; pass the PK name so an empty id auto-increments (and disables the ON DUPLICATE KEY upsert)
Escape a valuegetEscaped($v)does not add surrounding quotes → write "'".$db->getEscaped($v)."'"
Quote an identifiergetQuoted($name)adds backticks
Table prefixwrite #__tablereplaced with the real prefix at run time

Not available: quote(), quoteName(), transactionStart/Commit/Rollback. There are no transactions — order your statements so a partial failure is least harmful, and keep the script idempotent so a re-run converges.

Gotchas / checklist

  • File named exactly <version>.php, version higher than every existing file in the folder.
  • defined('CB_VALID_ENTRY') or die(); at the top.
  • Idempotent — guarded by existence checks; safe to run twice.
  • Uses #__ prefix placeholders and getEscaped() for values (no quote()).
  • Fails soft for non-critical work (log and skip) rather than throwing — a throw blocks all later migrations via failed_update_detected.
  • Verified locally by loading a page, then checking latest_customization_update_version bumped and failed_update_detected is unset.