From 4fa35e84bc187d159d072cce2d312b79fff4e923 Mon Sep 17 00:00:00 2001 From: Baptiste Mesta Date: Sun, 19 Nov 2017 19:33:08 +0100 Subject: [PATCH] Add Sql formatter This first version of the sql formatter is taken from dbeaver project. It is configurable using closure ``` sql { dbeaver().configFile 'sqlFormatter.properties } ``` configuration file support following properties (with default values) ``` sql.formatter.keyword.case=UPPER sql.formatter.statement.delimiter=; sql.formatter.indent.type=space sql.formatter.indent.size=4 ``` --- .../spotless/sql/DBeaverSQLFormatter.java | 38 ++ .../spotless/sql/DBeaverSQLFormatterStep.java | 56 ++ .../spotless/sql/dbeaver/DBPKeywordType.java | 27 + .../spotless/sql/dbeaver/FormatterToken.java | 67 ++ .../spotless/sql/dbeaver/KeywordCase.java | 41 ++ .../diffplug/spotless/sql/dbeaver/Pair.java | 50 ++ .../spotless/sql/dbeaver/SQLConstants.java | 571 ++++++++++++++++++ .../spotless/sql/dbeaver/SQLDialect.java | 116 ++++ .../dbeaver/SQLFormatterConfiguration.java | 77 +++ .../sql/dbeaver/SQLTokenizedFormatter.java | 440 ++++++++++++++ .../spotless/sql/dbeaver/SQLTokensParser.java | 302 +++++++++ .../spotless/sql/dbeaver/TokenType.java | 21 + .../diffplug/spotless/sql/package-info.java | 7 + .../gradle/spotless/SpotlessExtension.java | 5 + .../gradle/spotless/SqlExtension.java | 63 ++ .../gradle/spotless/SqlExtensionTest.java | 68 +++ .../test/resources/sql/dbeaver/create.clean | 26 + .../sql/dbeaver/create.clean.alternative | 26 + .../test/resources/sql/dbeaver/create.dirty | 25 + .../resources/sql/dbeaver/myConfig.properties | 4 + .../sql/DBeaverSQLFormatterStepTest.java | 78 +++ .../sql/dbeaver/V1_initial.sql.clean | 112 ++++ .../sql/dbeaver/V1_initial.sql.dirty | 76 +++ .../resources/sql/dbeaver/alter-table.clean | 7 + .../resources/sql/dbeaver/alter-table.dirty | 2 + .../test/resources/sql/dbeaver/create.clean | 26 + .../sql/dbeaver/create.clean.alternative | 26 + .../test/resources/sql/dbeaver/create.dirty | 25 + .../src/test/resources/sql/dbeaver/full.clean | 59 ++ .../src/test/resources/sql/dbeaver/full.dirty | 13 + .../sql/dbeaver/sqlConfig.properties | 4 + .../sql/dbeaver/sqlConfig2.properties | 4 + 32 files changed, 2462 insertions(+) create mode 100644 lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatter.java create mode 100644 lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java create mode 100644 lib/src/main/java/com/diffplug/spotless/sql/dbeaver/DBPKeywordType.java create mode 100644 lib/src/main/java/com/diffplug/spotless/sql/dbeaver/FormatterToken.java create mode 100644 lib/src/main/java/com/diffplug/spotless/sql/dbeaver/KeywordCase.java create mode 100644 lib/src/main/java/com/diffplug/spotless/sql/dbeaver/Pair.java create mode 100644 lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLConstants.java create mode 100644 lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLDialect.java create mode 100644 lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLFormatterConfiguration.java create mode 100644 lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokenizedFormatter.java create mode 100644 lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokensParser.java create mode 100644 lib/src/main/java/com/diffplug/spotless/sql/dbeaver/TokenType.java create mode 100644 lib/src/main/java/com/diffplug/spotless/sql/package-info.java create mode 100644 plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SqlExtension.java create mode 100644 plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SqlExtensionTest.java create mode 100644 plugin-gradle/src/test/resources/sql/dbeaver/create.clean create mode 100644 plugin-gradle/src/test/resources/sql/dbeaver/create.clean.alternative create mode 100644 plugin-gradle/src/test/resources/sql/dbeaver/create.dirty create mode 100644 plugin-gradle/src/test/resources/sql/dbeaver/myConfig.properties create mode 100644 testlib/src/test/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStepTest.java create mode 100644 testlib/src/test/resources/sql/dbeaver/V1_initial.sql.clean create mode 100644 testlib/src/test/resources/sql/dbeaver/V1_initial.sql.dirty create mode 100644 testlib/src/test/resources/sql/dbeaver/alter-table.clean create mode 100644 testlib/src/test/resources/sql/dbeaver/alter-table.dirty create mode 100644 testlib/src/test/resources/sql/dbeaver/create.clean create mode 100644 testlib/src/test/resources/sql/dbeaver/create.clean.alternative create mode 100644 testlib/src/test/resources/sql/dbeaver/create.dirty create mode 100644 testlib/src/test/resources/sql/dbeaver/full.clean create mode 100644 testlib/src/test/resources/sql/dbeaver/full.dirty create mode 100644 testlib/src/test/resources/sql/dbeaver/sqlConfig.properties create mode 100644 testlib/src/test/resources/sql/dbeaver/sqlConfig2.properties diff --git a/lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatter.java b/lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatter.java new file mode 100644 index 0000000000..a213140796 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatter.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.sql; + +import java.util.Properties; + +import com.diffplug.spotless.sql.dbeaver.SQLFormatterConfiguration; +import com.diffplug.spotless.sql.dbeaver.SQLTokenizedFormatter; + +/** + * @author Baptiste Mesta. + */ +public class DBeaverSQLFormatter { + + private final SQLTokenizedFormatter sqlTokenizedFormatter; + + DBeaverSQLFormatter(Properties properties) { + SQLFormatterConfiguration configuration = new SQLFormatterConfiguration(properties); + sqlTokenizedFormatter = new SQLTokenizedFormatter(configuration); + } + + public String format(String input) { + return sqlTokenizedFormatter.format(input); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java new file mode 100644 index 0000000000..2968e71aca --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java @@ -0,0 +1,56 @@ +/* + * Copyright 2016 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.sql; + +import java.io.File; +import java.io.Serializable; + +import com.diffplug.spotless.FileSignature; +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterProperties; +import com.diffplug.spotless.FormatterStep; + +/** Wraps up [BasicFormatterImpl](https://docs.jboss.org/hibernate/orm/4.1/javadocs/org/hibernate/engine/jdbc/internal/BasicFormatterImpl.html) as a FormatterStep. */ +public class DBeaverSQLFormatterStep { + + static final String NAME = "dbeaverSql"; + + // prevent direct instantiation + private DBeaverSQLFormatterStep() {} + + public static FormatterStep create(Iterable files) { + return FormatterStep.createLazy(NAME, + () -> new State(files), + State::createFormat); + } + + static final class State implements Serializable { + private static final long serialVersionUID = 1L; + + /** The signature of the settings file. */ + final FileSignature settings; + + State(final Iterable settingsFiles) throws Exception { + this.settings = FileSignature.signAsList(settingsFiles); + } + + FormatterFunc createFormat() throws Exception { + FormatterProperties preferences = FormatterProperties.from(settings.files()); + DBeaverSQLFormatter DBeaverSqlFormatter = new DBeaverSQLFormatter(preferences.getProperties()); + return DBeaverSqlFormatter::format; + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/DBPKeywordType.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/DBPKeywordType.java new file mode 100644 index 0000000000..2219fd160d --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/DBPKeywordType.java @@ -0,0 +1,27 @@ +/* + * Copyright 2016 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.sql.dbeaver; + +/** + * Forked from + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) + * +* Database keyword type +*/ +public enum DBPKeywordType { + KEYWORD, FUNCTION, TYPE, OTHER +} diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/FormatterToken.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/FormatterToken.java new file mode 100644 index 0000000000..977b3e5b49 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/FormatterToken.java @@ -0,0 +1,67 @@ +/* + * Copyright 2016 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.sql.dbeaver; + +class FormatterToken { + + private TokenType fType; + private String fString; + private int fPos = -1; + + public FormatterToken(final TokenType argType, final String argString, final int argPos) { + fType = argType; + fString = argString; + fPos = argPos; + } + + public FormatterToken(final TokenType argType, final String argString) { + this(argType, argString, -1); + } + + public void setType(final TokenType argType) { + fType = argType; + } + + public TokenType getType() { + return fType; + } + + public void setString(final String argString) { + fString = argString; + } + + public String getString() { + return fString; + } + + public void setPos(final int argPos) { + fPos = argPos; + } + + public int getPos() { + return fPos; + } + + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append(getClass().getName()); + buf.append("type=").append(fType); + buf.append(",string=").append(fString); + buf.append(",pos=").append(fPos); + buf.append("]"); + return buf.toString(); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/KeywordCase.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/KeywordCase.java new file mode 100644 index 0000000000..5840a4503c --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/KeywordCase.java @@ -0,0 +1,41 @@ +/* + * Copyright 2016 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.sql.dbeaver; + +import java.util.Locale; + +/** + * @author Baptiste Mesta. + */ +public enum KeywordCase { + UPPER { + public String transform(String value) { + return value.toUpperCase(Locale.ENGLISH); + } + }, + LOWER { + public String transform(String value) { + return value.toLowerCase(Locale.ENGLISH); + } + }, + ORIGINAL { + public String transform(String value) { + return value; + } + }; + + public abstract String transform(String value); +} diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/Pair.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/Pair.java new file mode 100644 index 0000000000..87385f2999 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/Pair.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.sql.dbeaver; + +/** + * Pair + */ +public class Pair { + private T1 first; + private T2 second; + + public Pair(T1 first, T2 second) { + this.first = first; + this.second = second; + } + + public T1 getFirst() { + return first; + } + + public void setFirst(T1 first) { + this.first = first; + } + + public T2 getSecond() { + return second; + } + + public void setSecond(T2 second) { + this.second = second; + } + + @Override + public String toString() { + return first + "=" + second; + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLConstants.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLConstants.java new file mode 100644 index 0000000000..0fe6b914ef --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLConstants.java @@ -0,0 +1,571 @@ +/* + * Copyright 2016 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.sql.dbeaver; + +/** + * Forked from + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) + * + * SQL editor constants + */ +class SQLConstants { + + static final String ML_COMMENT_START = "/*"; + static final String ML_COMMENT_END = "*/"; + static final String SL_COMMENT = "--"; + + private static final String KEYWORD_SELECT = "SELECT"; + private static final String KEYWORD_ON = "ON"; + + static final String[] SQL2003_RESERVED_KEYWORDS = { + "ALL", + "ALLOCATE", + "ALTER", + "AND", + "ANY", + "ARE", + "ARRAY", + "AS", + "ASENSITIVE", + "ASYMMETRIC", + "AT", + "ATOMIC", + "AUTHORIZATION", + "BEGIN", + "BETWEEN", + //"BIGINT", + "BINARY", + "BOTH", + "BY", + "CALL", + "CALLED", + "CARDINALITY", + "CASCADE", + "CASCADED", + "CASE", + "CAST", + "CEIL", + "CEILING", + "CHARACTER", + "CHECK", + "CLOSE", + "COALESCE", + "COLLATE", + "COLLECT", + "COLUMN", + "COMMIT", + "CONDITION", + "CONNECT", + "CONSTRAINT", + "CONVERT", + "CORR", + "CORRESPONDING", + "COVAR_POP", + "COVAR_SAMP", + "CREATE", + "CROSS", + "CUBE", + "CUME_DIST", + "CURRENT", + "CURSOR", + "CYCLE", + "DAY", + "DEALLOCATE", + "DEC", + "DECLARE", + "DEFAULT", + "DELETE", + "DENSE_RANK", + "DEREF", + "DESCRIBE", + "DETERMINISTIC", + "DISCONNECT", + "DISTINCT", + "DROP", + "DYNAMIC", + "EACH", + "ELEMENT", + "ELSE", + "END", + "END-EXEC", + "ESCAPE", + "EVERY", + "EXCEPT", + "EXEC", + "EXECUTE", + "EXISTS", + "EXP", + "EXTERNAL", + "EXTRACT", + "FALSE", + "FETCH", + "FILTER", + "FOR", + "FOREIGN", + "FREE", + "FROM", + "FULL", + "FUNCTION", + "FUSION", + "GET", + "GLOBAL", + "GRANT", + "GROUP", + "GROUPING", + "HAVING", + "HOLD", + "HOUR", + "IDENTITY", + "IF", + "IN", + "INDEX", + "INDICATOR", + "INNER", + "INOUT", + "INSENSITIVE", + "INSERT", + "INTERSECT", + "INTERSECTION", + "INTERVAL", + "INTO", + "IS", + "JOIN", + "LANGUAGE", + "LARGE", + "LATERAL", + "LEFT", + "LIKE", + "LN", + "LOCAL", + "LOCALTIME", + "LOCALTIMESTAMP", + "MATCH", + "MEMBER", + "MERGE", + "METHOD", + "MINUTE", + "MOD", + "MODIFIES", + // "MODULE", // too common for column names + "MONTH", + "MULTISET", + "NATIONAL", + "NATURAL", + //"NCHAR", + //"NCLOB", + "NEW", + "NO", + "NONE", + "NORMALIZE", + "NOT", + "NULL", + "NULLIF", + "NUMERIC", + "OF", + "OLD", + KEYWORD_ON, + "ONLY", + "OPEN", + "OR", + "ORDER", + "OUT", + "OUTER", + "OVER", + "OVERLAPS", + "OVERLAY", + "PARAMETER", + "PARTITION", + "POSITION", + "PRECISION", + "PREPARE", + "PRIMARY", + "PROCEDURE", + "RANGE", + "RANK", + "READS", + "REAL", + "RECURSIVE", + "REF", + "REFERENCES", + "REFERENCING", + "RELEASE", + "RENAME", + "RESULT", + "RETURN", + "RETURNS", + "REVOKE", + "RIGHT", + "ROLLBACK", + "ROLLUP", + "ROW", + "ROW_NUMBER", + "ROWS", + "SAVEPOINT", + "SCOPE", + "SCROLL", + "SEARCH", + "SECOND", + KEYWORD_SELECT, + "SENSITIVE", + "SESSION_USER", + "SET", + "SIMILAR", + "SMALLINT", + "SOME", + "SPECIFIC", + "SPECIFICTYPE", + "SQL", + "SQLEXCEPTION", + "SQLSTATE", + "SQLWARNING", + "START", + "STATIC", + "STDDEV_POP", + "STDDEV_SAMP", + "SUBMULTISET", + "SYMMETRIC", + "SYSTEM", + "SYSTEM_USER", + "TABLE", + "TABLESAMPLE", + "THEN", + "TIMEZONE_HOUR", + "TIMEZONE_MINUTE", + "TO", + "TRAILING", + "TRANSLATE", + "TRANSLATION", + "TREAT", + "TRIGGER", + "TRUE", + "UNION", + "UNIQUE", + "UNKNOWN", + "UNNEST", + "UPDATE", + "USER", + "USING", + //"VALUE", // too common for column names + "VALUES", + "VAR_POP", + "VAR_SAMP", + //"VARCHAR", + "VARYING", + "WHEN", + "WHENEVER", + "WHERE", + "WIDTH_BUCKET", + "WINDOW", + "WITH", + "WITHIN", + "WITHOUT", + "YEAR", + + "NULLS", + "FIRST", + "LAST", + + "FOLLOWING", + "PRECEDING", + "UNBOUNDED", + + "LENGTH", + "KEY", + "LEVEL", + + "VIEW", + "SEQUENCE", + "SCHEMA", + "ROLE", + "RESTRICT", + "ASC", + "DESC", + + // Not actually standard but widely used + "LIMIT", + + // Extended keywords + // "A", + "ABSOLUTE", + "ACTION", + // "ADA", + "ADD", + // "ADMIN", + "AFTER", + "ALWAYS", + // "ASC", + "ASSERTION", + "ASSIGNMENT", + "ATTRIBUTE", + "ATTRIBUTES", + "BEFORE", + // "BERNOULLI", + // "BREADTH", + // "C", + "CASCADE", + "CATALOG", + // "CATALOG_NAME", + "CHAIN", + // "CHARACTER_SET_CATALOG", + // "CHARACTER_SET_NAME", + // "CHARACTER_SET_SCHEMA", + "CHARACTERISTICS", + "CHARACTERS", + // "CLASS_ORIGIN", + // "COBOL", + "COLLATION", + // "COLLATION_CATALOG", + // "COLLATION_NAME", + // "COLLATION_SCHEMA", + // "COLUMN_NAME", + // "COMMAND_FUNCTION", + // "COMMAND_FUNCTION_CODE", + "COMMITTED", + // "CONDITION_NUMBER", + "CONNECTION", + // "CONNECTION_NAME", + // "CONSTRAINT_CATALOG", + // "CONSTRAINT_NAME", + // "CONSTRAINT_SCHEMA", + "CONSTRAINTS", + "CONSTRUCTOR", + "CONTAINS", + "CONTINUE", + "CURSOR_NAME", + "DATA", + // "DATETIME_INTERVAL_CODE", + // "DATETIME_INTERVAL_PRECISION", + "DEFAULTS", + "DEFERRABLE", + "DEFERRED", + "DEFINED", + "DEFINER", + "DEGREE", + "DEPTH", + "DERIVED", + // "DESC", + "DESCRIPTOR", + "DIAGNOSTICS", + "DISPATCH", + "DOMAIN", + // "DYNAMIC_FUNCTION", + // "DYNAMIC_FUNCTION_CODE", + "EQUALS", + "EXCEPTION", + "EXCLUDE", + "EXCLUDING", + "FINAL", + "FIRST", + // "FORTRAN", + "FOUND", + // "G", + "GENERAL", + "GENERATED", + "GO", + "GOTO", + "GRANTED", + "HIERARCHY", + "IMMEDIATE", + "IMPLEMENTATION", + "INCLUDING", + "INCREMENT", + "INITIALLY", + "INPUT", + "INSTANCE", + "INSTANTIABLE", + "INVOKER", + "ISOLATION", + // "K", + // "KEY_MEMBER", + "KEY_TYPE", + "LAST", + "LOCATOR", + // "M", + "MAP", + "MATCHED", + "MAXVALUE", + // "MESSAGE_LENGTH", + // "MESSAGE_OCTET_LENGTH", + // "MESSAGE_TEXT", + "MINVALUE", + "MORE", + "MUMPS", + // "NAME", + // "NAMES", + "NESTING", + "NEXT", + "NORMALIZED", + // "NULLABLE", + // "NULLS", + // "NUMBER", + "OBJECT", + "OCTETS", + "OPTION", + "OPTIONS", + "ORDERING", + "ORDINALITY", + "OTHERS", + "OUTPUT", + "OVERRIDING", + "PAD", + // "PARAMETER_MODE", + // "PARAMETER_NAME", + // "PARAMETER_ORDINAL_POSITION", + // "PARAMETER_SPECIFIC_CATALOG", + // "PARAMETER_SPECIFIC_NAME", + // "PARAMETER_SPECIFIC_SCHEMA", + "PARTIAL", + // "PASCAL", + "PATH", + "PLACING", + // "PLI", + "PRESERVE", + "PRIOR", + "PRIVILEGES", + // "PUBLIC", + "READ", + "RELATIVE", + "REPEATABLE", + "RESTART", + // "RETURNED_CARDINALITY", + // "RETURNED_LENGTH", + // "RETURNED_OCTET_LENGTH", + // "RETURNED_SQLSTATE", + "ROUTINE", + // "ROUTINE_CATALOG", + // "ROUTINE_NAME", + // "ROUTINE_SCHEMA", + // "ROW_COUNT", + "SCALE", + // "SCHEMA_NAME", + // "SCOPE_CATALOG", + // "SCOPE_NAME", + // "SCOPE_SCHEMA", + "SECTION", + "SECURITY", + "SELF", + "SERIALIZABLE", + // "SERVER_NAME", + "SESSION", + "SETS", + // "SIMPLE", + "SIZE", + "SOURCE", + "SPACE", + // "SPECIFIC_NAME", + // "STATE", // too common for column names + "STATEMENT", + "STRUCTURE", + "STYLE", + // "SUBCLASS_ORIGIN", + // "TABLE_NAME", + "TEMPORARY", + "TIES", + // "TOP_LEVEL_COUNT", + "TRANSACTION", + // "TRANSACTION_ACTIVE", + // "TRANSACTIONS_COMMITTED", + // "TRANSACTIONS_ROLLED_BACK", + "TRANSFORM", + "TRANSFORMS", + // "TRIGGER_CATALOG", + // "TRIGGER_NAME", + // "TRIGGER_SCHEMA", + "TYPE", + "UNCOMMITTED", + "UNDER", + "UNNAMED", + "USAGE", + // "USER_DEFINED_TYPE_CATALOG", + // "USER_DEFINED_TYPE_CODE", + // "USER_DEFINED_TYPE_NAME", + // "USER_DEFINED_TYPE_SCHEMA", + "WORK", + "WRITE", + "ZONE" + }; + + static final String[] SQL2003_FUNCTIONS = { + "ABS", + "AVG", + "CHAR_LENGTH", + "CHARACTER_LENGTH", + "COUNT", + "CURRENT_DATE", + "CURRENT_DEFAULT_TRANSFORM_GROUP", + "CURRENT_PATH", + "CURRENT_ROLE", + "CURRENT_TIME", + "CURRENT_TIMESTAMP", + "CURRENT_TRANSFORM_GROUP_FOR_TYPE", + "CURRENT_USER", + "FLOOR", + "LEADING", + "LOWER", + "MAX", + "MIN", + "OCTET_LENGTH", + "PERCENT_RANK", + "PERCENTILE_CONT", + "PERCENTILE_DISC", + "POWER", + "REGR_AVGX", + "REGR_AVGY", + "REGR_COUNT", + "REGR_INTERCEPT", + "REGR_R2", + "REGR_SLOPE", + "REGR_SXX", + "REGR_SXY", + "REGR_SYY", + "SQRT", + "SUBSTRING", + "SUM", + "TRIM", + "UESCAPE", + "UPPER", + }; + + static final String[] SQL_EX_KEYWORDS = { + "CHANGE", + "MODIFY", + }; + static final String[] DEFAULT_TYPES = { + "BOOLEAN", + "CHAR", + "VARCHAR", + "BINARY", + "VARBINARY", + "INT", + "INTEGER", + "SMALLINT", + "BIGINT", + "NUMBER", + "NUMERIC", + "DECIMAL", + "FLOAT", + "DOUBLE", + "DATE", + "TIME", + "TIMESTAMP", + "CLOB", + "BLOB", + }; + + static final char STRUCT_SEPARATOR = '.'; //$NON-NLS-1$ + +} diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLDialect.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLDialect.java new file mode 100644 index 0000000000..096daf9359 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLDialect.java @@ -0,0 +1,116 @@ +/* + * Copyright 2016 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.sql.dbeaver; + +import java.util.*; + +/** + * Basic SQL Dialect + */ +class SQLDialect { + + private static final String[] DEFAULT_LINE_COMMENTS = {SQLConstants.SL_COMMENT}; + private static final String[] EXEC_KEYWORDS = new String[0]; + + private static final String[][] DEFAULT_QUOTE_STRINGS = {{"\"", "\""}}; + + // Keywords + private TreeMap allKeywords = new TreeMap<>(); + + private final TreeSet functions = new TreeSet<>(); + private final TreeSet types = new TreeSet<>(); + // Comments + private Pair multiLineComments = new Pair<>(SQLConstants.ML_COMMENT_START, SQLConstants.ML_COMMENT_END); + + static final SQLDialect INSTANCE = new SQLDialect(); + + private SQLDialect() { + loadStandardKeywords(); + } + + String[][] getIdentifierQuoteStrings() { + return DEFAULT_QUOTE_STRINGS; + } + + private String[] getExecuteKeywords() { + return EXEC_KEYWORDS; + } + + private void addSQLKeyword(String keyword) { + allKeywords.put(keyword, DBPKeywordType.KEYWORD); + } + + /** + * Add keywords. + * @param set keywords. Must be in upper case. + * @param type keyword type + */ + private void addKeywords(Collection set, DBPKeywordType type) { + for (String keyword : set) { + keyword = keyword.toUpperCase(Locale.ENGLISH); + DBPKeywordType oldType = allKeywords.get(keyword); + if (oldType != DBPKeywordType.KEYWORD) { + // We can't mark keywords as functions or types because keywords are reserved and + // if some identifier conflicts with keyword it must be quoted. + allKeywords.put(keyword, type); + } + } + } + + DBPKeywordType getKeywordType(String word) { + return allKeywords.get(word.toUpperCase(Locale.ENGLISH)); + } + + String getCatalogSeparator() { + return String.valueOf(SQLConstants.STRUCT_SEPARATOR); + } + + char getStructSeparator() { + return SQLConstants.STRUCT_SEPARATOR; + } + + String getScriptDelimiter() { + return ";"; + } + + Pair getMultiLineComments() { + return multiLineComments; + } + + String[] getSingleLineComments() { + return DEFAULT_LINE_COMMENTS; + } + + private void loadStandardKeywords() { + // Add default set of keywords + Set all = new HashSet<>(); + Collections.addAll(all, SQLConstants.SQL2003_RESERVED_KEYWORDS); + Collections.addAll(all, SQLConstants.SQL_EX_KEYWORDS); + Collections.addAll(functions, SQLConstants.SQL2003_FUNCTIONS); + + for (String executeKeyword : getExecuteKeywords()) { + addSQLKeyword(executeKeyword); + } + + // Add default types + Collections.addAll(types, SQLConstants.DEFAULT_TYPES); + + addKeywords(all, DBPKeywordType.KEYWORD); + addKeywords(types, DBPKeywordType.TYPE); + addKeywords(functions, DBPKeywordType.FUNCTION); + } + +} diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLFormatterConfiguration.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLFormatterConfiguration.java new file mode 100644 index 0000000000..5aec381438 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLFormatterConfiguration.java @@ -0,0 +1,77 @@ +/* + * Copyright 2016 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.sql.dbeaver; + +import java.util.Properties; + +/** + * SQLFormatterConfiguration + */ +public class SQLFormatterConfiguration { + + /** + * UPPER, LOWER or ORIGINAL + */ + public static final String SQL_FORMATTER_KEYWORD_CASE = "sql.formatter.keyword.case"; + + /** + * ';' by default + */ + public static final String SQL_FORMATTER_STATEMENT_DELIMITER = "sql.formatter.statement.delimiter"; + /** + * space or tag + */ + public static final String SQL_FORMATTER_INDENT_TYPE = "sql.formatter.indent.type"; + /** + * 4 by default + */ + public static final String SQL_FORMATTER_INDENT_SIZE = "sql.formatter.indent.size"; + + private String statementDelimiters; + private KeywordCase keywordCase; + private String indentString; + + public SQLFormatterConfiguration(Properties properties) { + this.keywordCase = KeywordCase.valueOf(properties.getProperty(SQL_FORMATTER_KEYWORD_CASE, "UPPER")); + this.statementDelimiters = properties.getProperty(SQL_FORMATTER_STATEMENT_DELIMITER, SQLDialect.INSTANCE + .getScriptDelimiter()); + String indentType = properties.getProperty(SQL_FORMATTER_INDENT_TYPE, "space"); + int indentSize = Integer.parseInt(properties.getProperty(SQL_FORMATTER_INDENT_SIZE, "4")); + indentString = getIndentString(indentType, indentSize); + } + + private String getIndentString(String indentType, int indentSize) { + char indentChar = indentType.equals("space") ? ' ' : '\t'; + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < indentSize; i++) { + stringBuilder.append(indentChar); + } + return stringBuilder.toString(); + } + + String getStatementDelimiter() { + return statementDelimiters; + } + + String getIndentString() { + return indentString; + } + + KeywordCase getKeywordCase() { + return keywordCase; + } + +} diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokenizedFormatter.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokenizedFormatter.java new file mode 100644 index 0000000000..44fd0019ec --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokenizedFormatter.java @@ -0,0 +1,440 @@ +/* + * Copyright 2016 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.sql.dbeaver; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +/** + * Forked from + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) + * + * SQL formatter + */ +public class SQLTokenizedFormatter { + + private static final String[] JOIN_BEGIN = {"LEFT", "RIGHT", "INNER", "OUTER", "JOIN"}; + private static final SQLDialect sqlDialect = SQLDialect.INSTANCE; + private SQLFormatterConfiguration formatterCfg; + private List functionBracket = new ArrayList<>(); + private List statementDelimiters = new ArrayList<>(2); + + public SQLTokenizedFormatter(SQLFormatterConfiguration formatterCfg) { + this.formatterCfg = formatterCfg; + } + + public String format(final String argSql) { + + statementDelimiters.add(formatterCfg.getStatementDelimiter()); + SQLTokensParser fParser = new SQLTokensParser(); + + functionBracket.clear(); + + boolean isSqlEndsWithNewLine = false; + if (argSql.endsWith("\n")) { + isSqlEndsWithNewLine = true; + } + + List list = fParser.parse(argSql); + list = format(list); + + StringBuilder after = new StringBuilder(argSql.length() + 20); + for (FormatterToken token : list) { + after.append(token.getString()); + } + + if (isSqlEndsWithNewLine) { + after.append(getDefaultLineSeparator()); + } + + return after.toString(); + } + + private List format(final List argList) { + if (argList.isEmpty()) { + return argList; + } + + FormatterToken token = argList.get(0); + if (token.getType() == TokenType.SPACE) { + argList.remove(0); + if (argList.isEmpty()) { + return argList; + } + } + + token = argList.get(argList.size() - 1); + if (token.getType() == TokenType.SPACE) { + argList.remove(argList.size() - 1); + if (argList.isEmpty()) { + return argList; + } + } + + final KeywordCase keywordCase = formatterCfg.getKeywordCase(); + for (FormatterToken anArgList : argList) { + token = anArgList; + if (token.getType() == TokenType.KEYWORD) { + token.setString(keywordCase.transform(token.getString())); + } + } + + // Remove extra tokens (spaces, etc) + for (int index = argList.size() - 1; index >= 1; index--) { + token = argList.get(index); + FormatterToken prevToken = argList.get(index - 1); + if (token.getType() == TokenType.SPACE && (prevToken.getType() == TokenType.SYMBOL || prevToken.getType() == TokenType.COMMENT)) { + argList.remove(index); + } else if ((token.getType() == TokenType.SYMBOL || token.getType() == TokenType.COMMENT) && prevToken.getType() == TokenType.SPACE) { + argList.remove(index - 1); + } else if (token.getType() == TokenType.SPACE) { + token.setString(" "); + } + } + + for (int index = 0; index < argList.size() - 2; index++) { + FormatterToken t0 = argList.get(index); + FormatterToken t1 = argList.get(index + 1); + FormatterToken t2 = argList.get(index + 2); + + String tokenString = t0.getString().toUpperCase(Locale.ENGLISH); + String token2String = t2.getString().toUpperCase(Locale.ENGLISH); + // Concatenate tokens + if (t0.getType() == TokenType.KEYWORD && t1.getType() == TokenType.SPACE && t2.getType() == TokenType.KEYWORD) { + if (((tokenString.equals("ORDER") || tokenString.equals("GROUP") || tokenString.equals("CONNECT")) && token2String.equals("BY")) || + ((tokenString.equals("START")) && token2String.equals("WITH"))) { + t0.setString(t0.getString() + " " + t2.getString()); + argList.remove(index + 1); + argList.remove(index + 1); + } + } + + // Oracle style joins + if (tokenString.equals("(") && t1.getString().equals("+") && token2String.equals(")")) { //$NON-NLS-2$ //$NON-NLS-3$ + t0.setString("(+)"); + argList.remove(index + 1); + argList.remove(index + 1); + } + } + + int indent = 0; + final List bracketIndent = new ArrayList<>(); + FormatterToken prev = new FormatterToken(TokenType.SPACE, " "); + boolean encounterBetween = false; + for (int index = 0; index < argList.size(); index++) { + token = argList.get(index); + String tokenString = token.getString().toUpperCase(Locale.ENGLISH); + if (token.getType() == TokenType.SYMBOL) { + if (tokenString.equals("(")) { + functionBracket.add(isFunction(prev.getString()) ? Boolean.TRUE : Boolean.FALSE); + bracketIndent.add(indent); + indent++; + index += insertReturnAndIndent(argList, index + 1, indent); + } else if (tokenString.equals(")") && !bracketIndent.isEmpty() && !functionBracket.isEmpty()) { + indent = bracketIndent.remove(bracketIndent.size() - 1); + index += insertReturnAndIndent(argList, index, indent); + functionBracket.remove(functionBracket.size() - 1); + } else if (tokenString.equals(",")) { + index += insertReturnAndIndent(argList, index + 1, indent); + } else if (statementDelimiters.contains(tokenString)) { + indent = 0; + index += insertReturnAndIndent(argList, index, indent); + } + } else if (token.getType() == TokenType.KEYWORD) { + switch (tokenString) { + case "DELETE": + case "SELECT": + case "UPDATE": + case "INSERT": + case "INTO": + case "CREATE": + case "DROP": + case "TRUNCATE": + case "TABLE": + case "CASE": + indent++; + index += insertReturnAndIndent(argList, index + 1, indent); + break; + case "FROM": + case "WHERE": + case "SET": + case "START WITH": + case "CONNECT BY": + case "ORDER BY": + case "GROUP BY": + case "HAVING": + index += insertReturnAndIndent(argList, index, indent - 1); + index += insertReturnAndIndent(argList, index + 1, indent); + break; + case "LEFT": + case "RIGHT": + case "INNER": + case "OUTER": + case "JOIN": + if (isJoinStart(argList, index)) { + index += insertReturnAndIndent(argList, index, indent - 1); + } + break; + case "VALUES": + case "END": + indent--; + index += insertReturnAndIndent(argList, index, indent); + break; + case "OR": + case "WHEN": + case "ELSE": + index += insertReturnAndIndent(argList, index, indent); + break; + case "ON": + //indent++; + index += insertReturnAndIndent(argList, index + 1, indent); + break; + case "USING": //$NON-NLS-2$ + index += insertReturnAndIndent(argList, index, indent + 1); + break; + case "TOP": //$NON-NLS-2$ + // SQL Server specific + index += insertReturnAndIndent(argList, index, indent); + if (argList.size() < index + 3) { + index += insertReturnAndIndent(argList, index + 3, indent); + } + break; + case "UNION": + case "INTERSECT": + case "EXCEPT": + indent -= 2; + index += insertReturnAndIndent(argList, index, indent); + //index += insertReturnAndIndent(argList, index + 1, indent); + indent++; + break; + case "BETWEEN": + encounterBetween = true; + break; + case "AND": + if (!encounterBetween) { + index += insertReturnAndIndent(argList, index, indent); + } + encounterBetween = false; + break; + default: + break; + } + } else if (token.getType() == TokenType.COMMENT) { + boolean isComment = false; + String[] slComments = sqlDialect.getSingleLineComments(); + for (String slc : slComments) { + if (token.getString().startsWith(slc)) { + isComment = true; + break; + } + } + if (!isComment) { + Pair mlComments = sqlDialect.getMultiLineComments(); + if (token.getString().startsWith(mlComments.getFirst())) { + index += insertReturnAndIndent(argList, index + 1, indent); + } + } + } else if (token.getType() == TokenType.COMMAND) { + indent = 0; + if (index > 0) { + index += insertReturnAndIndent(argList, index, 0); + } + index += insertReturnAndIndent(argList, index + 1, 0); + } else { + if (statementDelimiters.contains(tokenString)) { + indent = 0; + index += insertReturnAndIndent(argList, index + 1, indent); + } + } + prev = token; + } + + for (int index = argList.size() - 1; index >= 4; index--) { + if (index >= argList.size()) { + continue; + } + + FormatterToken t0 = argList.get(index); + FormatterToken t1 = argList.get(index - 1); + FormatterToken t2 = argList.get(index - 2); + FormatterToken t3 = argList.get(index - 3); + FormatterToken t4 = argList.get(index - 4); + + if (t4.getString().equals("(") + && t3.getString().trim().isEmpty() + && t1.getString().trim().isEmpty() + && t0.getString().equalsIgnoreCase(")")) { + t4.setString(t4.getString() + t2.getString() + t0.getString()); + argList.remove(index); + argList.remove(index - 1); + argList.remove(index - 2); + argList.remove(index - 3); + } + } + + for (int index = 1; index < argList.size(); index++) { + prev = argList.get(index - 1); + token = argList.get(index); + + if (prev.getType() != TokenType.SPACE && + token.getType() != TokenType.SPACE && + !token.getString().startsWith("(")) { + if (token.getString().equals(",") || statementDelimiters.contains(token.getString())) { + continue; + } + if (isFunction(prev.getString()) + && token.getString().equals("(")) { + continue; + } + if (token.getType() == TokenType.VALUE && prev.getType() == TokenType.NAME) { + // Do not add space between name and value [JDBC:MSSQL] + continue; + } + if (token.getType() == TokenType.SYMBOL && isEmbeddedToken(token) || + prev.getType() == TokenType.SYMBOL && isEmbeddedToken(prev)) { + // Do not insert spaces around colons + continue; + } + if (token.getType() == TokenType.SYMBOL && prev.getType() == TokenType.SYMBOL) { + // Do not add space between symbols + continue; + } + argList.add(index, new FormatterToken(TokenType.SPACE, " ")); + } + } + + return argList; + } + + private static boolean isEmbeddedToken(FormatterToken token) { + return ":".equals(token.getString()) || ".".equals(token.getString()); + } + + private boolean isJoinStart(List argList, int index) { + // Keyword sequence must start from LEFT, RIGHT, INNER, OUTER or JOIN and must end with JOIN + // And we must be in the beginning of sequence + + // check current token + if (!contains(JOIN_BEGIN, argList.get(index).getString())) { + return false; + } + // check previous token + for (int i = index - 1; i >= 0; i--) { + FormatterToken token = argList.get(i); + if (token.getType() == TokenType.SPACE) { + continue; + } + if (contains(JOIN_BEGIN, token.getString())) { + // It is not the begin of sequence + return false; + } else { + break; + } + } + // check last token + for (int i = index; i < argList.size(); i++) { + FormatterToken token = argList.get(i); + if (token.getType() == TokenType.SPACE) { + continue; + } + if (token.getString().equals("JOIN")) { + return true; + } + if (!contains(JOIN_BEGIN, token.getString())) { + // It is not the begin of sequence + return false; + } + } + return false; + } + + boolean isFunction(String name) { + return sqlDialect.getKeywordType(name) == DBPKeywordType.FUNCTION; + } + + private static String getDefaultLineSeparator() { + return System.getProperty("line.separator", "\n"); + } + + private int insertReturnAndIndent(final List argList, final int argIndex, final int argIndent) { + if (functionBracket.contains(Boolean.TRUE)) + return 0; + try { + StringBuilder s = new StringBuilder(getDefaultLineSeparator()); + if (argIndex > 0) { + final FormatterToken prevToken = argList.get(argIndex - 1); + if (prevToken.getType() == TokenType.COMMENT && + isCommentLine(sqlDialect, prevToken.getString())) { + s = new StringBuilder(); + } + } + for (int index = 0; index < argIndent; index++) { + s.append(formatterCfg.getIndentString()); + } + + FormatterToken token = argList.get(argIndex); + if (token.getType() == TokenType.SPACE) { + token.setString(s.toString()); + return 0; + } + boolean isDelimiter = statementDelimiters.contains(token.getString().toUpperCase(Locale.ENGLISH)); + + if (!isDelimiter) { + token = argList.get(argIndex - 1); + if (token.getType() == TokenType.SPACE) { + token.setString(s.toString()); + return 0; + } + } + + if (isDelimiter) { + if (argList.size() > argIndex + 1) { + String string = s.toString(); + argList.add(argIndex + 1, new FormatterToken(TokenType.SPACE, string + string)); + } + } else { + argList.add(argIndex, new FormatterToken(TokenType.SPACE, s.toString())); + } + return 1; + } catch (IndexOutOfBoundsException e) { + e.printStackTrace(); + return 0; + } + } + + private static boolean isCommentLine(SQLDialect dialect, String line) { + for (String slc : dialect.getSingleLineComments()) { + if (line.startsWith(slc)) { + return true; + } + } + return false; + } + + private static boolean contains(OBJECT_TYPE[] array, OBJECT_TYPE value) { + if (array == null || array.length == 0) + return false; + for (OBJECT_TYPE anArray : array) { + if (Objects.equals(value, anArray)) + return true; + } + return false; + } + +} diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokensParser.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokensParser.java new file mode 100644 index 0000000000..c388a99e42 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/SQLTokensParser.java @@ -0,0 +1,302 @@ +/* + * Copyright 2016 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.sql.dbeaver; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** + * Forked from + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) + * + * SQLTokensParser + */ +class SQLTokensParser { + + private static final String[] twoCharacterSymbol = {"<>", "<=", ">=", "||", "()", "!=", ":=", ".*"}; + private static final SQLDialect sqlDialect = SQLDialect.INSTANCE; + + private final String[][] quoteStrings; + private String fBefore = null; + private int fPos; + private char structSeparator; + private String catalogSeparator; + private Set commands = new HashSet<>(); + private String[] singleLineComments; + private char[] singleLineCommentStart; + + public SQLTokensParser() { + this.structSeparator = sqlDialect.getStructSeparator(); + this.catalogSeparator = sqlDialect.getCatalogSeparator(); + this.quoteStrings = sqlDialect.getIdentifierQuoteStrings(); + this.singleLineComments = sqlDialect.getSingleLineComments(); + this.singleLineCommentStart = new char[this.singleLineComments.length]; + for (int i = 0; i < singleLineComments.length; i++) { + if (singleLineComments[i].isEmpty()) + singleLineCommentStart[i] = 0; + else + singleLineCommentStart[i] = singleLineComments[i].charAt(0); + } + } + + public static boolean isSpace(final char argChar) { + return Character.isWhitespace(argChar); + } + + public static boolean isLetter(final char argChar) { + return !isSpace(argChar) && !isDigit(argChar) && !isSymbol(argChar); + } + + public static boolean isDigit(final char argChar) { + return Character.isDigit(argChar); + } + + public static boolean isSymbol(final char argChar) { + switch (argChar) { + case '"': // double quote + case '?': // question mark + case '%': // percent + case '&': // ampersand + case '\'': // quote + case '(': // left paren + case ')': // right paren + case '|': // vertical bar + case '*': // asterisk + case '+': // plus sign + case ',': // comma + case '-': // minus sign + case '.': // period + case '/': // solidus + case ':': // colon + case ';': // semicolon + case '<': // less than operator + case '=': // equals operator + case '>': // greater than operator + case '!': // greater than operator + case '~': // greater than operator + case '`': // apos + case '[': // bracket open + case ']': // bracket close + return true; + default: + return false; + } + } + + FormatterToken nextToken() { + int start_pos = fPos; + if (fPos >= fBefore.length()) { + fPos++; + return new FormatterToken(TokenType.END, "", start_pos); + } + + char fChar = fBefore.charAt(fPos); + + if (isSpace(fChar)) { + StringBuilder workString = new StringBuilder(); + for (;;) { + workString.append(fChar); + fChar = fBefore.charAt(fPos); + if (!isSpace(fChar)) { + return new FormatterToken(TokenType.SPACE, workString.toString(), start_pos); + } + fPos++; + if (fPos >= fBefore.length()) { + return new FormatterToken(TokenType.SPACE, workString.toString(), start_pos); + } + } + } else if (fChar == ';') { + fPos++; + return new FormatterToken(TokenType.SYMBOL, ";", start_pos); + } else if (isDigit(fChar)) { + StringBuilder s = new StringBuilder(); + while (isDigit(fChar) || fChar == '.' || fChar == 'e' || fChar == 'E') { + // if (ch == '.') type = Token.REAL; + s.append(fChar); + fPos++; + + if (fPos >= fBefore.length()) { + break; + } + + fChar = fBefore.charAt(fPos); + } + return new FormatterToken(TokenType.VALUE, s.toString(), start_pos); + } + // single line comment + else if (contains(singleLineCommentStart, fChar)) { + fPos++; + String commentString = null; + for (String slc : singleLineComments) { + if (fBefore.length() >= start_pos + slc.length() && slc.equals(fBefore.substring(start_pos, start_pos + slc.length()))) { + commentString = slc; + break; + } + } + if (commentString == null) { + return new FormatterToken(TokenType.SYMBOL, String.valueOf(fChar), start_pos); + } + fPos += commentString.length() - 1; + while (fPos < fBefore.length()) { + fPos++; + if (fBefore.charAt(fPos - 1) == '\n') { + break; + } + } + commentString = fBefore.substring(start_pos, fPos); + return new FormatterToken(TokenType.COMMENT, commentString, start_pos); + } else if (isLetter(fChar)) { + StringBuilder s = new StringBuilder(); + while (isLetter(fChar) || isDigit(fChar) || fChar == '*' || structSeparator == fChar || catalogSeparator.indexOf(fChar) != -1) { + s.append(fChar); + fPos++; + if (fPos >= fBefore.length()) { + break; + } + + fChar = fBefore.charAt(fPos); + } + String word = s.toString(); + if (commands.contains(word.toUpperCase(Locale.ENGLISH))) { + s.setLength(0); + for (; fPos < fBefore.length(); fPos++) { + fChar = fBefore.charAt(fPos); + if (fChar == '\n' || fChar == '\r') { + break; + } else { + s.append(fChar); + } + } + return new FormatterToken(TokenType.COMMAND, word + s.toString(), start_pos); + } + if (sqlDialect.getKeywordType(word) != null) { + return new FormatterToken(TokenType.KEYWORD, word, start_pos); + } + return new FormatterToken(TokenType.NAME, word, start_pos); + } else if (fChar == '/') { + fPos++; + char ch2 = fBefore.charAt(fPos); + if (ch2 != '*') { + return new FormatterToken(TokenType.SYMBOL, "/", start_pos); + } + + StringBuilder s = new StringBuilder("/*"); + fPos++; + for (;;) { + int ch0 = fChar; + fChar = fBefore.charAt(fPos); + s.append(fChar); + fPos++; + if (ch0 == '*' && fChar == '/') { + return new FormatterToken(TokenType.COMMENT, s.toString(), start_pos); + } + } + } else { + if (fChar == '\'' || isQuoteChar(fChar)) { + fPos++; + char endQuoteChar = fChar; + // Close quote char may differ + if (quoteStrings != null) { + for (String[] quoteString : quoteStrings) { + if (quoteString[0].charAt(0) == endQuoteChar) { + endQuoteChar = quoteString[1].charAt(0); + break; + } + } + } + + StringBuilder s = new StringBuilder(); + s.append(fChar); + for (;;) { + fChar = fBefore.charAt(fPos); + s.append(fChar); + fPos++; + char fNextChar = fPos >= fBefore.length() - 1 ? 0 : fBefore.charAt(fPos); + if (fChar == endQuoteChar && fNextChar == endQuoteChar) { + // Escaped quote + s.append(fChar); + fPos++; + continue; + } + if (fChar == endQuoteChar) { + return new FormatterToken(TokenType.VALUE, s.toString(), start_pos); + } + } + } + + else if (isSymbol(fChar)) { + StringBuilder s = new StringBuilder(String.valueOf(fChar)); + fPos++; + if (fPos >= fBefore.length()) { + return new FormatterToken(TokenType.SYMBOL, s.toString(), start_pos); + } + char ch2 = fBefore.charAt(fPos); + for (String aTwoCharacterSymbol : twoCharacterSymbol) { + if (aTwoCharacterSymbol.charAt(0) == fChar && aTwoCharacterSymbol.charAt(1) == ch2) { + fPos++; + s.append(ch2); + break; + } + } + return new FormatterToken(TokenType.SYMBOL, s.toString(), start_pos); + } else { + fPos++; + return new FormatterToken(TokenType.UNKNOWN, String.valueOf(fChar), start_pos); + } + } + } + + private boolean isQuoteChar(char fChar) { + if (quoteStrings != null) { + for (String[] quoteString : quoteStrings) { + if (quoteString[0].charAt(0) == fChar) { + return true; + } + } + } + return false; + } + + List parse(final String argSql) { + fPos = 0; + fBefore = argSql; + + final List list = new ArrayList<>(); + for (;;) { + final FormatterToken token = nextToken(); + if (token.getType() == TokenType.END) { + break; + } + + list.add(token); + } + return list; + } + + private static boolean contains(char[] array, char value) { + if (array == null || array.length == 0) + return false; + for (char aChar : array) { + if (aChar == value) + return true; + } + return false; + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/TokenType.java b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/TokenType.java new file mode 100644 index 0000000000..8043555dff --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/sql/dbeaver/TokenType.java @@ -0,0 +1,21 @@ +/* + * Copyright 2016 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.sql.dbeaver; + +enum TokenType { + + SPACE, SYMBOL, KEYWORD, NAME, VALUE, COMMAND, COMMENT, END, UNKNOWN +} diff --git a/lib/src/main/java/com/diffplug/spotless/sql/package-info.java b/lib/src/main/java/com/diffplug/spotless/sql/package-info.java new file mode 100644 index 0000000000..77088bf36c --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/sql/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@ReturnValuesAreNonnullByDefault +package com.diffplug.spotless.sql; + +import javax.annotation.ParametersAreNonnullByDefault; + +import com.diffplug.spotless.annotations.ReturnValuesAreNonnullByDefault; diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java index 355c1a0075..15877034d9 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java @@ -113,6 +113,11 @@ public void groovyGradle(Action closure) { configure(GroovyGradleExtension.NAME, GroovyGradleExtension.class, closure); } + /** Configures the special sql-specific extension for SQL files. */ + public void sql(Action closure) { + configure(SqlExtension.NAME, SqlExtension.class, closure); + } + /** Configures a custom extension. */ public void format(String name, Action closure) { requireNonNull(name, "name"); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SqlExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SqlExtension.java new file mode 100644 index 0000000000..a19222715a --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SqlExtension.java @@ -0,0 +1,63 @@ +/* + * Copyright 2016 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.gradle.spotless; + +import static com.diffplug.gradle.spotless.PluginGradlePreconditions.requireElementsNonNull; + +import org.gradle.api.Project; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.sql.DBeaverSQLFormatterStep; + +public class SqlExtension extends FormatExtension { + static final String NAME = "sql"; + + public SqlExtension(SpotlessExtension rootExtension) { + super(rootExtension); + } + + public DBeaverSQLFormatterConfig dbeaver() { + return new DBeaverSQLFormatterConfig(); + } + + public class DBeaverSQLFormatterConfig { + Object[] configFiles; + + DBeaverSQLFormatterConfig() { + configFiles = new Object[0]; + addStep(createStep()); + } + + public void configFile(Object... configFiles) { + this.configFiles = requireElementsNonNull(configFiles); + replaceStep(createStep()); + } + + private FormatterStep createStep() { + Project project = getProject(); + return DBeaverSQLFormatterStep.create(project.files(configFiles).getFiles()); + } + } + + /** If the user hasn't specified the files yet, we'll assume he/she means all of the sql files. */ + @Override + protected void setupTask(SpotlessTask task) { + if (target == null) { + target("**/*.sql"); + } + super.setupTask(task); + } +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SqlExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SqlExtensionTest.java new file mode 100644 index 0000000000..bc18116fa3 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SqlExtensionTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2016 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.gradle.spotless; + +import java.io.File; +import java.io.IOException; + +import org.junit.Test; + +public class SqlExtensionTest extends GradleIntegrationTest { + + @Test + public void should_format_sql_with_default_configuration() throws IOException { + write("build.gradle", + "plugins {", + " id 'com.diffplug.gradle.spotless'", + "}", + "spotless {", + " sql {", + " dbeaver()", + " }", + "}"); + + File sqlFile = write("src/main/resources/aFolder/create.sql", getTestResource("sql/dbeaver/create.dirty")); + + // Run + gradleRunner().withArguments("spotlessApply").build(); + + // Common checks + assertFileContent(getTestResource("sql/dbeaver/create.clean"), sqlFile); + } + + @Test + public void should_format_sql_with_alternative_configuration() throws IOException { + write("build.gradle", + "plugins {", + " id 'com.diffplug.gradle.spotless'", + "}", + "spotless {", + " sql {", + " dbeaver().configFile 'myConfig.properties'", + " }", + "}"); + + File sqlFile = write("src/main/resources/aFolder/create.sql", getTestResource("sql/dbeaver/create.dirty")); + write("myConfig.properties", getTestResource("sql/dbeaver/myConfig.properties")); + + // Run + gradleRunner().withArguments("spotlessApply").build(); + + // Common checks + assertFileContent(getTestResource("sql/dbeaver/create.clean.alternative"), sqlFile); + } + +} diff --git a/plugin-gradle/src/test/resources/sql/dbeaver/create.clean b/plugin-gradle/src/test/resources/sql/dbeaver/create.clean new file mode 100644 index 0000000000..38d9d560cb --- /dev/null +++ b/plugin-gradle/src/test/resources/sql/dbeaver/create.clean @@ -0,0 +1,26 @@ +CREATE + TABLE + films( + code CHAR(5) CONSTRAINT firstkey PRIMARY KEY, + title VARCHAR(40) NOT NULL, + did INTEGER NOT NULL, + date_prod DATE, + kind VARCHAR(10), + len INTERVAL HOUR TO MINUTE + ); + +CREATE + TABLE + distributors( + did INTEGER PRIMARY KEY DEFAULT nextval('serial'), + name VARCHAR(40) NOT NULL CHECK( + name <> '' + ) + ); + +-- Create a table with a 2-dimensional array: + CREATE + TABLE + array_int( + vector INT [][] + ); diff --git a/plugin-gradle/src/test/resources/sql/dbeaver/create.clean.alternative b/plugin-gradle/src/test/resources/sql/dbeaver/create.clean.alternative new file mode 100644 index 0000000000..1cf1668ee4 --- /dev/null +++ b/plugin-gradle/src/test/resources/sql/dbeaver/create.clean.alternative @@ -0,0 +1,26 @@ +create + table + films( + code char(5) constraint firstkey primary key, + title varchar(40) not null, + did integer not null, + date_prod date, + kind varchar(10), + len interval hour to minute + ); + +create + table + distributors( + did integer primary key default nextval('serial'), + name varchar(40) not null check( + name <> '' + ) + ); + +-- Create a table with a 2-dimensional array: + create + table + array_int( + vector int [][] + ); diff --git a/plugin-gradle/src/test/resources/sql/dbeaver/create.dirty b/plugin-gradle/src/test/resources/sql/dbeaver/create.dirty new file mode 100644 index 0000000000..60b852ba64 --- /dev/null +++ b/plugin-gradle/src/test/resources/sql/dbeaver/create.dirty @@ -0,0 +1,25 @@ +CREATE TABLE films ( + code char(5) CONSTRAINT firstkey PRIMARY KEY, + title varchar(40) NOT NULL, + + + did integer NOT NULL, + date_prod date, + kind varchar(10), + len interval hour to minute +); + +CREATE TABLE distributors ( + + + + + did integer PRIMARY KEY DEFAULT nextval('serial'), + name varchar(40) NOT NULL CHECK (name <> '') +); + +-- Create a table with a 2-dimensional array: + + CREATE TABLE array_int ( + vector int[][] +); diff --git a/plugin-gradle/src/test/resources/sql/dbeaver/myConfig.properties b/plugin-gradle/src/test/resources/sql/dbeaver/myConfig.properties new file mode 100644 index 0000000000..4d1ac4c935 --- /dev/null +++ b/plugin-gradle/src/test/resources/sql/dbeaver/myConfig.properties @@ -0,0 +1,4 @@ +sql.formatter.keyword.case=LOWER +sql.formatter.statement.delimiter=; +sql.formatter.indent.type=tab +sql.formatter.indent.size=2 \ No newline at end of file diff --git a/testlib/src/test/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStepTest.java b/testlib/src/test/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStepTest.java new file mode 100644 index 0000000000..5941cc84b1 --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStepTest.java @@ -0,0 +1,78 @@ +/* + * Copyright 2016 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.sql; + +import java.io.File; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.ResourceHarness; +import com.diffplug.spotless.SerializableEqualityTester; +import com.diffplug.spotless.StepHarness; + +public class DBeaverSQLFormatterStepTest extends ResourceHarness { + + @Test + public void behavior() throws Exception { + FormatterStep step = DBeaverSQLFormatterStep.create(Collections.emptySet()); + StepHarness.forStep(step) + .testResource("sql/dbeaver/full.dirty", "sql/dbeaver/full.clean") + .testResource("sql/dbeaver/V1_initial.sql.dirty", "sql/dbeaver/V1_initial.sql.clean") + .testResource("sql/dbeaver/alter-table.dirty", "sql/dbeaver/alter-table.clean") + .testResource("sql/dbeaver/create.dirty", "sql/dbeaver/create.clean"); + } + + @Test + public void behaviorWithConfigFile() throws Exception { + FormatterStep step = DBeaverSQLFormatterStep.create(createTestFiles("sql/dbeaver/sqlConfig.properties")); + StepHarness.forStep(step) + .testResource("sql/dbeaver/create.dirty", "sql/dbeaver/create.clean"); + } + + @Test + public void behaviorWithAlternativeConfigFile() throws Exception { + FormatterStep step = DBeaverSQLFormatterStep.create(createTestFiles("sql/dbeaver/sqlConfig2.properties")); + StepHarness.forStep(step) + .testResource("sql/dbeaver/create.dirty", "sql/dbeaver/create.clean.alternative"); + } + + @Test + public void equality() throws Exception { + List sqlConfig1 = createTestFiles("sql/dbeaver/sqlConfig.properties"); + List sqlConfig2 = createTestFiles("sql/dbeaver/sqlConfig2.properties"); + new SerializableEqualityTester() { + List settingsFiles; + + @Override + protected void setupTest(API api) { + settingsFiles = sqlConfig1; + api.areDifferentThan(); + + settingsFiles = sqlConfig2; + api.areDifferentThan(); + } + + @Override + protected FormatterStep create() { + return DBeaverSQLFormatterStep.create(settingsFiles); + } + }.testEquals(); + } + +} diff --git a/testlib/src/test/resources/sql/dbeaver/V1_initial.sql.clean b/testlib/src/test/resources/sql/dbeaver/V1_initial.sql.clean new file mode 100644 index 0000000000..20d18b61ef --- /dev/null +++ b/testlib/src/test/resources/sql/dbeaver/V1_initial.sql.clean @@ -0,0 +1,112 @@ +--- Account management + CREATE + TABLE + account( + id serial PRIMARY KEY, + username VARCHAR(60) NOT NULL UNIQUE, + email VARCHAR(513) NOT NULL UNIQUE, + name VARCHAR(255), + --NULLABLE + created_at TIMESTAMP NOT NULL, + created_ip inet NOT NULL, + updated_at TIMESTAMP NOT NULL, + updated_ip inet NOT NULL, + last_seen_at TIMESTAMP NOT NULL, + last_seen_ip inet NOT NULL, + last_emailed_at TIMESTAMP NOT NULL + ); + +CREATE + TABLE + loginlink( + code CHAR(44) PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + expires_at TIMESTAMP NOT NULL, + requestor_ip inet NOT NULL, + account_id INT NOT NULL REFERENCES account(id) + ); + +CREATE + TABLE + confirmaccountlink( + code CHAR(44) PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + expires_at TIMESTAMP NOT NULL, + requestor_ip inet NOT NULL, + username VARCHAR(60) NOT NULL, + email VARCHAR(513) NOT NULL + ); + +--- Takes + CREATE + TABLE + takerevision( + id serial PRIMARY KEY, + parent_id INT REFERENCES takerevision(id), + --NULLABLE (null for root) + created_at TIMESTAMP NOT NULL, + created_ip inet NOT NULL, + title VARCHAR(255) NOT NULL, + blocks jsonb NOT NULL + ); + +CREATE + TABLE + takedraft( + id serial PRIMARY KEY, + user_id INT NOT NULL REFERENCES account(id), + last_revision INT NOT NULL REFERENCES takerevision(id) + ); + +CREATE + TABLE + takepublished( + id serial PRIMARY KEY, + user_id INT NOT NULL REFERENCES account(id), + title VARCHAR(255) NOT NULL, + title_slug VARCHAR(255) NOT NULL, + blocks jsonb NOT NULL, + published_at TIMESTAMP NOT NULL, + published_ip inet NOT NULL, + deleted_at TIMESTAMP, + --NULLABLE + deleted_ip inet, + --NULLABLE + count_view INT NOT NULL DEFAULT 0, + count_like INT NOT NULL DEFAULT 0, + count_bookmark INT NOT NULL DEFAULT 0, + count_spam INT NOT NULL DEFAULT 0, + count_illegal INT NOT NULL DEFAULT 0 + ); + +-- /user/title must be unique, and fast to lookup + CREATE + UNIQUE INDEX takepublished_title_user ON + takepublished( + title_slug, + user_id + ); + +CREATE + TYPE reaction AS ENUM( + 'like', + 'bookmark', + 'spam', + 'illegal' + ); + +CREATE + TABLE + takereaction( + take_id INT NOT NULL REFERENCES takepublished(id), + user_id INT NOT NULL REFERENCES account(id), + kind reaction NOT NULL, + PRIMARY KEY( + take_id, + user_id, + kind + ), + --user can only have one of each kind of reaction per take + reacted_at TIMESTAMP NOT NULL, + reacted_ip inet NOT NULL + ); \ No newline at end of file diff --git a/testlib/src/test/resources/sql/dbeaver/V1_initial.sql.dirty b/testlib/src/test/resources/sql/dbeaver/V1_initial.sql.dirty new file mode 100644 index 0000000000..7626d8d0d6 --- /dev/null +++ b/testlib/src/test/resources/sql/dbeaver/V1_initial.sql.dirty @@ -0,0 +1,76 @@ +--- Account management +CREATE TABLE account ( + id serial PRIMARY KEY, + username varchar(60) NOT NULL UNIQUE, + email varchar(513) NOT NULL UNIQUE, + name varchar(255), --NULLABLE + created_at timestamp NOT NULL, + created_ip inet NOT NULL, + updated_at timestamp NOT NULL, + updated_ip inet NOT NULL, + last_seen_at timestamp NOT NULL, + last_seen_ip inet NOT NULL, + last_emailed_at timestamp NOT NULL +); + +CREATE TABLE loginlink ( + code char(44) PRIMARY KEY, + created_at timestamp NOT NULL, + expires_at timestamp NOT NULL, + requestor_ip inet NOT NULL, + account_id int NOT NULL REFERENCES account (id) +); + +CREATE TABLE confirmaccountlink ( + code char(44) PRIMARY KEY, + created_at timestamp NOT NULL, + expires_at timestamp NOT NULL, + requestor_ip inet NOT NULL, + username varchar(60) NOT NULL, + email varchar(513) NOT NULL +); + +--- Takes +CREATE TABLE takerevision ( + id serial PRIMARY KEY, + parent_id int REFERENCES takerevision (id), --NULLABLE (null for root) + created_at timestamp NOT NULL, + created_ip inet NOT NULL, + title varchar(255) NOT NULL, + blocks jsonb NOT NULL +); + +CREATE TABLE takedraft ( + id serial PRIMARY KEY, + user_id int NOT NULL REFERENCES account (id), + last_revision int NOT NULL REFERENCES takerevision (id) +); + +CREATE TABLE takepublished ( + id serial PRIMARY KEY, + user_id int NOT NULL REFERENCES account (id), + title varchar(255) NOT NULL, + title_slug varchar(255) NOT NULL, + blocks jsonb NOT NULL, + published_at timestamp NOT NULL, + published_ip inet NOT NULL, + deleted_at timestamp, --NULLABLE + deleted_ip inet, --NULLABLE + count_view int NOT NULL DEFAULT 0, + count_like int NOT NULL DEFAULT 0, + count_bookmark int NOT NULL DEFAULT 0, + count_spam int NOT NULL DEFAULT 0, + count_illegal int NOT NULL DEFAULT 0 +); +-- /user/title must be unique, and fast to lookup +CREATE UNIQUE INDEX takepublished_title_user ON takepublished (title_slug, user_id); + +CREATE TYPE reaction AS ENUM ('like', 'bookmark', 'spam', 'illegal'); +CREATE TABLE takereaction ( + take_id int NOT NULL REFERENCES takepublished (id), + user_id int NOT NULL REFERENCES account (id), + kind reaction NOT NULL, + PRIMARY KEY (take_id, user_id, kind), --user can only have one of each kind of reaction per take + reacted_at timestamp NOT NULL, + reacted_ip inet NOT NULL +); \ No newline at end of file diff --git a/testlib/src/test/resources/sql/dbeaver/alter-table.clean b/testlib/src/test/resources/sql/dbeaver/alter-table.clean new file mode 100644 index 0000000000..7b12a355b3 --- /dev/null +++ b/testlib/src/test/resources/sql/dbeaver/alter-table.clean @@ -0,0 +1,7 @@ +ALTER TABLE + userinfo ALTER COLUMN id DROP + DEFAULT; + +ALTER TABLE + userinfo DROP + CONSTRAINT userinfo_pkey CASCADE; \ No newline at end of file diff --git a/testlib/src/test/resources/sql/dbeaver/alter-table.dirty b/testlib/src/test/resources/sql/dbeaver/alter-table.dirty new file mode 100644 index 0000000000..1a8324e9f9 --- /dev/null +++ b/testlib/src/test/resources/sql/dbeaver/alter-table.dirty @@ -0,0 +1,2 @@ +ALTER TABLE userinfo ALTER COLUMN id drop DEFAULT; +ALTER TABLE userinfo drop constraint userinfo_pkey cascade; \ No newline at end of file diff --git a/testlib/src/test/resources/sql/dbeaver/create.clean b/testlib/src/test/resources/sql/dbeaver/create.clean new file mode 100644 index 0000000000..38d9d560cb --- /dev/null +++ b/testlib/src/test/resources/sql/dbeaver/create.clean @@ -0,0 +1,26 @@ +CREATE + TABLE + films( + code CHAR(5) CONSTRAINT firstkey PRIMARY KEY, + title VARCHAR(40) NOT NULL, + did INTEGER NOT NULL, + date_prod DATE, + kind VARCHAR(10), + len INTERVAL HOUR TO MINUTE + ); + +CREATE + TABLE + distributors( + did INTEGER PRIMARY KEY DEFAULT nextval('serial'), + name VARCHAR(40) NOT NULL CHECK( + name <> '' + ) + ); + +-- Create a table with a 2-dimensional array: + CREATE + TABLE + array_int( + vector INT [][] + ); diff --git a/testlib/src/test/resources/sql/dbeaver/create.clean.alternative b/testlib/src/test/resources/sql/dbeaver/create.clean.alternative new file mode 100644 index 0000000000..1cf1668ee4 --- /dev/null +++ b/testlib/src/test/resources/sql/dbeaver/create.clean.alternative @@ -0,0 +1,26 @@ +create + table + films( + code char(5) constraint firstkey primary key, + title varchar(40) not null, + did integer not null, + date_prod date, + kind varchar(10), + len interval hour to minute + ); + +create + table + distributors( + did integer primary key default nextval('serial'), + name varchar(40) not null check( + name <> '' + ) + ); + +-- Create a table with a 2-dimensional array: + create + table + array_int( + vector int [][] + ); diff --git a/testlib/src/test/resources/sql/dbeaver/create.dirty b/testlib/src/test/resources/sql/dbeaver/create.dirty new file mode 100644 index 0000000000..60b852ba64 --- /dev/null +++ b/testlib/src/test/resources/sql/dbeaver/create.dirty @@ -0,0 +1,25 @@ +CREATE TABLE films ( + code char(5) CONSTRAINT firstkey PRIMARY KEY, + title varchar(40) NOT NULL, + + + did integer NOT NULL, + date_prod date, + kind varchar(10), + len interval hour to minute +); + +CREATE TABLE distributors ( + + + + + did integer PRIMARY KEY DEFAULT nextval('serial'), + name varchar(40) NOT NULL CHECK (name <> '') +); + +-- Create a table with a 2-dimensional array: + + CREATE TABLE array_int ( + vector int[][] +); diff --git a/testlib/src/test/resources/sql/dbeaver/full.clean b/testlib/src/test/resources/sql/dbeaver/full.clean new file mode 100644 index 0000000000..bd53dad562 --- /dev/null +++ b/testlib/src/test/resources/sql/dbeaver/full.clean @@ -0,0 +1,59 @@ +SELECT + * +FROM + TABLE1 t +WHERE + a > 100 + AND b BETWEEN 12 AND 45; + +SELECT + t.*, + j1.x, + j2.y +FROM + TABLE1 t +JOIN JT1 j1 ON + j1.a = t.a +LEFT OUTER JOIN JT2 j2 ON + j2.a = t.a + AND j2.b = j1.b +WHERE + t.xxx NOT NULL; + +DELETE +FROM + TABLE1 +WHERE + a = 1; + +UPDATE + TABLE1 +SET + a = 2 +WHERE + a = 1; + +SELECT + table1.id, + table2.number, + SUM( table1.amount ) +FROM + table1 +INNER JOIN table2 ON + table.id = table2.table1_id +WHERE + table1.id IN( + SELECT + table1_id + FROM + table3 + WHERE + table3.name = 'Foo Bar' + AND table3.type = 'unknown_type' + AND table3.param =:aParam + ) +GROUP BY + table1.id, + table2.number +ORDER BY + table1.id; \ No newline at end of file diff --git a/testlib/src/test/resources/sql/dbeaver/full.dirty b/testlib/src/test/resources/sql/dbeaver/full.dirty new file mode 100644 index 0000000000..03b722747a --- /dev/null +++ b/testlib/src/test/resources/sql/dbeaver/full.dirty @@ -0,0 +1,13 @@ +Select * From TABLE1 t Where a > 100 AND b between 12 AND 45; + +SELECT t.*,j1.x,j2.y FROM TABLE1 t JOIN JT1 j1 ON j1.a = t.a +LEFT OUTER JOIN JT2 j2 ON j2.a=t.a AND j2.b=j1.b +WHERE t.xxx NOT NULL; + +DELETE FROM TABLE1 WHERE a=1; + +UPDATE TABLE1 SET a=2 WHERE a=1; + +SELECT table1.id, table2.number, SUM(table1.amount) FROM table1 INNER JOIN table2 ON table.id = table2.table1_id +WHERE table1.id IN (SELECT table1_id FROM table3 WHERE table3.name = 'Foo Bar' and table3.type = 'unknown_type' And table3.param = :aParam) +GROUP BY table1.id, table2.number ORDER BY table1.id; \ No newline at end of file diff --git a/testlib/src/test/resources/sql/dbeaver/sqlConfig.properties b/testlib/src/test/resources/sql/dbeaver/sqlConfig.properties new file mode 100644 index 0000000000..855fdb5c27 --- /dev/null +++ b/testlib/src/test/resources/sql/dbeaver/sqlConfig.properties @@ -0,0 +1,4 @@ +sql.formatter.keyword.case=UPPER +sql.formatter.statement.delimiter=; +sql.formatter.indent.type=space +sql.formatter.indent.size=4 diff --git a/testlib/src/test/resources/sql/dbeaver/sqlConfig2.properties b/testlib/src/test/resources/sql/dbeaver/sqlConfig2.properties new file mode 100644 index 0000000000..4d1ac4c935 --- /dev/null +++ b/testlib/src/test/resources/sql/dbeaver/sqlConfig2.properties @@ -0,0 +1,4 @@ +sql.formatter.keyword.case=LOWER +sql.formatter.statement.delimiter=; +sql.formatter.indent.type=tab +sql.formatter.indent.size=2 \ No newline at end of file