]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-4470 Performance issue when migrating the Sonar DB from version 3.5 to version...
authorSimon Brandhof <simon.brandhof@gmail.com>
Fri, 5 Jul 2013 15:21:54 +0000 (17:21 +0200)
committerSimon Brandhof <simon.brandhof@gmail.com>
Fri, 5 Jul 2013 15:21:54 +0000 (17:21 +0200)
pom.xml
sonar-core/src/main/java/org/sonar/core/persistence/DatabaseMigrator.java [deleted file]
sonar-server/pom.xml
sonar-server/src/main/java/org/sonar/server/db/DatabaseMigration.java [new file with mode: 0644]
sonar-server/src/main/java/org/sonar/server/db/DatabaseMigrator.java [new file with mode: 0644]
sonar-server/src/main/java/org/sonar/server/db/migrations/ConvertViolationsToIssues.java [new file with mode: 0644]
sonar-server/src/main/java/org/sonar/server/platform/Platform.java
sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java
sonar-server/src/main/webapp/WEB-INF/db/migrate/401_migrate_violations_to_issues.rb
sonar-server/src/main/webapp/WEB-INF/lib/database_version.rb

diff --git a/pom.xml b/pom.xml
index e72f145243aa7a42178209f922675ccf82bed506..b4e6e19d0171e8cd71fc4d1857afa80b919276e7 100644 (file)
--- a/pom.xml
+++ b/pom.xml
         <artifactId>commons-collections</artifactId>
         <version>3.2.1</version>
       </dependency>
+      <dependency>
+        <groupId>commons-dbutils</groupId>
+        <artifactId>commons-dbutils</artifactId>
+        <version>1.5</version>
+      </dependency>
       <dependency>
         <groupId>com.google.guava</groupId>
         <artifactId>guava</artifactId>
diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseMigrator.java b/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseMigrator.java
deleted file mode 100644 (file)
index b15db5f..0000000
+++ /dev/null
@@ -1,68 +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.core.persistence;
-
-import org.apache.ibatis.session.SqlSession;
-import org.slf4j.LoggerFactory;
-import org.sonar.api.ServerComponent;
-
-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 MyBatis myBatis;
-  private 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();
-      DdlUtils.createSchema(connection, database.getDialect().getId());
-    } finally {
-      MyBatis.closeQuietly(session);
-
-      // The connection is probably already closed by session.close()
-      // but it's not documented in mybatis javadoc.
-      DatabaseUtils.closeQuietly(connection);
-    }
-    return true;
-  }
-}
index 1917e3a90e9bc0c0ce641c267f6ddc2520862e52..299e22e7e0ae023ff3fe69b83b66aefbf1525c73 100644 (file)
       <groupId>commons-collections</groupId>
       <artifactId>commons-collections</artifactId>
     </dependency>
