aboutsummaryrefslogtreecommitdiffstats
path: root/sonar-db
diff options
context:
space:
mode:
authorJenkins CI <ci@sonarsource.com>2016-04-26 08:07:44 +0200
committerJenkins CI <ci@sonarsource.com>2016-04-26 08:07:44 +0200
commit52f0e092b0dd4d6aaa2e1b0543b094ce7c79176b (patch)
treed3c9e04d32714dbd777d53c01972877019171526 /sonar-db
parent4fa52cd10f951bd2a0e66acd480e49158099c435 (diff)
parentc87a69b77de990c7efdf506b5a269e7facdb00b9 (diff)
downloadsonarqube-52f0e092b0dd4d6aaa2e1b0543b094ce7c79176b.tar.gz
sonarqube-52f0e092b0dd4d6aaa2e1b0543b094ce7c79176b.zip
Automatic merge from branch-5.5
* origin/branch-5.5: SONAR-7549 Automatic repair of MSSQL and MySQL case-insensitive columns SONAR-7549 SONAR-6171 verifies UTF8 charset and case-sensitive collation Remove bad comment in org.sonar.server.computation.measure.MeasureRepository
Diffstat (limited to 'sonar-db')
-rw-r--r--sonar-db/src/main/java/org/sonar/db/CollationChecker.java241
-rw-r--r--sonar-db/src/main/java/org/sonar/db/charset/CharsetHandler.java65
-rw-r--r--sonar-db/src/main/java/org/sonar/db/charset/ColumnDef.java92
-rw-r--r--sonar-db/src/main/java/org/sonar/db/charset/DatabaseCharsetChecker.java89
-rw-r--r--sonar-db/src/main/java/org/sonar/db/charset/MssqlCharsetHandler.java139
-rw-r--r--sonar-db/src/main/java/org/sonar/db/charset/MysqlCharsetHandler.java99
-rw-r--r--sonar-db/src/main/java/org/sonar/db/charset/OracleCharsetHandler.java52
-rw-r--r--sonar-db/src/main/java/org/sonar/db/charset/PostgresCharsetHandler.java80
-rw-r--r--sonar-db/src/main/java/org/sonar/db/charset/SqlExecutor.java80
-rw-r--r--sonar-db/src/main/java/org/sonar/db/charset/package-info.java24
-rw-r--r--sonar-db/src/test/java/org/sonar/db/CollationCheckerTest.java232
-rw-r--r--sonar-db/src/test/java/org/sonar/db/charset/DatabaseCharsetCheckerTest.java111
-rw-r--r--sonar-db/src/test/java/org/sonar/db/charset/MssqlCharsetHandlerTest.java111
-rw-r--r--sonar-db/src/test/java/org/sonar/db/charset/MysqlCharsetHandlerTest.java104
-rw-r--r--sonar-db/src/test/java/org/sonar/db/charset/OracleCharsetHandlerTest.java100
-rw-r--r--sonar-db/src/test/java/org/sonar/db/charset/PostgresCharsetHandlerTest.java112
-rw-r--r--sonar-db/src/test/java/org/sonar/db/charset/SelectExecutorTest.java56
-rw-r--r--sonar-db/src/test/java/org/sonar/db/charset/SqlExecutorTest.java75
18 files changed, 1389 insertions, 473 deletions
diff --git a/sonar-db/src/main/java/org/sonar/db/CollationChecker.java b/sonar-db/src/main/java/org/sonar/db/CollationChecker.java
deleted file mode 100644
index b09d9233c3b..00000000000
--- a/sonar-db/src/main/java/org/sonar/db/CollationChecker.java
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
- * 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.db;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Joiner;
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.List;
-import javax.annotation.CheckForNull;
-import org.apache.commons.lang.StringUtils;
-import org.picocontainer.Startable;
-import org.sonar.api.utils.MessageException;
-import org.sonar.api.utils.log.Loggers;
-import org.sonar.db.dialect.H2;
-import org.sonar.db.dialect.MsSql;
-import org.sonar.db.dialect.MySql;
-import org.sonar.db.dialect.Oracle;
-import org.sonar.db.dialect.PostgreSql;
-
-import static java.lang.String.format;
-import static org.apache.commons.lang.StringUtils.containsIgnoreCase;
-import static org.apache.commons.lang.StringUtils.endsWithIgnoreCase;
-
-/**
- * SONAR-6171
- * Check that database has UTF8 character set and case-sensitive collation.
- * As obviously tables must be checked after being created, this component
- * must not be executed at the same time as {@link DatabaseChecker}.
- */
-public class CollationChecker implements Startable {
-
- private static final String UTF8 = "utf8";
- private final Database db;
- private final StatementExecutor statementExecutor;
-
- public CollationChecker(Database db) {
- this(db, new StatementExecutor());
- }
-
- @VisibleForTesting
- CollationChecker(Database db, StatementExecutor statementExecutor) {
- this.db = db;
- this.statementExecutor = statementExecutor;
- }
-
- @Override
- public void start() {
- try {
- Loggers.get(getClass()).info("Verify database collation");
- check();
- } catch (SQLException e) {
- throw new IllegalStateException(e);
- }
- }
-
- @Override
- public void stop() {
- // nothing to do
- }
-
- private void check() throws SQLException {
- try (Connection connection = db.getDataSource().getConnection()) {
- switch (db.getDialect().getId()) {
- case H2.ID:
- // nothing to check
- break;
- case Oracle.ID:
- checkOracle(connection);
- break;
- case PostgreSql.ID:
- checkPostgreSql(connection);
- break;
- case MySql.ID:
- checkMySql(connection);
- break;
- case MsSql.ID:
- checkMsSql(connection);
- break;
- default:
- throw new IllegalArgumentException("Database not supported: " + db.getDialect().getId());
- }
- }
- }
-
- /**
- * Oracle does not allow to override character set on tables. Only global charset is verified.
- */
- private void checkOracle(Connection connection) throws SQLException {
- String charset = selectSingleCell(connection, "select value from nls_database_parameters where parameter='NLS_CHARACTERSET'");
- String sort = selectSingleCell(connection, "select value from nls_database_parameters where parameter='NLS_SORT'");
- if (!containsIgnoreCase(charset, UTF8) || !"BINARY".equalsIgnoreCase(sort)) {
- throw MessageException.of(format("Oracle must be have UTF8 charset and BINARY sort. NLS_CHARACTERSET is %s and NLS_SORT is %s.", charset, sort));
- }
- }
-
- /**
- * PostgreSQL does not support case-insensitive collations. Only character set must be verified.
- */
- private void checkPostgreSql(Connection connection) throws SQLException {
- // Character set is defined globally and can be overridden on each column.
- // This request returns all VARCHAR columns. Collation may be empty.
- // Examples:
- // issues | key | ''
- // projects | name | utf8
- List<String[]> rows = select(connection, "select table_name, column_name, collation_name " +
- "from information_schema.columns " +
- "where table_schema='public' " +
- "and udt_name='varchar' " +
- "order by table_name, column_name", 3);
- boolean mustCheckGlobalCollation = false;
- List<String> errors = new ArrayList<>();
- for (String[] row : rows) {
- if (StringUtils.isBlank(row[2])) {
- mustCheckGlobalCollation = true;
- } else if (!containsIgnoreCase(row[2], UTF8)) {
- errors.add(format("%s.%s", row[0], row[1]));
- }
- }
-
- if (mustCheckGlobalCollation) {
- String charset = selectSingleCell(connection, "SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database()");
- if (!containsIgnoreCase(charset, UTF8)) {
- throw MessageException.of(format("Database charset is %s. It must be UTF8.", charset));
- }
- }
- if (!errors.isEmpty()) {
- throw MessageException.of(format("Database columns [%s] must have UTF8 charset.", Joiner.on(", ").join(errors)));
- }
- }
-
- /**
- * Check VARCHAR columns
- */
- private void checkMySql(Connection connection) throws SQLException {
- // All VARCHAR columns are returned. No need to check database general collation.
- // Example of row:
- // issues | kee | utf8 | utf8_bin
- List<String[]> rows = select(connection,
- "SELECT table_name, column_name, character_set_name, collation_name " +
- "FROM INFORMATION_SCHEMA.columns " +
- "WHERE table_schema=database() and character_set_name is not null and collation_name is not null", 4 /* columns */);
- List<String> errors = new ArrayList<>();
- for (String[] row : rows) {
- if (!containsIgnoreCase(row[2], UTF8) || endsWithIgnoreCase(row[3], "_ci")) {
- errors.add(format("%s.%s", row[0], row[1]));
- }
- }
- if (!errors.isEmpty()) {
- throw MessageException.of(format("UTF8 charset and case-sensitive collation are required for database columns [%s]", Joiner.on(", ").join(errors)));
- }
- }
-
- private void checkMsSql(Connection connection) throws SQLException {
- // All VARCHAR columns are returned. No need to check database general collation.
- // Example of row:
- // issues | kee | Latin1_General_CS_AS
- List<String[]> rows = select(connection,
- "SELECT table_name, column_name, collation_name " +
- "FROM [INFORMATION_SCHEMA].[COLUMNS] " +
- "WHERE collation_name is not null " +
- "ORDER BY table_name,column_name", 3 /* columns */);
- List<String> errors = new ArrayList<>();
- for (String[] row : rows) {
- if (!endsWithIgnoreCase(row[2], "_CS_AS")) {
- errors.add(row[0] + "." + row[1]);
- }
- }
- if (!errors.isEmpty()) {
- throw MessageException.of(format("Case-sensitive and accent-sensitive charset (CS_AS) is required for database columns [%s]", Joiner.on(", ").join(errors)));
- }
- }
-
- @CheckForNull
- private String selectSingleCell(Connection connection, String sql) throws SQLException {
- String[] cols = selectSingleRow(connection, sql, 1);
- return cols == null ? null : cols[0];
- }
-
- @CheckForNull
- private String[] selectSingleRow(Connection connection, String sql, int columns) throws SQLException {
- List<String[]> rows = select(connection, sql, columns);
- if (rows.isEmpty()) {
- return null;
- }
- if (rows.size() == 1) {
- return rows.get(0);
- }
- throw new IllegalStateException("Expecting only one result for [" + sql + "]");
- }
-
- private List<String[]> select(Connection connection, String sql, int columns) throws SQLException {
- return statementExecutor.executeQuery(connection, sql, columns);
- }
-
- @VisibleForTesting
- static class StatementExecutor {
- List<String[]> executeQuery(Connection connection, String sql, int columns) throws SQLException {
- Statement stmt = null;
- ResultSet rs = null;
- try {
- stmt = connection.createStatement();
- rs = stmt.executeQuery(sql);
- List<String[]> result = new ArrayList<>();
- while (rs.next()) {
- String[] row = new String[columns];
- for (int i = 0; i < columns; i++) {
- row[i] = DatabaseUtils.getString(rs, i + 1);
- }
- result.add(row);
- }
- return result;
-
- } finally {
- DatabaseUtils.closeQuietly(rs);
- DatabaseUtils.closeQuietly(stmt);
- }
- }
- }
-
-}
diff --git a/sonar-db/src/main/java/org/sonar/db/charset/CharsetHandler.java b/sonar-db/src/main/java/org/sonar/db/charset/CharsetHandler.java
new file mode 100644
index 00000000000..233cb14b5a8
--- /dev/null
+++ b/sonar-db/src/main/java/org/sonar/db/charset/CharsetHandler.java
@@ -0,0 +1,65 @@
+/*
+ * 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.db.charset;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.List;
+import javax.annotation.CheckForNull;
+
+abstract class CharsetHandler {
+
+ protected static final String UTF8 = "utf8";
+
+ private final SqlExecutor selectExecutor;
+
+ protected CharsetHandler(SqlExecutor selectExecutor) {
+ this.selectExecutor = selectExecutor;
+ }
+
+ abstract void handle(Connection connection, boolean enforceUtf8) throws SQLException;
+
+ protected SqlExecutor getSqlExecutor() {
+ return selectExecutor;
+ }
+
+ @CheckForNull
+ protected final String selectSingleString(Connection connection, String sql) throws SQLException {
+ String[] cols = selectSingleRow(connection, sql, new SqlExecutor.StringsConverter(1));
+ return cols == null ? null : cols[0];
+ }
+
+ @CheckForNull
+ protected final <T> T selectSingleRow(Connection connection, String sql, SqlExecutor.RowConverter<T> rowConverter) throws SQLException {
+ List<T> rows = select(connection, sql, rowConverter);
+ if (rows.isEmpty()) {
+ return null;
+ }
+ if (rows.size() == 1) {
+ return rows.get(0);
+ }
+ throw new IllegalStateException("Expecting only one result for [" + sql + "]");
+ }
+
+ protected final <T> List<T> select(Connection connection, String sql, SqlExecutor.RowConverter<T> rowConverter) throws SQLException {
+ return selectExecutor.executeSelect(connection, sql, rowConverter);
+ }
+
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/charset/ColumnDef.java b/sonar-db/src/main/java/org/sonar/db/charset/ColumnDef.java
new file mode 100644
index 00000000000..45d4b017546
--- /dev/null
+++ b/sonar-db/src/main/java/org/sonar/db/charset/ColumnDef.java
@@ -0,0 +1,92 @@
+/*
+ * 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.db.charset;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Result of standard SQL command "select * from INFORMATION_SCHEMA" (columns listed in {@link #SELECT_COLUMNS}).
+ */
+@Immutable
+public class ColumnDef {
+
+ public static final String SELECT_COLUMNS = "select table_name, column_name, character_set_name, collation_name, data_type, character_maximum_length, is_nullable ";
+
+ private final String table;
+ private final String column;
+ private final String charset;
+ private final String collation;
+ private final String dataType;
+ private final long size;
+ private final boolean nullable;
+
+ public ColumnDef(String table, String column, String charset, String collation, String dataType, long size, boolean nullable) {
+ this.table = table;
+ this.column = column;
+ this.charset = charset;
+ this.collation = collation;
+ this.dataType = dataType;
+ this.size = size;
+ this.nullable = nullable;
+ }
+
+ public String getTable() {
+ return table;
+ }
+
+ public String getColumn() {
+ return column;
+ }
+
+ public String getCharset() {
+ return charset;
+ }
+
+ public String getCollation() {
+ return collation;
+ }
+
+ public String getDataType() {
+ return dataType;
+ }
+
+ public long getSize() {
+ return size;
+ }
+
+ public boolean isNullable() {
+ return nullable;
+ }
+
+ public enum ColumnDefRowConverter implements SqlExecutor.RowConverter<ColumnDef> {
+ INSTANCE;
+
+ @Override
+ public ColumnDef convert(ResultSet rs) throws SQLException {
+ String nullableText = rs.getString(7);
+ boolean nullable = "YES".equalsIgnoreCase(nullableText);
+
+ return new ColumnDef(
+ rs.getString(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getString(5), rs.getLong(6), nullable);
+ }
+ }
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/charset/DatabaseCharsetChecker.java b/sonar-db/src/main/java/org/sonar/db/charset/DatabaseCharsetChecker.java
new file mode 100644
index 00000000000..cdc87bc3b2d
--- /dev/null
+++ b/sonar-db/src/main/java/org/sonar/db/charset/DatabaseCharsetChecker.java
@@ -0,0 +1,89 @@
+/*
+ * 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.db.charset;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.sql.Connection;
+import java.sql.SQLException;
+import javax.annotation.CheckForNull;
+import org.sonar.db.Database;
+import org.sonar.db.dialect.Dialect;
+import org.sonar.db.dialect.H2;
+import org.sonar.db.dialect.MsSql;
+import org.sonar.db.dialect.MySql;
+import org.sonar.db.dialect.Oracle;
+import org.sonar.db.dialect.PostgreSql;
+
+/**
+ * On fresh installations, checks that all db columns are UTF8. On all installations on MySQL or MSSQL,
+ * whatever fresh or upgrade, fixes case-insensitive columns by converting them to
+ * case-sensitive.
+ *
+ * See SONAR-6171 and SONAR-7549
+ */
+public class DatabaseCharsetChecker {
+
+ private final Database db;
+ private final SqlExecutor selectExecutor;
+
+ public DatabaseCharsetChecker(Database db) {
+ this(db, new SqlExecutor());
+ }
+
+ @VisibleForTesting
+ DatabaseCharsetChecker(Database db, SqlExecutor selectExecutor) {
+ this.db = db;
+ this.selectExecutor = selectExecutor;
+ }
+
+ public void check(boolean enforceUtf8) {
+ try {
+ try (Connection connection = db.getDataSource().getConnection()) {
+ CharsetHandler handler = getHandler(db.getDialect());
+ if (handler != null) {
+ handler.handle(connection, enforceUtf8);
+ }
+ }
+ } catch (SQLException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @VisibleForTesting
+ @CheckForNull
+ CharsetHandler getHandler(Dialect dialect) {
+ switch (dialect.getId()) {
+ case H2.ID:
+ // nothing to check
+ return null;
+ case Oracle.ID:
+ return new OracleCharsetHandler(selectExecutor);
+ case PostgreSql.ID:
+ return new PostgresCharsetHandler(selectExecutor);
+ case MySql.ID:
+ return new MysqlCharsetHandler(selectExecutor);
+ case MsSql.ID:
+ return new MssqlCharsetHandler(selectExecutor);
+ default:
+ throw new IllegalArgumentException("Database not supported: " + dialect.getId());
+ }
+ }
+
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/charset/MssqlCharsetHandler.java b/sonar-db/src/main/java/org/sonar/db/charset/MssqlCharsetHandler.java
new file mode 100644
index 00000000000..347fc372133
--- /dev/null
+++ b/sonar-db/src/main/java/org/sonar/db/charset/MssqlCharsetHandler.java
@@ -0,0 +1,139 @@
+/*
+ * 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.db.charset;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+import static java.lang.String.format;
+import static org.apache.commons.lang.StringUtils.endsWithIgnoreCase;
+
+class MssqlCharsetHandler extends CharsetHandler {
+
+ private static final Logger LOGGER = Loggers.get(MssqlCharsetHandler.class);
+
+ protected MssqlCharsetHandler(SqlExecutor selectExecutor) {
+ super(selectExecutor);
+ }
+
+ @Override
+ void handle(Connection connection, boolean enforceUtf8) throws SQLException {
+ LOGGER.info("Verify that database collation is case-sensitive and accent-sensitive");
+ checkCollation(connection);
+ }
+
+ private void checkCollation(Connection connection) throws SQLException {
+ // All VARCHAR columns are returned. No need to check database general collation.
+ // Example of row:
+ // issues | kee | Latin1_General_CS_AS
+ List<ColumnDef> columns = select(connection,
+ ColumnDef.SELECT_COLUMNS +
+ "FROM [INFORMATION_SCHEMA].[COLUMNS] " +
+ "WHERE collation_name is not null " +
+ "ORDER BY table_name,column_name", ColumnDef.ColumnDefRowConverter.INSTANCE);
+ for (ColumnDef column : columns) {
+ if (!endsWithIgnoreCase(column.getCollation(), "_CS_AS")) {
+ repairColumnCollation(connection, column);
+ }
+ }
+ }
+
+ private void repairColumnCollation(Connection connection, ColumnDef column) throws SQLException {
+ // 1. select the indices defined on this column
+ String selectIndicesSql = format("SELECT I.name as index_name, I.is_unique as unik, IndexedColumns " +
+ " FROM sys.indexes I " +
+ " JOIN sys.tables T ON T.Object_id = I.Object_id " +
+ " JOIN (SELECT * FROM ( " +
+ " SELECT IC2.object_id, IC2.index_id, " +
+ " STUFF((SELECT ' ,' + C.name " +
+ " FROM sys.index_columns IC1 " +
+ " JOIN sys.columns C " +
+ " ON C.object_id = IC1.object_id " +
+ " AND C.column_id = IC1.column_id " +
+ " AND IC1.is_included_column = 0 " +
+ " WHERE IC1.object_id = IC2.object_id " +
+ " AND IC1.index_id = IC2.index_id " +
+ " GROUP BY IC1.object_id,C.name,index_id " +
+ " ORDER BY MAX(IC1.key_ordinal) " +
+ " FOR XML PATH('')), 1, 2, '') IndexedColumns " +
+ " FROM sys.index_columns IC2 " +
+ " GROUP BY IC2.object_id ,IC2.index_id) tmp1 )tmp2 " +
+ " ON I.object_id = tmp2.object_id AND I.Index_id = tmp2.index_id " +
+ " WHERE I.is_primary_key = 0 AND I.is_unique_constraint = 0 " +
+ " and T.name =('%s') " +
+ " and CHARINDEX ('%s',IndexedColumns)>0", column.getTable(), column.getColumn());
+ List<ColumnIndex> indices = getSqlExecutor().executeSelect(connection, selectIndicesSql, ColumnIndexConverter.INSTANCE);
+
+ // 2. drop indices
+ for (ColumnIndex index : indices) {
+ getSqlExecutor().executeUpdate(connection, format("DROP INDEX %s.%s", column.getTable(), index.name));
+ }
+
+ // 3. alter collation of column
+ String csCollation = toCaseSensitive(column.getCollation());
+
+ String nullability = column.isNullable() ? "NULL" : "NOT NULL";
+ String size = column.getSize() >= 0 ? String.valueOf(column.getSize()) : "max";
+ String alterSql = format("ALTER TABLE %s ALTER COLUMN %s %s(%s) COLLATE %s %s",
+ column.getTable(), column.getColumn(), column.getDataType(), size, csCollation, nullability);
+ LOGGER.info("Changing collation of column [{}.{}] from {} to {} | sql=", column.getTable(), column.getColumn(), column.getCollation(), csCollation, alterSql);
+ getSqlExecutor().executeUpdate(connection, alterSql);
+
+ // 4. re-create indices
+ for (ColumnIndex index : indices) {
+ String uniqueSql = index.unique ? "UNIQUE" : "";
+ String createIndexSql = format("CREATE %s INDEX %s ON %s (%s)", uniqueSql, index.name, column.getTable(), index.csvColumns);
+ getSqlExecutor().executeUpdate(connection, createIndexSql);
+ }
+ }
+
+ @VisibleForTesting
+ static String toCaseSensitive(String ciCollation) {
+ // Example: Latin1_General_CI_AI --> Latin1_General_CS_AS
+ return ciCollation.substring(0, ciCollation.length() - "_CI_AI".length()) + "_CS_AS";
+ }
+
+ @VisibleForTesting
+ static class ColumnIndex {
+ private final String name;
+ private final boolean unique;
+ private final String csvColumns;
+
+ public ColumnIndex(String name, boolean unique, String csvColumns) {
+ this.name = name;
+ this.unique = unique;
+ this.csvColumns = csvColumns;
+ }
+ }
+
+ @VisibleForTesting
+ enum ColumnIndexConverter implements SqlExecutor.RowConverter<ColumnIndex> {
+ INSTANCE;
+ @Override
+ public ColumnIndex convert(ResultSet rs) throws SQLException {
+ return new ColumnIndex(rs.getString(1), rs.getBoolean(2), rs.getString(3));
+ }
+ }
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/charset/MysqlCharsetHandler.java b/sonar-db/src/main/java/org/sonar/db/charset/MysqlCharsetHandler.java
new file mode 100644
index 00000000000..43ffd828cbe
--- /dev/null
+++ b/sonar-db/src/main/java/org/sonar/db/charset/MysqlCharsetHandler.java
@@ -0,0 +1,99 @@
+/*
+ * 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.db.charset;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+
+import static java.lang.String.format;
+import static org.apache.commons.lang.StringUtils.containsIgnoreCase;
+import static org.apache.commons.lang.StringUtils.endsWithIgnoreCase;
+
+class MysqlCharsetHandler extends CharsetHandler {
+
+ private static final Logger LOGGER = Loggers.get(MysqlCharsetHandler.class);
+ private static final String TYPE_LONGTEXT = "longtext";
+
+ protected MysqlCharsetHandler(SqlExecutor selectExecutor) {
+ super(selectExecutor);
+ }
+
+ @Override
+ void handle(Connection connection, boolean enforceUtf8) throws SQLException {
+ logInit(enforceUtf8);
+ checkCollation(connection, enforceUtf8);
+ }
+
+ private static void logInit(boolean enforceUtf8) {
+ String message = "Verify that database collation is case-sensitive";
+ if (enforceUtf8) {
+ message = "Verify that database collation is UTF8";
+ }
+ LOGGER.info(message);
+ }
+
+ private void checkCollation(Connection connection, boolean enforceUtf8) throws SQLException {
+ // All VARCHAR columns are returned. No need to check database general collation.
+ // Example of row:
+ // issues | kee | utf8 | utf8_bin
+ List<ColumnDef> columns = select(connection,
+ ColumnDef.SELECT_COLUMNS +
+ "FROM INFORMATION_SCHEMA.columns " +
+ "WHERE table_schema=database() and character_set_name is not null and collation_name is not null", ColumnDef.ColumnDefRowConverter.INSTANCE);
+ List<String> utf8Errors = new ArrayList<>();
+ for (ColumnDef column : columns) {
+ if (enforceUtf8 && !containsIgnoreCase(column.getCharset(), UTF8)) {
+ utf8Errors.add(format("%s.%s", column.getTable(), column.getColumn()));
+ } else if (endsWithIgnoreCase(column.getCollation(), "_ci")) {
+ repairCaseInsensitiveColumn(connection, column);
+ }
+ }
+ if (!utf8Errors.isEmpty()) {
+ throw MessageException.of(format("UTF8 case-sensitive collation is required for database columns [%s]", Joiner.on(", ").join(utf8Errors)));
+ }
+ }
+
+ private void repairCaseInsensitiveColumn(Connection connection, ColumnDef column)
+ throws SQLException {
+ String csCollation = toCaseSensitive(column.getCollation());
+
+ String nullability = column.isNullable() ? "NULL" : "NOT NULL";
+ String type = column.getDataType().equalsIgnoreCase(TYPE_LONGTEXT) ? TYPE_LONGTEXT : format("%s(%d)", column.getDataType(), column.getSize());
+ String alterSql = format("ALTER TABLE %s MODIFY %s %s CHARACTER SET '%s' COLLATE '%s' %s",
+ column.getTable(), column.getColumn(), type, column.getCharset(), csCollation, nullability);
+ LOGGER.info("Changing collation of column [{}.{}] from {} to {} | sql={}", column.getTable(), column.getColumn(), column.getCollation(), csCollation, alterSql);
+ getSqlExecutor().executeUpdate(connection, alterSql);
+ }
+
+ @VisibleForTesting
+ static String toCaseSensitive(String caseInsensitiveCollation) {
+ // Example: big5_chinese_ci becomes big5_bin
+ // Full list of collations is available with SQL request "show collation"
+ return StringUtils.substringBefore(caseInsensitiveCollation, "_") + "_bin";
+ }
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/charset/OracleCharsetHandler.java b/sonar-db/src/main/java/org/sonar/db/charset/OracleCharsetHandler.java
new file mode 100644
index 00000000000..179d7c2f796
--- /dev/null
+++ b/sonar-db/src/main/java/org/sonar/db/charset/OracleCharsetHandler.java
@@ -0,0 +1,52 @@
+/*
+ * 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.db.charset;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.Loggers;
+
+import static java.lang.String.format;
+import static org.apache.commons.lang.StringUtils.containsIgnoreCase;
+
+class OracleCharsetHandler extends CharsetHandler {
+
+ protected OracleCharsetHandler(SqlExecutor selectExecutor) {
+ super(selectExecutor);
+ }
+
+ @Override
+ public void handle(Connection connection, boolean enforceUtf8) throws SQLException {
+ // Oracle does not allow to override character set on tables. Only global charset is verified.
+ if (enforceUtf8) {
+ Loggers.get(getClass()).info("Verify that database charset is UTF8");
+ checkUtf8(connection);
+ }
+ }
+
+ private void checkUtf8(Connection connection) throws SQLException {
+ String charset = selectSingleString(connection, "select value from nls_database_parameters where parameter='NLS_CHARACTERSET'");
+ String sort = selectSingleString(connection, "select value from nls_database_parameters where parameter='NLS_SORT'");
+ if (!containsIgnoreCase(charset, UTF8) || !"BINARY".equalsIgnoreCase(sort)) {
+ throw MessageException.of(format("Oracle must be have UTF8 charset and BINARY sort. NLS_CHARACTERSET is %s and NLS_SORT is %s.", charset, sort));
+ }
+ }
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/charset/PostgresCharsetHandler.java b/sonar-db/src/main/java/org/sonar/db/charset/PostgresCharsetHandler.java
new file mode 100644
index 00000000000..1452750ae1f
--- /dev/null
+++ b/sonar-db/src/main/java/org/sonar/db/charset/PostgresCharsetHandler.java
@@ -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.db.charset;
+
+import com.google.common.base.Joiner;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.Loggers;
+
+import static java.lang.String.format;
+import static org.apache.commons.lang.StringUtils.containsIgnoreCase;
+
+class PostgresCharsetHandler extends CharsetHandler {
+
+ protected PostgresCharsetHandler(SqlExecutor selectExecutor) {
+ super(selectExecutor);
+ }
+
+ @Override
+ void handle(Connection connection, boolean enforceUtf8) throws SQLException {
+ // PostgreSQL does not support case-insensitive collations. Only charset must be verified.
+ if (enforceUtf8) {
+ Loggers.get(getClass()).info("Verify that database collation supports UTF8");
+ checkUtf8(connection);
+ }
+ }
+
+ private void checkUtf8(Connection connection) throws SQLException {
+ // Character set is defined globally and can be overridden on each column.
+ // This request returns all VARCHAR columns. Collation may be empty.
+ // Examples:
+ // issues | key | ''
+ // projects | name | utf8
+ List<String[]> rows = select(connection, "select table_name, column_name, collation_name " +
+ "from information_schema.columns " +
+ "where table_schema='public' " +
+ "and udt_name='varchar' " +
+ "order by table_name, column_name", new SqlExecutor.StringsConverter(3 /* columns returned by SELECT */));
+ boolean mustCheckGlobalCollation = false;
+ List<String> errors = new ArrayList<>();
+ for (String[] row : rows) {
+ if (StringUtils.isBlank(row[2])) {
+ mustCheckGlobalCollation = true;
+ } else if (!containsIgnoreCase(row[2], UTF8)) {
+ errors.add(format("%s.%s", row[0], row[1]));
+ }
+ }
+
+ if (mustCheckGlobalCollation) {
+ String charset = selectSingleString(connection, "SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database()");
+ if (!containsIgnoreCase(charset, UTF8)) {
+ throw MessageException.of(format("Database collation is %s. It must support UTF8.", charset));
+ }
+ }
+ if (!errors.isEmpty()) {
+ throw MessageException.of(format("Database columns [%s] must support UTF8 collation.", Joiner.on(", ").join(errors)));
+ }
+ }
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/charset/SqlExecutor.java b/sonar-db/src/main/java/org/sonar/db/charset/SqlExecutor.java
new file mode 100644
index 00000000000..6d0e60a8a0f
--- /dev/null
+++ b/sonar-db/src/main/java/org/sonar/db/charset/SqlExecutor.java
@@ -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.db.charset;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import org.sonar.db.DatabaseUtils;
+
+public class SqlExecutor {
+
+ public <T> List<T> executeSelect(Connection connection, String sql, RowConverter<T> rowConverter) throws SQLException {
+ PreparedStatement stmt = null;
+ ResultSet rs = null;
+ try {
+ stmt = connection.prepareStatement(sql);
+ rs = stmt.executeQuery();
+ List<T> result = new ArrayList<>();
+ while (rs.next()) {
+ result.add(rowConverter.convert(rs));
+ }
+ return result;
+
+ } finally {
+ DatabaseUtils.closeQuietly(rs);
+ DatabaseUtils.closeQuietly(stmt);
+ }
+ }
+
+ public void executeUpdate(Connection connection, String sql) throws SQLException {
+ PreparedStatement stmt = null;
+ try {
+ stmt = connection.prepareStatement(sql);
+ stmt.executeUpdate();
+ } finally {
+ DatabaseUtils.closeQuietly(stmt);
+ }
+ }
+
+ public interface RowConverter<T> {
+ T convert(ResultSet rs) throws SQLException;
+ }
+
+ public static class StringsConverter implements RowConverter<String[]> {
+ private final int nbColumns;
+
+ public StringsConverter(int nbColumns) {
+ this.nbColumns = nbColumns;
+ }
+
+ @Override
+ public String[] convert(ResultSet rs) throws SQLException {
+ String[] row = new String[nbColumns];
+ for (int i = 0; i < nbColumns; i++) {
+ row[i] = DatabaseUtils.getString(rs, i + 1);
+ }
+ return row;
+ }
+ }
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/charset/package-info.java b/sonar-db/src/main/java/org/sonar/db/charset/package-info.java
new file mode 100644
index 00000000000..25a4faf160e
--- /dev/null
+++ b/sonar-db/src/main/java/org/sonar/db/charset/package-info.java
@@ -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.db.charset;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-db/src/test/java/org/sonar/db/CollationCheckerTest.java b/sonar-db/src/test/java/org/sonar/db/CollationCheckerTest.java
deleted file mode 100644
index d653ab32cdb..00000000000
--- a/sonar-db/src/test/java/org/sonar/db/CollationCheckerTest.java
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- * 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.db;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.mockito.Mockito;
-import org.sonar.api.utils.MessageException;
-import org.sonar.db.dialect.MsSql;
-import org.sonar.db.dialect.MySql;
-import org.sonar.db.dialect.Oracle;
-import org.sonar.db.dialect.PostgreSql;
-
-import static java.util.Arrays.asList;
-import static java.util.Collections.singletonList;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-public class CollationCheckerTest {
-
- private static final String TABLE_ISSUES = "issues";
- private static final String TABLE_PROJECTS = "projects";
- private static final String COLUMN_KEE = "kee";
- private static final String COLUMN_NAME = "name";
-
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
-
- Database db = mock(Database.class, Mockito.RETURNS_MOCKS);
- CollationChecker.StatementExecutor statementExecutor = mock(CollationChecker.StatementExecutor.class);
- CollationChecker underTest = new CollationChecker(db, statementExecutor);
-
- @Test
- public void valid_oracle() throws Exception {
- when(db.getDialect()).thenReturn(new Oracle());
- answerSql(
- singletonList(new String[] {"UTF8"}), singletonList(new String[] {"BINARY"}));
-
- underTest.start();
- }
-
- @Test
- public void support_oracle_al32utf8() throws Exception {
- when(db.getDialect()).thenReturn(new Oracle());
- answerSql(
- singletonList(new String[] {"AL32UTF8"}), singletonList(new String[] {"BINARY"}));
-
- underTest.start();
- }
-
- @Test
- public void fail_if_oracle_is_not_utf8() throws Exception {
- when(db.getDialect()).thenReturn(new Oracle());
- answerSql(
- singletonList(new String[] {"LATIN"}), singletonList(new String[] {"BINARY"}));
-
- expectedException.expect(MessageException.class);
- expectedException.expectMessage("Oracle must be have UTF8 charset and BINARY sort. NLS_CHARACTERSET is LATIN and NLS_SORT is BINARY.");
-
- underTest.start();
- }
-
- @Test
- public void fail_if_oracle_is_not_case_sensitive() throws Exception {
- when(db.getDialect()).thenReturn(new Oracle());
- answerSql(
- singletonList(new String[] {"UTF8"}), singletonList(new String[] {"LINGUISTIC"}));
-
- expectedException.expect(MessageException.class);
- expectedException.expectMessage("Oracle must be have UTF8 charset and BINARY sort. NLS_CHARACTERSET is UTF8 and NLS_SORT is LINGUISTIC.");
-
- underTest.start();
- }
-
- @Test
- public void fail_if_can_not_get_oracle_charset() throws Exception {
- when(db.getDialect()).thenReturn(new Oracle());
- answerSql(Collections.<String[]>emptyList(), Collections.<String[]>emptyList());
-
- expectedException.expect(MessageException.class);
-
- underTest.start();
- }
-
- @Test
- public void valid_postgresql() throws Exception {
- when(db.getDialect()).thenReturn(new PostgreSql());
- answerSql(asList(
- new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8"},
- new String[] {TABLE_PROJECTS, COLUMN_NAME, "utf8"}));
-
- underTest.start();
- }
-
- @Test
- public void fail_if_postgresql_has_non_utf8_column() throws Exception {
- when(db.getDialect()).thenReturn(new PostgreSql());
- answerSql(asList(
- new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8"},
- new String[] {TABLE_PROJECTS, COLUMN_KEE, "latin"},
- new String[] {TABLE_PROJECTS, COLUMN_NAME, "latin"}));
-
- expectedException.expect(MessageException.class);
- expectedException.expectMessage("Database columns [projects.kee, projects.name] must have UTF8 charset.");
-
- underTest.start();
- }
-
- @Test
- public void fail_if_postgresql_has_non_utf8_db() throws Exception {
- when(db.getDialect()).thenReturn(new PostgreSql());
- answerSql(
- // first request to get columns
- asList(
- new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8"},
- new String[] {TABLE_PROJECTS, COLUMN_NAME, "" /* unset -> uses db collation */}),
-
- // second request to get db collation
- Arrays.<String[]>asList(new String[] {"latin"}));
-
- expectedException.expect(MessageException.class);
- expectedException.expectMessage("Database charset is latin. It must be UTF8.");
-
- underTest.start();
- }
-
- @Test
- public void valid_postgresql_if_utf8_db() throws Exception {
- when(db.getDialect()).thenReturn(new PostgreSql());
- answerSql(
- // first request to get columns
- asList(
- new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8"},
- new String[] {TABLE_PROJECTS, COLUMN_NAME, "" /* unset -> uses db collation */}),
-
- // second request to get db collation
- Arrays.<String[]>asList(new String[] {"utf8"}));
-
- // no error
- underTest.start();
- }
-
- @Test
- public void valid_mysql() throws Exception {
- when(db.getDialect()).thenReturn(new MySql());
- answerSql(asList(
- new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8", "utf8_bin"},
- new String[] {TABLE_PROJECTS, COLUMN_NAME, "utf8", "utf8_bin"}));
-
- underTest.start();
- }
-
- @Test
- public void fail_if_mysql_is_not_utf8_charset() throws Exception {
- when(db.getDialect()).thenReturn(new MySql());
- answerSql(asList(
- new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8", "utf8_bin"},
- new String[] {TABLE_PROJECTS, COLUMN_KEE, "latin1", "utf8_bin"},
- new String[] {TABLE_PROJECTS, COLUMN_NAME, "latin1", "utf8_bin"}));
-
- expectedException.expect(MessageException.class);
- expectedException.expectMessage("UTF8 charset and case-sensitive collation are required for database columns [projects.kee, projects.name]");
-
- underTest.start();
- }
-
- @Test
- public void fail_if_mysql_is_not_case_sensitive() throws Exception {
- when(db.getDialect()).thenReturn(new MySql());
- answerSql(asList(
- new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8", "utf8_bin"},
- new String[] {TABLE_PROJECTS, COLUMN_NAME, "utf8", "latin1_swedish_ci"}));
-
- expectedException.expect(MessageException.class);
-
- underTest.start();
- }
-
- @Test
- public void valid_mssql() throws Exception {
- when(db.getDialect()).thenReturn(new MsSql());
- answerSql(asList(
- new String[] {TABLE_ISSUES, COLUMN_KEE, "Latin1_General_CS_AS"},
- new String[] {TABLE_PROJECTS, COLUMN_NAME, "Latin1_General_CS_AS"}));
-
- underTest.start();
- }
-
- @Test
- public void fail_if_mssql_is_not_case_sensitive() throws Exception {
- when(db.getDialect()).thenReturn(new MsSql());
- answerSql(asList(
- new String[] {TABLE_ISSUES, COLUMN_KEE, "Latin1_General_CS_AS"},
- new String[] {TABLE_PROJECTS, COLUMN_KEE, "Latin1_General_CI_AI"},
- new String[] {TABLE_PROJECTS, COLUMN_NAME, "Latin1_General_CI_AI"}));
-
- expectedException.expect(MessageException.class);
- expectedException.expectMessage("Case-sensitive and accent-sensitive charset (CS_AS) is required for database columns [projects.kee, projects.name]");
-
- underTest.start();
- }
-
- private void answerSql(List<String[]> firstRequest, List<String[]>... otherRequests) throws SQLException {
- when(statementExecutor.executeQuery(any(Connection.class), anyString(), anyInt())).thenReturn(firstRequest, otherRequests);
- }
-}
diff --git a/sonar-db/src/test/java/org/sonar/db/charset/DatabaseCharsetCheckerTest.java b/sonar-db/src/test/java/org/sonar/db/charset/DatabaseCharsetCheckerTest.java
new file mode 100644
index 00000000000..88e47d4af2e
--- /dev/null
+++ b/sonar-db/src/test/java/org/sonar/db/charset/DatabaseCharsetCheckerTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.db.charset;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.Mockito;
+import org.sonar.db.Database;
+import org.sonar.db.dialect.Dialect;
+import org.sonar.db.dialect.H2;
+import org.sonar.db.dialect.MsSql;
+import org.sonar.db.dialect.MySql;
+import org.sonar.db.dialect.Oracle;
+import org.sonar.db.dialect.PostgreSql;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class DatabaseCharsetCheckerTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ Database db = mock(Database.class, Mockito.RETURNS_MOCKS);
+ CharsetHandler handler = mock(CharsetHandler.class);
+ DatabaseCharsetChecker underTest = spy(new DatabaseCharsetChecker(db));
+
+ @Test
+ public void executes_handler() throws Exception {
+ Oracle dialect = new Oracle();
+ when(underTest.getHandler(dialect)).thenReturn(handler);
+ when(db.getDialect()).thenReturn(dialect);
+
+ underTest.check(true);
+ verify(handler).handle(any(Connection.class), eq(true));
+ }
+
+ @Test
+ public void throws_ISE_if_handler_fails() throws Exception {
+ Oracle dialect = new Oracle();
+ when(underTest.getHandler(dialect)).thenReturn(handler);
+ when(db.getDialect()).thenReturn(dialect);
+ doThrow(new SQLException("failure")).when(handler).handle(any(Connection.class), anyBoolean());
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("failure");
+ underTest.check(true);
+ }
+
+ @Test
+ public void does_nothing_if_h2() throws Exception {
+ assertThat(underTest.getHandler(new H2())).isNull();
+ }
+
+ @Test
+ public void getHandler_returns_MysqlCharsetHandler_if_mysql() throws Exception {
+ assertThat(underTest.getHandler(new MySql())).isInstanceOf(MysqlCharsetHandler.class);
+ }
+
+ @Test
+ public void getHandler_returns_MssqlCharsetHandler_if_mssql() throws Exception {
+ assertThat(underTest.getHandler(new MsSql())).isInstanceOf(MssqlCharsetHandler.class);
+ }
+
+ @Test
+ public void getHandler_returns_OracleCharsetHandler_if_oracle() throws Exception {
+ assertThat(underTest.getHandler(new Oracle())).isInstanceOf(OracleCharsetHandler.class);
+ }
+
+ @Test
+ public void getHandler_returns_PostgresCharsetHandler_if_postgres() throws Exception {
+ assertThat(underTest.getHandler(new PostgreSql())).isInstanceOf(PostgresCharsetHandler.class);
+ }
+
+ @Test
+ public void getHandler_throws_IAE_if_unsupported_db() throws Exception {
+ Dialect unsupportedDialect = mock(Dialect.class);
+ when(unsupportedDialect.getId()).thenReturn("foo");
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Database not supported: foo");
+ underTest.getHandler(unsupportedDialect);
+ }
+}
diff --git a/sonar-db/src/test/java/org/sonar/db/charset/MssqlCharsetHandlerTest.java b/sonar-db/src/test/java/org/sonar/db/charset/MssqlCharsetHandlerTest.java
new file mode 100644
index 00000000000..2712b66b274
--- /dev/null
+++ b/sonar-db/src/test/java/org/sonar/db/charset/MssqlCharsetHandlerTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.db.charset;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static java.util.Arrays.asList;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class MssqlCharsetHandlerTest {
+
+ private static final String TABLE_ISSUES = "issues";
+ private static final String TABLE_PROJECTS = "projects";
+ private static final String COLUMN_KEE = "kee";
+ private static final String COLUMN_NAME = "name";
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ SqlExecutor selectExecutor = mock(SqlExecutor.class);
+ MssqlCharsetHandler underTest = new MssqlCharsetHandler(selectExecutor);
+
+ @Test
+ public void does_not_fail_if_charsets_of_all_columns_are_utf8() throws Exception {
+ answerColumns(asList(
+ new ColumnDef(TABLE_ISSUES, COLUMN_KEE, "Latin1_General", "Latin1_General_CS_AS", "varchar", 10, false),
+ new ColumnDef(TABLE_PROJECTS, COLUMN_NAME, "Latin1_General", "Latin1_General_CS_AS", "varchar", 10, false)));
+
+ underTest.handle(mock(Connection.class), true);
+ }
+
+ @Test
+ public void repairs_case_insensitive_column_without_index() throws Exception {
+ answerColumns(asList(
+ new ColumnDef(TABLE_ISSUES, COLUMN_KEE, "Latin1_General", "Latin1_General_CS_AS", "varchar", 10, false),
+ new ColumnDef(TABLE_PROJECTS, COLUMN_NAME, "Latin1_General", "Latin1_General_CI_AI", "varchar", 10, false)));
+
+ Connection connection = mock(Connection.class);
+ underTest.handle(connection, false);
+
+ verify(selectExecutor).executeUpdate(connection, "ALTER TABLE projects ALTER COLUMN name varchar(10) COLLATE Latin1_General_CS_AS NOT NULL");
+ }
+
+ @Test
+ public void repairs_case_insensitive_column_with_indices() throws Exception {
+ answerColumns(asList(
+ new ColumnDef(TABLE_ISSUES, COLUMN_KEE, "Latin1_General", "Latin1_General_CS_AS", "varchar", 10, false),
+ new ColumnDef(TABLE_PROJECTS, COLUMN_NAME, "Latin1_General", "Latin1_General_CI_AI", "varchar", 10, false)));
+ answerIndices(asList(
+ new MssqlCharsetHandler.ColumnIndex("projects_name", false, "name"),
+ // This index is on two columns. Note that it does not make sense for table "projects" !
+ new MssqlCharsetHandler.ColumnIndex("projects_login_and_name", true, "login,name")));
+
+ Connection connection = mock(Connection.class);
+ underTest.handle(connection, false);
+
+ verify(selectExecutor).executeUpdate(connection, "DROP INDEX projects.projects_name");
+ verify(selectExecutor).executeUpdate(connection, "DROP INDEX projects.projects_login_and_name");
+ verify(selectExecutor).executeUpdate(connection, "ALTER TABLE projects ALTER COLUMN name varchar(10) COLLATE Latin1_General_CS_AS NOT NULL");
+ verify(selectExecutor).executeUpdate(connection, "CREATE INDEX projects_name ON projects (name)");
+ verify(selectExecutor).executeUpdate(connection, "CREATE UNIQUE INDEX projects_login_and_name ON projects (login,name)");
+ }
+
+ @Test
+ public void support_the_max_size_of_varchar_column() throws Exception {
+ // returned size is -1
+ answerColumns(asList(new ColumnDef(TABLE_PROJECTS, COLUMN_NAME, "Latin1_General", "Latin1_General_CI_AI", "nvarchar", -1, false)));
+ answerIndices(Collections.<MssqlCharsetHandler.ColumnIndex>emptyList());
+
+ Connection connection = mock(Connection.class);
+ underTest.handle(connection, false);
+
+ verify(selectExecutor).executeUpdate(connection, "ALTER TABLE projects ALTER COLUMN name nvarchar(max) COLLATE Latin1_General_CS_AS NOT NULL");
+ }
+
+ private void answerColumns(List<ColumnDef> columnDefs) throws SQLException {
+ when(selectExecutor.executeSelect(any(Connection.class), anyString(), eq(ColumnDef.ColumnDefRowConverter.INSTANCE))).thenReturn(columnDefs);
+ }
+
+ private void answerIndices(List<MssqlCharsetHandler.ColumnIndex> indices) throws SQLException {
+ when(selectExecutor.executeSelect(any(Connection.class), anyString(), eq(MssqlCharsetHandler.ColumnIndexConverter.INSTANCE))).thenReturn(indices);
+ }
+}
diff --git a/sonar-db/src/test/java/org/sonar/db/charset/MysqlCharsetHandlerTest.java b/sonar-db/src/test/java/org/sonar/db/charset/MysqlCharsetHandlerTest.java
new file mode 100644
index 00000000000..9e9687a2bfd
--- /dev/null
+++ b/sonar-db/src/test/java/org/sonar/db/charset/MysqlCharsetHandlerTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.db.charset;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.MessageException;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class MysqlCharsetHandlerTest {
+
+ private static final String TABLE_ISSUES = "issues";
+ private static final String TABLE_PROJECTS = "projects";
+ private static final String COLUMN_KEE = "kee";
+ private static final String COLUMN_NAME = "name";
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ SqlExecutor selectExecutor = mock(SqlExecutor.class);
+ MysqlCharsetHandler underTest = new MysqlCharsetHandler(selectExecutor);
+
+ @Test
+ public void does_not_fail_if_charsets_of_all_columns_are_utf8() throws Exception {
+ answerColumnDef(asList(
+ new ColumnDef(TABLE_ISSUES, COLUMN_KEE, "utf8", "utf8_bin", "varchar", 10, false),
+ new ColumnDef(TABLE_PROJECTS, COLUMN_NAME, "utf8", "utf8_bin", "varchar", 10, false)));
+
+ // all columns are utf8
+ underTest.handle(mock(Connection.class), true);
+ }
+
+ @Test
+ public void fails_if_not_utf8() throws Exception {
+ answerColumnDef(asList(
+ new ColumnDef(TABLE_ISSUES, COLUMN_KEE, "utf8", "utf8_bin", "varchar", 10, false),
+ new ColumnDef(TABLE_PROJECTS, COLUMN_KEE, "latin1", "latin1_german1_ci", "varchar", 10, false),
+ new ColumnDef(TABLE_PROJECTS, COLUMN_NAME, "latin1", "latin1_swedish_ci", "varchar", 20, false)));
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("UTF8 case-sensitive collation is required for database columns [projects.kee, projects.name]");
+ underTest.handle(mock(Connection.class), true);
+ }
+
+ @Test
+ public void repairs_case_insensitive_column() throws Exception {
+ answerColumnDef(asList(
+ new ColumnDef(TABLE_ISSUES, COLUMN_KEE, "utf8", "utf8_bin", "varchar", 10, false),
+ new ColumnDef(TABLE_PROJECTS, COLUMN_NAME, "latin1", "latin1_swedish_ci", "varchar", 10, false)));
+
+ Connection connection = mock(Connection.class);
+ underTest.handle(connection, false);
+
+ verify(selectExecutor).executeUpdate(connection, "ALTER TABLE projects MODIFY name varchar(10) CHARACTER SET 'latin1' COLLATE 'latin1_bin' NOT NULL");
+ }
+
+ @Test
+ public void size_should_be_ignored_on_longtext_column() throws Exception {
+ answerColumnDef(asList(new ColumnDef(TABLE_ISSUES, COLUMN_KEE, "latin1", "latin1_german1_ci", "longtext", 4_294_967_295L, false)));
+
+ Connection connection = mock(Connection.class);
+ underTest.handle(connection, false);
+
+ verify(selectExecutor).executeUpdate(connection, "ALTER TABLE " + TABLE_ISSUES + " MODIFY " + COLUMN_KEE + " longtext CHARACTER SET 'latin1' COLLATE 'latin1_bin' NOT NULL");
+ }
+
+ @Test
+ public void tests_toCaseSensitive() {
+ assertThat(MysqlCharsetHandler.toCaseSensitive("big5_chinese_ci")).isEqualTo("big5_bin");
+ }
+
+ private void answerColumnDef(List<ColumnDef> columnDefs) throws SQLException {
+ when(selectExecutor.executeSelect(any(Connection.class), anyString(), eq(ColumnDef.ColumnDefRowConverter.INSTANCE))).thenReturn(columnDefs);
+ }
+}
diff --git a/sonar-db/src/test/java/org/sonar/db/charset/OracleCharsetHandlerTest.java b/sonar-db/src/test/java/org/sonar/db/charset/OracleCharsetHandlerTest.java
new file mode 100644
index 00000000000..0d39cdb598d
--- /dev/null
+++ b/sonar-db/src/test/java/org/sonar/db/charset/OracleCharsetHandlerTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.db.charset;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.MessageException;
+
+import static java.util.Collections.singletonList;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class OracleCharsetHandlerTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ SqlExecutor selectExecutor = mock(SqlExecutor.class);
+ OracleCharsetHandler underTest = new OracleCharsetHandler(selectExecutor);
+
+ @Test
+ public void checks_utf8() throws Exception {
+ answerSql(
+ singletonList(new String[] {"UTF8"}), singletonList(new String[] {"BINARY"}));
+
+ underTest.handle(mock(Connection.class), true);
+ }
+
+ @Test
+ public void supports_al32utf8() throws Exception {
+ answerSql(
+ singletonList(new String[] {"AL32UTF8"}), singletonList(new String[] {"BINARY"}));
+
+ underTest.handle(mock(Connection.class), true);
+ }
+
+ @Test
+ public void fails_if_charset_is_not_utf8() throws Exception {
+ answerSql(
+ singletonList(new String[] {"LATIN"}), singletonList(new String[] {"BINARY"}));
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Oracle must be have UTF8 charset and BINARY sort. NLS_CHARACTERSET is LATIN and NLS_SORT is BINARY.");
+
+ underTest.handle(mock(Connection.class), true);
+ }
+
+ @Test
+ public void fails_if_not_case_sensitive() throws Exception {
+ answerSql(
+ singletonList(new String[] {"UTF8"}), singletonList(new String[] {"LINGUISTIC"}));
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Oracle must be have UTF8 charset and BINARY sort. NLS_CHARACTERSET is UTF8 and NLS_SORT is LINGUISTIC.");
+
+ underTest.handle(mock(Connection.class), true);
+ }
+
+ @Test
+ public void fails_if_can_not_get_charset() throws Exception {
+ answerSql(Collections.<String[]>emptyList(), Collections.<String[]>emptyList());
+
+ expectedException.expect(MessageException.class);
+
+ underTest.handle(mock(Connection.class), true);
+ }
+
+ @Test
+ public void does_nothing_if_utf8_must_not_verified() throws Exception {
+ underTest.handle(mock(Connection.class), false);
+ }
+
+ private void answerSql(List<String[]> firstRequest, List<String[]>... otherRequests) throws SQLException {
+ when(selectExecutor.executeSelect(any(Connection.class), anyString(), any(SqlExecutor.StringsConverter.class))).thenReturn(firstRequest, otherRequests);
+ }
+}
diff --git a/sonar-db/src/test/java/org/sonar/db/charset/PostgresCharsetHandlerTest.java b/sonar-db/src/test/java/org/sonar/db/charset/PostgresCharsetHandlerTest.java
new file mode 100644
index 00000000000..de30b65f63a
--- /dev/null
+++ b/sonar-db/src/test/java/org/sonar/db/charset/PostgresCharsetHandlerTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.db.charset;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.MessageException;
+
+import static java.util.Arrays.asList;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class PostgresCharsetHandlerTest {
+
+ private static final String TABLE_ISSUES = "issues";
+ private static final String TABLE_PROJECTS = "projects";
+ private static final String COLUMN_KEE = "kee";
+ private static final String COLUMN_NAME = "name";
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ SqlExecutor selectExecutor = mock(SqlExecutor.class);
+ PostgresCharsetHandler underTest = new PostgresCharsetHandler(selectExecutor);
+
+ @Test
+ public void checks_that_column_is_utf8() throws Exception {
+ answerSql(asList(
+ new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8"},
+ new String[] {TABLE_PROJECTS, COLUMN_NAME, "utf8"}));
+
+ underTest.handle(mock(Connection.class), true);
+ }
+
+ @Test
+ public void checks_that_db_is_utf8_if_column_collation_is_not_defined() throws Exception {
+ answerSql(
+ // first request to get columns
+ asList(
+ new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8"},
+ new String[] {TABLE_PROJECTS, COLUMN_NAME, "" /* unset -> uses db collation */}),
+
+ // second request to get db collation
+ Arrays.<String[]>asList(new String[] {"utf8"}));
+
+ // no error
+ underTest.handle(mock(Connection.class), true);
+ }
+
+ @Test
+ public void fails_if_non_utf8_column() throws Exception {
+ answerSql(asList(
+ new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8"},
+ new String[] {TABLE_PROJECTS, COLUMN_KEE, "latin"},
+ new String[] {TABLE_PROJECTS, COLUMN_NAME, "latin"}));
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Database columns [projects.kee, projects.name] must support UTF8 collation.");
+
+ underTest.handle(mock(Connection.class), true);
+ }
+
+ @Test
+ public void fails_if_non_utf8_db() throws Exception {
+ answerSql(
+ // first request to get columns
+ asList(
+ new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8"},
+ new String[] {TABLE_PROJECTS, COLUMN_NAME, "" /* unset -> uses db collation */}),
+
+ // second request to get db collation
+ Arrays.<String[]>asList(new String[] {"latin"}));
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Database collation is latin. It must support UTF8.");
+
+ underTest.handle(mock(Connection.class), true);
+ }
+
+ @Test
+ public void does_nothing_if_utf8_must_not_verified() throws Exception {
+ underTest.handle(mock(Connection.class), false);
+ }
+
+ private void answerSql(List<String[]> firstRequest, List<String[]>... otherRequests) throws SQLException {
+ when(selectExecutor.executeSelect(any(Connection.class), anyString(), any(SqlExecutor.StringsConverter.class))).thenReturn(firstRequest, otherRequests);
+ }
+}
diff --git a/sonar-db/src/test/java/org/sonar/db/charset/SelectExecutorTest.java b/sonar-db/src/test/java/org/sonar/db/charset/SelectExecutorTest.java
new file mode 100644
index 00000000000..5c36b5f6cb8
--- /dev/null
+++ b/sonar-db/src/test/java/org/sonar/db/charset/SelectExecutorTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.db.charset;
+
+import java.sql.Connection;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.user.UserDto;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SelectExecutorTest {
+
+ @Rule
+ public DbTester dbTester = DbTester.create(System2.INSTANCE);
+
+ SqlExecutor underTest = new SqlExecutor();
+
+ @Test
+ public void testExecuteQuery() throws Exception {
+ DbSession session = dbTester.getSession();
+ dbTester.getDbClient().userDao().insert(session, new UserDto().setLogin("him").setName("Him"));
+ dbTester.getDbClient().userDao().insert(session, new UserDto().setLogin("her").setName("Her"));
+ session.commit();
+
+ try (Connection connection = dbTester.openConnection()) {
+ List<String[]> rows = underTest.executeSelect(connection, "select login, name from users order by login", new SqlExecutor.StringsConverter(2));
+ assertThat(rows).hasSize(2);
+ assertThat(rows.get(0)[0]).isEqualTo("her");
+ assertThat(rows.get(0)[1]).isEqualTo("Her");
+ assertThat(rows.get(1)[0]).isEqualTo("him");
+ assertThat(rows.get(1)[1]).isEqualTo("Him");
+ }
+ }
+}
diff --git a/sonar-db/src/test/java/org/sonar/db/charset/SqlExecutorTest.java b/sonar-db/src/test/java/org/sonar/db/charset/SqlExecutorTest.java
new file mode 100644
index 00000000000..0ca4dc561c2
--- /dev/null
+++ b/sonar-db/src/test/java/org/sonar/db/charset/SqlExecutorTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.db.charset;
+
+import com.google.common.collect.ImmutableMap;
+import java.sql.Connection;
+import java.util.List;
+import java.util.Map;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SqlExecutorTest {
+
+ private static final String LOGIN_DB_COLUMN = "login";
+ private static final String NAME_DB_COLUMN = "name";
+ private static final String USERS_DB_TABLE = "users";
+
+ SqlExecutor underTest = new SqlExecutor();
+
+ @Rule
+ public DbTester dbTester = DbTester.create(System2.INSTANCE);
+
+ @Test
+ public void executeSelect_executes_PreparedStatement() throws Exception {
+ dbTester.executeInsert(USERS_DB_TABLE, ImmutableMap.of(LOGIN_DB_COLUMN, "login1", NAME_DB_COLUMN, "name one"));
+ dbTester.executeInsert(USERS_DB_TABLE, ImmutableMap.of(LOGIN_DB_COLUMN, "login2", NAME_DB_COLUMN, "name two"));
+
+ dbTester.commit();
+
+ try (Connection connection = dbTester.openConnection()) {
+ List<String[]> users = underTest.executeSelect(connection, "select " + LOGIN_DB_COLUMN + ", " + NAME_DB_COLUMN + " from users order by id", new SqlExecutor.StringsConverter(
+ 2));
+ assertThat(users).hasSize(2);
+ assertThat(users.get(0)[0]).isEqualTo("login1");
+ assertThat(users.get(0)[1]).isEqualTo("name one");
+ assertThat(users.get(1)[0]).isEqualTo("login2");
+ assertThat(users.get(1)[1]).isEqualTo("name two");
+ }
+ }
+
+ @Test
+ public void executeUpdate_executes_PreparedStatement() throws Exception {
+ dbTester.executeInsert(USERS_DB_TABLE, ImmutableMap.of(LOGIN_DB_COLUMN, "the_login", NAME_DB_COLUMN, "the name"));
+ dbTester.commit();
+
+ try (Connection connection = dbTester.openConnection()) {
+ underTest.executeUpdate(connection, "update users set " + NAME_DB_COLUMN + "='new name' where " + LOGIN_DB_COLUMN + "='the_login'");
+ connection.commit();
+ }
+ Map<String, Object> row = dbTester.selectFirst("select " + NAME_DB_COLUMN + " from users where " + LOGIN_DB_COLUMN + "='the_login'");
+ assertThat(row.get("NAME")).isEqualTo("new name");
+ }
+
+}