@@ -14,8 +14,43 @@ | |||
<description>Create SonarQube schema</description> | |||
<dependencies> | |||
<dependency> | |||
<groupId>org.sonarsource.sonarqube</groupId> | |||
<artifactId>sonar-core</artifactId> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.sonarsource.sonarqube</groupId> | |||
<artifactId>sonar-db</artifactId> | |||
</dependency> | |||
<dependency> | |||
<groupId>com.google.code.findbugs</groupId> | |||
<artifactId>jsr305</artifactId> | |||
<scope>provided</scope> | |||
</dependency> | |||
<!-- tests --> | |||
<dependency> | |||
<groupId>${project.groupId}</groupId> | |||
<artifactId>sonar-testing-harness</artifactId> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>${project.groupId}</groupId> | |||
<artifactId>sonar-db</artifactId> | |||
<version>${project.version}</version> | |||
<type>test-jar</type> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.dbunit</groupId> | |||
<artifactId>dbunit</artifactId> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>com.h2database</groupId> | |||
<artifactId>h2</artifactId> | |||
<scope>test</scope> | |||
</dependency> | |||
</dependencies> | |||
@@ -0,0 +1,48 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.platform.db.migration.history; | |||
import org.sonar.api.Startable; | |||
/** | |||
* This class is responsible for ensuring at startup that the persistence of migration history is possible. | |||
* <p> | |||
* Therefor, it will create the migration history table if it does not exist yet, update it if necessary and fail | |||
* if any of the two previous operations fails. | |||
* </p> | |||
* <p> | |||
* This class is intended to be present only in the WebServer and only the web server is the startup leader. | |||
* </p> | |||
*/ | |||
public interface MigrationHistoryTable extends Startable { | |||
String NAME = "schema_migrations"; | |||
/** | |||
* Ensures that the history of db migrations can be persisted to database: | |||
* <ul> | |||
* <li>underlying table {@code SCHEMA_MIGRATIONS} is created if it does not exist</li> | |||
* <li>underlying table {@code SCHEMA_MIGRATIONS} is updated if needed</li> | |||
* </ul> | |||
* | |||
* @throws IllegalStateException if we can not ensure that table {@code SCHEMA_MIGRATIONS} can be accessed correctly | |||
*/ | |||
@Override | |||
void start(); | |||
} |
@@ -0,0 +1,80 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.platform.db.migration.history; | |||
import java.sql.Connection; | |||
import java.sql.SQLException; | |||
import java.sql.Statement; | |||
import java.util.List; | |||
import org.sonar.api.utils.log.Loggers; | |||
import org.sonar.db.DatabaseUtils; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.version.CreateTableBuilder; | |||
import org.sonar.db.version.VarcharColumnDef; | |||
public class MigrationHistoryTableImpl implements MigrationHistoryTable { | |||
private static final String VERSION_COLUMN_NAME = "version"; | |||
private final DbClient dbClient; | |||
public MigrationHistoryTableImpl(DbClient dbClient) { | |||
this.dbClient = dbClient; | |||
} | |||
@Override | |||
public void start() { | |||
try (Connection connection = createDdlConnection(dbClient)) { | |||
if (!DatabaseUtils.tableExists(NAME, connection)) { | |||
createTable(connection); | |||
} | |||
} catch (SQLException e) { | |||
throw new IllegalStateException("Failed to create table " + NAME, e); | |||
} | |||
} | |||
private void createTable(Connection connection) throws SQLException { | |||
List<String> sqls = new CreateTableBuilder(dbClient.getDatabase().getDialect(), NAME) | |||
.addColumn(VarcharColumnDef.newVarcharColumnDefBuilder().setColumnName(VERSION_COLUMN_NAME).setIsNullable(false).setLimit(255).build()) | |||
.build(); | |||
Loggers.get(MigrationHistoryTableImpl.class).info("Creating table " + NAME); | |||
for (String sql : sqls) { | |||
execute(connection, sql); | |||
} | |||
} | |||
private static Connection createDdlConnection(DbClient dbClient) throws SQLException { | |||
Connection res = dbClient.getDatabase().getDataSource().getConnection(); | |||
res.setAutoCommit(false); | |||
return res; | |||
} | |||
private static void execute(Connection connection, String sql) throws SQLException { | |||
try (Statement stmt = connection.createStatement()) { | |||
stmt.execute(sql); | |||
connection.commit(); | |||
} | |||
} | |||
@Override | |||
public void stop() { | |||
// nothing to do | |||
} | |||
} |
@@ -0,0 +1,24 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.server.platform.db.migration.history; | |||
import javax.annotation.ParametersAreNonnullByDefault; | |||
@@ -0,0 +1,77 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.platform.db.migration.history; | |||
import java.sql.Connection; | |||
import java.sql.SQLException; | |||
import java.sql.Statement; | |||
import java.sql.Types; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.DbTester; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
public class MigrationHistoryTableImplTest { | |||
private static final String TABLE_SCHEMA_MIGRATIONS = "schema_migrations"; | |||
@Rule | |||
public DbTester dbTester = DbTester.createForSchema(System2.INSTANCE, MigrationHistoryTableImplTest.class, "empty.sql"); | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
private MigrationHistoryTableImpl underTest = new MigrationHistoryTableImpl(dbTester.getDbClient()); | |||
@Test | |||
public void start_creates_table_on_empty_schema() { | |||
underTest.start(); | |||
verifyTable(); | |||
} | |||
@Test | |||
public void start_does_not_fail_if_table_exists() throws SQLException { | |||
executeDdl("create table " + TABLE_SCHEMA_MIGRATIONS + " (version varchar(255) not null)"); | |||
verifyTable(); | |||
underTest.start(); | |||
verifyTable(); | |||
} | |||
private void executeDdl(String sql) throws SQLException { | |||
try (DbSession dbSession = dbTester.getDbClient().openSession(false); | |||
Connection connection = dbSession.getConnection()) { | |||
connection.setAutoCommit(false); | |||
try (Statement statement = connection.createStatement()) { | |||
statement.execute(sql); | |||
connection.commit(); | |||
} | |||
} | |||
} | |||
private void verifyTable() { | |||
assertThat(dbTester.countRowsOfTable(TABLE_SCHEMA_MIGRATIONS)).isEqualTo(0); | |||
dbTester.assertColumnDefinition(TABLE_SCHEMA_MIGRATIONS, "version", Types.VARCHAR, 255, false); | |||
} | |||
} |
@@ -113,6 +113,6 @@ public class DatabaseMigrator implements Startable { | |||
@VisibleForTesting | |||
protected void createSchema(Connection connection, String dialectId) { | |||
DdlUtils.createSchema(connection, dialectId); | |||
DdlUtils.createSchema(connection, dialectId, false); | |||
} | |||
} |
@@ -29,6 +29,7 @@ import org.sonar.db.version.MigrationStepModule; | |||
import org.sonar.server.platform.DefaultServerUpgradeStatus; | |||
import org.sonar.server.platform.StartupMetadataProvider; | |||
import org.sonar.server.platform.db.CheckDatabaseCharsetAtStartup; | |||
import org.sonar.server.platform.db.migration.history.MigrationHistoryTableImpl; | |||
import org.sonar.server.platform.db.migrations.DatabaseMigrator; | |||
import org.sonar.server.platform.db.migrations.PlatformDatabaseMigration; | |||
import org.sonar.server.platform.db.migrations.PlatformDatabaseMigrationExecutorServiceImpl; | |||
@@ -67,11 +68,16 @@ public class PlatformLevel2 extends PlatformLevel { | |||
// depends on plugins | |||
RailsAppsDeployer.class, | |||
DefaultI18n.class, | |||
RuleI18nManager.class, | |||
RuleI18nManager.class); | |||
// DB migration | |||
// Full Java DB Migration framework and configuration | |||
addIfStartupLeader(MigrationHistoryTableImpl.class); | |||
// platform DB migration (TODO remove call to Ruby's Active Record and use DbMigrationEngine instead) | |||
add( | |||
PlatformDatabaseMigrationExecutorServiceImpl.class, | |||
PlatformDatabaseMigration.class, | |||
PlatformDatabaseMigration.class); | |||
// Ruby DB Migration | |||
add( | |||
DatabaseMigrator.class, | |||
MigrationStepModule.class); | |||
@@ -42,7 +42,10 @@ public final class DdlUtils { | |||
/** | |||
* The connection is commited in this method but not closed. | |||
*/ | |||
public static void createSchema(Connection connection, String dialect) { | |||
public static void createSchema(Connection connection, String dialect, boolean createSchemaMigrations) { | |||
if (createSchemaMigrations) { | |||
executeScript(connection, "org/sonar/db/version/schema_migrations-" + dialect + ".ddl"); | |||
} | |||
executeScript(connection, "org/sonar/db/version/schema-" + dialect + ".ddl"); | |||
executeScript(connection, "org/sonar/db/version/rows-" + dialect + ".sql"); | |||
} |
@@ -144,7 +144,7 @@ public class DatabaseVersion { | |||
return null; | |||
} catch (RuntimeException e) { | |||
// The table SCHEMA_MIGRATIONS does not exist. | |||
// Ignore this exception -> it will be created by Ruby on Rails migrations. | |||
// Ignore this exception -> it will be created by MigrationHistoryTable class. | |||
return null; | |||
} finally { |
@@ -96,13 +96,6 @@ CREATE TABLE "SNAPSHOTS" ( | |||
CREATE INDEX "SNAPSHOT_COMPONENT" ON "SNAPSHOTS" ("COMPONENT_UUID"); | |||
CREATE UNIQUE INDEX "ANALYSES_UUID" ON "SNAPSHOTS" ("UUID"); | |||
CREATE TABLE "SCHEMA_MIGRATIONS" ( | |||
"VERSION" VARCHAR(256) NOT NULL | |||
); | |||
CREATE INDEX "UNIQUE_SCHEMA_MIGRATIONS" ON "SCHEMA_MIGRATIONS" ("VERSION"); | |||
CREATE TABLE "GROUP_ROLES" ( | |||
"ID" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), | |||
"ORGANIZATION_UUID" VARCHAR(40) NOT NULL, |
@@ -0,0 +1,4 @@ | |||
CREATE TABLE "SCHEMA_MIGRATIONS" ( | |||
"VERSION" VARCHAR(256) NOT NULL | |||
); | |||
CREATE INDEX "UNIQUE_SCHEMA_MIGRATIONS" ON "SCHEMA_MIGRATIONS" ("VERSION"); |
@@ -23,13 +23,18 @@ import java.sql.Connection; | |||
import java.sql.DriverManager; | |||
import java.sql.ResultSet; | |||
import java.sql.SQLException; | |||
import java.sql.Statement; | |||
import org.assertj.core.api.Assertions; | |||
import org.h2.Driver; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
public class DdlUtilsTest { | |||
@Rule | |||
public ExpectedException expectedException = ExpectedException.none(); | |||
@Test | |||
public void shouldSupportOnlyH2() { | |||
@@ -40,15 +45,29 @@ public class DdlUtilsTest { | |||
} | |||
@Test | |||
public void shouldCreateSchema() throws SQLException { | |||
public void shouldCreateSchema_with_schema_migrations() throws SQLException { | |||
DriverManager.registerDriver(new Driver()); | |||
Connection connection = DriverManager.getConnection("jdbc:h2:mem:sonar_test"); | |||
DdlUtils.createSchema(connection, "h2"); | |||
try (Connection connection = DriverManager.getConnection("jdbc:h2:mem:sonar_test");) { | |||
DdlUtils.createSchema(connection, "h2", true); | |||
int tableCount = countTables(connection); | |||
int tableCount = countTables(connection); | |||
assertThat(tableCount).isGreaterThan(30); | |||
connection.close(); | |||
assertThat(tableCount).isGreaterThan(30); | |||
verifySchemaMigrations(connection); | |||
} | |||
} | |||
@Test | |||
public void shouldCreateSchema_without_schema_migrations() throws SQLException { | |||
DriverManager.registerDriver(new Driver()); | |||
try (Connection connection = DriverManager.getConnection("jdbc:h2:mem:sonar_test2")) { | |||
try (Statement statement = connection.createStatement()) { | |||
statement.execute("create table schema_migrations (version varchar(255) not null)"); | |||
} | |||
DdlUtils.createSchema(connection, "h2", false); | |||
verifySchemaMigrations(connection); | |||
} | |||
} | |||
static int countTables(Connection connection) throws SQLException { | |||
@@ -60,4 +79,13 @@ public class DdlUtilsTest { | |||
resultSet.close(); | |||
return count; | |||
} | |||
private void verifySchemaMigrations(Connection connection) throws SQLException { | |||
try (Statement statement = connection.createStatement(); | |||
ResultSet resultSet = statement.executeQuery("select count(*) from schema_migrations")) { | |||
assertThat(resultSet.next()).isTrue(); | |||
assertThat(resultSet.getLong(1)).isGreaterThan(150); | |||
assertThat(resultSet.next()).isFalse(); | |||
} | |||
} | |||
} |
@@ -71,7 +71,7 @@ public class H2Database implements Database { | |||
Connection connection = null; | |||
try { | |||
connection = datasource.getConnection(); | |||
DdlUtils.createSchema(connection, "h2"); | |||
DdlUtils.createSchema(connection, "h2", true); | |||
} catch (SQLException e) { | |||
throw new IllegalStateException("Fail to create schema", e); |