]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-15845 Ensure database entity identifier is not a reserved SQL keyword
authorKlaudio Sinani <klaudio.sinani@sonarsource.com>
Tue, 1 Feb 2022 16:33:44 +0000 (17:33 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 2 Feb 2022 20:02:54 +0000 (20:02 +0000)
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/def/Validations.java
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/def/ValidationsTest.java

index 26f6b44dc0eec2a3ba5bc702c306da82ceca2545..7890b65316715e97bf989ab37f7fec2c6ef28c41 100644 (file)
 package org.sonar.server.platform.db.migration.def;
 
 import com.google.common.base.CharMatcher;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import javax.annotation.Nullable;
 
 import static com.google.common.base.CharMatcher.anyOf;
@@ -39,6 +44,99 @@ public class Validations {
   private static final CharMatcher UPPER_CASE_ASCII_LETTERS_CHAR_MATCHER = inRange('A', 'Z');
   private static final CharMatcher UNDERSCORE_CHAR_MATCHER = anyOf("_");
 
+  // TODO: Refactor all existing identifiers that match SQL reserved keywords,
+  // the list below is used as a workaround for validating existing non-complaint identifiers
+  private static final String VALUE_COLUMN_NAME = "value";
+  private static final String GROUPS_TABLE_NAME = "groups";
+  private static final List<String> ALLOWED_IDENTIFIERS = List.of(VALUE_COLUMN_NAME, GROUPS_TABLE_NAME);
+
+  // MS SQL keywords retrieved from: com.microsoft.sqlserver.jdbc.SQLServerDatabaseMetaData#createSqlKeyWords
+  protected static final Set<String> MSSQL_KEYWORDS =  Set.of(
+          "ADD", "ALL", "ALTER", "AND", "ANY", "AS", "ASC", "AUTHORIZATION",
+          "BACKUP", "BEGIN", "BETWEEN", "BREAK", "BROWSE", "BULK", "BY",
+          "CASCADE", "CASE", "CHECK", "CHECKPOINT", "CLOSE", "CLUSTERED", "COALESCE", "COLLATE", "COLUMN", "COMMIT", "COMPUTE", "CONSTRAINT", "CONTAINS",
+          "CONTAINSTABLE", "CONTINUE", "CONVERT", "CREATE", "CROSS", "CURRENT", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "CURRENT_USER,CURSOR",
+          "DATABASE", "DBCC", "DEALLOCATE", "DECLARE", "DEFAULT", "DELETE", "DENY", "DESC", "DISK", "DISTINCT", "DISTRIBUTED", "DOUBLE", "DROP", "DUMP",
+          "ELSE", "END", "ERRLVL", "ESCAPE", "EXCEPT", "EXEC", "EXECUTE", "EXISTS", "EXIT", "EXTERNAL",
+          "FETCH", "FILE", "FILLFACTOR", "FOR", "FOREIGN", "FREETEXT", "FREETEXTTABLE", "FROM", "FULL", "FUNCTION",
+          "GOTO", "GRANT", "GROUP",
+          "HAVING", "HOLDLOCK",
+          "IDENTITY", "IDENTITY_INSERT", "IDENTITYCOL", "IF", "IN", "INDEX", "INNER", "INSERT", "INTERSECT", "INTO", "IS",
+          "JOIN",
+          "KEY", "KILL",
+          "LEFT", "LIKE",
+          "LINENO", "LOAD",
+          "MERGE",
+          "NATIONAL", "NOCHECK", "NONCLUSTERED", "NOT", "NULL", "NULLIF",
+          "OF", "OFF", "OFFSETS", "ON", "OPEN", "OPENDATASOURCE", "OPENQUERY", "OPENROWSET", "OPENXML", "OPTION", "OR", "ORDER", "OUTER", "OVER",
+          "PERCENT", "PIVOT", "PLAN", "PRECISION", "PRIMARY", "PRINT", "PROC", "PROCEDURE", "PUBLIC",
+          "RAISERROR", "READ", "READTEXT", "RECONFIGURE", "REFERENCES", "REPLICATION", "RESTORE", "RESTRICT",
+          "RETURN", "REVERT", "REVOKE", "RIGHT", "ROLLBACK", "ROWCOUNT", "ROWGUIDCOL", "RULE",
+          "SAVE", "SCHEMA", "SECURITYAUDIT", "SELECT", "SEMANTICKEYPHRASETABLE", "SEMANTICSIMILARITYDETAILSTABLE",
+          "SEMANTICSIMILARITYTABLE", "SESSION_USER", "SET", "SETUSER", "SHUTDOWN", "SOME", "STATISTICS", "SYSTEM_USER",
+          "TABLE", "TABLESAMPLE", "TEXTSIZE", "THEN", "TO", "TOP", "TRAN", "TRANSACTION", "TRIGGER", "TRUNCATE", "TRY_CONVERT", "TSEQUAL",
+          "UNION", "UNIQUE", "UNPIVOT", "UPDATE", "UPDATETEXT", "USE", "USER",
+          "VALUES", "VARYING", "VIEW",
+          "WAITFOR", "WHEN", "WHERE", "WHILE", "WITH", "WITHIN GROUP", "WRITETEXT");
+
+  // H2 SQL keywords retrieved from: http://www.h2database.com/html/advanced.html
+  protected static final Set<String> H2_KEYWORDS =  Set.of(
+          "ALL", "AND", "ANY", "ARRAY", "AS", "ASYMMETRIC", "AUTHORIZATION",
+          "BETWEEN", "BOTH",
+          "CASE", "CAST", "CHECK", "CONSTRAINT", "CROSS", "CURRENT_CATALOG", "CURRENT_DATE", "CURRENT_PATH",
+          "CURRENT_ROLE", "CURRENT_SCHEMA", "CURRENT_TIME", "CURRENT_TIMESTAMP", "CURRENT_USER",
+          "DAY", "DEFAULT", "DISTINCT",
+          "ELSE", "END", "EXISTS",
+          "FALSE", "FETCH", "FILTER", "FOR", "FOREIGN", "FROM", "FULL",
+          "GROUP", "GROUPS",
+          "HAVING", "HOUR",
+          "IF", "ILIKE", "IN", "INNER", "INTERSECT", "INTERVAL", "IS",
+          "JOIN",
+          "KEY",
+          "LEADING", "LEFT", "LIKE", "LIMIT", "LOCALTIME", "LOCALTIMESTAMP",
+          "MINUS", "MINUTE", "MONTH",
+          "NATURAL", "NOT", "NULL",
+          "OFFSET", "ON", "OR", "ORDER", "OVER",
+          "PARTITION", "PRIMARY",
+          "QUALIFY",
+          "RANGE", "REGEXP", "RIGHT", "ROW", "ROWNUM", "ROWS",
+          "SECOND", "SELECT", "SESSION_USER", "SET", "SOME", "SYMMETRIC", "SYSTEM_USER",
+          "TABLE", "TO", "TOP", "TRAILING", "TRUE",
+          "UESCAPE", "UNION", "UNIQUE", "UNKNOWN", "USER", "USING",
+          "VALUE", "VALUES",
+          "WHEN", "WHERE", "WINDOW", "WITH",
+          "YEAR",
+          "_ROWID_");
+
+  // PostgreSQL keywords retrieved from: https://www.postgresql.org/docs/current/sql-keywords-appendix.html
+  protected static final Set<String> POSTGRESQL_KEYWORDS =  Set.of(
+          "ALL", "AND", "ANY", "ARRAY", "AS", "ASYMMETRIC",
+          "BIGINT", "BINARY", "BIT", "BOOLEAN", "BOTH",
+          "CASE", "CAST", "CHAR", "CHARACTER", "COALESCE", "COLLATE", "COLLATION", "COLUMN", "CONCURRENTLY", "CREATE",
+          "CURRENT_CATALOG", "CURRENT_ROLE", "CURRENT_SCHEMA", "CURRENT_USER",
+          "DEC", "DECIMAL", "DEFERRABLE", "DESC",
+          "ELSE", "END", "EXCEPT",
+          "FALSE", "FOR", "FREEZE", "FROM", "FULL",
+          "GRANT", "GREATEST", "GROUPING",
+          "ILIKE", "IN", "INITIALLY", "INTO", "IS", "ISNULL",
+          "JOIN",
+          "LATERAL", "LEADING", "LEFT", "LIKE", "LIMIT", "LOCALTIME", "LOCALTIMESTAMP",
+          "NATIONAL", "NATURAL", "NCHAR", "NOT", "NOTNULL", "NULL",
+          "OFFSET", "ON", "ONLY", "OR", "OUTER", "OVERLAPS",
+          "PLACING",
+          "REFERENCES", "RETURNING",
+          "SETOF", "SIMILAR", "SMALLINT", "SOME", "SUBSTRING", "SYMMETRIC",
+          "TABLESAMPLE", "THEN", "TIME", "TIMESTAMP", "TO", "TRAILING", "TREAT", "TRIM", "TRUE",
+          "USER", "USING",
+          "VARCHAR", "VARIADIC", "VERBOSE",
+          "WHEN", "WINDOW", "WITH",
+          "XMLCONCAT", "XMLELEMENT", "XMLEXISTS", "XMLFOREST", "XMLNAMESPACES", "XMLPARSE", "XMLPI", "XMLROOT", "XMLSERIALIZE", "XMLTABLE",
+          "YEAR");
+
+  protected static final  Set<String> ALL_KEYWORDS = Stream.of(MSSQL_KEYWORDS, H2_KEYWORDS, POSTGRESQL_KEYWORDS)
+          .flatMap(Set::stream)
+          .collect(Collectors.toSet());
+
   private Validations() {
     // Only static stuff here
   }
@@ -125,6 +223,14 @@ public class Validations {
     return res;
   }
 
+  private static boolean isSqlKeyword(String identifier) {
+    return ALL_KEYWORDS.contains(identifier);
+  }
+
+  private static boolean isSqlKeywordIgnoreCase(String identifier) {
+    return isSqlKeyword(identifier.toUpperCase(Locale.ENGLISH));
+  }
+
   private static void checkDbIdentifierCharacters(String identifier, String identifierDesc) {
     checkArgument(identifier.length() > 0, "Identifier must not be empty");
     checkArgument(
@@ -133,6 +239,10 @@ public class Validations {
     checkArgument(
       DIGIT_CHAR_MATCHER.or(UNDERSCORE_CHAR_MATCHER).matchesNoneOf(identifier.subSequence(0, 1)),
       "%s must not start by a number or '_', got '%s'", identifierDesc, identifier);
+    checkArgument(!isSqlKeyword(identifier.toUpperCase(Locale.ENGLISH)) || ALLOWED_IDENTIFIERS.contains(identifier),
+      "%s must not be an SQL reserved keyword, got '%s'",
+      identifierDesc,
+      identifier);
   }
 
   private static void checkDbIdentifierCharactersIgnoreCase(String identifier, String identifierDesc) {
@@ -142,5 +252,9 @@ public class Validations {
     checkArgument(
       DIGIT_CHAR_MATCHER.or(UNDERSCORE_CHAR_MATCHER).matchesNoneOf(identifier.subSequence(0, 1)),
       "%s must not start by a number or '_', got '%s'", identifierDesc, identifier);
+    checkArgument(!isSqlKeywordIgnoreCase(identifier) || ALLOWED_IDENTIFIERS.contains(identifier.toLowerCase(Locale.ENGLISH)),
+      "%s must not be an SQL reserved keyword, got '%s'",
+      identifierDesc,
+      identifier);
   }
 }
index f51973197a0b5754c8f59dffd869edbb9349a8b8..52e8f1a55eb9f1d901bb2526e14bd4befc58e761 100644 (file)
@@ -22,9 +22,11 @@ package org.sonar.server.platform.db.migration.def;
 import org.junit.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.sonar.server.platform.db.migration.def.Validations.validateColumnName;
 import static org.sonar.server.platform.db.migration.def.Validations.validateIndexName;
+import static org.sonar.server.platform.db.migration.def.Validations.validateIndexNameIgnoreCase;
 
 public class ValidationsTest {
   @Test
@@ -40,6 +42,39 @@ public class ValidationsTest {
       .hasMessage("Column name cannot be null");
   }
 
+  @Test
+  public void fail_when_column_name_is_an_SQL_reserved_keyword() {
+    assertThatThrownBy(() -> validateColumnName("values"))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("Column name must not be an SQL reserved keyword, got 'values'");
+  }
+
+  @Test
+  public void accept_allowed_identifier_for_column_name_that_is_SQL_reserved_keyword() {
+    assertThatCode(() -> validateColumnName("value"))
+      .doesNotThrowAnyException();
+  }
+
+  @Test
+  public void fail_when_index_name_is_an_SQL_reserved_keyword_ignoring_case() {
+    assertThatThrownBy(() -> validateIndexNameIgnoreCase("VALUES"))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("Index name must not be an SQL reserved keyword, got 'VALUES'");
+
+    assertThatThrownBy(() -> validateIndexNameIgnoreCase("values"))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("Index name must not be an SQL reserved keyword, got 'values'");
+  }
+
+  @Test
+  public void accept_allowed_identifier_for_index_name_that_is_SQL_reserved_keyword_ignoring_case() {
+    assertThatCode(() -> validateIndexNameIgnoreCase("value"))
+      .doesNotThrowAnyException();
+
+    assertThatCode(() -> validateIndexNameIgnoreCase("VALUE"))
+      .doesNotThrowAnyException();
+  }
+
   @Test
   public void fail_when_column_name_is_in_upper_case() {
     assertThatThrownBy(() -> validateColumnName("DATE_IN_MS"))