This is an automated email from the ASF dual-hosted git repository.

shuber pushed a commit to branch unomi-3-dev
in repository https://gitbox.apache.org/repos/asf/unomi.git


The following commit(s) were added to refs/heads/unomi-3-dev by this push:
     new 92222a26c UNOMI-828 Migrate legacy queryBuilder IDs to new format.
92222a26c is described below

commit 92222a26cfe9882c95a4fb710e8b9833e0a70052
Author: Serge Huber <[email protected]>
AuthorDate: Thu Jan 1 15:14:29 2026 +0100

    UNOMI-828 Migrate legacy queryBuilder IDs to new format.
    
    - Added a migration script 
(`migrate-3.1.0-15-updateLegacyQueryBuilder.groovy`) to update condition types 
using legacy `*ESQueryBuilder` IDs to the new `QueryBuilder` format in the 
systemitems index.
    - Introduced a Painless script (`update_legacy_querybuilder.painless`) to 
handle replacements directly in Elasticsearch.
    - Updated integration tests to validate the migration process and ensure 
proper ID updates.
    - Refactored test utilities in `Migrate16xToCurrentVersionIT` for improved 
readability and maintainability.
---
 .../migration/Migrate16xToCurrentVersionIT.java    | 290 ++++++++++++++++++---
 ...igrate-3.1.0-15-updateLegacyQueryBuilder.groovy | 129 +++++++++
 .../3.1.0/update_legacy_querybuilder.painless      |  33 +++
 3 files changed, 417 insertions(+), 35 deletions(-)

diff --git 
a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java
 
b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java
index da2ceecaf..830246aa8 100644
--- 
a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java
+++ 
b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java
@@ -53,11 +53,19 @@ public class Migrate16xToCurrentVersionIT extends BaseIT {
             "context-patch", "context-jsonschema", "context-importconfig", 
"context-exportconfig", "context-rulestats");
 
     // Elasticsearch connection constants
-    private static final String ES_BASE_URL = "http://localhost:9400";;
-    private static final String ES_SNAPSHOT_REPO = ES_BASE_URL + 
"/_snapshot/snapshots_repository/";
-    private static final String ES_SNAPSHOT_STATUS = ES_BASE_URL + 
"/_snapshot/_status";
+    private static String getEsBaseUrl() {
+        return "http://localhost:"; + getSearchPort();
+    }
+    private static String getEsSnapshotRepo() {
+        return getEsBaseUrl() + "/_snapshot/snapshots_repository/";
+    }
+    private static String getEsSnapshotStatus() {
+        return getEsBaseUrl() + "/_snapshot/_status";
+    }
     private static final String ES_SNAPSHOT_2 = "snapshot_2";
-    private static final String ES_SNAPSHOT_RESTORE_URL = ES_SNAPSHOT_REPO + 
ES_SNAPSHOT_2 + "/_restore?wait_for_completion=true";
+    private static String getEsSnapshotRestoreUrl() {
+        return getEsSnapshotRepo() + ES_SNAPSHOT_2 + 
"/_restore?wait_for_completion=true";
+    }
 
     // Index prefix constants
     private static final String INDEX_PREFIX_CONTEXT = "context-";
