diff options
author | Jenkins CI <ci@sonarsource.com> | 2016-04-26 08:07:44 +0200 |
---|---|---|
committer | Jenkins CI <ci@sonarsource.com> | 2016-04-26 08:07:44 +0200 |
commit | 52f0e092b0dd4d6aaa2e1b0543b094ce7c79176b (patch) | |
tree | d3c9e04d32714dbd777d53c01972877019171526 /sonar-db | |
parent | 4fa52cd10f951bd2a0e66acd480e49158099c435 (diff) | |
parent | c87a69b77de990c7efdf506b5a269e7facdb00b9 (diff) | |
download | sonarqube-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')
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"); + } + +} |