diff options
author | Jacek <jacek.poreda@sonarsource.com> | 2021-08-12 16:55:04 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-09-08 20:03:34 +0000 |
commit | cfe5faf0b14841bdad0cae24ac89832c98d1d32a (patch) | |
tree | b089d578bc9b598f46c79e0c22d254fafafcc1fe /server/sonar-db-migration | |
parent | 6ad72a24b224d7949ad790b70ef4ae8352ba1899 (diff) | |
download | sonarqube-cfe5faf0b14841bdad0cae24ac89832c98d1d32a.tar.gz sonarqube-cfe5faf0b14841bdad0cae24ac89832c98d1d32a.zip |
SONAR-15259 Migrate portfolios from XML to DB
Diffstat (limited to 'server/sonar-db-migration')
6 files changed, 1125 insertions, 2 deletions
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/CreatePortfoliosTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/CreatePortfoliosTable.java index a25d3b1adcf..26d9035fcbc 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/CreatePortfoliosTable.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/CreatePortfoliosTable.java @@ -40,7 +40,7 @@ public class CreatePortfoliosTable extends CreateTableChange { public void execute(Context context, String tableName) throws SQLException { context.execute(new CreateTableBuilder(getDialect(), tableName) .addPkColumn(newVarcharColumnDefBuilder().setColumnName("uuid").setIsNullable(false).setLimit(UUID_SIZE).build()) - .addColumn(newVarcharColumnDefBuilder().setColumnName("kee").setIsNullable(false).setLimit(UUID_SIZE).build()) + .addColumn(newVarcharColumnDefBuilder().setColumnName("kee").setIsNullable(false).setLimit(400).build()) .addColumn(newVarcharColumnDefBuilder().setColumnName("name").setIsNullable(false).setLimit(2000).build()) .addColumn(newVarcharColumnDefBuilder().setColumnName("description").setIsNullable(true).setLimit(2000).build()) .addColumn(newVarcharColumnDefBuilder().setColumnName("root_uuid").setIsNullable(false).setLimit(UUID_SIZE).build()) diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/DbVersion91.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/DbVersion91.java index 45dc9309a74..6a000293a25 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/DbVersion91.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/DbVersion91.java @@ -39,6 +39,7 @@ public class DbVersion91 implements DbVersion { .add(6011, "Create 'portfolios' table", CreatePortfoliosTable.class) .add(6012, "Create 'portfolio_references' table", CreatePortfolioReferencesTable.class) .add(6013, "Create 'portfolio_projects' table", CreatePortfolioProjectsTable.class) + .add(6014, "Migrate portfolios to new tables", MigratePortfoliosToNewTables.class) ; } } diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTables.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTables.java new file mode 100644 index 00000000000..377f7261c28 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTables.java @@ -0,0 +1,581 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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 this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v91; + +import com.google.common.collect.Sets; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import javax.xml.XMLConstants; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.transform.sax.SAXSource; +import javax.xml.validation.SchemaFactory; +import org.apache.commons.lang.StringUtils; +import org.codehaus.staxmate.SMInputFactory; +import org.codehaus.staxmate.in.SMHierarchicCursor; +import org.codehaus.staxmate.in.SMInputCursor; +import org.sonar.api.utils.System2; +import org.sonar.core.util.UuidFactory; +import org.sonar.db.Database; +import org.sonar.db.DatabaseUtils; +import org.sonar.server.platform.db.migration.step.DataChange; +import org.sonar.server.platform.db.migration.step.MassUpdate; +import org.sonar.server.platform.db.migration.step.Select; +import org.sonar.server.platform.db.migration.step.Upsert; +import org.sonar.server.platform.db.migration.version.v91.MigratePortfoliosToNewTables.ViewXml.ViewDef; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Optional.ofNullable; +import static javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING; +import static org.apache.commons.io.IOUtils.toInputStream; +import static org.apache.commons.lang.StringUtils.trim; + +public class MigratePortfoliosToNewTables extends DataChange { + private static final String SELECT_DEFAULT_VISIBILITY = "select text_value from properties where prop_key = 'projects.default.visibility'"; + private static final String SELECT_UUID_VISIBILITY_BY_COMPONENT_KEY = "select c.uuid, c.private from components c where c.kee = ?"; + private static final String SELECT_PORTFOLIO_UUID_AND_SELECTION_MODE_BY_KEY = "select uuid,selection_mode from portfolios where kee = ?"; + private static final String SELECT_PROJECT_KEYS_BY_PORTFOLIO_UUID = "select p.kee from portfolio_projects pp " + + "join projects p on pp.project_uuid = p.uuid where pp.portfolio_uuid = ?"; + private static final String SELECT_PROJECT_UUIDS_BY_KEYS = "select p.uuid,p.kee from projects p where p.kee in (PLACEHOLDER)"; + private static final String VIEWS_DEF_KEY = "views.def"; + private static final String PLACEHOLDER = "PLACEHOLDER"; + + private final UuidFactory uuidFactory; + private final System2 system; + + private boolean defaultPrivateFlag; + + public enum SelectionMode { + NONE, MANUAL, REGEXP, REST, TAGS + } + + public MigratePortfoliosToNewTables(Database db, UuidFactory uuidFactory, System2 system) { + super(db); + + this.uuidFactory = uuidFactory; + this.system = system; + } + + @Override + protected void execute(Context context) throws SQLException { + String xml = getViewsDefinition(context); + // skip migration if `views.def` does not exist in the db + if (xml == null) { + return; + } + + this.defaultPrivateFlag = ofNullable(context.prepareSelect(SELECT_DEFAULT_VISIBILITY) + .get(row -> "private".equals(row.getString(1)))) + .orElse(false); + + try { + Map<String, ViewXml.ViewDef> portfolioXmlMap = ViewXml.parse(xml); + List<ViewXml.ViewDef> portfolios = new LinkedList<>(portfolioXmlMap.values()); + + Map<String, PortfolioDb> portfolioDbMap = new HashMap<>(); + for (ViewXml.ViewDef portfolio : portfolios) { + PortfolioDb createdPortfolio = insertPortfolio(context, portfolio); + if (createdPortfolio.selectionMode == SelectionMode.MANUAL) { + insertPortfolioProjects(context, portfolio, createdPortfolio); + } + portfolioDbMap.put(createdPortfolio.kee, createdPortfolio); + } + // all portfolio has been created and new uuids assigned + // update portfolio hierarchy parent/root + insertReferences(context, portfolioXmlMap, portfolioDbMap); + updateHierarchy(context, portfolioXmlMap, portfolioDbMap); + } catch (Exception e) { + throw new IllegalStateException("Failed to migrate views definitions property.", e); + } + } + + private PortfolioDb insertPortfolio(Context context, ViewXml.ViewDef portfolioFromXml) throws SQLException { + long now = system.now(); + PortfolioDb portfolioDb = context.prepareSelect(SELECT_PORTFOLIO_UUID_AND_SELECTION_MODE_BY_KEY) + .setString(1, portfolioFromXml.key) + .get(r -> new PortfolioDb(r.getString(1), portfolioFromXml.key, SelectionMode.valueOf(r.getString(2)))); + + Optional<ComponentDb> componentDbOpt = ofNullable(context.prepareSelect(SELECT_UUID_VISIBILITY_BY_COMPONENT_KEY) + .setString(1, portfolioFromXml.key) + .get(row -> new ComponentDb(row.getString(1), row.getBoolean(2)))); + + // no portfolio -> insert + if (portfolioDb == null) { + Upsert insertPortfolioQuery = context.prepareUpsert("insert into " + + "portfolios(uuid, kee, private, name, description, root_uuid, parent_uuid, selection_mode, selection_expression, updated_at, created_at) " + + "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + + String portfolioUuid = componentDbOpt.map(c -> c.uuid).orElse(uuidFactory.create()); + insertPortfolioQuery.setString(1, portfolioUuid) + .setString(2, portfolioFromXml.key) + .setBoolean(3, componentDbOpt.map(c -> c.visibility).orElse(this.defaultPrivateFlag)) + .setString(4, portfolioFromXml.name) + .setString(5, portfolioFromXml.desc) + .setString(6, PLACEHOLDER) + .setString(7, PLACEHOLDER); + SelectionMode selectionMode = SelectionMode.NONE; + String selectionExpression = null; + if (portfolioFromXml.getProjects() != null && !portfolioFromXml.getProjects().isEmpty()) { + selectionMode = SelectionMode.MANUAL; + } else if (portfolioFromXml.regexp != null && !portfolioFromXml.regexp.isBlank()) { + selectionMode = SelectionMode.REGEXP; + selectionExpression = portfolioFromXml.regexp; + } else if (portfolioFromXml.def) { + selectionMode = SelectionMode.REST; + } else if (portfolioFromXml.tagsAssociation != null && !portfolioFromXml.tagsAssociation.isEmpty()) { + selectionMode = SelectionMode.TAGS; + selectionExpression = String.join(",", portfolioFromXml.tagsAssociation); + } + + insertPortfolioQuery.setString(8, selectionMode.name()) + .setString(9, selectionExpression) + // set dates + .setLong(10, now) + .setLong(11, now) + .execute() + .commit(); + return new PortfolioDb(portfolioUuid, portfolioFromXml.key, selectionMode); + } + return portfolioDb; + } + + private void insertPortfolioProjects(Context context, ViewDef portfolio, PortfolioDb createdPortfolio) throws SQLException { + long now = system.now(); + // select all already added project uuids + Set<String> alreadyAddedPortfolioProjects = new HashSet<>( + context.prepareSelect(SELECT_PROJECT_KEYS_BY_PORTFOLIO_UUID) + .setString(1, createdPortfolio.uuid) + .list(r -> r.getString(1))); + + Set<String> projectKeysFromXml = new HashSet<>(portfolio.getProjects()); + Set<String> projectKeysToBeAdded = Sets.difference(projectKeysFromXml, alreadyAddedPortfolioProjects); + + if (!projectKeysToBeAdded.isEmpty()) { + List<ProjectDb> projects = findPortfolioProjects(context, projectKeysToBeAdded); + + var upsert = context.prepareUpsert("insert into " + + "portfolio_projects(uuid, portfolio_uuid, project_uuid, created_at) " + + "values (?, ?, ?, ?)"); + for (ProjectDb projectDb : projects) { + upsert.setString(1, uuidFactory.create()) + .setString(2, createdPortfolio.uuid) + .setString(3, projectDb.uuid) + .setLong(4, now) + .addBatch(); + } + if (!projects.isEmpty()) { + upsert.execute() + .commit(); + } + } + } + + private static List<ProjectDb> findPortfolioProjects(Context context, Set<String> projectKeysToBeAdded) { + return DatabaseUtils.executeLargeInputs(projectKeysToBeAdded, keys -> { + try { + String selectQuery = SELECT_PROJECT_UUIDS_BY_KEYS.replace(PLACEHOLDER, + keys.stream().map(key -> "'" + key + "'").collect( + Collectors.joining(","))); + return context.prepareSelect(selectQuery) + .list(r -> new ProjectDb(r.getString(1), r.getString(2))); + } catch (SQLException e) { + throw new IllegalStateException("Could not execute 'in' query", e); + } + }); + } + + private void insertReferences(Context context, Map<String, ViewDef> portfolioXmlMap, + Map<String, PortfolioDb> portfolioDbMap) throws SQLException { + Upsert insertQuery = context.prepareUpsert("insert into portfolio_references(uuid, portfolio_uuid, reference_uuid, created_at) values (?, ?, ?, ?)"); + + long now = system.now(); + boolean shouldExecuteQuery = false; + for (ViewDef portfolio : portfolioXmlMap.values()) { + var currentPortfolioUuid = portfolioDbMap.get(portfolio.key).uuid; + Set<String> referencesFromXml = new HashSet<>(portfolio.getReferences()); + Set<String> referencesFromDb = new HashSet<>(context.prepareSelect("select pr.reference_uuid from portfolio_references pr where pr.portfolio_uuid = ?") + .setString(1, currentPortfolioUuid) + .list(row -> row.getString(1))); + + for (String appOrPortfolio : referencesFromXml) { + // if portfolio and hasn't been added already + if (portfolioDbMap.containsKey(appOrPortfolio) && !referencesFromDb.contains(portfolioDbMap.get(appOrPortfolio).uuid)) { + insertQuery + .setString(1, uuidFactory.create()) + .setString(2, currentPortfolioUuid) + .setString(3, portfolioDbMap.get(appOrPortfolio).uuid) + .setLong(4, now) + .addBatch(); + shouldExecuteQuery = true; + } else { + // if application exist and haven't been added + String appUuid = context.prepareSelect("select p.uuid from projects p where p.kee = ?") + .setString(1, appOrPortfolio) + .get(row -> row.getString(1)); + if (appUuid != null && !referencesFromDb.contains(appUuid)) { + insertQuery + .setString(1, uuidFactory.create()) + .setString(2, currentPortfolioUuid) + .setString(3, appUuid) + .setLong(4, now) + .addBatch(); + shouldExecuteQuery = true; + } + } + } + } + if (shouldExecuteQuery) { + insertQuery + .execute() + .commit(); + } + + } + + private static void updateHierarchy(Context context, Map<String, ViewXml.ViewDef> defs, Map<String, PortfolioDb> portfolioDbMap) throws SQLException { + MassUpdate massUpdate = context.prepareMassUpdate(); + massUpdate.select("select uuid, kee from portfolios where root_uuid = ? or parent_uuid = ?") + .setString(1, PLACEHOLDER) + .setString(2, PLACEHOLDER); + massUpdate.update("update portfolios set root_uuid = ?, parent_uuid = ? where uuid = ?"); + massUpdate.execute((row, update) -> { + String currentPortfolioUuid = row.getString(1); + String currentPortfolioKey = row.getString(2); + + var currentPortfolio = defs.get(currentPortfolioKey); + String parentUuid = ofNullable(currentPortfolio.parent).map(parent -> portfolioDbMap.get(parent).uuid).orElse(null); + String rootUuid = ofNullable(currentPortfolio.root).map(root -> portfolioDbMap.get(root).uuid).orElse(currentPortfolioUuid); + update.setString(1, rootUuid) + .setString(2, parentUuid) + .setString(3, currentPortfolioUuid); + return true; + }); + } + + @CheckForNull + private static String getViewsDefinition(DataChange.Context context) throws SQLException { + Select select = context.prepareSelect("select text_value,clob_value from internal_properties where kee=?"); + select.setString(1, VIEWS_DEF_KEY); + return select.get(row -> { + String v = row.getString(1); + if (v != null) { + return v; + } else { + return row.getString(2); + } + }); + } + + private static class ComponentDb { + String uuid; + boolean visibility; + + public ComponentDb(String uuid, boolean visibility) { + this.uuid = uuid; + this.visibility = visibility; + } + } + + private static class PortfolioDb { + String uuid; + String kee; + SelectionMode selectionMode; + + PortfolioDb(String uuid, String kee, + SelectionMode selectionMode) { + this.uuid = uuid; + this.kee = kee; + this.selectionMode = selectionMode; + } + } + + private static class ProjectDb { + String uuid; + String kee; + + ProjectDb(String uuid, String kee) { + this.uuid = uuid; + this.kee = kee; + } + } + + static class ViewXml { + static final String SCHEMA_VIEWS = "/static/views.xsd"; + static final String VIEWS_HEADER_BARE = "<views>"; + static final Pattern VIEWS_HEADER_BARE_PATTERN = Pattern.compile(VIEWS_HEADER_BARE); + static final String VIEWS_HEADER_FQ = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + + "<views xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://sonarsource.com/schema/views\">"; + + private ViewXml() { + // nothing to do here + } + + private static Map<String, ViewDef> parse(String xml) throws ParserConfigurationException, SAXException, IOException, XMLStreamException { + if (StringUtils.isEmpty(xml)) { + return new LinkedHashMap<>(0); + } + + List<ViewDef> views; + validate(xml); + SMInputFactory inputFactory = initStax(); + SMHierarchicCursor rootC = inputFactory.rootElementCursor(new StringReader(xml)); + rootC.advance(); // <views> + SMInputCursor cursor = rootC.childElementCursor(); + views = parseViewDefinitions(cursor); + + Map<String, ViewDef> result = new LinkedHashMap<>(views.size()); + for (ViewDef def : views) { + result.put(def.key, def); + } + + return result; + } + + private static void validate(String xml) throws IOException, SAXException, ParserConfigurationException { + // Replace bare, namespace unaware header with fully qualified header (with schema declaration) + String fullyQualifiedXml = VIEWS_HEADER_BARE_PATTERN.matcher(xml).replaceFirst(VIEWS_HEADER_FQ); + try (InputStream xsd = MigratePortfoliosToNewTables.class.getResourceAsStream(SCHEMA_VIEWS)) { + InputSource viewsDefinition = new InputSource(new InputStreamReader(toInputStream(fullyQualifiedXml, UTF_8), UTF_8)); + + SchemaFactory saxSchemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + saxSchemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + saxSchemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + + SAXParserFactory parserFactory = SAXParserFactory.newInstance(); + parserFactory.setFeature(FEATURE_SECURE_PROCESSING, true); + parserFactory.setNamespaceAware(true); + parserFactory.setSchema(saxSchemaFactory.newSchema(new SAXSource(new InputSource(xsd)))); + + SAXParser saxParser = parserFactory.newSAXParser(); + saxParser.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + saxParser.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + saxParser.parse(viewsDefinition, new ViewsValidator()); + } + } + + private static List<ViewDef> parseViewDefinitions(SMInputCursor viewsCursor) throws XMLStreamException { + List<ViewDef> views = new ArrayList<>(); + while (viewsCursor.getNext() != null) { + ViewDef viewDef = new ViewDef(); + viewDef.setKey(viewsCursor.getAttrValue("key")); + viewDef.setDef(Boolean.parseBoolean(viewsCursor.getAttrValue("def"))); + viewDef.setParent(viewsCursor.getAttrValue("parent")); + viewDef.setRoot(viewsCursor.getAttrValue("root")); + SMInputCursor viewCursor = viewsCursor.childElementCursor(); + while (viewCursor.getNext() != null) { + String nodeName = viewCursor.getLocalName(); + parseChildElement(viewDef, viewCursor, nodeName); + } + views.add(viewDef); + } + return views; + } + + private static void parseChildElement(ViewDef viewDef, SMInputCursor viewCursor, String nodeName) throws XMLStreamException { + if (StringUtils.equals(nodeName, "name")) { + viewDef.setName(trim(viewCursor.collectDescendantText())); + } else if (StringUtils.equals(nodeName, "desc")) { + viewDef.setDesc(trim(viewCursor.collectDescendantText())); + } else if (StringUtils.equals(nodeName, "regexp")) { + viewDef.setRegexp(trim(viewCursor.collectDescendantText())); + } else if (StringUtils.equals(nodeName, "language")) { + viewDef.setLanguage(trim(viewCursor.collectDescendantText())); + } else if (StringUtils.equals(nodeName, "tag_key")) { + viewDef.setTagKey(trim(viewCursor.collectDescendantText())); + } else if (StringUtils.equals(nodeName, "tag_value")) { + viewDef.setTagValue(trim(viewCursor.collectDescendantText())); + } else if (StringUtils.equals(nodeName, "p")) { + viewDef.addProject(trim(viewCursor.collectDescendantText())); + } else if (StringUtils.equals(nodeName, "vw-ref")) { + viewDef.addReference(trim(viewCursor.collectDescendantText())); + } else if (StringUtils.equals(nodeName, "qualifier")) { + viewDef.setQualifier(trim(viewCursor.collectDescendantText())); + } else if (StringUtils.equals(nodeName, "tagsAssociation")) { + parseTagsAssociation(viewDef, viewCursor); + } + } + + private static void parseTagsAssociation(ViewDef def, SMInputCursor viewCursor) throws XMLStreamException { + SMInputCursor projectCursor = viewCursor.childElementCursor(); + while (projectCursor.getNext() != null) { + def.addTagAssociation(trim(projectCursor.collectDescendantText())); + } + } + + private static SMInputFactory initStax() { + XMLInputFactory xmlFactory = XMLInputFactory.newInstance(); + xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE); + xmlFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE); + // just so it won't try to load DTD in if there's DOCTYPE + xmlFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE); + xmlFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE); + return new SMInputFactory(xmlFactory); + } + + static class ViewDef { + String key = null; + + String parent = null; + + String root = null; + + boolean def = false; + + List<String> p = new ArrayList<>(); + + List<String> vwRef = new ArrayList<>(); + + String name = null; + + String desc = null; + + String regexp = null; + + String language = null; + + String tagKey = null; + + String tagValue = null; + + String qualifier = null; + + Set<String> tagsAssociation = new TreeSet<>(); + + public List<String> getProjects() { + return p; + } + + public List<String> getReferences() { + return vwRef; + } + + public ViewDef setKey(String key) { + this.key = key; + return this; + } + + public ViewDef setParent(String parent) { + this.parent = parent; + return this; + } + + public ViewDef setRoot(@Nullable String root) { + this.root = root; + return this; + } + + public ViewDef setDef(boolean def) { + this.def = def; + return this; + } + + public ViewDef addProject(String project) { + this.p.add(project); + return this; + } + + public ViewDef setName(String name) { + this.name = name; + return this; + } + + public ViewDef setDesc(@Nullable String desc) { + this.desc = desc; + return this; + } + + public ViewDef setRegexp(@Nullable String regexp) { + this.regexp = regexp; + return this; + } + + public ViewDef setLanguage(@Nullable String language) { + this.language = language; + return this; + } + + public ViewDef setTagKey(@Nullable String tagKey) { + this.tagKey = tagKey; + return this; + } + + public ViewDef setTagValue(@Nullable String tagValue) { + this.tagValue = tagValue; + return this; + } + + public ViewDef addReference(String reference) { + this.vwRef.add(reference); + return this; + } + + public ViewDef setQualifier(@Nullable String qualifier) { + this.qualifier = qualifier; + return this; + } + + public ViewDef addTagAssociation(String tag) { + this.tagsAssociation.add(tag); + return this; + } + } + + private static final class ViewsValidator extends DefaultHandler { + @Override + public void error(SAXParseException exception) throws SAXException { + throw exception; + } + + @Override + public void warning(SAXParseException exception) throws SAXException { + throw exception; + } + + @Override + public void fatalError(SAXParseException exception) throws SAXException { + throw exception; + } + } + } +} diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v91/DbVersion91Test.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v91/DbVersion91Test.java index e5b51a680b6..0a361304f1b 100644 --- a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v91/DbVersion91Test.java +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v91/DbVersion91Test.java @@ -41,7 +41,7 @@ public class DbVersion91Test { @Test public void verify_migration_count() { - verifyMigrationCount(underTest, 13); + verifyMigrationCount(underTest, 14); } } diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTablesTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTablesTest.java new file mode 100644 index 00000000000..e8c8e48d9be --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTablesTest.java @@ -0,0 +1,431 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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 this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v91; + +import java.sql.SQLException; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.impl.utils.TestSystem2; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.utils.System2; +import org.sonar.core.util.UuidFactory; +import org.sonar.core.util.UuidFactoryFast; +import org.sonar.db.CoreDbTester; +import org.sonar.server.platform.db.migration.step.DataChange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.tuple; + +public class MigratePortfoliosToNewTablesTest { + static final int TEXT_VALUE_MAX_LENGTH = 4000; + private static final long NOW = 10_000_000L; + + @Rule + public final CoreDbTester db = CoreDbTester.createForSchema(MigratePortfoliosToNewTablesTest.class, "schema.sql"); + + private final UuidFactory uuidFactory = UuidFactoryFast.getInstance(); + private final System2 system2 = new TestSystem2().setNow(NOW); + + private final DataChange underTest = new MigratePortfoliosToNewTables(db.database(), uuidFactory, system2); + + private final String SIMPLE_XML_ONE_PORTFOLIO = "<views>" + + "<vw key=\"port1\" def=\"false\">\n" + + " <name><![CDATA[name]]></name>\n" + + " <desc><![CDATA[description]]></desc>\n" + + " <qualifier><![CDATA[VW]]></qualifier>\n" + + " </vw>\n" + + "<vw key=\"port2\" def=\"false\">\n" + + " <name><![CDATA[name2]]></name>\n" + + " <desc><![CDATA[description2]]></desc>\n" + + " <qualifier><![CDATA[VW]]></qualifier>\n" + + " </vw>\n" + + "</views>"; + + private final String SIMPLE_XML_ONLY_PORTFOLIOS = "<views>" + + "<vw key=\"port1\" def=\"true\">\n" + + " <name><![CDATA[port1]]></name>\n" + + " <desc><![CDATA[]]></desc>\n" + + " <qualifier><![CDATA[VW]]></qualifier>\n" + + " </vw>\n" + + "<vw key=\"port3\" def=\"false\">\n" + + " <name><![CDATA[port3]]></name>\n" + + " <desc><![CDATA[]]></desc>\n" + + " <qualifier><![CDATA[VW]]></qualifier>\n" + + " </vw>\n" + + "<vw key=\"port2\" def=\"false\">\n" + + " <name><![CDATA[port2]]></name>\n" + + " <desc><![CDATA[]]></desc>\n" + + " <qualifier><![CDATA[VW]]></qualifier>\n" + + " <vw-ref><![CDATA[port1]]></vw-ref>\n" + + " <vw-ref><![CDATA[port3]]></vw-ref>\n" + + " </vw>\n" + + "</views>"; + + private final String SIMPLE_XML_PORTFOLIOS_AND_APP = "<views>" + + "<vw key=\"port1\" def=\"true\">\n" + + " <name><![CDATA[port1]]></name>\n" + + " <desc><![CDATA[]]></desc>\n" + + " <qualifier><![CDATA[VW]]></qualifier>\n" + + " </vw>\n" + + "<vw key=\"port3\" def=\"false\">\n" + + " <name><![CDATA[port3]]></name>\n" + + " <desc><![CDATA[]]></desc>\n" + + " <qualifier><![CDATA[VW]]></qualifier>\n" + + " </vw>\n" + + "<vw key=\"port2\" def=\"false\">\n" + + " <name><![CDATA[port2]]></name>\n" + + " <desc><![CDATA[]]></desc>\n" + + " <qualifier><![CDATA[VW]]></qualifier>\n" + + " <vw-ref><![CDATA[port1]]></vw-ref>\n" + + " <vw-ref><![CDATA[port3]]></vw-ref>\n" + + " </vw>\n" + + "<vw key=\"port4\" def=\"false\">\n" + + " <name><![CDATA[port2]]></name>\n" + + " <desc><![CDATA[]]></desc>\n" + + " <qualifier><![CDATA[VW]]></qualifier>\n" + + " <vw-ref><![CDATA[port2]]></vw-ref>\n" + + " <vw-ref><![CDATA[app1-key]]></vw-ref>\n" + + " <vw-ref><![CDATA[app2-key]]></vw-ref>\n" + + " <vw-ref><![CDATA[app3-key-not-existing]]></vw-ref>\n" + + " </vw>\n" + + "</views>"; + + private final String SIMPLE_XML_PORTFOLIOS_HIERARCHY = "<views>" + + " <vw key=\"port1\" def=\"false\">\n" + + " <name><![CDATA[port1]]></name>\n" + + " <desc><![CDATA[port1]]></desc>\n" + + " <qualifier><![CDATA[VW]]></qualifier>\n" + + " </vw>\n" + + " <vw key=\"port2\" def=\"false\" root=\"port1\" parent=\"port1\">\n" + + " <name><![CDATA[port2]]></name>\n" + + " <desc><![CDATA[port2]]></desc>\n" + + " </vw>\n" + + " <vw key=\"port3\" def=\"false\" root=\"port1\" parent=\"port1\">\n" + + " <name><![CDATA[port3]]></name>\n" + + " <desc><![CDATA[port3]]></desc>\n" + + " </vw>\n" + + " <vw key=\"port4\" def=\"false\" root=\"port1\" parent=\"port1\">\n" + + " <name><![CDATA[port4]]></name>\n" + + " <desc><![CDATA[port4]]></desc>\n" + + " </vw>\n" + + " <vw key=\"port5\" def=\"false\" root=\"port1\" parent=\"port2\">\n" + + " <name><![CDATA[port5]]></name>\n" + + " </vw>\n" + + " <vw key=\"port6\" def=\"false\" root=\"port1\" parent=\"port3\">\n" + + " <name><![CDATA[port6]]></name>\n" + + " <desc><![CDATA[port6]]></desc>\n" + + " </vw>\n" + + " <vw key=\"port7\" def=\"false\" root=\"port1\" parent=\"port3\">\n" + + " <name><![CDATA[port7]]></name>\n" + + " <desc><![CDATA[port7]]></desc>\n" + + " </vw>" + + "</views>"; + + private final String SIMPLE_XML_PORTFOLIOS_DIFFERENT_SELECTIONS = "<views>" + + " <vw key=\"port-regexp\" def=\"false\">\n" + + " <name><![CDATA[port-regexp]]></name>\n" + + " <desc><![CDATA[port-regexp]]></desc>\n" + + " <regexp><![CDATA[.*port.*]]></regexp>\n" + + " <qualifier><![CDATA[VW]]></qualifier>\n" + + " </vw>" + + " <vw key=\"port-tags\" def=\"false\">\n" + + " <name><![CDATA[port-tags]]></name>\n" + + " <desc><![CDATA[port-tags]]></desc>\n" + + " <qualifier><![CDATA[VW]]></qualifier>\n" + + " <tagsAssociation>\n" + + " <tag>tag1</tag>\n" + + " <tag>tag2</tag>\n" + + " <tag>tag3</tag>\n" + + " </tagsAssociation>\n" + + " </vw>" + + " <vw key=\"port-projects\" def=\"false\">\n" + + " <name><![CDATA[port-projects]]></name>\n" + + " <desc><![CDATA[port-projects]]></desc>\n" + + " <qualifier><![CDATA[VW]]></qualifier>\n" + + " <p>p1-key</p>\n" + + " <p>p2-key</p>\n" + + " <p>p3-key</p>\n" + + " <p>p4-key-not-existing</p>\n" + + " </vw>" + + "</views>"; + + private final String XML_PORTFOLIOS_LEGACY_SELECTIONS = "<views>" + + " <vw key=\"port-language\" def=\"false\">\n" + + " <name><![CDATA[port-language]]></name>\n" + + " <desc><![CDATA[port-language]]></desc>\n" + + " <language><![CDATA[javascript]]></language>\n" + + " <qualifier><![CDATA[VW]]></qualifier>\n" + + " </vw>" + + " <vw key=\"port-tag-value\" def=\"false\">\n" + + " <name><![CDATA[port-tag-value]]></name>\n" + + " <desc><![CDATA[port-tag-value]]></desc>\n" + + " <qualifier><![CDATA[VW]]></qualifier>\n" + + " <tag_key>tag-key</tag_key>\n" + + " <tag_value>tag-value</tag_value>\n" + + " </vw>" + + "</views>"; + + @Test + public void does_not_fail_when_nothing_to_migrate() throws SQLException { + assertThatCode(underTest::execute) + .doesNotThrowAnyException(); + } + + @Test + public void migrate_single_portfolio_with_default_visibility() throws SQLException { + insertViewsDefInternalProperty(SIMPLE_XML_ONE_PORTFOLIO); + insertDefaultVisibilityProperty(true); + insertComponent("uuid", "port2", false); + + underTest.execute(); + // re-entrant + underTest.execute(); + + assertThat(db.select("select kee, private, name, description, " + + "selection_mode, selection_expression, " + + "updated_at, created_at from portfolios")) + .extracting( + row -> row.get("KEE"), + row -> row.get("PRIVATE"), + row -> row.get("NAME"), + row -> row.get("DESCRIPTION"), + row -> row.get("SELECTION_MODE"), + row -> row.get("SELECTION_EXPRESSION"), + row -> row.get("UPDATED_AT"), + row -> row.get("CREATED_AT")) + .containsExactlyInAnyOrder( + tuple("port1", true, "name", "description", "NONE", null, NOW, NOW), + tuple("port2", false, "name2", "description2", "NONE", null, NOW, NOW)); + } + + @Test + public void migrate_portfolios_should_assign_component_uuid() throws SQLException { + insertViewsDefInternalProperty(SIMPLE_XML_ONE_PORTFOLIO); + insertComponent("uuid1", "port1", true); + insertComponent("uuid2", "port2", false); + + underTest.execute(); + // re-entrant + underTest.execute(); + + assertThat(db.select("select uuid, kee, private, name, description, " + + "selection_mode, selection_expression, " + + "updated_at, created_at from portfolios")) + .extracting( + row -> row.get("UUID"), + row -> row.get("KEE"), + row -> row.get("PRIVATE"), + row -> row.get("NAME"), + row -> row.get("DESCRIPTION"), + row -> row.get("SELECTION_MODE"), + row -> row.get("SELECTION_EXPRESSION"), + row -> row.get("UPDATED_AT"), + row -> row.get("CREATED_AT")) + .containsExactlyInAnyOrder( + tuple("uuid1", "port1", true, "name", "description", "NONE", null, NOW, NOW), + tuple("uuid2", "port2", false, "name2", "description2", "NONE", null, NOW, NOW)); + } + + @Test + public void migrate_simple_xml_only_portfolios_references() throws SQLException { + insertViewsDefInternalProperty(SIMPLE_XML_ONLY_PORTFOLIOS); + + underTest.execute(); + // re-entrant + underTest.execute(); + + assertThat(db.select("select kee from portfolios")) + .extracting(row -> row.get("KEE")) + .containsExactlyInAnyOrder("port1", "port2", "port3"); + + var portfolioKeyByUuid = db.select("select uuid, kee from portfolios").stream() + .collect(Collectors.toMap(row -> row.get("KEE"), row -> row.get("UUID").toString())); + + String portfolio1Uuid = portfolioKeyByUuid.get("port1"); + String portfolio2Uuid = portfolioKeyByUuid.get("port2"); + String portfolio3Uuid = portfolioKeyByUuid.get("port3"); + + assertThat(db.select("select portfolio_uuid, reference_uuid from portfolio_references")) + .extracting(row -> row.get("PORTFOLIO_UUID"), row -> row.get("REFERENCE_UUID")) + .containsExactlyInAnyOrder(tuple(portfolio2Uuid, portfolio1Uuid), tuple(portfolio2Uuid, portfolio3Uuid)); + } + + @Test + public void migrate_simple_xml_portfolios_and_apps_references() throws SQLException { + insertViewsDefInternalProperty(SIMPLE_XML_PORTFOLIOS_AND_APP); + + insertProject("app1-uuid", "app1-key", Qualifiers.APP); + insertProject("app2-uuid", "app2-key", Qualifiers.APP); + insertProject("proj1", "proj1-key", Qualifiers.PROJECT); + + underTest.execute(); + // re-entrant + underTest.execute(); + + assertThat(db.select("select kee from portfolios")) + .extracting(row -> row.get("KEE")) + .containsExactlyInAnyOrder("port1", "port2", "port3", "port4"); + + var portfolioKeyByUuid = db.select("select uuid, kee from portfolios").stream() + .collect(Collectors.toMap(row -> row.get("KEE"), row -> row.get("UUID").toString())); + + String portfolio1Uuid = portfolioKeyByUuid.get("port1"); + String portfolio2Uuid = portfolioKeyByUuid.get("port2"); + String portfolio3Uuid = portfolioKeyByUuid.get("port3"); + String portfolio4Uuid = portfolioKeyByUuid.get("port4"); + + assertThat(db.select("select portfolio_uuid, reference_uuid from portfolio_references")) + .extracting(row -> row.get("PORTFOLIO_UUID"), row -> row.get("REFERENCE_UUID")) + .containsExactlyInAnyOrder( + tuple(portfolio2Uuid, portfolio1Uuid), + tuple(portfolio2Uuid, portfolio3Uuid), + tuple(portfolio4Uuid, portfolio2Uuid), + tuple(portfolio4Uuid, "app1-uuid"), + tuple(portfolio4Uuid, "app2-uuid")) + .doesNotContain(tuple(portfolio4Uuid, "app3-key-not-existing")); + } + + @Test + public void migrate_xml_portfolios_hierarchy() throws SQLException { + insertViewsDefInternalProperty(SIMPLE_XML_PORTFOLIOS_HIERARCHY); + + underTest.execute(); + // re-entrant + underTest.execute(); + + assertThat(db.select("select kee from portfolios")) + .extracting(row -> row.get("KEE")) + .containsExactlyInAnyOrder("port1", "port2", "port3", "port4", "port5", "port6", "port7"); + + var portfolioKeyByUuid = db.select("select uuid, kee from portfolios").stream() + .collect(Collectors.toMap(row -> row.get("KEE"), row -> row.get("UUID").toString())); + + String portfolio1Uuid = portfolioKeyByUuid.get("port1"); + String portfolio2Uuid = portfolioKeyByUuid.get("port2"); + String portfolio3Uuid = portfolioKeyByUuid.get("port3"); + String portfolio4Uuid = portfolioKeyByUuid.get("port4"); + String portfolio5Uuid = portfolioKeyByUuid.get("port5"); + String portfolio6Uuid = portfolioKeyByUuid.get("port6"); + String portfolio7Uuid = portfolioKeyByUuid.get("port7"); + + assertThat(db.select("select uuid, parent_uuid, root_uuid from portfolios")) + .extracting(row -> row.get("UUID"), row -> row.get("PARENT_UUID"), row -> row.get("ROOT_UUID")) + .containsExactlyInAnyOrder( + tuple(portfolio1Uuid, null, portfolio1Uuid), + tuple(portfolio2Uuid, portfolio1Uuid, portfolio1Uuid), + tuple(portfolio3Uuid, portfolio1Uuid, portfolio1Uuid), + tuple(portfolio4Uuid, portfolio1Uuid, portfolio1Uuid), + tuple(portfolio5Uuid, portfolio2Uuid, portfolio1Uuid), + tuple(portfolio6Uuid, portfolio3Uuid, portfolio1Uuid), + tuple(portfolio7Uuid, portfolio3Uuid, portfolio1Uuid)); + } + + @Test + public void migrate_xml_portfolios_different_selections() throws SQLException { + insertViewsDefInternalProperty(SIMPLE_XML_PORTFOLIOS_DIFFERENT_SELECTIONS); + + insertProject("p1-uuid", "p1-key", Qualifiers.PROJECT); + insertProject("p2-uuid", "p2-key", Qualifiers.PROJECT); + insertProject("p3-uuid", "p3-key", Qualifiers.PROJECT); + + underTest.execute(); + // re-entrant + underTest.execute(); + + assertThat(db.select("select kee, selection_mode, selection_expression from portfolios")) + .extracting(row -> row.get("KEE"), row -> row.get("SELECTION_MODE"), row -> row.get("SELECTION_EXPRESSION")) + .containsExactlyInAnyOrder( + tuple("port-regexp", "REGEXP", ".*port.*"), + tuple("port-projects", "MANUAL", null), + tuple("port-tags", "TAGS", "tag1,tag2,tag3")); + + // verify projects + assertThat(db.select("select p.kee, pp.project_uuid from " + + "portfolio_projects pp join portfolios p on pp.portfolio_uuid = p.uuid where p.kee = 'port-projects'")) + .extracting(row -> row.get("KEE"), row -> row.get("PROJECT_UUID")) + .containsExactlyInAnyOrder( + tuple("port-projects", "p1-uuid"), + tuple("port-projects", "p2-uuid"), + tuple("port-projects", "p3-uuid")) + .doesNotContain(tuple("port-projects", "p4-uuid-not-existing")); + } + + @Test + public void migrate_xml_portfolios_legacy_selections() throws SQLException { + insertViewsDefInternalProperty(XML_PORTFOLIOS_LEGACY_SELECTIONS); + underTest.execute(); + // re-entrant + underTest.execute(); + + assertThat(db.select("select kee, selection_mode, selection_expression from portfolios")) + .extracting(row -> row.get("KEE"), row -> row.get("SELECTION_MODE"), row -> row.get("SELECTION_EXPRESSION")) + .containsExactlyInAnyOrder( + tuple("port-language", "NONE", null), + tuple("port-tag-value", "NONE", null)); + } + + private void insertProject(String uuid, String key, String qualifier) { + db.executeInsert("PROJECTS", + "UUID", uuid, + "KEE", key, + "QUALIFIER", qualifier, + "ORGANIZATION_UUID", uuid + "-key", + "TAGS", "tag1", + "PRIVATE", Boolean.toString(false), + "UPDATED_AT", System2.INSTANCE.now()); + } + + private void insertViewsDefInternalProperty(@Nullable String xml) { + String valueColumn = "text_value"; + if (xml != null && xml.length() > TEXT_VALUE_MAX_LENGTH) { + valueColumn = "clob_value"; + } + + db.executeInsert("internal_properties", + "kee", "views.def", + "is_empty", "false", + valueColumn, xml, + "created_at", system2.now()); + } + + private void insertComponent(String uuid, String kee, boolean isPrivate) { + db.executeInsert("components", + "uuid", uuid, + "kee", kee, + "enabled", false, + "private", isPrivate, + "root_uuid", uuid, + "uuid_path", uuid, + "project_uuid", uuid); + } + + private void insertDefaultVisibilityProperty(boolean isPrivate) { + db.executeInsert("properties", + "uuid", uuidFactory.create(), + "prop_key", "projects.default.visibility", + "IS_EMPTY", false, + "text_value", isPrivate ? "private" : "public", + "created_at", system2.now()); + } + +} diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTablesTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTablesTest/schema.sql new file mode 100644 index 00000000000..3cd223894dd --- /dev/null +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v91/MigratePortfoliosToNewTablesTest/schema.sql @@ -0,0 +1,110 @@ +CREATE TABLE "PROJECTS"( + "UUID" VARCHAR(40) NOT NULL, + "KEE" VARCHAR(400) NOT NULL, + "QUALIFIER" VARCHAR(10) NOT NULL, + "ORGANIZATION_UUID" VARCHAR(40) NOT NULL, + "NAME" VARCHAR(2000), + "DESCRIPTION" VARCHAR(2000), + "PRIVATE" BOOLEAN NOT NULL, + "TAGS" VARCHAR(500), + "CREATED_AT" BIGINT, + "UPDATED_AT" BIGINT NOT NULL +); +ALTER TABLE "PROJECTS" ADD CONSTRAINT "PK_NEW_PROJECTS" PRIMARY KEY("UUID"); +CREATE UNIQUE INDEX "UNIQ_PROJECTS_KEE" ON "PROJECTS"("KEE"); +CREATE INDEX "IDX_QUALIFIER" ON "PROJECTS"("QUALIFIER"); + +CREATE TABLE "INTERNAL_PROPERTIES"( + "KEE" VARCHAR(20) NOT NULL, + "IS_EMPTY" BOOLEAN NOT NULL, + "TEXT_VALUE" VARCHAR(4000), + "CLOB_VALUE" CLOB, + "CREATED_AT" BIGINT NOT NULL +); +ALTER TABLE "INTERNAL_PROPERTIES" ADD CONSTRAINT "PK_INTERNAL_PROPERTIES" PRIMARY KEY("KEE"); + +CREATE TABLE "PORTFOLIO_PROJECTS"( + "UUID" VARCHAR(40) NOT NULL, + "PORTFOLIO_UUID" VARCHAR(40) NOT NULL, + "PROJECT_UUID" VARCHAR(40) NOT NULL, + "CREATED_AT" BIGINT NOT NULL +); +ALTER TABLE "PORTFOLIO_PROJECTS" ADD CONSTRAINT "PK_PORTFOLIO_PROJECTS" PRIMARY KEY("UUID"); + +CREATE TABLE "PORTFOLIO_REFERENCES"( + "UUID" VARCHAR(40) NOT NULL, + "PORTFOLIO_UUID" VARCHAR(40) NOT NULL, + "REFERENCE_UUID" VARCHAR(40) NOT NULL, + "CREATED_AT" BIGINT NOT NULL +); +ALTER TABLE "PORTFOLIO_REFERENCES" ADD CONSTRAINT "PK_PORTFOLIO_REFERENCES" PRIMARY KEY("UUID"); + +CREATE TABLE "PORTFOLIOS"( + "UUID" VARCHAR(40) NOT NULL, + "KEE" VARCHAR(400) NOT NULL, + "NAME" VARCHAR(2000) NOT NULL, + "DESCRIPTION" VARCHAR(2000), + "ROOT_UUID" VARCHAR(40) NOT NULL, + "PARENT_UUID" VARCHAR(40), + "PRIVATE" BOOLEAN NOT NULL, + "SELECTION_MODE" VARCHAR(50) NOT NULL, + "SELECTION_EXPRESSION" VARCHAR(50), + "CREATED_AT" BIGINT NOT NULL, + "UPDATED_AT" BIGINT NOT NULL +); +ALTER TABLE "PORTFOLIOS" ADD CONSTRAINT "PK_PORTFOLIOS" PRIMARY KEY("UUID"); + +CREATE TABLE "PROPERTIES"( + "UUID" VARCHAR(40) NOT NULL, + "PROP_KEY" VARCHAR(512) NOT NULL, + "IS_EMPTY" BOOLEAN NOT NULL, + "TEXT_VALUE" VARCHAR(4000), + "CLOB_VALUE" CLOB, + "CREATED_AT" BIGINT NOT NULL, + "COMPONENT_UUID" VARCHAR(40), + "USER_UUID" VARCHAR(255) +); +ALTER TABLE "PROPERTIES" ADD CONSTRAINT "PK_PROPERTIES" PRIMARY KEY("UUID"); +CREATE INDEX "PROPERTIES_KEY" ON "PROPERTIES"("PROP_KEY"); + +CREATE TABLE "COMPONENTS"( + "UUID" VARCHAR(50) NOT NULL, + "KEE" VARCHAR(400), + "DEPRECATED_KEE" VARCHAR(400), + "NAME" VARCHAR(2000), + "LONG_NAME" VARCHAR(2000), + "DESCRIPTION" VARCHAR(2000), + "ENABLED" BOOLEAN DEFAULT TRUE NOT NULL, + "SCOPE" VARCHAR(3), + "QUALIFIER" VARCHAR(10), + "PRIVATE" BOOLEAN NOT NULL, + "ROOT_UUID" VARCHAR(50) NOT NULL, + "LANGUAGE" VARCHAR(20), + "COPY_COMPONENT_UUID" VARCHAR(50), + "PATH" VARCHAR(2000), + "UUID_PATH" VARCHAR(1500) NOT NULL, + "PROJECT_UUID" VARCHAR(50) NOT NULL, + "MODULE_UUID" VARCHAR(50), + "MODULE_UUID_PATH" VARCHAR(1500), + "MAIN_BRANCH_PROJECT_UUID" VARCHAR(50), + "B_CHANGED" BOOLEAN, + "B_NAME" VARCHAR(500), + "B_LONG_NAME" VARCHAR(500), + "B_DESCRIPTION" VARCHAR(2000), + "B_ENABLED" BOOLEAN, + "B_QUALIFIER" VARCHAR(10), + "B_LANGUAGE" VARCHAR(20), + "B_COPY_COMPONENT_UUID" VARCHAR(50), + "B_PATH" VARCHAR(2000), + "B_UUID_PATH" VARCHAR(1500), + "B_MODULE_UUID" VARCHAR(50), + "B_MODULE_UUID_PATH" VARCHAR(1500), + "CREATED_AT" TIMESTAMP +); +CREATE UNIQUE INDEX "PROJECTS_KEE" ON "COMPONENTS"("KEE"); +CREATE INDEX "PROJECTS_MODULE_UUID" ON "COMPONENTS"("MODULE_UUID"); +CREATE INDEX "PROJECTS_PROJECT_UUID" ON "COMPONENTS"("PROJECT_UUID"); +CREATE INDEX "PROJECTS_QUALIFIER" ON "COMPONENTS"("QUALIFIER"); +CREATE INDEX "PROJECTS_ROOT_UUID" ON "COMPONENTS"("ROOT_UUID"); +CREATE INDEX "PROJECTS_UUID" ON "COMPONENTS"("UUID"); +CREATE INDEX "IDX_MAIN_BRANCH_PRJ_UUID" ON "COMPONENTS"("MAIN_BRANCH_PROJECT_UUID"); |