@@ -113,16 +121,16 @@ public class Migrate16xToCurrentVersionIT extends BaseIT {
         // Restore snapshot from 1.6.x
         try (CloseableHttpClient httpClient = HttpUtils.initHttpClient(true, 
null)) {
             // Create snapshot repo
-            HttpUtils.executePutRequest(httpClient, ES_SNAPSHOT_REPO, 
resourceAsString(RESOURCE_CREATE_SNAPSHOTS_REPO), null);
+            HttpUtils.executePutRequest(httpClient, getEsSnapshotRepo(), 
resourceAsString(RESOURCE_CREATE_SNAPSHOTS_REPO), null);
             // Get snapshot, insure it exists
-            String snapshot = HttpUtils.executeGetRequest(httpClient, 
ES_SNAPSHOT_REPO + ES_SNAPSHOT_2, null);
+            String snapshot = HttpUtils.executeGetRequest(httpClient, 
getEsSnapshotRepo() + ES_SNAPSHOT_2, null);
             if (snapshot == null || !snapshot.contains(ES_SNAPSHOT_2)) {
                 throw new RuntimeException("Unable to retrieve 1.6.x snapshot 
for ES restore");
             }
             // Restore the snapshot
-            HttpUtils.executePostRequest(httpClient, ES_SNAPSHOT_RESTORE_URL, 
"{}", null);
+            HttpUtils.executePostRequest(httpClient, 
getEsSnapshotRestoreUrl(), "{}", null);
 
-            String snapshotStatus = HttpUtils.executeGetRequest(httpClient, 
ES_SNAPSHOT_STATUS, null);
+            String snapshotStatus = HttpUtils.executeGetRequest(httpClient, 
getEsSnapshotStatus(), null);
             System.out.println("Snapshot status: " + snapshotStatus);
             LOGGER.info("Snapshot status: {}", snapshotStatus);
 
@@ -209,6 +217,7 @@ public class Migrate16xToCurrentVersionIT extends BaseIT {
         checkTenantIdsApplied();
         checkDefaultTenantCreated();
         checkDefinitionsServiceObjectsAccessible();
+        checkLegacyQueryBuilderMigration();
     }
 
     /**
@@ -218,16 +227,16 @@ public class Migrate16xToCurrentVersionIT extends BaseIT {
      * - persona sessions are now merged in session index due to index 
reduction in 2_2_0 (+2 sessions in final count)
      */
     private void checkEventSessionRollover2_2_0() throws IOException {
-        Assert.assertTrue(MigrationUtils.indexExists(httpClient, ES_BASE_URL, 
INDEX_EVENT + "000001"));
-        Assert.assertTrue(MigrationUtils.indexExists(httpClient, ES_BASE_URL, 
INDEX_SESSION + "000001"));
+        Assert.assertTrue(MigrationUtils.indexExists(httpClient, 
getEsBaseUrl(), INDEX_EVENT + "000001"));
+        Assert.assertTrue(MigrationUtils.indexExists(httpClient, 
getEsBaseUrl(), INDEX_SESSION + "000001"));
 
         int newEventcount = 0;
-        for (String eventIndex : 
MigrationUtils.getIndexesPrefixedBy(httpClient, ES_BASE_URL, INDEX_EVENT + 
"0")) {
+        for (String eventIndex : 
MigrationUtils.getIndexesPrefixedBy(httpClient, getEsBaseUrl(), INDEX_EVENT + 
"0")) {
             newEventcount += countItems(httpClient, eventIndex, null);
         }
 
         int newSessioncount = 0;
-        for (String sessionIndex : 
MigrationUtils.getIndexesPrefixedBy(httpClient, ES_BASE_URL, INDEX_SESSION + 
"0")) {
+        for (String sessionIndex : 
MigrationUtils.getIndexesPrefixedBy(httpClient, getEsBaseUrl(), INDEX_SESSION + 
"0")) {
             newSessioncount += countItems(httpClient, sessionIndex, null);
         }
         Assert.assertEquals(eventCount, newEventcount);
@@ -236,11 +245,11 @@ public class Migrate16xToCurrentVersionIT extends BaseIT {
 
     private void checkIndexReductions2_2_0() throws IOException {
         // new index for system items:
-        Assert.assertTrue(MigrationUtils.indexExists(httpClient, ES_BASE_URL, 
INDEX_SYSTEMITEMS));
+        Assert.assertTrue(MigrationUtils.indexExists(httpClient, 
getEsBaseUrl(), INDEX_SYSTEMITEMS));
 
         // old indices should be removed:
         for (String oldSystemItemsIndex : oldSystemItemsIndices) {
-            Assert.assertFalse(MigrationUtils.indexExists(httpClient, 
ES_BASE_URL, oldSystemItemsIndex));
+            Assert.assertFalse(MigrationUtils.indexExists(httpClient, 
getEsBaseUrl(), oldSystemItemsIndex));
         }
     }
 
@@ -248,16 +257,16 @@ public class Migrate16xToCurrentVersionIT extends BaseIT {
      * Multiple index mappings have been update, check a simple check that 
after migration those mappings contains the latest modifications.
      */
     private void checkForMappingUpdates() throws IOException {
-        Assert.assertTrue(HttpUtils.executeGetRequest(httpClient, ES_BASE_URL 
+ "/" + INDEX_SYSTEMITEMS + "/_mapping", 
null).contains("\"match\":\"*\",\"match_mapping_type\":\"string\",\"mapping\":{\"analyzer\":\"folding\""));
-        Assert.assertTrue(HttpUtils.executeGetRequest(httpClient, ES_BASE_URL 
+ "/" + INDEX_SYSTEMITEMS + "/_mapping", 
null).contains("\"condition\":{\"type\":\"object\",\"enabled\":false}"));
-        Assert.assertTrue(HttpUtils.executeGetRequest(httpClient, ES_BASE_URL 
+ "/" + INDEX_SYSTEMITEMS + "/_mapping", 
null).contains("\"entryCondition\":{\"type\":\"object\",\"enabled\":false}"));
-        Assert.assertTrue(HttpUtils.executeGetRequest(httpClient, ES_BASE_URL 
+ "/" + INDEX_SYSTEMITEMS + "/_mapping", 
null).contains("\"parentCondition\":{\"type\":\"object\",\"enabled\":false}"));
-        Assert.assertTrue(HttpUtils.executeGetRequest(httpClient, ES_BASE_URL 
+ "/" + INDEX_SYSTEMITEMS + "/_mapping", 
null).contains("\"startEvent\":{\"type\":\"object\",\"enabled\":false}"));
-        Assert.assertTrue(HttpUtils.executeGetRequest(httpClient, ES_BASE_URL 
+ "/" + INDEX_SYSTEMITEMS + "/_mapping", 
null).contains("\"data\":{\"type\":\"object\",\"enabled\":false}"));
-        Assert.assertTrue(HttpUtils.executeGetRequest(httpClient, ES_BASE_URL 
+ "/" + INDEX_SYSTEMITEMS + "/_mapping", 
null).contains("\"parameterValues\":{\"type\":\"object\",\"enabled\":false}"));
-        Assert.assertTrue(HttpUtils.executeGetRequest(httpClient, ES_BASE_URL 
+ "/" + INDEX_PROFILE + "/_mapping", 
null).contains("\"interests\":{\"type\":\"nested\""));
-        for (String eventIndex : 
MigrationUtils.getIndexesPrefixedBy(httpClient, ES_BASE_URL, INDEX_EVENT)) {
-            Assert.assertTrue(HttpUtils.executeGetRequest(httpClient, 
ES_BASE_URL + "/" + eventIndex + "/_mapping", 
null).contains("\"flattenedProperties\":{\"type\":\"flattened\"}"));
+        Assert.assertTrue(HttpUtils.executeGetRequest(httpClient, 
getEsBaseUrl() + "/" + INDEX_SYSTEMITEMS + "/_mapping", 
null).contains("\"match\":\"*\",\"match_mapping_type\":\"string\",\"mapping\":{\"analyzer\":\"folding\""));
+        Assert.assertTrue(HttpUtils.executeGetRequest(httpClient, 
getEsBaseUrl() + "/" + INDEX_SYSTEMITEMS + "/_mapping", 
null).contains("\"condition\":{\"type\":\"object\",\"enabled\":false}"));
+        Assert.assertTrue(HttpUtils.executeGetRequest(httpClient, 
getEsBaseUrl() + "/" + INDEX_SYSTEMITEMS + "/_mapping", 
null).contains("\"entryCondition\":{\"type\":\"object\",\"enabled\":false}"));
+        Assert.assertTrue(HttpUtils.executeGetRequest(httpClient, 
getEsBaseUrl() + "/" + INDEX_SYSTEMITEMS + "/_mapping", 
null).contains("\"parentCondition\":{\"type\":\"object\",\"enabled\":false}"));
+        Assert.assertTrue(HttpUtils.executeGetRequest(httpClient, 
getEsBaseUrl() + "/" + INDEX_SYSTEMITEMS + "/_mapping", 
null).contains("\"startEvent\":{\"type\":\"object\",\"enabled\":false}"));
+        Assert.assertTrue(HttpUtils.executeGetRequest(httpClient, 
getEsBaseUrl() + "/" + INDEX_SYSTEMITEMS + "/_mapping", 
null).contains("\"data\":{\"type\":\"object\",\"enabled\":false}"));
+        Assert.assertTrue(HttpUtils.executeGetRequest(httpClient, 
getEsBaseUrl() + "/" + INDEX_SYSTEMITEMS + "/_mapping", 
null).contains("\"parameterValues\":{\"type\":\"object\",\"enabled\":false}"));
+        Assert.assertTrue(HttpUtils.executeGetRequest(httpClient, 
getEsBaseUrl() + "/" + INDEX_PROFILE + "/_mapping", 
null).contains("\"interests\":{\"type\":\"nested\""));
+        for (String eventIndex : 
MigrationUtils.getIndexesPrefixedBy(httpClient, getEsBaseUrl(), INDEX_EVENT)) {
+            Assert.assertTrue(HttpUtils.executeGetRequest(httpClient, 
getEsBaseUrl() + "/" + eventIndex + "/_mapping", 
null).contains("\"flattenedProperties\":{\"type\":\"flattened\"}"));
         }
     }
 
@@ -433,12 +442,12 @@ public class Migrate16xToCurrentVersionIT extends BaseIT {
 
     private void initCounts(CloseableHttpClient httpClient) {
         try {
-            for (String eventIndex : 
MigrationUtils.getIndexesPrefixedBy(httpClient, ES_BASE_URL, INDEX_EVENT + 
"date")) {
+            for (String eventIndex : 
MigrationUtils.getIndexesPrefixedBy(httpClient, getEsBaseUrl(), INDEX_EVENT + 
"date")) {
                 getScopeFromEvents(httpClient, eventIndex);
                 eventCount += countItems(httpClient, eventIndex, 
resourceAsString(RESOURCE_MUST_NOT_MATCH_EVENTTYPE));
             }
 
-            for (String sessionIndex : 
MigrationUtils.getIndexesPrefixedBy(httpClient, ES_BASE_URL, INDEX_SESSION + 
"date")) {
+            for (String sessionIndex : 
MigrationUtils.getIndexesPrefixedBy(httpClient, getEsBaseUrl(), INDEX_SESSION + 
"date")) {
                 sessionCount += countItems(httpClient, sessionIndex, null);
             }
         } catch (IOException e) {
@@ -448,7 +457,7 @@ public class Migrate16xToCurrentVersionIT extends BaseIT {
 
     private void countNumberOfSessionIndices() {
         try {
-           Set<String> sessionIndices = 
MigrationUtils.getIndexesPrefixedBy(httpClient, "http://localhost:9400";, 
"context-session");
+           Set<String> sessionIndices = 
MigrationUtils.getIndexesPrefixedBy(httpClient, getEsBaseUrl(), 
"context-session");
             Assert.assertEquals(2, sessionIndices.size());
         } catch (IOException e) {
             throw new RuntimeException(e);
@@ -456,7 +465,7 @@ public class Migrate16xToCurrentVersionIT extends BaseIT {
     }
     private void getScopeFromEvents(CloseableHttpClient httpClient, String 
eventIndex) throws IOException {
         String requestBody = resourceAsString(RESOURCE_MATCH_ALL_LOGIN_EVENT);
-        JsonNode jsonNode = 
objectMapper.readTree(HttpUtils.executePostRequest(httpClient, ES_BASE_URL + 
"/" + eventIndex + "/_search", requestBody, null));
+        JsonNode jsonNode = 
objectMapper.readTree(HttpUtils.executePostRequest(httpClient, getEsBaseUrl() + 
"/" + eventIndex + "/_search", requestBody, null));
         if (jsonNode.has("hits") && jsonNode.get("hits").has("hits") && 
!jsonNode.get("hits").get("hits").isEmpty()) {
             jsonNode.get("hits").get("hits").forEach(doc -> {
                 JsonNode event = doc.get("_source");
@@ -480,7 +489,7 @@ public class Migrate16xToCurrentVersionIT extends BaseIT {
         if (requestBody == null) {
             requestBody = resourceAsString(RESOURCE_MUST_NOT_MATCH_EVENTTYPE);
         }
-        JsonNode jsonNode = 
objectMapper.readTree(HttpUtils.executePostRequest(httpClient, ES_BASE_URL + 
"/" + index + "/_count", requestBody, null));
+        JsonNode jsonNode = 
objectMapper.readTree(HttpUtils.executePostRequest(httpClient, getEsBaseUrl() + 
"/" + index + "/_count", requestBody, null));
         return jsonNode.get("count").asInt();
     }
 
@@ -513,12 +522,12 @@ public class Migrate16xToCurrentVersionIT extends BaseIT {
         checkDocumentsInIndex(INDEX_PROFILE, TEST_TENANT_ID, false);
 
         // Check event IDs have tenant prefix and audit metadata
-        for (String eventIndex : 
MigrationUtils.getIndexesPrefixedBy(httpClient, ES_BASE_URL, INDEX_EVENT)) {
+        for (String eventIndex : 
MigrationUtils.getIndexesPrefixedBy(httpClient, getEsBaseUrl(), INDEX_EVENT)) {
             checkDocumentsInIndex(eventIndex, TEST_TENANT_ID, false);
         }
 
         // Check session IDs have tenant prefix and audit metadata
-        for (String sessionIndex : 
MigrationUtils.getIndexesPrefixedBy(httpClient, ES_BASE_URL, INDEX_SESSION)) {
+        for (String sessionIndex : 
MigrationUtils.getIndexesPrefixedBy(httpClient, getEsBaseUrl(), INDEX_SESSION)) 
{
             checkDocumentsInIndex(sessionIndex, TEST_TENANT_ID, false);
         }
 
@@ -534,7 +543,7 @@ public class Migrate16xToCurrentVersionIT extends BaseIT {
      * @param isSystemIndex Whether this is a system index that can have both 
system and test tenant IDs
      */
     private void checkDocumentsInIndex(String indexName, String 
expectedTenantId, boolean isSystemIndex) throws IOException {
-        String query = HttpUtils.executeGetRequest(httpClient, ES_BASE_URL + 
"/" + indexName + "/_search?size=10", null);
+        String query = HttpUtils.executeGetRequest(httpClient, getEsBaseUrl() 
+ "/" + indexName + "/_search?size=10", null);
         JsonNode jsonNode = objectMapper.readTree(query);
         if (jsonNode.has("hits") && jsonNode.get("hits").has("hits") && 
!jsonNode.get("hits").get("hits").isEmpty()) {
             for (JsonNode hit : jsonNode.get("hits").get("hits")) {
@@ -614,7 +623,7 @@ public class Migrate16xToCurrentVersionIT extends BaseIT {
         }
         
         // Check that the default tenant index exists
-        Assert.assertTrue("Default tenant index should exist", 
MigrationUtils.indexExists(httpClient, ES_BASE_URL, INDEX_PREFIX_CONTEXT + 
"tenant"));
+        Assert.assertTrue("Default tenant index should exist", 
MigrationUtils.indexExists(httpClient, getEsBaseUrl(), INDEX_PREFIX_CONTEXT + 
"tenant"));
         
         // Check that the default tenant was created with correct structure
         String tenantId = "itTestTenant"; // This should match the tenant ID 
from migration config
@@ -624,7 +633,7 @@ public class Migrate16xToCurrentVersionIT extends BaseIT {
         // The migration creates a tenant with the ID from the migration config
         if (defaultTenant == null) {
             // Check if tenant exists in Elasticsearch directly
-            String query = HttpUtils.executeGetRequest(httpClient, ES_BASE_URL 
+ "/" + INDEX_PREFIX_CONTEXT + "tenant/_search?q=itemId:" + tenantId, null);
+            String query = HttpUtils.executeGetRequest(httpClient, 
getEsBaseUrl() + "/" + INDEX_PREFIX_CONTEXT + "tenant/_search?q=itemId:" + 
tenantId, null);
             JsonNode jsonNode = objectMapper.readTree(query);
             if (jsonNode.has("hits") && jsonNode.get("hits").has("hits") && 
!jsonNode.get("hits").get("hits").isEmpty()) {
                 JsonNode tenantDoc = 
jsonNode.get("hits").get("hits").get(0).get("_source");
@@ -680,7 +689,7 @@ public class Migrate16xToCurrentVersionIT extends BaseIT {
         query.set("query", queryWrapper);
         query.put("size", 1000);
         
-        String response = HttpUtils.executePostRequest(httpClient, ES_BASE_URL 
+ "/" + INDEX_SYSTEMITEMS + "/_search", objectMapper.writeValueAsString(query), 
null);
+        String response = HttpUtils.executePostRequest(httpClient, 
getEsBaseUrl() + "/" + INDEX_SYSTEMITEMS + "/_search", 
objectMapper.writeValueAsString(query), null);
         JsonNode jsonNode = objectMapper.readTree(response);
         
         Set<String> itemIds = new HashSet<>();
@@ -759,5 +768,216 @@ public class Migrate16xToCurrentVersionIT extends BaseIT {
                 inaccessibleItems.isEmpty());
     }
 
+    /**
+     * Test that condition types with legacy queryBuilder IDs have been 
migrated to use new queryBuilder IDs.
+     * This verifies that the migrate-3.1.0-15-updateLegacyQueryBuilder 
migration script correctly updates
+     * all condition types that use legacy *ESQueryBuilder syntax to use the 
new generic QueryBuilder syntax.
+     */
+    private void checkLegacyQueryBuilderMigration() throws Exception {
+        if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)) {
+            System.out.println("Migration from 1.x to 2.x not supported for 
OpenSearch, skipping checks");
+            return;
+        }
+        
+        // Refresh the definitions service cache to ensure migrated items are 
loaded
+        definitionsService.refresh();
+        Thread.sleep(1000);
+        
+        // Legacy to new queryBuilder ID mappings
+        // Based on 
ConditionQueryBuilderDispatcher.LEGACY_TO_NEW_QUERY_BUILDER_IDS
+        String[][] legacyMappings = {
+            {"idsConditionESQueryBuilder", "idsConditionQueryBuilder"},
+            {"geoLocationByPointSessionConditionESQueryBuilder", 
"geoLocationByPointSessionConditionQueryBuilder"},
+            {"pastEventConditionESQueryBuilder", 
"pastEventConditionQueryBuilder"},
+            {"booleanConditionESQueryBuilder", "booleanConditionQueryBuilder"},
+            {"notConditionESQueryBuilder", "notConditionQueryBuilder"},
+            {"matchAllConditionESQueryBuilder", 
"matchAllConditionQueryBuilder"},
+            {"propertyConditionESQueryBuilder", 
"propertyConditionQueryBuilder"},
+            {"sourceEventPropertyConditionESQueryBuilder", 
"sourceEventPropertyConditionQueryBuilder"},
+            {"nestedConditionESQueryBuilder", "nestedConditionQueryBuilder"}
+        };
+        
+        // Query systemitems index for condition types
+        ObjectNode query = JsonNodeFactory.instance.objectNode();
+        ObjectNode termQuery = JsonNodeFactory.instance.objectNode();
+        termQuery.put("itemType.keyword", "conditiontype");
+        ObjectNode queryWrapper = JsonNodeFactory.instance.objectNode();
+        queryWrapper.set("term", termQuery);
+        query.set("query", queryWrapper);
+        query.put("size", 1000);
+        
+        String response = HttpUtils.executePostRequest(httpClient, 
getEsBaseUrl() + "/" + INDEX_SYSTEMITEMS + "/_search", 
objectMapper.writeValueAsString(query), null);
+        JsonNode jsonNode = objectMapper.readTree(response);
+        
+        int conditionTypesChecked = 0;
+        int conditionTypesWithLegacyIds = 0;
+        int conditionTypesWithNewIds = 0;
+        
+        if (jsonNode.has("hits") && jsonNode.get("hits").has("hits")) {
+            for (JsonNode hit : jsonNode.get("hits").get("hits")) {
+                JsonNode source = hit.get("_source");
+                
+                // Only check condition types that have a queryBuilder field
+                if (source.has("queryBuilder")) {
+                    String queryBuilder = source.get("queryBuilder").asText();
+                    conditionTypesChecked++;
+                    
+                    // Check if this is a legacy ID
+                    boolean isLegacyId = false;
+                    for (String[] mapping : legacyMappings) {
+                        if (mapping[0].equals(queryBuilder)) {
+                            isLegacyId = true;
+                            conditionTypesWithLegacyIds++;
+                            String expectedNewId = mapping[1];
+                            Assert.fail("Condition type " + 
source.get("itemId") + " still has legacy queryBuilder ID: " + queryBuilder + 
+                                      ". Expected: " + expectedNewId);
+                            break;
+                        }
+                    }
+                    
+                    // Check if this is a new ID (verify migration worked)
+                    if (!isLegacyId) {
+                        for (String[] mapping : legacyMappings) {
+                            if (mapping[1].equals(queryBuilder)) {
+                                conditionTypesWithNewIds++;
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        
+        // Verify that no condition types have legacy IDs
+        Assert.assertEquals("All condition types with legacy queryBuilder IDs 
should have been migrated. Found " + 
+                          conditionTypesWithLegacyIds + " condition types 
still using legacy IDs", 
+                          0, conditionTypesWithLegacyIds);
+        
+        LOGGER.info("Checked {} condition types for legacy queryBuilder IDs. 
Found {} with new IDs.", 
+                   conditionTypesChecked, conditionTypesWithNewIds);
+        
+        // Verify that rules and segments don't have embedded condition types 
with legacy queryBuilder IDs
+        // Rules and segments only store conditionTypeId references, not full 
ConditionType objects,
+        // but we should verify this to be safe
+        checkRulesAndSegmentsForEmbeddedConditionTypes();
+    }
+    
+    /**
+     * Verifies that rules and segments don't have embedded ConditionType 
objects with legacy queryBuilder IDs.
+     * Rules and segments should only store conditionTypeId references, not 
full ConditionType objects.
+     * This test ensures that even if there were any embedded condition types 
in the past, they don't exist now.
+     */
+    private void checkRulesAndSegmentsForEmbeddedConditionTypes() throws 
Exception {
+        String[][] legacyMappings = {
+            {"idsConditionESQueryBuilder", "idsConditionQueryBuilder"},
+            {"geoLocationByPointSessionConditionESQueryBuilder", 
"geoLocationByPointSessionConditionQueryBuilder"},
+            {"pastEventConditionESQueryBuilder", 
"pastEventConditionQueryBuilder"},
+            {"booleanConditionESQueryBuilder", "booleanConditionQueryBuilder"},
+            {"notConditionESQueryBuilder", "notConditionQueryBuilder"},
+            {"matchAllConditionESQueryBuilder", 
"matchAllConditionQueryBuilder"},
+            {"propertyConditionESQueryBuilder", 
"propertyConditionQueryBuilder"},
+            {"sourceEventPropertyConditionESQueryBuilder", 
"sourceEventPropertyConditionQueryBuilder"},
+            {"nestedConditionESQueryBuilder", "nestedConditionQueryBuilder"}
+        };
+        
+        // Check rules index (rules are stored in systemitems index with 
itemType="rule")
+        // We need to query systemitems for rules, not a separate rules index
+        String rulesIndex = INDEX_SYSTEMITEMS;
+        if (MigrationUtils.indexExists(httpClient, getEsBaseUrl(), 
rulesIndex)) {
+            // Query for rules (itemType="rule") with condition field
+            ObjectNode query = JsonNodeFactory.instance.objectNode();
+            ObjectNode boolQuery = JsonNodeFactory.instance.objectNode();
+            ObjectNode termItemType = JsonNodeFactory.instance.objectNode();
+            termItemType.put("itemType.keyword", "rule");
+            boolQuery.set("must", 
JsonNodeFactory.instance.arrayNode().add(JsonNodeFactory.instance.objectNode().set("term",
 termItemType)));
+            query.set("query", 
JsonNodeFactory.instance.objectNode().set("bool", boolQuery));
+            query.put("size", 100);
+            query.put("_source", "condition");
+            
+            String response = HttpUtils.executePostRequest(httpClient, 
getEsBaseUrl() + "/" + rulesIndex + "/_search", 
+                objectMapper.writeValueAsString(query), null);
+            JsonNode jsonNode = objectMapper.readTree(response);
+            
+            int rulesChecked = 0;
+            int rulesWithEmbeddedConditionTypes = 0;
+            
+            if (jsonNode.has("hits") && jsonNode.get("hits").has("hits")) {
+                for (JsonNode hit : jsonNode.get("hits").get("hits")) {
+                    JsonNode source = hit.get("_source");
+                    if (source.has("condition")) {
+                        rulesChecked++;
+                        // Check if condition has embedded conditionType with 
queryBuilder
+                        JsonNode condition = source.get("condition");
+                        if (condition.has("conditionType") && 
condition.get("conditionType").has("queryBuilder")) {
+                            rulesWithEmbeddedConditionTypes++;
+                            String queryBuilder = 
condition.get("conditionType").get("queryBuilder").asText();
+                            // Check if it's a legacy ID
+                            for (String[] mapping : legacyMappings) {
+                                if (mapping[0].equals(queryBuilder)) {
+                                    Assert.fail("Rule " + 
hit.get("_id").asText() + " has embedded ConditionType with legacy queryBuilder 
ID: " + 
+                                              queryBuilder + ". Rules should 
only store conditionTypeId references, not full ConditionType objects.");
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            
+            LOGGER.info("Checked {} rules for embedded ConditionType objects. 
Found {} with embedded types (should be 0).", 
+                       rulesChecked, rulesWithEmbeddedConditionTypes);
+            Assert.assertEquals("Rules should not have embedded ConditionType 
objects. Found " + rulesWithEmbeddedConditionTypes + 
+                              " rules with embedded types.", 0, 
rulesWithEmbeddedConditionTypes);
+        }
+        
+        // Check segments index (segments are stored in systemitems index with 
itemType="segment")
+        // We need to query systemitems for segments, not a separate segments 
index
+        String segmentsIndex = INDEX_SYSTEMITEMS;
+        if (MigrationUtils.indexExists(httpClient, getEsBaseUrl(), 
segmentsIndex)) {
+            // Query for segments (itemType="segment") with condition field
+            ObjectNode query = JsonNodeFactory.instance.objectNode();
+            ObjectNode boolQuery = JsonNodeFactory.instance.objectNode();
+            ObjectNode termItemType = JsonNodeFactory.instance.objectNode();
+            termItemType.put("itemType.keyword", "segment");
+            boolQuery.set("must", 
JsonNodeFactory.instance.arrayNode().add(JsonNodeFactory.instance.objectNode().set("term",
 termItemType)));
+            query.set("query", 
JsonNodeFactory.instance.objectNode().set("bool", boolQuery));
+            query.put("size", 100);
+            query.put("_source", "condition");
+            
+            String response = HttpUtils.executePostRequest(httpClient, 
getEsBaseUrl() + "/" + segmentsIndex + "/_search", 
+                objectMapper.writeValueAsString(query), null);
+            JsonNode jsonNode = objectMapper.readTree(response);
+            
+            int segmentsChecked = 0;
+            int segmentsWithEmbeddedConditionTypes = 0;
+            
+            if (jsonNode.has("hits") && jsonNode.get("hits").has("hits")) {
+                for (JsonNode hit : jsonNode.get("hits").get("hits")) {
+                    JsonNode source = hit.get("_source");
+                    if (source.has("condition")) {
+                        segmentsChecked++;
+                        // Check if condition has embedded conditionType with 
queryBuilder
+                        JsonNode condition = source.get("condition");
+                        if (condition.has("conditionType") && 
condition.get("conditionType").has("queryBuilder")) {
+                            segmentsWithEmbeddedConditionTypes++;
+                            String queryBuilder = 
condition.get("conditionType").get("queryBuilder").asText();
+                            // Check if it's a legacy ID
+                            for (String[] mapping : legacyMappings) {
+                                if (mapping[0].equals(queryBuilder)) {
+                                    Assert.fail("Segment " + 
hit.get("_id").asText() + " has embedded ConditionType with legacy queryBuilder 
ID: " + 
+                                              queryBuilder + ". Segments 
should only store conditionTypeId references, not full ConditionType objects.");
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            
+            LOGGER.info("Checked {} segments for embedded ConditionType 
objects. Found {} with embedded types (should be 0).", 
+                       segmentsChecked, segmentsWithEmbeddedConditionTypes);
+            Assert.assertEquals("Segments should not have embedded 
ConditionType objects. Found " + segmentsWithEmbeddedConditionTypes + 
+                              " segments with embedded types.", 0, 
segmentsWithEmbeddedConditionTypes);
+        }
+    }
+
 }
 
diff --git 
a/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-3.1.0-15-updateLegacyQueryBuilder.groovy
 
b/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-3.1.0-15-updateLegacyQueryBuilder.groovy
new file mode 100644
index 000000000..9e89f6414
--- /dev/null
+++ 
b/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-3.1.0-15-updateLegacyQueryBuilder.groovy
@@ -0,0 +1,129 @@
+import org.apache.unomi.shell.migration.service.MigrationContext
+import org.apache.unomi.shell.migration.utils.HttpUtils
+import org.apache.unomi.shell.migration.utils.MigrationUtils
+import org.json.JSONArray
+import org.json.JSONObject
+
+import static 
org.apache.unomi.shell.migration.service.MigrationConfig.CONFIG_ES_ADDRESS
+import static 
org.apache.unomi.shell.migration.service.MigrationConfig.INDEX_PREFIX
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+MigrationContext context = migrationContext
+String esAddress = context.getConfigString(CONFIG_ES_ADDRESS)
+String indexPrefix = context.getConfigString(INDEX_PREFIX)
+
+// This migration updates all condition types that still use the legacy 
*ESQueryBuilder syntax
+// and replaces them with the proper generic QueryBuilder syntax.
+// Uses pattern matching to find any queryBuilder ending with "ESQueryBuilder" 
and replace
+// it with "QueryBuilder" (e.g., "propertyConditionESQueryBuilder" → 
"propertyConditionQueryBuilder").
+// This approach is more robust than a hardcoded list and will catch all 
legacy IDs, including
+// custom ones that might have been created by plugins.
+context.performMigrationStep("3.1.0-update-legacy-querybuilder", () -> {
+    String systemItemsIndex = "${indexPrefix}-systemitems"
+
+    if (MigrationUtils.indexExists(context.getHttpClient(), esAddress, 
systemItemsIndex)) {
+        context.printMessage("Updating condition types with legacy 
queryBuilder IDs in systemitems index")
+
+        // Get the Painless script from file
+        String updateScript = 
MigrationUtils.getFileWithoutComments(bundleContext, 
"requestBody/3.1.0/update_legacy_querybuilder.painless")
+
+        // Build the update request using JSONObject to properly escape the 
script
+        JSONObject scriptObj = new JSONObject()
+        scriptObj.put("source", updateScript)
+        scriptObj.put("lang", "painless")
+
+        // Query for condition types with legacy queryBuilder IDs
+        JSONObject queryObj = new JSONObject()
+        JSONObject boolObj = new JSONObject()
+        JSONArray mustArray = new JSONArray()
+
+        // Match condition types - handle both "conditionType" and 
"conditiontype" casings
+        // Note: itemType can be stored with different casings, so we use a 
should clause
+        // to match either variant. The queryBuilder wildcard will catch all 
legacy IDs regardless.
+        JSONObject itemTypeBool = new JSONObject()
+        JSONArray shouldItemTypeArray = new JSONArray()
+
+        JSONObject termItemType1 = new JSONObject()
+        JSONObject termItemTypeValue1 = new JSONObject()
+        termItemTypeValue1.put("itemType.keyword", "conditionType")
+        termItemType1.put("term", termItemTypeValue1)
+        shouldItemTypeArray.put(termItemType1)
+
+        JSONObject termItemType2 = new JSONObject()
+        JSONObject termItemTypeValue2 = new JSONObject()
+        termItemTypeValue2.put("itemType.keyword", "conditiontype")
+        termItemType2.put("term", termItemTypeValue2)
+        shouldItemTypeArray.put(termItemType2)
+
+        itemTypeBool.put("should", shouldItemTypeArray)
+        itemTypeBool.put("minimum_should_match", 1)
+        JSONObject itemTypeBoolWrapper = new JSONObject()
+        itemTypeBoolWrapper.put("bool", itemTypeBool)
+        mustArray.put(itemTypeBoolWrapper)
+
+        // Match any queryBuilder ending with "ESQueryBuilder" using a 
wildcard query
+        // This is more robust than a hardcoded list and will catch all legacy 
IDs
+        JSONObject wildcardQueryBuilder = new JSONObject()
+        JSONObject wildcardQueryBuilderValue = new JSONObject()
+        wildcardQueryBuilderValue.put("queryBuilder.keyword", 
"*ESQueryBuilder")
+        wildcardQueryBuilder.put("wildcard", wildcardQueryBuilderValue)
+        mustArray.put(wildcardQueryBuilder)
+
+        boolObj.put("must", mustArray)
+        queryObj.put("bool", boolObj)
+
+        JSONObject updateRequestObj = new JSONObject()
+        updateRequestObj.put("script", scriptObj)
+        updateRequestObj.put("query", queryObj)
+
+        String updateRequest = updateRequestObj.toString()
+
+        try {
+            context.printMessage("Updating condition types with legacy 
queryBuilder IDs...")
+            String updateResponse = 
MigrationUtils.updateByQuery(context.getHttpClient(), esAddress, 
systemItemsIndex, updateRequest)
+            context.printMessage("Update response: ${updateResponse}")
+
+            // Parse response to get update count
+            try {
+                JSONObject responseObj = new JSONObject(updateResponse)
+                if (responseObj.has("updated")) {
+                    int updatedCount = responseObj.getInt("updated")
+                    context.printMessage("Successfully updated ${updatedCount} 
condition type(s) with legacy queryBuilder IDs")
+                } else if (responseObj.has("total")) {
+                    int totalCount = responseObj.getInt("total")
+                    context.printMessage("Found ${totalCount} condition 
type(s) to update")
+                }
+            } catch (Exception parseException) {
+                context.printMessage("Could not parse update response, but 
update completed")
+            }
+
+            context.printMessage("Successfully updated condition types with 
legacy queryBuilder IDs")
+        } catch (Exception e) {
+            context.printException("Error updating condition types with legacy 
queryBuilder IDs", e)
+            throw e
+        }
+
+        // Refresh the index to make changes visible
+        HttpUtils.executePostRequest(context.getHttpClient(), esAddress + 
"/${systemItemsIndex}/_refresh", null, null)
+        context.printMessage("Migration completed: Updated condition types 
with legacy queryBuilder IDs")
+    } else {
+        context.printMessage("Systemitems index does not exist, skipping 
legacy queryBuilder update")
+    }
+})
+
diff --git 
a/tools/shell-commands/src/main/resources/requestBody/3.1.0/update_legacy_querybuilder.painless
 
b/tools/shell-commands/src/main/resources/requestBody/3.1.0/update_legacy_querybuilder.painless
new file mode 100644
index 000000000..edaf05d0b
--- /dev/null
+++ 
b/tools/shell-commands/src/main/resources/requestBody/3.1.0/update_legacy_querybuilder.painless
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Update legacy queryBuilder IDs to new format
+// This script updates condition types that use legacy *ESQueryBuilder syntax
+// to use the new generic QueryBuilder syntax
+// Uses pattern matching to replace any queryBuilder ending with 
"ESQueryBuilder" 
+// with "QueryBuilder" (e.g., "propertyConditionESQueryBuilder" → 
"propertyConditionQueryBuilder")
+if (ctx._source.queryBuilder != null && ctx._source.queryBuilder instanceof 
String) {
+    def queryBuilder = ctx._source.queryBuilder;
+    
+    // Check if queryBuilder ends with "ESQueryBuilder" and replace with 
"QueryBuilder"
+    if (queryBuilder.endsWith("ESQueryBuilder")) {
+        // Replace "ESQueryBuilder" suffix with "QueryBuilder"
+        // String.replace() in Painless replaces all occurrences, which is 
what we want
+        ctx._source.queryBuilder = queryBuilder.replace("ESQueryBuilder", 
"QueryBuilder");
+    }
+}
+


Reply via email to