From: Simon Brandhof Date: Tue, 24 Sep 2013 18:04:22 +0000 (+0200) Subject: SONAR-4690 SONAR-4691 speed-up db migration of violations X-Git-Tag: 3.7.2~5 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=52a508dc5fb51957159f0e5bf2a5c80e2a77f9b5;p=sonarqube.git SONAR-4690 SONAR-4691 speed-up db migration of violations --- diff --git a/sonar-core/src/test/java/org/sonar/core/persistence/TestDatabase.java b/sonar-core/src/test/java/org/sonar/core/persistence/TestDatabase.java index 796625ca1be..e750985fd74 100644 --- a/sonar-core/src/test/java/org/sonar/core/persistence/TestDatabase.java +++ b/sonar-core/src/test/java/org/sonar/core/persistence/TestDatabase.java @@ -55,6 +55,8 @@ import java.net.HttpURLConnection; import java.net.URI; import java.net.URISyntaxException; import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; import java.util.Map; @@ -143,6 +145,28 @@ public class TestDatabase extends ExternalResource { } } + public int count(String sql) { + Connection connection = null; + PreparedStatement stmt = null; + ResultSet rs = null; + try { + connection = openConnection(); + stmt = connection.prepareStatement(sql); + rs = stmt.executeQuery(); + if (rs.next()) { + return rs.getInt(1); + } + throw new IllegalStateException("No results for " + sql); + + } catch (Exception e) { + throw new IllegalStateException("Fail to execute sql: " + sql); + + } finally { + DbUtils.closeQuietly(connection, stmt, rs); + } + } + + public void prepareDbUnit(Class testClass, String... testNames) { InputStream[] streams = new InputStream[testNames.length]; try { @@ -278,5 +302,4 @@ public class TestDatabase extends ExternalResource { IOUtils.closeQuietly(input); } } - } diff --git a/sonar-server/src/main/java/org/sonar/server/db/DatabaseMigration.java b/sonar-server/src/main/java/org/sonar/server/db/DatabaseMigration.java deleted file mode 100644 index 2e99e4b30bb..00000000000 --- a/sonar-server/src/main/java/org/sonar/server/db/DatabaseMigration.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2013 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube 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. - * - * SonarQube 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.db; - -import org.sonar.core.persistence.Database; - -/** - * Java alternative of ActiveRecord::Migration. - * @since 3.7 - */ -public interface DatabaseMigration { - - void execute(Database db); - -} diff --git a/sonar-server/src/main/java/org/sonar/server/db/DatabaseMigrator.java b/sonar-server/src/main/java/org/sonar/server/db/DatabaseMigrator.java deleted file mode 100644 index 6b36e055134..00000000000 --- a/sonar-server/src/main/java/org/sonar/server/db/DatabaseMigrator.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2013 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube 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. - * - * SonarQube 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.db; - -import com.google.common.annotations.VisibleForTesting; -import org.apache.commons.dbutils.DbUtils; -import org.apache.ibatis.session.SqlSession; -import org.slf4j.LoggerFactory; -import org.sonar.api.ServerComponent; -import org.sonar.core.persistence.Database; -import org.sonar.core.persistence.DdlUtils; -import org.sonar.core.persistence.MyBatis; - -import java.sql.Connection; - -/** - * Restore schema by executing DDL scripts. Only H2 database is supported. - * Other databases are created by Ruby on Rails migrations. - * - * @since 2.12 - */ -public class DatabaseMigrator implements ServerComponent { - - private final MyBatis myBatis; - private final Database database; - - public DatabaseMigrator(MyBatis myBatis, Database database) { - this.myBatis = myBatis; - this.database = database; - } - - /** - * @return true if the database has been created, false if this database is not supported - */ - public boolean createDatabase() { - if (!DdlUtils.supportsDialect(database.getDialect().getId())) { - return false; - } - - LoggerFactory.getLogger(getClass()).info("Create database"); - SqlSession session = null; - Connection connection = null; - try { - session = myBatis.openSession(); - connection = session.getConnection(); - createSchema(connection, database.getDialect().getId()); - return true; - } finally { - MyBatis.closeQuietly(session); - - // The connection is probably already closed by session.close() - // but it's not documented in mybatis javadoc. - DbUtils.closeQuietly(connection); - } - } - - public void executeMigration(String className) { - try { - Class migrationClass = (Class) Class.forName(className); - DatabaseMigration migration = migrationClass.newInstance(); - migration.execute(database); - - } catch (Exception e) { - // duplication between log and exception because webapp does not correctly log initial stacktrace - String msg = "Fail to execute database migration: " + className; - LoggerFactory.getLogger(getClass()).error(msg, e); - throw new IllegalStateException(msg, e); - } - } - - @VisibleForTesting - protected void createSchema(Connection connection, String dialectId) { - DdlUtils.createSchema(connection, dialectId); - } -} diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/ConvertViolationsToIssues.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/ConvertViolationsToIssues.java deleted file mode 100644 index be1d2c4ce5e..00000000000 --- a/sonar-server/src/main/java/org/sonar/server/db/migrations/ConvertViolationsToIssues.java +++ /dev/null @@ -1,408 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2013 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube 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. - * - * SonarQube 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.db.migrations; - -import com.google.common.base.Objects; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import org.apache.commons.dbutils.DbUtils; -import org.apache.commons.dbutils.QueryRunner; -import org.apache.commons.dbutils.ResultSetHandler; -import org.apache.commons.dbutils.handlers.AbstractListHandler; -import org.slf4j.LoggerFactory; -import org.sonar.api.rule.Severity; -import org.sonar.core.persistence.Database; -import org.sonar.core.persistence.dialect.Oracle; -import org.sonar.server.db.DatabaseMigration; - -import javax.annotation.CheckForNull; -import javax.annotation.Nullable; - -import java.sql.Connection; -import java.sql.Date; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -/** - * Used in the Active Record Migration 401 - */ -public class ConvertViolationsToIssues implements DatabaseMigration { - - private static final int GROUP_SIZE = 500; - private static final String PROJECT_ID = "projectId"; - private static final String CREATED_AT = "createdAt"; - private static final String REVIEW_ID = "reviewId"; - private static final String SEVERITY = "severity"; - private static final String REVIEW_STATUS = "reviewStatus"; - private static final String REVIEW_MANUAL_SEVERITY = "reviewManualSeverity"; - private static final String REVIEW_SEVERITY = "reviewSeverity"; - private static final String REVIEW_UPDATED_AT = "reviewUpdatedAt"; - private static final String ROOT_PROJECT_ID = "rootProjectId"; - private static final String RULE_ID = "ruleId"; - private static final String MESSAGE = "message"; - private static final String LINE = "line"; - private static final String COST = "cost"; - private static final String CHECKSUM = "checksum"; - private static final String REVIEW_RESOLUTION = "reviewResolution"; - private static final String REVIEW_REPORTER_ID = "reviewReporterId"; - private static final String REVIEW_ASSIGNEE_ID = "reviewAssigneeId"; - private static final String REVIEW_DATA = "reviewData"; - private static final String REVIEW_MANUAL_VIOLATION = "reviewManualViolation"; - private static final String PLAN_ID = "planId"; - private static final String ISSUE_KEY = "issueKey"; - - private static final String STATUS_OPEN = "OPEN"; - private static final String STATUS_CONFIRMED = "CONFIRMED"; - private static final String UPDATED_AT = "updatedAt"; - private static final String REVIEW_TEXT = "reviewText"; - private static final String USER_ID = "userId"; - private static final String SEVERITY_MAJOR = "MAJOR"; - - private QueryRunner runner = new QueryRunner(); - - @Override - public void execute(Database db) { - Connection readConnection = null, writeConnection = null; - try { - readConnection = db.getDataSource().getConnection(); - writeConnection = db.getDataSource().getConnection(); - writeConnection.setAutoCommit(false); - truncateIssueTables(writeConnection); - convertViolations(readConnection, new Converter(db, runner, readConnection, writeConnection)); - } catch (Exception e) { - throw new IllegalStateException("Fail to convert violations to issues", e); - } finally { - DbUtils.closeQuietly(readConnection); - DbUtils.closeQuietly(writeConnection); - } - } - - private void truncateIssueTables(Connection writeConnection) throws SQLException { - // lower-case table names for SQLServer.... - runner.update(writeConnection, "TRUNCATE TABLE issues"); - runner.update(writeConnection, "TRUNCATE TABLE issue_changes"); - writeConnection.commit(); - } - - private void convertViolations(Connection readConnection, Converter converter) throws SQLException { - runner.query(readConnection, "select id from rule_failures", new ViolationIdHandler(converter)); - } - - - /** - * Browse violation ids and process them by groups of {@link #GROUP_SIZE}. - */ - private static class ViolationIdHandler implements ResultSetHandler { - private Converter converter; - private Object[] violationIds = new Object[GROUP_SIZE]; - private int cursor = 0; - - private ViolationIdHandler(Converter converter) { - this.converter = converter; - } - - @Override - public Object handle(ResultSet rs) throws SQLException { - int total = 0; - while (rs.next()) { - long violationId = rs.getLong(1); - violationIds[cursor++] = violationId; - if (cursor == GROUP_SIZE) { - convert(); - Arrays.fill(violationIds, -1L); - cursor = 0; - } - total++; - } - if (cursor > 0) { - convert(); - } - LoggerFactory.getLogger(getClass()).info(String.format("%d violations migrated to issues", total)); - return null; - } - - private void convert() throws SQLException { - if (cursor > 0) { - converter.convert(violationIds); - } - } - } - - private static class Converter { - private static final long ONE_YEAR = 365L * 24 * 60 * 60 * 1000; - private String insertSql; - private String insertChangeSql; - private Date oneYearAgo = new Date(System.currentTimeMillis() - ONE_YEAR); - private QueryRunner runner; - private Connection readConnection, writeConnection; - private Map loginsByUserId; - private Map plansById; - - private Converter(Database database, QueryRunner runner, Connection readConnection, Connection writeConnection) throws SQLException { - this.runner = runner; - this.readConnection = readConnection; - this.writeConnection = writeConnection; - initInsertSql(database); - initUsers(); - initPlans(); - } - - private void initInsertSql(Database database) { - String issuesColumnsWithoutId = "kee, component_id, root_component_id, rule_id, severity, manual_severity, message, line, effort_to_fix, status, resolution, " + - "checksum, reporter, assignee, action_plan_key, issue_attributes, issue_creation_date, issue_update_date, created_at, updated_at"; - - if (Oracle.ID.equals(database.getDialect().getId())) { - insertSql = "INSERT INTO issues(id, "+ issuesColumnsWithoutId + ") " + - " VALUES (issues_seq.nextval, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; - insertChangeSql = "INSERT INTO issue_changes(id, kee, issue_key, user_login, change_type, change_data, created_at, updated_at) " + - " VALUES (issue_changes_seq.nextval, ?, ?, ?, 'comment', ?, ?, ?)"; - } else { - insertSql = "INSERT INTO issues("+ issuesColumnsWithoutId + ") " + - " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; - insertChangeSql = "INSERT INTO issue_changes(kee, issue_key, user_login, change_type, change_data, created_at, updated_at) VALUES (?, ?, ?, 'comment', ?, ?, ?)"; - } - } - - - private void initUsers() throws SQLException { - loginsByUserId = selectLongString("select id,login from users"); - } - - private void initPlans() throws SQLException { - plansById = selectLongString("select id,kee from action_plans"); - } - - private Map selectLongString(String sql) throws SQLException { - return runner.query(readConnection, sql, new ResultSetHandler>() { - @Override - public Map handle(ResultSet rs) throws SQLException { - Map map = Maps.newHashMap(); - while (rs.next()) { - map.put(rs.getLong(1), rs.getString(2)); - } - return map; - } - }); - } - - /** - * Convert a group of maximum {@link #GROUP_SIZE} violations to issues - */ - void convert(Object[] violationIds) throws SQLException { - List> rows = runner.query(readConnection, ViolationHandler.SQL, new ViolationHandler(), violationIds); - List allParams = Lists.newArrayList(); - List> allComments = Lists.newArrayList(); - - for (Map row : rows) { - Long componentId = (Long) row.get(PROJECT_ID); - if (componentId == null) { - continue; - } - String issueKey = UUID.randomUUID().toString(); - String status, severity, reporter = null; - boolean manualSeverity; - Object createdAt = Objects.firstNonNull(row.get(CREATED_AT), oneYearAgo); - Object updatedAt; - Long reviewId = (Long) row.get(REVIEW_ID); - if (reviewId == null) { - // violation without review - status = STATUS_OPEN; - manualSeverity = false; - severity = (String) row.get(SEVERITY); - updatedAt = createdAt; - } else { - // violation + review - String reviewStatus = (String) row.get(REVIEW_STATUS); - status = (STATUS_OPEN.equals(reviewStatus) ? STATUS_CONFIRMED : reviewStatus); - manualSeverity = Objects.firstNonNull((Boolean) row.get(REVIEW_MANUAL_SEVERITY), false); - severity = (String) row.get(REVIEW_SEVERITY); - updatedAt = Objects.firstNonNull(row.get(REVIEW_UPDATED_AT), oneYearAgo); - if ((Boolean) row.get(REVIEW_MANUAL_VIOLATION)) { - reporter = login((Long) row.get(REVIEW_REPORTER_ID)); - } - - List> comments = runner.query(readConnection, ReviewCommentsHandler.SQL + reviewId, new ReviewCommentsHandler()); - for (Map comment : comments) { - comment.put(ISSUE_KEY, issueKey); - allComments.add(comment); - } - } - - Object[] params = new Object[20]; - params[0] = issueKey; - params[1] = componentId; - params[2] = row.get(ROOT_PROJECT_ID); - params[3] = row.get(RULE_ID); - params[4] = severity; - params[5] = manualSeverity; - params[6] = row.get(MESSAGE); - params[7] = row.get(LINE); - params[8] = row.get(COST); - params[9] = status; - params[10] = row.get(REVIEW_RESOLUTION); - params[11] = row.get(CHECKSUM); - params[12] = reporter; - params[13] = login((Long) row.get(REVIEW_ASSIGNEE_ID)); - params[14] = plan((Long) row.get(PLAN_ID)); - params[15] = row.get(REVIEW_DATA); - params[16] = createdAt; - params[17] = updatedAt; - params[18] = createdAt; - params[19] = updatedAt; - - allParams.add(params); - } - runner.batch(writeConnection, insertSql, allParams.toArray(new Object[allParams.size()][])); - writeConnection.commit(); - - insertComments(writeConnection, allComments); - } - - private void insertComments(Connection writeConnection, List> comments) throws SQLException { - List allParams = Lists.newArrayList(); - - for (Map comment : comments) { - String login = login((Long) comment.get(USER_ID)); - if (login != null) { - Object[] params = new Object[6]; - params[0] = UUID.randomUUID().toString(); - params[1] = comment.get(ISSUE_KEY); - params[2] = login; - params[3] = comment.get(REVIEW_TEXT); - params[4] = comment.get(CREATED_AT); - params[5] = comment.get(UPDATED_AT); - allParams.add(params); - } - } - if (!allParams.isEmpty()) { - runner.batch(writeConnection, insertChangeSql, allParams.toArray(new Object[allParams.size()][])); - writeConnection.commit(); - } - } - - @CheckForNull - private String login(@Nullable Long userId) { - if (userId != null) { - return loginsByUserId.get(userId); - } - return null; - } - - @CheckForNull - private String plan(@Nullable Long planId) { - if (planId != null) { - return plansById.get(planId); - } - return null; - } - } - - - private static class ViolationHandler extends AbstractListHandler> { - static final String SQL; - static { - StringBuilder sb = new StringBuilder("select rev.id as reviewId, s.project_id as projectId, rf.rule_id as ruleId, " + - " rf.failure_level as failureLevel, rf.message as message, rf.line as line, " + - " rf.cost as cost, rf.created_at as createdAt, rf.checksum as checksum, rev.user_id as reviewReporterId, " + - " rev.assignee_id as reviewAssigneeId, rev.status as reviewStatus, " + - " rev.severity as reviewSeverity, rev.resolution as reviewResolution, rev.manual_severity as reviewManualSeverity, " + - " rev.data as reviewData, rev.updated_at as reviewUpdatedAt, " + - " s.root_project_id as rootProjectId, rev.manual_violation as reviewManualViolation, planreviews.action_plan_id as planId " + - " from rule_failures rf " + - " inner join snapshots s on s.id=rf.snapshot_id " + - " left join reviews rev on rev.rule_failure_permanent_id=rf.permanent_id " + - " left join action_plans_reviews planreviews on planreviews.review_id=rev.id " + - " where "); - for (int i = 0; i < GROUP_SIZE; i++) { - if (i > 0) { - sb.append(" or "); - } - sb.append("rf.id=?"); - } - SQL = sb.toString(); - } - - private static final Map SEVERITIES = ImmutableMap.of(1, Severity.INFO, 2, Severity.MINOR, 3, Severity.MAJOR, 4, Severity.CRITICAL, 5, Severity.BLOCKER); - - @Override - protected Map handleRow(ResultSet rs) throws SQLException { - Map map = Maps.newHashMap(); - map.put(REVIEW_ID, getLong(rs, REVIEW_ID)); - map.put(PROJECT_ID, getLong(rs, PROJECT_ID)); - map.put(ROOT_PROJECT_ID, getLong(rs, ROOT_PROJECT_ID)); - map.put(RULE_ID, getLong(rs, RULE_ID)); - map.put(SEVERITY, Objects.firstNonNull(SEVERITIES.get(getInt(rs, "failureLevel")), SEVERITY_MAJOR)); - map.put(MESSAGE, rs.getString(MESSAGE)); - map.put(LINE, getInt(rs, LINE)); - map.put(COST, getDouble(rs, COST)); - map.put(CHECKSUM, rs.getString(CHECKSUM)); - map.put(CREATED_AT, rs.getTimestamp(CREATED_AT)); - map.put(REVIEW_RESOLUTION, rs.getString(REVIEW_RESOLUTION)); - map.put(REVIEW_SEVERITY, Objects.firstNonNull(rs.getString(REVIEW_SEVERITY), SEVERITY_MAJOR)); - map.put(REVIEW_STATUS, rs.getString(REVIEW_STATUS)); - map.put(REVIEW_REPORTER_ID, getLong(rs, REVIEW_REPORTER_ID)); - map.put(REVIEW_ASSIGNEE_ID, getLong(rs, REVIEW_ASSIGNEE_ID)); - map.put(REVIEW_DATA, rs.getString(REVIEW_DATA)); - map.put(REVIEW_MANUAL_SEVERITY, rs.getBoolean(REVIEW_MANUAL_SEVERITY)); - map.put(REVIEW_UPDATED_AT, rs.getTimestamp(REVIEW_UPDATED_AT)); - map.put(REVIEW_MANUAL_VIOLATION, rs.getBoolean(REVIEW_MANUAL_VIOLATION)); - map.put(PLAN_ID, getLong(rs, PLAN_ID)); - return map; - } - } - - private static class ReviewCommentsHandler extends AbstractListHandler> { - static final String SQL = "select created_at as createdAt, updated_at as updatedAt, user_id as userId, review_text as reviewText from review_comments where review_id="; - - @Override - protected Map handleRow(ResultSet rs) throws SQLException { - Map map = Maps.newHashMap(); - map.put(CREATED_AT, rs.getTimestamp(CREATED_AT)); - map.put(UPDATED_AT, rs.getTimestamp(UPDATED_AT)); - map.put(USER_ID, getLong(rs, USER_ID)); - map.put(REVIEW_TEXT, rs.getString(REVIEW_TEXT)); - return map; - } - } - - @CheckForNull - static Long getLong(ResultSet rs, String columnName) throws SQLException { - long l = rs.getLong(columnName); - return rs.wasNull() ? null : l; - } - - @CheckForNull - static Double getDouble(ResultSet rs, String columnName) throws SQLException { - double d = rs.getDouble(columnName); - return rs.wasNull() ? null : d; - } - - @CheckForNull - static Integer getInt(ResultSet rs, String columnName) throws SQLException { - int i = rs.getInt(columnName); - return rs.wasNull() ? null : i; - } - -} diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigration.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigration.java new file mode 100644 index 00000000000..4126138c7d7 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigration.java @@ -0,0 +1,30 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations; + +/** + * Java alternative of ActiveRecord::Migration. Do not forget to declare implementation classes in {@link DatabaseMigrations#CLASSES} + * @since 3.7 + */ +public interface DatabaseMigration { + + void execute(); + +} diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigrations.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigrations.java new file mode 100644 index 00000000000..982042cec98 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigrations.java @@ -0,0 +1,33 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations; + +import com.google.common.collect.ImmutableList; +import org.sonar.server.db.migrations.violation.ViolationMigration; + +import java.util.List; + +public interface DatabaseMigrations { + + List> CLASSES = ImmutableList.>of( + ViolationMigration.class + ); + +} diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigrator.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigrator.java new file mode 100644 index 00000000000..2a0a165c8b7 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigrator.java @@ -0,0 +1,102 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.commons.dbutils.DbUtils; +import org.apache.ibatis.session.SqlSession; +import org.slf4j.LoggerFactory; +import org.sonar.api.ServerComponent; +import org.sonar.core.persistence.Database; +import org.sonar.core.persistence.DdlUtils; +import org.sonar.core.persistence.MyBatis; + +import java.sql.Connection; + +/** + * Restore schema by executing DDL scripts. Only H2 database is supported. + * Other databases are created by Ruby on Rails migrations. + * + * @since 2.12 + */ +public class DatabaseMigrator implements ServerComponent { + + private final MyBatis myBatis; + private final Database database; + private final DatabaseMigration[] migrations; + + public DatabaseMigrator(MyBatis myBatis, Database database, DatabaseMigration[] migrations) { + this.myBatis = myBatis; + this.database = database; + this.migrations = migrations; + } + + /** + * @return true if the database has been created, false if this database is not supported + */ + public boolean createDatabase() { + if (!DdlUtils.supportsDialect(database.getDialect().getId())) { + return false; + } + + LoggerFactory.getLogger(getClass()).info("Create database"); + SqlSession session = null; + Connection connection = null; + try { + session = myBatis.openSession(); + connection = session.getConnection(); + createSchema(connection, database.getDialect().getId()); + return true; + } finally { + MyBatis.closeQuietly(session); + + // The connection is probably already closed by session.close() + // but it's not documented in mybatis javadoc. + DbUtils.closeQuietly(connection); + } + } + + public void executeMigration(String className) { + DatabaseMigration migration = getMigration(className); + try { + migration.execute(); + + } catch (Exception e) { + // duplication between log and exception because webapp does not correctly log initial stacktrace + String msg = "Fail to execute database migration: " + className; + LoggerFactory.getLogger(getClass()).error(msg, e); + throw new IllegalStateException(msg, e); + } + } + + private DatabaseMigration getMigration(String className) { + for (DatabaseMigration migration : migrations) { + if (migration.getClass().getName().equals(className)) { + return migration; + } + } + throw new IllegalArgumentException("Database migration not found: " + className); + } + + @VisibleForTesting + protected void createSchema(Connection connection, String dialectId) { + DdlUtils.createSchema(connection, dialectId); + } +} diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/Progress.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/Progress.java new file mode 100644 index 00000000000..d74f6182b65 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/Progress.java @@ -0,0 +1,72 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations.violation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This task logs every minute the status of migration. It is destroyed + * when migration is finished. + */ +class Progress extends TimerTask { + + static final String THREAD_NAME = "Violation Migration Progress"; + static final long DELAY_MS = 60000L; + + private final AtomicInteger counter = new AtomicInteger(0); + private final Logger logger; + private final int totalViolations; + private final long start; + + Progress(int totalViolations, Logger logger, long startDate) { + this.totalViolations = totalViolations; + this.logger = logger; + this.start = startDate; + } + + Progress(int totalViolations) { + this(totalViolations, LoggerFactory.getLogger(Progress.class), System.currentTimeMillis()); + } + + void increment(int delta) { + counter.addAndGet(delta); + } + + @Override + public void run() { + int totalIssues = counter.get(); + long durationMinutes = (System.currentTimeMillis() - start) / 60000L; + int percents = (100 * totalIssues) / totalViolations; + if (totalIssues>0 && durationMinutes > 0) { + int frequency = (int) (totalIssues / durationMinutes); + int remaining = (totalViolations - totalIssues) / frequency; + logger.info(String.format( + "%d%% [%d/%d violations, %d minutes remaining]", percents, totalIssues, totalViolations, remaining) + ); + } else { + logger.info(String.format("%d%% [%d/%d violations]", percents, totalIssues, totalViolations)); + } + + } +} diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/Referentials.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/Referentials.java new file mode 100644 index 00000000000..2260b160f4d --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/Referentials.java @@ -0,0 +1,135 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations.violation; + +import com.google.common.collect.Maps; +import org.apache.commons.dbutils.DbUtils; +import org.apache.commons.dbutils.QueryRunner; +import org.apache.commons.dbutils.ResultSetHandler; +import org.sonar.core.persistence.Database; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Data loaded from database before migrating violations. It is + * shared amongst converter parallel tasks. + */ +class Referentials { + + static final int VIOLATION_GROUP_SIZE = 1000; + + private final Map loginsByUserId; + private final Map plansById; + private final Queue groupsOfViolationIds; + + // int is enough, it allows to upgrade up to 2 billions violations ! + private int totalViolations = 0; + + Referentials(Database database) throws SQLException { + loginsByUserId = selectLongString(database, "select id,login from users"); + plansById = selectLongString(database, "select id,kee from action_plans"); + groupsOfViolationIds = initGroupOfViolationIds(database); + } + + @CheckForNull + String actionPlan(@Nullable Long id) { + return id != null ? plansById.get(id) : null; + } + + @CheckForNull + String userLogin(@Nullable Long id) { + return id != null ? loginsByUserId.get(id) : null; + } + + int totalViolations() { + return totalViolations; + } + + Long[] pollGroupOfViolationIds() { + long[] longs = groupsOfViolationIds.poll(); + if (longs == null) { + return new Long[0]; + } + Long[] objects = new Long[longs.length]; + for (int i = 0; i < longs.length; i++) { + objects[i] = Long.valueOf(longs[i]); + } + return objects; + } + + private Map selectLongString(Database database, String sql) throws SQLException { + Connection connection = database.getDataSource().getConnection(); + try { + return new QueryRunner().query(connection, sql, new ResultSetHandler>() { + @Override + public Map handle(ResultSet rs) throws SQLException { + Map map = Maps.newHashMap(); + while (rs.next()) { + map.put(rs.getLong(1), rs.getString(2)); + } + return map; + } + }); + } finally { + DbUtils.closeQuietly(connection); + } + } + + private Queue initGroupOfViolationIds(Database database) throws SQLException { + Connection connection = database.getDataSource().getConnection(); + Statement stmt = null; + ResultSet rs = null; + try { + connection.setAutoCommit(false); + stmt = connection.createStatement(); + stmt.setFetchSize(10000); + rs = stmt.executeQuery("select id from rule_failures"); + ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue(); + + totalViolations = 0; + long[] block = new long[VIOLATION_GROUP_SIZE]; + int cursor = 0; + while (rs.next()) { + block[cursor++] = rs.getLong(1); + totalViolations++; + if (cursor == VIOLATION_GROUP_SIZE) { + queue.add(block); + block = new long[VIOLATION_GROUP_SIZE]; + cursor = 0; + } + } + if (cursor > 0) { + queue.add(block); + } + return queue; + } finally { + DbUtils.closeQuietly(connection, stmt, rs); + } + } + +} diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/SqlUtil.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/SqlUtil.java new file mode 100644 index 00000000000..df1a9d867ae --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/SqlUtil.java @@ -0,0 +1,63 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations.violation; + +import org.slf4j.Logger; + +import javax.annotation.CheckForNull; +import java.sql.ResultSet; +import java.sql.SQLException; + +class SqlUtil { + + private SqlUtil() { + // only static methods + } + + /** + * Logback does not log exceptions associated to {@link java.sql.SQLException#getNextException()}. + * See http://jira.qos.ch/browse/LOGBACK-775 + */ + static void log(Logger logger, SQLException e) { + SQLException next = e.getNextException(); + while (next != null) { + logger.error("SQL error: {}. Message: {}", next.getSQLState(), next.getMessage()); + next = next.getNextException(); + } + } + + @CheckForNull + static Long getLong(ResultSet rs, String columnName) throws SQLException { + long l = rs.getLong(columnName); + return rs.wasNull() ? null : l; + } + + @CheckForNull + static Double getDouble(ResultSet rs, String columnName) throws SQLException { + double d = rs.getDouble(columnName); + return rs.wasNull() ? null : d; + } + + @CheckForNull + static Integer getInt(ResultSet rs, String columnName) throws SQLException { + int i = rs.getInt(columnName); + return rs.wasNull() ? null : i; + } +} diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationConverter.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationConverter.java new file mode 100644 index 00000000000..de525fc2b31 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationConverter.java @@ -0,0 +1,309 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations.violation; + +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.apache.commons.dbutils.DbUtils; +import org.apache.commons.dbutils.QueryRunner; +import org.apache.commons.dbutils.handlers.AbstractListHandler; +import org.sonar.api.rule.Severity; +import org.sonar.core.persistence.Database; + +import java.sql.Connection; +import java.sql.Date; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Callable; + +class ViolationConverter implements Callable { + + private static final long ONE_YEAR = 365L * 24 * 60 * 60 * 1000; + private static final Date ONE_YEAR_AGO = new Date(System.currentTimeMillis() - ONE_YEAR); + + private static final String PROJECT_ID = "projectId"; + private static final String CREATED_AT = "createdAt"; + private static final String REVIEW_ID = "reviewId"; + private static final String SEVERITY = "severity"; + private static final String REVIEW_STATUS = "reviewStatus"; + private static final String REVIEW_MANUAL_SEVERITY = "reviewManualSeverity"; + private static final String REVIEW_SEVERITY = "reviewSeverity"; + private static final String REVIEW_UPDATED_AT = "reviewUpdatedAt"; + private static final String ROOT_PROJECT_ID = "rootProjectId"; + private static final String RULE_ID = "ruleId"; + private static final String MESSAGE = "message"; + private static final String LINE = "line"; + private static final String COST = "cost"; + private static final String CHECKSUM = "checksum"; + private static final String REVIEW_RESOLUTION = "reviewResolution"; + private static final String REVIEW_REPORTER_ID = "reviewReporterId"; + private static final String REVIEW_ASSIGNEE_ID = "reviewAssigneeId"; + private static final String REVIEW_DATA = "reviewData"; + private static final String REVIEW_MANUAL_VIOLATION = "reviewManualViolation"; + private static final String PLAN_ID = "planId"; + private static final String ISSUE_KEY = "issueKey"; + private static final String STATUS_OPEN = "OPEN"; + private static final String STATUS_CONFIRMED = "CONFIRMED"; + private static final String UPDATED_AT = "updatedAt"; + private static final String REVIEW_TEXT = "reviewText"; + private static final String USER_ID = "userId"; + private static final String SEVERITY_MAJOR = "MAJOR"; + + + private static final String SQL_ISSUE_COLUMNS = "kee, component_id, root_component_id, rule_id, severity, manual_severity, message, line, effort_to_fix, status, resolution, " + + "checksum, reporter, assignee, action_plan_key, issue_attributes, issue_creation_date, issue_update_date, created_at, updated_at"; + + private static final String SQL_INSERT_ISSUE = "INSERT INTO issues(" + SQL_ISSUE_COLUMNS + ")" + + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + private static final String SQL_INSERT_ISSUE_CHANGE = "INSERT INTO issue_changes(kee, issue_key, user_login, change_type, change_data, created_at, updated_at)" + + " VALUES (?, ?, ?, 'comment', ?, ?, ?)"; + + private static final String SQL_DELETE_RULE_FAILURES; + + static { + StringBuilder sb = new StringBuilder("delete rule_failures where "); + for (int i = 0; i < Referentials.VIOLATION_GROUP_SIZE; i++) { + if (i > 0) { + sb.append(" or "); + } + sb.append("id=?"); + } + SQL_DELETE_RULE_FAILURES = sb.toString(); + } + + static final String SQL_SELECT_RULE_FAILURES; + + static { + StringBuilder sb = new StringBuilder("select rev.id as reviewId, s.project_id as projectId, rf.rule_id as ruleId, " + + " rf.failure_level as failureLevel, rf.message as message, rf.line as line, " + + " rf.cost as cost, rf.created_at as createdAt, rf.checksum as checksum, rev.user_id as reviewReporterId, " + + " rev.assignee_id as reviewAssigneeId, rev.status as reviewStatus, " + + " rev.severity as reviewSeverity, rev.resolution as reviewResolution, rev.manual_severity as reviewManualSeverity, " + + " rev.data as reviewData, rev.updated_at as reviewUpdatedAt, " + + " s.root_project_id as rootProjectId, rev.manual_violation as reviewManualViolation, planreviews.action_plan_id as planId " + + " from rule_failures rf " + + " inner join snapshots s on s.id=rf.snapshot_id " + + " left join reviews rev on rev.rule_failure_permanent_id=rf.permanent_id " + + " left join action_plans_reviews planreviews on planreviews.review_id=rev.id " + + " where "); + for (int i = 0; i < Referentials.VIOLATION_GROUP_SIZE; i++) { + if (i > 0) { + sb.append(" or "); + } + sb.append("rf.id=?"); + } + SQL_SELECT_RULE_FAILURES = sb.toString(); + } + + private final Database db; + private final Referentials referentials; + private final Progress progress; + + ViolationConverter(Referentials referentials, Database db, Progress progress) { + this.referentials = referentials; + this.db = db; + this.progress = progress; + } + + @Override + public Object call() throws Exception { + // For each group of 1000 violation ids: + // - load related violations, reviews and action plans + // - in a transaction + // -- insert issues + // -- insert issue_changes if there are review comments + // -- delete violations + + Long[] violationIds = referentials.pollGroupOfViolationIds(); + while (violationIds.length>0) { + List> rows = selectRows(violationIds); + convert(rows, violationIds); + + violationIds = referentials.pollGroupOfViolationIds(); + } + return null; + } + + private List> selectRows(Long[] violationIds) throws SQLException { + Connection readConnection = null; + try { + readConnection = db.getDataSource().getConnection(); + ViolationHandler violationHandler = new ViolationHandler(); + return new QueryRunner().query(readConnection, SQL_SELECT_RULE_FAILURES, violationHandler, violationIds); + + } finally { + DbUtils.closeQuietly(readConnection); + } + } + + private void convert(List> rows, Long[] violationIds) throws SQLException { + Connection readConnection = null; + Connection writeConnection = null; + try { + readConnection = db.getDataSource().getConnection(); + writeConnection = db.getDataSource().getConnection(); + writeConnection.setAutoCommit(false); + + List allParams = Lists.newArrayList(); + List> allComments = Lists.newArrayList(); + + QueryRunner runner = new QueryRunner(); + for (Map row : rows) { + Long componentId = (Long) row.get(PROJECT_ID); + if (componentId == null) { + continue; + } + String issueKey = UUID.randomUUID().toString(); + String status, severity, reporter = null; + boolean manualSeverity; + Object createdAt = Objects.firstNonNull(row.get(CREATED_AT), ONE_YEAR_AGO); + Object updatedAt; + Long reviewId = (Long) row.get(REVIEW_ID); + if (reviewId == null) { + // violation without review + status = STATUS_OPEN; + manualSeverity = false; + severity = (String) row.get(SEVERITY); + updatedAt = createdAt; + } else { + // violation + review + String reviewStatus = (String) row.get(REVIEW_STATUS); + status = (STATUS_OPEN.equals(reviewStatus) ? STATUS_CONFIRMED : reviewStatus); + manualSeverity = Objects.firstNonNull((Boolean) row.get(REVIEW_MANUAL_SEVERITY), false); + severity = (String) row.get(REVIEW_SEVERITY); + updatedAt = Objects.firstNonNull(row.get(REVIEW_UPDATED_AT), ONE_YEAR_AGO); + if ((Boolean) row.get(REVIEW_MANUAL_VIOLATION)) { + reporter = referentials.userLogin((Long) row.get(REVIEW_REPORTER_ID)); + } + + List> comments = runner.query(readConnection, ReviewCommentsHandler.SQL + reviewId, new ReviewCommentsHandler()); + for (Map comment : comments) { + comment.put(ISSUE_KEY, issueKey); + allComments.add(comment); + } + } + Object[] params = new Object[20]; + params[0] = issueKey; + params[1] = componentId; + params[2] = row.get(ROOT_PROJECT_ID); + params[3] = row.get(RULE_ID); + params[4] = severity; + params[5] = manualSeverity; + params[6] = row.get(MESSAGE); + params[7] = row.get(LINE); + params[8] = row.get(COST); + params[9] = status; + params[10] = row.get(REVIEW_RESOLUTION); + params[11] = row.get(CHECKSUM); + params[12] = reporter; + params[13] = referentials.userLogin((Long) row.get(REVIEW_ASSIGNEE_ID)); + params[14] = referentials.actionPlan((Long) row.get(PLAN_ID)); + params[15] = row.get(REVIEW_DATA); + params[16] = createdAt; + params[17] = updatedAt; + params[18] = createdAt; + params[19] = updatedAt; + allParams.add(params); + } + runner.batch(writeConnection, SQL_INSERT_ISSUE, allParams.toArray(new Object[allParams.size()][])); + insertComments(writeConnection, allComments); + runner.update(writeConnection, SQL_DELETE_RULE_FAILURES, violationIds); + writeConnection.commit(); + progress.increment(rows.size()); + + } finally { + DbUtils.closeQuietly(readConnection); + DbUtils.closeQuietly(writeConnection); + } + } + + private void insertComments(Connection writeConnection, List> comments) throws SQLException { + List allParams = Lists.newArrayList(); + + for (Map comment : comments) { + String login = referentials.userLogin((Long) comment.get(USER_ID)); + if (login != null) { + Object[] params = new Object[6]; + params[0] = UUID.randomUUID().toString(); + params[1] = comment.get(ISSUE_KEY); + params[2] = login; + params[3] = comment.get(REVIEW_TEXT); + params[4] = comment.get(CREATED_AT); + params[5] = comment.get(UPDATED_AT); + allParams.add(params); + } + } + if (!allParams.isEmpty()) { + new QueryRunner().batch(writeConnection, SQL_INSERT_ISSUE_CHANGE, allParams.toArray(new Object[allParams.size()][])); + } + } + + + private static class ReviewCommentsHandler extends AbstractListHandler> { + static final String SQL = "select created_at as createdAt, updated_at as updatedAt, user_id as userId, review_text as reviewText from review_comments where review_id="; + + @Override + protected Map handleRow(ResultSet rs) throws SQLException { + Map map = Maps.newHashMap(); + map.put(CREATED_AT, rs.getTimestamp(CREATED_AT)); + map.put(UPDATED_AT, rs.getTimestamp(UPDATED_AT)); + map.put(USER_ID, SqlUtil.getLong(rs, USER_ID)); + map.put(REVIEW_TEXT, rs.getString(REVIEW_TEXT)); + return map; + } + } + + private static class ViolationHandler extends AbstractListHandler> { + private static final Map SEVERITIES = ImmutableMap.of(1, Severity.INFO, 2, Severity.MINOR, 3, Severity.MAJOR, 4, Severity.CRITICAL, 5, Severity.BLOCKER); + + + @Override + protected Map handleRow(ResultSet rs) throws SQLException { + Map map = Maps.newHashMap(); + map.put(REVIEW_ID, SqlUtil.getLong(rs, REVIEW_ID)); + map.put(PROJECT_ID, SqlUtil.getLong(rs, PROJECT_ID)); + map.put(ROOT_PROJECT_ID, SqlUtil.getLong(rs, ROOT_PROJECT_ID)); + map.put(RULE_ID, SqlUtil.getLong(rs, RULE_ID)); + map.put(SEVERITY, Objects.firstNonNull(SEVERITIES.get(SqlUtil.getInt(rs, "failureLevel")), SEVERITY_MAJOR)); + map.put(MESSAGE, rs.getString(MESSAGE)); + map.put(LINE, SqlUtil.getInt(rs, LINE)); + map.put(COST, SqlUtil.getDouble(rs, COST)); + map.put(CHECKSUM, rs.getString(CHECKSUM)); + map.put(CREATED_AT, rs.getTimestamp(CREATED_AT)); + map.put(REVIEW_RESOLUTION, rs.getString(REVIEW_RESOLUTION)); + map.put(REVIEW_SEVERITY, Objects.firstNonNull(rs.getString(REVIEW_SEVERITY), SEVERITY_MAJOR)); + map.put(REVIEW_STATUS, rs.getString(REVIEW_STATUS)); + map.put(REVIEW_REPORTER_ID, SqlUtil.getLong(rs, REVIEW_REPORTER_ID)); + map.put(REVIEW_ASSIGNEE_ID, SqlUtil.getLong(rs, REVIEW_ASSIGNEE_ID)); + map.put(REVIEW_DATA, rs.getString(REVIEW_DATA)); + map.put(REVIEW_MANUAL_SEVERITY, rs.getBoolean(REVIEW_MANUAL_SEVERITY)); + map.put(REVIEW_UPDATED_AT, rs.getTimestamp(REVIEW_UPDATED_AT)); + map.put(REVIEW_MANUAL_VIOLATION, rs.getBoolean(REVIEW_MANUAL_VIOLATION)); + map.put(PLAN_ID, SqlUtil.getLong(rs, PLAN_ID)); + return map; + } + } +} diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationConverters.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationConverters.java new file mode 100644 index 00000000000..640903d8850 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationConverters.java @@ -0,0 +1,77 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations.violation; + +import com.google.common.collect.Lists; +import org.sonar.api.config.Settings; +import org.sonar.core.persistence.Database; + +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.*; + +class ViolationConverters { + + static final int DEFAULT_THREADS = 5; + static final String THREADS_PROPERTY = "sonar.violationMigration.threads"; + private final Settings settings; + + ViolationConverters(Settings settings) { + this.settings = settings; + } + + void execute(Referentials referentials, Database db) throws Exception { + Progress progress = new Progress(referentials.totalViolations()); + + List> converters = Lists.newArrayList(); + for (int i = 0; i < numberOfThreads(); i++) { + converters.add(new ViolationConverter(referentials, db, progress)); + } + + doExecute(progress, converters); + } + + void doExecute(TimerTask progress, List> converters) throws InterruptedException, ExecutionException { + Timer timer = new Timer(Progress.THREAD_NAME); + timer.schedule(progress, Progress.DELAY_MS, Progress.DELAY_MS); + try { + ExecutorService executor = Executors.newFixedThreadPool(converters.size()); + List> results = executor.invokeAll(converters); + executor.shutdown(); + for (Future result : results) { + result.get(); + } + } finally { + timer.cancel(); + } + } + + int numberOfThreads() { + int threads = settings.getInt(THREADS_PROPERTY); + if (threads < 0) { + throw new IllegalArgumentException(String.format("Bad value of %s: %d", THREADS_PROPERTY, threads)); + } + if (threads == 0) { + threads = DEFAULT_THREADS; + } + return threads; + } +} diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationMigration.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationMigration.java new file mode 100644 index 00000000000..88c60a8e815 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/ViolationMigration.java @@ -0,0 +1,75 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations.violation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.config.Settings; +import org.sonar.api.utils.MessageException; +import org.sonar.core.persistence.Database; +import org.sonar.server.db.migrations.DatabaseMigration; + +import java.sql.SQLException; + +/** + * Used in the Active Record Migration 401 + */ +public class ViolationMigration implements DatabaseMigration { + + private final Settings settings; + + private Logger logger = LoggerFactory.getLogger(ViolationMigration.class); + private final Database db; + + public ViolationMigration(Database database, Settings settings) { + this.db = database; + this.settings = settings; + } + + @Override + public void execute() { + try { + migrate(); + + } catch (SQLException e) { + logger.error("Fail to convert violations to issues", e); + SqlUtil.log(logger, e); + throw MessageException.of("Fail to convert violations to issues"); + + } catch (Exception e) { + logger.error("Fail to convert violations to issues", e); + throw MessageException.of("Fail to convert violations to issues"); + } + } + + public void migrate() throws Exception { + logger.info("Initialize input"); + Referentials referentials = new Referentials(db); + + if (referentials.totalViolations() > 0) { + logger.info("Migrate {} violations", referentials.totalViolations()); + + ViolationConverters converters = new ViolationConverters(settings); + converters.execute(referentials, db); + } + } + + +} diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/package-info.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/package-info.java new file mode 100644 index 00000000000..969ef6eed7e --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/violation/package-info.java @@ -0,0 +1,24 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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. + */ + +@ParametersAreNonnullByDefault +package org.sonar.server.db.migrations.violation; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java index 55f07767e23..e6cabb36702 100644 --- a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java +++ b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java @@ -81,7 +81,9 @@ import org.sonar.server.component.DefaultComponentFinder; import org.sonar.server.component.DefaultRubyComponentService; import org.sonar.server.configuration.Backup; import org.sonar.server.configuration.ProfilesManager; -import org.sonar.server.db.DatabaseMigrator; +import org.sonar.server.db.migrations.DatabaseMigration; +import org.sonar.server.db.migrations.DatabaseMigrations; +import org.sonar.server.db.migrations.DatabaseMigrator; import org.sonar.server.db.EmbeddedDatabaseFactory; import org.sonar.server.issue.ActionPlanService; import org.sonar.server.issue.ActionService; @@ -224,6 +226,9 @@ public final class Platform { rootContainer.addSingleton(MyBatis.class); rootContainer.addSingleton(DefaultServerUpgradeStatus.class); rootContainer.addSingleton(DatabaseServerCompatibility.class); + for (Class migrationClass : DatabaseMigrations.CLASSES) { + rootContainer.addSingleton(migrationClass); + } rootContainer.addSingleton(DatabaseMigrator.class); rootContainer.addSingleton(DatabaseVersion.class); for (Class daoClass : DaoUtils.getDaoClasses()) { diff --git a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java index 340eb9ef3a1..807cb564be7 100644 --- a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java +++ b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java @@ -57,7 +57,7 @@ import org.sonar.core.resource.ResourceKeyUpdaterDao; import org.sonar.core.timemachine.Periods; import org.sonar.server.configuration.Backup; import org.sonar.server.configuration.ProfilesManager; -import org.sonar.server.db.DatabaseMigrator; +import org.sonar.server.db.migrations.DatabaseMigrator; import org.sonar.server.platform.Platform; import org.sonar.server.platform.ServerIdGenerator; import org.sonar.server.platform.ServerSettings; diff --git a/sonar-server/src/main/webapp/WEB-INF/config/environment.rb b/sonar-server/src/main/webapp/WEB-INF/config/environment.rb index 3d6ea3cf3d1..32cfe4c771e 100644 --- a/sonar-server/src/main/webapp/WEB-INF/config/environment.rb +++ b/sonar-server/src/main/webapp/WEB-INF/config/environment.rb @@ -163,6 +163,11 @@ class ActiveRecord::Migration execute_ddl("drop trigger #{trigger_name}", "DROP TRIGGER #{trigger_name}") end + def self.write(text="") + # See migration.rb, the method write directly calls "puts" + Java::OrgSlf4j::LoggerFactory::getLogger('DbMigration').info(text) if verbose + end + private def self.execute_ddl(message, ddl) diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/401_migrate_violations_to_issues.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/401_migrate_violations_to_issues.rb index f1e7a5cd349..20bb8d034a6 100644 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/401_migrate_violations_to_issues.rb +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/401_migrate_violations_to_issues.rb @@ -25,7 +25,7 @@ class MigrateViolationsToIssues < ActiveRecord::Migration def self.up - Java::OrgSonarServerUi::JRubyFacade.getInstance().databaseMigrator().executeMigration('org.sonar.server.db.migrations.ConvertViolationsToIssues') + Java::OrgSonarServerUi::JRubyFacade.getInstance().databaseMigrator().executeMigration('org.sonar.server.db.migrations.violation.ViolationMigration') # Currently not possible in Java because of Oracle (triggers and sequences must be dropped) drop_table('rule_failures') @@ -34,4 +34,4 @@ class MigrateViolationsToIssues < ActiveRecord::Migration drop_table('action_plans_reviews') end -end \ No newline at end of file +end diff --git a/sonar-server/src/test/java/org/sonar/server/db/DatabaseMigratorTest.java b/sonar-server/src/test/java/org/sonar/server/db/DatabaseMigratorTest.java deleted file mode 100644 index 2cc20b3c5e3..00000000000 --- a/sonar-server/src/test/java/org/sonar/server/db/DatabaseMigratorTest.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2013 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube 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. - * - * SonarQube 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.db; - -import org.apache.ibatis.session.SqlSession; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.sonar.core.persistence.AbstractDaoTestCase; -import org.sonar.core.persistence.Database; -import org.sonar.core.persistence.MyBatis; -import org.sonar.core.persistence.dialect.Dialect; -import org.sonar.core.persistence.dialect.H2; -import org.sonar.core.persistence.dialect.MySql; - -import java.sql.Connection; - -import static org.fest.assertions.Assertions.assertThat; -import static org.mockito.Mockito.*; - -public class DatabaseMigratorTest extends AbstractDaoTestCase { - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - MyBatis mybatis = mock(MyBatis.class); - Database database = mock(Database.class); - - @Test - public void should_support_only_creation_of_h2_database() throws Exception { - when(database.getDialect()).thenReturn(new MySql()); - - DatabaseMigrator migrator = new DatabaseMigrator(mybatis, database); - - assertThat(migrator.createDatabase()).isFalse(); - verifyZeroInteractions(mybatis); - } - - @Test - public void fail_if_execute_unknown_migration() throws Exception { - thrown.expect(IllegalStateException.class); - thrown.expectMessage("Fail to execute database migration: org.xxx.UnknownMigration"); - - DatabaseMigrator migrator = new DatabaseMigrator(mybatis, database); - migrator.executeMigration("org.xxx.UnknownMigration"); - } - - @Test - public void fail_if_execute_not_a_migration() throws Exception { - thrown.expect(IllegalStateException.class); - thrown.expectMessage("Fail to execute database migration: java.lang.String"); - - DatabaseMigrator migrator = new DatabaseMigrator(mybatis, database); - migrator.executeMigration("java.lang.String"); - } - - @Test - public void execute_migration() throws Exception { - DatabaseMigrator migrator = new DatabaseMigrator(mybatis, database); - assertThat(FakeMigration.executed).isFalse(); - migrator.executeMigration(FakeMigration.class.getName()); - assertThat(FakeMigration.executed).isTrue(); - } - - @Test - public void should_create_schema_on_h2() throws Exception { - - Dialect supportedDialect = new H2(); - when(database.getDialect()).thenReturn(supportedDialect); - Connection connection = mock(Connection.class); - SqlSession session = mock(SqlSession.class); - when(session.getConnection()).thenReturn(connection); - when(mybatis.openSession()).thenReturn(session); - - DatabaseMigrator databaseMigrator = new DatabaseMigrator(mybatis, database) { - @Override - protected void createSchema(Connection connection, String dialectId) { - } - }; - - assertThat(databaseMigrator.createDatabase()).isTrue(); - } - - public static class FakeMigration implements DatabaseMigration { - static boolean executed = false; - @Override - public void execute(Database db) { - executed = true; - } - } -} diff --git a/sonar-server/src/test/java/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest.java b/sonar-server/src/test/java/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest.java deleted file mode 100644 index c2ab498add3..00000000000 --- a/sonar-server/src/test/java/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2013 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube 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. - * - * SonarQube 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.db.migrations; - -import org.junit.Rule; -import org.junit.Test; -import org.sonar.core.persistence.TestDatabase; - -public class ConvertViolationsToIssuesTest { - - @Rule - public TestDatabase db = new TestDatabase().schema(getClass(), "schema.sql"); - - @Test - public void convert_violations() throws Exception { - db.prepareDbUnit(getClass(), "convert_violations.xml"); - - new ConvertViolationsToIssues().execute(db.database()); - - db.assertDbUnit(getClass(), "convert_violations_result.xml", "issues", "issue_changes"); - } -} diff --git a/sonar-server/src/test/java/org/sonar/server/db/migrations/DatabaseMigratorTest.java b/sonar-server/src/test/java/org/sonar/server/db/migrations/DatabaseMigratorTest.java new file mode 100644 index 00000000000..a8e3d06d87e --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/db/migrations/DatabaseMigratorTest.java @@ -0,0 +1,100 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations; + +import org.apache.ibatis.session.SqlSession; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.core.persistence.AbstractDaoTestCase; +import org.sonar.core.persistence.Database; +import org.sonar.core.persistence.MyBatis; +import org.sonar.core.persistence.dialect.Dialect; +import org.sonar.core.persistence.dialect.H2; +import org.sonar.core.persistence.dialect.MySql; + +import java.sql.Connection; + +import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Mockito.*; + +public class DatabaseMigratorTest extends AbstractDaoTestCase { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + MyBatis mybatis = mock(MyBatis.class); + Database database = mock(Database.class); + DatabaseMigration[] migrations = new DatabaseMigration[]{new FakeMigration()}; + + @Test + public void should_support_only_creation_of_h2_database() throws Exception { + when(database.getDialect()).thenReturn(new MySql()); + + DatabaseMigrator migrator = new DatabaseMigrator(mybatis, database, migrations); + + assertThat(migrator.createDatabase()).isFalse(); + verifyZeroInteractions(mybatis); + } + + @Test + public void fail_if_execute_unknown_migration() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Database migration not found: org.xxx.UnknownMigration"); + + DatabaseMigrator migrator = new DatabaseMigrator(mybatis, database, migrations); + migrator.executeMigration("org.xxx.UnknownMigration"); + } + + @Test + public void execute_migration() throws Exception { + DatabaseMigrator migrator = new DatabaseMigrator(mybatis, database, migrations); + assertThat(FakeMigration.executed).isFalse(); + migrator.executeMigration(FakeMigration.class.getName()); + assertThat(FakeMigration.executed).isTrue(); + } + + @Test + public void should_create_schema_on_h2() throws Exception { + + Dialect supportedDialect = new H2(); + when(database.getDialect()).thenReturn(supportedDialect); + Connection connection = mock(Connection.class); + SqlSession session = mock(SqlSession.class); + when(session.getConnection()).thenReturn(connection); + when(mybatis.openSession()).thenReturn(session); + + DatabaseMigrator databaseMigrator = new DatabaseMigrator(mybatis, database, migrations) { + @Override + protected void createSchema(Connection connection, String dialectId) { + } + }; + + assertThat(databaseMigrator.createDatabase()).isTrue(); + } + + public static class FakeMigration implements DatabaseMigration { + static boolean executed = false; + @Override + public void execute() { + executed = true; + } + } +} diff --git a/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ProgressTest.java b/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ProgressTest.java new file mode 100644 index 00000000000..87c0e2fadb1 --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ProgressTest.java @@ -0,0 +1,64 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations.violation; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.slf4j.Logger; + +import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class ProgressTest { + @Test + public void log_progress() throws Exception { + Logger logger = mock(Logger.class); + ArgumentCaptor argument = ArgumentCaptor.forClass(String.class); + + Progress progress = new Progress(5000, logger, System.currentTimeMillis()); + progress.run(); + progress.increment(200); + progress.increment(130); + progress.run(); + progress.increment(1670); + progress.run(); + + verify(logger, times(3)).info(argument.capture()); + assertThat(argument.getAllValues().get(0)).isEqualTo("0% [0/5000 violations]"); + assertThat(argument.getAllValues().get(1)).isEqualTo("6% [330/5000 violations]"); + assertThat(argument.getAllValues().get(2)).isEqualTo("40% [2000/5000 violations]"); + } + + @Test + public void log_remaining_time() throws Exception { + Logger logger = mock(Logger.class); + ArgumentCaptor argument = ArgumentCaptor.forClass(String.class); + + long fiveMinutesAgo = System.currentTimeMillis() - 5 * 60 * 1000; + Progress progress = new Progress(5000, logger, fiveMinutesAgo); + progress.increment(2000); + progress.run(); + + verify(logger).info(argument.capture()); + assertThat(argument.getValue()).isEqualTo("40% [2000/5000 violations, 7 minutes remaining]"); + } +} diff --git a/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/SqlUtilTest.java b/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/SqlUtilTest.java new file mode 100644 index 00000000000..69c15933875 --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/SqlUtilTest.java @@ -0,0 +1,43 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations.violation; + +import org.junit.Test; +import org.slf4j.Logger; + +import java.sql.SQLException; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class SqlUtilTest { + + @Test + public void log_all_sql_exceptions() { + SQLException root = new SQLException("this is root", "123"); + SQLException next = new SQLException("this is next", "456"); + root.setNextException(next); + + Logger logger = mock(Logger.class); + SqlUtil.log(logger, root); + + verify(logger).error("SQL error: {}. Message: {}", "456", "this is next"); + } +} diff --git a/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ViolationConvertersTest.java b/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ViolationConvertersTest.java new file mode 100644 index 00000000000..9b144fe7846 --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ViolationConvertersTest.java @@ -0,0 +1,82 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations.violation; + +import com.google.common.collect.Lists; +import org.junit.Test; +import org.sonar.api.config.Settings; + +import java.util.List; +import java.util.TimerTask; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ViolationConvertersTest { + + @Test + public void default_number_of_threads() throws Exception { + assertThat(new ViolationConverters(new Settings()).numberOfThreads()).isEqualTo(ViolationConverters.DEFAULT_THREADS); + } + + @Test + public void configure_number_of_threads() throws Exception { + Settings settings = new Settings(); + settings.setProperty(ViolationConverters.THREADS_PROPERTY, 2); + assertThat(new ViolationConverters(settings).numberOfThreads()).isEqualTo(2); + } + + @Test + public void number_of_threads_should_not_be_negative() throws Exception { + try { + Settings settings = new Settings(); + settings.setProperty(ViolationConverters.THREADS_PROPERTY, -2); + new ViolationConverters(settings).numberOfThreads(); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).isEqualTo("Bad value of " + ViolationConverters.THREADS_PROPERTY + ": -2"); + } + } + + @Test + public void propagate_converter_failure() throws Exception { + Callable callable = mock(Callable.class); + when(callable.call()).thenThrow(new IllegalStateException("Need to cry")); + + List> callables = Lists.newArrayList(callable); + try { + new ViolationConverters(new Settings()).doExecute(new FakeTimerTask(), callables); + fail(); + } catch (ExecutionException e) { + assertThat(e.getCause().getMessage()).isEqualTo("Need to cry"); + } + + } + + static class FakeTimerTask extends TimerTask { + @Override + public void run() { + } + } +} diff --git a/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ViolationMigrationTest.java b/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ViolationMigrationTest.java new file mode 100644 index 00000000000..360e699a802 --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/db/migrations/violation/ViolationMigrationTest.java @@ -0,0 +1,66 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations.violation; + +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.config.Settings; +import org.sonar.core.persistence.TestDatabase; + +import java.util.Set; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; + +public class ViolationMigrationTest { + + @Rule + public TestDatabase db = new TestDatabase().schema(getClass(), "schema.sql"); + + @Test + public void migrate_violations() throws Exception { + db.prepareDbUnit(getClass(), "migrate_violations.xml"); + + new ViolationMigration(db.database(), new Settings()).execute(); + + db.assertDbUnit(getClass(), "migrate_violations_result.xml", "issues", "issue_changes"); + assertMigrationEnded(); + } + + @Test + public void no_violations_to_migrate() throws Exception { + db.prepareDbUnit(getClass(), "no_violations_to_migrate.xml"); + + new ViolationMigration(db.database(), new Settings()).execute(); + + db.assertDbUnit(getClass(), "no_violations_to_migrate_result.xml", "issues", "issue_changes"); + assertMigrationEnded(); + } + + private void assertMigrationEnded() { + assertThat(db.count("select count(id) from rule_failures")).isEqualTo(0); + + // Progress thread is dead + Set threads = Thread.getAllStackTraces().keySet(); + for (Thread thread : threads) { + assertThat(thread.getName()).isNotEqualTo(Progress.THREAD_NAME); + } + } +} diff --git a/sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/convert_violations.xml b/sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/convert_violations.xml deleted file mode 100644 index 5a97e0df76e..00000000000 --- a/sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/convert_violations.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/convert_violations_result.xml b/sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/convert_violations_result.xml deleted file mode 100644 index 8c0330bcb15..00000000000 --- a/sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/convert_violations_result.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - diff --git a/sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/schema.sql b/sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/schema.sql deleted file mode 100644 index eb8d0511291..00000000000 --- a/sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/schema.sql +++ /dev/null @@ -1,152 +0,0 @@ --- 3.5 - -CREATE TABLE "SNAPSHOTS" ( - "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), - "CREATED_AT" TIMESTAMP, - "BUILD_DATE" TIMESTAMP, - "PROJECT_ID" INTEGER NOT NULL, - "PARENT_SNAPSHOT_ID" INTEGER, - "STATUS" VARCHAR(4) NOT NULL DEFAULT 'U', - "PURGE_STATUS" INTEGER, - "ISLAST" BOOLEAN NOT NULL DEFAULT FALSE, - "SCOPE" VARCHAR(3), - "QUALIFIER" VARCHAR(10), - "ROOT_SNAPSHOT_ID" INTEGER, - "VERSION" VARCHAR(500), - "PATH" VARCHAR(500), - "DEPTH" INTEGER, - "ROOT_PROJECT_ID" INTEGER, - "PERIOD1_MODE" VARCHAR(100), - "PERIOD1_PARAM" VARCHAR(100), - "PERIOD1_DATE" TIMESTAMP, - "PERIOD2_MODE" VARCHAR(100), - "PERIOD2_PARAM" VARCHAR(100), - "PERIOD2_DATE" TIMESTAMP, - "PERIOD3_MODE" VARCHAR(100), - "PERIOD3_PARAM" VARCHAR(100), - "PERIOD3_DATE" TIMESTAMP, - "PERIOD4_MODE" VARCHAR(100), - "PERIOD4_PARAM" VARCHAR(100), - "PERIOD4_DATE" TIMESTAMP, - "PERIOD5_MODE" VARCHAR(100), - "PERIOD5_PARAM" VARCHAR(100), - "PERIOD5_DATE" TIMESTAMP -); - -CREATE TABLE "USERS" ( - "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), - "LOGIN" VARCHAR(40), - "NAME" VARCHAR(200), - "EMAIL" VARCHAR(100), - "CRYPTED_PASSWORD" VARCHAR(40), - "SALT" VARCHAR(40), - "CREATED_AT" TIMESTAMP, - "UPDATED_AT" TIMESTAMP, - "REMEMBER_TOKEN" VARCHAR(500), - "REMEMBER_TOKEN_EXPIRES_AT" TIMESTAMP, - "ACTIVE" BOOLEAN DEFAULT TRUE -); - - -CREATE TABLE "RULE_FAILURES" ( - "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), - "SNAPSHOT_ID" INTEGER NOT NULL, - "RULE_ID" INTEGER NOT NULL, - "FAILURE_LEVEL" INTEGER NOT NULL, - "MESSAGE" VARCHAR(4000), - "LINE" INTEGER, - "COST" DOUBLE, - "CREATED_AT" TIMESTAMP, - "CHECKSUM" VARCHAR(1000), - "PERMANENT_ID" INTEGER, - "SWITCHED_OFF" BOOLEAN, - "PERSON_ID" INTEGER -); - -CREATE TABLE "ACTION_PLANS" ( - "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), - "KEE" VARCHAR(100), - "NAME" VARCHAR(200), - "DESCRIPTION" VARCHAR(1000), - "DEADLINE" TIMESTAMP, - "USER_LOGIN" VARCHAR(40), - "PROJECT_ID" INTEGER, - "STATUS" VARCHAR(10), - "CREATED_AT" TIMESTAMP, - "UPDATED_AT" TIMESTAMP -); - -CREATE TABLE "ACTION_PLANS_REVIEWS" ( - "ACTION_PLAN_ID" INTEGER, - "REVIEW_ID" INTEGER -); - -CREATE TABLE "REVIEWS" ( - "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), - "CREATED_AT" TIMESTAMP, - "UPDATED_AT" TIMESTAMP, - "USER_ID" INTEGER, - "ASSIGNEE_ID" INTEGER, - "TITLE" VARCHAR(500), - "STATUS" VARCHAR(10), - "SEVERITY" VARCHAR(10), - "RULE_FAILURE_PERMANENT_ID" INTEGER, - "PROJECT_ID" INTEGER, - "RESOURCE_ID" INTEGER, - "RESOURCE_LINE" INTEGER, - "RESOLUTION" VARCHAR(200), - "RULE_ID" INTEGER, - "MANUAL_VIOLATION" BOOLEAN NOT NULL, - "MANUAL_SEVERITY" BOOLEAN NOT NULL, - "DATA" VARCHAR(4000) -); - -CREATE TABLE "REVIEW_COMMENTS" ( - "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), - "CREATED_AT" TIMESTAMP, - "UPDATED_AT" TIMESTAMP, - "REVIEW_ID" INTEGER, - "USER_ID" INTEGER, - "REVIEW_TEXT" VARCHAR(16777215) -); - - - --- 3.6 - -CREATE TABLE "ISSUES" ( - "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), - "KEE" VARCHAR(50) UNIQUE NOT NULL, - "COMPONENT_ID" INTEGER NOT NULL, - "ROOT_COMPONENT_ID" INTEGER, - "RULE_ID" INTEGER, - "SEVERITY" VARCHAR(10), - "MANUAL_SEVERITY" BOOLEAN NOT NULL, - "MESSAGE" VARCHAR(4000), - "LINE" INTEGER, - "EFFORT_TO_FIX" DOUBLE, - "STATUS" VARCHAR(20), - "RESOLUTION" VARCHAR(20), - "CHECKSUM" VARCHAR(1000), - "REPORTER" VARCHAR(40), - "ASSIGNEE" VARCHAR(40), - "AUTHOR_LOGIN" VARCHAR(100), - "ACTION_PLAN_KEY" VARCHAR(50) NULL, - "ISSUE_ATTRIBUTES" VARCHAR(4000), - "ISSUE_CREATION_DATE" TIMESTAMP, - "ISSUE_CLOSE_DATE" TIMESTAMP, - "ISSUE_UPDATE_DATE" TIMESTAMP, - "CREATED_AT" TIMESTAMP, - "UPDATED_AT" TIMESTAMP -); - -CREATE TABLE "ISSUE_CHANGES" ( - "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), - "KEE" VARCHAR(50), - "ISSUE_KEY" VARCHAR(50) NOT NULL, - "USER_LOGIN" VARCHAR(40), - "CHANGE_TYPE" VARCHAR(40), - "CHANGE_DATA" VARCHAR(16777215), - "CREATED_AT" TIMESTAMP, - "UPDATED_AT" TIMESTAMP, -); \ No newline at end of file diff --git a/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/migrate_violations.xml b/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/migrate_violations.xml new file mode 100644 index 00000000000..0d4a6e8ebc0 --- /dev/null +++ b/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/migrate_violations.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/migrate_violations_result.xml b/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/migrate_violations_result.xml new file mode 100644 index 00000000000..cafbaaf179e --- /dev/null +++ b/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/migrate_violations_result.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/no_violations_to_migrate.xml b/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/no_violations_to_migrate.xml new file mode 100644 index 00000000000..416efbc3086 --- /dev/null +++ b/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/no_violations_to_migrate.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/no_violations_to_migrate_result.xml b/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/no_violations_to_migrate_result.xml new file mode 100644 index 00000000000..416efbc3086 --- /dev/null +++ b/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/no_violations_to_migrate_result.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/schema.sql b/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/schema.sql new file mode 100644 index 00000000000..eb8d0511291 --- /dev/null +++ b/sonar-server/src/test/resources/org/sonar/server/db/migrations/violation/ViolationMigrationTest/schema.sql @@ -0,0 +1,152 @@ +-- 3.5 + +CREATE TABLE "SNAPSHOTS" ( + "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), + "CREATED_AT" TIMESTAMP, + "BUILD_DATE" TIMESTAMP, + "PROJECT_ID" INTEGER NOT NULL, + "PARENT_SNAPSHOT_ID" INTEGER, + "STATUS" VARCHAR(4) NOT NULL DEFAULT 'U', + "PURGE_STATUS" INTEGER, + "ISLAST" BOOLEAN NOT NULL DEFAULT FALSE, + "SCOPE" VARCHAR(3), + "QUALIFIER" VARCHAR(10), + "ROOT_SNAPSHOT_ID" INTEGER, + "VERSION" VARCHAR(500), + "PATH" VARCHAR(500), + "DEPTH" INTEGER, + "ROOT_PROJECT_ID" INTEGER, + "PERIOD1_MODE" VARCHAR(100), + "PERIOD1_PARAM" VARCHAR(100), + "PERIOD1_DATE" TIMESTAMP, + "PERIOD2_MODE" VARCHAR(100), + "PERIOD2_PARAM" VARCHAR(100), + "PERIOD2_DATE" TIMESTAMP, + "PERIOD3_MODE" VARCHAR(100), + "PERIOD3_PARAM" VARCHAR(100), + "PERIOD3_DATE" TIMESTAMP, + "PERIOD4_MODE" VARCHAR(100), + "PERIOD4_PARAM" VARCHAR(100), + "PERIOD4_DATE" TIMESTAMP, + "PERIOD5_MODE" VARCHAR(100), + "PERIOD5_PARAM" VARCHAR(100), + "PERIOD5_DATE" TIMESTAMP +); + +CREATE TABLE "USERS" ( + "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), + "LOGIN" VARCHAR(40), + "NAME" VARCHAR(200), + "EMAIL" VARCHAR(100), + "CRYPTED_PASSWORD" VARCHAR(40), + "SALT" VARCHAR(40), + "CREATED_AT" TIMESTAMP, + "UPDATED_AT" TIMESTAMP, + "REMEMBER_TOKEN" VARCHAR(500), + "REMEMBER_TOKEN_EXPIRES_AT" TIMESTAMP, + "ACTIVE" BOOLEAN DEFAULT TRUE +); + + +CREATE TABLE "RULE_FAILURES" ( + "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), + "SNAPSHOT_ID" INTEGER NOT NULL, + "RULE_ID" INTEGER NOT NULL, + "FAILURE_LEVEL" INTEGER NOT NULL, + "MESSAGE" VARCHAR(4000), + "LINE" INTEGER, + "COST" DOUBLE, + "CREATED_AT" TIMESTAMP, + "CHECKSUM" VARCHAR(1000), + "PERMANENT_ID" INTEGER, + "SWITCHED_OFF" BOOLEAN, + "PERSON_ID" INTEGER +); + +CREATE TABLE "ACTION_PLANS" ( + "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), + "KEE" VARCHAR(100), + "NAME" VARCHAR(200), + "DESCRIPTION" VARCHAR(1000), + "DEADLINE" TIMESTAMP, + "USER_LOGIN" VARCHAR(40), + "PROJECT_ID" INTEGER, + "STATUS" VARCHAR(10), + "CREATED_AT" TIMESTAMP, + "UPDATED_AT" TIMESTAMP +); + +CREATE TABLE "ACTION_PLANS_REVIEWS" ( + "ACTION_PLAN_ID" INTEGER, + "REVIEW_ID" INTEGER +); + +CREATE TABLE "REVIEWS" ( + "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), + "CREATED_AT" TIMESTAMP, + "UPDATED_AT" TIMESTAMP, + "USER_ID" INTEGER, + "ASSIGNEE_ID" INTEGER, + "TITLE" VARCHAR(500), + "STATUS" VARCHAR(10), + "SEVERITY" VARCHAR(10), + "RULE_FAILURE_PERMANENT_ID" INTEGER, + "PROJECT_ID" INTEGER, + "RESOURCE_ID" INTEGER, + "RESOURCE_LINE" INTEGER, + "RESOLUTION" VARCHAR(200), + "RULE_ID" INTEGER, + "MANUAL_VIOLATION" BOOLEAN NOT NULL, + "MANUAL_SEVERITY" BOOLEAN NOT NULL, + "DATA" VARCHAR(4000) +); + +CREATE TABLE "REVIEW_COMMENTS" ( + "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), + "CREATED_AT" TIMESTAMP, + "UPDATED_AT" TIMESTAMP, + "REVIEW_ID" INTEGER, + "USER_ID" INTEGER, + "REVIEW_TEXT" VARCHAR(16777215) +); + + + +-- 3.6 + +CREATE TABLE "ISSUES" ( + "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), + "KEE" VARCHAR(50) UNIQUE NOT NULL, + "COMPONENT_ID" INTEGER NOT NULL, + "ROOT_COMPONENT_ID" INTEGER, + "RULE_ID" INTEGER, + "SEVERITY" VARCHAR(10), + "MANUAL_SEVERITY" BOOLEAN NOT NULL, + "MESSAGE" VARCHAR(4000), + "LINE" INTEGER, + "EFFORT_TO_FIX" DOUBLE, + "STATUS" VARCHAR(20), + "RESOLUTION" VARCHAR(20), + "CHECKSUM" VARCHAR(1000), + "REPORTER" VARCHAR(40), + "ASSIGNEE" VARCHAR(40), + "AUTHOR_LOGIN" VARCHAR(100), + "ACTION_PLAN_KEY" VARCHAR(50) NULL, + "ISSUE_ATTRIBUTES" VARCHAR(4000), + "ISSUE_CREATION_DATE" TIMESTAMP, + "ISSUE_CLOSE_DATE" TIMESTAMP, + "ISSUE_UPDATE_DATE" TIMESTAMP, + "CREATED_AT" TIMESTAMP, + "UPDATED_AT" TIMESTAMP +); + +CREATE TABLE "ISSUE_CHANGES" ( + "ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), + "KEE" VARCHAR(50), + "ISSUE_KEY" VARCHAR(50) NOT NULL, + "USER_LOGIN" VARCHAR(40), + "CHANGE_TYPE" VARCHAR(40), + "CHANGE_DATA" VARCHAR(16777215), + "CREATED_AT" TIMESTAMP, + "UPDATED_AT" TIMESTAMP, +); \ No newline at end of file