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.phpcallsConfigboxUpdateHelper::applyUpdates()(docroot/components/com_configbox/helpers/update.php). - It scans two folders for
*.phpfiles, sorts them withversion_compare, andrequire()s every file whose version is greater than the last-applied version:- core:
helpers/updates/→ tracked bylatest_update_version - custom (yours):
data/customization/updates/→ tracked bylatest_customization_update_version
- core:
- Version state is stored in
e5xae_configbox_system_vars(columns`key`/`value`). - Failure behavior: if a script throws,
failed_update_detectedis 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_progressexists; if the process dies hard (kill, OOM, timeout) the marker survives andapplyUpdates()silently skips all migrations on every request — no flag, no log entry. Symptom:latest_customization_update_versionstays behind the newest file inupdates/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:
| Need | Use | Notes |
|---|---|---|
| Run a query | setQuery($sql) then query() | query() throws on error |
| Single value | loadResult() | |
| One row as object | loadObject() | returns a KenedoObject |
| Lists | loadObjectList(), loadAssocList(), loadResultList() | |
| Insert an object | insertObject('#__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 value | getEscaped($v) | does not add surrounding quotes → write "'".$db->getEscaped($v)."'" |
| Quote an identifier | getQuoted($name) | adds backticks |
| Table prefix | write #__table | replaced 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 andgetEscaped()for values (noquote()). - 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_versionbumped andfailed_update_detectedis unset.