--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.adhoc;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.DatabaseUtils;
+import org.sonar.server.platform.db.migration.def.BooleanColumnDef;
+import org.sonar.server.platform.db.migration.def.ColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class AbstractAddMeasuresMigratedColumnToTable extends DdlChange {
+
+ public static final String MIGRATION_FLAG_COLUMN_NAME = "measures_migrated";
+ private final String tableName;
+
+ public AbstractAddMeasuresMigratedColumnToTable(Database db, String tableName) {
+ super(db);
+ this.tableName = tableName;
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (Connection connection = getDatabase().getDataSource().getConnection()) {
+ if (!DatabaseUtils.tableColumnExists(connection, tableName, MIGRATION_FLAG_COLUMN_NAME)) {
+ ColumnDef columnDef = BooleanColumnDef.newBooleanColumnDefBuilder()
+ .setColumnName(MIGRATION_FLAG_COLUMN_NAME)
+ .setIsNullable(false)
+ .setDefaultValue(false)
+ .build();
+ context.execute(new AddColumnsBuilder(getDialect(), tableName)
+ .addColumn(columnDef)
+ .build());
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.adhoc;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.DatabaseUtils;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class AbstractCreateIndexOnMeasuresMigrated extends DdlChange {
+
+ static final String COLUMN_NAME = "measures_migrated";
+ private final String tableName;
+ private final String indexName;
+
+ public AbstractCreateIndexOnMeasuresMigrated(Database db, String tableName, String indexName) {
+ super(db);
+ this.tableName = tableName;
+ this.indexName = indexName;
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ try (Connection connection = getDatabase().getDataSource().getConnection()) {
+ if (!DatabaseUtils.indexExistsIgnoreCase(tableName, indexName, connection)) {
+ context.execute(new CreateIndexBuilder()
+ .setTable(tableName)
+ .setName(indexName)
+ .addColumn(COLUMN_NAME)
+ .build());
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.adhoc;
+
+import org.sonar.db.Database;
+
+public class AddMeasuresMigratedColumnToPortfoliosTable extends AbstractAddMeasuresMigratedColumnToTable {
+
+ static final String PORTFOLIOS_TABLE_NAME = "portfolios";
+
+ public AddMeasuresMigratedColumnToPortfoliosTable(Database db) {
+ super(db, PORTFOLIOS_TABLE_NAME);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.adhoc;
+
+import org.sonar.db.Database;
+
+public class AddMeasuresMigratedColumnToProjectBranchesTable extends AbstractAddMeasuresMigratedColumnToTable {
+
+ public static final String PROJECT_BRANCHES_TABLE_NAME = "project_branches";
+
+ public AddMeasuresMigratedColumnToProjectBranchesTable(Database db) {
+ super(db, PROJECT_BRANCHES_TABLE_NAME);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.adhoc;
+
+import org.sonar.db.Database;
+
+public class CreateIndexOnPortfoliosMeasuresMigrated extends AbstractCreateIndexOnMeasuresMigrated {
+
+ static final String TABLE_NAME = "portfolios";
+ static final String INDEX_NAME = "portfolios_measures_migrated";
+
+ public CreateIndexOnPortfoliosMeasuresMigrated(Database db) {
+ super(db, TABLE_NAME, INDEX_NAME);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.adhoc;
+
+import org.sonar.db.Database;
+
+public class CreateIndexOnProjectBranchesMeasuresMigrated extends AbstractCreateIndexOnMeasuresMigrated {
+
+ static final String TABLE_NAME = "project_branches";
+ static final String INDEX_NAME = "pb_measures_migrated";
+
+ public CreateIndexOnProjectBranchesMeasuresMigrated(Database db) {
+ super(db, TABLE_NAME, INDEX_NAME);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.adhoc;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.sql.CreateTableBuilder;
+import org.sonar.server.platform.db.migration.step.CreateTableChange;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.server.platform.db.migration.def.BigIntegerColumnDef.newBigIntegerColumnDefBuilder;
+import static org.sonar.server.platform.db.migration.def.ClobColumnDef.newClobColumnDefBuilder;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder;
+
+public class CreateMeasuresTable extends CreateTableChange {
+ static final String MEASURES_TABLE_NAME = "measures";
+ static final String COLUMN_COMPONENT_UUID = "component_uuid";
+ static final String COLUMN_BRANCH_UUID = "branch_uuid";
+ static final String COLUMN_JSON_VALUE = "json_value";
+ static final String COLUMN_JSON_VALUE_HASH = "json_value_hash";
+ static final String COLUMN_CREATED_AT = "created_at";
+ static final String COLUMN_UPDATED_AT = "updated_at";
+
+ public CreateMeasuresTable(Database db) {
+ super(db, MEASURES_TABLE_NAME);
+ }
+
+ @Override
+ public void execute(DdlChange.Context context, String tableName) throws SQLException {
+ context.execute(new CreateTableBuilder(getDialect(), tableName)
+ .addColumn(newVarcharColumnDefBuilder().setColumnName(COLUMN_COMPONENT_UUID).setIsNullable(false).setLimit(UUID_SIZE).build())
+ .addColumn(newVarcharColumnDefBuilder().setColumnName(COLUMN_BRANCH_UUID).setIsNullable(false).setLimit(UUID_SIZE).build())
+ .addColumn(newClobColumnDefBuilder().setColumnName(COLUMN_JSON_VALUE).setIsNullable(false).build())
+ .addColumn(newBigIntegerColumnDefBuilder().setColumnName(COLUMN_JSON_VALUE_HASH).setIsNullable(false).build())
+ .addColumn(newBigIntegerColumnDefBuilder().setColumnName(COLUMN_CREATED_AT).setIsNullable(false).build())
+ .addColumn(newBigIntegerColumnDefBuilder().setColumnName(COLUMN_UPDATED_AT).setIsNullable(false).build())
+ .build());
+ }
+}
--- /dev/null
+Those migrations are not meant to be executed during automatic or manual migrations.
+They are meant to be executed on a specific action during the normal runtime of the server, for example on a call of some API endpoint.
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.platform.db.migration.adhoc;
+
+import javax.annotation.ParametersAreNonnullByDefault;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.adhoc;
+
+import java.sql.SQLException;
+import java.sql.Types;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.db.CoreDbTester;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode;
+import static org.sonar.server.platform.db.migration.adhoc.AddMeasuresMigratedColumnToPortfoliosTable.MIGRATION_FLAG_COLUMN_NAME;
+import static org.sonar.server.platform.db.migration.adhoc.AddMeasuresMigratedColumnToPortfoliosTable.PORTFOLIOS_TABLE_NAME;
+
+public class AddMeasuresMigratedColumnToPortfoliosTableTest {
+
+ @Rule
+ public final CoreDbTester db = CoreDbTester.createForSchema(AddMeasuresMigratedColumnToPortfoliosTableTest.class, "schema.sql");
+ private final AddMeasuresMigratedColumnToPortfoliosTable underTest = new AddMeasuresMigratedColumnToPortfoliosTable(db.database());
+
+ @Test
+ public void execute_whenColumnDoesNotExist_shouldCreateColumn() throws SQLException {
+ db.assertColumnDoesNotExist(PORTFOLIOS_TABLE_NAME, MIGRATION_FLAG_COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(PORTFOLIOS_TABLE_NAME, MIGRATION_FLAG_COLUMN_NAME, Types.BOOLEAN, null, false);
+ }
+
+ @Test
+ public void execute_whenColumnAlreadyExists_shouldNotFail() throws SQLException {
+ underTest.execute();
+ assertThatCode(underTest::execute).doesNotThrowAnyException();
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.adhoc;
+
+import java.sql.SQLException;
+import java.sql.Types;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.db.CoreDbTester;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode;
+import static org.sonar.server.platform.db.migration.adhoc.AddMeasuresMigratedColumnToProjectBranchesTable.MIGRATION_FLAG_COLUMN_NAME;
+import static org.sonar.server.platform.db.migration.adhoc.AddMeasuresMigratedColumnToProjectBranchesTable.PROJECT_BRANCHES_TABLE_NAME;
+
+public class AddMeasuresMigratedColumnToProjectBranchesTableTest {
+
+ @Rule
+ public final CoreDbTester db = CoreDbTester.createForSchema(AddMeasuresMigratedColumnToProjectBranchesTableTest.class, "schema.sql");
+ private final AddMeasuresMigratedColumnToProjectBranchesTable underTest = new AddMeasuresMigratedColumnToProjectBranchesTable(db.database());
+
+ @Test
+ public void execute_whenColumnDoesNotExist_shouldCreateColumn() throws SQLException {
+ db.assertColumnDoesNotExist(PROJECT_BRANCHES_TABLE_NAME, MIGRATION_FLAG_COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(PROJECT_BRANCHES_TABLE_NAME, MIGRATION_FLAG_COLUMN_NAME, Types.BOOLEAN, null, false);
+ }
+
+ @Test
+ public void execute_whenColumnAlreadyExists_shouldNotFail() throws SQLException {
+ underTest.execute();
+ assertThatCode(underTest::execute).doesNotThrowAnyException();
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.adhoc;
+
+import java.sql.SQLException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.db.CoreDbTester;
+
+import static org.sonar.server.platform.db.migration.adhoc.CreateIndexOnPortfoliosMeasuresMigrated.COLUMN_NAME;
+import static org.sonar.server.platform.db.migration.adhoc.CreateIndexOnPortfoliosMeasuresMigrated.TABLE_NAME;
+
+public class CreateIndexOnPortfoliosMeasuresMigratedTest {
+
+ private static final String INDEX_NAME = "portfolios_measures_migrated";
+ @Rule
+ public final CoreDbTester db = CoreDbTester.createForSchema(CreateIndexOnPortfoliosMeasuresMigratedTest.class, "schema.sql");
+ private final CreateIndexOnPortfoliosMeasuresMigrated underTest = new CreateIndexOnPortfoliosMeasuresMigrated(db.database());
+
+ @Test
+ public void migration_should_create_index() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+
+ underTest.execute();
+
+ db.assertIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME);
+ }
+
+ @Test
+ public void migration_should_be_reentrant() throws SQLException {
+ underTest.execute();
+ underTest.execute();
+
+ db.assertIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.adhoc;
+
+import java.sql.SQLException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.db.CoreDbTester;
+
+import static org.sonar.server.platform.db.migration.adhoc.CreateIndexOnProjectBranchesMeasuresMigrated.COLUMN_NAME;
+import static org.sonar.server.platform.db.migration.adhoc.CreateIndexOnProjectBranchesMeasuresMigrated.TABLE_NAME;
+
+public class CreateIndexOnProjectBranchesMeasuresMigratedTest {
+
+ private static final String INDEX_NAME = "pb_measures_migrated";
+
+ @Rule
+ public final CoreDbTester db = CoreDbTester.createForSchema(CreateIndexOnProjectBranchesMeasuresMigratedTest.class, "schema.sql");
+ private final CreateIndexOnProjectBranchesMeasuresMigrated underTest = new CreateIndexOnProjectBranchesMeasuresMigrated(db.database());
+
+ @Test
+ public void migration_should_create_index() throws SQLException {
+ db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME);
+
+ underTest.execute();
+
+ db.assertIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME);
+ }
+
+ @Test
+ public void migration_should_be_reentrant() throws SQLException {
+ underTest.execute();
+ underTest.execute();
+
+ db.assertIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.adhoc;
+
+import java.sql.SQLException;
+import java.sql.Types;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.db.CoreDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.server.platform.db.migration.adhoc.CreateMeasuresTable.COLUMN_BRANCH_UUID;
+import static org.sonar.server.platform.db.migration.adhoc.CreateMeasuresTable.COLUMN_COMPONENT_UUID;
+import static org.sonar.server.platform.db.migration.adhoc.CreateMeasuresTable.COLUMN_CREATED_AT;
+import static org.sonar.server.platform.db.migration.adhoc.CreateMeasuresTable.COLUMN_JSON_VALUE;
+import static org.sonar.server.platform.db.migration.adhoc.CreateMeasuresTable.COLUMN_JSON_VALUE_HASH;
+import static org.sonar.server.platform.db.migration.adhoc.CreateMeasuresTable.COLUMN_UPDATED_AT;
+import static org.sonar.server.platform.db.migration.adhoc.CreateMeasuresTable.MEASURES_TABLE_NAME;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+
+public class CreateMeasuresTableTest {
+
+ @Rule
+ public final CoreDbTester db = CoreDbTester.createEmpty();
+
+ private final DdlChange underTest = new CreateMeasuresTable(db.database());
+
+ @Test
+ public void execute_shouldCreateTable() throws SQLException {
+ db.assertTableDoesNotExist(MEASURES_TABLE_NAME);
+
+ underTest.execute();
+
+ db.assertTableExists(MEASURES_TABLE_NAME);
+ db.assertNoPrimaryKey(MEASURES_TABLE_NAME);
+ db.assertColumnDefinition(MEASURES_TABLE_NAME, COLUMN_COMPONENT_UUID, Types.VARCHAR, UUID_SIZE, false);
+ db.assertColumnDefinition(MEASURES_TABLE_NAME, COLUMN_BRANCH_UUID, Types.VARCHAR, UUID_SIZE, false);
+ db.assertColumnDefinition(MEASURES_TABLE_NAME, COLUMN_JSON_VALUE, Types.CLOB, null, false);
+ db.assertColumnDefinition(MEASURES_TABLE_NAME, COLUMN_JSON_VALUE_HASH, Types.BIGINT, null, false);
+ db.assertColumnDefinition(MEASURES_TABLE_NAME, COLUMN_CREATED_AT, Types.BIGINT, null, false);
+ db.assertColumnDefinition(MEASURES_TABLE_NAME, COLUMN_UPDATED_AT, Types.BIGINT, null, false);
+ }
+
+ @Test
+ public void execute_shouldBeReentrant() throws SQLException {
+ db.assertTableDoesNotExist(MEASURES_TABLE_NAME);
+
+ underTest.execute();
+ underTest.execute();
+
+ db.assertTableExists(MEASURES_TABLE_NAME);
+ }
+}
--- /dev/null
+CREATE TABLE "PORTFOLIOS"(
+ "UUID" CHARACTER VARYING(40) NOT NULL,
+ "KEE" CHARACTER VARYING(400) NOT NULL,
+ "NAME" CHARACTER VARYING(2000) NOT NULL,
+ "DESCRIPTION" CHARACTER VARYING(2000),
+ "ROOT_UUID" CHARACTER VARYING(40) NOT NULL,
+ "PARENT_UUID" CHARACTER VARYING(40),
+ "PRIVATE" BOOLEAN NOT NULL,
+ "SELECTION_MODE" CHARACTER VARYING(50) NOT NULL,
+ "SELECTION_EXPRESSION" CHARACTER VARYING(4000),
+ "CREATED_AT" BIGINT NOT NULL,
+ "UPDATED_AT" BIGINT NOT NULL,
+ "BRANCH_KEY" CHARACTER VARYING(255)
+);
+ALTER TABLE "PORTFOLIOS" ADD CONSTRAINT "PK_PORTFOLIOS" PRIMARY KEY("UUID");
+CREATE UNIQUE INDEX "UNIQ_PORTFOLIOS_KEE" ON "PORTFOLIOS"("KEE" NULLS FIRST);
--- /dev/null
+CREATE TABLE "PROJECT_BRANCHES"(
+ "UUID" CHARACTER VARYING(50) NOT NULL,
+ "PROJECT_UUID" CHARACTER VARYING(50) NOT NULL,
+ "KEE" CHARACTER VARYING(255) NOT NULL,
+ "BRANCH_TYPE" CHARACTER VARYING(12) NOT NULL,
+ "MERGE_BRANCH_UUID" CHARACTER VARYING(50),
+ "PULL_REQUEST_BINARY" BINARY LARGE OBJECT,
+ "MANUAL_BASELINE_ANALYSIS_UUID" CHARACTER VARYING(40),
+ "CREATED_AT" BIGINT NOT NULL,
+ "UPDATED_AT" BIGINT NOT NULL,
+ "EXCLUDE_FROM_PURGE" BOOLEAN DEFAULT FALSE NOT NULL,
+ "NEED_ISSUE_SYNC" BOOLEAN NOT NULL
+);
+ALTER TABLE "PROJECT_BRANCHES" ADD CONSTRAINT "PK_PROJECT_BRANCHES" PRIMARY KEY("UUID");
+CREATE UNIQUE INDEX "UNIQ_PROJECT_BRANCHES" ON "PROJECT_BRANCHES"("BRANCH_TYPE" NULLS FIRST, "PROJECT_UUID" NULLS FIRST, "KEE" NULLS FIRST);
--- /dev/null
+CREATE TABLE "PORTFOLIOS"(
+ "UUID" CHARACTER VARYING(40) NOT NULL,
+ "KEE" CHARACTER VARYING(400) NOT NULL,
+ "NAME" CHARACTER VARYING(2000) NOT NULL,
+ "DESCRIPTION" CHARACTER VARYING(2000),
+ "ROOT_UUID" CHARACTER VARYING(40) NOT NULL,
+ "PARENT_UUID" CHARACTER VARYING(40),
+ "PRIVATE" BOOLEAN NOT NULL,
+ "SELECTION_MODE" CHARACTER VARYING(50) NOT NULL,
+ "SELECTION_EXPRESSION" CHARACTER VARYING(4000),
+ "CREATED_AT" BIGINT NOT NULL,
+ "UPDATED_AT" BIGINT NOT NULL,
+ "BRANCH_KEY" CHARACTER VARYING(255),
+ "MEASURES_MIGRATED" BOOLEAN NOT NULL DEFAULT FALSE
+);
+ALTER TABLE "PORTFOLIOS" ADD CONSTRAINT "PK_PORTFOLIOS" PRIMARY KEY("UUID");
+CREATE UNIQUE INDEX "UNIQ_PORTFOLIOS_KEE" ON "PORTFOLIOS"("KEE" NULLS FIRST);
--- /dev/null
+CREATE TABLE "PROJECT_BRANCHES"(
+ "UUID" CHARACTER VARYING(50) NOT NULL,
+ "PROJECT_UUID" CHARACTER VARYING(50) NOT NULL,
+ "KEE" CHARACTER VARYING(255) NOT NULL,
+ "BRANCH_TYPE" CHARACTER VARYING(12) NOT NULL,
+ "MERGE_BRANCH_UUID" CHARACTER VARYING(50),
+ "PULL_REQUEST_BINARY" BINARY LARGE OBJECT,
+ "MANUAL_BASELINE_ANALYSIS_UUID" CHARACTER VARYING(40),
+ "CREATED_AT" BIGINT NOT NULL,
+ "UPDATED_AT" BIGINT NOT NULL,
+ "EXCLUDE_FROM_PURGE" BOOLEAN DEFAULT FALSE NOT NULL,
+ "NEED_ISSUE_SYNC" BOOLEAN NOT NULL,
+ "MEASURES_MIGRATED" BOOLEAN NOT NULL DEFAULT FALSE
+);
+ALTER TABLE "PROJECT_BRANCHES" ADD CONSTRAINT "PK_PROJECT_BRANCHES" PRIMARY KEY("UUID");
+CREATE UNIQUE INDEX "UNIQ_PROJECT_BRANCHES" ON "PROJECT_BRANCHES"("BRANCH_TYPE" NULLS FIRST, "PROJECT_UUID" NULLS FIRST, "KEE" NULLS FIRST);
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.ws;
+
+import com.google.common.io.Resources;
+import java.sql.SQLException;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.server.ws.WebService.NewAction;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.db.DbClient;
+import org.sonar.db.property.PropertyDto;
+import org.sonar.server.platform.db.migration.adhoc.AddMeasuresMigratedColumnToPortfoliosTable;
+import org.sonar.server.platform.db.migration.adhoc.AddMeasuresMigratedColumnToProjectBranchesTable;
+import org.sonar.server.platform.db.migration.adhoc.CreateIndexOnPortfoliosMeasuresMigrated;
+import org.sonar.server.platform.db.migration.adhoc.CreateIndexOnProjectBranchesMeasuresMigrated;
+import org.sonar.server.platform.db.migration.adhoc.CreateMeasuresTable;
+import org.sonar.server.user.UserSession;
+
+import static java.lang.String.format;
+
+/**
+ * Implementation of the {@code prepare_migration} action for the System WebService.
+ */
+public class PrepareMigrationAction implements SystemWsAction {
+
+ public static final String PARAM_ENABLE = "enable";
+ public static final String SYSTEM_MEASURES_MIGRATION_ENABLED = "system.measures.migration.enabled";
+ private final UserSession userSession;
+ private final DbClient dbClient;
+ private final CreateMeasuresTable createMeasuresTable;
+ private final AddMeasuresMigratedColumnToProjectBranchesTable addMeasuresMigratedColumnToProjectBranchesTable;
+ private final AddMeasuresMigratedColumnToPortfoliosTable addMeasuresMigratedColumnToPortfoliosTable;
+ private final CreateIndexOnProjectBranchesMeasuresMigrated createIndexOnProjectBranchesMeasuresMigrated;
+ private final CreateIndexOnPortfoliosMeasuresMigrated createIndexOnPortfoliosMeasuresMigrated;
+
+ public PrepareMigrationAction(UserSession userSession, DbClient dbClient, CreateMeasuresTable createMeasuresTable,
+ AddMeasuresMigratedColumnToProjectBranchesTable addMeasuresMigratedColumnToProjectBranchesTable,
+ AddMeasuresMigratedColumnToPortfoliosTable addMeasuresMigratedColumnToPortfoliosTable,
+ CreateIndexOnProjectBranchesMeasuresMigrated createIndexOnProjectBranchesMeasuresMigrated,
+ CreateIndexOnPortfoliosMeasuresMigrated createIndexOnPortfoliosMeasuresMigrated) {
+ this.userSession = userSession;
+ this.dbClient = dbClient;
+ this.createMeasuresTable = createMeasuresTable;
+ this.addMeasuresMigratedColumnToProjectBranchesTable = addMeasuresMigratedColumnToProjectBranchesTable;
+ this.addMeasuresMigratedColumnToPortfoliosTable = addMeasuresMigratedColumnToPortfoliosTable;
+ this.createIndexOnProjectBranchesMeasuresMigrated = createIndexOnProjectBranchesMeasuresMigrated;
+ this.createIndexOnPortfoliosMeasuresMigrated = createIndexOnPortfoliosMeasuresMigrated;
+ }
+
+ @Override
+ public void define(WebService.NewController controller) {
+ NewAction action = controller.createAction("prepare_migration")
+ .setDescription("Prepare the migration to the next major version of SonarQube." +
+ "<br/>" +
+ "Sending a POST request to this URL enables the 'live_measures' table migration. " +
+ "It is strongly advised to <strong>make a database backup</strong> before invoking this WS. " +
+ "Requires system administration permission.")
+ .setSince("9.9.8")
+ .setPost(true)
+ .setHandler(this)
+ .setInternal(true)
+ .setResponseExample(Resources.getResource(this.getClass(), "example-prepare_migration.json"));
+
+ action.createParam(PARAM_ENABLE)
+ .setDescription("Set to true to enable the migration mode. Set to false to disable.")
+ .setBooleanPossibleValues()
+ .setRequired(true);
+ }
+
+ @Override
+ public void handle(Request request, Response response) throws Exception {
+ userSession.checkIsSystemAdministrator();
+
+ boolean enable = request.mandatoryParamAsBoolean(PARAM_ENABLE);
+ if (enable) {
+ updateDdl();
+ }
+ updateProperty(enable);
+
+ try (JsonWriter json = response.newJsonWriter()) {
+ json.beginObject()
+ .prop("message", format("The 'live_measures' migration mode is %s", enable ? "enabled" : "disabled"))
+ .endObject();
+ }
+ }
+
+ private void updateDdl() throws SQLException {
+ createMeasuresTable.execute();
+ addMeasuresMigratedColumnToProjectBranchesTable.execute();
+ addMeasuresMigratedColumnToPortfoliosTable.execute();
+ createIndexOnProjectBranchesMeasuresMigrated.execute();
+ createIndexOnPortfoliosMeasuresMigrated.execute();
+ }
+
+ private void updateProperty(boolean enable) {
+ dbClient.propertiesDao().saveProperty(new PropertyDto().setKey(SYSTEM_MEASURES_MIGRATION_ENABLED).setValue(Boolean.toString(enable)));
+ }
+
+}
package org.sonar.server.platform.ws;
import org.sonar.core.platform.Module;
+import org.sonar.server.platform.db.migration.adhoc.AddMeasuresMigratedColumnToPortfoliosTable;
+import org.sonar.server.platform.db.migration.adhoc.AddMeasuresMigratedColumnToProjectBranchesTable;
+import org.sonar.server.platform.db.migration.adhoc.CreateIndexOnPortfoliosMeasuresMigrated;
+import org.sonar.server.platform.db.migration.adhoc.CreateIndexOnProjectBranchesMeasuresMigrated;
+import org.sonar.server.platform.db.migration.adhoc.CreateMeasuresTable;
public class SystemWsModule extends Module {
LivenessActionSupport.class,
LivenessAction.class,
+ CreateMeasuresTable.class,
+ AddMeasuresMigratedColumnToProjectBranchesTable.class,
+ AddMeasuresMigratedColumnToPortfoliosTable.class,
+ CreateIndexOnProjectBranchesMeasuresMigrated.class,
+ CreateIndexOnPortfoliosMeasuresMigrated.class,
+ PrepareMigrationAction.class,
+
InfoAction.class,
LogsAction.class,
MigrateDbAction.class,
--- /dev/null
+{
+ "message": "The 'live_measures' migration mode is enabled"
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.ws;
+
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import java.sql.SQLException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.sonar.db.DbTester;
+import org.sonar.db.property.PropertyDto;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.platform.db.migration.adhoc.AddMeasuresMigratedColumnToPortfoliosTable;
+import org.sonar.server.platform.db.migration.adhoc.AddMeasuresMigratedColumnToProjectBranchesTable;
+import org.sonar.server.platform.db.migration.adhoc.CreateIndexOnPortfoliosMeasuresMigrated;
+import org.sonar.server.platform.db.migration.adhoc.CreateIndexOnProjectBranchesMeasuresMigrated;
+import org.sonar.server.platform.db.migration.adhoc.CreateMeasuresTable;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.TestResponse;
+import org.sonar.server.ws.WsActionTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.sonar.server.platform.ws.PrepareMigrationAction.SYSTEM_MEASURES_MIGRATION_ENABLED;
+import static org.sonar.test.JsonAssert.assertJson;
+
+@RunWith(DataProviderRunner.class)
+public class PrepareMigrationActionTest {
+
+ public static final String PARAM_ENABLE = "enable";
+ @Rule
+ public UserSessionRule userSessionRule = UserSessionRule.standalone().logIn().setSystemAdministrator();
+
+ @Rule
+ public DbTester dbTester = DbTester.create();
+
+ private final CreateMeasuresTable createMeasuresTable = mock();
+ private final AddMeasuresMigratedColumnToProjectBranchesTable addMeasuresMigratedColumnToProjectBranchesTable = mock();
+ private final AddMeasuresMigratedColumnToPortfoliosTable addMeasuresMigratedColumnToPortfoliosTable = mock();
+ private final CreateIndexOnProjectBranchesMeasuresMigrated createIndexOnProjectBranchesMeasuresMigrated = mock();
+ private final CreateIndexOnPortfoliosMeasuresMigrated createIndexOnPortfoliosMeasuresMigrated = mock();
+
+ private final PrepareMigrationAction underTest = new PrepareMigrationAction(userSessionRule, dbTester.getDbClient(), createMeasuresTable,
+ addMeasuresMigratedColumnToProjectBranchesTable, addMeasuresMigratedColumnToPortfoliosTable, createIndexOnProjectBranchesMeasuresMigrated, createIndexOnPortfoliosMeasuresMigrated);
+ private final WsActionTester tester = new WsActionTester(underTest);
+
+ @Test
+ public void should_throw_if_enable_parameter_is_missing() {
+ TestRequest request = tester.newRequest();
+
+ assertThatIllegalArgumentException()
+ .isThrownBy(request::execute)
+ .withMessage("The 'enable' parameter is missing");
+ }
+
+ @Test
+ public void verify_example() {
+ TestResponse response = tester.newRequest()
+ .setParam(PARAM_ENABLE, "true")
+ .execute();
+
+ assertJson(response.getInput()).isSimilarTo(getClass().getResource("example-prepare_migration.json"));
+ }
+
+ @Test
+ public void throws_ForbiddenException_if_user_is_not_logged_in() {
+ userSessionRule.anonymous();
+
+ TestRequest request = tester.newRequest();
+
+ assertThatExceptionOfType(ForbiddenException.class)
+ .isThrownBy(request::execute);
+ }
+
+ @Test
+ public void throws_ForbiddenException_if_user_is_not_system_admin() {
+ userSessionRule.logIn();
+
+ TestRequest request = tester.newRequest();
+
+ assertThatExceptionOfType(ForbiddenException.class)
+ .isThrownBy(request::execute);
+ }
+
+ @Test
+ @DataProvider(value = {"true", "yes"})
+ public void should_enable_migration(String enableParamValue) throws SQLException {
+ assertThat(getPropertyValue()).isNull();
+
+ TestResponse response = tester.newRequest()
+ .setParam(PARAM_ENABLE, enableParamValue)
+ .execute();
+
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThat(getPropertyValue()).isTrue();
+
+ verify(createMeasuresTable).execute();
+ verify(addMeasuresMigratedColumnToProjectBranchesTable).execute();
+ verify(addMeasuresMigratedColumnToPortfoliosTable).execute();
+ verify(createIndexOnProjectBranchesMeasuresMigrated).execute();
+ verify(createIndexOnPortfoliosMeasuresMigrated).execute();
+
+ // reentrant
+ response = tester.newRequest()
+ .setParam(PARAM_ENABLE, enableParamValue)
+ .execute();
+
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThat(getPropertyValue()).isTrue();
+ }
+
+ @Test
+ public void property_is_unchanged_if_the_migrations_failed() throws SQLException {
+ doThrow(new SQLException("Oops")).when(createMeasuresTable).execute();
+
+ TestRequest request = tester.newRequest()
+ .setParam(PARAM_ENABLE, "true");
+
+ assertThatExceptionOfType(RuntimeException.class)
+ .isThrownBy(request::execute);
+
+ assertThat(getPropertyValue()).isNull();
+ }
+
+ @Test
+ @DataProvider(value = {"false", "no"})
+ public void should_disable_migration(String disableParamValue) {
+ dbTester.getDbClient().propertiesDao().saveProperty(new PropertyDto().setKey(SYSTEM_MEASURES_MIGRATION_ENABLED).setValue("true"));
+
+ TestResponse response = tester.newRequest()
+ .setParam(PARAM_ENABLE, disableParamValue)
+ .execute();
+
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThat(getPropertyValue()).isFalse();
+
+ verifyNoInteractions(createMeasuresTable, addMeasuresMigratedColumnToPortfoliosTable, addMeasuresMigratedColumnToProjectBranchesTable,
+ createIndexOnProjectBranchesMeasuresMigrated, createIndexOnPortfoliosMeasuresMigrated);
+
+ // reentrant
+ response = tester.newRequest()
+ .setParam(PARAM_ENABLE, disableParamValue)
+ .execute();
+
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThat(getPropertyValue()).isFalse();
+ }
+
+ private Boolean getPropertyValue() {
+ PropertyDto propertyDto = dbTester.getDbClient().propertiesDao().selectGlobalProperty(SYSTEM_MEASURES_MIGRATION_ENABLED);
+ if (propertyDto == null) {
+ return null;
+ }
+ return Boolean.parseBoolean(propertyDto.getValue());
+ }
+
+}
public void verify_count_of_added_components() {
ListContainer container = new ListContainer();
new SystemWsModule().configure(container);
- assertThat(container.getAddedObjects()).hasSize(15);
+ assertThat(container.getAddedObjects()).hasSize(21);
}
}