From 85799e5c6c1390e48150b39ef55bf065d16bab9d Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Sun, 12 Jan 2025 20:34:31 -0800 Subject: [PATCH] SearchKit - Refresh DB entities via table-swap Overview --------- This updates the handling of "DB Entity" (table-mode). In this mode, you need some mechanism to update the content of the table (e.g. the "refresh" operation). This updates the mechanism. Before ------ TRUNCATE the table and re-INSERTs with regenerated data. In the period between the truncation and regenerated data, the content of the table is ill-defined. After ----- CREATE a new table with a temporary name and fill that. Once ready, swap the old table and new table (atomicly). --- .../Civi/Api4/Action/SKEntity/Refresh.php | 33 +++++++++++++++++-- .../Exception/RefreshInProgressException.php | 5 +++ 2 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 ext/search_kit/Civi/Search/Exception/RefreshInProgressException.php diff --git a/ext/search_kit/Civi/Api4/Action/SKEntity/Refresh.php b/ext/search_kit/Civi/Api4/Action/SKEntity/Refresh.php index 10e6aa710e99..03e984847482 100644 --- a/ext/search_kit/Civi/Api4/Action/SKEntity/Refresh.php +++ b/ext/search_kit/Civi/Api4/Action/SKEntity/Refresh.php @@ -32,13 +32,40 @@ public function _run(Result $result) { return; } + // Build a new table with full data. Swap-in the new table and drop the old one. + // + // NOTE: This protocol destroys inbound FKs. But the prior protocol (TRUNCATE + INSERT SELECT) + // also destroyed inbound FKs. To keep inbound FKs, you would probably wind up working on + // something more incremental. (Maybe put new data into TEMPORARY table - and use INSERT/DELETE/UPDATE + // to sync to the real table. But that requires guaranteeing the presence of a stable PK column(s), + // and it would change the default ordering over time.) + + // Prepare a sketch of the process. Ensure metadata is well-formed. $query = (new SKEntityGenerator())->createQuery($display['saved_search_id.api_entity'], $display['saved_search_id.api_params'], $display['settings']); $sql = $query->getSql(); - $tableName = _getSearchKitDisplayTableName($displayName); + $finalTable = _getSearchKitDisplayTableName($displayName); $columnSpecs = array_column($display['settings']['columns'], 'spec'); $columns = implode(', ', array_column($columnSpecs, 'name')); - \CRM_Core_DAO::executeQuery("TRUNCATE TABLE `$tableName`"); - \CRM_Core_DAO::executeQuery("INSERT INTO `$tableName` ($columns) $sql"); + $newTable = \CRM_Utils_SQL_TempTable::build()->setDurable()->setAutodrop(FALSE)->getName(); + $junkTable = \CRM_Utils_SQL_TempTable::build()->setDurable()->setAutodrop(FALSE)->getName(); + + // Only one process should actually refresh this entity (at a given time). + $lock = \Civi::lockManager()->acquire("data.skentity." . $display['id'], 1); + if (!$lock->isAcquired()) { + throw new \Civi\Search\Exception\RefreshInProgressException(sprintf('Refresh (%s) is already in progress', $this->getEntityName())); + } + $releaseLock = \CRM_Utils_AutoClean::with([$lock, 'release']); + + // Go! + \CRM_Core_DAO::executeQuery("CREATE TABLE `$newTable` LIKE `$finalTable`"); + \CRM_Core_DAO::executeQuery("INSERT INTO `$newTable` ($columns) $sql"); + \CRM_Core_DAO::executeQuery(sprintf('RENAME TABLE `%s` TO `%s`, `%s` TO `%s`', + $finalTable, $junkTable, + $newTable, $finalTable + )); + \CRM_Core_DAO::executeQuery(sprintf('DROP TABLE `%s`', $junkTable)); + + // All done $result[] = [ 'refresh_date' => \CRM_Core_DAO::singleValueQuery("SELECT NOW()"), ]; diff --git a/ext/search_kit/Civi/Search/Exception/RefreshInProgressException.php b/ext/search_kit/Civi/Search/Exception/RefreshInProgressException.php new file mode 100644 index 000000000000..2ac35b3ad096 --- /dev/null +++ b/ext/search_kit/Civi/Search/Exception/RefreshInProgressException.php @@ -0,0 +1,5 @@ +