diff --git a/AUTHORS b/AUTHORS index 0997ab29c..9ef0cbfff 100644 --- a/AUTHORS +++ b/AUTHORS @@ -21,6 +21,7 @@ Erik Forsström Gajus Kuizinas George Leslie-Waksman Grant Forsythe +Hugh Cameron htaketani Ian Campbell ivan baktsheev diff --git a/README.md b/README.md index 9dce310f7..af7aab23c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ It started as a port of a [PHP Library][], but has since considerably diverged. It supports various SQL dialects: -GCP BigQuery, IBM DB2, Apache Hive, MariaDB, MySQL, TiDB, Couchbase N1QL, Oracle PL/SQL, PostgreSQL, Amazon Redshift, SingleStoreDB, Snowflake, Spark, SQL Server Transact-SQL, Trino (and Presto). +GCP BigQuery, IBM DB2, DuckDB, Apache Hive, MariaDB, MySQL, TiDB, Couchbase N1QL, Oracle PL/SQL, PostgreSQL, Amazon Redshift, SingleStoreDB, Snowflake, Spark, SQL Server Transact-SQL, Trino (and Presto). See [language option docs](docs/language.md) for more details. It does not support: diff --git a/docs/dialect.md b/docs/dialect.md index 798f3cc7c..c9a07da16 100644 --- a/docs/dialect.md +++ b/docs/dialect.md @@ -24,6 +24,7 @@ The following dialects can be imported from `"sql-formatter"` module: - `bigquery` - [GCP BigQuery][] - `db2` - [IBM DB2][] - `db2i` - [IBM DB2i][] (experimental) +- `duckdb` - [DuckDB][] - `hive` - [Apache Hive][] - `mariadb` - [MariaDB][] - `mysql` - [MySQL][] @@ -73,6 +74,7 @@ You likely only want to use this if your other alternative is to fork SQL Format [gcp bigquery]: https://cloud.google.com/bigquery [ibm db2]: https://www.ibm.com/analytics/us/en/technology/db2/ [ibm db2i]: https://www.ibm.com/docs/en/i/7.5?topic=overview-db2-i +[duckdb]: https://duckdb.org/ [apache hive]: https://hive.apache.org/ [mariadb]: https://mariadb.com/ [mysql]: https://www.mysql.com/ diff --git a/docs/language.md b/docs/language.md index a3d36d15c..027687cb6 100644 --- a/docs/language.md +++ b/docs/language.md @@ -16,6 +16,7 @@ const result = format('SELECT * FROM tbl', { language: 'sqlite' }); - `"bigquery"` - [GCP BigQuery][] - `"db2"` - [IBM DB2][] - `"db2i"` - [IBM DB2i][] (experimental) +- `"duckdb"` - [DuckDB][] - `"hive"` - [Apache Hive][] - `"mariadb"` - [MariaDB][] - `"mysql"` - [MySQL][] @@ -50,6 +51,7 @@ See docs for [dialect][] option. [gcp bigquery]: https://cloud.google.com/bigquery [ibm db2]: https://www.ibm.com/analytics/us/en/technology/db2/ [ibm db2i]: https://www.ibm.com/docs/en/i/7.5?topic=overview-db2-i +[duckdb]: https://duckdb.org/ [apache hive]: https://hive.apache.org/ [mariadb]: https://mariadb.com/ [mysql]: https://www.mysql.com/ diff --git a/package.json b/package.json index 63c68f17e..e467af7d4 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "trino", "presto", "prestosql", - "snowflake" + "snowflake", + "duckdb" ], "files": [ "dist", diff --git a/src/allDialects.ts b/src/allDialects.ts index 790ef2095..8b41f9735 100644 --- a/src/allDialects.ts +++ b/src/allDialects.ts @@ -1,6 +1,7 @@ export { bigquery } from './languages/bigquery/bigquery.formatter.js'; export { db2 } from './languages/db2/db2.formatter.js'; export { db2i } from './languages/db2i/db2i.formatter.js'; +export { duckdb } from './languages/duckdb/duckdb.formatter.js'; export { hive } from './languages/hive/hive.formatter.js'; export { mariadb } from './languages/mariadb/mariadb.formatter.js'; export { mysql } from './languages/mysql/mysql.formatter.js'; diff --git a/src/index.ts b/src/index.ts index d4aab0173..b5677f680 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ export { ConfigError } from './validateConfig.js'; export { bigquery } from './languages/bigquery/bigquery.formatter.js'; export { db2 } from './languages/db2/db2.formatter.js'; export { db2i } from './languages/db2i/db2i.formatter.js'; +export { duckdb } from './languages/duckdb/duckdb.formatter.js'; export { hive } from './languages/hive/hive.formatter.js'; export { mariadb } from './languages/mariadb/mariadb.formatter.js'; export { mysql } from './languages/mysql/mysql.formatter.js'; diff --git a/src/languages/duckdb/duckdb.formatter.ts b/src/languages/duckdb/duckdb.formatter.ts new file mode 100644 index 000000000..1f920bf2a --- /dev/null +++ b/src/languages/duckdb/duckdb.formatter.ts @@ -0,0 +1,216 @@ +import { DialectOptions } from '../../dialect.js'; +import { expandPhrases } from '../../expandPhrases.js'; +import { functions } from './duckdb.functions.js'; +import { dataTypes, keywords } from './duckdb.keywords.js'; + +const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT]']); + +const reservedClauses = expandPhrases([ + // queries + 'WITH [RECURSIVE]', + 'FROM', + 'WHERE', + 'GROUP BY [ALL]', + 'HAVING', + 'WINDOW', + 'PARTITION BY', + 'ORDER BY [ALL]', + 'LIMIT', + 'OFFSET', + // 'USING' (conflicts with 'USING' in JOIN) + 'USING SAMPLE', + 'QUALIFY', + // Data manipulation + // - insert: + 'INSERT [OR REPLACE] INTO', + 'VALUES', + 'DEFAULT VALUES', + // - update: + 'SET', + // other: + 'RETURNING', +]); + +const standardOnelineClauses = expandPhrases([ + 'CREATE [OR REPLACE] [TEMPORARY | TEMP] TABLE [IF NOT EXISTS]', +]); + +const tabularOnelineClauses = expandPhrases([ + // TABLE + // - update: + 'UPDATE', + // - insert: + 'ON CONFLICT', + // - delete: + 'DELETE FROM', + // - drop table: + 'DROP TABLE [IF EXISTS]', + // - truncate + 'TRUNCATE', + // - alter table: + 'ALTER TABLE', + 'ADD [COLUMN] [IF NOT EXISTS]', + 'ADD PRIMARY KEY', + 'DROP [COLUMN] [IF EXISTS]', + 'ALTER [COLUMN]', + 'RENAME [COLUMN]', + 'RENAME TO', + 'SET [DATA] TYPE', // for alter column + '{SET | DROP} DEFAULT', // for alter column + '{SET | DROP} NOT NULL', // for alter column + + // MACRO / FUNCTION + 'CREATE [OR REPLACE] [TEMPORARY | TEMP] {MACRO | FUNCTION}', + 'DROP MACRO [TABLE] [IF EXISTS]', + 'DROP FUNCTION [IF EXISTS]', + // INDEX + 'CREATE [UNIQUE] INDEX [IF NOT EXISTS]', + 'DROP INDEX [IF EXISTS]', + // SCHEMA + 'CREATE [OR REPLACE] SCHEMA [IF NOT EXISTS]', + 'DROP SCHEMA [IF EXISTS]', + // SECRET + 'CREATE [OR REPLACE] [PERSISTENT | TEMPORARY] SECRET [IF NOT EXISTS]', + 'DROP [PERSISTENT | TEMPORARY] SECRET [IF EXISTS]', + // SEQUENCE + 'CREATE [OR REPLACE] [TEMPORARY | TEMP] SEQUENCE', + 'DROP SEQUENCE [IF EXISTS]', + // VIEW + 'CREATE [OR REPLACE] [TEMPORARY | TEMP] VIEW [IF NOT EXISTS]', + 'DROP VIEW [IF EXISTS]', + 'ALTER VIEW', + // TYPE + 'CREATE TYPE', + 'DROP TYPE [IF EXISTS]', + + // other + 'ANALYZE', + 'ATTACH [DATABASE] [IF NOT EXISTS]', + 'DETACH [DATABASE] [IF EXISTS]', + 'CALL', + '[FORCE] CHECKPOINT', + 'COMMENT ON [TABLE | COLUMN | VIEW | INDEX | SEQUENCE | TYPE | MACRO | MACRO TABLE]', + 'COPY [FROM DATABASE]', + 'DESCRIBE', + 'EXPORT DATABASE', + 'IMPORT DATABASE', + 'INSTALL', + 'LOAD', + 'PIVOT', + 'PIVOT_WIDER', + 'UNPIVOT', + 'EXPLAIN [ANALYZE]', + // plain SET conflicts with SET clause in UPDATE + 'SET {LOCAL | SESSION | GLOBAL}', + 'RESET [LOCAL | SESSION | GLOBAL]', + '{SET | RESET} VARIABLE', + 'SUMMARIZE', + 'BEGIN TRANSACTION', + 'ROLLBACK', + 'COMMIT', + 'ABORT', + 'USE', + 'VACUUM [ANALYZE]', + // prepared statements + 'PREPARE', + 'EXECUTE', + 'DEALLOCATE [PREPARE]', +]); + +const reservedSetOperations = expandPhrases([ + 'UNION [ALL | BY NAME]', + 'EXCEPT [ALL]', + 'INTERSECT [ALL]', +]); + +const reservedJoins = expandPhrases([ + 'JOIN', + '{LEFT | RIGHT | FULL} [OUTER] JOIN', + '{INNER | CROSS} JOIN', + '{NATURAL | ASOF} [INNER] JOIN', + '{NATURAL | ASOF} {LEFT | RIGHT | FULL} [OUTER] JOIN', + 'POSITIONAL JOIN', + 'ANTI JOIN', + 'SEMI JOIN', +]); + +const reservedPhrases = expandPhrases([ + '{ROWS | RANGE | GROUPS} BETWEEN', + 'SIMILAR TO', + 'IS [NOT] DISTINCT FROM', + 'TIMESTAMP WITH TIME ZONE', +]); + +export const duckdb: DialectOptions = { + name: 'duckdb', + tokenizerOptions: { + reservedSelect, + reservedClauses: [...reservedClauses, ...standardOnelineClauses, ...tabularOnelineClauses], + reservedSetOperations, + reservedJoins, + reservedPhrases, + supportsXor: true, + reservedKeywords: keywords, + reservedDataTypes: dataTypes, + reservedFunctionNames: functions, + nestedBlockComments: true, + extraParens: ['[]', '{}'], + stringTypes: [ + '$$', + "''-qq", + { quote: "''-qq-bs", prefixes: ['E'], requirePrefix: true }, + { quote: "''-raw", prefixes: ['B', 'X'], requirePrefix: true }, + ], + identTypes: [`""-qq`], + identChars: { rest: '$' }, + // TODO: named params $foo currently conflict with $$-quoted strings + paramTypes: { positional: true, numbered: ['$'], quoted: ['$'] }, + operators: [ + // Arithmetic: + '//', + '%', + '**', + '^', + '!', + // Bitwise: + '&', + '|', + '~', + '<<', + '>>', + // Cast: + '::', + // Comparison: + '==', + // Lambda: + '->', + // key-value separator: + ':', + // Named function params: + ':=', + '=>', + // Pattern matching: + '~~', + '!~~', + '~~*', + '!~~*', + '~~~', + // Regular expressions: + '~', + '!~', + '~*', + '!~*', + // String: + '^@', + '||', + // INET extension: + '>>=', + '<<=', + ], + }, + formatOptions: { + alwaysDenseOperators: ['::'], + onelineClauses: [...standardOnelineClauses, ...tabularOnelineClauses], + tabularOnelineClauses, + }, +}; diff --git a/src/languages/duckdb/duckdb.functions.ts b/src/languages/duckdb/duckdb.functions.ts new file mode 100644 index 000000000..56399a534 --- /dev/null +++ b/src/languages/duckdb/duckdb.functions.ts @@ -0,0 +1,665 @@ +export const functions: string[] = [ + // Functions from DuckDB (excluding those that start with an underscore): + // SELECT DISTINCT upper(function_name) AS function_name + // FROM duckdb_functions() + // WHERE function_name SIMILAR TO '^[a-z].*' + // ORDER BY function_name + 'ABS', + 'ACOS', + 'ADD', + 'ADD_PARQUET_KEY', + 'AGE', + 'AGGREGATE', + 'ALIAS', + 'ALL_PROFILING_OUTPUT', + 'ANY_VALUE', + 'APPLY', + 'APPROX_COUNT_DISTINCT', + 'APPROX_QUANTILE', + 'ARBITRARY', + 'ARGMAX', + 'ARGMIN', + 'ARG_MAX', + 'ARG_MAX_NULL', + 'ARG_MIN', + 'ARG_MIN_NULL', + 'ARRAY_AGG', + 'ARRAY_AGGR', + 'ARRAY_AGGREGATE', + 'ARRAY_APPEND', + 'ARRAY_APPLY', + 'ARRAY_CAT', + 'ARRAY_CONCAT', + 'ARRAY_CONTAINS', + 'ARRAY_COSINE_SIMILARITY', + 'ARRAY_CROSS_PRODUCT', + 'ARRAY_DISTANCE', + 'ARRAY_DISTINCT', + 'ARRAY_DOT_PRODUCT', + 'ARRAY_EXTRACT', + 'ARRAY_FILTER', + 'ARRAY_GRADE_UP', + 'ARRAY_HAS', + 'ARRAY_HAS_ALL', + 'ARRAY_HAS_ANY', + 'ARRAY_INDEXOF', + 'ARRAY_INNER_PRODUCT', + 'ARRAY_INTERSECT', + 'ARRAY_LENGTH', + 'ARRAY_POP_BACK', + 'ARRAY_POP_FRONT', + 'ARRAY_POSITION', + 'ARRAY_PREPEND', + 'ARRAY_PUSH_BACK', + 'ARRAY_PUSH_FRONT', + 'ARRAY_REDUCE', + 'ARRAY_RESIZE', + 'ARRAY_REVERSE', + 'ARRAY_REVERSE_SORT', + 'ARRAY_SELECT', + 'ARRAY_SLICE', + 'ARRAY_SORT', + 'ARRAY_TO_JSON', + 'ARRAY_TO_STRING', + 'ARRAY_TRANSFORM', + 'ARRAY_UNIQUE', + 'ARRAY_VALUE', + 'ARRAY_WHERE', + 'ARRAY_ZIP', + 'ARROW_SCAN', + 'ARROW_SCAN_DUMB', + 'ASCII', + 'ASIN', + 'ATAN', + 'ATAN2', + 'AVG', + 'BASE64', + 'BIN', + 'BITSTRING', + 'BITSTRING_AGG', + 'BIT_AND', + 'BIT_COUNT', + 'BIT_LENGTH', + 'BIT_OR', + 'BIT_POSITION', + 'BIT_XOR', + 'BOOL_AND', + 'BOOL_OR', + 'CARDINALITY', + 'CBRT', + 'CEIL', + 'CEILING', + 'CENTURY', + 'CHECKPOINT', + 'CHR', + 'COLLATIONS', + 'COL_DESCRIPTION', + 'COMBINE', + 'CONCAT', + 'CONCAT_WS', + 'CONSTANT_OR_NULL', + 'CONTAINS', + 'COPY_DATABASE', + 'CORR', + 'COS', + 'COT', + 'COUNT', + 'COUNT_IF', + 'COUNT_STAR', + 'COVAR_POP', + 'COVAR_SAMP', + 'CREATE_SORT_KEY', + 'CURRENT_CATALOG', + 'CURRENT_DATABASE', + 'CURRENT_DATE', + 'CURRENT_LOCALTIME', + 'CURRENT_LOCALTIMESTAMP', + 'CURRENT_QUERY', + 'CURRENT_ROLE', + 'CURRENT_SCHEMA', + 'CURRENT_SCHEMAS', + 'CURRENT_SETTING', + 'CURRENT_USER', + 'CURRVAL', + 'DAMERAU_LEVENSHTEIN', + 'DATABASE_LIST', + 'DATABASE_SIZE', + 'DATEDIFF', + 'DATEPART', + 'DATESUB', + 'DATETRUNC', + 'DATE_ADD', + 'DATE_DIFF', + 'DATE_PART', + 'DATE_SUB', + 'DATE_TRUNC', + 'DAY', + 'DAYNAME', + 'DAYOFMONTH', + 'DAYOFWEEK', + 'DAYOFYEAR', + 'DECADE', + 'DECODE', + 'DEGREES', + 'DISABLE_CHECKPOINT_ON_SHUTDOWN', + 'DISABLE_OBJECT_CACHE', + 'DISABLE_OPTIMIZER', + 'DISABLE_PRINT_PROGRESS_BAR', + 'DISABLE_PROFILE', + 'DISABLE_PROFILING', + 'DISABLE_PROGRESS_BAR', + 'DISABLE_VERIFICATION', + 'DISABLE_VERIFY_EXTERNAL', + 'DISABLE_VERIFY_FETCH_ROW', + 'DISABLE_VERIFY_PARALLELISM', + 'DISABLE_VERIFY_SERIALIZER', + 'DIVIDE', + 'DUCKDB_COLUMNS', + 'DUCKDB_CONSTRAINTS', + 'DUCKDB_DATABASES', + 'DUCKDB_DEPENDENCIES', + 'DUCKDB_EXTENSIONS', + 'DUCKDB_FUNCTIONS', + 'DUCKDB_INDEXES', + 'DUCKDB_KEYWORDS', + 'DUCKDB_MEMORY', + 'DUCKDB_OPTIMIZERS', + 'DUCKDB_SCHEMAS', + 'DUCKDB_SECRETS', + 'DUCKDB_SEQUENCES', + 'DUCKDB_SETTINGS', + 'DUCKDB_TABLES', + 'DUCKDB_TEMPORARY_FILES', + 'DUCKDB_TYPES', + 'DUCKDB_VIEWS', + 'EDIT', + 'EDITDIST3', + 'ELEMENT_AT', + 'ENABLE_CHECKPOINT_ON_SHUTDOWN', + 'ENABLE_OBJECT_CACHE', + 'ENABLE_OPTIMIZER', + 'ENABLE_PRINT_PROGRESS_BAR', + 'ENABLE_PROFILE', + 'ENABLE_PROFILING', + 'ENABLE_PROGRESS_BAR', + 'ENABLE_VERIFICATION', + 'ENCODE', + 'ENDS_WITH', + 'ENTROPY', + 'ENUM_CODE', + 'ENUM_FIRST', + 'ENUM_LAST', + 'ENUM_RANGE', + 'ENUM_RANGE_BOUNDARY', + 'EPOCH', + 'EPOCH_MS', + 'EPOCH_NS', + 'EPOCH_US', + 'ERA', + 'ERROR', + 'EVEN', + 'EXP', + 'FACTORIAL', + 'FAVG', + 'FDIV', + 'FILTER', + 'FINALIZE', + 'FIRST', + 'FLATTEN', + 'FLOOR', + 'FMOD', + 'FORCE_CHECKPOINT', + 'FORMAT', + 'FORMATREADABLEDECIMALSIZE', + 'FORMATREADABLESIZE', + 'FORMAT_BYTES', + 'FORMAT_PG_TYPE', + 'FORMAT_TYPE', + 'FROM_BASE64', + 'FROM_BINARY', + 'FROM_HEX', + 'FROM_JSON', + 'FROM_JSON_STRICT', + 'FSUM', + 'FUNCTIONS', + 'GAMMA', + 'GCD', + 'GENERATE_SERIES', + 'GENERATE_SUBSCRIPTS', + 'GEN_RANDOM_UUID', + 'GEOMEAN', + 'GEOMETRIC_MEAN', + 'GETENV', + 'GET_BIT', + 'GET_BLOCK_SIZE', + 'GET_CURRENT_TIME', + 'GET_CURRENT_TIMESTAMP', + 'GLOB', + 'GRADE_UP', + 'GREATEST', + 'GREATEST_COMMON_DIVISOR', + 'GROUP_CONCAT', + 'HAMMING', + 'HASH', + 'HAS_ANY_COLUMN_PRIVILEGE', + 'HAS_COLUMN_PRIVILEGE', + 'HAS_DATABASE_PRIVILEGE', + 'HAS_FOREIGN_DATA_WRAPPER_PRIVILEGE', + 'HAS_FUNCTION_PRIVILEGE', + 'HAS_LANGUAGE_PRIVILEGE', + 'HAS_SCHEMA_PRIVILEGE', + 'HAS_SEQUENCE_PRIVILEGE', + 'HAS_SERVER_PRIVILEGE', + 'HAS_TABLESPACE_PRIVILEGE', + 'HAS_TABLE_PRIVILEGE', + 'HEX', + 'HISTOGRAM', + 'HOUR', + 'ICU_CALENDAR_NAMES', + 'ICU_SORT_KEY', + 'ILIKE_ESCAPE', + 'IMPORT_DATABASE', + 'INDEX_SCAN', + 'INET_CLIENT_ADDR', + 'INET_CLIENT_PORT', + 'INET_SERVER_ADDR', + 'INET_SERVER_PORT', + 'INSTR', + 'IN_SEARCH_PATH', + 'ISFINITE', + 'ISINF', + 'ISNAN', + 'ISODOW', + 'ISOYEAR', + 'JACCARD', + 'JARO_SIMILARITY', + 'JARO_WINKLER_SIMILARITY', + // 'JSON', + 'JSON_ARRAY', + 'JSON_ARRAY_LENGTH', + 'JSON_CONTAINS', + 'JSON_DESERIALIZE_SQL', + 'JSON_EXECUTE_SERIALIZED_SQL', + 'JSON_EXTRACT', + 'JSON_EXTRACT_PATH', + 'JSON_EXTRACT_PATH_TEXT', + 'JSON_EXTRACT_STRING', + 'JSON_GROUP_ARRAY', + 'JSON_GROUP_OBJECT', + 'JSON_GROUP_STRUCTURE', + 'JSON_KEYS', + 'JSON_MERGE_PATCH', + 'JSON_OBJECT', + 'JSON_QUOTE', + 'JSON_SERIALIZE_PLAN', + 'JSON_SERIALIZE_SQL', + 'JSON_STRUCTURE', + 'JSON_TRANSFORM', + 'JSON_TRANSFORM_STRICT', + 'JSON_TYPE', + 'JSON_VALID', + 'JULIAN', + 'KAHAN_SUM', + 'KURTOSIS', + 'KURTOSIS_POP', + 'LAST', + 'LAST_DAY', + 'LCASE', + 'LCM', + 'LEAST', + 'LEAST_COMMON_MULTIPLE', + 'LEFT', + 'LEFT_GRAPHEME', + 'LEN', + 'LENGTH', + 'LENGTH_GRAPHEME', + 'LEVENSHTEIN', + 'LGAMMA', + 'LIKE_ESCAPE', + 'LIST', + 'LISTAGG', + 'LIST_AGGR', + 'LIST_AGGREGATE', + 'LIST_ANY_VALUE', + 'LIST_APPEND', + 'LIST_APPLY', + 'LIST_APPROX_COUNT_DISTINCT', + 'LIST_AVG', + 'LIST_BIT_AND', + 'LIST_BIT_OR', + 'LIST_BIT_XOR', + 'LIST_BOOL_AND', + 'LIST_BOOL_OR', + 'LIST_CAT', + 'LIST_CONCAT', + 'LIST_CONTAINS', + 'LIST_COSINE_SIMILARITY', + 'LIST_COUNT', + 'LIST_DISTANCE', + 'LIST_DISTINCT', + 'LIST_DOT_PRODUCT', + 'LIST_ELEMENT', + 'LIST_ENTROPY', + 'LIST_EXTRACT', + 'LIST_FILTER', + 'LIST_FIRST', + 'LIST_GRADE_UP', + 'LIST_HAS', + 'LIST_HAS_ALL', + 'LIST_HAS_ANY', + 'LIST_HISTOGRAM', + 'LIST_INDEXOF', + 'LIST_INNER_PRODUCT', + 'LIST_INTERSECT', + 'LIST_KURTOSIS', + 'LIST_KURTOSIS_POP', + 'LIST_LAST', + 'LIST_MAD', + 'LIST_MAX', + 'LIST_MEDIAN', + 'LIST_MIN', + 'LIST_MODE', + 'LIST_PACK', + 'LIST_POSITION', + 'LIST_PREPEND', + 'LIST_PRODUCT', + 'LIST_REDUCE', + 'LIST_RESIZE', + 'LIST_REVERSE', + 'LIST_REVERSE_SORT', + 'LIST_SELECT', + 'LIST_SEM', + 'LIST_SKEWNESS', + 'LIST_SLICE', + 'LIST_SORT', + 'LIST_STDDEV_POP', + 'LIST_STDDEV_SAMP', + 'LIST_STRING_AGG', + 'LIST_SUM', + 'LIST_TRANSFORM', + 'LIST_UNIQUE', + 'LIST_VALUE', + 'LIST_VAR_POP', + 'LIST_VAR_SAMP', + 'LIST_WHERE', + 'LIST_ZIP', + 'LN', + 'LOG', + 'LOG10', + 'LOG2', + 'LOWER', + 'LPAD', + 'LSMODE', + 'LTRIM', + 'MAD', + 'MAKE_DATE', + 'MAKE_TIME', + 'MAKE_TIMESTAMP', + 'MAKE_TIMESTAMPTZ', + 'MAP', + 'MAP_CONCAT', + 'MAP_ENTRIES', + 'MAP_EXTRACT', + 'MAP_FROM_ENTRIES', + 'MAP_KEYS', + 'MAP_VALUES', + 'MAX', + 'MAX_BY', + 'MD5', + 'MD5_NUMBER', + 'MD5_NUMBER_LOWER', + 'MD5_NUMBER_UPPER', + 'MEAN', + 'MEDIAN', + 'METADATA_INFO', + 'MICROSECOND', + 'MILLENNIUM', + 'MILLISECOND', + 'MIN', + 'MINUTE', + 'MIN_BY', + 'MISMATCHES', + 'MOD', + 'MODE', + 'MONTH', + 'MONTHNAME', + 'MULTIPLY', + 'NEXTAFTER', + 'NEXTVAL', + 'NFC_NORMALIZE', + 'NOT_ILIKE_ESCAPE', + 'NOT_LIKE_ESCAPE', + 'NOW', + 'NULLIF', + 'OBJ_DESCRIPTION', + 'OCTET_LENGTH', + 'ORD', + 'PARQUET_FILE_METADATA', + 'PARQUET_KV_METADATA', + 'PARQUET_METADATA', + 'PARQUET_SCAN', + 'PARQUET_SCHEMA', + 'PARSE_DIRNAME', + 'PARSE_DIRPATH', + 'PARSE_FILENAME', + 'PARSE_PATH', + 'PG_COLLATION_IS_VISIBLE', + 'PG_CONF_LOAD_TIME', + 'PG_CONVERSION_IS_VISIBLE', + 'PG_FUNCTION_IS_VISIBLE', + 'PG_GET_CONSTRAINTDEF', + 'PG_GET_EXPR', + 'PG_GET_VIEWDEF', + 'PG_HAS_ROLE', + 'PG_IS_OTHER_TEMP_SCHEMA', + 'PG_MY_TEMP_SCHEMA', + 'PG_OPCLASS_IS_VISIBLE', + 'PG_OPERATOR_IS_VISIBLE', + 'PG_OPFAMILY_IS_VISIBLE', + 'PG_POSTMASTER_START_TIME', + 'PG_SIZE_PRETTY', + 'PG_TABLE_IS_VISIBLE', + 'PG_TIMEZONE_NAMES', + 'PG_TS_CONFIG_IS_VISIBLE', + 'PG_TS_DICT_IS_VISIBLE', + 'PG_TS_PARSER_IS_VISIBLE', + 'PG_TS_TEMPLATE_IS_VISIBLE', + 'PG_TYPEOF', + 'PG_TYPE_IS_VISIBLE', + 'PI', + 'PLATFORM', + 'POSITION', + 'POW', + 'POWER', + 'PRAGMA_COLLATIONS', + 'PRAGMA_DATABASE_SIZE', + 'PRAGMA_METADATA_INFO', + 'PRAGMA_PLATFORM', + 'PRAGMA_SHOW', + 'PRAGMA_STORAGE_INFO', + 'PRAGMA_TABLE_INFO', + 'PRAGMA_USER_AGENT', + 'PRAGMA_VERSION', + 'PREFIX', + 'PRINTF', + 'PRODUCT', + 'QUANTILE', + 'QUANTILE_CONT', + 'QUANTILE_DISC', + 'QUARTER', + 'RADIANS', + 'RANDOM', + 'RANGE', + 'READFILE', + 'READ_BLOB', + 'READ_CSV', + 'READ_CSV_AUTO', + 'READ_JSON', + 'READ_JSON_AUTO', + 'READ_JSON_OBJECTS', + 'READ_JSON_OBJECTS_AUTO', + 'READ_NDJSON', + 'READ_NDJSON_AUTO', + 'READ_NDJSON_OBJECTS', + 'READ_PARQUET', + 'READ_TEXT', + 'REDUCE', + 'REGEXP_ESCAPE', + 'REGEXP_EXTRACT', + 'REGEXP_EXTRACT_ALL', + 'REGEXP_FULL_MATCH', + 'REGEXP_MATCHES', + 'REGEXP_REPLACE', + 'REGEXP_SPLIT_TO_ARRAY', + 'REGEXP_SPLIT_TO_TABLE', + 'REGR_AVGX', + 'REGR_AVGY', + 'REGR_COUNT', + 'REGR_INTERCEPT', + 'REGR_R2', + 'REGR_SLOPE', + 'REGR_SXX', + 'REGR_SXY', + 'REGR_SYY', + 'REPEAT', + 'REPEAT_ROW', + 'REPLACE', + 'RESERVOIR_QUANTILE', + 'REVERSE', + 'RIGHT', + 'RIGHT_GRAPHEME', + 'ROUND', + 'ROUNDBANKERS', + 'ROUND_EVEN', + 'ROW', + 'ROW_TO_JSON', + 'RPAD', + 'RTRIM', + 'SECOND', + 'SEM', + 'SEQ_SCAN', + 'SESSION_USER', + 'SETSEED', + 'SET_BIT', + 'SHA256', + 'SHA3', + 'SHELL_ADD_SCHEMA', + 'SHELL_ESCAPE_CRNL', + 'SHELL_IDQUOTE', + 'SHELL_MODULE_SCHEMA', + 'SHELL_PUTSNL', + 'SHOBJ_DESCRIPTION', + 'SHOW', + 'SHOW_DATABASES', + 'SHOW_TABLES', + 'SHOW_TABLES_EXPANDED', + 'SIGN', + 'SIGNBIT', + 'SIN', + 'SKEWNESS', + 'SNIFF_CSV', + 'SPLIT', + 'SPLIT_PART', + 'SQL_AUTO_COMPLETE', + 'SQRT', + 'STARTS_WITH', + 'STATS', + 'STDDEV', + 'STDDEV_POP', + 'STDDEV_SAMP', + 'STORAGE_INFO', + 'STRFTIME', + 'STRING_AGG', + 'STRING_SPLIT', + 'STRING_SPLIT_REGEX', + 'STRING_TO_ARRAY', + 'STRIP_ACCENTS', + 'STRLEN', + 'STRPOS', + 'STRPTIME', + 'STRUCT_EXTRACT', + 'STRUCT_INSERT', + 'STRUCT_PACK', + 'STR_SPLIT', + 'STR_SPLIT_REGEX', + 'SUBSTR', + 'SUBSTRING', + 'SUBSTRING_GRAPHEME', + 'SUBTRACT', + 'SUFFIX', + 'SUM', + 'SUMKAHAN', + 'SUMMARY', + 'SUM_NO_OVERFLOW', + 'TABLE_INFO', + 'TAN', + 'TEST_ALL_TYPES', + 'TEST_VECTOR_TYPES', + 'TIMEZONE', + 'TIMEZONE_HOUR', + 'TIMEZONE_MINUTE', + 'TIME_BUCKET', + 'TODAY', + 'TO_BASE', + 'TO_BASE64', + 'TO_BINARY', + 'TO_CENTURIES', + 'TO_DAYS', + 'TO_DECADES', + 'TO_HEX', + 'TO_HOURS', + 'TO_JSON', + 'TO_MICROSECONDS', + 'TO_MILLENNIA', + 'TO_MILLISECONDS', + 'TO_MINUTES', + 'TO_MONTHS', + 'TO_SECONDS', + 'TO_TIMESTAMP', + 'TO_WEEKS', + 'TO_YEARS', + 'TRANSACTION_TIMESTAMP', + 'TRANSLATE', + 'TRIM', + 'TRUNC', + 'TRY_STRPTIME', + 'TXID_CURRENT', + 'TYPEOF', + 'UCASE', + 'UNBIN', + 'UNHEX', + 'UNICODE', + 'UNION_EXTRACT', + 'UNION_TAG', + 'UNION_VALUE', + 'UNNEST', + 'UNPIVOT_LIST', + 'UPPER', + 'USER', + 'USER_AGENT', + 'UUID', + 'VARIANCE', + 'VAR_POP', + 'VAR_SAMP', + 'VECTOR_TYPE', + 'VERIFY_EXTERNAL', + 'VERIFY_FETCH_ROW', + 'VERIFY_PARALLELISM', + 'VERIFY_SERIALIZER', + 'VERSION', + 'WEEK', + 'WEEKDAY', + 'WEEKOFYEAR', + 'WHICH_SECRET', + 'WRITEFILE', + 'XOR', + 'YEAR', + 'YEARWEEK', + + // Keywords that also need to be listed as functions + 'CAST', + 'COALESCE', + 'NULL', + 'RANK', + 'ROW_NUMBER', +]; diff --git a/src/languages/duckdb/duckdb.keywords.ts b/src/languages/duckdb/duckdb.keywords.ts new file mode 100644 index 000000000..6f6faa520 --- /dev/null +++ b/src/languages/duckdb/duckdb.keywords.ts @@ -0,0 +1,164 @@ +export const keywords: string[] = [ + // Keywords from DuckDB: + // SELECT upper(keyword_name) + // FROM duckdb_keywords() + // WHERE keyword_category = 'reserved' + // ORDER BY keyword_name + 'ALL', + 'ANALYSE', + 'ANALYZE', + 'AND', + 'ANY', + 'AS', + 'ASC', + 'ATTACH', + 'ASYMMETRIC', + 'BOTH', + 'CASE', + 'CAST', + 'CHECK', + 'COLLATE', + 'COLUMN', + 'CONSTRAINT', + 'CREATE', + 'DEFAULT', + 'DEFERRABLE', + 'DESC', + 'DESCRIBE', + 'DETACH', + 'DISTINCT', + 'DO', + 'ELSE', + 'END', + 'EXCEPT', + 'FALSE', + 'FETCH', + 'FOR', + 'FOREIGN', + 'FROM', + 'GRANT', + 'GROUP', + 'HAVING', + 'IN', + 'INITIALLY', + 'INTERSECT', + 'INTO', + 'LATERAL', + 'LEADING', + 'LIMIT', + 'NOT', + 'NULL', + 'OFFSET', + 'ON', + 'ONLY', + 'OR', + 'ORDER', + 'PIVOT', + 'PIVOT_LONGER', + 'PIVOT_WIDER', + 'PLACING', + 'PRIMARY', + 'REFERENCES', + 'RETURNING', + 'SELECT', + 'SHOW', + 'SOME', + 'SUMMARIZE', + 'SYMMETRIC', + 'TABLE', + 'THEN', + 'TO', + 'TRAILING', + 'TRUE', + 'UNION', + 'UNIQUE', + 'UNPIVOT', + 'USING', + 'VARIADIC', + 'WHEN', + 'WHERE', + 'WINDOW', + 'WITH', +]; + +export const dataTypes: string[] = [ + // Types from DuckDB: + // SELECT DISTINCT upper(type_name) + // FROM duckdb_types() + // ORDER BY type_name + 'ARRAY', + 'BIGINT', + 'BINARY', + 'BIT', + 'BITSTRING', + 'BLOB', + 'BOOL', + 'BOOLEAN', + 'BPCHAR', + 'BYTEA', + 'CHAR', + 'DATE', + 'DATETIME', + 'DEC', + 'DECIMAL', + 'DOUBLE', + 'ENUM', + 'FLOAT', + 'FLOAT4', + 'FLOAT8', + 'GUID', + 'HUGEINT', + 'INET', + 'INT', + 'INT1', + 'INT128', + 'INT16', + 'INT2', + 'INT32', + 'INT4', + 'INT64', + 'INT8', + 'INTEGER', + 'INTEGRAL', + 'INTERVAL', + 'JSON', + 'LIST', + 'LOGICAL', + 'LONG', + 'MAP', + // 'NULL' is a keyword + 'NUMERIC', + 'NVARCHAR', + 'OID', + 'REAL', + 'ROW', + 'SHORT', + 'SIGNED', + 'SMALLINT', + 'STRING', + 'STRUCT', + 'TEXT', + 'TIME', + 'TIMESTAMP_MS', + 'TIMESTAMP_NS', + 'TIMESTAMP_S', + 'TIMESTAMP_US', + 'TIMESTAMP', + 'TIMESTAMPTZ', + 'TIMETZ', + 'TINYINT', + 'UBIGINT', + 'UHUGEINT', + 'UINT128', + 'UINT16', + 'UINT32', + 'UINT64', + 'UINT8', + 'UINTEGER', + 'UNION', + 'USMALLINT', + 'UTINYINT', + 'UUID', + 'VARBINARY', + 'VARCHAR', +]; diff --git a/src/sqlFormatter.ts b/src/sqlFormatter.ts index b35ac56e2..f89573c80 100644 --- a/src/sqlFormatter.ts +++ b/src/sqlFormatter.ts @@ -9,6 +9,7 @@ const dialectNameMap: Record { + expect(format('SELECT foo$, some$$ident')).toBe(dedent` + SELECT + foo$, + some$$ident + `); + }); + + it('formats type-cast operator without spaces', () => { + expect(format('SELECT 2 :: numeric AS foo;')).toBe(dedent` + SELECT + 2::numeric AS foo; + `); + }); + + // issue #144 (unsolved) + // This is currently far from ideal. + it('formats SELECT DISTINCT ON () syntax', () => { + expect(format('SELECT DISTINCT ON (c1, c2) c1, c2 FROM tbl;')).toBe(dedent` + SELECT DISTINCT + ON (c1, c2) c1, + c2 + FROM + tbl; + `); + }); + + it('formats ALTER TABLE ... ALTER COLUMN', () => { + expect( + format( + `ALTER TABLE t ALTER COLUMN foo SET DATA TYPE VARCHAR; + ALTER TABLE t ALTER COLUMN foo SET DEFAULT 5; + ALTER TABLE t ALTER COLUMN foo DROP DEFAULT; + ALTER TABLE t ALTER COLUMN foo SET NOT NULL; + ALTER TABLE t ALTER COLUMN foo DROP NOT NULL;` + ) + ).toBe(dedent` + ALTER TABLE t + ALTER COLUMN foo + SET DATA TYPE VARCHAR; + + ALTER TABLE t + ALTER COLUMN foo + SET DEFAULT 5; + + ALTER TABLE t + ALTER COLUMN foo + DROP DEFAULT; + + ALTER TABLE t + ALTER COLUMN foo + SET NOT NULL; + + ALTER TABLE t + ALTER COLUMN foo + DROP NOT NULL; + `); + }); + + // Issue #685 + it('allows TYPE to be used as an identifier', () => { + expect(format(`SELECT type, modified_at FROM items;`)).toBe(dedent` + SELECT + type, + modified_at + FROM + items; + `); + }); + + // Issue #156, #709 + it('does not recognize common fields names as keywords', () => { + expect( + format(`SELECT id, type, name, location, label, password FROM release;`, { + keywordCase: 'upper', + }) + ).toBe(dedent` + SELECT + id, + type, + name, + location, + label, + password + FROM + release; + `); + }); + + it('formats DEFAULT VALUES clause', () => { + expect( + format(`INSERT INTO items default values RETURNING id;`, { + keywordCase: 'upper', + }) + ).toBe(dedent` + INSERT INTO + items + DEFAULT VALUES + RETURNING + id; + `); + }); + + // Issue #726 + it('treats TEXT as data-type (not as plain keyword)', () => { + expect( + format(`CREATE TABLE foo (items text);`, { + dataTypeCase: 'upper', + }) + ).toBe(dedent` + CREATE TABLE foo (items TEXT); + `); + + expect( + format(`CREATE TABLE foo (text VARCHAR(100));`, { + keywordCase: 'upper', + }) + ).toBe(dedent` + CREATE TABLE foo (text VARCHAR(100)); + `); + }); +} diff --git a/test/duckdb.test.ts b/test/duckdb.test.ts new file mode 100644 index 000000000..0f5c46c8c --- /dev/null +++ b/test/duckdb.test.ts @@ -0,0 +1,203 @@ +import dedent from 'dedent-js'; + +import { format as originalFormat, FormatFn } from '../src/sqlFormatter.js'; + +import behavesLikePostgresqlFormatter from './behavesLikePostgresqlFormatter.js'; +import supportsCreateTable from './features/createTable.js'; +import supportsDropTable from './features/dropTable.js'; +import supportsJoin from './features/join.js'; +import supportsOperators from './features/operators.js'; +import supportsStrings from './features/strings.js'; +import supportsIdentifiers from './features/identifiers.js'; +import supportsParams from './options/param.js'; +import supportsSetOperations from './features/setOperations.js'; +import supportsLimiting from './features/limiting.js'; +import supportsUpdate from './features/update.js'; +import supportsCreateView from './features/createView.js'; +import supportsArrayLiterals from './features/arrayLiterals.js'; +import supportsTruncateTable from './features/truncateTable.js'; + +describe('DuckDBFormatter', () => { + const language = 'duckdb'; + const format: FormatFn = (query, cfg = {}) => originalFormat(query, { ...cfg, language }); + + behavesLikePostgresqlFormatter(format); + supportsCreateView(format, { orReplace: true, ifNotExists: true }); + supportsCreateTable(format, { orReplace: true, ifNotExists: true }); + supportsDropTable(format, { ifExists: true }); + supportsArrayLiterals(format, { withoutArrayPrefix: true }); + supportsUpdate(format); + supportsTruncateTable(format, { withTable: false, withoutTable: true }); + supportsStrings(format, ["''-qq", "X''", "B''", "E''", '$$']); + supportsIdentifiers(format, [`""-qq`]); + // Missing: '::' type cast (tested separately) + supportsOperators( + format, + [ + // Arithmetic: + '//', + '%', + '**', + '^', + '!', + // Bitwise: + '&', + '|', + '~', + '<<', + '>>', + // Comparison: + '==', + // Lambda: + '->', + // Named function params: + ':=', + '=>', + // Pattern matching: + '~~', + '!~~', + '~~*', + '!~~*', + '~~~', + // Regular expressions: + '~', + '!~', + '~*', + '!~*', + // String: + '^@', + '||', + // INET extension: + '>>=', + '<<=', + ], + { logicalOperators: ['AND', 'OR', 'XOR'], any: true } + ); + supportsJoin(format, { + additionally: [ + 'ASOF JOIN', + 'ASOF INNER JOIN', + 'ASOF LEFT JOIN', + 'ASOF LEFT OUTER JOIN', + 'ASOF RIGHT JOIN', + 'ASOF RIGHT OUTER JOIN', + 'ASOF FULL JOIN', + 'ASOF FULL OUTER JOIN', + 'POSITIONAL JOIN', + 'SEMI JOIN', + 'ANTI JOIN', + ], + }); + supportsSetOperations(format, [ + 'UNION', + 'UNION ALL', + 'UNION BY NAME', + 'EXCEPT', + 'EXCEPT ALL', + 'INTERSECT', + 'INTERSECT ALL', + ]); + // TODO: named params $foo currently conflict with $$-quoted strings + supportsParams(format, { positional: true, numbered: ['$'], quoted: ['$""'] }); + supportsLimiting(format, { limit: true, offset: true }); + + it('formats prefix aliases', () => { + expect(format("SELECT foo:10, bar:'hello';")).toBe(dedent` + SELECT + foo: 10, + bar: 'hello'; + `); + }); + + it('formats {} struct literal (string keys)', () => { + expect(format("SELECT {'id':1,'type':'Tarzan'} AS obj;")).toBe(dedent` + SELECT + {'id': 1, 'type': 'Tarzan'} AS obj; + `); + }); + + it('formats {} struct literal (identifier keys)', () => { + expect(format("SELECT {id:1,type:'Tarzan'} AS obj;")).toBe(dedent` + SELECT + {id: 1, type: 'Tarzan'} AS obj; + `); + }); + + it('formats {} struct literal (quoted identifier keys)', () => { + expect(format(`SELECT {"id":1,"type":'Tarzan'} AS obj;`)).toBe(dedent` + SELECT + {"id": 1, "type": 'Tarzan'} AS obj; + `); + }); + + it('formats large struct and list literals', () => { + const result = format(` + INSERT INTO heroes (KEY, VALUE) VALUES ('123', {'id': 1, 'type': 'Tarzan', + 'array': [123456789, 123456789, 123456789, 123456789, 123456789], 'hello': 'world'}); + `); + expect(result).toBe(dedent` + INSERT INTO + heroes (KEY, VALUE) + VALUES + ( + '123', + { + 'id': 1, + 'type': 'Tarzan', + 'array': [ + 123456789, + 123456789, + 123456789, + 123456789, + 123456789 + ], + 'hello': 'world' + } + ); + `); + }); + + // TODO: This currently conflicts with ":"-operator in struct literals + it.skip('supports array slice operator', () => { + expect(format('SELECT foo[:5], bar[1:], baz[1:5], zap[:];')).toBe(dedent` + SELECT + foo[:5], + bar[1:], + baz[1:5], + zap[:]; + `); + }); + + // TODO: This currently conflicts with the modulo operator + it.skip('formats percentage value in LIMIT clause', () => { + expect(format('SELECT * FROM foo LIMIT 10%;')).toBe(dedent` + SELECT + * + FROM + foo + LIMIT + 10%; + `); + }); + + it('formats TIMESTAMP WITH TIME ZONE syntax', () => { + expect( + format(` + CREATE TABLE time_table (id INT PRIMARY KEY NOT NULL, + created_at TIMESTAMP WITH TIME ZONE);`) + ).toBe(dedent` + CREATE TABLE time_table ( + id INT PRIMARY KEY NOT NULL, + created_at TIMESTAMP WITH TIME ZONE + ); + `); + }); + + it('formats JSON data type', () => { + expect( + format(`CREATE TABLE foo (bar json, baz json);`, { + dataTypeCase: 'upper', + }) + ).toBe('CREATE TABLE foo (bar JSON, baz JSON);'); + }); +}); diff --git a/test/features/strings.ts b/test/features/strings.ts index ee27c489a..4f3ae4a2a 100644 --- a/test/features/strings.ts +++ b/test/features/strings.ts @@ -17,7 +17,9 @@ type StringType = | "B''" // no escaping | 'B""' // no escaping | "R''" // no escaping - | 'R""'; // no escaping + | 'R""' // no escaping + | "E''" // with backslash escaping + | '$$'; // no escaping export default function supportsStrings(format: FormatFn, stringTypes: StringType[]) { if (stringTypes.includes('""-qq') || stringTypes.includes('""-bs')) { @@ -238,4 +240,42 @@ export default function supportsStrings(format: FormatFn, stringTypes: StringTyp expect(format(`r"a ha"r"hm mm"`)).toBe(`r"a ha" r"hm mm"`); }); } + + if (stringTypes.includes("E''")) { + it("supports E'' strings with C-style escapes", () => { + expect(format("E'blah blah'")).toBe("E'blah blah'"); + expect(format("E'some \\' FROM escapes'")).toBe("E'some \\' FROM escapes'"); + expect(format("SELECT E'blah' FROM foo")).toBe(dedent` + SELECT + E'blah' + FROM + foo + `); + expect(format("E'blah''blah'")).toBe("E'blah''blah'"); + }); + + it(`detects consecutive E'' strings as separate ones`, () => { + expect(format(`e'a ha'e'hm mm'`)).toBe(`e'a ha' e'hm mm'`); + }); + } + + if (stringTypes.includes('$$')) { + it('supports dollar-quoted strings', () => { + expect(format('$$foo JOIN bar$$')).toBe('$$foo JOIN bar$$'); + expect(format('$$foo $ JOIN bar$$')).toBe('$$foo $ JOIN bar$$'); + expect(format('$$foo \n bar$$')).toBe('$$foo \n bar$$'); + expect(format('SELECT $$where$$ FROM $$update$$')).toBe(dedent` + SELECT + $$where$$ + FROM + $$update$$ + `); + }); + + it('supports tagged dollar-quoted strings', () => { + expect(format('$xxx$foo $$ LEFT JOIN $yyy$ bar$xxx$')).toBe( + '$xxx$foo $$ LEFT JOIN $yyy$ bar$xxx$' + ); + }); + } } diff --git a/test/features/truncateTable.ts b/test/features/truncateTable.ts index 6aea06387..325fbf1b9 100644 --- a/test/features/truncateTable.ts +++ b/test/features/truncateTable.ts @@ -3,19 +3,22 @@ import dedent from 'dedent-js'; import { FormatFn } from '../../src/sqlFormatter.js'; interface TruncateTableConfig { + withTable?: boolean; withoutTable?: boolean; } export default function supportsTruncateTable( format: FormatFn, - { withoutTable }: TruncateTableConfig = {} + { withTable = true, withoutTable }: TruncateTableConfig = {} ) { - it('formats TRUNCATE TABLE statement', () => { - const result = format('TRUNCATE TABLE Customers;'); - expect(result).toBe(dedent` - TRUNCATE TABLE Customers; - `); - }); + if (withTable) { + it('formats TRUNCATE TABLE statement', () => { + const result = format('TRUNCATE TABLE Customers;'); + expect(result).toBe(dedent` + TRUNCATE TABLE Customers; + `); + }); + } if (withoutTable) { it('formats TRUNCATE statement (without TABLE)', () => { diff --git a/test/options/param.ts b/test/options/param.ts index 7eab62393..1f8ecb48b 100644 --- a/test/options/param.ts +++ b/test/options/param.ts @@ -6,7 +6,7 @@ interface ParamsTypes { positional?: boolean; numbered?: ('?' | '$' | ':')[]; named?: (':' | '$' | '@')[]; - quoted?: ('@""' | '@[]' | '@``')[]; + quoted?: ('$""' | '@""' | '@[]' | '@``')[]; } export default function supportsParams(format: FormatFn, params: ParamsTypes) { @@ -198,6 +198,28 @@ export default function supportsParams(format: FormatFn, params: ParamsTypes) { }); } + if (params.quoted?.includes('$""')) { + it(`recognizes $"name" placeholders`, () => { + expect(format(`SELECT $"foo", $"foo bar";`)).toBe(dedent` + SELECT + $"foo", + $"foo bar"; + `); + }); + + it(`replaces $"name" placeholders with param values`, () => { + expect( + format(`WHERE name = $"name" AND age > $"current age";`, { + params: { 'name': "'John'", 'current age': '10' }, + }) + ).toBe(dedent` + WHERE + name = 'John' + AND age > 10; + `); + }); + } + if (params.quoted?.includes('@""')) { it(`recognizes @"name" placeholders`, () => { expect(format(`SELECT @"foo", @"foo bar";`)).toBe(dedent` diff --git a/test/postgresql.test.ts b/test/postgresql.test.ts index c725b57bc..2d02ff1cb 100644 --- a/test/postgresql.test.ts +++ b/test/postgresql.test.ts @@ -2,62 +2,39 @@ import dedent from 'dedent-js'; import { format as originalFormat, FormatFn } from '../src/sqlFormatter.js'; -import behavesLikeSqlFormatter from './behavesLikeSqlFormatter.js'; -import supportsAlterTable from './features/alterTable.js'; -import supportsBetween from './features/between.js'; import supportsCreateTable from './features/createTable.js'; import supportsDropTable from './features/dropTable.js'; import supportsJoin from './features/join.js'; import supportsOperators from './features/operators.js'; import supportsSchema from './features/schema.js'; import supportsStrings from './features/strings.js'; -import supportsReturning from './features/returning.js'; import supportsConstraints from './features/constraints.js'; -import supportsDeleteFrom from './features/deleteFrom.js'; -import supportsComments from './features/comments.js'; -import supportsCommentOn from './features/commentOn.js'; import supportsIdentifiers from './features/identifiers.js'; import supportsParams from './options/param.js'; -import supportsArrayAndMapAccessors from './features/arrayAndMapAccessors.js'; -import supportsWindow from './features/window.js'; import supportsSetOperations from './features/setOperations.js'; import supportsLimiting from './features/limiting.js'; -import supportsInsertInto from './features/insertInto.js'; import supportsUpdate from './features/update.js'; import supportsTruncateTable from './features/truncateTable.js'; import supportsCreateView from './features/createView.js'; -import supportsOnConflict from './features/onConflict.js'; import supportsIsDistinctFrom from './features/isDistinctFrom.js'; import supportsArrayLiterals from './features/arrayLiterals.js'; import supportsDataTypeCase from './options/dataTypeCase.js'; +import behavesLikePostgresqlFormatter from './behavesLikePostgresqlFormatter.js'; describe('PostgreSqlFormatter', () => { const language = 'postgresql'; const format: FormatFn = (query, cfg = {}) => originalFormat(query, { ...cfg, language }); - behavesLikeSqlFormatter(format); - supportsComments(format, { nestedBlockComments: true }); - supportsCommentOn(format); + behavesLikePostgresqlFormatter(format); supportsCreateView(format, { orReplace: true, materialized: true, ifNotExists: true }); supportsCreateTable(format, { ifNotExists: true }); supportsDropTable(format, { ifExists: true }); supportsConstraints(format, ['NO ACTION', 'RESTRICT', 'CASCADE', 'SET NULL', 'SET DEFAULT']); supportsArrayLiterals(format, { withArrayPrefix: true }); - supportsArrayAndMapAccessors(format); - supportsAlterTable(format, { - addColumn: true, - dropColumn: true, - renameTo: true, - renameColumn: true, - }); - supportsDeleteFrom(format); - supportsInsertInto(format); - supportsOnConflict(format); supportsUpdate(format, { whereCurrentOf: true }); supportsTruncateTable(format, { withoutTable: true }); - supportsStrings(format, ["''-qq", "U&''", "X''", "B''"]); + supportsStrings(format, ["''-qq", "U&''", "X''", "B''", "E''", '$$']); supportsIdentifiers(format, [`""-qq`, 'U&""']); - supportsBetween(format); supportsSchema(format); // Missing: '::' type cast (tested separately) supportsOperators( @@ -158,64 +135,18 @@ describe('PostgreSqlFormatter', () => { supportsIsDistinctFrom(format); supportsJoin(format); supportsSetOperations(format); - supportsReturning(format); supportsParams(format, { numbered: ['$'] }); - supportsWindow(format); supportsLimiting(format, { limit: true, offset: true, fetchFirst: true, fetchNext: true }); supportsDataTypeCase(format); - it('allows $ character as part of identifiers', () => { - expect(format('SELECT foo$, some$$ident')).toBe(dedent` - SELECT - foo$, - some$$ident - `); - }); - - // Postgres-specific string types - it("supports E'' strings with C-style escapes", () => { - expect(format("E'blah blah'")).toBe("E'blah blah'"); - expect(format("E'some \\' FROM escapes'")).toBe("E'some \\' FROM escapes'"); - expect(format("SELECT E'blah' FROM foo")).toBe(dedent` - SELECT - E'blah' - FROM - foo - `); - expect(format("E'blah''blah'")).toBe("E'blah''blah'"); - }); - - it('supports dollar-quoted strings', () => { - expect(format('$xxx$foo $$ LEFT JOIN $yyy$ bar$xxx$')).toBe( - '$xxx$foo $$ LEFT JOIN $yyy$ bar$xxx$' - ); - expect(format('$$foo JOIN bar$$')).toBe('$$foo JOIN bar$$'); - expect(format('$$foo $ JOIN bar$$')).toBe('$$foo $ JOIN bar$$'); - expect(format('$$foo \n bar$$')).toBe('$$foo \n bar$$'); - expect(format('SELECT $$where$$ FROM $$update$$')).toBe(dedent` - SELECT - $$where$$ - FROM - $$update$$ - `); - }); - - it('formats type-cast operator without spaces', () => { - expect(format('SELECT 2 :: numeric AS foo;')).toBe(dedent` + // Regression test for issue #624 + it('supports array slice operator', () => { + expect(format('SELECT foo[:5], bar[1:], baz[1:5], zap[:];')).toBe(dedent` SELECT - 2::numeric AS foo; - `); - }); - - // issue #144 (unsolved) - // This is currently far from ideal. - it('formats SELECT DISTINCT ON () syntax', () => { - expect(format('SELECT DISTINCT ON (c1, c2) c1, c2 FROM tbl;')).toBe(dedent` - SELECT DISTINCT - ON (c1, c2) c1, - c2 - FROM - tbl; + foo[:5], + bar[1:], + baz[1:5], + zap[:]; `); }); @@ -244,49 +175,6 @@ describe('PostgreSqlFormatter', () => { `); }); - // Regression test for issue #624 - it('supports array slice operator', () => { - expect(format('SELECT foo[:5], bar[1:], baz[1:5], zap[:];')).toBe(dedent` - SELECT - foo[:5], - bar[1:], - baz[1:5], - zap[:]; - `); - }); - - it('formats ALTER TABLE ... ALTER COLUMN', () => { - expect( - format( - `ALTER TABLE t ALTER COLUMN foo SET DATA TYPE VARCHAR; - ALTER TABLE t ALTER COLUMN foo SET DEFAULT 5; - ALTER TABLE t ALTER COLUMN foo DROP DEFAULT; - ALTER TABLE t ALTER COLUMN foo SET NOT NULL; - ALTER TABLE t ALTER COLUMN foo DROP NOT NULL;` - ) - ).toBe(dedent` - ALTER TABLE t - ALTER COLUMN foo - SET DATA TYPE VARCHAR; - - ALTER TABLE t - ALTER COLUMN foo - SET DEFAULT 5; - - ALTER TABLE t - ALTER COLUMN foo - DROP DEFAULT; - - ALTER TABLE t - ALTER COLUMN foo - SET NOT NULL; - - ALTER TABLE t - ALTER COLUMN foo - DROP NOT NULL; - `); - }); - it('formats FOR UPDATE clause', () => { expect( format(` @@ -309,69 +197,6 @@ describe('PostgreSqlFormatter', () => { `); }); - // Issue #685 - it('allows TYPE to be used as an identifier', () => { - expect(format(`SELECT type, modified_at FROM items;`)).toBe(dedent` - SELECT - type, - modified_at - FROM - items; - `); - }); - - // Issue #156, #709 - it('does not recognize common fields names as keywords', () => { - expect( - format(`SELECT id, type, name, location, label, password FROM release;`, { - keywordCase: 'upper', - }) - ).toBe(dedent` - SELECT - id, - type, - name, - location, - label, - password - FROM - release; - `); - }); - - it('formats DEFAULT VALUES clause', () => { - expect( - format(`INSERT INTO items default values RETURNING id;`, { - keywordCase: 'upper', - }) - ).toBe(dedent` - INSERT INTO - items - DEFAULT VALUES - RETURNING - id; - `); - }); - - // Issue #726 - it('treats TEXT as data-type (not as plain keyword)', () => { - expect( - format(`CREATE TABLE foo (items text);`, { - dataTypeCase: 'upper', - }) - ).toBe(dedent` - CREATE TABLE foo (items TEXT); - `); - - expect( - format(`CREATE TABLE foo (text VARCHAR(100));`, { - keywordCase: 'upper', - }) - ).toBe(dedent` - CREATE TABLE foo (text VARCHAR(100)); - `); - }); - // Issue #711 it('supports OPERATOR() syntax', () => { expect(format(`SELECT foo OPERATOR(public.===) bar;`)).toBe(dedent`