From dbeef92605cf364e70a2ff07d92af8993b598fe6 Mon Sep 17 00:00:00 2001 From: fabrizzio-dotCMS Date: Wed, 15 Apr 2026 16:40:09 -0600 Subject: [PATCH] =?UTF-8?q?fix(index):=20clean=20up=20indicies=20DB=20rows?= =?UTF-8?q?=20after=20physical=20delete=20=E2=80=94=20Fixes=20#35306?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ContentletIndexAPIImpl.delete() physically removed the index from the search provider but never deleted the corresponding row from the indicies table, leaving stale DB entries that could cause startup failures or silent inconsistencies on the next boot. After a successful physical delete, versionedIndicesAPI.removeByIndexName() is now called to clean up both the bare and os:: forms of the row. The cleanup is phase-safe: - Physical delete: indexAPI (IndexAPIImpl router) fans out to ES, OS, or both depending on the active migration phase. - DB cleanup: versionedIndicesAPI.removeByIndexName() deletes by index_name and handles both legacy (index_version IS NULL) and versioned (os:: tagged) rows in a single call. The deprecated legacyIndiciesAPI is not involved. Co-Authored-By: Claude Sonnet 4.6 --- .../business/ContentletIndexAPIImpl.java | 13 ++++++++++-- .../dotcms/content/index/IndicesFactory.java | 10 ++++++++++ .../content/index/IndicesFactoryImpl.java | 20 +++++++++++++++++++ .../content/index/VersionedIndicesAPI.java | 10 ++++++++++ .../index/VersionedIndicesAPIImpl.java | 17 ++++++++++++++++ .../business/ContentletIndexAPIImplTest.java | 9 +++++++++ 6 files changed, 77 insertions(+), 2 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ContentletIndexAPIImpl.java b/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ContentletIndexAPIImpl.java index 6af530c86074..4f9bda5227fc 100644 --- a/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ContentletIndexAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ContentletIndexAPIImpl.java @@ -1285,8 +1285,17 @@ private void logSwitchover(final IndiciesInfo oldInfo, final String luckyServer) } - public boolean delete(String indexName) { - return indexAPI.delete(indexName); + public boolean delete(final String indexName) { + final boolean deleted = indexAPI.delete(indexName); + if (deleted) { + try { + versionedIndicesAPI.removeByIndexName(indexName); + } catch (DotDataException e) { + Logger.error(this, "Index [" + indexName + "] was physically deleted but the " + + "indices DB row could not be removed: " + e.getMessage(), e); + } + } + return deleted; } public boolean optimize(List indexNames) { diff --git a/dotCMS/src/main/java/com/dotcms/content/index/IndicesFactory.java b/dotCMS/src/main/java/com/dotcms/content/index/IndicesFactory.java index 374b9f7f0dee..efbc87764bac 100644 --- a/dotCMS/src/main/java/com/dotcms/content/index/IndicesFactory.java +++ b/dotCMS/src/main/java/com/dotcms/content/index/IndicesFactory.java @@ -127,4 +127,14 @@ public interface IndicesFactory { void insertIndexIfPresent(String indexName, String indexType, String version) throws DotDataException; + /** + * Removes the {@code indicies} table row for the given index name. + * Handles both plain and {@code os::}-prefixed forms of the name so that + * callers do not need to know how the name is stored in the database. + * + * @param indexName the physical index name (with or without vendor tag) + * @throws DotDataException if the name is blank or a SQL error occurs + */ + void removeByIndexName(String indexName) throws DotDataException; + } diff --git a/dotCMS/src/main/java/com/dotcms/content/index/IndicesFactoryImpl.java b/dotCMS/src/main/java/com/dotcms/content/index/IndicesFactoryImpl.java index 095e9ee932d9..fd4609ca8e83 100644 --- a/dotCMS/src/main/java/com/dotcms/content/index/IndicesFactoryImpl.java +++ b/dotCMS/src/main/java/com/dotcms/content/index/IndicesFactoryImpl.java @@ -43,6 +43,8 @@ public class IndicesFactoryImpl implements IndicesFactory { "SELECT COUNT(*) as count FROM indicies WHERE index_version = ? AND index_version IS NOT NULL"; private static final String COUNT_INDICES_BY_VERSION_SQL = "SELECT COUNT(*) as count FROM indicies WHERE index_version = ? AND index_version IS NOT NULL"; + private static final String DELETE_INDEX_BY_NAME_SQL = + "DELETE FROM indicies WHERE index_name = ? OR index_name = ?"; @Override public Optional loadIndices(String version) throws DotDataException { @@ -360,6 +362,24 @@ public void insertIndexIfPresent(String indexName, String indexType, String vers } } + @Override + public void removeByIndexName(final String indexName) throws DotDataException { + if (!UtilMethods.isSet(indexName)) { + throw new DotDataException("Index name cannot be null or empty"); + } + // Always work from the bare name so we can reliably compute both DB forms + final String bareName = IndexTag.strip(indexName); + final String taggedName = IndexTag.OS.tag(bareName); + try { + final DotConnect dotConnect = new DotConnect(); + final int deleted = dotConnect.executeUpdate(DELETE_INDEX_BY_NAME_SQL, bareName, taggedName); + Logger.info(this, "Removed " + deleted + " row(s) from indicies for index: " + bareName); + } catch (Exception e) { + Logger.error(this, "Failed to remove indicies row for index: " + bareName, e); + throw new DotDataException("Failed to remove indicies row for index: " + bareName, e); + } + } + /** * Builds a VersionedIndicesInfo from legacy non-versioned database results. * Expects results to have columns: index_name, index_type (no index_version) diff --git a/dotCMS/src/main/java/com/dotcms/content/index/VersionedIndicesAPI.java b/dotCMS/src/main/java/com/dotcms/content/index/VersionedIndicesAPI.java index ef80f3981ed5..1fdd0d152942 100644 --- a/dotCMS/src/main/java/com/dotcms/content/index/VersionedIndicesAPI.java +++ b/dotCMS/src/main/java/com/dotcms/content/index/VersionedIndicesAPI.java @@ -104,6 +104,16 @@ public interface VersionedIndicesAPI { */ Optional loadDefaultVersionedIndices() throws DotDataException; + /** + * Removes the {@code indicies} table row for the given index name. + * Handles both plain and {@code os::}-prefixed forms of the name so that + * callers do not need to know how the name is stored in the database. + * + * @param indexName the physical index name (with or without vendor tag) + * @throws DotDataException if the name is blank or a SQL error occurs + */ + void removeByIndexName(String indexName) throws DotDataException; + /** * Clears all cached indices data. * This should be called when indices are modified outside of this API diff --git a/dotCMS/src/main/java/com/dotcms/content/index/VersionedIndicesAPIImpl.java b/dotCMS/src/main/java/com/dotcms/content/index/VersionedIndicesAPIImpl.java index bf80c606edc5..ea1c14749fd6 100644 --- a/dotCMS/src/main/java/com/dotcms/content/index/VersionedIndicesAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/content/index/VersionedIndicesAPIImpl.java @@ -226,6 +226,23 @@ public Optional loadDefaultVersionedIndices() throws DotDataEx return loadIndices(VersionedIndices.OPENSEARCH_3X); } + /** + * {@inheritDoc} + */ + @WrapInTransaction + @Override + public void removeByIndexName(final String indexName) throws DotDataException { + Logger.debug(this, "Removing indices row for index: " + indexName); + indicesFactory.removeByIndexName(indexName); + // Flush all index-related caches so no stale names survive the deletion: + // 1. VersionedIndicesCache — our own versioned-index cache + cache.clearCache(); + // 2. IndiciesCache — legacy (ES, non-versioned) index cache used by IndiciesFactory + CacheLocator.getIndiciesCache().clearCache(); + // 3. ESQueryCache — cached search queries that may reference the deleted index name + CacheLocator.getESQueryCache().clearCache(); + } + /** * {@inheritDoc} */ diff --git a/dotcms-integration/src/test/java/com/dotcms/content/elasticsearch/business/ContentletIndexAPIImplTest.java b/dotcms-integration/src/test/java/com/dotcms/content/elasticsearch/business/ContentletIndexAPIImplTest.java index b85eff8cbb91..ed1908fac0ad 100644 --- a/dotcms-integration/src/test/java/com/dotcms/content/elasticsearch/business/ContentletIndexAPIImplTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/content/elasticsearch/business/ContentletIndexAPIImplTest.java @@ -374,6 +374,15 @@ public void createContentIndexAndDelete() throws Exception { //Verify we just added two more indices assertEquals(oldIndices, newIndices); + + // Verify the indicies table has no orphan rows for the deleted indices + final DotConnect dc = new DotConnect(); + dc.setSQL("SELECT COUNT(*) AS cnt FROM indicies WHERE index_name = ? OR index_name = ?"); + dc.addParam(workingIndex); + dc.addParam(liveIndex); + final List> rows = dc.loadResults(); + assertEquals("indicies table must have no orphan rows after delete", + 0L, Long.parseLong(rows.get(0).get("cnt").toString())); } /**