diff --git a/repository/src/main/java/org/alfresco/repo/admin/patch/impl/SchemaUpgradeScriptPatch.java b/repository/src/main/java/org/alfresco/repo/admin/patch/impl/SchemaUpgradeScriptPatch.java index fc0cab680d..f2106e814a 100644 --- a/repository/src/main/java/org/alfresco/repo/admin/patch/impl/SchemaUpgradeScriptPatch.java +++ b/repository/src/main/java/org/alfresco/repo/admin/patch/impl/SchemaUpgradeScriptPatch.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2021 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -40,7 +40,8 @@ public class SchemaUpgradeScriptPatch extends AbstractPatch { private static final String MSG_NOT_EXECUTED = "patch.schemaUpgradeScript.err.not_executed"; - private String scriptUrl; + private String scriptUrl; + private String problemsPatternFileUrl; public SchemaUpgradeScriptPatch() { @@ -52,8 +53,13 @@ public class SchemaUpgradeScriptPatch extends AbstractPatch public String getScriptUrl() { return scriptUrl; - } + } + public String getProblemPatternsFileUrl() + { + return problemsPatternFileUrl; + } + /** * Set the URL of the upgrade scriptUrl to execute. This is the full URL of the * file, e.g. classpath:alfresco/patch/scripts/upgrade-1.4/${hibernate.dialect.class}/patchAlfrescoSchemaUpdate-1.4-2.sql @@ -65,12 +71,25 @@ public class SchemaUpgradeScriptPatch extends AbstractPatch public void setScriptUrl(String script) { this.scriptUrl = script; - } + } + + /** + * Set the URL of the problems pattern file to accompany the upgrade script. This is the full URL of the + * file, e.g. classpath:alfresco/patch/scripts/upgrade-1.4/${hibernate.dialect.class}/patchAlfrescoSchemaUpdate-1.4-2-problems.txt + * where the ${hibernate.dialect.class} placeholder will be substituted with the Hibernate + * Dialect as configured for the system. + * + * @param problemsFile the problems file + */ + public void setProblemsPatternFileUrl(String problemsFile) + { + this.problemsPatternFileUrl = problemsFile; + } protected void checkProperties() { super.checkProperties(); - checkPropertyNotNull(scriptUrl, "scriptUrl"); + checkPropertyNotNull(scriptUrl, "scriptUrl"); } /** @@ -79,6 +98,6 @@ public class SchemaUpgradeScriptPatch extends AbstractPatch @Override protected String applyInternal() throws Exception { - throw new PatchException(MSG_NOT_EXECUTED, scriptUrl); - } + throw new PatchException(MSG_NOT_EXECUTED, scriptUrl); + } } diff --git a/repository/src/main/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java b/repository/src/main/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java index a18212cc53..873fd3a753 100644 --- a/repository/src/main/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java +++ b/repository/src/main/java/org/alfresco/repo/domain/schema/SchemaBootstrap.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2021 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -85,12 +85,14 @@ import org.alfresco.util.DialectUtil; import org.alfresco.util.LogUtil; import org.alfresco.util.PropertyCheck; import org.alfresco.util.TempFileProvider; +import org.alfresco.util.schemacomp.Difference; import org.alfresco.util.schemacomp.ExportDb; import org.alfresco.util.schemacomp.MultiFileDumper; import org.alfresco.util.schemacomp.MultiFileDumper.DbToXMLFactory; import org.alfresco.util.schemacomp.Result; import org.alfresco.util.schemacomp.Results; import org.alfresco.util.schemacomp.SchemaComparator; +import org.alfresco.util.schemacomp.SchemaDifferenceHelper; import org.alfresco.util.schemacomp.XMLToSchema; import org.alfresco.util.schemacomp.model.Schema; import org.apache.commons.logging.Log; @@ -124,6 +126,7 @@ public class SchemaBootstrap extends AbstractLifecycleBean private static final String MSG_EXECUTING_COPIED_SCRIPT = "schema.update.msg.executing_copied_script"; private static final String MSG_EXECUTING_STATEMENT = "schema.update.msg.executing_statement"; private static final String MSG_OPTIONAL_STATEMENT_FAILED = "schema.update.msg.optional_statement_failed"; + private static final String MSG_OPTIONAL_PATCH_RUN_SUGGESTION = "system.schema_comp.patch_run_suggestion"; private static final String ERR_FORCED_STOP = "schema.update.err.forced_stop"; private static final String ERR_MULTIPLE_SCHEMAS = "schema.update.err.found_multiple"; private static final String ERR_PREVIOUS_FAILED_BOOTSTRAP = "schema.update.err.previous_failed"; @@ -153,7 +156,8 @@ public class SchemaBootstrap extends AbstractLifecycleBean private static volatile int maxStringLength = DEFAULT_MAX_STRING_LENGTH; private Dialect dialect; - + private SchemaDifferenceHelper differenceHelper; + private ResourcePatternResolver rpr = new PathMatchingResourcePatternResolver(this.getClass().getClassLoader()); /** @@ -233,6 +237,11 @@ public class SchemaBootstrap extends AbstractLifecycleBean this.dialect = dialect; } + public void setDifferenceHelper(SchemaDifferenceHelper differenceHelper) + { + this.differenceHelper = differenceHelper; + } + private static Log logger = LogFactory.getLog(SchemaBootstrap.class); private DescriptorService descriptorService; @@ -1815,7 +1824,7 @@ public class SchemaBootstrap extends AbstractLifecycleBean // Return number of problems found across all reference files. return totalProblems; } - + private int validateSchema(Resource referenceResource, String outputFileNameTemplate, PrintWriter out) { try @@ -1916,11 +1925,42 @@ public class SchemaBootstrap extends AbstractLifecycleBean pw = out; } + Map> optionalPatchMessages = new HashMap<>(); // Populate the file with details of the comparison's results. for (Result result : results) { - pw.print(result.describe()); + String optionalPatchId = findPatchCausingDifference(result, target); + String differenceMessage = result.describe(); + if (optionalPatchId == null) + { + pw.print(differenceMessage); + pw.print(SchemaComparator.LINE_SEPARATOR); + } + else + { + if (optionalPatchMessages.containsKey(optionalPatchId)) + { + optionalPatchMessages.get(optionalPatchId).add(differenceMessage); + } + else + { + List newResults = new ArrayList<>(); + newResults.add(differenceMessage); + optionalPatchMessages.put(optionalPatchId, newResults); + } + } + } + + for (String optionalPatchId: optionalPatchMessages.keySet()) + { pw.print(SchemaComparator.LINE_SEPARATOR); + pw.print(I18NUtil.getMessage(MSG_OPTIONAL_PATCH_RUN_SUGGESTION, optionalPatchId)); + pw.print(SchemaComparator.LINE_SEPARATOR); + for (String optionalPatchMessage: optionalPatchMessages.get(optionalPatchId)) + { + pw.print(optionalPatchMessage); + pw.print(SchemaComparator.LINE_SEPARATOR); + } } } finally @@ -1946,7 +1986,7 @@ public class SchemaBootstrap extends AbstractLifecycleBean } else { - LogUtil.warn(logger, WARN_SCHEMA_COMP_PROBLEMS_FOUND, numProblems, outputFile); + LogUtil.warn(logger, WARN_SCHEMA_COMP_PROBLEMS_FOUND, numProblems, outputFile); } } Date endTime = new Date(); @@ -1956,6 +1996,17 @@ public class SchemaBootstrap extends AbstractLifecycleBean return results.size(); } + private String findPatchCausingDifference(Result result, Schema currentDb) + { + // In new installations of the system the schema validation is run twice. Since none of the alf_ tables is present there is no need to seek for unapplied patches. + if (!currentDb.containsByName("alf_applied_patch")) + { + return null; + } + + return differenceHelper.findPatchCausingDifference((Difference)result); + } + /** * Produces schema dump in XML format: this is performed pre- and post-upgrade (i.e. if * changes are made to the schema) and can made upon demand via JMX. diff --git a/repository/src/main/java/org/alfresco/repo/domain/schema/SchemaBootstrapRegistration.java b/repository/src/main/java/org/alfresco/repo/domain/schema/SchemaBootstrapRegistration.java index 619d7590ef..686859f2e4 100644 --- a/repository/src/main/java/org/alfresco/repo/domain/schema/SchemaBootstrapRegistration.java +++ b/repository/src/main/java/org/alfresco/repo/domain/schema/SchemaBootstrapRegistration.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2021 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -29,7 +29,8 @@ import java.util.Collections; import java.util.List; import org.alfresco.repo.admin.patch.impl.SchemaUpgradeScriptPatch; -import org.alfresco.util.PropertyCheck; +import org.alfresco.util.PropertyCheck; +import org.alfresco.util.schemacomp.SchemaDifferenceHelper; /** * Registers a list of create scripts. @@ -44,7 +45,8 @@ public class SchemaBootstrapRegistration private List postCreateScriptUrls; private List preUpdateScriptPatches; private List postUpdateScriptPatches; - private List updateActivitiScriptPatches; + private List updateActivitiScriptPatches; + private SchemaDifferenceHelper differenceHelper; public SchemaBootstrapRegistration() { @@ -61,6 +63,14 @@ public class SchemaBootstrapRegistration public void setSchemaBootstrap(SchemaBootstrap schemaBootstrap) { this.schemaBootstrap = schemaBootstrap; + } + + /** + * @param differenceHelper the component with which to register upgrade script pacthes + */ + public void setDifferenceHelper(SchemaDifferenceHelper differenceHelper) + { + this.differenceHelper = differenceHelper; } /** @@ -139,7 +149,8 @@ public class SchemaBootstrapRegistration } for (SchemaUpgradeScriptPatch postUpdateScriptPatch : postUpdateScriptPatches) { - schemaBootstrap.addPostUpdateScriptPatch(postUpdateScriptPatch); + schemaBootstrap.addPostUpdateScriptPatch(postUpdateScriptPatch); + differenceHelper.addUpgradeScriptPatch(postUpdateScriptPatch); } for (SchemaUpgradeScriptPatch updateActivitiScriptPatch : updateActivitiScriptPatches) { diff --git a/repository/src/main/java/org/alfresco/util/schemacomp/SchemaDifferenceHelper.java b/repository/src/main/java/org/alfresco/util/schemacomp/SchemaDifferenceHelper.java new file mode 100644 index 0000000000..9b12bcafd1 --- /dev/null +++ b/repository/src/main/java/org/alfresco/util/schemacomp/SchemaDifferenceHelper.java @@ -0,0 +1,170 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.util.schemacomp; + +import static java.util.Locale.ENGLISH; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import org.alfresco.repo.admin.patch.PatchService; +import org.alfresco.repo.admin.patch.impl.SchemaUpgradeScriptPatch; +import org.alfresco.repo.domain.dialect.Dialect; +import org.alfresco.util.DialectUtil; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.extensions.surf.util.I18NUtil; + +public class SchemaDifferenceHelper +{ + private static Log logger = LogFactory.getLog(SchemaDifferenceHelper.class); + + private Dialect dialect; + private PatchService patchService; + private List optionalUpgradePatches; + private ResourcePatternResolver rpr = new PathMatchingResourcePatternResolver(this.getClass().getClassLoader()); + + public SchemaDifferenceHelper(Dialect dialect, PatchService patchService) + { + this.dialect = dialect; + this.patchService = patchService; + this.optionalUpgradePatches = new ArrayList(4); + } + + public SchemaDifferenceHelper(Dialect dialect, PatchService patchService, + List upgradePatches) + { + this.dialect = dialect; + this.patchService = patchService; + this.optionalUpgradePatches = upgradePatches; + } + + public void addUpgradeScriptPatch(SchemaUpgradeScriptPatch patch) + { + if (patch.isIgnored()) + { + this.optionalUpgradePatches.add(patch); + } + } + + public String findPatchCausingDifference(Difference difference) + { + for (SchemaUpgradeScriptPatch patch: optionalUpgradePatches) + { + if (!isPatchApplied(patch)) + { + List problemPatterns = getProblemsPatterns(patch); + for (String problemPattern: problemPatterns) + { + if (describe(difference).matches(problemPattern)) + { + return patch.getId(); + } + } + } + } + + return null; + } + + private boolean isPatchApplied(SchemaUpgradeScriptPatch patch) + { + return patchService.getPatch(patch.getId()) != null; + } + + protected Resource getDialectResource(String resourceUrl) + { + if(resourceUrl == null) + { + return null; + } + + return DialectUtil.getDialectResource(rpr, dialect.getClass(), resourceUrl); + } + + private List getProblemsPatterns(SchemaUpgradeScriptPatch patch) + { + List optionalProblems = new ArrayList<>(); + String problemFileUrl = patch.getProblemPatternsFileUrl(); + Resource problemFile = getDialectResource(problemFileUrl); + + if (problemFile != null) + { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(problemFile.getInputStream(), StandardCharsets.UTF_8))) + { + String line = reader.readLine(); + while (line != null) + { + optionalProblems.add(line); + line = reader.readLine(); + } + } + catch (Exception ex) + { + logger.error("Error while parsing problems patterns for patch " + patch.getId() + ex); + } + } + + return optionalProblems; + } + + protected String describe(Difference difference) + { + if (difference.getLeft() == null) + { + return I18NUtil.getMessage( + "system.schema_comp.diff.target_only", + ENGLISH, + difference.getRight().getDbObject().getTypeName(), + difference.getRight().getPath(), + difference.getRight().getPropertyValue()); + } + if (difference.getRight() == null) + { + return I18NUtil.getMessage( + "system.schema_comp.diff.ref_only", + ENGLISH, + difference.getLeft().getDbObject().getTypeName(), + difference.getLeft().getPath(), + difference.getLeft().getPropertyValue()); + } + + return I18NUtil.getMessage( + "system.schema_comp.diff", + ENGLISH, + difference.getLeft().getDbObject().getTypeName(), + difference.getLeft().getPath(), + difference.getLeft().getPropertyValue(), + difference.getRight().getPath(), + difference.getRight().getPropertyValue()); + } +} diff --git a/repository/src/main/resources/alfresco/bootstrap-context.xml b/repository/src/main/resources/alfresco/bootstrap-context.xml index d47197fa22..31d9da6957 100644 --- a/repository/src/main/resources/alfresco/bootstrap-context.xml +++ b/repository/src/main/resources/alfresco/bootstrap-context.xml @@ -96,6 +96,14 @@ classpath:alfresco/dbscripts/create/${db.script.dialect}/Schema-Reference-ACT.xml + + + + + + + + diff --git a/repository/src/main/resources/alfresco/dbscripts/db-schema-context.xml b/repository/src/main/resources/alfresco/dbscripts/db-schema-context.xml index f646454b35..9f1c79fb9d 100644 --- a/repository/src/main/resources/alfresco/dbscripts/db-schema-context.xml +++ b/repository/src/main/resources/alfresco/dbscripts/db-schema-context.xml @@ -29,6 +29,7 @@ + diff --git a/repository/src/main/resources/alfresco/messages/system-messages.properties b/repository/src/main/resources/alfresco/messages/system-messages.properties index 8ca6e11ff6..a563e83277 100644 --- a/repository/src/main/resources/alfresco/messages/system-messages.properties +++ b/repository/src/main/resources/alfresco/messages/system-messages.properties @@ -37,6 +37,8 @@ system.schema_comp.name_validator=name must match pattern ''{0}'' system.schema_comp.index_columns_validator=Number of columns in index doesn''t match. Was {0}, but expected {1} system.schema_comp.column_names_validator=Column types do not match. Was {0}, but expected {1} system.schema_comp.schema_version_validator=version must be at least ''{0}'' +# Optional long running patch messages... +system.schema_comp.patch_run_suggestion=The following problems will be resolved once the long running patch {0} has been run # Clustering system.cluster.license.not_enabled=License does not permit clustering: clustering is disabled. diff --git a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java index d2cdb9d177..2ce7585989 100644 --- a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java +++ b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2020 Alfresco Software Limited + * Copyright (C) 2005 - 2021 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -233,7 +233,9 @@ import org.junit.runners.Suite; org.alfresco.repo.rendition2.TransformationOptionsConverterTest.class, org.alfresco.transform.client.registry.TransformServiceRegistryConfigTest.class, - org.alfresco.repo.event2.RepoEvent2UnitSuite.class + org.alfresco.repo.event2.RepoEvent2UnitSuite.class, + + org.alfresco.util.schemacomp.SchemaDifferenceHelperUnitTest.class }) public class AllUnitTestsSuite { diff --git a/repository/src/test/java/org/alfresco/AppContext04TestSuite.java b/repository/src/test/java/org/alfresco/AppContext04TestSuite.java index 52c936b170..45676ea4b1 100644 --- a/repository/src/test/java/org/alfresco/AppContext04TestSuite.java +++ b/repository/src/test/java/org/alfresco/AppContext04TestSuite.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2017 Alfresco Software Limited + * Copyright (C) 2005 - 2021 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -85,6 +85,7 @@ import org.junit.runners.Suite; org.alfresco.util.schemacomp.DbToXMLTest.class, org.alfresco.util.schemacomp.ExportDbTest.class, org.alfresco.util.schemacomp.SchemaReferenceFileTest.class, + org.alfresco.util.schemacomp.SchemaBootstrapTest.class, org.alfresco.repo.module.ModuleComponentHelperTest.class, org.alfresco.repo.node.getchildren.GetChildrenCannedQueryTest.class, diff --git a/repository/src/test/java/org/alfresco/util/schemacomp/SchemaBootstrapTest.java b/repository/src/test/java/org/alfresco/util/schemacomp/SchemaBootstrapTest.java new file mode 100644 index 0000000000..f1bfe14f5f --- /dev/null +++ b/repository/src/test/java/org/alfresco/util/schemacomp/SchemaBootstrapTest.java @@ -0,0 +1,86 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.util.schemacomp; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.List; + +import org.alfresco.repo.admin.patch.impl.SchemaUpgradeScriptPatch; +import org.alfresco.repo.domain.schema.SchemaBootstrap; +import org.alfresco.test_category.OwnJVMTestsCategory; +import org.alfresco.util.test.junitrules.ApplicationContextInit; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.RuleChain; + +@Category({OwnJVMTestsCategory.class}) +public class SchemaBootstrapTest +{ + private static final String BOOTSTRAP_TEST_CONTEXT = "classpath*:alfresco/dbscripts/test-bootstrap-context.xml"; + private static final List TEST_SCHEMA_REFERENCE_URLS = Arrays.asList( + "classpath:alfresco/dbscripts/create/${db.script.dialect}/Test-Schema-Reference-ALF.xml", + "classpath:alfresco/dbscripts/create/${db.script.dialect}/Schema-Reference-ACT.xml"); + + private static ApplicationContextInit APP_CONTEXT_INIT = ApplicationContextInit.createStandardContextWithOverrides(BOOTSTRAP_TEST_CONTEXT); + + @ClassRule + public static RuleChain staticRuleChain = RuleChain.outerRule(APP_CONTEXT_INIT); + + private SchemaBootstrap schemaBootstrap; + private SchemaUpgradeScriptPatch optionalPatch; + + @Before + public void setUp() throws Exception + { + schemaBootstrap = (SchemaBootstrap) APP_CONTEXT_INIT.getApplicationContext().getBean("schemaBootstrap"); + schemaBootstrap.setSchemaReferenceUrls(TEST_SCHEMA_REFERENCE_URLS); + optionalPatch = (SchemaUpgradeScriptPatch) APP_CONTEXT_INIT.getApplicationContext().getBean("patchDbVOAddIndexTest"); + } + + @Test + public void shouldSchemaValidationReportProblemsCausedByUnappliedOptionalPatch() + { + ByteArrayOutputStream buff = new ByteArrayOutputStream(); + + PrintWriter out = new PrintWriter(buff); + int numProblems = schemaBootstrap.validateSchema(null, out); + out.flush(); + + assertEquals(1, numProblems); + String problems = buff.toString(); + assertTrue("Missing optional patch-specific problems report: \n" + problems, + problems.contains("The following problems will be resolved once the long running patch " + + optionalPatch.getId() + " has been run")); + } + +} diff --git a/repository/src/test/java/org/alfresco/util/schemacomp/SchemaDifferenceHelperUnitTest.java b/repository/src/test/java/org/alfresco/util/schemacomp/SchemaDifferenceHelperUnitTest.java new file mode 100644 index 0000000000..2592e45600 --- /dev/null +++ b/repository/src/test/java/org/alfresco/util/schemacomp/SchemaDifferenceHelperUnitTest.java @@ -0,0 +1,207 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.util.schemacomp; + +import static org.alfresco.util.schemacomp.Difference.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; + +import org.alfresco.repo.admin.patch.AppliedPatch; +import org.alfresco.repo.admin.patch.PatchService; +import org.alfresco.repo.admin.patch.impl.SchemaUpgradeScriptPatch; +import org.alfresco.repo.domain.dialect.Dialect; +import org.alfresco.util.schemacomp.model.Index; +import org.alfresco.util.schemacomp.model.Schema; +import org.alfresco.util.schemacomp.model.Table; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; + +public class SchemaDifferenceHelperUnitTest +{ + private static final String TEST_PATCH_ID = "patch.db-V1.0-test"; + private static final String BASE_PROBLEM_PATTERN = ".*missing %s.*%s"; + + private SchemaDifferenceHelper differenceHelper; + private PatchService patchService; + private Dialect dialect; + + @Rule + public TemporaryFolder testFolder = new TemporaryFolder(); + + @Before + public void setup() + { + dialect = mock(Dialect.class); + patchService = mock(PatchService.class); + } + + @Test + public void shouldNotFindPatchWhenThereAreNoUpgradePatches() + { + Difference diff = createDifference(); + differenceHelper = createHelper(Arrays.asList()); + + assertNull(differenceHelper.findPatchCausingDifference(diff)); + } + + @Test + public void shouldNotFindPatchWhenUpgradePatchHasBeenApplied() throws IOException + { + Difference diff = createDifference(); + SchemaUpgradeScriptPatch upgradeScript = createUpgradeScript(TEST_PATCH_ID); + when(patchService.getPatch(TEST_PATCH_ID)).thenReturn(new AppliedPatch()); + + differenceHelper = createHelper(Arrays.asList(upgradeScript)); + String result = differenceHelper.findPatchCausingDifference(diff); + + assertNull(result); + } + + @Test + public void shouldNotFindPatchWhenUpgradePatchDoesNotProvideAnyProblemPatternsFile() throws IOException + { + Difference diff = createDifference(); + SchemaUpgradeScriptPatch upgradeScript = createUpgradeScript(TEST_PATCH_ID); + upgradeScript.setProblemsPatternFileUrl(null); + when(patchService.getPatch(TEST_PATCH_ID)).thenReturn(null); + + differenceHelper = createHelper(Arrays.asList(upgradeScript)); + String result = differenceHelper.findPatchCausingDifference(diff); + + assertNull(result); + } + + @Test + public void shouldFindPatchWhenDifferenceCausedByUpgradePatchIsDetected() throws IOException + { + Index index = createTableIndex("alf_node"); + Difference diff = new Difference(Where.ONLY_IN_REFERENCE, new DbProperty(index), null); + SchemaUpgradeScriptPatch upgradeScript = createUpgradeScript(TEST_PATCH_ID, + String.format(BASE_PROBLEM_PATTERN, index.getTypeName(), "idx_alf_node_test")); + when(patchService.getPatch(TEST_PATCH_ID)).thenReturn(null); + + differenceHelper = createHelper(Arrays.asList(upgradeScript)); + String result = differenceHelper.findPatchCausingDifference(diff); + + assertEquals(TEST_PATCH_ID, result); + } + + private Difference createDifference() + { + Difference difference = new Difference(Where.IN_BOTH_BUT_DIFFERENCE, mock(DbProperty.class), mock(DbProperty.class)); + return difference; + } + + private Index createTableIndex(String tableName) + { + Table table = new Table(tableName); + table.setParent(new Schema("")); + return new Index(table, "idx_alf_node_test", Arrays.asList("col_a", "col_b")); + } + + private SchemaUpgradeScriptPatch createUpgradeScript(String id, String problemPattern) throws IOException + { + SchemaUpgradeScriptPatch upgradeScript = new SchemaUpgradeScriptPatch(); + upgradeScript.setId(id); + Path file = createTempFile(problemPattern); + upgradeScript.setProblemsPatternFileUrl(file.toAbsolutePath().toString()); + return upgradeScript; + } + + private SchemaUpgradeScriptPatch createUpgradeScript(String id) throws IOException + { + return createUpgradeScript(id, ""); + } + + private Path createTempFile() throws IOException + { + return Files.createTempFile(testFolder.getRoot().toPath(), null, "txt"); + } + + private Path createTempFile(String content) throws IOException + { + Path tempFile = createTempFile(); + Files.write(tempFile, content.getBytes(StandardCharsets.UTF_8)); + return tempFile; + } + + private SchemaDifferenceHelper createHelper(List upgradePatches) + { + return new SchemaDifferenceHelper(dialect, patchService, upgradePatches) { + @Override + protected String describe(Difference difference) + { + if (difference.getLeft() == null) + { + return String.format("Difference: unexpected %s found in database with path: %s", + + difference.getRight().getDbObject().getTypeName(), + difference.getRight().getPath()); + } + if(difference.getRight() == null) + { + return String.format("Difference: missing %s from database, expected at path: %s", + difference.getLeft().getDbObject().getTypeName(), + difference.getLeft().getPath()); + } + + return String.format("Difference: expected %s %s=\"%s\", but was %s=\"%s\"", + difference.getLeft().getDbObject().getTypeName(), + difference.getLeft().getPath(), + difference.getLeft().getPropertyValue(), + difference.getRight().getPath(), + difference.getRight().getPropertyValue()); + } + + @Override + protected Resource getDialectResource(String resourceUrl) + { + try + { + return new InputStreamResource(new FileInputStream(resourceUrl)); + } + catch (Exception e) + { + return null; + } + } + }; + } +} diff --git a/repository/src/test/resources/alfresco/dbscripts/create/org.alfresco.repo.domain.dialect.PostgreSQLDialect/Test-Schema-Reference-ALF.xml b/repository/src/test/resources/alfresco/dbscripts/create/org.alfresco.repo.domain.dialect.PostgreSQLDialect/Test-Schema-Reference-ALF.xml new file mode 100644 index 0000000000..927a2fc2a2 --- /dev/null +++ b/repository/src/test/resources/alfresco/dbscripts/create/org.alfresco.repo.domain.dialect.PostgreSQLDialect/Test-Schema-Reference-ALF.xml @@ -0,0 +1,2829 @@ + + + + + + .* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + bool + false + false + + + int4 + false + false + + + int8 + true + false + + + + + id + + + + + context_id + alf_ace_context + id + + + authority_id + alf_authority + id + + + permission_id + alf_permission + id + + + + + + permission_id + authority_id + allowed + applies + + + + + authority_id + + + + + context_id + + + + + permission_id + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + varchar(36) + false + false + + + bool + false + false + + + int8 + false + false + + + bool + false + false + + + int8 + true + false + + + int4 + false + false + + + int8 + true + false + + + bool + false + false + + + bool + false + false + + + int8 + true + false + + + + + id + + + + + acl_change_set + alf_acl_change_set + id + + + + + + acl_id + latest + acl_version + + + + + acl_change_set + + + + + inherits + inherits_from + + + + + acl_change_set + id + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + varchar(1024) + true + false + + + varchar(1024) + true + false + + + varchar(1024) + true + false + + + + + id + + + + +
+ + + + int8 + false + false + + + int8 + true + false + + + + + id + + + + + + + commit_time_ms + id + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + int4 + false + false + + + + + id + + + + + ace_id + alf_access_control_entry + id + + + acl_id + alf_access_control_list + id + + + + + + acl_id + ace_id + pos + + + + + ace_id + + + + + acl_id + + + +
+ + + + int8 + false + false + + + int8 + true + false + + + timestamp + false + false + + + varchar(1024) + true + false + + + varchar(255) + true + false + + + varchar(255) + false + false + + + varchar(255) + true + false + + + varchar(36) + true + false + + + varchar(255) + false + false + + + timestamp + false + false + + + + + id + + + + + + + feed_user_id + + + + + post_date + + + + + post_user_id + + + + + site_network + + + +
+ + + + int8 + false + false + + + varchar(255) + false + false + + + varchar(255) + true + false + + + varchar(36) + true + false + + + timestamp + false + false + + + + + id + + + + + + + feed_user_id + + + +
+ + + + int8 + false + false + + + timestamp + false + false + + + varchar(10) + false + false + + + varchar(1024) + false + false + + + varchar(255) + false + false + + + int4 + false + false + + + varchar(255) + true + false + + + varchar(36) + true + false + + + varchar(255) + false + false + + + timestamp + false + false + + + + + sequence_id + + + + + + + job_task_node + + + + + status + + + +
+ + + + varchar(64) + false + false + + + varchar(1024) + true + false + + + int4 + true + false + + + int4 + true + false + + + int4 + true + false + + + int4 + true + false + + + timestamp + true + false + + + varchar(64) + true + false + + + bool + true + false + + + bool + true + false + + + varchar(1024) + true + false + + + + + id + + + + +
+ + + + int8 + false + false + + + int4 + false + false + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + + + id + + + + + audit_model_id + alf_audit_model + id + + + disabled_paths_id + alf_prop_root + id + + + app_name_id + alf_prop_value + id + + + + + + app_name_id + + + + + disabled_paths_id + + + + + audit_model_id + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + int8 + true + false + + + int8 + true + false + + + + + id + + + + + audit_app_id + alf_audit_app + id + + + audit_values_id + alf_prop_root + id + + + audit_user_id + alf_prop_value + id + + + + + + audit_app_id + + + + + audit_values_id + + + + + audit_user_id + + + + + audit_time + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + + + id + + + + + content_data_id + alf_content_data + id + + + + + + content_crc + + + + + content_data_id + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + varchar(100) + true + false + + + int8 + true + false + + + + + id + + + + + + + authority + crc + + + + + authority + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + + + id + + + + + alias_id + alf_authority + id + + + auth_id + alf_authority + id + + + + + + auth_id + alias_id + + + + + alias_id + + + + + auth_id + + + +
+ + + + int8 + false + false + + + varchar(100) + false + false + + + bool + false + false + + + bool + false + false + + + bytea + false + false + + + varchar(10) + false + false + + + + + id + + + + + + username + authorized + + + + + deleted + + + + + authaction + + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + varchar(50) + false + false + + + int8 + false + false + + + int8 + false + false + + + varchar(255) + false + false + + + int8 + false + false + + + bool + true + false + + + int4 + true + false + + + + + id + + + + + qname_ns_id + alf_namespace + id + + + parent_node_id + alf_node + id + + + child_node_id + alf_node + id + + + type_qname_id + alf_qname + id + + + + + + parent_node_id + type_qname_id + child_node_name_crc + child_node_name + + + + + child_node_id + + + + + parent_node_id + assoc_index + id + + + + + qname_ns_id + + + + + type_qname_id + + + + + parent_node_id + is_primary + child_node_id + + + + + qname_crc + type_qname_id + parent_node_id + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + int8 + true + false + + + int8 + true + false + + + int8 + true + false + + + int8 + true + false + + + + + id + + + + + content_url_id + alf_content_url + id + + + content_encoding_id + alf_encoding + id + + + content_locale_id + alf_locale + id + + + content_mimetype_id + alf_mimetype + id + + + + + + content_encoding_id + + + + + content_locale_id + + + + + content_mimetype_id + + + + + content_url_id + + + +
+ + + + int8 + false + false + + + varchar(255) + false + false + + + varchar(12) + false + false + + + int8 + false + false + + + int8 + false + false + + + int8 + true + false + + + + + id + + + + + + + content_url_short + content_url_crc + + + + + orphan_time + + + + + content_size + id + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + varchar(10) + false + false + + + int4 + false + false + + + bytea + false + false + + + varchar(20) + false + false + + + varchar(15) + false + false + + + int8 + true + false + + + + + id + + + + + content_url_id + alf_content_url + id + + + + + + content_url_id + + + + + master_key_alias + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + varchar(100) + false + false + + + + + id + + + + + + + encoding_str + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + varchar(20) + false + false + + + + + id + + + + + + + locale_str + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + varchar(36) + false + false + + + int8 + false + false + + + int8 + false + false + + + + + id + + + + + excl_resource_id + alf_lock_resource + id + + + shared_resource_id + alf_lock_resource + id + + + + + + shared_resource_id + excl_resource_id + + + + + excl_resource_id + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + varchar(255) + false + false + + + + + id + + + + + qname_ns_id + alf_namespace + id + + + + + + qname_ns_id + qname_localname + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + varchar(100) + false + false + + + + + id + + + + + + + mimetype_str + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + varchar(100) + false + false + + + + + id + + + + + + + uri + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + varchar(36) + false + false + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + int8 + true + false + + + varchar(255) + true + false + + + varchar(30) + true + false + + + varchar(255) + true + false + + + varchar(30) + true + false + + + varchar(30) + true + false + + + + + + + alf_node_pkey1? + + + + + id + + + + + acl_id + alf_access_control_list + id + + + locale_id + alf_locale + id + + + type_qname_id + alf_qname + id + + + store_id + alf_store + id + + + transaction_id + alf_transaction + id + + + + + + store_id + uuid + + + + + acl_id + + + + + locale_id + + + + + store_id + + + + + type_qname_id + store_id + id + + + + + transaction_id + type_qname_id + + + + + store_id + type_qname_id + id + + + + + audit_creator + store_id + type_qname_id + id + + + + + audit_created + store_id + type_qname_id + id + + + + + audit_modifier + store_id + type_qname_id + id + + + + + audit_modified + store_id + type_qname_id + id + + + + + version + + + + + transaction_id + + + + + acl_id + audit_creator + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + + + node_id + qname_id + + + + + node_id + alf_node + id + + + qname_id + alf_qname + id + + + + + + node_id + + + + + qname_id + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + + + + + alf_node_assoc_pkey1? + + + + + id + + + + + target_node_id + alf_node + id + + + source_node_id + alf_node + id + + + type_qname_id + alf_qname + id + + + + + + source_node_id + target_node_id + type_qname_id + + + + + source_node_id + type_qname_id + assoc_index + + + + + target_node_id + type_qname_id + + + + + type_qname_id + + + +
+ + + + int8 + false + false + + + int4 + false + false + + + int4 + false + false + + + bool + true + false + + + int8 + true + false + + + float4 + true + false + + + float8 + true + false + + + varchar(1024) + true + false + + + bytea + true + false + + + int8 + false + false + + + int4 + false + false + + + int8 + false + false + + + + + node_id + qname_id + list_index + locale_id + + + + + locale_id + alf_locale + id + + + node_id + alf_node + id + + + qname_id + alf_qname + id + + + + + + locale_id + + + + + node_id + + + + + qname_id + + + + + qname_id + string_value + node_id + + + + + qname_id + long_value + node_id + + + + + + qname_id + boolean_value + node_id + + + + + qname_id + double_value + node_id + + + + + qname_id + float_value + node_id + + +
+ + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + varchar(100) + false + false + + + + + id + + + + + type_qname_id + alf_qname + id + + + + + + type_qname_id + name + + + + + type_qname_id + + + +
+ + + + int8 + false + false + + + varchar(255) + false + false + + + varchar(32) + false + false + + + int8 + false + false + + + + + id + + + + + + + java_class_name_crc + java_class_name_short + + + + + java_class_name + + + +
+ + + + int8 + false + false + + + int4 + false + false + + + int2 + false + false + + + int2 + false + false + + + int2 + false + false + + + int2 + false + false + + + int2 + false + false + + + int4 + false + false + + + int2 + false + false + + + int2 + false + false + + + + + date_value + + + + + + + full_year + month_of_year + day_of_month + + + +
+ + + + int8 + false + false + + + float8 + false + false + + + + + id + + + + + + + double_value + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + + + root_prop_id + contained_in + prop_index + + + + + root_prop_id + alf_prop_root + id + + + value_prop_id + alf_prop_value + id + + + key_prop_id + alf_prop_value + id + + + + + + key_prop_id + + + + + value_prop_id + + + + + root_prop_id + key_prop_id + value_prop_id + + + +
+ + + + int8 + false + false + + + int4 + false + false + + + + + id + + + + +
+ + + + int8 + false + false + + + bytea + false + false + + + + + id + + + + +
+ + + + int8 + false + false + + + varchar(1024) + false + false + + + varchar(16) + false + false + + + int8 + false + false + + + + + id + + + + + + + string_end_lower + string_crc + + + + + string_value + + + +
+ + + + int8 + false + false + + + int4 + false + false + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + int8 + true + false + + + + + id + + + + + prop1_id + alf_prop_root + id + + + value3_prop_id + alf_prop_value + id + + + value2_prop_id + alf_prop_value + id + + + value1_prop_id + alf_prop_value + id + + + + + + value1_prop_id + value2_prop_id + value3_prop_id + + + + + prop1_id + + + + + value2_prop_id + + + + + value3_prop_id + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + int2 + false + false + + + int8 + false + false + + + + + id + + + + + + + actual_type_id + long_value + + + + + persisted_type + long_value + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + varchar(200) + false + false + + + + + id + + + + + ns_id + alf_namespace + id + + + + + + ns_id + local_name + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + varchar(39) + false + false + + + + + id + + + + + + + ip_address + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + varchar(50) + false + false + + + varchar(100) + false + false + + + int8 + true + false + + + + + id + + + + + root_node_id + alf_node + id + + + + + + protocol + identifier + + + + + root_node_id + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + + + user_node_id + node_id + + + + + node_id + alf_node + id + + + user_node_id + alf_node + id + + + + + + node_id + + + +
+ + + + varchar(75) + false + false + + + int8 + false + false + + + bool + false + false + + + varchar(75) + true + false + + + varchar(255) + true + false + + + varchar(255) + true + false + + + + + tenant_domain + + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + int8 + true + false + + + varchar(56) + false + false + + + int8 + true + false + + + + + id + + + + + server_id + alf_server + id + + + + + + server_id + + + + + commit_time_ms + id + + + + + commit_time_ms + + + + + id + commit_time_ms + + + +
+ + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + int8 + false + false + + + + + id + + + + + node_id + alf_node + id + + + + + + node_id + + + +
+
+
diff --git a/repository/src/test/resources/alfresco/dbscripts/test-bootstrap-context.xml b/repository/src/test/resources/alfresco/dbscripts/test-bootstrap-context.xml new file mode 100644 index 0000000000..e8bdc861c6 --- /dev/null +++ b/repository/src/test/resources/alfresco/dbscripts/test-bootstrap-context.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + patch.db-V0-add-index-test + patch.db-V0-add-index-test.description + 0 + 15000 + 15001 + ${system.new-node-transaction-indexes.ignored} + + classpath:alfresco/dbscripts/upgrade/0/${db.script.dialect}/add-index-test.sql + + + classpath:alfresco/dbscripts/upgrade/0/${db.script.dialect}/add-index-test-problem-patterns.txt + + + + \ No newline at end of file diff --git a/repository/src/test/resources/alfresco/dbscripts/upgrade/0/org.alfresco.repo.domain.dialect.PostgreSQLDialect/add-index-test-problem-patterns.txt b/repository/src/test/resources/alfresco/dbscripts/upgrade/0/org.alfresco.repo.domain.dialect.PostgreSQLDialect/add-index-test-problem-patterns.txt new file mode 100644 index 0000000000..9fcd0f3e8c --- /dev/null +++ b/repository/src/test/resources/alfresco/dbscripts/upgrade/0/org.alfresco.repo.domain.dialect.PostgreSQLDialect/add-index-test-problem-patterns.txt @@ -0,0 +1 @@ +.*missing index.*.alf_node.idx_alf_node_test \ No newline at end of file diff --git a/repository/src/test/resources/alfresco/dbscripts/upgrade/0/org.alfresco.repo.domain.dialect.PostgreSQLDialect/add-index-test.sql b/repository/src/test/resources/alfresco/dbscripts/upgrade/0/org.alfresco.repo.domain.dialect.PostgreSQLDialect/add-index-test.sql new file mode 100644 index 0000000000..0370e43385 --- /dev/null +++ b/repository/src/test/resources/alfresco/dbscripts/upgrade/0/org.alfresco.repo.domain.dialect.PostgreSQLDialect/add-index-test.sql @@ -0,0 +1,15 @@ + +CREATE INDEX idx_alf_node_test ON alf_node (acl_id, audit_creator); --(optional) + + +-- +-- Record script finish +-- +DELETE FROM alf_applied_patch WHERE id = 'patch.db-V0-add-index-test'; +INSERT INTO alf_applied_patch + (id, description, fixes_from_schema, fixes_to_schema, applied_to_schema, target_schema, applied_on_date, applied_to_server, was_executed, succeeded, report) + VALUES + ( + 'patch.db-V0-add-index-test', 'Manually executed script upgrade V0: Added new index test', + 0, 15000, -1, 15001, null, 'UNKNOWN', ${TRUE}, ${TRUE}, 'Script completed' + ); \ No newline at end of file