aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Brandhof <simon.brandhof@gmail.com>2013-07-04 19:23:31 +0200
committerSimon Brandhof <simon.brandhof@gmail.com>2013-07-04 19:26:01 +0200
commitccb95087fd7aab08d2e8e057eecd21797ed7f9b6 (patch)
tree2bcb2aaccbe363ed93dea95e0d96e182692c264c
parenta60604f5d250121fd594bb3d331a0f85fe889797 (diff)
downloadsonarqube-ccb95087fd7aab08d2e8e057eecd21797ed7f9b6.tar.gz
sonarqube-ccb95087fd7aab08d2e8e057eecd21797ed7f9b6.zip
SONAR-4470 Performance issue when migrating DB from 3.5 to 3.6 (mainly with Oracle)
-rw-r--r--pom.xml5
-rw-r--r--sonar-application/src/main/assembly/conf/sonar.properties2
-rw-r--r--sonar-core/pom.xml4
-rw-r--r--sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterExecutor.java4
-rw-r--r--sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterSql.java6
-rw-r--r--sonar-core/src/main/java/org/sonar/core/persistence/DatabaseUtils.java128
-rw-r--r--sonar-core/src/main/java/org/sonar/core/persistence/DatabaseVersion.java60
-rw-r--r--sonar-core/src/main/java/org/sonar/core/persistence/DbTemplate.java25
-rw-r--r--sonar-core/src/main/java/org/sonar/core/persistence/DdlUtils.java2
-rw-r--r--sonar-core/src/main/java/org/sonar/core/persistence/DefaultDatabase.java3
-rw-r--r--sonar-core/src/test/java/org/sonar/core/persistence/AbstractDaoTestCase.java4
-rw-r--r--sonar-core/src/test/java/org/sonar/core/persistence/DatabaseCommands.java13
-rw-r--r--sonar-core/src/test/java/org/sonar/core/persistence/DatabaseUtilsTest.java146
-rw-r--r--sonar-core/src/test/java/org/sonar/core/persistence/H2Database.java25
-rw-r--r--sonar-core/src/test/java/org/sonar/core/persistence/H2DatabaseTest.java2
-rw-r--r--sonar-core/src/test/java/org/sonar/core/persistence/MyBatisTest.java2
-rw-r--r--sonar-core/src/test/java/org/sonar/core/persistence/TestDatabase.java293
-rw-r--r--sonar-core/src/test/java/org/sonar/jpa/test/AbstractDbUnitTestCase.java2
-rw-r--r--sonar-server/pom.xml12
-rw-r--r--sonar-server/src/main/java/org/sonar/server/db/DatabaseMigration.java32
-rw-r--r--sonar-server/src/main/java/org/sonar/server/db/DatabaseMigrator.java (renamed from sonar-core/src/main/java/org/sonar/core/persistence/DatabaseMigrator.java)26
-rw-r--r--sonar-server/src/main/java/org/sonar/server/db/EmbeddedDatabase.java (renamed from sonar-server/src/main/java/org/sonar/server/database/EmbeddedDatabase.java)2
-rw-r--r--sonar-server/src/main/java/org/sonar/server/db/EmbeddedDatabaseFactory.java (renamed from sonar-server/src/main/java/org/sonar/server/database/EmbeddedDatabaseFactory.java)2
-rw-r--r--sonar-server/src/main/java/org/sonar/server/db/migrations/ConvertViolationsToIssues.java365
-rw-r--r--sonar-server/src/main/java/org/sonar/server/db/migrations/package-info.java24
-rw-r--r--sonar-server/src/main/java/org/sonar/server/db/package-info.java24
-rw-r--r--sonar-server/src/main/java/org/sonar/server/platform/Platform.java3
-rw-r--r--sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java6
-rw-r--r--sonar-server/src/main/webapp/WEB-INF/db/migrate/401_migrate_violations_to_issues.rb135
-rw-r--r--sonar-server/src/main/webapp/WEB-INF/db/migrate/README.txt4
-rw-r--r--sonar-server/src/main/webapp/WEB-INF/lib/database_version.rb2
-rw-r--r--sonar-server/src/test/java/org/sonar/server/db/DatabaseMigratorTest.java84
-rw-r--r--sonar-server/src/test/java/org/sonar/server/db/EmbeddedDatabaseTest.java (renamed from sonar-server/src/test/java/org/sonar/server/database/EmbeddedDatabaseTest.java)2
-rw-r--r--sonar-server/src/test/java/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest.java39
-rw-r--r--sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/convert_violations.xml41
-rw-r--r--sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/convert_violations_result.xml21
-rw-r--r--sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/schema.sql152
37 files changed, 1244 insertions, 458 deletions
diff --git a/pom.xml b/pom.xml
index 7823fecb393..e8450774379 100644
--- a/pom.xml
+++ b/pom.xml
@@ -685,6 +685,11 @@
<version>1.3.9</version>
</dependency>
<dependency>
+ <groupId>commons-dbutils</groupId>
+ <artifactId>commons-dbutils</artifactId>
+ <version>1.5</version>
+ </dependency>
+ <dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
diff --git a/sonar-application/src/main/assembly/conf/sonar.properties b/sonar-application/src/main/assembly/conf/sonar.properties
index 024dc35d67f..0e7f3a8157b 100644
--- a/sonar-application/src/main/assembly/conf/sonar.properties
+++ b/sonar-application/src/main/assembly/conf/sonar.properties
@@ -61,7 +61,7 @@ sonar.jdbc.url: jdbc:h2:tcp://localhost:9092/sonar
#----- Oracle 10g/11g
# To connect to Oracle database :
#
-# - It's recommended to use the latest version of the JDBC driver (either ojdbc6.jar for Java 6 or ojdbc5.jar for Java 5).
+# - It's recommended to use the latest version of the JDBC driver (ojdbc6.jar).
# Download it in http://www.oracle.com/technetwork/database/enterprise-edition/jdbc-112010-090769.html
# - Copy the driver to the directory extensions/jdbc-driver/oracle/
# - Comment the embedded database and uncomment the following line :
diff --git a/sonar-core/pom.xml b/sonar-core/pom.xml
index c7c65fa9e66..7047fc7d230 100644
--- a/sonar-core/pom.xml
+++ b/sonar-core/pom.xml
@@ -61,6 +61,10 @@
<artifactId>commons-dbcp</artifactId>
</dependency>
<dependency>
+ <groupId>commons-dbutils</groupId>
+ <artifactId>commons-dbutils</artifactId>
+ </dependency>
+ <dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-classworlds</artifactId>
</dependency>
diff --git a/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterExecutor.java b/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterExecutor.java
index 2e90c431243..81168944cf2 100644
--- a/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterExecutor.java
+++ b/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterExecutor.java
@@ -20,10 +20,10 @@
package org.sonar.core.measure;
import com.google.common.base.Strings;
+import org.apache.commons.dbutils.DbUtils;
import org.apache.ibatis.session.SqlSession;
import org.sonar.api.ServerComponent;
import org.sonar.core.persistence.Database;
-import org.sonar.core.persistence.DatabaseUtils;
import org.sonar.core.persistence.MyBatis;
import org.sonar.core.resource.ResourceDao;
@@ -67,7 +67,7 @@ public class MeasureFilterExecutor implements ServerComponent {
} finally {
MyBatis.closeQuietly(session);
// connection is supposed to be closed by the session
- DatabaseUtils.closeQuietly(connection);
+ DbUtils.closeQuietly(connection);
}
return rows;
diff --git a/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterSql.java b/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterSql.java
index 8545d98be8f..bed09124a09 100644
--- a/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterSql.java
+++ b/sonar-core/src/main/java/org/sonar/core/measure/MeasureFilterSql.java
@@ -22,10 +22,10 @@ package org.sonar.core.measure;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
+import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.sonar.core.persistence.Database;
-import org.sonar.core.persistence.DatabaseUtils;
import org.sonar.core.resource.SnapshotDto;
import javax.annotation.Nullable;
@@ -67,8 +67,8 @@ class MeasureFilterSql {
return process(rs);
} finally {
- DatabaseUtils.closeQuietly(rs);
- DatabaseUtils.closeQuietly(statement);
+ DbUtils.closeQuietly(rs);
+ DbUtils.closeQuietly(statement);
}
}
diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseUtils.java b/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseUtils.java
deleted file mode 100644
index aba584bcaa3..00000000000
--- a/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseUtils.java
+++ /dev/null
@@ -1,128 +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.slf4j.LoggerFactory;
-
-import javax.annotation.Nullable;
-
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-
-/**
- * @since 2.13
- */
-public final class DatabaseUtils {
- private DatabaseUtils() {
- }
-
- /**
- * List of all the tables.
- * This list is hardcoded because we didn't succeed in using java.sql.DatabaseMetaData#getTables() in the same way
- * for all the supported databases, particularly due to Oracle results.
- */
- static final String[] TABLE_NAMES = {
- "action_plans",
- "active_dashboards",
- "active_rules",
- "active_rule_changes",
- "active_rule_parameters",
- "active_rule_param_changes",
- "alerts",
- "authors",
- "characteristics",
- "characteristic_edges",
- "characteristic_properties",
- "dashboards",
- "dependencies",
- "duplications_index",
- "events",
- "graphs",
- "groups",
- "groups_users",
- "group_roles",
- "issues",
- "issue_changes",
- "issue_filters",
- "issue_filter_favourites",
- "loaded_templates",
- "manual_measures",
- "measure_data",
- "measure_filters",
- "measure_filter_favourites",
- "metrics",
- "notifications",
- "permission_templates",
- "perm_templates_users",
- "perm_templates_groups",
- "projects",
- "project_links",
- "project_measures",
- "properties",
- "quality_models",
- "resource_index",
- "rules",
- "rules_parameters",
- "rules_profiles",
- "semaphores",
- "schema_migrations",
- "snapshots",
- "snapshot_sources",
- "snapshot_data",
- "users",
- "user_roles",
- "widgets",
- "widget_properties"};
-
- public static void closeQuietly(@Nullable Connection connection) {
- if (connection != null) {
- try {
- connection.close();
- } catch (SQLException e) {
- LoggerFactory.getLogger(DatabaseUtils.class).warn("Fail to close connection", e);
- // ignore
- }
- }
- }
-
- public static void closeQuietly(@Nullable Statement stmt) {
- if (stmt != null) {
- try {
- stmt.close();
- } catch (SQLException e) {
- LoggerFactory.getLogger(DatabaseUtils.class).warn("Fail to close statement", e);
- // ignore
- }
- }
- }
-
- public static void closeQuietly(@Nullable ResultSet rs) {
- if (rs != null) {
- try {
- rs.close();
- } catch (SQLException e) {
- LoggerFactory.getLogger(DatabaseUtils.class).warn("Fail to close result set", e);
- // ignore
- }
- }
- }
-}
diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseVersion.java b/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseVersion.java
index 16bde3cd397..e77f001bd6b 100644
--- a/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseVersion.java
+++ b/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseVersion.java
@@ -20,6 +20,7 @@
package org.sonar.core.persistence;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
import org.apache.ibatis.session.SqlSession;
import org.sonar.api.BatchComponent;
import org.sonar.api.ServerComponent;
@@ -38,6 +39,65 @@ public class DatabaseVersion implements BatchComponent, ServerComponent {
UP_TO_DATE, REQUIRES_UPGRADE, REQUIRES_DOWNGRADE, FRESH_INSTALL
}
+ /**
+ * List of all the tables.
+ * This list is hardcoded because we didn't succeed in using java.sql.DatabaseMetaData#getTables() in the same way
+ * for all the supported databases, particularly due to Oracle results.
+ */
+ public static final List<String> TABLES = ImmutableList.of(
+ "action_plans",
+ "active_dashboards",
+ "active_rules",
+ "active_rule_changes",
+ "active_rule_parameters",
+ "active_rule_param_changes",
+ "alerts",
+ "authors",
+ "characteristics",
+ "characteristic_edges",
+ "characteristic_properties",
+ "dashboards",
+ "dependencies",
+ "duplications_index",
+ "events",
+ "graphs",
+ "groups",
+ "groups_users",
+ "group_roles",
+ "issues",
+ "issue_changes",
+ "issue_filters",
+ "issue_filter_favourites",
+ "loaded_templates",
+ "manual_measures",
+ "measure_data",
+ "measure_filters",
+ "measure_filter_favourites",
+ "metrics",
+ "notifications",
+ "permission_templates",
+ "perm_templates_users",
+ "perm_templates_groups",
+ "projects",
+ "project_links",
+ "project_measures",
+ "properties",
+ "quality_models",
+ "resource_index",
+ "rules",
+ "rules_parameters",
+ "rules_profiles",
+ "semaphores",
+ "schema_migrations",
+ "snapshots",
+ "snapshot_sources",
+ "snapshot_data",
+ "users",
+ "user_roles",
+ "widgets",
+ "widget_properties"
+ );
+
private MyBatis mybatis;
public DatabaseVersion(MyBatis mybatis) {
diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/DbTemplate.java b/sonar-core/src/main/java/org/sonar/core/persistence/DbTemplate.java
index 52e2dbd1c41..2a1fb7d9d1b 100644
--- a/sonar-core/src/main/java/org/sonar/core/persistence/DbTemplate.java
+++ b/sonar-core/src/main/java/org/sonar/core/persistence/DbTemplate.java
@@ -23,6 +23,7 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import org.apache.commons.dbcp.BasicDataSource;
+import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -89,12 +90,12 @@ public class DbTemplate implements ServerComponent {
LOG.error("Fail to copy table " + table, e);
throw new IllegalStateException("Fail to copy table " + table, e);
} finally {
- DatabaseUtils.closeQuietly(destStatement);
- DatabaseUtils.closeQuietly(destResultSet);
- DatabaseUtils.closeQuietly(destConnection);
- DatabaseUtils.closeQuietly(sourceResultSet);
- DatabaseUtils.closeQuietly(sourceStatement);
- DatabaseUtils.closeQuietly(sourceConnection);
+ DbUtils.closeQuietly(destStatement);
+ DbUtils.closeQuietly(destResultSet);
+ DbUtils.closeQuietly(destConnection);
+ DbUtils.closeQuietly(sourceResultSet);
+ DbUtils.closeQuietly(sourceStatement);
+ DbUtils.closeQuietly(sourceConnection);
}
return this;
@@ -158,9 +159,9 @@ public class DbTemplate implements ServerComponent {
LOG.error("Fail to get row count for table " + table, e);
throw new SonarException("Fail to get row count for table " + table, e);
} finally {
- DatabaseUtils.closeQuietly(resultSet);
- DatabaseUtils.closeQuietly(statement);
- DatabaseUtils.closeQuietly(connection);
+ DbUtils.closeQuietly(resultSet);
+ DbUtils.closeQuietly(statement);
+ DbUtils.closeQuietly(connection);
}
}
@@ -175,8 +176,8 @@ public class DbTemplate implements ServerComponent {
LOG.error("Fail to truncate table " + table, e);
throw new SonarException("Fail to truncate table " + table, e);
} finally {
- DatabaseUtils.closeQuietly(statement);
- DatabaseUtils.closeQuietly(connection);
+ DbUtils.closeQuietly(statement);
+ DbUtils.closeQuietly(connection);
}
return this;
@@ -200,7 +201,7 @@ public class DbTemplate implements ServerComponent {
LOG.error("Fail to createSchema local database schema", e);
throw new SonarException("Fail to createSchema local database schema", e);
} finally {
- DatabaseUtils.closeQuietly(connection);
+ DbUtils.closeQuietly(connection);
}
return this;
diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/DdlUtils.java b/sonar-core/src/main/java/org/sonar/core/persistence/DdlUtils.java
index 94ac7774d43..0ea501c2083 100644
--- a/sonar-core/src/main/java/org/sonar/core/persistence/DdlUtils.java
+++ b/sonar-core/src/main/java/org/sonar/core/persistence/DdlUtils.java
@@ -48,7 +48,7 @@ public final class DdlUtils {
executeScript(connection, "org/sonar/core/persistence/rows-" + dialect + ".sql");
}
- private static void executeScript(Connection connection, String path) {
+ public static void executeScript(Connection connection, String path) {
ScriptRunner scriptRunner = newScriptRunner(connection);
try {
scriptRunner.runScript(Resources.getResourceAsReader(path));
diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/DefaultDatabase.java b/sonar-core/src/main/java/org/sonar/core/persistence/DefaultDatabase.java
index 3b8d5c5c2d6..5d0752f0d4d 100644
--- a/sonar-core/src/main/java/org/sonar/core/persistence/DefaultDatabase.java
+++ b/sonar-core/src/main/java/org/sonar/core/persistence/DefaultDatabase.java
@@ -22,6 +22,7 @@ package org.sonar.core.persistence;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.dbcp.BasicDataSource;
import org.apache.commons.dbcp.BasicDataSourceFactory;
+import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.lang.StringUtils;
import org.hibernate.cfg.Environment;
import org.slf4j.Logger;
@@ -141,7 +142,7 @@ public class DefaultDatabase implements Database {
} catch (Exception e) {
LOG.error("Can not connect to database. Please check connectivity and settings (see the properties prefixed by 'sonar.jdbc.').", e);
} finally {
- DatabaseUtils.closeQuietly(connection);
+ DbUtils.closeQuietly(connection);
}
}
diff --git a/sonar-core/src/test/java/org/sonar/core/persistence/AbstractDaoTestCase.java b/sonar-core/src/test/java/org/sonar/core/persistence/AbstractDaoTestCase.java
index a6e1e1b5d05..4d208829db1 100644
--- a/sonar-core/src/test/java/org/sonar/core/persistence/AbstractDaoTestCase.java
+++ b/sonar-core/src/test/java/org/sonar/core/persistence/AbstractDaoTestCase.java
@@ -60,7 +60,7 @@ import java.util.Properties;
import static org.junit.Assert.fail;
public abstract class AbstractDaoTestCase {
- private static Logger LOG = LoggerFactory.getLogger(AbstractDaoTestCase.class);
+ private static final Logger LOG = LoggerFactory.getLogger(AbstractDaoTestCase.class);
private static Database database;
private static DatabaseCommands databaseCommands;
private static IDatabaseTester databaseTester;
@@ -80,7 +80,7 @@ public abstract class AbstractDaoTestCase {
if (hasDialect) {
database = new DefaultDatabase(settings);
} else {
- database = new H2Database("h2Tests");
+ database = new H2Database("h2Tests", true);
}
database.start();
LOG.info("Test Database: " + database);
diff --git a/sonar-core/src/test/java/org/sonar/core/persistence/DatabaseCommands.java b/sonar-core/src/test/java/org/sonar/core/persistence/DatabaseCommands.java
index 2205ac573f5..21fb4c923c1 100644
--- a/sonar-core/src/test/java/org/sonar/core/persistence/DatabaseCommands.java
+++ b/sonar-core/src/test/java/org/sonar/core/persistence/DatabaseCommands.java
@@ -119,9 +119,14 @@ public abstract class DatabaseCommands {
connection.setAutoCommit(false);
Statement statement = connection.createStatement();
- for (String table : DatabaseUtils.TABLE_NAMES) {
- statement.executeUpdate("TRUNCATE TABLE " + table);
- connection.commit();
+ for (String table : DatabaseVersion.TABLES) {
+ try {
+ statement.executeUpdate("TRUNCATE TABLE " + table);
+ connection.commit();
+ } catch (Exception e) {
+ // ignore
+ connection.rollback();
+ }
}
statement.close();
@@ -133,7 +138,7 @@ public abstract class DatabaseCommands {
connection.setAutoCommit(false);
Statement statement = connection.createStatement();
- for (String table : DatabaseUtils.TABLE_NAMES) {
+ for (String table : DatabaseVersion.TABLES) {
try {
ResultSet result = statement.executeQuery("SELECT CASE WHEN MAX(ID) IS NULL THEN 1 ELSE MAX(ID)+1 END FROM " + table);
result.next();
diff --git a/sonar-core/src/test/java/org/sonar/core/persistence/DatabaseUtilsTest.java b/sonar-core/src/test/java/org/sonar/core/persistence/DatabaseUtilsTest.java
deleted file mode 100644
index 28e1ef948f7..00000000000
--- a/sonar-core/src/test/java/org/sonar/core/persistence/DatabaseUtilsTest.java
+++ /dev/null
@@ -1,146 +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.junit.Test;
-import org.sonar.core.persistence.dialect.Oracle;
-
-import java.sql.Connection;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-
-import static org.fest.assertions.Assertions.assertThat;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-public class DatabaseUtilsTest extends AbstractDaoTestCase {
-
- @Test
- public void should_close_connection() throws SQLException {
- Connection connection = getConnection();
- assertThat(isClosed(connection)).isFalse();
-
- DatabaseUtils.closeQuietly(connection);
- assertThat(isClosed(connection)).isTrue();
- }
-
- @Test
- public void should_support_null_connection() {
- DatabaseUtils.closeQuietly((Connection) null);
- // no failure
- }
-
- @Test
- public void should_close_statement_and_resultset() throws SQLException {
- Connection connection = getConnection();
- try {
- PreparedStatement statement = connection.prepareStatement(selectDual());
- ResultSet rs = statement.executeQuery();
-
- DatabaseUtils.closeQuietly(rs);
- DatabaseUtils.closeQuietly(statement);
-
- assertThat(isClosed(statement)).isTrue();
- assertThat(isClosed(rs)).isTrue();
- } finally {
- DatabaseUtils.closeQuietly(connection);
- }
- }
-
- @Test
- public void should_not_fail_on_connection_errors() throws SQLException {
- Connection connection = mock(Connection.class);
- doThrow(new SQLException()).when(connection).close();
-
- DatabaseUtils.closeQuietly(connection);
-
- // no failure
- verify(connection).close(); // just to be sure
- }
-
- @Test
- public void should_not_fail_on_statement_errors() throws SQLException {
- Statement statement = mock(Statement.class);
- doThrow(new SQLException()).when(statement).close();
-
- DatabaseUtils.closeQuietly(statement);
-
- // no failure
- verify(statement).close(); // just to be sure
- }
-
- @Test
- public void should_not_fail_on_resulset_errors() throws SQLException {
- ResultSet rs = mock(ResultSet.class);
- doThrow(new SQLException()).when(rs).close();
-
- DatabaseUtils.closeQuietly(rs);
-
- // no failure
- verify(rs).close(); // just to be sure
- }
-
- /**
- * Connection.isClosed() has been introduced in java 1.6
- */
- private boolean isClosed(Connection c) {
- try {
- c.createStatement().execute(selectDual());
- return false;
- } catch (Exception e) {
- return true;
- }
- }
-
- /**
- * Statement.isClosed() has been introduced in java 1.6
- */
- private boolean isClosed(Statement s) {
- try {
- s.execute("SELECT 1");
- return false;
- } catch (Exception e) {
- return true;
- }
- }
-
- /**
- * ResultSet.isClosed() has been introduced in java 1.6
- */
- private boolean isClosed(ResultSet rs) {
- try {
- rs.next();
- return false;
- } catch (Exception e) {
- return true;
- }
- }
-
- private String selectDual() {
- String sql = "SELECT 1";
- if (Oracle.ID.equals(getDatabase().getDialect().getId())) {
- sql = "SELECT 1 FROM DUAL";
- }
- return sql;
- }
-}
diff --git a/sonar-core/src/test/java/org/sonar/core/persistence/H2Database.java b/sonar-core/src/test/java/org/sonar/core/persistence/H2Database.java
index 4536993eb1d..31c6619aea7 100644
--- a/sonar-core/src/test/java/org/sonar/core/persistence/H2Database.java
+++ b/sonar-core/src/test/java/org/sonar/core/persistence/H2Database.java
@@ -20,6 +20,7 @@
package org.sonar.core.persistence;
import org.apache.commons.dbcp.BasicDataSource;
+import org.apache.commons.dbutils.DbUtils;
import org.hibernate.cfg.Environment;
import org.sonar.core.persistence.dialect.Dialect;
import org.sonar.core.persistence.dialect.H2;
@@ -38,18 +39,22 @@ import java.util.Properties;
*/
public class H2Database implements Database {
private final String name;
+ private final boolean createSchema;
private BasicDataSource datasource;
/**
* IMPORTANT: change DB name in order to not conflict with {@link DefaultDatabaseTest}
*/
- public H2Database(String name) {
+ public H2Database(String name, boolean createSchema) {
this.name = name;
+ this.createSchema = createSchema;
}
public H2Database start() {
startDatabase();
- createSchema();
+ if (createSchema) {
+ createSchema();
+ }
return this;
}
@@ -70,10 +75,24 @@ public class H2Database implements Database {
try {
connection = datasource.getConnection();
DdlUtils.createSchema(connection, "h2");
+
} catch (SQLException e) {
throw new IllegalStateException("Fail to create schema", e);
} finally {
- DatabaseUtils.closeQuietly(connection);
+ DbUtils.closeQuietly(connection);
+ }
+ }
+
+ public void executeScript(String classloaderPath) {
+ Connection connection = null;
+ try {
+ connection = datasource.getConnection();
+ DdlUtils.executeScript(connection, classloaderPath);
+
+ } catch (SQLException e) {
+ throw new IllegalStateException("Fail to execute script: " + classloaderPath, e);
+ } finally {
+ DbUtils.closeQuietly(connection);
}
}
diff --git a/sonar-core/src/test/java/org/sonar/core/persistence/H2DatabaseTest.java b/sonar-core/src/test/java/org/sonar/core/persistence/H2DatabaseTest.java
index 67703eda4f5..19ab908c98a 100644
--- a/sonar-core/src/test/java/org/sonar/core/persistence/H2DatabaseTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/persistence/H2DatabaseTest.java
@@ -30,7 +30,7 @@ import java.sql.SQLException;
import static org.fest.assertions.Assertions.assertThat;
public class H2DatabaseTest {
- H2Database db = new H2Database("sonar2");
+ H2Database db = new H2Database("sonar2", true);
@Before
public void startDb() {
diff --git a/sonar-core/src/test/java/org/sonar/core/persistence/MyBatisTest.java b/sonar-core/src/test/java/org/sonar/core/persistence/MyBatisTest.java
index d0455791383..3a3bfcad71a 100644
--- a/sonar-core/src/test/java/org/sonar/core/persistence/MyBatisTest.java
+++ b/sonar-core/src/test/java/org/sonar/core/persistence/MyBatisTest.java
@@ -41,7 +41,7 @@ public class MyBatisTest {
@BeforeClass
public static void start() {
- database = new H2Database("sonar2");
+ database = new H2Database("sonar2", true);
database.start();
}
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
new file mode 100644
index 00000000000..39d0bf63762
--- /dev/null
+++ b/sonar-core/src/test/java/org/sonar/core/persistence/TestDatabase.java
@@ -0,0 +1,293 @@
+/*
+ * 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 com.google.common.collect.Maps;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.dbutils.DbUtils;
+import org.apache.commons.dbutils.QueryRunner;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.text.StrSubstitutor;
+import org.dbunit.Assertion;
+import org.dbunit.DataSourceDatabaseTester;
+import org.dbunit.DatabaseUnitException;
+import org.dbunit.IDatabaseTester;
+import org.dbunit.assertion.DiffCollectingFailureHandler;
+import org.dbunit.assertion.Difference;
+import org.dbunit.database.DatabaseConfig;
+import org.dbunit.database.IDatabaseConnection;
+import org.dbunit.dataset.CompositeDataSet;
+import org.dbunit.dataset.IDataSet;
+import org.dbunit.dataset.ReplacementDataSet;
+import org.dbunit.dataset.xml.FlatXmlDataSet;
+import org.dbunit.ext.mssql.InsertIdentityOperation;
+import org.dbunit.operation.DatabaseOperation;
+import org.junit.rules.ExternalResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.config.Settings;
+import org.sonar.core.config.Logback;
+import org.sonar.core.persistence.dialect.Dialect;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import static org.junit.Assert.fail;
+
+public class TestDatabase extends ExternalResource {
+
+ private static final Logger LOG = LoggerFactory.getLogger(TestDatabase.class);
+
+ private Database db;
+ private DatabaseCommands commands;
+ private IDatabaseTester tester;
+ private MyBatis myBatis;
+ private String schemaPath = null;
+
+
+ public TestDatabase schema(Class baseClass, String filename) {
+ String path = StringUtils.replaceChars(baseClass.getCanonicalName(), '.', '/');
+ schemaPath = path + "/" + filename;
+ return this;
+ }
+
+ @Override
+ protected void before() throws Throwable {
+ if (db == null) {
+ Settings settings = new Settings().setProperties(Maps.fromProperties(System.getProperties()));
+ if (settings.hasKey("orchestrator.configUrl")) {
+ loadOrchestratorSettings(settings);
+ }
+ for (String key : settings.getKeysStartingWith("sonar.jdbc")) {
+ LOG.info(key + ": " + settings.getString(key));
+ }
+ boolean hasDialect = settings.hasKey("sonar.jdbc.dialect");
+ if (hasDialect) {
+ db = new DefaultDatabase(settings);
+ } else {
+ db = new H2Database("h2Tests" + DigestUtils.md5Hex(StringUtils.defaultString(schemaPath)), schemaPath == null);
+ }
+ db.start();
+ if (schemaPath != null) {
+ // will fail if not H2
+ ((H2Database) db).executeScript(schemaPath);
+ }
+ LOG.info("Test Database: " + db);
+
+ commands = DatabaseCommands.forDialect(db.getDialect());
+ tester = new DataSourceDatabaseTester(db.getDataSource());
+
+ myBatis = new MyBatis(db, settings, new Logback());
+ myBatis.start();
+ }
+ commands.truncateDatabase(db.getDataSource());
+ }
+
+
+ @Override
+ protected void after() {
+ if (db != null) {
+ db.stop();
+ db = null;
+ commands = null;
+ myBatis = null;
+ }
+ }
+
+ public Database database() {
+ return db;
+ }
+
+ public Dialect dialect() {
+ return db.getDialect();
+ }
+
+ public DatabaseCommands commands() {
+ return commands;
+ }
+
+ public MyBatis myBatis() {
+ return myBatis;
+ }
+
+ public Connection openConnection() throws SQLException {
+ return db.getDataSource().getConnection();
+ }
+
+ public void executeUpdateSql(String sql) {
+ Connection connection = null;
+ try {
+ connection = openConnection();
+ new QueryRunner().update(connection, sql);
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to execute sql: " + sql);
+ } finally {
+ DbUtils.commitAndCloseQuietly(connection);
+ }
+ }
+
+ public void prepareDbUnit(Class testClass, String... testNames) {
+ InputStream[] streams = new InputStream[testNames.length];
+ try {
+ for (int i = 0; i < testNames.length; i++) {
+ String path = "/" + testClass.getName().replace('.', '/') + "/" + testNames[i];
+ streams[i] = testClass.getResourceAsStream(path);
+ if (streams[i] == null) {
+ throw new IllegalStateException("DbUnit file not found: " + path);
+ }
+ }
+
+ prepareDbUnit(streams);
+ commands.resetPrimaryKeys(db.getDataSource());
+ } catch (SQLException e) {
+ throw translateException("Could not setup DBUnit data", e);
+ } finally {
+ for (InputStream stream : streams) {
+ IOUtils.closeQuietly(stream);
+ }
+ }
+ }
+
+ private void prepareDbUnit(InputStream... dataSetStream) {
+ IDatabaseConnection connection = null;
+ try {
+ IDataSet[] dataSets = new IDataSet[dataSetStream.length];
+ for (int i = 0; i < dataSetStream.length; i++) {
+ dataSets[i] = dbUnitDataSet(dataSetStream[i]);
+ }
+ tester.setDataSet(new CompositeDataSet(dataSets));
+ connection = dbUnitConnection();
+ new InsertIdentityOperation(DatabaseOperation.INSERT).execute(connection, tester.getDataSet());
+ } catch (Exception e) {
+ throw translateException("Could not setup DBUnit data", e);
+ } finally {
+ closeQuietly(connection);
+ }
+ }
+
+ public void assertDbUnit(Class testClass, String filename, String... tables) {
+ IDatabaseConnection connection = null;
+ try {
+ connection = dbUnitConnection();
+ IDataSet dataSet = connection.createDataSet();
+ String path = "/" + testClass.getName().replace('.', '/') + "/" + filename;
+ IDataSet expectedDataSet = dbUnitDataSet(testClass.getResourceAsStream(path));
+ for (String table : tables) {
+ DiffCollectingFailureHandler diffHandler = new DiffCollectingFailureHandler();
+
+ Assertion.assertEquals(expectedDataSet.getTable(table), dataSet.getTable(table), diffHandler);
+ // Evaluate the differences and ignore some column values
+ List diffList = diffHandler.getDiffList();
+ for (Object o : diffList) {
+ Difference diff = (Difference) o;
+ if (!"[ignore]".equals(diff.getExpectedValue())) {
+ throw new DatabaseUnitException(diff.toString());
+ }
+ }
+ }
+ } catch (DatabaseUnitException e) {
+ fail(e.getMessage());
+ } catch (Exception e) {
+ throw translateException("Error while checking results", e);
+ } finally {
+ closeQuietly(connection);
+ }
+ }
+
+ private IDataSet dbUnitDataSet(InputStream stream) {
+ try {
+ ReplacementDataSet dataSet = new ReplacementDataSet(new FlatXmlDataSet(stream));
+ dataSet.addReplacementObject("[null]", null);
+ dataSet.addReplacementObject("[false]", Boolean.FALSE);
+ dataSet.addReplacementObject("[true]", Boolean.TRUE);
+ return dataSet;
+ } catch (Exception e) {
+ throw translateException("Could not read the dataset stream", e);
+ }
+ }
+
+ private IDatabaseConnection dbUnitConnection() {
+ try {
+ IDatabaseConnection connection = tester.getConnection();
+ connection.getConfig().setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, commands.getDbUnitFactory());
+ return connection;
+ } catch (Exception e) {
+ throw translateException("Error while getting connection", e);
+ }
+ }
+
+ private void closeQuietly(IDatabaseConnection connection) {
+ try {
+ if (connection != null) {
+ connection.close();
+ }
+ } catch (SQLException e) {
+ // ignore
+ }
+ }
+
+ private static RuntimeException translateException(String msg, Exception cause) {
+ RuntimeException runtimeException = new RuntimeException(String.format("%s: [%s] %s", msg, cause.getClass().getName(), cause.getMessage()));
+ runtimeException.setStackTrace(cause.getStackTrace());
+ return runtimeException;
+ }
+
+ private void loadOrchestratorSettings(Settings settings) throws URISyntaxException, IOException {
+ String url = settings.getString("orchestrator.configUrl");
+ URI uri = new URI(url);
+ InputStream input = null;
+ try {
+ if (url.startsWith("file:")) {
+ File file = new File(uri);
+ input = FileUtils.openInputStream(file);
+ } else {
+ HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
+ int responseCode = connection.getResponseCode();
+ if (responseCode >= 400) {
+ throw new IllegalStateException("Fail to request: " + uri + ". Status code=" + responseCode);
+ }
+
+ input = connection.getInputStream();
+
+ }
+ Properties props = new Properties();
+ props.load(input);
+ settings.addProperties(props);
+ for (Map.Entry<String, String> entry : settings.getProperties().entrySet()) {
+ String interpolatedValue = StrSubstitutor.replace(entry.getValue(), System.getenv(), "${", "}");
+ settings.setProperty(entry.getKey(), interpolatedValue);
+ }
+ } finally {
+ IOUtils.closeQuietly(input);
+ }
+ }
+
+}
diff --git a/sonar-core/src/test/java/org/sonar/jpa/test/AbstractDbUnitTestCase.java b/sonar-core/src/test/java/org/sonar/jpa/test/AbstractDbUnitTestCase.java
index 097ff170350..7998f074bd7 100644
--- a/sonar-core/src/test/java/org/sonar/jpa/test/AbstractDbUnitTestCase.java
+++ b/sonar-core/src/test/java/org/sonar/jpa/test/AbstractDbUnitTestCase.java
@@ -63,7 +63,7 @@ public abstract class AbstractDbUnitTestCase {
@Before
public void startDatabase() throws SQLException {
if (database == null) {
- database = new H2Database("sonarHibernate");
+ database = new H2Database("sonarHibernate", true);
database.start();
databaseCommands = DatabaseCommands.forDialect(database.getDialect());
diff --git a/sonar-server/pom.xml b/sonar-server/pom.xml
index 092a26276b4..bd83907bd0c 100644
--- a/sonar-server/pom.xml
+++ b/sonar-server/pom.xml
@@ -86,18 +86,22 @@
<artifactId>xstream</artifactId>
</dependency>
<dependency>
- <groupId>commons-lang</groupId>
- <artifactId>commons-lang</artifactId>
- </dependency>
- <dependency>
<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>
</dependency>
<dependency>
+ <groupId>commons-lang</groupId>
+ <artifactId>commons-lang</artifactId>
+ </dependency>
+ <dependency>
<groupId>org.picocontainer</groupId>
<artifactId>picocontainer</artifactId>
</dependency>
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
index 00000000000..2e99e4b30bb
--- /dev/null
+++ b/sonar-server/src/main/java/org/sonar/server/db/DatabaseMigration.java
@@ -0,0 +1,32 @@
+/*
+ * 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-core/src/main/java/org/sonar/core/persistence/DatabaseMigrator.java b/sonar-server/src/main/java/org/sonar/server/db/DatabaseMigrator.java
index b15db5f3711..8df637b7313 100644
--- a/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseMigrator.java
+++ b/sonar-server/src/main/java/org/sonar/server/db/DatabaseMigrator.java
@@ -17,11 +17,15 @@
* 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;
+package org.sonar.server.db;
+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;
@@ -33,8 +37,8 @@ import java.sql.Connection;
*/
public class DatabaseMigrator implements ServerComponent {
- private MyBatis myBatis;
- private Database database;
+ private final MyBatis myBatis;
+ private final Database database;
public DatabaseMigrator(MyBatis myBatis, Database database) {
this.myBatis = myBatis;
@@ -56,13 +60,25 @@ public class DatabaseMigrator implements ServerComponent {
session = myBatis.openSession();
connection = session.getConnection();
DdlUtils.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.
- DatabaseUtils.closeQuietly(connection);
+ DbUtils.closeQuietly(connection);
+ }
+ }
+
+ 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);
}
- return true;
}
}
diff --git a/sonar-server/src/main/java/org/sonar/server/database/EmbeddedDatabase.java b/sonar-server/src/main/java/org/sonar/server/db/EmbeddedDatabase.java
index d76e9ad577d..f3bb4486ba0 100644
--- a/sonar-server/src/main/java/org/sonar/server/database/EmbeddedDatabase.java
+++ b/sonar-server/src/main/java/org/sonar/server/db/EmbeddedDatabase.java
@@ -17,7 +17,7 @@
* 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.database;
+package org.sonar.server.db;
import org.apache.commons.lang.StringUtils;
import org.h2.Driver;
diff --git a/sonar-server/src/main/java/org/sonar/server/database/EmbeddedDatabaseFactory.java b/sonar-server/src/main/java/org/sonar/server/db/EmbeddedDatabaseFactory.java
index 7179cd2824d..6ab6122a3b2 100644
--- a/sonar-server/src/main/java/org/sonar/server/database/EmbeddedDatabaseFactory.java
+++ b/sonar-server/src/main/java/org/sonar/server/db/EmbeddedDatabaseFactory.java
@@ -17,7 +17,7 @@
* 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.database;
+package org.sonar.server.db;
import org.sonar.api.config.Settings;
import org.sonar.api.database.DatabaseProperties;
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
index 00000000000..7613a7d1845
--- /dev/null
+++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/ConvertViolationsToIssues.java
@@ -0,0 +1,365 @@
+/*
+ * 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 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 {
+ 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>> {
+ private static String SQL = "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, plan.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 plan on plan.review_id=rev.id " +
+ " where ";
+
+ static {
+ for (int i = 0; i < GROUP_SIZE; i++) {
+ if (i > 0) {
+ SQL += " or ";
+ }
+ SQL += "rf.id=?";
+ }
+ }
+
+ private static 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 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;
+ }
+
+
+}
diff --git a/sonar-server/src/main/java/org/sonar/server/db/migrations/package-info.java b/sonar-server/src/main/java/org/sonar/server/db/migrations/package-info.java
new file mode 100644
index 00000000000..63993d65144
--- /dev/null
+++ b/sonar-server/src/main/java/org/sonar/server/db/migrations/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;
+
+import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file
diff --git a/sonar-server/src/main/java/org/sonar/server/db/package-info.java b/sonar-server/src/main/java/org/sonar/server/db/package-info.java
new file mode 100644
index 00000000000..79dcbe940a5
--- /dev/null
+++ b/sonar-server/src/main/java/org/sonar/server/db/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;
+
+import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file
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 c80b904ad84..2d7014b3728 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
@@ -72,7 +72,8 @@ import org.sonar.server.charts.ChartFactory;
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.db.EmbeddedDatabaseFactory;
import org.sonar.server.issue.*;
import org.sonar.server.notifications.NotificationCenter;
import org.sonar.server.notifications.NotificationService;
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 5c2626bf14c..cd8a3f00a6d 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
@@ -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 */
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 f3b618c5f26..f1e7a5cd349 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
@@ -24,145 +24,14 @@
#
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
diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/README.txt b/sonar-server/src/main/webapp/WEB-INF/db/migrate/README.txt
index c9b1b28eb5a..a618f27923e 100644
--- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/README.txt
+++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/README.txt
@@ -5,8 +5,8 @@ HOW TO ADD A MIGRATION
+ sonar-core/src/main/resources/org/sonar/core/persistence/schema-h2.ddl
+ sonar-core/src/main/resources/org/sonar/core/persistence/rows-h2.sql :
- add "INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('<THE MIGRATION ID>')"
-* Update the migration id defined in sonar-core/src/main/java/org/sonar/core/persistence/DatabaseVersion.java
-* If a table is added or removed, then edit sonar-core/src/main/java/org/sonar/core/persistence/DatabaseUtils.java
+* Update the migration id defined in the Java class org.sonar.core.persistence.DatabaseTest
+* If a table is added or removed, then update the list org.sonar.core.persistence.DatabaseTest#TABLES
diff --git a/sonar-server/src/main/webapp/WEB-INF/lib/database_version.rb b/sonar-server/src/main/webapp/WEB-INF/lib/database_version.rb
index bba78a4e195..3984050e0c7 100644
--- a/sonar-server/src/main/webapp/WEB-INF/lib/database_version.rb
+++ b/sonar-server/src/main/webapp/WEB-INF/lib/database_version.rb
@@ -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)
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
new file mode 100644
index 00000000000..7977ecfa717
--- /dev/null
+++ b/sonar-server/src/test/java/org/sonar/server/db/DatabaseMigratorTest.java
@@ -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.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.MySql;
+
+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();
+ }
+
+ 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/database/EmbeddedDatabaseTest.java b/sonar-server/src/test/java/org/sonar/server/db/EmbeddedDatabaseTest.java
index e2444269d11..0ab88116674 100644
--- a/sonar-server/src/test/java/org/sonar/server/database/EmbeddedDatabaseTest.java
+++ b/sonar-server/src/test/java/org/sonar/server/db/EmbeddedDatabaseTest.java
@@ -17,7 +17,7 @@
* 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.database;
+package org.sonar.server.db;
import org.h2.Driver;
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
new file mode 100644
index 00000000000..c2ab498add3
--- /dev/null
+++ b/sonar-server/src/test/java/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest.java
@@ -0,0 +1,39 @@
+/*
+ * 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/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
new file mode 100644
index 00000000000..5a97e0df76e
--- /dev/null
+++ b/sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/convert_violations.xml
@@ -0,0 +1,41 @@
+<dataset>
+ <snapshots id="10" root_project_id="10" project_id="11"/>
+ <users id="200" login="fabrice"/>
+ <users id="201" login="julien"/>
+ <action_plans id="999" kee="PLAN-999"/>
+
+ <!-- violation without review -->
+ <rule_failures id="1" snapshot_id="10" rule_id="20" failure_level="2" message="the message" line="1234" cost="3.14"
+ created_at="2012-01-05" checksum="ABCDE" permanent_id="1"/>
+
+ <!-- violation with review -->
+ <rule_failures id="2" snapshot_id="10" rule_id="22" failure_level="2" message="another message" line="[null]" cost="[null]"
+ created_at="2012-01-05" checksum="FGHIJ" permanent_id="2"/>
+ <reviews id="1" rule_failure_permanent_id="2" MANUAL_VIOLATION="[false]" MANUAL_SEVERITY="[true]" SEVERITY="BLOCKER" UPDATED_AT="2013-05-18"
+ STATUS="OPEN" RESOLUTION="[null]" USER_ID="[null]" ASSIGNEE_ID="201"/>
+
+ <review_comments ID="1" REVIEW_ID="1" USER_ID="200" REVIEW_TEXT="a comment" CREATED_AT="2012-04-28" UPDATED_AT="2012-04-29"/>
+
+ <!-- comment by unknown user -->
+ <review_comments ID="2" REVIEW_ID="1" USER_ID="999" REVIEW_TEXT="to be ignored because unknown user" CREATED_AT="2012-04-28" UPDATED_AT="2012-04-29"/>
+
+ <!-- manual violation -->
+ <rule_failures id="3" snapshot_id="10" rule_id="22" failure_level="2" message="another message" line="[null]" cost="[null]"
+ created_at="2012-01-05" checksum="FGHIJ" permanent_id="3"/>
+ <reviews id="2" rule_failure_permanent_id="3" MANUAL_VIOLATION="[true]" MANUAL_SEVERITY="[true]" SEVERITY="BLOCKER" UPDATED_AT="2013-05-18"
+ STATUS="RESOLVED" RESOLUTION="FIXED" USER_ID="200" ASSIGNEE_ID="201"/>
+
+ <action_plans_reviews review_id="1" action_plan_id="999"/>
+
+
+
+
+
+ <!-- to be truncated -->
+ <issues ID="1" COMPONENT_ID="11" ROOT_COMPONENT_ID="10" RULE_ID="20" SEVERITY="MINOR" KEE="[ignore]"
+ ACTION_PLAN_KEY="[null]" ASSIGNEE="[null]" AUTHOR_LOGIN="[null]" CHECKSUM="ABCDE"
+ CREATED_AT="2012-01-05" EFFORT_TO_FIX="3.14" ISSUE_ATTRIBUTES="[null]" ISSUE_CLOSE_DATE="[null]" ISSUE_CREATION_DATE="2012-01-05"
+ ISSUE_UPDATE_DATE="2012-01-05" LINE="1234" MANUAL_SEVERITY="[false]" MESSAGE="the message" REPORTER="[null]"
+ RESOLUTION="[null]" STATUS="OPEN" UPDATED_AT="2012-01-05"/>
+ <issue_changes ID="1" ISSUE_KEY="ABCDE"/>
+</dataset> \ 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
new file mode 100644
index 00000000000..8c0330bcb15
--- /dev/null
+++ b/sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/convert_violations_result.xml
@@ -0,0 +1,21 @@
+<dataset>
+ <issues ID="2" COMPONENT_ID="11" ROOT_COMPONENT_ID="10" RULE_ID="20" SEVERITY="MINOR" KEE="[ignore]"
+ ACTION_PLAN_KEY="[null]" ASSIGNEE="[null]" AUTHOR_LOGIN="[null]" CHECKSUM="ABCDE"
+ CREATED_AT="2012-01-05" EFFORT_TO_FIX="3.14" ISSUE_ATTRIBUTES="[null]" ISSUE_CLOSE_DATE="[null]" ISSUE_CREATION_DATE="2012-01-05"
+ ISSUE_UPDATE_DATE="2012-01-05" LINE="1234" MANUAL_SEVERITY="[false]" MESSAGE="the message" REPORTER="[null]"
+ RESOLUTION="[null]" STATUS="OPEN" UPDATED_AT="2012-01-05"/>
+
+ <issues ID="3" COMPONENT_ID="11" ROOT_COMPONENT_ID="10" RULE_ID="22" SEVERITY="BLOCKER" KEE="[ignore]"
+ ACTION_PLAN_KEY="PLAN-999" ASSIGNEE="julien" AUTHOR_LOGIN="[null]" CHECKSUM="FGHIJ"
+ CREATED_AT="2012-01-05" EFFORT_TO_FIX="[null]" ISSUE_ATTRIBUTES="[null]" ISSUE_CLOSE_DATE="[null]" ISSUE_CREATION_DATE="2012-01-05"
+ ISSUE_UPDATE_DATE="2013-05-18" LINE="[null]" MANUAL_SEVERITY="[true]" MESSAGE="another message" REPORTER="[null]"
+ RESOLUTION="[null]" STATUS="CONFIRMED" UPDATED_AT="2013-05-18"/>
+
+ <issues ID="4" COMPONENT_ID="11" ROOT_COMPONENT_ID="10" RULE_ID="22" SEVERITY="BLOCKER" KEE="[ignore]"
+ ACTION_PLAN_KEY="[null]" ASSIGNEE="julien" AUTHOR_LOGIN="[null]" CHECKSUM="FGHIJ"
+ CREATED_AT="2012-01-05" EFFORT_TO_FIX="[null]" ISSUE_ATTRIBUTES="[null]" ISSUE_CLOSE_DATE="[null]" ISSUE_CREATION_DATE="2012-01-05"
+ ISSUE_UPDATE_DATE="2013-05-18" LINE="[null]" MANUAL_SEVERITY="[true]" MESSAGE="another message" REPORTER="fabrice"
+ RESOLUTION="FIXED" STATUS="RESOLVED" UPDATED_AT="2013-05-18"/>
+
+ <issue_changes id="2" KEE="[ignore]" ISSUE_KEY="[ignore]" CHANGE_TYPE="comment" CHANGE_DATA="a comment" USER_LOGIN="fabrice" CREATED_AT="2012-04-28" UPDATED_AT="2012-04-29"/>
+</dataset>
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
new file mode 100644
index 00000000000..eb8d0511291
--- /dev/null
+++ b/sonar-server/src/test/resources/org/sonar/server/db/migrations/ConvertViolationsToIssuesTest/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