+    <dependency>
+      <groupId>commons-dbutils</groupId>
+      <artifactId>commons-dbutils</artifactId>
+    </dependency>
     <dependency>
       <groupId>commons-io</groupId>
       <artifactId>commons-io</artifactId>
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
new file mode 100644 (file)
index 0000000..4e66147
--- /dev/null
@@ -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;
+
+import org.sonar.core.persistence.Database;
+
+/**
+ * Java alternative of ActiveRecord::Migration.
+ *
+ * @since 3.7
+ */
+public interface DatabaseMigration {
+
+  void execute(Database db);
+
+}
\ No newline at end of file
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
new file mode 100644 (file)
index 0000000..0c1691b
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * 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.slf4j.LoggerFactory;
+import org.sonar.api.ServerComponent;
+import org.sonar.core.persistence.Database;
+import org.sonar.core.persistence.DatabaseUtils;
+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 MyBatis myBatis;
+  private 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();
+      DdlUtils.createSchema(connection, database.getDialect().getId());
+    } finally {
+      MyBatis.closeQuietly(session);
+
+      // The connection is probably already closed by session.close()
+      // but it's not documented in mybatis javadoc.
+      DatabaseUtils.closeQuietly(connection);
+    }
+    return true;
+  }
+
+  public void executeMigration(String className) {
+    try {
+      Class<DatabaseMigration> migrationClass = (Class<DatabaseMigration>)Class.forName(className);
+      DatabaseMigration migration = migrationClass.newInstance();
+      migration.execute(database);
+
+    } catch (Exception e) {
+      e.printStackTrace();
+      throw new IllegalStateException("Fail to execute database migration: " + className, e);
+    }
+  }
+}
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
new file mode 100644 (file)
index 0000000..7fcec67
--- /dev/null
@@ -0,0 +1,373 @@
+/*
+ * 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.apache.commons.lang.time.DateUtils;
+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.ResultSet;
+import java.sql.SQLException;
+import java.util.*;
+
+/**
+ * Used in the Active Record Migration 401
+ */
+public class ConvertViolationsToIssues implements DatabaseMigration {
+
+  private static final int GROUP_SIZE = 500;
+  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 String insertSql;
+    private Date oneYearAgo = DateUtils.addYears(new Date(), -1);
+    private QueryRunner runner;
+    private Connection readConnection, writeConnection;
+    private Map<Long, String> loginsByUserId;
+    private Map<Long, String> 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) {
+      if (Oracle.ID.equals(database.getDialect().getId())) {
+        insertSql = "INSERT INTO issues(id, 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) " +
+          " VALUES (issues_seq.nextval, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+      } else {
+        insertSql = "INSERT INTO issues(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) " +
+          " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+      }
+    }
+
+
+    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<Long, String> selectLongString(String sql) throws SQLException {
+      return runner.query(readConnection, sql, new ResultSetHandler<Map<Long, String>>() {
+        @Override
+        public Map<Long, String> handle(ResultSet rs) throws SQLException {
+          Map<Long, String> 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<Map<String, Object>> rows = runner.query(readConnection, ViolationHandler.SQL, new ViolationHandler(), violationIds);
+      List<Object[]> allParams = Lists.newArrayList();
+      List<Map<String, Object>> allComments = Lists.newArrayList();
+
+      for (Map<String, Object> row : rows) {
+        Long componentId = (Long) row.get("projectId");
+        if (componentId == null) {
+          continue;
+        }
+        String issueKey = UUID.randomUUID().toString();
+        String status, severity, reporter = null;
+        boolean manualSeverity;
+        Date createdAt = Objects.firstNonNull((Date) row.get("createdAt"), oneYearAgo);
+        Date updatedAt;
+        Long reviewId = (Long) row.get("reviewId");
+        if (reviewId == null) {
+          // violation without review
+          status = "OPEN";
+          manualSeverity = false;
+          severity = (String) row.get("severity");
+          updatedAt = createdAt;
+        } else {
+          // violation + review
+          String reviewStatus = (String) row.get("reviewStatus");
+          status = ("OPEN".equals(reviewStatus) ? "CONFIRMED" : reviewStatus);
+          manualSeverity = Objects.firstNonNull((Boolean) row.get("reviewManualSeverity"), false);
+          severity = (String) row.get("reviewSeverity");
+          updatedAt = Objects.firstNonNull((Date) row.get("reviewUpdatedAt"), oneYearAgo);
+          if ((Boolean) row.get("reviewManualViolation")) {
+            reporter = login((Long) row.get("reviewReporterId"));
+          }
+
+          List<Map<String, Object>> comments = runner.query(readConnection, ReviewCommentsHandler.SQL + reviewId, new ReviewCommentsHandler());
+          for (Map<String, Object> comment : comments) {
+            comment.put("issueKey", issueKey);
+            allComments.add(comment);
+          }
+        }
+
+        Object[] params = new Object[20];
+        params[0] = issueKey;
+        params[1] = componentId;
+        params[2] = row.get("rootProjectId");
+        params[3] = row.get("ruleId");
+        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("reviewResolution");
+        params[11] = row.get("checksum");
+        params[12] = reporter;
+        params[13] = login((Long) row.get("reviewAssigneeId"));
+        params[14] = plan((Long) row.get("planId"));
+        params[15] = row.get("reviewData");
+        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<Map<String, Object>> comments) throws SQLException {
+      List<Object[]> allParams = Lists.newArrayList();
+
+      for (Map<String, Object> comment : comments) {
+        String login = login((Long) comment.get("userId"));
+        if (login != null) {
+          Object[] params = new Object[6];
+          params[0] = UUID.randomUUID().toString();
+          params[1] = comment.get("issueKey");
+          params[2] = login;
+          params[3] = comment.get("reviewText");
+          params[4] = comment.get("createdAt");
+          params[5] = comment.get("updatedAt");
+          allParams.add(params);
+        }
+      }
+      if (!allParams.isEmpty()) {
+        runner.batch(writeConnection,
+          "INSERT INTO issue_changes(kee, issue_key, user_login, change_type, change_data, created_at, updated_at) VALUES (?, ?, ?, 'comment', ?, ?, ?)",
+          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<Map<String, Object>> {
+    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<Integer, String> SEVERITIES = ImmutableMap.of(1, Severity.INFO, 2, Severity.MINOR, 3, Severity.MAJOR, 4, Severity.CRITICAL, 5, Severity.BLOCKER);
+
+    @Override
+    protected Map<String, Object> handleRow(ResultSet rs) throws SQLException {
+      Map<String, Object> map = Maps.newHashMap();
+      map.put("reviewId", getLong(rs, "reviewId"));
+      map.put("projectId", getLong(rs, "projectId"));
+      map.put("rootProjectId", getLong(rs, "rootProjectId"));
+      map.put("ruleId", getLong(rs, "ruleId"));
+      map.put("severity", Objects.firstNonNull(SEVERITIES.get(getInt(rs, "failureLevel")), "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("createdAt", rs.getTimestamp("createdAt"));
+      map.put("reviewResolution", rs.getString("reviewResolution"));
+      map.put("reviewSeverity", Objects.firstNonNull(rs.getString("reviewSeverity"), "MAJOR"));
+      map.put("reviewStatus", rs.getString("reviewStatus"));
+      map.put("reviewReporterId", getLong(rs, "reviewReporterId"));
+      map.put("reviewAssigneeId", getLong(rs, "reviewAssigneeId"));
+      map.put("reviewData", rs.getString("reviewData"));
+      map.put("reviewManualSeverity", rs.getBoolean("reviewManualSeverity"));
+      map.put("reviewUpdatedAt", rs.getTimestamp("reviewUpdatedAt"));
+      map.put("reviewManualViolation", rs.getBoolean("reviewManualViolation"));
+      map.put("planId", getLong(rs, "planId"));
+      return map;
+    }
+  }
+
+  private static class ReviewCommentsHandler extends AbstractListHandler<Map<String, Object>> {
+    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<String, Object> handleRow(ResultSet rs) throws SQLException {
+      Map<String, Object> map = Maps.newHashMap();
+      map.put("createdAt", rs.getTimestamp("createdAt"));
+      map.put("updatedAt", rs.getTimestamp("updatedAt"));
+      map.put("userId", getLong(rs, "userId"));
+      map.put("reviewText", rs.getString("reviewText"));
+      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;
+  }
+
+
+}
index 8a52b6f8124e1b1082070071f5bac2350d8557c7..cdbd2dde61eb33262d9b5cc8ea324fa47246ffd7 100644 (file)
@@ -72,6 +72,7 @@ import org.sonar.server.component.DefaultRubyComponentService;
 import org.sonar.server.configuration.Backup;
 import org.sonar.server.configuration.ProfilesManager;
 import org.sonar.server.database.EmbeddedDatabaseFactory;
+import org.sonar.server.db.DatabaseMigrator;
 import org.sonar.server.issue.*;
 import org.sonar.server.notifications.NotificationCenter;
 import org.sonar.server.notifications.NotificationService;
index 5c2626bf14c3bc7db85fd203ab780387270a49b5..cd8a3f00a6d9d87942bffaf45b767c855aa9723c 100644 (file)
@@ -46,7 +46,7 @@ import org.sonar.core.i18n.RuleI18nManager;
 import org.sonar.core.measure.MeasureFilterEngine;
 import org.sonar.core.measure.MeasureFilterResult;
 import org.sonar.core.persistence.Database;
-import org.sonar.core.persistence.DatabaseMigrator;
+import org.sonar.server.db.DatabaseMigrator;
 import org.sonar.core.persistence.DryRunDatabaseFactory;
 import org.sonar.core.purge.PurgeDao;
 import org.sonar.core.resource.ResourceIndexerDao;
@@ -237,8 +237,8 @@ public final class JRubyFacade {
     return get(Database.class);
   }
 
-  public boolean createDatabase() {
-    return get(DatabaseMigrator.class).createDatabase();
+  public DatabaseMigrator databaseMigrator() {
+    return get(DatabaseMigrator.class);
   }
 
   /* PROFILES CONSOLE : RULES AND METRIC THRESHOLDS */
index f3b618c5f26902e1ea3f9704666ed2306c04e8eb..3dc58a53b74823a0f0e0a3738c30f506ce8bdcb3 100644 (file)
 
 #
 # Sonar 3.6
-# See SONAR-4305
+# See SONAR-4305 and SONAR-4470
 #
 class MigrateViolationsToIssues < ActiveRecord::Migration
 
-  class RuleFailure < ActiveRecord::Base
-  end
-
-  class Issue < ActiveRecord::Base
-  end
-
-  class IssueChange < ActiveRecord::Base
-  end
-
-  class User < ActiveRecord::Base
-  end
-
-  class ActionPlan < ActiveRecord::Base
-  end
-
-  PRIORITY_TO_SEVERITY = {1 => 'INFO', 2 => 'MINOR', 3 => 'MAJOR', 4 => 'CRITICAL', 5 => 'BLOCKER'}
-
   def self.up
-    truncate_issues
-
-    violation_ids = ActiveRecord::Base.connection.select_rows('select id from rule_failures')
-
-    one_year_ago = Time.now.years_ago(1)
-
-    say_with_time "Convert #{violation_ids.size} violations to issues" do
-      logins_by_id = User.all.inject({}) do |result, user|
-        result[user.id]=user.login
-        result
-      end
-
-      plans_by_id = ActionPlan.all.inject({}) do |result, plan|
-        result[plan.id]=plan.kee
-        result
-      end
-
-      violation_ids.each_slice(999) do |ids|
-        violations = ActiveRecord::Base.connection.select_rows(sql_select_violation(ids))
-        ActiveRecord::Base.transaction do
-          violations.each do |violation|
-            issue_key = new_key
-            review_id = violation[0]
-            created_at = violation[7] || one_year_ago
-            resource_id = violation[1]
-            if resource_id.present?
-              issue = Issue.new(
-                :kee => issue_key,
-                :component_id => violation[1],
-                :rule_id => violation[2],
-                :severity => PRIORITY_TO_SEVERITY[violation[3].to_i] || 'MAJOR',
-                :message => violation[4],
-                :line => violation[5],
-                :effort_to_fix => violation[6],
-                :resolution => violation[13],
-                :checksum => violation[8],
-                :author_login => nil,
-                :issue_attributes => violation[15],
-                :issue_creation_date => created_at,
-                :issue_close_date => nil,
-                :created_at => created_at,
-                :root_component_id => violation[17]
-              )
-
-              if review_id.present?
-                # has review
-                status = violation[11]
-                manual_violation = ActiveRecord::ConnectionAdapters::Column.value_to_boolean(violation[18])
-                issue.status=(status=='OPEN' ? 'CONFIRMED' : status)
-                issue.issue_update_date=violation[16] || one_year_ago
-                issue.updated_at=violation[16] || one_year_ago
-                issue.severity=violation[12] || 'MAJOR'
-                issue.manual_severity=violation[14]
-                issue.reporter=logins_by_id[violation[9].to_i] if (violation[9].present? && manual_violation)
-                issue.assignee=logins_by_id[violation[10].to_i] if violation[10].present?
-
-                plan_id = select_plan_id(review_id)
-                issue.action_plan_key=plans_by_id[plan_id.to_i] if plan_id
-
-                review_comments = select_review_comments(review_id)
-                review_comments.each do |review_comment|
-                  user_id = review_comment[2]
-                  login = logins_by_id[user_id.to_i]
-                  if login
-                    IssueChange.create(
-                      :kee => new_key,
-                      :issue_key => issue_key,
-                      :user_login => login,
-                      :change_type => 'comment',
-                      :change_data => review_comment[3],
-                      :created_at => review_comment[0],
-                      :updated_at => review_comment[1]
-                    )
-                  end
-                end
-
-              else
-                # does not have review
-                issue.status='OPEN'
-                issue.issue_update_date=created_at || one_year_ago
-                issue.updated_at=created_at || one_year_ago
-                issue.manual_severity=false
-              end
-              issue.save
-            end
-          end
-        end
-      end
-    end
+    Java::OrgSonarServerUi::JRubyFacade.getInstance().databaseMigrator().executeMigration('org.sonar.server.db.migrations.ConvertViolationsToIssues')
 
+    # Currently not possible in Java because of Oracle (triggers and sequences must be dropped)
     drop_table('rule_failures')
     drop_table('reviews')
     drop_table('review_comments')
     drop_table('action_plans_reviews')
   end
 
-  def self.truncate_issues
-    ActiveRecord::Base.connection.execute('truncate table issues')
-    ActiveRecord::Base.connection.execute('truncate table issue_changes')
-  end
-
-  def self.sql_select_violation(ids)
-    "select rev.id, s.project_id, rf.rule_id, rf.failure_level, rf.message, rf.line, rf.cost, rf.created_at,
-           rf.checksum,
-           rev.user_id, rev.assignee_id, rev.status, rev.severity, rev.resolution, rev.manual_severity, rev.data,
-           rev.updated_at, s.root_project_id, rev.manual_violation
-    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
-    where rf.id in (#{ids.flatten.join(',')})"
-  end
-
-  def self.new_key
-    Java::JavaUtil::UUID.randomUUID().toString()
-  end
-
-  def self.select_plan_id(review_id)
-    ActiveRecord::Base.connection.select_value("select action_plan_id from action_plans_reviews where review_id=#{review_id}")
-  end
-
-  def self.select_review_comments(review_id)
-    ActiveRecord::Base.connection.select_rows "select created_at, updated_at, user_id, review_text from review_comments where review_id=#{review_id}"
-  end
 end
\ No newline at end of file
index bba78a4e1955f6c26603f93c8ee28704300e13c4..3984050e0c74d618ca05e94807e20e35eac02738 100644 (file)
@@ -85,7 +85,7 @@ class DatabaseVersion
   end
 
   def self.try_restore_structure_dump()
-    ::Java::OrgSonarServerUi::JRubyFacade.getInstance().createDatabase()
+    ::Java::OrgSonarServerUi::JRubyFacade.getInstance().databaseMigrator().createDatabase()
   end
 
   def self.execute_sql_requests(requests)