├── .babelrc ├── .codesandbox └── ci.json ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── formatting-bug-report.md │ ├── new-feature.md │ ├── script-bug-report.md │ ├── vscode-prettier-sql.yml │ └── vscode-sql-formatter.yml └── workflows │ ├── coveralls.yaml │ └── webpack.yaml ├── .gitignore ├── .pre-commit-hooks.yaml ├── .prettierignore ├── .prettierrc.json ├── .release-it.json ├── AUTHORS ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin └── sql-formatter-cli.cjs ├── dependabot.yml ├── docs ├── dataTypeCase.md ├── denseOperators.md ├── dialect.md ├── expressionWidth.md ├── functionCase.md ├── identifierCase.md ├── indentStyle.md ├── keywordCase.md ├── language.md ├── linesBetweenQueries.md ├── logicalOperatorNewline.md ├── newlineBeforeSemicolon.md ├── paramTypes.md ├── params.md ├── tabWidth.md └── useTabs.md ├── package.json ├── src ├── FormatOptions.ts ├── allDialects.ts ├── dialect.ts ├── expandPhrases.ts ├── formatter │ ├── ExpressionFormatter.ts │ ├── Formatter.ts │ ├── Indentation.ts │ ├── InlineLayout.ts │ ├── Layout.ts │ ├── Params.ts │ ├── config.ts │ └── tabularStyle.ts ├── index.ts ├── languages │ ├── bigquery │ │ ├── bigquery.formatter.ts │ │ ├── bigquery.functions.ts │ │ └── bigquery.keywords.ts │ ├── db2 │ │ ├── db2.formatter.ts │ │ ├── db2.functions.ts │ │ └── db2.keywords.ts │ ├── db2i │ │ ├── db2i.formatter.ts │ │ ├── db2i.functions.ts │ │ └── db2i.keywords.ts │ ├── duckdb │ │ ├── duckdb.formatter.ts │ │ ├── duckdb.functions.ts │ │ └── duckdb.keywords.ts │ ├── hive │ │ ├── hive.formatter.ts │ │ ├── hive.functions.ts │ │ └── hive.keywords.ts │ ├── mariadb │ │ ├── likeMariaDb.ts │ │ ├── mariadb.formatter.ts │ │ ├── mariadb.functions.ts │ │ └── mariadb.keywords.ts │ ├── mysql │ │ ├── mysql.formatter.ts │ │ ├── mysql.functions.ts │ │ └── mysql.keywords.ts │ ├── n1ql │ │ ├── n1ql.formatter.ts │ │ ├── n1ql.functions.ts │ │ └── n1ql.keywords.ts │ ├── plsql │ │ ├── plsql.formatter.ts │ │ ├── plsql.functions.ts │ │ └── plsql.keywords.ts │ ├── postgresql │ │ ├── postgresql.formatter.ts │ │ ├── postgresql.functions.ts │ │ └── postgresql.keywords.ts │ ├── redshift │ │ ├── redshift.formatter.ts │ │ ├── redshift.functions.ts │ │ └── redshift.keywords.ts │ ├── singlestoredb │ │ ├── singlestoredb.formatter.ts │ │ ├── singlestoredb.functions.ts │ │ └── singlestoredb.keywords.ts │ ├── snowflake │ │ ├── snowflake.formatter.ts │ │ ├── snowflake.functions.ts │ │ └── snowflake.keywords.ts │ ├── spark │ │ ├── spark.formatter.ts │ │ ├── spark.functions.ts │ │ └── spark.keywords.ts │ ├── sql │ │ ├── sql.formatter.ts │ │ ├── sql.functions.ts │ │ └── sql.keywords.ts │ ├── sqlite │ │ ├── sqlite.formatter.ts │ │ ├── sqlite.functions.ts │ │ └── sqlite.keywords.ts │ ├── tidb │ │ ├── tidb.formatter.ts │ │ ├── tidb.functions.ts │ │ └── tidb.keywords.ts │ ├── transactsql │ │ ├── transactsql.formatter.ts │ │ ├── transactsql.functions.ts │ │ └── transactsql.keywords.ts │ └── trino │ │ ├── trino.formatter.ts │ │ ├── trino.functions.ts │ │ └── trino.keywords.ts ├── lexer │ ├── NestedComment.ts │ ├── Tokenizer.ts │ ├── TokenizerEngine.ts │ ├── TokenizerOptions.ts │ ├── disambiguateTokens.ts │ ├── lineColFromIndex.ts │ ├── regexFactory.ts │ ├── regexUtil.ts │ └── token.ts ├── parser │ ├── LexerAdapter.ts │ ├── ast.ts │ ├── createParser.ts │ └── grammar.ne ├── sqlFormatter.ts ├── utils.ts └── validateConfig.ts ├── static ├── index.css ├── index.html ├── index.js └── sql-formatter-icon.png ├── test ├── behavesLikeDb2Formatter.ts ├── behavesLikeMariaDbFormatter.ts ├── behavesLikePostgresqlFormatter.ts ├── behavesLikeSqlFormatter.ts ├── bigquery.test.ts ├── db2.test.ts ├── db2i.test.ts ├── duckdb.test.ts ├── features │ ├── alterTable.ts │ ├── arrayAndMapAccessors.ts │ ├── arrayLiterals.ts │ ├── between.ts │ ├── case.ts │ ├── commentOn.ts │ ├── comments.ts │ ├── constraints.ts │ ├── createTable.ts │ ├── createView.ts │ ├── deleteFrom.ts │ ├── disableComment.ts │ ├── dropTable.ts │ ├── identifiers.ts │ ├── insertInto.ts │ ├── isDistinctFrom.ts │ ├── join.ts │ ├── limiting.ts │ ├── mergeInto.ts │ ├── numbers.ts │ ├── onConflict.ts │ ├── operators.ts │ ├── returning.ts │ ├── schema.ts │ ├── setOperations.ts │ ├── strings.ts │ ├── truncateTable.ts │ ├── update.ts │ ├── window.ts │ ├── windowFunctions.ts │ └── with.ts ├── hive.test.ts ├── mariadb.test.ts ├── mysql.test.ts ├── n1ql.test.ts ├── options │ ├── dataTypeCase.ts │ ├── expressionWidth.ts │ ├── functionCase.ts │ ├── identifierCase.ts │ ├── indentStyle.ts │ ├── keywordCase.ts │ ├── linesBetweenQueries.ts │ ├── logicalOperatorNewline.ts │ ├── newlineBeforeSemicolon.ts │ ├── param.ts │ ├── paramTypes.ts │ ├── tabWidth.ts │ └── useTabs.ts ├── perf │ └── perf-test.js ├── perftest.ts ├── plsql.test.ts ├── postgresql.test.ts ├── redshift.test.ts ├── singlestoredb.test.ts ├── snowflake.test.ts ├── spark.test.ts ├── sql.test.ts ├── sqlFormatter.test.ts ├── sqlite.test.ts ├── tidb.test.ts ├── transactsql.test.ts ├── trino.test.ts └── unit │ ├── Layout.test.ts │ ├── NestedComment.test.ts │ ├── Parser.test.ts │ ├── Tokenizer.test.ts │ ├── __snapshots__ │ ├── Parser.test.ts.snap │ └── Tokenizer.test.ts.snap │ ├── expandPhrases.test.ts │ └── tabularStyle.test.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── tsconfig.json ├── webpack.common.js ├── webpack.prod.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/preset-env", { "modules": false }], "@babel/preset-typescript"], 3 | "plugins": ["add-module-exports", ["babel-plugin-inline-import", { "extensions": [".sql"] }]], 4 | "targets": { 5 | "node": 14 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "18" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /lib 3 | /dist 4 | /coverage 5 | webpack.*.js 6 | /src/parser/grammar.ts 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { "project": "./tsconfig.json", "ecmaVersion": 6, "sourceType": "module" }, 4 | "extends": ["airbnb-base", "airbnb-typescript/base", "plugin:import/typescript", "prettier"], 5 | "plugins": ["@typescript-eslint", "prettier"], 6 | "globals": { 7 | "document": true, 8 | "sqlFormatter": true 9 | }, 10 | "rules": { 11 | "class-methods-use-this": "off", 12 | "consistent-return": "off", 13 | "curly": ["error", "all"], 14 | "eqeqeq": "error", 15 | "func-names": "error", 16 | "import/no-extraneous-dependencies": [ 17 | "error", 18 | { 19 | "devDependencies": ["test/**", "**/*.test.js", "**/*.test.ts"], 20 | "packageDir": ["./"] 21 | } 22 | ], 23 | "no-continue": "off", 24 | "no-param-reassign": "off", 25 | "no-plusplus": "off", 26 | "no-else-return": "off", 27 | "no-use-before-define": "off", 28 | "no-useless-concat": "off", 29 | "no-restricted-syntax": "off", 30 | "no-constant-condition": "off", 31 | "prefer-template": "off", 32 | "default-case": "off", 33 | "import/prefer-default-export": "off", 34 | "import/extensions": ["error", "always"], 35 | "prettier/prettier": ["error"], 36 | "@typescript-eslint/no-use-before-define": "off", 37 | "@typescript-eslint/comma-dangle": "off", 38 | "@typescript-eslint/indent": "off", 39 | "@typescript-eslint/lines-between-class-members": "off", 40 | "@typescript-eslint/naming-convention": "error", 41 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 42 | "@typescript-eslint/quotes": [ 43 | "error", 44 | "single", 45 | { "avoidEscape": true, "allowTemplateLiterals": true } 46 | ], 47 | "@typescript-eslint/semi": "error", 48 | "@typescript-eslint/no-non-null-assertion": "error", 49 | "@typescript-eslint/consistent-type-exports": "error" 50 | }, 51 | "settings": { 52 | "import/resolver": { 53 | "node": { 54 | "extensions": [".js", ".ts"] 55 | } 56 | } 57 | }, 58 | "env": { 59 | "jest": true 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/formatting-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Formatting Bug Report 3 | about: Raise an issue about the FORMATTING output 4 | title: '[FORMATTING] Issue Title Here' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Input data** 10 | 11 | Which SQL and options did you provide as input? 12 | 13 | ```sql 14 | -- ADD SQL HERE 15 | ``` 16 | 17 | **Expected Output** 18 | 19 | ```sql 20 | -- ADD SQL HERE 21 | ``` 22 | 23 | **Actual Output** 24 | 25 | ```sql 26 | -- ADD SQL HERE 27 | ``` 28 | 29 | **Usage** 30 | 31 | - How are you calling / using the library? 32 | - What SQL language(s) does this apply to? 33 | - Which SQL Formatter version are you using? 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Suggest Improvement/Feature 3 | about: Suggest new features or improvements to existing ones 4 | title: 'Feature Request: Title Here' 5 | labels: feature 6 | assignees: '' 7 | --- 8 | 9 | **Describe the Feature** 10 | A clear and concise description of what the feature is. 11 | 12 | **Why do you want this feature?** 13 | Your reasoning behind suggesting this feature. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/script-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Generic Bug Report 3 | about: Report any other problem with SQL formatter (crash, vulnerability, etc) 4 | title: 'Issue Title Here' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | List steps needed to reproduce the problem. 11 | 12 | **Expected behavior** 13 | What did you expect to happen? 14 | 15 | **Actual behavior** 16 | What happened instead? 17 | 18 | **Usage** 19 | 20 | - How are you calling / using the library? 21 | - What SQL language(s) does this apply to? 22 | - Which SQL Formatter version are you using? 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/vscode-prettier-sql.yml: -------------------------------------------------------------------------------- 1 | name: VSCode Prettier SQL extension bug report 2 | description: I have a problem with the Prettier SQL VSCode extension. 3 | title: 'Problem with outdated Prettier SQL extension' 4 | labels: ['vscode'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | **Please don't report issues about the [Prettier SQL VSCode](https://marketplace.visualstudio.com/items?itemName=inferrinizzard.prettier-sql-vscode) extension here. It is no more maintained by its author.** 10 | 11 | Instead, consider switching to the official [SQL Formatter VSCode](https://marketplace.visualstudio.com/items?itemName=ReneSaarsoo.sql-formatter-vsc) extension, which uses the latest SQL Formatter library and is actively maintained. 12 | - type: textarea 13 | id: problem 14 | attributes: 15 | label: Tell us why you regardless of the above still want to report an issue 16 | validations: 17 | required: true 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/vscode-sql-formatter.yml: -------------------------------------------------------------------------------- 1 | name: VSCode SQL Formatter extension bug report 2 | description: I have a problem with the SQL Formatter VSCode extension 3 | title: '[VSCode]:' 4 | labels: vscode 5 | body: 6 | - type: textarea 7 | id: problem 8 | attributes: 9 | label: Describe your problem 10 | validations: 11 | required: true 12 | - type: dropdown 13 | id: dialect 14 | attributes: 15 | label: Which SQL dialect does this apply to? 16 | multiple: true 17 | options: 18 | - Google BigQuery 19 | - IBM DB2 for LUW (Linux, Unix, Windows) 20 | - IBM DB2 for iSystem 21 | - Apache Hive 22 | - MariaDB 23 | - MySQL 24 | - Couchbase N1QL 25 | - Oracle PL/SQL 26 | - PostgreSQL 27 | - Amazon Redshift 28 | - SingleStoreDB 29 | - Snowflake 30 | - Spark 31 | - SQLite 32 | - TiDB 33 | - Trino 34 | - Presto 35 | - Microsoft SQL Server Transact-SQL 36 | - A dialect not supported by SQL Formatter 37 | validations: 38 | required: true 39 | - type: dropdown 40 | id: configured_dialect 41 | attributes: 42 | label: Which SQL dialect is configured in your VSCode extension settings? 43 | description: Go to Settings -> SQL Formatter VSCode -> dialect. 44 | options: 45 | - Rely on VSCode to detect the relevant SQL dialect 46 | - Google BigQuery 47 | - IBM DB2 for LUW (Linux, Unix, Windows) 48 | - IBM DB2 for iSystem 49 | - Apache Hive 50 | - MariaDB 51 | - MySQL 52 | - Couchbase N1QL 53 | - Oracle PL/SQL 54 | - PostgreSQL 55 | - Amazon Redshift 56 | - SingleStoreDB 57 | - Snowflake 58 | - Spark 59 | - SQLite 60 | - TiDB 61 | - Trino (should also work for Presto) 62 | - Microsoft SQL Server Transact-SQL 63 | - Basic SQL - generally not recommended 64 | validations: 65 | required: true 66 | - type: input 67 | id: version 68 | attributes: 69 | label: Version of the VSCode extension 70 | validations: 71 | required: true 72 | - type: checkboxes 73 | id: repoduces 74 | attributes: 75 | label: I have tried to reproduce this issue on the [demo page](https://sql-formatter-org.github.io/sql-formatter/), and it 76 | options: 77 | - label: Reproduces 78 | - label: Does not reproduce 79 | -------------------------------------------------------------------------------- /.github/workflows/coveralls.yaml: -------------------------------------------------------------------------------- 1 | name: coveralls 2 | 3 | on: 4 | push: 5 | branches: [master, develop] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [18.x] 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - name: Install 24 | run: yarn install --ignore-scripts 25 | 26 | - name: Test 27 | run: yarn test --coverage 28 | 29 | - name: Coveralls GitHub Action 30 | uses: coverallsapp/github-action@1.1.3 31 | with: 32 | github-token: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/webpack.yaml: -------------------------------------------------------------------------------- 1 | name: webpack 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [18.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | 22 | - name: Install 23 | run: yarn install --ignore-scripts 24 | 25 | - name: Test 26 | run: yarn test 27 | 28 | - name: Build 29 | run: yarn build 30 | 31 | - name: Typecheck 32 | run: yarn ts:check 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | lib 3 | node_modules 4 | .DS_Store 5 | coverage 6 | .eslintcache 7 | src/parser/grammar.ts 8 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: sql-formatter 2 | name: sql-formatter 3 | description: 'Reformat SQL files with sql-formatter' 4 | entry: sql-formatter --fix 5 | language: node 6 | types: [sql] 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /lib 4 | /node_modules 5 | yarn.lock 6 | LICENSE 7 | /src/parser/grammar.ts 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "arrowParens": "avoid", 5 | "semi": true, 6 | "bracketSpacing": true, 7 | "useTabs": false, 8 | "tabWidth": 2, 9 | "endOfLine": "lf", 10 | "quoteProps": "consistent" 11 | } 12 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": {}, 3 | "git": { 4 | "changelog": "git log --pretty=format:\"* %s (%h)\" ${latestTag}...HEAD", 5 | "requireCleanWorkingDir": true, 6 | "requireBranch": "master", 7 | "requireUpstream": true, 8 | "requireCommits": false, 9 | "addUntrackedFiles": false, 10 | "commit": true, 11 | "commitMessage": "Release v${version}", 12 | "tag": true, 13 | "tagAnnotation": "${version}", 14 | "push": true, 15 | "pushArgs": ["--follow-tags"] 16 | }, 17 | "npm": { 18 | "publish": true, 19 | "publishPath": ".", 20 | "publishArgs": [], 21 | "tag": null, 22 | "otp": null, 23 | "ignoreVersion": true, 24 | "skipChecks": false, 25 | "timeout": 10 26 | }, 27 | "github": { 28 | "release": true, 29 | "releaseName": "${version}", 30 | "releaseNotes": "git log --pretty=format:\"* %s (%h)\" ${latestTag}...HEAD", 31 | "preRelease": false, 32 | "draft": false, 33 | "skipChecks": false, 34 | "web": true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Rene Saarsoo 2 | 3 | # Contributors 4 | Adrien Pyke 5 | Ahmad Khan 6 | Alexandr Kozhevnikov 7 | Alexander Prinzhorn 8 | An Phi 9 | Andrew 10 | Benjamin Bellamy 11 | bingou 12 | Boris Verkhovskiy 13 | Christian Jorgensen 14 | Christopher Manouvrier 15 | Damon Davison 16 | Daniël van Eeden 17 | Davut Can Abacigil 18 | eeeXun 19 | Erik Hirmo 20 | Erik Forsström 21 | Gajus Kuizinas 22 | George Leslie-Waksman 23 | Grant Forsythe 24 | Hugh Cameron 25 | htaketani 26 | Ian Campbell 27 | ivan baktsheev 28 | Jacobo Bouzas Quiroga 29 | JagmitSwami 30 | João Pimentel Ferreira 31 | James A Rosen 32 | Jonathan Schuster 33 | Josh Sandler 34 | Justin Dane Vallar 35 | Karl Horky 36 | Martin Nowak 37 | Matheus Salmi 38 | Matheus Teixeira 39 | Max R 40 | Michael Giannakopoulos 41 | musjj 42 | Nathan Walters 43 | Nicolas Dermine 44 | Offir Baron 45 | Olexandr Sydorchuk 46 | outslept 47 | Pavel Djundik 48 | pokutuna 49 | Rafael Pinto 50 | Rahel Rjadnev-Meristo 51 | Rodrigo Stuchi 52 | Romain Rigaux 53 | Sasha Aliashkevich 54 | Sean Song 55 | Sebastian Lyng Johansen 56 | Sergei Egorov 57 | Stanislav Germanovskii 58 | Steven Yung 59 | Timon Jurschitsch 60 | Tito Griné 61 | Toliver 62 | Toni Müller 63 | Tony Coconate 64 | Tyler Jones 65 | Uku Pattak 66 | VdustR 67 | Wylie Conlon 68 | Xin Hu 69 | Zhongxian Liang 70 | 0xflotus <0xflotus@gmail.com> 71 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Setup 4 | 5 | Run `yarn` after checkout to install all dependencies. 6 | 7 | ## Tests 8 | 9 | Tests can be run with `yarn test`. 10 | 11 | Please add new tests for any new features and bug fixes. 12 | Language-specific tests should be included in their respective `sqldialect.test.ts` files. 13 | Tests that apply to all languages should be in `behavesLikeSqlFormatter.ts`. 14 | 15 | ## Publish Flow 16 | 17 | For those who have admin access on the repo, the new release publish flow is as such: 18 | 19 | - `npm run release` (bumps version, git tag, git release, npm release) (does not work with `yarn`). 20 | - `git subtree push --prefix static origin gh-pages` (pushes demo page to GH pages) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2020 ZeroTurnaround LLC 4 | Copyright (c) 2020-2021 George Leslie-Waksman and other contributors 5 | Copyright (c) 2021-Present inferrinizzard and other contributors 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' # See documentation for possible values 9 | directory: '/' # Location of package manifests 10 | schedule: 11 | interval: 'daily' 12 | -------------------------------------------------------------------------------- /docs/dataTypeCase.md: -------------------------------------------------------------------------------- 1 | # dataTypeCase 2 | 3 | Converts data types to upper- or lowercase. 4 | 5 | ## Options 6 | 7 | - `"preserve"` (default) preserves the original case. 8 | - `"upper"` converts to uppercase. 9 | - `"lower"` converts to lowercase. 10 | 11 | ### preserve 12 | 13 | ```sql 14 | CREATE TABLE user ( 15 | id InTeGeR PRIMARY KEY, 16 | first_name VarChaR(30) NOT NULL, 17 | bio teXT, 18 | is_email_verified BooL, 19 | created_timestamp timestamP 20 | ); 21 | ``` 22 | 23 | ### upper 24 | 25 | ```sql 26 | CREATE TABLE user ( 27 | id INTEGER PRIMARY KEY, 28 | first_name VARCHAR(30) NOT NULL, 29 | bio TEXT, 30 | is_email_verified BOOL, 31 | created_timestamp TIMESTAMP 32 | ); 33 | ``` 34 | 35 | ### lower 36 | 37 | ```sql 38 | CREATE TABLE user ( 39 | id integer PRIMARY KEY, 40 | first_name varchar(30) NOT NULL, 41 | bio text, 42 | is_email_verified bool, 43 | created_timestamp timestamp 44 | ); 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/denseOperators.md: -------------------------------------------------------------------------------- 1 | # denseOperators 2 | 3 | Decides whitespace around operators. 4 | 5 | ## Options 6 | 7 | - `false` (default) surrounds operators with spaces. 8 | - `true` packs operators densely without spaces. 9 | 10 | Does not apply to logical operators (AND, OR, XOR). 11 | 12 | ### denseOperators: false (default) 13 | 14 | ``` 15 | SELECT 16 | price + (price * tax) AS bruto 17 | FROM 18 | prices 19 | ``` 20 | 21 | ### denseOperators: true 22 | 23 | ``` 24 | SELECT 25 | price+(price*tax) AS bruto 26 | FROM 27 | prices 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/dialect.md: -------------------------------------------------------------------------------- 1 | # dialect 2 | 3 | Specifies the SQL dialect to use. 4 | 5 | ## Usage 6 | 7 | ```ts 8 | import { formatDialect, sqlite } from 'sql-formatter'; 9 | 10 | const result = formatDialect('SELECT * FROM tbl', { dialect: sqlite }); 11 | ``` 12 | 13 | **Note:** This is part of new API, introduced in version 12. 14 | It can only be used together with the new `formatDialect()` function, 15 | not with the old `format()` function. 16 | It also can't be used in config file of the command line tool - 17 | for that, use the [language][] option. 18 | 19 | ## Options 20 | 21 | The following dialects can be imported from `"sql-formatter"` module: 22 | 23 | - `sql` - [Standard SQL][] 24 | - `bigquery` - [GCP BigQuery][] 25 | - `db2` - [IBM DB2][] 26 | - `db2i` - [IBM DB2i][] (experimental) 27 | - `duckdb` - [DuckDB][] 28 | - `hive` - [Apache Hive][] 29 | - `mariadb` - [MariaDB][] 30 | - `mysql` - [MySQL][] 31 | - `tidb` - [TiDB][] 32 | - `n1ql` - [Couchbase N1QL][] 33 | - `plsql` - [Oracle PL/SQL][] 34 | - `postgresql` - [PostgreSQL][] 35 | - `redshift` - [Amazon Redshift][] 36 | - `singlestoredb` - [SingleStoreDB][] 37 | - `snowflake` - [Snowflake][] 38 | - `spark` - [Spark][] 39 | - `sqlite` - [SQLite][] 40 | - `transactsql` - [SQL Server Transact-SQL][tsql] 41 | - `trino` - [Trino][] / [Presto][] 42 | 43 | The `sql` dialect is meant for cases where you don't know which dialect of SQL you're about to format. 44 | It's not an auto-detection, it just supports a subset of features common enough in many SQL implementations. 45 | This might or might not work for your specific dialect. 46 | Better to always pick something more specific if possible. 47 | 48 | ## Custom dialect configuration (experimental) 49 | 50 | The `dialect` parameter can also be used to specify a custom SQL dialect configuration: 51 | 52 | ```ts 53 | import { formatDialect, DialectOptions } from 'sql-formatter'; 54 | 55 | const myDialect: DialectOptions { 56 | name: 'my_dialect', 57 | tokenizerOptions: { 58 | // See source code for examples of tokenizer config options 59 | // For example: src/languages/sqlite/sqlite.formatter.ts 60 | }, 61 | formatOptions: { 62 | // ... 63 | }, 64 | }; 65 | 66 | const result = formatDialect('SELECT * FROM tbl', { dialect: myDialect }); 67 | ``` 68 | 69 | **NB!** This functionality is experimental and there are no stability guarantees for this API. 70 | The `DialectOptions` interface can (and likely will) change in non-major releases. 71 | You likely only want to use this if your other alternative is to fork SQL Formatter. 72 | 73 | [standard sql]: https://en.wikipedia.org/wiki/SQL:2011 74 | [gcp bigquery]: https://cloud.google.com/bigquery 75 | [ibm db2]: https://www.ibm.com/analytics/us/en/technology/db2/ 76 | [ibm db2i]: https://www.ibm.com/docs/en/i/7.5?topic=overview-db2-i 77 | [duckdb]: https://duckdb.org/ 78 | [apache hive]: https://hive.apache.org/ 79 | [mariadb]: https://mariadb.com/ 80 | [mysql]: https://www.mysql.com/ 81 | [tidb]: https://github.com/pingcap/tidb/ 82 | [couchbase n1ql]: http://www.couchbase.com/n1ql 83 | [oracle pl/sql]: http://www.oracle.com/technetwork/database/features/plsql/index.html 84 | [postgresql]: https://www.postgresql.org/ 85 | [presto]: https://prestodb.io/docs/current/ 86 | [amazon redshift]: https://docs.aws.amazon.com/redshift/latest/dg/cm_chap_SQLCommandRef.html 87 | [singlestoredb]: https://docs.singlestore.com/managed-service/en/reference.html 88 | [snowflake]: https://docs.snowflake.com/en/index.html 89 | [spark]: https://spark.apache.org/docs/latest/api/sql/index.html 90 | [sqlite]: https://sqlite.org/index.html 91 | [trino]: https://trino.io/docs/current/ 92 | [tsql]: https://docs.microsoft.com/en-us/sql/sql-server/ 93 | [language]: ./language.md 94 | -------------------------------------------------------------------------------- /docs/expressionWidth.md: -------------------------------------------------------------------------------- 1 | # expressionWidth 2 | 3 | Determines maximum length of parenthesized expressions. 4 | 5 | ## Option value 6 | 7 | A number (default `50`) specifying the maximum length of parenthesized expression 8 | that's does not get split up to multiple lines. 9 | 10 | ### expressionWidth: 50 (default) 11 | 12 | Keeps the parenthesized expression (with length of 42) on single line: 13 | 14 | ``` 15 | SELECT 16 | product.price + (product.original_price * product.sales_tax) AS total 17 | FROM 18 | product 19 | ``` 20 | 21 | ### expressionWidth: 40 22 | 23 | Splits the parenthesized expression (with length of 42) to multiple lines: 24 | 25 | ``` 26 | SELECT 27 | product.price + ( 28 | product.original_price * product.sales_tax 29 | ) AS total 30 | FROM 31 | product 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/functionCase.md: -------------------------------------------------------------------------------- 1 | # functionCase 2 | 3 | Converts function names to upper- or lowercase. 4 | 5 | ## Options 6 | 7 | - `"preserve"` (default) preserves the original case. 8 | - `"upper"` converts to uppercase. 9 | - `"lower"` converts to lowercase. 10 | 11 | ### preserve 12 | 13 | ```sql 14 | SELECT 15 | Concat(Trim(first_name), ' ', Trim(last_name)) AS name, 16 | Max(salary) AS max_pay, 17 | Cast(ssid AS INT) 18 | FROM 19 | employee 20 | WHERE 21 | expires_at > Now() 22 | ``` 23 | 24 | ### upper 25 | 26 | ```sql 27 | SELECT 28 | CONCAT(TRIM(first_name), ' ', TRIM(last_name)) AS name, 29 | MAX(salary) AS max_pay, 30 | CAST(ssid AS INT) 31 | FROM 32 | employee 33 | WHERE 34 | expires_at > NOW() 35 | ``` 36 | 37 | ### lower 38 | 39 | ```sql 40 | SELECT 41 | concat(trim(first_name), ' ', trim(last_name)) AS name, 42 | max(salary) AS max_pay, 43 | cast(ssid AS INT) 44 | FROM 45 | employee 46 | WHERE 47 | expires_at > now() 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/identifierCase.md: -------------------------------------------------------------------------------- 1 | # identifierCase (experimental) 2 | 3 | Converts identifiers to upper- or lowercase. Only unquoted identifiers are converted. 4 | 5 | This option doesn't yet support all types of identifiers: 6 | 7 | - prefixed variables like `@my_var` are not converted. 8 | - parameter placeholders like `:param` are not converted. 9 | 10 | **NB!** The use of this option is generally not recommended, 11 | because SQL Formatter leans on the side of detecting as few keywords as possible 12 | (to avoid converting them to uppercase when `keywordCase: "upper"` is used), 13 | which on the flip side means that everything else will be labeled as identifiers. 14 | 15 | The only reasonable cases to use this option is when you want all your SQL to 16 | be either in uppercase or lowercase. But if you only want keywords to be in 17 | uppercase, only use the `keywordCase: "upper"` option. 18 | 19 | ## Options 20 | 21 | - `"preserve"` (default) preserves the original case. 22 | - `"upper"` converts to uppercase. 23 | - `"lower"` converts to lowercase. 24 | 25 | ### preserve 26 | 27 | ``` 28 | select 29 | count(a.Column1), 30 | max(a.Column2 + a.Column3), 31 | a.Column4 AS myCol 32 | from 33 | Table1 as a 34 | where 35 | Column6 36 | and Column7 37 | group by 38 | Column4 39 | ``` 40 | 41 | ### upper 42 | 43 | ``` 44 | select 45 | count(A.COLUMN1), 46 | max(A.COLUMN2 + A.COLUMN3), 47 | A.COLUMN4 AS MYCOL 48 | from 49 | TABLE1 as A 50 | where 51 | COLUMN6 52 | and COLUMN7 53 | group by 54 | COLUMN4 55 | ``` 56 | 57 | ### lower 58 | 59 | ``` 60 | select 61 | count(a.column1), 62 | max(a.column2 + a.column3), 63 | a.column4 AS mycol 64 | from 65 | table1 as a 66 | where 67 | column6 68 | and column7 69 | group by 70 | column4 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/indentStyle.md: -------------------------------------------------------------------------------- 1 | # indentStyle (DEPRECATED!) 2 | 3 | Switches between different indentation styles. 4 | 5 | ## Options 6 | 7 | - `"standard"` (default) indents code by the amount specified by `tabWidth` option. 8 | - `"tabularLeft"` indents in tabular style with 10 spaces, aligning keywords to left. 9 | - `"tabularRight"` indents in tabular style with 10 spaces, aligning keywords to right. 10 | 11 | Caveats of using `"tabularLeft"` and `"tabularRight"`: 12 | 13 | - `tabWidth` option is ignored. Indentation will always be 10 spaces, regardless of what is specified by `tabWidth`. 14 | - The implementation of these styles is more of a bolted-on feature which has never worked quite as well as the `"standard"` style. 15 | 16 | ### standard 17 | 18 | ``` 19 | SELECT 20 | COUNT(a.column1), 21 | MAX(b.column2 + b.column3), 22 | b.column4 AS four 23 | FROM 24 | ( 25 | SELECT 26 | column1, 27 | column5 28 | FROM 29 | table1 30 | ) a 31 | JOIN table2 b ON a.column5 = b.column5 32 | WHERE 33 | column6 34 | AND column7 35 | GROUP BY column4 36 | ``` 37 | 38 | ### tabularLeft 39 | 40 | ``` 41 | SELECT COUNT(a.column1), 42 | MAX(b.column2 + b.column3), 43 | b.column4 AS four 44 | FROM ( 45 | SELECT column1, 46 | column5 47 | FROM table1 48 | ) a 49 | JOIN table2 b ON a.column5 = b.column5 50 | WHERE column6 51 | AND column7 52 | GROUP BY column4 53 | ``` 54 | 55 | ### tabularRight 56 | 57 | ``` 58 | SELECT COUNT(a.column1), 59 | MAX(b.column2 + b.column3), 60 | b.column4 AS four 61 | FROM ( 62 | SELECT column1, 63 | column5 64 | FROM table1 65 | ) a 66 | JOIN table2 b ON a.column5 = b.column5 67 | WHERE column6 68 | AND column7 69 | GROUP BY column4 70 | ``` 71 | -------------------------------------------------------------------------------- /docs/keywordCase.md: -------------------------------------------------------------------------------- 1 | # keywordCase 2 | 3 | Converts reserved keywords to upper- or lowercase. 4 | 5 | ## Options 6 | 7 | - `"preserve"` (default) preserves the original case. 8 | - `"upper"` converts to uppercase. 9 | - `"lower"` converts to lowercase. 10 | 11 | ### preserve 12 | 13 | ``` 14 | Select 15 | count(a.column1), 16 | max(a.column2 + a.column3), 17 | a.column4 AS myCol 18 | From 19 | table1 as a 20 | Where 21 | column6 22 | and column7 23 | Group by column4 24 | ``` 25 | 26 | ### upper 27 | 28 | ``` 29 | SELECT 30 | COUNT(a.column1), 31 | MAX(a.column2 + a.column3), 32 | a.column4 AS myCol 33 | FROM 34 | table1 AS a 35 | WHERE 36 | column6 37 | AND column7 38 | GROUP BY column4 39 | ``` 40 | 41 | ### lower 42 | 43 | ``` 44 | select 45 | count(a.column1), 46 | max(a.column2 + a.column3), 47 | a.column4 as myCol 48 | from 49 | table1 as a 50 | where 51 | column6 52 | and column7 53 | group by column4 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/language.md: -------------------------------------------------------------------------------- 1 | # language 2 | 3 | Specifies the SQL dialect to use. 4 | 5 | ## Usage 6 | 7 | ```ts 8 | import { format } from 'sql-formatter'; 9 | 10 | const result = format('SELECT * FROM tbl', { language: 'sqlite' }); 11 | ``` 12 | 13 | ## Options 14 | 15 | - `"sql"` - (default) [Standard SQL][] 16 | - `"bigquery"` - [GCP BigQuery][] 17 | - `"db2"` - [IBM DB2][] 18 | - `"db2i"` - [IBM DB2i][] (experimental) 19 | - `"duckdb"` - [DuckDB][] 20 | - `"hive"` - [Apache Hive][] 21 | - `"mariadb"` - [MariaDB][] 22 | - `"mysql"` - [MySQL][] 23 | - `"tidb"` - [TiDB][] 24 | - `"n1ql"` - [Couchbase N1QL][] 25 | - `"plsql"` - [Oracle PL/SQL][] 26 | - `"postgresql"` - [PostgreSQL][] 27 | - `"redshift"` - [Amazon Redshift][] 28 | - `"singlestoredb"` - [SingleStoreDB][] 29 | - `"snowflake"` - [Snowflake][] 30 | - `"spark"` - [Spark][] 31 | - `"sqlite"` - [SQLite][sqlite] 32 | - `"transactsql"` or `"tsql"` - [SQL Server Transact-SQL][tsql] 33 | - `"trino"` - [Trino][] (should also work for [Presto][], which is very similar dialect, though technically different) 34 | 35 | The default `"sql"` dialect is meant for cases where you don't know which dialect of SQL you're about to format. 36 | It's not an auto-detection, it just supports a subset of features common enough in many SQL implementations. 37 | This might or might not work for your specific dialect. 38 | Better to always pick something more specific if possible. 39 | 40 | ## Impact on bundle size 41 | 42 | Using the `language` option has the downside that the used dialects are determined at runtime 43 | and therefore they all have to be bundled when e.g. building a bundle with Webpack. 44 | This can result in significant overhead when you only need to format one or two dialects. 45 | 46 | To solve this problem, version 12 of SQL Formatter introduces a new API, 47 | that allows explicitly importing the dialects. 48 | See docs for [dialect][] option. 49 | 50 | [standard sql]: https://en.wikipedia.org/wiki/SQL:2011 51 | [gcp bigquery]: https://cloud.google.com/bigquery 52 | [ibm db2]: https://www.ibm.com/analytics/us/en/technology/db2/ 53 | [ibm db2i]: https://www.ibm.com/docs/en/i/7.5?topic=overview-db2-i 54 | [duckdb]: https://duckdb.org/ 55 | [apache hive]: https://hive.apache.org/ 56 | [mariadb]: https://mariadb.com/ 57 | [mysql]: https://www.mysql.com/ 58 | [tidb]: https://github.com/pingcap/tidb/ 59 | [couchbase n1ql]: http://www.couchbase.com/n1ql 60 | [oracle pl/sql]: http://www.oracle.com/technetwork/database/features/plsql/index.html 61 | [postgresql]: https://www.postgresql.org/ 62 | [presto]: https://prestodb.io/docs/current/ 63 | [amazon redshift]: https://docs.aws.amazon.com/redshift/latest/dg/cm_chap_SQLCommandRef.html 64 | [singlestoredb]: https://docs.singlestore.com/managed-service/en/reference.html 65 | [snowflake]: https://docs.snowflake.com/en/index.html 66 | [spark]: https://spark.apache.org/docs/latest/api/sql/index.html 67 | [sqlite]: https://sqlite.org/index.html 68 | [trino]: https://trino.io/docs/current/ 69 | [tsql]: https://docs.microsoft.com/en-us/sql/sql-server/ 70 | [dialect]: ./dialect.md 71 | -------------------------------------------------------------------------------- /docs/linesBetweenQueries.md: -------------------------------------------------------------------------------- 1 | # linesBetweenQueries 2 | 3 | Decides how many empty lines to leave between SQL statements. 4 | 5 | ## Option value 6 | 7 | A number of empty lines. Defaults to `1`. Must be positive. 8 | 9 | - `1` (default) adds newline before open-parenthesis. 10 | - `false` no newline. 11 | 12 | ### linesBetweenQueries: 1 (default) 13 | 14 | ```sql 15 | SELECT 16 | * 17 | FROM 18 | foo; 19 | 20 | SELECT 21 | * 22 | FROM 23 | bar; 24 | ``` 25 | 26 | ### linesBetweenQueries: 0 27 | 28 | ```sql 29 | SELECT 30 | * 31 | FROM 32 | foo; 33 | SELECT 34 | * 35 | FROM 36 | bar; 37 | ``` 38 | 39 | ### linesBetweenQueries: 2 40 | 41 | ```sql 42 | SELECT 43 | * 44 | FROM 45 | foo; 46 | 47 | 48 | SELECT 49 | * 50 | FROM 51 | bar; 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/logicalOperatorNewline.md: -------------------------------------------------------------------------------- 1 | # logicalOperatorNewline 2 | 3 | Decides newline placement before or after logical operators (AND, OR, XOR). 4 | 5 | ## Options 6 | 7 | - `"before"` (default) adds newline before the operator. 8 | - `"after"` adds newline after the operator. 9 | 10 | ### before 11 | 12 | ```sql 13 | SELECT 14 | * 15 | FROM 16 | persons 17 | WHERE 18 | age > 10 19 | AND height < 150 20 | OR occupation IS NULL 21 | ``` 22 | 23 | ### after 24 | 25 | ```sql 26 | SELECT 27 | * 28 | FROM 29 | persons 30 | WHERE 31 | age > 10 AND 32 | height < 150 OR 33 | occupation IS NULL 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/newlineBeforeSemicolon.md: -------------------------------------------------------------------------------- 1 | # newlineBeforeSemicolon 2 | 3 | Whether to place query separator (`;`) on a separate line. 4 | 5 | ## Options 6 | 7 | - `false` (default) no empty line before semicolon. 8 | - `true` places semicolon on a separate line. 9 | 10 | ### newlineBeforeSemicolon: false (default) 11 | 12 | ```sql 13 | SELECT 14 | * 15 | FROM 16 | foo; 17 | ``` 18 | 19 | ### newlineBeforeSemicolon: true 20 | 21 | ```sql 22 | SELECT 23 | * 24 | FROM 25 | foo 26 | ; 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/params.md: -------------------------------------------------------------------------------- 1 | # params 2 | 3 | Specifies parameter values to fill in for placeholders inside SQL. 4 | 5 | This option is designed to be used through API (though nothing really prevents usage from command line). 6 | 7 | ## Option value 8 | 9 | - `Array` of strings for position placeholders. 10 | - `Object` of name-value pairs for named (and indexed) placeholders. 11 | 12 | Note: The escaping of values must be handled by user of the API. 13 | 14 | ### Positional placeholders 15 | 16 | For positional placeholders use array of values: 17 | 18 | ```js 19 | format('SELECT * FROM persons WHERE fname = ? AND age = ?', { 20 | params: ["'John'", '27'], 21 | language: 'sql', 22 | }); 23 | ``` 24 | 25 | Results in: 26 | 27 | ```sql 28 | SELECT 29 | * 30 | FROM 31 | persons 32 | WHERE 33 | fname = 'John' 34 | AND age = 27 35 | ``` 36 | 37 | ### Named placeholders 38 | 39 | For named placeholders use object of name-value pairs: 40 | 41 | ```js 42 | format('SELECT * FROM persons WHERE fname = @name AND age = @age', { 43 | params: { name: "'John'", age: '27' }, 44 | language: 'tsql', 45 | }); 46 | ``` 47 | 48 | Results in: 49 | 50 | ```sql 51 | SELECT 52 | * 53 | FROM 54 | persons 55 | WHERE 56 | fname = 'John' 57 | AND age = 27 58 | ``` 59 | 60 | ### Numbered placeholders 61 | 62 | Treat numbered placeholders the same as named ones and use an object of number-value pairs: 63 | 64 | ```js 65 | format('SELECT * FROM persons WHERE fname = $1 AND age = $2', { 66 | params: { 1: "'John'", 2: '27' }, 67 | language: 'postgresql', 68 | }); 69 | ``` 70 | 71 | Results in: 72 | 73 | ```sql 74 | SELECT 75 | * 76 | FROM 77 | persons 78 | WHERE 79 | fname = 'John' 80 | AND age = 27 81 | ``` 82 | 83 | ### Quoted placeholders 84 | 85 | Some dialects (BigQuery, Transact SQL) also support quoted names for placeholders: 86 | 87 | ```js 88 | format('SELECT * FROM persons WHERE fname = @`first name` AND age = @`age`', { 89 | params: { 'first name': "'John'", 'age': '27' }, 90 | language: 'bigquery', 91 | }); 92 | ``` 93 | 94 | Results in: 95 | 96 | ```sql 97 | SELECT 98 | * 99 | FROM 100 | persons 101 | WHERE 102 | fname = 'John' 103 | AND age = 27 104 | ``` 105 | 106 | ## Available placeholder types 107 | 108 | The placeholder types available by default depend on SQL dialect used: 109 | 110 | - sql - `?` 111 | - bigquery - `?`, `@name`, `` @`name` `` 112 | - db2 - `?`, `:name` 113 | - db2i - `?`, `:name` 114 | - hive - _no support_ 115 | - mariadb - `?` 116 | - mysql - `?` 117 | - n1ql - `?`, `$1`, `$name` 118 | - plsql - `:1`, `:name` 119 | - postgresql - `$1` 120 | - redshift - `$1` 121 | - snowflake - _no support_ 122 | - sqlite - `?`, `?1`, `:name`, `@name`, `$name` 123 | - spark - _no support_ 124 | - tidb - `?` 125 | - tsql - `@name`, `@"name"`, `@[name]` 126 | - trino - _no support_ 127 | 128 | If you need to use a different placeholder syntax than the builtin one, 129 | you can configure the supported placeholder types using the [paramTypes][] config option. 130 | 131 | [paramtypes]: ./paramTypes.md 132 | -------------------------------------------------------------------------------- /docs/tabWidth.md: -------------------------------------------------------------------------------- 1 | # tabWidth 2 | 3 | Specifies amount of spaces to be used for indentation. 4 | 5 | ## Option value 6 | 7 | A string containing the characters of one indentation step. 8 | Defaults to two spaces (`" \ "`). 9 | 10 | This option is ignored when `useTabs` option is enabled. 11 | 12 | ### Indenting by 2 spaces (default) 13 | 14 | ```sql 15 | SELECT 16 | *, 17 | FROM 18 | ( 19 | SELECT 20 | column1, 21 | column5 22 | FROM 23 | table1 24 | ) a 25 | JOIN table2 26 | WHERE 27 | column6 28 | AND column7 29 | GROUP BY column4 30 | ``` 31 | 32 | ### Indenting by 4 spaces 33 | 34 | Using `indent: 4`: 35 | 36 | ```sql 37 | SELECT 38 | *, 39 | FROM 40 | ( 41 | SELECT 42 | column1, 43 | column5 44 | FROM 45 | table1 46 | ) a 47 | JOIN table2 48 | WHERE 49 | column6 50 | AND column7 51 | GROUP BY column4 52 | ``` 53 | 54 | ### Indenting with tabs 55 | 56 | Using `indent: "\t"`: 57 | 58 | ```sql 59 | SELECT 60 | *, 61 | FROM 62 | ( 63 | SELECT 64 | column1, 65 | column5 66 | FROM 67 | table1 68 | ) a 69 | JOIN table2 70 | WHERE 71 | column6 72 | AND column7 73 | GROUP BY column4 74 | ``` 75 | 76 | Imagine that these long sequences of spaces are actually TAB characters :) 77 | -------------------------------------------------------------------------------- /docs/useTabs.md: -------------------------------------------------------------------------------- 1 | # useTabs 2 | 3 | Uses TAB characters for indentation. 4 | 5 | ## Options 6 | 7 | - `false` (default) use spaces (see `tabWidth` option). 8 | - `true` use tabs. 9 | 10 | ### Indenting with tabs 11 | 12 | Using `useTabs: true`: 13 | 14 | ```sql 15 | SELECT 16 | *, 17 | FROM 18 | ( 19 | SELECT 20 | column1, 21 | column5 22 | FROM 23 | table1 24 | ) a 25 | JOIN table2 26 | WHERE 27 | column6 28 | AND column7 29 | GROUP BY column4 30 | ``` 31 | 32 | Imagine that these long sequences of spaces are actually TAB characters :) 33 | -------------------------------------------------------------------------------- /src/FormatOptions.ts: -------------------------------------------------------------------------------- 1 | // import only type to avoid ESLint no-cycle rule producing an error 2 | import { ParamItems } from './formatter/Params.js'; 3 | import { ParamTypes } from './lexer/TokenizerOptions.js'; 4 | 5 | export type IndentStyle = 'standard' | 'tabularLeft' | 'tabularRight'; 6 | 7 | export type KeywordCase = 'preserve' | 'upper' | 'lower'; 8 | export type IdentifierCase = KeywordCase; 9 | export type DataTypeCase = KeywordCase; 10 | export type FunctionCase = KeywordCase; 11 | 12 | export type LogicalOperatorNewline = 'before' | 'after'; 13 | 14 | export interface FormatOptions { 15 | tabWidth: number; 16 | useTabs: boolean; 17 | keywordCase: KeywordCase; 18 | identifierCase: IdentifierCase; 19 | dataTypeCase: DataTypeCase; 20 | functionCase: FunctionCase; 21 | indentStyle: IndentStyle; 22 | logicalOperatorNewline: LogicalOperatorNewline; 23 | expressionWidth: number; 24 | linesBetweenQueries: number; 25 | denseOperators: boolean; 26 | newlineBeforeSemicolon: boolean; 27 | params?: ParamItems | string[]; 28 | paramTypes?: ParamTypes; 29 | } 30 | -------------------------------------------------------------------------------- /src/allDialects.ts: -------------------------------------------------------------------------------- 1 | export { bigquery } from './languages/bigquery/bigquery.formatter.js'; 2 | export { db2 } from './languages/db2/db2.formatter.js'; 3 | export { db2i } from './languages/db2i/db2i.formatter.js'; 4 | export { duckdb } from './languages/duckdb/duckdb.formatter.js'; 5 | export { hive } from './languages/hive/hive.formatter.js'; 6 | export { mariadb } from './languages/mariadb/mariadb.formatter.js'; 7 | export { mysql } from './languages/mysql/mysql.formatter.js'; 8 | export { tidb } from './languages/tidb/tidb.formatter.js'; 9 | export { n1ql } from './languages/n1ql/n1ql.formatter.js'; 10 | export { plsql } from './languages/plsql/plsql.formatter.js'; 11 | export { postgresql } from './languages/postgresql/postgresql.formatter.js'; 12 | export { redshift } from './languages/redshift/redshift.formatter.js'; 13 | export { spark } from './languages/spark/spark.formatter.js'; 14 | export { sqlite } from './languages/sqlite/sqlite.formatter.js'; 15 | export { sql } from './languages/sql/sql.formatter.js'; 16 | export { trino } from './languages/trino/trino.formatter.js'; 17 | export { transactsql } from './languages/transactsql/transactsql.formatter.js'; 18 | export { singlestoredb } from './languages/singlestoredb/singlestoredb.formatter.js'; 19 | export { snowflake } from './languages/snowflake/snowflake.formatter.js'; 20 | -------------------------------------------------------------------------------- /src/dialect.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DialectFormatOptions, 3 | ProcessedDialectFormatOptions, 4 | } from './formatter/ExpressionFormatter.js'; 5 | import Tokenizer from './lexer/Tokenizer.js'; 6 | import { TokenizerOptions } from './lexer/TokenizerOptions.js'; 7 | 8 | export interface DialectOptions { 9 | name: string; 10 | tokenizerOptions: TokenizerOptions; 11 | formatOptions: DialectFormatOptions; 12 | } 13 | 14 | export interface Dialect { 15 | tokenizer: Tokenizer; 16 | formatOptions: ProcessedDialectFormatOptions; 17 | } 18 | 19 | const cache = new Map(); 20 | 21 | /** 22 | * Factory function for building Dialect objects. 23 | * When called repeatedly with same options object returns the cached Dialect, 24 | * to avoid the cost of creating it again. 25 | */ 26 | export const createDialect = (options: DialectOptions): Dialect => { 27 | let dialect = cache.get(options); 28 | if (!dialect) { 29 | dialect = dialectFromOptions(options); 30 | cache.set(options, dialect); 31 | } 32 | return dialect; 33 | }; 34 | 35 | const dialectFromOptions = (dialectOptions: DialectOptions): Dialect => ({ 36 | tokenizer: new Tokenizer(dialectOptions.tokenizerOptions, dialectOptions.name), 37 | formatOptions: processDialectFormatOptions(dialectOptions.formatOptions), 38 | }); 39 | 40 | const processDialectFormatOptions = ( 41 | options: DialectFormatOptions 42 | ): ProcessedDialectFormatOptions => ({ 43 | alwaysDenseOperators: options.alwaysDenseOperators || [], 44 | onelineClauses: Object.fromEntries(options.onelineClauses.map(name => [name, true])), 45 | tabularOnelineClauses: Object.fromEntries( 46 | (options.tabularOnelineClauses ?? options.onelineClauses).map(name => [name, true]) 47 | ), 48 | }); 49 | -------------------------------------------------------------------------------- /src/formatter/Formatter.ts: -------------------------------------------------------------------------------- 1 | import { FormatOptions } from '../FormatOptions.js'; 2 | import { indentString } from './config.js'; 3 | import Params from './Params.js'; 4 | 5 | import { createParser } from '../parser/createParser.js'; 6 | import { StatementNode } from '../parser/ast.js'; 7 | import { Dialect } from '../dialect.js'; 8 | 9 | import ExpressionFormatter from './ExpressionFormatter.js'; 10 | import Layout, { WS } from './Layout.js'; 11 | import Indentation from './Indentation.js'; 12 | 13 | /** Main formatter class that produces a final output string from list of tokens */ 14 | export default class Formatter { 15 | private dialect: Dialect; 16 | private cfg: FormatOptions; 17 | private params: Params; 18 | 19 | constructor(dialect: Dialect, cfg: FormatOptions) { 20 | this.dialect = dialect; 21 | this.cfg = cfg; 22 | this.params = new Params(this.cfg.params); 23 | } 24 | 25 | /** 26 | * Formats an SQL query. 27 | * @param {string} query - The SQL query string to be formatted 28 | * @return {string} The formatter query 29 | */ 30 | public format(query: string): string { 31 | const ast = this.parse(query); 32 | const formattedQuery = this.formatAst(ast); 33 | return formattedQuery.trimEnd(); 34 | } 35 | 36 | private parse(query: string): StatementNode[] { 37 | return createParser(this.dialect.tokenizer).parse(query, this.cfg.paramTypes || {}); 38 | } 39 | 40 | private formatAst(statements: StatementNode[]): string { 41 | return statements 42 | .map(stat => this.formatStatement(stat)) 43 | .join('\n'.repeat(this.cfg.linesBetweenQueries + 1)); 44 | } 45 | 46 | private formatStatement(statement: StatementNode): string { 47 | const layout = new ExpressionFormatter({ 48 | cfg: this.cfg, 49 | dialectCfg: this.dialect.formatOptions, 50 | params: this.params, 51 | layout: new Layout(new Indentation(indentString(this.cfg))), 52 | }).format(statement.children); 53 | 54 | if (!statement.hasSemicolon) { 55 | // do nothing 56 | } else if (this.cfg.newlineBeforeSemicolon) { 57 | layout.add(WS.NEWLINE, ';'); 58 | } else { 59 | layout.add(WS.NO_NEWLINE, ';'); 60 | } 61 | return layout.toString(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/formatter/Indentation.ts: -------------------------------------------------------------------------------- 1 | import { last } from '../utils.js'; 2 | 3 | const INDENT_TYPE_TOP_LEVEL = 'top-level'; 4 | const INDENT_TYPE_BLOCK_LEVEL = 'block-level'; 5 | 6 | /** 7 | * Manages indentation levels. 8 | * 9 | * There are two types of indentation levels: 10 | * 11 | * - BLOCK_LEVEL : increased by open-parenthesis 12 | * - TOP_LEVEL : increased by RESERVED_CLAUSE words 13 | */ 14 | export default class Indentation { 15 | private indentTypes: string[] = []; 16 | 17 | /** 18 | * @param {string} indent A string to indent with 19 | */ 20 | constructor(private indent: string) {} 21 | 22 | /** 23 | * Returns indentation string for single indentation step. 24 | */ 25 | getSingleIndent(): string { 26 | return this.indent; 27 | } 28 | 29 | /** 30 | * Returns current indentation level 31 | */ 32 | getLevel(): number { 33 | return this.indentTypes.length; 34 | } 35 | 36 | /** 37 | * Increases indentation by one top-level indent. 38 | */ 39 | increaseTopLevel() { 40 | this.indentTypes.push(INDENT_TYPE_TOP_LEVEL); 41 | } 42 | 43 | /** 44 | * Increases indentation by one block-level indent. 45 | */ 46 | increaseBlockLevel() { 47 | this.indentTypes.push(INDENT_TYPE_BLOCK_LEVEL); 48 | } 49 | 50 | /** 51 | * Decreases indentation by one top-level indent. 52 | * Does nothing when the previous indent is not top-level. 53 | */ 54 | decreaseTopLevel() { 55 | if (this.indentTypes.length > 0 && last(this.indentTypes) === INDENT_TYPE_TOP_LEVEL) { 56 | this.indentTypes.pop(); 57 | } 58 | } 59 | 60 | /** 61 | * Decreases indentation by one block-level indent. 62 | * If there are top-level indents within the block-level indent, 63 | * throws away these as well. 64 | */ 65 | decreaseBlockLevel() { 66 | while (this.indentTypes.length > 0) { 67 | const type = this.indentTypes.pop(); 68 | if (type !== INDENT_TYPE_TOP_LEVEL) { 69 | break; 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/formatter/InlineLayout.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line max-classes-per-file 2 | import Indentation from './Indentation.js'; 3 | import Layout, { WS } from './Layout.js'; 4 | 5 | /** 6 | * Like Layout, but only formats single-line expressions. 7 | * 8 | * Throws InlineLayoutError: 9 | * - when encountering a newline 10 | * - when exceeding configured expressionWidth 11 | */ 12 | export default class InlineLayout extends Layout { 13 | private length = 0; 14 | // Keeps track of the trailing whitespace, 15 | // so that we can decrease length when encountering WS.NO_SPACE, 16 | // but only when there actually is a space to remove. 17 | private trailingSpace = false; 18 | 19 | constructor(private expressionWidth: number) { 20 | super(new Indentation('')); // no indentation in inline layout 21 | } 22 | 23 | public add(...items: (WS | string)[]) { 24 | items.forEach(item => this.addToLength(item)); 25 | if (this.length > this.expressionWidth) { 26 | // We have exceeded the allowable width 27 | throw new InlineLayoutError(); 28 | } 29 | super.add(...items); 30 | } 31 | 32 | private addToLength(item: WS | string) { 33 | if (typeof item === 'string') { 34 | this.length += item.length; 35 | this.trailingSpace = false; 36 | } else if (item === WS.MANDATORY_NEWLINE || item === WS.NEWLINE) { 37 | // newlines not allowed within inline block 38 | throw new InlineLayoutError(); 39 | } else if (item === WS.INDENT || item === WS.SINGLE_INDENT || item === WS.SPACE) { 40 | if (!this.trailingSpace) { 41 | this.length++; 42 | this.trailingSpace = true; 43 | } 44 | } else if (item === WS.NO_NEWLINE || item === WS.NO_SPACE) { 45 | if (this.trailingSpace) { 46 | this.trailingSpace = false; 47 | this.length--; 48 | } 49 | } 50 | } 51 | } 52 | 53 | /** 54 | * Thrown when block of SQL can't be formatted as a single line. 55 | */ 56 | export class InlineLayoutError extends Error {} 57 | -------------------------------------------------------------------------------- /src/formatter/Params.ts: -------------------------------------------------------------------------------- 1 | export type ParamItems = { [k: string]: string }; 2 | 3 | /** 4 | * Handles placeholder replacement with given params. 5 | */ 6 | export default class Params { 7 | private params: ParamItems | string[] | undefined; 8 | private index: number; 9 | 10 | constructor(params: ParamItems | string[] | undefined) { 11 | this.params = params; 12 | this.index = 0; 13 | } 14 | 15 | /** 16 | * Returns param value that matches given placeholder with param key. 17 | */ 18 | public get({ key, text }: { key?: string; text: string }): string { 19 | if (!this.params) { 20 | return text; 21 | } 22 | 23 | if (key) { 24 | return (this.params as ParamItems)[key]; 25 | } 26 | return (this.params as string[])[this.index++]; 27 | } 28 | 29 | /** 30 | * Returns index of current positional parameter. 31 | */ 32 | public getPositionalParameterIndex(): number { 33 | return this.index; 34 | } 35 | 36 | /** 37 | * Sets index of current positional parameter. 38 | */ 39 | public setPositionalParameterIndex(i: number) { 40 | this.index = i; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/formatter/config.ts: -------------------------------------------------------------------------------- 1 | import { FormatOptions } from '../FormatOptions.js'; 2 | 3 | // Utility functions for config options 4 | 5 | /** 6 | * Creates a string to use for one step of indentation. 7 | */ 8 | export function indentString(cfg: FormatOptions): string { 9 | if (cfg.indentStyle === 'tabularLeft' || cfg.indentStyle === 'tabularRight') { 10 | return ' '.repeat(10); 11 | } 12 | if (cfg.useTabs) { 13 | return '\t'; 14 | } 15 | return ' '.repeat(cfg.tabWidth); 16 | } 17 | 18 | /** 19 | * True when indentStyle is one of the tabular ones. 20 | */ 21 | export function isTabularStyle(cfg: FormatOptions): boolean { 22 | return cfg.indentStyle === 'tabularLeft' || cfg.indentStyle === 'tabularRight'; 23 | } 24 | -------------------------------------------------------------------------------- /src/formatter/tabularStyle.ts: -------------------------------------------------------------------------------- 1 | import { IndentStyle } from '../FormatOptions.js'; 2 | import { isLogicalOperator, TokenType } from '../lexer/token.js'; 3 | 4 | /** 5 | * When tabular style enabled, 6 | * produces a 10-char wide version of token text. 7 | */ 8 | export default function toTabularFormat(tokenText: string, indentStyle: IndentStyle): string { 9 | if (indentStyle === 'standard') { 10 | return tokenText; 11 | } 12 | 13 | let tail = [] as string[]; // rest of keyword 14 | if (tokenText.length >= 10 && tokenText.includes(' ')) { 15 | // split for long keywords like INNER JOIN or UNION DISTINCT 16 | [tokenText, ...tail] = tokenText.split(' '); 17 | } 18 | 19 | if (indentStyle === 'tabularLeft') { 20 | tokenText = tokenText.padEnd(9, ' '); 21 | } else { 22 | tokenText = tokenText.padStart(9, ' '); 23 | } 24 | 25 | return tokenText + ['', ...tail].join(' '); 26 | } 27 | 28 | /** 29 | * True when the token can be formatted in tabular style 30 | */ 31 | export function isTabularToken(type: TokenType): boolean { 32 | return ( 33 | isLogicalOperator(type) || 34 | type === TokenType.RESERVED_CLAUSE || 35 | type === TokenType.RESERVED_SELECT || 36 | type === TokenType.RESERVED_SET_OPERATION || 37 | type === TokenType.RESERVED_JOIN || 38 | type === TokenType.LIMIT 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { supportedDialects, format, formatDialect } from './sqlFormatter.js'; 2 | export { expandPhrases } from './expandPhrases.js'; 3 | export { ConfigError } from './validateConfig.js'; 4 | 5 | // When adding a new dialect, be sure to add it to the list of exports below. 6 | export { bigquery } from './languages/bigquery/bigquery.formatter.js'; 7 | export { db2 } from './languages/db2/db2.formatter.js'; 8 | export { db2i } from './languages/db2i/db2i.formatter.js'; 9 | export { duckdb } from './languages/duckdb/duckdb.formatter.js'; 10 | export { hive } from './languages/hive/hive.formatter.js'; 11 | export { mariadb } from './languages/mariadb/mariadb.formatter.js'; 12 | export { mysql } from './languages/mysql/mysql.formatter.js'; 13 | export { tidb } from './languages/tidb/tidb.formatter.js'; 14 | export { n1ql } from './languages/n1ql/n1ql.formatter.js'; 15 | export { plsql } from './languages/plsql/plsql.formatter.js'; 16 | export { postgresql } from './languages/postgresql/postgresql.formatter.js'; 17 | export { redshift } from './languages/redshift/redshift.formatter.js'; 18 | export { spark } from './languages/spark/spark.formatter.js'; 19 | export { sqlite } from './languages/sqlite/sqlite.formatter.js'; 20 | export { sql } from './languages/sql/sql.formatter.js'; 21 | export { trino } from './languages/trino/trino.formatter.js'; 22 | export { transactsql } from './languages/transactsql/transactsql.formatter.js'; 23 | export { singlestoredb } from './languages/singlestoredb/singlestoredb.formatter.js'; 24 | export { snowflake } from './languages/snowflake/snowflake.formatter.js'; 25 | 26 | // NB! To re-export types the "export type" syntax is required by webpack. 27 | // Otherwise webpack build will fail. 28 | export type { 29 | SqlLanguage, 30 | FormatOptionsWithLanguage, 31 | FormatOptionsWithDialect, 32 | } from './sqlFormatter.js'; 33 | export type { 34 | IndentStyle, 35 | KeywordCase, 36 | DataTypeCase, 37 | FunctionCase, 38 | IdentifierCase, 39 | LogicalOperatorNewline, 40 | FormatOptions, 41 | } from './FormatOptions.js'; 42 | export type { ParamItems } from './formatter/Params.js'; 43 | export type { ParamTypes } from './lexer/TokenizerOptions.js'; 44 | export type { DialectOptions } from './dialect.js'; 45 | -------------------------------------------------------------------------------- /src/languages/bigquery/bigquery.keywords.ts: -------------------------------------------------------------------------------- 1 | export const keywords: string[] = [ 2 | // https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#reserved_keywords 3 | 'ALL', 4 | 'AND', 5 | 'ANY', 6 | 'AS', 7 | 'ASC', 8 | 'ASSERT_ROWS_MODIFIED', 9 | 'AT', 10 | 'BETWEEN', 11 | 'BY', 12 | 'CASE', 13 | 'CAST', 14 | 'COLLATE', 15 | 'CONTAINS', 16 | 'CREATE', 17 | 'CROSS', 18 | 'CUBE', 19 | 'CURRENT', 20 | 'DEFAULT', 21 | 'DEFINE', 22 | 'DESC', 23 | 'DISTINCT', 24 | 'ELSE', 25 | 'END', 26 | 'ENUM', 27 | 'ESCAPE', 28 | 'EXCEPT', 29 | 'EXCLUDE', 30 | 'EXISTS', 31 | 'EXTRACT', 32 | 'FALSE', 33 | 'FETCH', 34 | 'FOLLOWING', 35 | 'FOR', 36 | 'FROM', 37 | 'FULL', 38 | 'GROUP', 39 | 'GROUPING', 40 | 'GROUPS', 41 | 'HASH', 42 | 'HAVING', 43 | 'IF', 44 | 'IGNORE', 45 | 'IN', 46 | 'INNER', 47 | 'INTERSECT', 48 | 'INTO', 49 | 'IS', 50 | 'JOIN', 51 | 'LATERAL', 52 | 'LEFT', 53 | 'LIMIT', 54 | 'LOOKUP', 55 | 'MERGE', 56 | 'NATURAL', 57 | 'NEW', 58 | 'NO', 59 | 'NOT', 60 | 'NULL', 61 | 'NULLS', 62 | 'OF', 63 | 'ON', 64 | 'OR', 65 | 'ORDER', 66 | 'OUTER', 67 | 'OVER', 68 | 'PARTITION', 69 | 'PRECEDING', 70 | 'PROTO', 71 | 'RANGE', 72 | 'RECURSIVE', 73 | 'RESPECT', 74 | 'RIGHT', 75 | 'ROLLUP', 76 | 'ROWS', 77 | 'SELECT', 78 | 'SET', 79 | 'SOME', 80 | 'TABLE', 81 | 'TABLESAMPLE', 82 | 'THEN', 83 | 'TO', 84 | 'TREAT', 85 | 'TRUE', 86 | 'UNBOUNDED', 87 | 'UNION', 88 | 'UNNEST', 89 | 'USING', 90 | 'WHEN', 91 | 'WHERE', 92 | 'WINDOW', 93 | 'WITH', 94 | 'WITHIN', 95 | 96 | // misc 97 | 'SAFE', 98 | 99 | // https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language 100 | 'LIKE', // CREATE TABLE LIKE 101 | 'COPY', // CREATE TABLE COPY 102 | 'CLONE', // CREATE TABLE CLONE 103 | 'IN', 104 | 'OUT', 105 | 'INOUT', 106 | 'RETURNS', 107 | 'LANGUAGE', 108 | 'CASCADE', 109 | 'RESTRICT', 110 | 'DETERMINISTIC', 111 | ]; 112 | 113 | export const dataTypes: string[] = [ 114 | // https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types 115 | 'ARRAY', // parametric, ARRAY 116 | 'BOOL', 117 | 'BYTES', // parameterised, BYTES(Length) 118 | 'DATE', 119 | 'DATETIME', 120 | 'GEOGRAPHY', 121 | 'INTERVAL', 122 | 'INT64', 123 | 'INT', 124 | 'SMALLINT', 125 | 'INTEGER', 126 | 'BIGINT', 127 | 'TINYINT', 128 | 'BYTEINT', 129 | 'NUMERIC', // parameterised, NUMERIC(Precision[, Scale]) 130 | 'DECIMAL', // parameterised, DECIMAL(Precision[, Scale]) 131 | 'BIGNUMERIC', // parameterised, BIGNUMERIC(Precision[, Scale]) 132 | 'BIGDECIMAL', // parameterised, BIGDECIMAL(Precision[, Scale]) 133 | 'FLOAT64', 134 | 'STRING', // parameterised, STRING(Length) 135 | 'STRUCT', // parametric, STRUCT 136 | 'TIME', 137 | 'TIMEZONE', 138 | ]; 139 | -------------------------------------------------------------------------------- /src/languages/duckdb/duckdb.keywords.ts: -------------------------------------------------------------------------------- 1 | export const keywords: string[] = [ 2 | // Keywords from DuckDB: 3 | // SELECT upper(keyword_name) 4 | // FROM duckdb_keywords() 5 | // WHERE keyword_category = 'reserved' 6 | // ORDER BY keyword_name 7 | 'ALL', 8 | 'ANALYSE', 9 | 'ANALYZE', 10 | 'AND', 11 | 'ANY', 12 | 'AS', 13 | 'ASC', 14 | 'ATTACH', 15 | 'ASYMMETRIC', 16 | 'BOTH', 17 | 'CASE', 18 | 'CAST', 19 | 'CHECK', 20 | 'COLLATE', 21 | 'COLUMN', 22 | 'CONSTRAINT', 23 | 'CREATE', 24 | 'DEFAULT', 25 | 'DEFERRABLE', 26 | 'DESC', 27 | 'DESCRIBE', 28 | 'DETACH', 29 | 'DISTINCT', 30 | 'DO', 31 | 'ELSE', 32 | 'END', 33 | 'EXCEPT', 34 | 'FALSE', 35 | 'FETCH', 36 | 'FOR', 37 | 'FOREIGN', 38 | 'FROM', 39 | 'GRANT', 40 | 'GROUP', 41 | 'HAVING', 42 | 'IN', 43 | 'INITIALLY', 44 | 'INTERSECT', 45 | 'INTO', 46 | 'LATERAL', 47 | 'LEADING', 48 | 'LIMIT', 49 | 'NOT', 50 | 'NULL', 51 | 'OFFSET', 52 | 'ON', 53 | 'ONLY', 54 | 'OR', 55 | 'ORDER', 56 | 'PIVOT', 57 | 'PIVOT_LONGER', 58 | 'PIVOT_WIDER', 59 | 'PLACING', 60 | 'PRIMARY', 61 | 'REFERENCES', 62 | 'RETURNING', 63 | 'SELECT', 64 | 'SHOW', 65 | 'SOME', 66 | 'SUMMARIZE', 67 | 'SYMMETRIC', 68 | 'TABLE', 69 | 'THEN', 70 | 'TO', 71 | 'TRAILING', 72 | 'TRUE', 73 | 'UNION', 74 | 'UNIQUE', 75 | 'UNPIVOT', 76 | 'USING', 77 | 'VARIADIC', 78 | 'WHEN', 79 | 'WHERE', 80 | 'WINDOW', 81 | 'WITH', 82 | ]; 83 | 84 | export const dataTypes: string[] = [ 85 | // Types from DuckDB: 86 | // SELECT DISTINCT upper(type_name) 87 | // FROM duckdb_types() 88 | // ORDER BY type_name 89 | 'ARRAY', 90 | 'BIGINT', 91 | 'BINARY', 92 | 'BIT', 93 | 'BITSTRING', 94 | 'BLOB', 95 | 'BOOL', 96 | 'BOOLEAN', 97 | 'BPCHAR', 98 | 'BYTEA', 99 | 'CHAR', 100 | 'DATE', 101 | 'DATETIME', 102 | 'DEC', 103 | 'DECIMAL', 104 | 'DOUBLE', 105 | 'ENUM', 106 | 'FLOAT', 107 | 'FLOAT4', 108 | 'FLOAT8', 109 | 'GUID', 110 | 'HUGEINT', 111 | 'INET', 112 | 'INT', 113 | 'INT1', 114 | 'INT128', 115 | 'INT16', 116 | 'INT2', 117 | 'INT32', 118 | 'INT4', 119 | 'INT64', 120 | 'INT8', 121 | 'INTEGER', 122 | 'INTEGRAL', 123 | 'INTERVAL', 124 | 'JSON', 125 | 'LIST', 126 | 'LOGICAL', 127 | 'LONG', 128 | 'MAP', 129 | // 'NULL' is a keyword 130 | 'NUMERIC', 131 | 'NVARCHAR', 132 | 'OID', 133 | 'REAL', 134 | 'ROW', 135 | 'SHORT', 136 | 'SIGNED', 137 | 'SMALLINT', 138 | 'STRING', 139 | 'STRUCT', 140 | 'TEXT', 141 | 'TIME', 142 | 'TIMESTAMP_MS', 143 | 'TIMESTAMP_NS', 144 | 'TIMESTAMP_S', 145 | 'TIMESTAMP_US', 146 | 'TIMESTAMP', 147 | 'TIMESTAMPTZ', 148 | 'TIMETZ', 149 | 'TINYINT', 150 | 'UBIGINT', 151 | 'UHUGEINT', 152 | 'UINT128', 153 | 'UINT16', 154 | 'UINT32', 155 | 'UINT64', 156 | 'UINT8', 157 | 'UINTEGER', 158 | 'UNION', 159 | 'USMALLINT', 160 | 'UTINYINT', 161 | 'UUID', 162 | 'VARBINARY', 163 | 'VARCHAR', 164 | ]; 165 | -------------------------------------------------------------------------------- /src/languages/hive/hive.formatter.ts: -------------------------------------------------------------------------------- 1 | import { DialectOptions } from '../../dialect.js'; 2 | import { expandPhrases } from '../../expandPhrases.js'; 3 | import { functions } from './hive.functions.js'; 4 | import { dataTypes, keywords } from './hive.keywords.js'; 5 | 6 | const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT]']); 7 | 8 | const reservedClauses = expandPhrases([ 9 | // queries 10 | 'WITH', 11 | 'FROM', 12 | 'WHERE', 13 | 'GROUP BY', 14 | 'HAVING', 15 | 'WINDOW', 16 | 'PARTITION BY', 17 | 'ORDER BY', 18 | 'SORT BY', 19 | 'CLUSTER BY', 20 | 'DISTRIBUTE BY', 21 | 'LIMIT', 22 | // Data manipulation 23 | // - insert: 24 | // Hive does not actually support plain INSERT INTO, only INSERT INTO TABLE 25 | // but it's a nuisance to not support it, as all other dialects do. 26 | 'INSERT INTO [TABLE]', 27 | 'VALUES', 28 | // - update: 29 | 'SET', 30 | // - merge: 31 | 'MERGE INTO', 32 | 'WHEN [NOT] MATCHED [THEN]', 33 | 'UPDATE SET', 34 | 'INSERT [VALUES]', 35 | // - insert overwrite directory: 36 | // https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DML#LanguageManualDML-Writingdataintothefilesystemfromqueries 37 | 'INSERT OVERWRITE [LOCAL] DIRECTORY', 38 | // - load: 39 | // https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DML#LanguageManualDML-Loadingfilesintotables 40 | 'LOAD DATA [LOCAL] INPATH', 41 | '[OVERWRITE] INTO TABLE', 42 | ]); 43 | 44 | const standardOnelineClauses = expandPhrases([ 45 | 'CREATE [TEMPORARY] [EXTERNAL] TABLE [IF NOT EXISTS]', 46 | ]); 47 | 48 | const tabularOnelineClauses = expandPhrases([ 49 | // - create: 50 | 'CREATE [MATERIALIZED] VIEW [IF NOT EXISTS]', 51 | // - update: 52 | 'UPDATE', 53 | // - delete: 54 | 'DELETE FROM', 55 | // - drop table: 56 | 'DROP TABLE [IF EXISTS]', 57 | // - alter table: 58 | 'ALTER TABLE', 59 | 'RENAME TO', 60 | // - truncate: 61 | 'TRUNCATE [TABLE]', 62 | // other 63 | 'ALTER', 64 | 'CREATE', 65 | 'USE', 66 | 'DESCRIBE', 67 | 'DROP', 68 | 'FETCH', 69 | 'SHOW', 70 | 'STORED AS', 71 | 'STORED BY', 72 | 'ROW FORMAT', 73 | ]); 74 | 75 | const reservedSetOperations = expandPhrases(['UNION [ALL | DISTINCT]']); 76 | 77 | const reservedJoins = expandPhrases([ 78 | 'JOIN', 79 | '{LEFT | RIGHT | FULL} [OUTER] JOIN', 80 | '{INNER | CROSS} JOIN', 81 | // non-standard joins 82 | 'LEFT SEMI JOIN', 83 | ]); 84 | 85 | const reservedPhrases = expandPhrases(['{ROWS | RANGE} BETWEEN']); 86 | 87 | // https://cwiki.apache.org/confluence/display/Hive/LanguageManual 88 | export const hive: DialectOptions = { 89 | name: 'hive', 90 | tokenizerOptions: { 91 | reservedSelect, 92 | reservedClauses: [...reservedClauses, ...standardOnelineClauses, ...tabularOnelineClauses], 93 | reservedSetOperations, 94 | reservedJoins, 95 | reservedPhrases, 96 | reservedKeywords: keywords, 97 | reservedDataTypes: dataTypes, 98 | reservedFunctionNames: functions, 99 | extraParens: ['[]'], 100 | stringTypes: ['""-bs', "''-bs"], 101 | identTypes: ['``'], 102 | variableTypes: [{ quote: '{}', prefixes: ['$'], requirePrefix: true }], 103 | operators: ['%', '~', '^', '|', '&', '<=>', '==', '!', '||'], 104 | }, 105 | formatOptions: { 106 | onelineClauses: [...standardOnelineClauses, ...tabularOnelineClauses], 107 | tabularOnelineClauses, 108 | }, 109 | }; 110 | -------------------------------------------------------------------------------- /src/languages/hive/hive.functions.ts: -------------------------------------------------------------------------------- 1 | export const functions: string[] = [ 2 | // https://cwiki.apache.org/confluence/display/Hive/LanguageManual+UDF 3 | // math 4 | 'ABS', 5 | 'ACOS', 6 | 'ASIN', 7 | 'ATAN', 8 | 'BIN', 9 | 'BROUND', 10 | 'CBRT', 11 | 'CEIL', 12 | 'CEILING', 13 | 'CONV', 14 | 'COS', 15 | 'DEGREES', 16 | // 'E', 17 | 'EXP', 18 | 'FACTORIAL', 19 | 'FLOOR', 20 | 'GREATEST', 21 | 'HEX', 22 | 'LEAST', 23 | 'LN', 24 | 'LOG', 25 | 'LOG10', 26 | 'LOG2', 27 | 'NEGATIVE', 28 | 'PI', 29 | 'PMOD', 30 | 'POSITIVE', 31 | 'POW', 32 | 'POWER', 33 | 'RADIANS', 34 | 'RAND', 35 | 'ROUND', 36 | 'SHIFTLEFT', 37 | 'SHIFTRIGHT', 38 | 'SHIFTRIGHTUNSIGNED', 39 | 'SIGN', 40 | 'SIN', 41 | 'SQRT', 42 | 'TAN', 43 | 'UNHEX', 44 | 'WIDTH_BUCKET', 45 | 46 | // array 47 | 'ARRAY_CONTAINS', 48 | 'MAP_KEYS', 49 | 'MAP_VALUES', 50 | 'SIZE', 51 | 'SORT_ARRAY', 52 | 53 | // conversion 54 | 'BINARY', 55 | 'CAST', 56 | 57 | // date 58 | 'ADD_MONTHS', 59 | 'DATE', 60 | 'DATE_ADD', 61 | 'DATE_FORMAT', 62 | 'DATE_SUB', 63 | 'DATEDIFF', 64 | 'DAY', 65 | 'DAYNAME', 66 | 'DAYOFMONTH', 67 | 'DAYOFYEAR', 68 | 'EXTRACT', 69 | 'FROM_UNIXTIME', 70 | 'FROM_UTC_TIMESTAMP', 71 | 'HOUR', 72 | 'LAST_DAY', 73 | 'MINUTE', 74 | 'MONTH', 75 | 'MONTHS_BETWEEN', 76 | 'NEXT_DAY', 77 | 'QUARTER', 78 | 'SECOND', 79 | 'TIMESTAMP', 80 | 'TO_DATE', 81 | 'TO_UTC_TIMESTAMP', 82 | 'TRUNC', 83 | 'UNIX_TIMESTAMP', 84 | 'WEEKOFYEAR', 85 | 'YEAR', 86 | 87 | // conditional 88 | 'ASSERT_TRUE', 89 | 'COALESCE', 90 | 'IF', 91 | 'ISNOTNULL', 92 | 'ISNULL', 93 | 'NULLIF', 94 | 'NVL', 95 | 96 | // string 97 | 'ASCII', 98 | 'BASE64', 99 | 'CHARACTER_LENGTH', 100 | 'CHR', 101 | 'CONCAT', 102 | 'CONCAT_WS', 103 | 'CONTEXT_NGRAMS', 104 | 'DECODE', 105 | 'ELT', 106 | 'ENCODE', 107 | 'FIELD', 108 | 'FIND_IN_SET', 109 | 'FORMAT_NUMBER', 110 | 'GET_JSON_OBJECT', 111 | 'IN_FILE', 112 | 'INITCAP', 113 | 'INSTR', 114 | 'LCASE', 115 | 'LENGTH', 116 | 'LEVENSHTEIN', 117 | 'LOCATE', 118 | 'LOWER', 119 | 'LPAD', 120 | 'LTRIM', 121 | 'NGRAMS', 122 | 'OCTET_LENGTH', 123 | 'PARSE_URL', 124 | 'PRINTF', 125 | 'QUOTE', 126 | 'REGEXP_EXTRACT', 127 | 'REGEXP_REPLACE', 128 | 'REPEAT', 129 | 'REVERSE', 130 | 'RPAD', 131 | 'RTRIM', 132 | 'SENTENCES', 133 | 'SOUNDEX', 134 | 'SPACE', 135 | 'SPLIT', 136 | 'STR_TO_MAP', 137 | 'SUBSTR', 138 | 'SUBSTRING', 139 | 'TRANSLATE', 140 | 'TRIM', 141 | 'UCASE', 142 | 'UNBASE64', 143 | 'UPPER', 144 | 145 | // masking 146 | 'MASK', 147 | 'MASK_FIRST_N', 148 | 'MASK_HASH', 149 | 'MASK_LAST_N', 150 | 'MASK_SHOW_FIRST_N', 151 | 'MASK_SHOW_LAST_N', 152 | 153 | // misc 154 | 'AES_DECRYPT', 155 | 'AES_ENCRYPT', 156 | 'CRC32', 157 | 'CURRENT_DATABASE', 158 | 'CURRENT_USER', 159 | 'HASH', 160 | 'JAVA_METHOD', 161 | 'LOGGED_IN_USER', 162 | 'MD5', 163 | 'REFLECT', 164 | 'SHA', 165 | 'SHA1', 166 | 'SHA2', 167 | 'SURROGATE_KEY', 168 | 'VERSION', 169 | 170 | // aggregate 171 | 'AVG', 172 | 'COLLECT_LIST', 173 | 'COLLECT_SET', 174 | 'CORR', 175 | 'COUNT', 176 | 'COVAR_POP', 177 | 'COVAR_SAMP', 178 | 'HISTOGRAM_NUMERIC', 179 | 'MAX', 180 | 'MIN', 181 | 'NTILE', 182 | 'PERCENTILE', 183 | 'PERCENTILE_APPROX', 184 | 'REGR_AVGX', 185 | 'REGR_AVGY', 186 | 'REGR_COUNT', 187 | 'REGR_INTERCEPT', 188 | 'REGR_R2', 189 | 'REGR_SLOPE', 190 | 'REGR_SXX', 191 | 'REGR_SXY', 192 | 'REGR_SYY', 193 | 'STDDEV_POP', 194 | 'STDDEV_SAMP', 195 | 'SUM', 196 | 'VAR_POP', 197 | 'VAR_SAMP', 198 | 'VARIANCE', 199 | 200 | // table 201 | 'EXPLODE', 202 | 'INLINE', 203 | 'JSON_TUPLE', 204 | 'PARSE_URL_TUPLE', 205 | 'POSEXPLODE', 206 | 'STACK', 207 | 208 | // https://cwiki.apache.org/confluence/display/Hive/LanguageManual+WindowingAndAnalytics 209 | 'LEAD', 210 | 'LAG', 211 | 'FIRST_VALUE', 212 | 'LAST_VALUE', 213 | 'RANK', 214 | 'ROW_NUMBER', 215 | 'DENSE_RANK', 216 | 'CUME_DIST', 217 | 'PERCENT_RANK', 218 | 'NTILE', 219 | ]; 220 | -------------------------------------------------------------------------------- /src/languages/mariadb/likeMariaDb.ts: -------------------------------------------------------------------------------- 1 | import { EOF_TOKEN, isToken, Token, TokenType } from '../../lexer/token.js'; 2 | 3 | // Shared functionality used by all MariaDB-like SQL dialects. 4 | 5 | export function postProcess(tokens: Token[]) { 6 | return tokens.map((token, i) => { 7 | const nextToken = tokens[i + 1] || EOF_TOKEN; 8 | if (isToken.SET(token) && nextToken.text === '(') { 9 | // This is SET datatype, not SET statement 10 | return { ...token, type: TokenType.RESERVED_FUNCTION_NAME }; 11 | } 12 | const prevToken = tokens[i - 1] || EOF_TOKEN; 13 | if (isToken.VALUES(token) && prevToken.text === '=') { 14 | // This is VALUES() function, not VALUES clause 15 | return { ...token, type: TokenType.RESERVED_FUNCTION_NAME }; 16 | } 17 | return token; 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/languages/n1ql/n1ql.formatter.ts: -------------------------------------------------------------------------------- 1 | import { DialectOptions } from '../../dialect.js'; 2 | import { expandPhrases } from '../../expandPhrases.js'; 3 | import { functions } from './n1ql.functions.js'; 4 | import { dataTypes, keywords } from './n1ql.keywords.js'; 5 | 6 | const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT]']); 7 | 8 | const reservedClauses = expandPhrases([ 9 | // queries 10 | 'WITH', 11 | 'FROM', 12 | 'WHERE', 13 | 'GROUP BY', 14 | 'HAVING', 15 | 'WINDOW', 16 | 'PARTITION BY', 17 | 'ORDER BY', 18 | 'LIMIT', 19 | 'OFFSET', 20 | // Data manipulation 21 | // - insert: 22 | 'INSERT INTO', 23 | 'VALUES', 24 | // - update: 25 | 'SET', 26 | // - merge: 27 | 'MERGE INTO', 28 | 'WHEN [NOT] MATCHED THEN', 29 | 'UPDATE SET', 30 | 'INSERT', 31 | // other 32 | 'NEST', 33 | 'UNNEST', 34 | 'RETURNING', 35 | ]); 36 | 37 | const onelineClauses = expandPhrases([ 38 | // - update: 39 | 'UPDATE', 40 | // - delete: 41 | 'DELETE FROM', 42 | // - set schema: 43 | 'SET SCHEMA', 44 | // https://docs.couchbase.com/server/current/n1ql/n1ql-language-reference/reservedwords.html 45 | 'ADVISE', 46 | 'ALTER INDEX', 47 | 'BEGIN TRANSACTION', 48 | 'BUILD INDEX', 49 | 'COMMIT TRANSACTION', 50 | 'CREATE COLLECTION', 51 | 'CREATE FUNCTION', 52 | 'CREATE INDEX', 53 | 'CREATE PRIMARY INDEX', 54 | 'CREATE SCOPE', 55 | 'DROP COLLECTION', 56 | 'DROP FUNCTION', 57 | 'DROP INDEX', 58 | 'DROP PRIMARY INDEX', 59 | 'DROP SCOPE', 60 | 'EXECUTE', 61 | 'EXECUTE FUNCTION', 62 | 'EXPLAIN', 63 | 'GRANT', 64 | 'INFER', 65 | 'PREPARE', 66 | 'REVOKE', 67 | 'ROLLBACK TRANSACTION', 68 | 'SAVEPOINT', 69 | 'SET TRANSACTION', 70 | 'UPDATE STATISTICS', 71 | 'UPSERT', 72 | // other 73 | 'LET', 74 | 'SET CURRENT SCHEMA', 75 | 'SHOW', 76 | 'USE [PRIMARY] KEYS', 77 | ]); 78 | 79 | const reservedSetOperations = expandPhrases(['UNION [ALL]', 'EXCEPT [ALL]', 'INTERSECT [ALL]']); 80 | 81 | const reservedJoins = expandPhrases(['JOIN', '{LEFT | RIGHT} [OUTER] JOIN', 'INNER JOIN']); 82 | 83 | const reservedPhrases = expandPhrases(['{ROWS | RANGE | GROUPS} BETWEEN']); 84 | 85 | // For reference: http://docs.couchbase.com.s3-website-us-west-1.amazonaws.com/server/6.0/n1ql/n1ql-language-reference/index.html 86 | export const n1ql: DialectOptions = { 87 | name: 'n1ql', 88 | tokenizerOptions: { 89 | reservedSelect, 90 | reservedClauses: [...reservedClauses, ...onelineClauses], 91 | reservedSetOperations, 92 | reservedJoins, 93 | reservedPhrases, 94 | supportsXor: true, 95 | reservedKeywords: keywords, 96 | reservedDataTypes: dataTypes, 97 | reservedFunctionNames: functions, 98 | // NOTE: single quotes are actually not supported in N1QL, 99 | // but we support them anyway as all other SQL dialects do, 100 | // which simplifies writing tests that are shared between all dialects. 101 | stringTypes: ['""-bs', "''-bs"], 102 | identTypes: ['``'], 103 | extraParens: ['[]', '{}'], 104 | paramTypes: { positional: true, numbered: ['$'], named: ['$'] }, 105 | lineCommentTypes: ['#', '--'], 106 | operators: ['%', '==', ':', '||'], 107 | }, 108 | formatOptions: { 109 | onelineClauses, 110 | }, 111 | }; 112 | -------------------------------------------------------------------------------- /src/languages/snowflake/snowflake.keywords.ts: -------------------------------------------------------------------------------- 1 | export const keywords: string[] = [ 2 | // https://docs.snowflake.com/en/sql-reference/reserved-keywords.html 3 | // 4 | // run in console on this page: $x('//tbody/tr/*[1]/p/text()').map(x => x.nodeValue) 5 | 'ACCOUNT', 6 | 'ALL', 7 | 'ALTER', 8 | 'AND', 9 | 'ANY', 10 | 'AS', 11 | 'BETWEEN', 12 | 'BY', 13 | 'CASE', 14 | 'CAST', 15 | 'CHECK', 16 | 'COLUMN', 17 | 'CONNECT', 18 | 'CONNECTION', 19 | 'CONSTRAINT', 20 | 'CREATE', 21 | 'CROSS', 22 | 'CURRENT', 23 | 'CURRENT_DATE', 24 | 'CURRENT_TIME', 25 | 'CURRENT_TIMESTAMP', 26 | 'CURRENT_USER', 27 | 'DATABASE', 28 | 'DELETE', 29 | 'DISTINCT', 30 | 'DROP', 31 | 'ELSE', 32 | 'EXISTS', 33 | 'FALSE', 34 | 'FOLLOWING', 35 | 'FOR', 36 | 'FROM', 37 | 'FULL', 38 | 'GRANT', 39 | 'GROUP', 40 | 'GSCLUSTER', 41 | 'HAVING', 42 | 'ILIKE', 43 | 'IN', 44 | 'INCREMENT', 45 | 'INNER', 46 | 'INSERT', 47 | 'INTERSECT', 48 | 'INTO', 49 | 'IS', 50 | 'ISSUE', 51 | 'JOIN', 52 | 'LATERAL', 53 | 'LEFT', 54 | 'LIKE', 55 | 'LOCALTIME', 56 | 'LOCALTIMESTAMP', 57 | 'MINUS', 58 | 'NATURAL', 59 | 'NOT', 60 | 'NULL', 61 | 'OF', 62 | 'ON', 63 | 'OR', 64 | 'ORDER', 65 | 'ORGANIZATION', 66 | 'QUALIFY', 67 | 'REGEXP', 68 | 'REVOKE', 69 | 'RIGHT', 70 | 'RLIKE', 71 | 'ROW', 72 | 'ROWS', 73 | 'SAMPLE', 74 | 'SCHEMA', 75 | 'SELECT', 76 | 'SET', 77 | 'SOME', 78 | 'START', 79 | 'TABLE', 80 | 'TABLESAMPLE', 81 | 'THEN', 82 | 'TO', 83 | 'TRIGGER', 84 | 'TRUE', 85 | 'TRY_CAST', 86 | 'UNION', 87 | 'UNIQUE', 88 | 'UPDATE', 89 | 'USING', 90 | 'VALUES', 91 | 'VIEW', 92 | 'WHEN', 93 | 'WHENEVER', 94 | 'WHERE', 95 | 'WITH', 96 | 97 | // These are definitely keywords, but haven't found a definite list in the docs 98 | 'COMMENT', 99 | ]; 100 | 101 | export const dataTypes: string[] = [ 102 | 'NUMBER', 103 | 'DECIMAL', 104 | 'NUMERIC', 105 | 'INT', 106 | 'INTEGER', 107 | 'BIGINT', 108 | 'SMALLINT', 109 | 'TINYINT', 110 | 'BYTEINT', 111 | 'FLOAT', 112 | 'FLOAT4', 113 | 'FLOAT8', 114 | 'DOUBLE', 115 | 'DOUBLE PRECISION', 116 | 'REAL', 117 | 'VARCHAR', 118 | 'CHAR', 119 | 'CHARACTER', 120 | 'STRING', 121 | 'TEXT', 122 | 'BINARY', 123 | 'VARBINARY', 124 | 'BOOLEAN', 125 | 'DATE', 126 | 'DATETIME', 127 | 'TIME', 128 | 'TIMESTAMP', 129 | 'TIMESTAMP_LTZ', 130 | 'TIMESTAMP_NTZ', 131 | 'TIMESTAMP', 132 | 'TIMESTAMP_TZ', 133 | 'VARIANT', 134 | 'OBJECT', 135 | 'ARRAY', 136 | 'GEOGRAPHY', 137 | 'GEOMETRY', 138 | ]; 139 | -------------------------------------------------------------------------------- /src/languages/sql/sql.formatter.ts: -------------------------------------------------------------------------------- 1 | import { DialectOptions } from '../../dialect.js'; 2 | import { expandPhrases } from '../../expandPhrases.js'; 3 | import { functions } from './sql.functions.js'; 4 | import { dataTypes, keywords } from './sql.keywords.js'; 5 | 6 | const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT]']); 7 | 8 | const reservedClauses = expandPhrases([ 9 | // queries 10 | 'WITH [RECURSIVE]', 11 | 'FROM', 12 | 'WHERE', 13 | 'GROUP BY [ALL | DISTINCT]', 14 | 'HAVING', 15 | 'WINDOW', 16 | 'PARTITION BY', 17 | 'ORDER BY', 18 | 'LIMIT', 19 | 'OFFSET', 20 | 'FETCH {FIRST | NEXT}', 21 | // Data manipulation 22 | // - insert: 23 | 'INSERT INTO', 24 | 'VALUES', 25 | // - update: 26 | 'SET', 27 | ]); 28 | 29 | const standardOnelineClauses = expandPhrases(['CREATE [GLOBAL TEMPORARY | LOCAL TEMPORARY] TABLE']); 30 | 31 | const tabularOnelineClauses = expandPhrases([ 32 | // - create: 33 | 'CREATE [RECURSIVE] VIEW', 34 | // - update: 35 | 'UPDATE', 36 | 'WHERE CURRENT OF', 37 | // - delete: 38 | 'DELETE FROM', 39 | // - drop table: 40 | 'DROP TABLE', 41 | // - alter table: 42 | 'ALTER TABLE', 43 | 'ADD COLUMN', 44 | 'DROP [COLUMN]', 45 | 'RENAME COLUMN', 46 | 'RENAME TO', 47 | 'ALTER [COLUMN]', 48 | '{SET | DROP} DEFAULT', // for alter column 49 | 'ADD SCOPE', // for alter column 50 | 'DROP SCOPE {CASCADE | RESTRICT}', // for alter column 51 | 'RESTART WITH', // for alter column 52 | // - truncate: 53 | 'TRUNCATE TABLE', 54 | // other 55 | 'SET SCHEMA', 56 | ]); 57 | 58 | const reservedSetOperations = expandPhrases([ 59 | 'UNION [ALL | DISTINCT]', 60 | 'EXCEPT [ALL | DISTINCT]', 61 | 'INTERSECT [ALL | DISTINCT]', 62 | ]); 63 | 64 | const reservedJoins = expandPhrases([ 65 | 'JOIN', 66 | '{LEFT | RIGHT | FULL} [OUTER] JOIN', 67 | '{INNER | CROSS} JOIN', 68 | 'NATURAL [INNER] JOIN', 69 | 'NATURAL {LEFT | RIGHT | FULL} [OUTER] JOIN', 70 | ]); 71 | 72 | const reservedPhrases = expandPhrases([ 73 | 'ON {UPDATE | DELETE} [SET NULL | SET DEFAULT]', 74 | '{ROWS | RANGE} BETWEEN', 75 | ]); 76 | 77 | export const sql: DialectOptions = { 78 | name: 'sql', 79 | tokenizerOptions: { 80 | reservedSelect, 81 | reservedClauses: [...reservedClauses, ...standardOnelineClauses, ...tabularOnelineClauses], 82 | reservedSetOperations, 83 | reservedJoins, 84 | reservedPhrases, 85 | reservedKeywords: keywords, 86 | reservedDataTypes: dataTypes, 87 | reservedFunctionNames: functions, 88 | stringTypes: [ 89 | { quote: "''-qq-bs", prefixes: ['N', 'U&'] }, 90 | { quote: "''-raw", prefixes: ['X'], requirePrefix: true }, 91 | ], 92 | identTypes: [`""-qq`, '``'], 93 | paramTypes: { positional: true }, 94 | operators: ['||'], 95 | }, 96 | formatOptions: { 97 | onelineClauses: [...standardOnelineClauses, ...tabularOnelineClauses], 98 | tabularOnelineClauses, 99 | }, 100 | }; 101 | -------------------------------------------------------------------------------- /src/languages/sql/sql.functions.ts: -------------------------------------------------------------------------------- 1 | export const functions: string[] = [ 2 | // https://jakewheat.github.io/sql-overview/sql-2008-foundation-grammar.html#_6_9_set_function_specification 3 | 'GROUPING', 4 | 5 | // https://jakewheat.github.io/sql-overview/sql-2008-foundation-grammar.html#_6_10_window_function 6 | 'RANK', 7 | 'DENSE_RANK', 8 | 'PERCENT_RANK', 9 | 'CUME_DIST', 10 | 'ROW_NUMBER', 11 | 12 | // https://jakewheat.github.io/sql-overview/sql-2008-foundation-grammar.html#_6_27_numeric_value_function 13 | 'POSITION', 14 | 'OCCURRENCES_REGEX', 15 | 'POSITION_REGEX', 16 | 'EXTRACT', 17 | 'CHAR_LENGTH', 18 | 'CHARACTER_LENGTH', 19 | 'OCTET_LENGTH', 20 | 'CARDINALITY', 21 | 'ABS', 22 | 'MOD', 23 | 'LN', 24 | 'EXP', 25 | 'POWER', 26 | 'SQRT', 27 | 'FLOOR', 28 | 'CEIL', 29 | 'CEILING', 30 | 'WIDTH_BUCKET', 31 | 32 | // https://jakewheat.github.io/sql-overview/sql-2008-foundation-grammar.html#_6_29_string_value_function 33 | 'SUBSTRING', 34 | 'SUBSTRING_REGEX', 35 | 'UPPER', 36 | 'LOWER', 37 | 'CONVERT', 38 | 'TRANSLATE', 39 | 'TRANSLATE_REGEX', 40 | 'TRIM', 41 | 'OVERLAY', 42 | 'NORMALIZE', 43 | 'SPECIFICTYPE', 44 | 45 | // https://jakewheat.github.io/sql-overview/sql-2008-foundation-grammar.html#_6_31_datetime_value_function 46 | 'CURRENT_DATE', 47 | 'CURRENT_TIME', 48 | 'LOCALTIME', 49 | 'CURRENT_TIMESTAMP', 50 | 'LOCALTIMESTAMP', 51 | 52 | // https://jakewheat.github.io/sql-overview/sql-2008-foundation-grammar.html#_6_38_multiset_value_function 53 | // SET serves multiple roles: a SET() function and a SET keyword e.g. in UPDATE table SET ... 54 | // multiset 55 | // 'SET', (disabled for now) 56 | 57 | // https://jakewheat.github.io/sql-overview/sql-2008-foundation-grammar.html#_10_9_aggregate_function 58 | 'COUNT', 59 | 'AVG', 60 | 'MAX', 61 | 'MIN', 62 | 'SUM', 63 | // 'EVERY', 64 | // 'ANY', 65 | // 'SOME', 66 | 'STDDEV_POP', 67 | 'STDDEV_SAMP', 68 | 'VAR_SAMP', 69 | 'VAR_POP', 70 | 'COLLECT', 71 | 'FUSION', 72 | 'INTERSECTION', 73 | 'COVAR_POP', 74 | 'COVAR_SAMP', 75 | 'CORR', 76 | 'REGR_SLOPE', 77 | 'REGR_INTERCEPT', 78 | 'REGR_COUNT', 79 | 'REGR_R2', 80 | 'REGR_AVGX', 81 | 'REGR_AVGY', 82 | 'REGR_SXX', 83 | 'REGR_SYY', 84 | 'REGR_SXY', 85 | 'PERCENTILE_CONT', 86 | 'PERCENTILE_DISC', 87 | 88 | // CAST is a pretty complex case, involving multiple forms: 89 | // - CAST(col AS int) 90 | // - CAST(...) WITH ... 91 | // - CAST FROM int 92 | // - CREATE CAST(mycol AS int) WITH ... 93 | 'CAST', 94 | 95 | // Shorthand functions to use in place of CASE expression 96 | 'COALESCE', 97 | 'NULLIF', 98 | 99 | // Non-standard functions that have widespread support 100 | 'ROUND', 101 | 'SIN', 102 | 'COS', 103 | 'TAN', 104 | 'ASIN', 105 | 'ACOS', 106 | 'ATAN', 107 | ]; 108 | -------------------------------------------------------------------------------- /src/languages/sqlite/sqlite.formatter.ts: -------------------------------------------------------------------------------- 1 | import { DialectOptions } from '../../dialect.js'; 2 | import { expandPhrases } from '../../expandPhrases.js'; 3 | import { functions } from './sqlite.functions.js'; 4 | import { dataTypes, keywords } from './sqlite.keywords.js'; 5 | 6 | const reservedSelect = expandPhrases(['SELECT [ALL | DISTINCT]']); 7 | 8 | const reservedClauses = expandPhrases([ 9 | // queries 10 | 'WITH [RECURSIVE]', 11 | 'FROM', 12 | 'WHERE', 13 | 'GROUP BY', 14 | 'HAVING', 15 | 'WINDOW', 16 | 'PARTITION BY', 17 | 'ORDER BY', 18 | 'LIMIT', 19 | 'OFFSET', 20 | // Data manipulation 21 | // - insert: 22 | 'INSERT [OR ABORT | OR FAIL | OR IGNORE | OR REPLACE | OR ROLLBACK] INTO', 23 | 'REPLACE INTO', 24 | 'VALUES', 25 | // - update: 26 | 'SET', 27 | ]); 28 | 29 | const standardOnelineClauses = expandPhrases(['CREATE [TEMPORARY | TEMP] TABLE [IF NOT EXISTS]']); 30 | 31 | const tabularOnelineClauses = expandPhrases([ 32 | // - create: 33 | 'CREATE [TEMPORARY | TEMP] VIEW [IF NOT EXISTS]', 34 | // - update: 35 | 'UPDATE [OR ABORT | OR FAIL | OR IGNORE | OR REPLACE | OR ROLLBACK]', 36 | // - insert: 37 | 'ON CONFLICT', 38 | // - delete: 39 | 'DELETE FROM', 40 | // - drop table: 41 | 'DROP TABLE [IF EXISTS]', 42 | // - alter table: 43 | 'ALTER TABLE', 44 | 'ADD [COLUMN]', 45 | 'DROP [COLUMN]', 46 | 'RENAME [COLUMN]', 47 | 'RENAME TO', 48 | // - set schema 49 | 'SET SCHEMA', 50 | ]); 51 | 52 | const reservedSetOperations = expandPhrases(['UNION [ALL]', 'EXCEPT', 'INTERSECT']); 53 | 54 | // joins - https://www.sqlite.org/syntax/join-operator.html 55 | const reservedJoins = expandPhrases([ 56 | 'JOIN', 57 | '{LEFT | RIGHT | FULL} [OUTER] JOIN', 58 | '{INNER | CROSS} JOIN', 59 | 'NATURAL [INNER] JOIN', 60 | 'NATURAL {LEFT | RIGHT | FULL} [OUTER] JOIN', 61 | ]); 62 | 63 | const reservedPhrases = expandPhrases([ 64 | 'ON {UPDATE | DELETE} [SET NULL | SET DEFAULT]', 65 | '{ROWS | RANGE | GROUPS} BETWEEN', 66 | 'DO UPDATE', 67 | ]); 68 | 69 | export const sqlite: DialectOptions = { 70 | name: 'sqlite', 71 | tokenizerOptions: { 72 | reservedSelect, 73 | reservedClauses: [...reservedClauses, ...standardOnelineClauses, ...tabularOnelineClauses], 74 | reservedSetOperations, 75 | reservedJoins, 76 | reservedPhrases, 77 | reservedKeywords: keywords, 78 | reservedDataTypes: dataTypes, 79 | reservedFunctionNames: functions, 80 | stringTypes: [ 81 | "''-qq", 82 | { quote: "''-raw", prefixes: ['X'], requirePrefix: true }, 83 | // Depending on context SQLite also supports double-quotes for strings, 84 | // and single-quotes for identifiers. 85 | ], 86 | identTypes: [`""-qq`, '``', '[]'], 87 | // https://www.sqlite.org/lang_expr.html#parameters 88 | paramTypes: { positional: true, numbered: ['?'], named: [':', '@', '$'] }, 89 | operators: ['%', '~', '&', '|', '<<', '>>', '==', '->', '->>', '||'], 90 | }, 91 | formatOptions: { 92 | onelineClauses: [...standardOnelineClauses, ...tabularOnelineClauses], 93 | tabularOnelineClauses, 94 | }, 95 | }; 96 | -------------------------------------------------------------------------------- /src/languages/sqlite/sqlite.functions.ts: -------------------------------------------------------------------------------- 1 | export const functions: string[] = [ 2 | // https://www.sqlite.org/lang_corefunc.html 3 | 'ABS', 4 | 'CHANGES', 5 | 'CHAR', 6 | 'COALESCE', 7 | 'FORMAT', 8 | 'GLOB', 9 | 'HEX', 10 | 'IFNULL', 11 | 'IIF', 12 | 'INSTR', 13 | 'LAST_INSERT_ROWID', 14 | 'LENGTH', 15 | 'LIKE', 16 | 'LIKELIHOOD', 17 | 'LIKELY', 18 | 'LOAD_EXTENSION', 19 | 'LOWER', 20 | 'LTRIM', 21 | 'NULLIF', 22 | 'PRINTF', 23 | 'QUOTE', 24 | 'RANDOM', 25 | 'RANDOMBLOB', 26 | 'REPLACE', 27 | 'ROUND', 28 | 'RTRIM', 29 | 'SIGN', 30 | 'SOUNDEX', 31 | 'SQLITE_COMPILEOPTION_GET', 32 | 'SQLITE_COMPILEOPTION_USED', 33 | 'SQLITE_OFFSET', 34 | 'SQLITE_SOURCE_ID', 35 | 'SQLITE_VERSION', 36 | 'SUBSTR', 37 | 'SUBSTRING', 38 | 'TOTAL_CHANGES', 39 | 'TRIM', 40 | 'TYPEOF', 41 | 'UNICODE', 42 | 'UNLIKELY', 43 | 'UPPER', 44 | 'ZEROBLOB', 45 | 46 | // https://www.sqlite.org/lang_aggfunc.html 47 | 'AVG', 48 | 'COUNT', 49 | 'GROUP_CONCAT', 50 | 'MAX', 51 | 'MIN', 52 | 'SUM', 53 | 'TOTAL', 54 | 55 | // https://www.sqlite.org/lang_datefunc.html 56 | 'DATE', 57 | 'TIME', 58 | 'DATETIME', 59 | 'JULIANDAY', 60 | 'UNIXEPOCH', 61 | 'STRFTIME', 62 | 63 | // https://www.sqlite.org/windowfunctions.html#biwinfunc 64 | 'row_number', 65 | 'rank', 66 | 'dense_rank', 67 | 'percent_rank', 68 | 'cume_dist', 69 | 'ntile', 70 | 'lag', 71 | 'lead', 72 | 'first_value', 73 | 'last_value', 74 | 'nth_value', 75 | 76 | // https://www.sqlite.org/lang_mathfunc.html 77 | 'ACOS', 78 | 'ACOSH', 79 | 'ASIN', 80 | 'ASINH', 81 | 'ATAN', 82 | 'ATAN2', 83 | 'ATANH', 84 | 'CEIL', 85 | 'CEILING', 86 | 'COS', 87 | 'COSH', 88 | 'DEGREES', 89 | 'EXP', 90 | 'FLOOR', 91 | 'LN', 92 | 'LOG', 93 | 'LOG', 94 | 'LOG10', 95 | 'LOG2', 96 | 'MOD', 97 | 'PI', 98 | 'POW', 99 | 'POWER', 100 | 'RADIANS', 101 | 'SIN', 102 | 'SINH', 103 | 'SQRT', 104 | 'TAN', 105 | 'TANH', 106 | 'TRUNC', 107 | 108 | // https://www.sqlite.org/json1.html 109 | 'JSON', 110 | 'JSON_ARRAY', 111 | 'JSON_ARRAY_LENGTH', 112 | 'JSON_ARRAY_LENGTH', 113 | 'JSON_EXTRACT', 114 | 'JSON_INSERT', 115 | 'JSON_OBJECT', 116 | 'JSON_PATCH', 117 | 'JSON_REMOVE', 118 | 'JSON_REPLACE', 119 | 'JSON_SET', 120 | 'JSON_TYPE', 121 | 'JSON_TYPE', 122 | 'JSON_VALID', 123 | 'JSON_QUOTE', 124 | 'JSON_GROUP_ARRAY', 125 | 'JSON_GROUP_OBJECT', 126 | 'JSON_EACH', 127 | 'JSON_TREE', 128 | 129 | // cast 130 | 'CAST', 131 | ]; 132 | -------------------------------------------------------------------------------- /src/languages/sqlite/sqlite.keywords.ts: -------------------------------------------------------------------------------- 1 | export const keywords: string[] = [ 2 | // https://www.sqlite.org/lang_keywords.html 3 | // Note: The keywords listed on that URL are not all reserved keywords. 4 | // We'll need to clean up this list to only include reserved keywords. 5 | 'ABORT', 6 | 'ACTION', 7 | 'ADD', 8 | 'AFTER', 9 | 'ALL', 10 | 'ALTER', 11 | 'AND', 12 | 'ARE', 13 | 'ALWAYS', 14 | 'ANALYZE', 15 | 'AS', 16 | 'ASC', 17 | 'ATTACH', 18 | 'AUTOINCREMENT', 19 | 'BEFORE', 20 | 'BEGIN', 21 | 'BETWEEN', 22 | 'BY', 23 | 'CASCADE', 24 | 'CASE', 25 | 'CAST', 26 | 'CHECK', 27 | 'COLLATE', 28 | 'COLUMN', 29 | 'COMMIT', 30 | 'CONFLICT', 31 | 'CONSTRAINT', 32 | 'CREATE', 33 | 'CROSS', 34 | 'CURRENT', 35 | 'CURRENT_DATE', 36 | 'CURRENT_TIME', 37 | 'CURRENT_TIMESTAMP', 38 | 'DATABASE', 39 | 'DEFAULT', 40 | 'DEFERRABLE', 41 | 'DEFERRED', 42 | 'DELETE', 43 | 'DESC', 44 | 'DETACH', 45 | 'DISTINCT', 46 | 'DO', 47 | 'DROP', 48 | 'EACH', 49 | 'ELSE', 50 | 'END', 51 | 'ESCAPE', 52 | 'EXCEPT', 53 | 'EXCLUDE', 54 | 'EXCLUSIVE', 55 | 'EXISTS', 56 | 'EXPLAIN', 57 | 'FAIL', 58 | 'FILTER', 59 | 'FIRST', 60 | 'FOLLOWING', 61 | 'FOR', 62 | 'FOREIGN', 63 | 'FROM', 64 | 'FULL', 65 | 'GENERATED', 66 | 'GLOB', 67 | 'GROUP', 68 | 'HAVING', 69 | 'IF', 70 | 'IGNORE', 71 | 'IMMEDIATE', 72 | 'IN', 73 | 'INDEX', 74 | 'INDEXED', 75 | 'INITIALLY', 76 | 'INNER', 77 | 'INSERT', 78 | 'INSTEAD', 79 | 'INTERSECT', 80 | 'INTO', 81 | 'IS', 82 | 'ISNULL', 83 | 'JOIN', 84 | 'KEY', 85 | 'LAST', 86 | 'LEFT', 87 | 'LIKE', 88 | 'LIMIT', 89 | 'MATCH', 90 | 'MATERIALIZED', 91 | 'NATURAL', 92 | 'NO', 93 | 'NOT', 94 | 'NOTHING', 95 | 'NOTNULL', 96 | 'NULL', 97 | 'NULLS', 98 | 'OF', 99 | 'OFFSET', 100 | 'ON', 101 | 'ONLY', 102 | 'OPEN', 103 | 'OR', 104 | 'ORDER', 105 | 'OTHERS', 106 | 'OUTER', 107 | 'OVER', 108 | 'PARTITION', 109 | 'PLAN', 110 | 'PRAGMA', 111 | 'PRECEDING', 112 | 'PRIMARY', 113 | 'QUERY', 114 | 'RAISE', 115 | 'RANGE', 116 | 'RECURSIVE', 117 | 'REFERENCES', 118 | 'REGEXP', 119 | 'REINDEX', 120 | 'RELEASE', 121 | 'RENAME', 122 | 'REPLACE', 123 | 'RESTRICT', 124 | 'RETURNING', 125 | 'RIGHT', 126 | 'ROLLBACK', 127 | 'ROW', 128 | 'ROWS', 129 | 'SAVEPOINT', 130 | 'SELECT', 131 | 'SET', 132 | 'TABLE', 133 | 'TEMP', 134 | 'TEMPORARY', 135 | 'THEN', 136 | 'TIES', 137 | 'TO', 138 | 'TRANSACTION', 139 | 'TRIGGER', 140 | 'UNBOUNDED', 141 | 'UNION', 142 | 'UNIQUE', 143 | 'UPDATE', 144 | 'USING', 145 | 'VACUUM', 146 | 'VALUES', 147 | 'VIEW', 148 | 'VIRTUAL', 149 | 'WHEN', 150 | 'WHERE', 151 | 'WINDOW', 152 | 'WITH', 153 | 'WITHOUT', 154 | ]; 155 | 156 | export const dataTypes: string[] = [ 157 | // SQLite allows any word as a data type, e.g. CREATE TABLE foo (col1 madeupname(123)); 158 | // Here we just list some common ones as SQL Formatter 159 | // is only able to detect a predefined list of data types. 160 | // https://www.sqlite.org/stricttables.html 161 | // https://www.sqlite.org/datatype3.html 162 | 'ANY', 163 | 'ARRAY', 164 | 'BLOB', 165 | 'CHARACTER', 166 | 'DECIMAL', 167 | 'INT', 168 | 'INTEGER', 169 | 'NATIVE CHARACTER', 170 | 'NCHAR', 171 | 'NUMERIC', 172 | 'NVARCHAR', 173 | 'REAL', 174 | 'TEXT', 175 | 'VARCHAR', 176 | 'VARYING CHARACTER', 177 | ]; 178 | -------------------------------------------------------------------------------- /src/languages/transactsql/transactsql.keywords.ts: -------------------------------------------------------------------------------- 1 | export const keywords: string[] = [ 2 | // https://docs.microsoft.com/en-us/sql/t-sql/language-elements/reserved-keywords-transact-sql?view=sql-server-ver15 3 | // standard 4 | 'ADD', 5 | 'ALL', 6 | 'ALTER', 7 | 'AND', 8 | 'ANY', 9 | 'AS', 10 | 'ASC', 11 | 'AUTHORIZATION', 12 | 'BACKUP', 13 | 'BEGIN', 14 | 'BETWEEN', 15 | 'BREAK', 16 | 'BROWSE', 17 | 'BULK', 18 | 'BY', 19 | 'CASCADE', 20 | 'CHECK', 21 | 'CHECKPOINT', 22 | 'CLOSE', 23 | 'CLUSTERED', 24 | 'COALESCE', 25 | 'COLLATE', 26 | 'COLUMN', 27 | 'COMMIT', 28 | 'COMPUTE', 29 | 'CONSTRAINT', 30 | 'CONTAINS', 31 | 'CONTAINSTABLE', 32 | 'CONTINUE', 33 | 'CONVERT', 34 | 'CREATE', 35 | 'CROSS', 36 | 'CURRENT', 37 | 'CURRENT_DATE', 38 | 'CURRENT_TIME', 39 | 'CURRENT_TIMESTAMP', 40 | 'CURRENT_USER', 41 | 'CURSOR', 42 | 'DATABASE', 43 | 'DBCC', 44 | 'DEALLOCATE', 45 | 'DECLARE', 46 | 'DEFAULT', 47 | 'DELETE', 48 | 'DENY', 49 | 'DESC', 50 | 'DISK', 51 | 'DISTINCT', 52 | 'DISTRIBUTED', 53 | 'DROP', 54 | 'DUMP', 55 | 'ERRLVL', 56 | 'ESCAPE', 57 | 'EXEC', 58 | 'EXECUTE', 59 | 'EXISTS', 60 | 'EXIT', 61 | 'EXTERNAL', 62 | 'FETCH', 63 | 'FILE', 64 | 'FILLFACTOR', 65 | 'FOR', 66 | 'FOREIGN', 67 | 'FREETEXT', 68 | 'FREETEXTTABLE', 69 | 'FROM', 70 | 'FULL', 71 | 'FUNCTION', 72 | 'GOTO', 73 | 'GRANT', 74 | 'GROUP', 75 | 'HAVING', 76 | 'HOLDLOCK', 77 | 'IDENTITY', 78 | 'IDENTITYCOL', 79 | 'IDENTITY_INSERT', 80 | 'IF', 81 | 'IN', 82 | 'INDEX', 83 | 'INNER', 84 | 'INSERT', 85 | 'INTERSECT', 86 | 'INTO', 87 | 'IS', 88 | 'JOIN', 89 | 'KEY', 90 | 'KILL', 91 | 'LEFT', 92 | 'LIKE', 93 | 'LINENO', 94 | 'LOAD', 95 | 'MERGE', 96 | 'NOCHECK', 97 | 'NONCLUSTERED', 98 | 'NOT', 99 | 'NULL', 100 | 'NULLIF', 101 | 'OF', 102 | 'OFF', 103 | 'OFFSETS', 104 | 'ON', 105 | 'OPEN', 106 | 'OPENDATASOURCE', 107 | 'OPENQUERY', 108 | 'OPENROWSET', 109 | 'OPENXML', 110 | 'OPTION', 111 | 'OR', 112 | 'ORDER', 113 | 'OUTER', 114 | 'OVER', 115 | 'PERCENT', 116 | 'PIVOT', 117 | 'PLAN', 118 | 'PRIMARY', 119 | 'PRINT', 120 | 'PROC', 121 | 'PROCEDURE', 122 | 'PUBLIC', 123 | 'RAISERROR', 124 | 'READ', 125 | 'READTEXT', 126 | 'RECONFIGURE', 127 | 'REFERENCES', 128 | 'REPLICATION', 129 | 'RESTORE', 130 | 'RESTRICT', 131 | 'RETURN', 132 | 'REVERT', 133 | 'REVOKE', 134 | 'RIGHT', 135 | 'ROLLBACK', 136 | 'ROWCOUNT', 137 | 'ROWGUIDCOL', 138 | 'RULE', 139 | 'SAVE', 140 | 'SCHEMA', 141 | 'SECURITYAUDIT', 142 | 'SELECT', 143 | 'SEMANTICKEYPHRASETABLE', 144 | 'SEMANTICSIMILARITYDETAILSTABLE', 145 | 'SEMANTICSIMILARITYTABLE', 146 | 'SESSION_USER', 147 | 'SET', 148 | 'SETUSER', 149 | 'SHUTDOWN', 150 | 'SOME', 151 | 'STATISTICS', 152 | 'SYSTEM_USER', 153 | 'TABLE', 154 | 'TABLESAMPLE', 155 | 'TEXTSIZE', 156 | 'THEN', 157 | 'TO', 158 | 'TOP', 159 | 'TRAN', 160 | 'TRANSACTION', 161 | 'TRIGGER', 162 | 'TRUNCATE', 163 | 'TRY_CONVERT', 164 | 'TSEQUAL', 165 | 'UNION', 166 | 'UNIQUE', 167 | 'UNPIVOT', 168 | 'UPDATE', 169 | 'UPDATETEXT', 170 | 'USE', 171 | 'USER', 172 | 'VALUES', 173 | 'VIEW', 174 | 'WAITFOR', 175 | 'WHERE', 176 | 'WHILE', 177 | 'WITH', 178 | 'WITHIN GROUP', 179 | 'WRITETEXT', 180 | // https://learn.microsoft.com/en-us/sql/t-sql/queries/output-clause-transact-sql?view=sql-server-ver16#action 181 | '$ACTION', 182 | ]; 183 | 184 | export const dataTypes: string[] = [ 185 | // https://learn.microsoft.com/en-us/sql/t-sql/data-types/data-types-transact-sql?view=sql-server-ver15 186 | 'BINARY', 187 | 'BIT', 188 | 'CHAR', 189 | 'CHAR', 190 | 'CHARACTER', 191 | 'DATE', 192 | 'DATETIME2', 193 | 'DATETIMEOFFSET', 194 | 'DEC', 195 | 'DECIMAL', 196 | 'DOUBLE', 197 | 'FLOAT', 198 | 'INT', 199 | 'INTEGER', 200 | 'NATIONAL', 201 | 'NCHAR', 202 | 'NUMERIC', 203 | 'NVARCHAR', 204 | 'PRECISION', 205 | 'REAL', 206 | 'SMALLINT', 207 | 'TIME', 208 | 'TIMESTAMP', 209 | 'VARBINARY', 210 | 'VARCHAR', 211 | ]; 212 | -------------------------------------------------------------------------------- /src/lexer/NestedComment.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-cond-assign */ 2 | import { RegExpLike } from './TokenizerEngine.js'; 3 | 4 | const START = /\/\*/uy; // matches: /* 5 | const ANY_CHAR = /[\s\S]/uy; // matches single character 6 | const END = /\*\//uy; // matches: */ 7 | 8 | /** 9 | * An object mimicking a regular expression, 10 | * for matching nested block-comments. 11 | */ 12 | export class NestedComment implements RegExpLike { 13 | public lastIndex: number = 0; 14 | 15 | public exec(input: string): string[] | null { 16 | let result = ''; 17 | let match: string | null; 18 | let nestLevel = 0; 19 | 20 | if ((match = this.matchSection(START, input))) { 21 | result += match; 22 | nestLevel++; 23 | } else { 24 | return null; 25 | } 26 | 27 | while (nestLevel > 0) { 28 | if ((match = this.matchSection(START, input))) { 29 | result += match; 30 | nestLevel++; 31 | } else if ((match = this.matchSection(END, input))) { 32 | result += match; 33 | nestLevel--; 34 | } else if ((match = this.matchSection(ANY_CHAR, input))) { 35 | result += match; 36 | } else { 37 | return null; 38 | } 39 | } 40 | 41 | return [result]; 42 | } 43 | 44 | private matchSection(regex: RegExp, input: string): string | null { 45 | regex.lastIndex = this.lastIndex; 46 | const matches = regex.exec(input); 47 | if (matches) { 48 | this.lastIndex += matches[0].length; 49 | } 50 | return matches ? matches[0] : null; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/lexer/TokenizerEngine.ts: -------------------------------------------------------------------------------- 1 | import { Token, TokenType } from './token.js'; 2 | import { lineColFromIndex } from './lineColFromIndex.js'; 3 | import { WHITESPACE_REGEX } from './regexUtil.js'; 4 | 5 | export interface RegExpLike { 6 | lastIndex: number; 7 | exec(input: string): string[] | null; 8 | } 9 | 10 | export interface TokenRule { 11 | type: TokenType; 12 | // Normally a RegExp object. 13 | // But to allow for more complex matching logic, 14 | // an object can be given that implements a RegExpLike interface. 15 | regex: RegExpLike; 16 | // Called with the raw string that was matched 17 | text?: (rawText: string) => string; 18 | key?: (rawText: string) => string; 19 | } 20 | 21 | export default class TokenizerEngine { 22 | private input = ''; // The input SQL string to process 23 | private index = 0; // Current position in string 24 | 25 | constructor(private rules: TokenRule[], private dialectName: string) {} 26 | 27 | /** 28 | * Takes a SQL string and breaks it into tokens. 29 | * Each token is an object with type and value. 30 | * 31 | * @param {string} input - The SQL string 32 | * @returns {Token[]} output token stream 33 | */ 34 | public tokenize(input: string): Token[] { 35 | this.input = input; 36 | this.index = 0; 37 | const tokens: Token[] = []; 38 | let token: Token | undefined; 39 | 40 | // Keep processing the string until end is reached 41 | while (this.index < this.input.length) { 42 | // skip any preceding whitespace 43 | const precedingWhitespace = this.getWhitespace(); 44 | 45 | if (this.index < this.input.length) { 46 | // Get the next token and the token type 47 | token = this.getNextToken(); 48 | if (!token) { 49 | throw this.createParseError(); 50 | } 51 | 52 | tokens.push({ ...token, precedingWhitespace }); 53 | } 54 | } 55 | return tokens; 56 | } 57 | 58 | private createParseError(): Error { 59 | const text = this.input.slice(this.index, this.index + 10); 60 | const { line, col } = lineColFromIndex(this.input, this.index); 61 | return new Error( 62 | `Parse error: Unexpected "${text}" at line ${line} column ${col}.\n${this.dialectInfo()}` 63 | ); 64 | } 65 | 66 | private dialectInfo(): string { 67 | if (this.dialectName === 'sql') { 68 | return ( 69 | `This likely happens because you're using the default "sql" dialect.\n` + 70 | `If possible, please select a more specific dialect (like sqlite, postgresql, etc).` 71 | ); 72 | } else { 73 | return `SQL dialect used: "${this.dialectName}".`; 74 | } 75 | } 76 | 77 | private getWhitespace(): string | undefined { 78 | WHITESPACE_REGEX.lastIndex = this.index; 79 | 80 | const matches = WHITESPACE_REGEX.exec(this.input); 81 | if (matches) { 82 | // Advance current position by matched whitespace length 83 | this.index += matches[0].length; 84 | return matches[0]; 85 | } 86 | return undefined; 87 | } 88 | 89 | private getNextToken(): Token | undefined { 90 | for (const rule of this.rules) { 91 | const token = this.match(rule); 92 | if (token) { 93 | return token; 94 | } 95 | } 96 | return undefined; 97 | } 98 | 99 | // Attempts to match token rule regex at current position in input 100 | private match(rule: TokenRule): Token | undefined { 101 | rule.regex.lastIndex = this.index; 102 | const matches = rule.regex.exec(this.input); 103 | if (matches) { 104 | const matchedText = matches[0]; 105 | 106 | const token: Token = { 107 | type: rule.type, 108 | raw: matchedText, 109 | text: rule.text ? rule.text(matchedText) : matchedText, 110 | start: this.index, 111 | }; 112 | 113 | if (rule.key) { 114 | token.key = rule.key(matchedText); 115 | } 116 | 117 | // Advance current position by matched token length 118 | this.index += matchedText.length; 119 | return token; 120 | } 121 | return undefined; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/lexer/disambiguateTokens.ts: -------------------------------------------------------------------------------- 1 | import { isReserved, Token, TokenType } from './token.js'; 2 | 3 | /** 4 | * Ensures that no keyword token (RESERVED_*) is preceded or followed by a dot (.) 5 | * or any other property-access operator. 6 | * 7 | * Ensures that all RESERVED_FUNCTION_NAME tokens are followed by "(". 8 | * If they're not, converts the token to IDENTIFIER. 9 | * 10 | * Converts RESERVED_DATA_TYPE tokens followed by "(" to RESERVED_PARAMETERIZED_DATA_TYPE. 11 | * 12 | * When IDENTIFIER or RESERVED_DATA_TYPE token is followed by "[" 13 | * converts it to ARRAY_IDENTIFIER or ARRAY_KEYWORD accordingly. 14 | * 15 | * This is needed to avoid ambiguity in parser which expects function names 16 | * to always be followed by open-paren, and to distinguish between 17 | * array accessor `foo[1]` and array literal `[1, 2, 3]`. 18 | */ 19 | export function disambiguateTokens(tokens: Token[]): Token[] { 20 | return tokens 21 | .map(propertyNameKeywordToIdent) 22 | .map(funcNameToIdent) 23 | .map(dataTypeToParameterizedDataType) 24 | .map(identToArrayIdent) 25 | .map(dataTypeToArrayKeyword); 26 | } 27 | 28 | const propertyNameKeywordToIdent = (token: Token, i: number, tokens: Token[]): Token => { 29 | if (isReserved(token.type)) { 30 | const prevToken = prevNonCommentToken(tokens, i); 31 | if (prevToken && prevToken.type === TokenType.PROPERTY_ACCESS_OPERATOR) { 32 | return { ...token, type: TokenType.IDENTIFIER, text: token.raw }; 33 | } 34 | const nextToken = nextNonCommentToken(tokens, i); 35 | if (nextToken && nextToken.type === TokenType.PROPERTY_ACCESS_OPERATOR) { 36 | return { ...token, type: TokenType.IDENTIFIER, text: token.raw }; 37 | } 38 | } 39 | return token; 40 | }; 41 | 42 | const funcNameToIdent = (token: Token, i: number, tokens: Token[]): Token => { 43 | if (token.type === TokenType.RESERVED_FUNCTION_NAME) { 44 | const nextToken = nextNonCommentToken(tokens, i); 45 | if (!nextToken || !isOpenParen(nextToken)) { 46 | return { ...token, type: TokenType.IDENTIFIER, text: token.raw }; 47 | } 48 | } 49 | return token; 50 | }; 51 | 52 | const dataTypeToParameterizedDataType = (token: Token, i: number, tokens: Token[]): Token => { 53 | if (token.type === TokenType.RESERVED_DATA_TYPE) { 54 | const nextToken = nextNonCommentToken(tokens, i); 55 | if (nextToken && isOpenParen(nextToken)) { 56 | return { ...token, type: TokenType.RESERVED_PARAMETERIZED_DATA_TYPE }; 57 | } 58 | } 59 | return token; 60 | }; 61 | 62 | const identToArrayIdent = (token: Token, i: number, tokens: Token[]): Token => { 63 | if (token.type === TokenType.IDENTIFIER) { 64 | const nextToken = nextNonCommentToken(tokens, i); 65 | if (nextToken && isOpenBracket(nextToken)) { 66 | return { ...token, type: TokenType.ARRAY_IDENTIFIER }; 67 | } 68 | } 69 | return token; 70 | }; 71 | 72 | const dataTypeToArrayKeyword = (token: Token, i: number, tokens: Token[]): Token => { 73 | if (token.type === TokenType.RESERVED_DATA_TYPE) { 74 | const nextToken = nextNonCommentToken(tokens, i); 75 | if (nextToken && isOpenBracket(nextToken)) { 76 | return { ...token, type: TokenType.ARRAY_KEYWORD }; 77 | } 78 | } 79 | return token; 80 | }; 81 | 82 | const prevNonCommentToken = (tokens: Token[], index: number): Token | undefined => 83 | nextNonCommentToken(tokens, index, -1); 84 | 85 | const nextNonCommentToken = ( 86 | tokens: Token[], 87 | index: number, 88 | dir: -1 | 1 = 1 89 | ): Token | undefined => { 90 | let i = 1; 91 | while (tokens[index + i * dir] && isComment(tokens[index + i * dir])) { 92 | i++; 93 | } 94 | return tokens[index + i * dir]; 95 | }; 96 | 97 | const isOpenParen = (t: Token): boolean => t.type === TokenType.OPEN_PAREN && t.text === '('; 98 | 99 | const isOpenBracket = (t: Token): boolean => t.type === TokenType.OPEN_PAREN && t.text === '['; 100 | 101 | const isComment = (t: Token): boolean => 102 | t.type === TokenType.BLOCK_COMMENT || t.type === TokenType.LINE_COMMENT; 103 | -------------------------------------------------------------------------------- /src/lexer/lineColFromIndex.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Determines line and column number of character index in source code. 3 | */ 4 | export function lineColFromIndex(source: string, index: number): LineCol { 5 | const lines = source.slice(0, index).split(/\n/); 6 | return { line: lines.length, col: lines[lines.length - 1].length + 1 }; 7 | } 8 | 9 | export interface LineCol { 10 | line: number; 11 | col: number; 12 | } 13 | -------------------------------------------------------------------------------- /src/lexer/regexUtil.ts: -------------------------------------------------------------------------------- 1 | import { PrefixedQuoteType } from './TokenizerOptions.js'; 2 | 3 | // Escapes regex special chars 4 | export const escapeRegExp = (string: string) => string.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); 5 | 6 | export const WHITESPACE_REGEX = /\s+/uy; 7 | 8 | export const patternToRegex = (pattern: string): RegExp => new RegExp(`(?:${pattern})`, 'uy'); 9 | 10 | // Converts "ab" to "[Aa][Bb]" 11 | export const toCaseInsensitivePattern = (prefix: string): string => 12 | prefix 13 | .split('') 14 | .map(char => (/ /gu.test(char) ? '\\s+' : `[${char.toUpperCase()}${char.toLowerCase()}]`)) 15 | .join(''); 16 | 17 | export const withDashes = (pattern: string): string => pattern + '(?:-' + pattern + ')*'; 18 | 19 | // Converts ["a", "b"] to "(?:[Aa]|[Bb]|)" or "(?:[Aa]|[Bb])" when required = true 20 | export const prefixesPattern = ({ prefixes, requirePrefix }: PrefixedQuoteType): string => 21 | `(?:${prefixes.map(toCaseInsensitivePattern).join('|')}${requirePrefix ? '' : '|'})`; 22 | -------------------------------------------------------------------------------- /src/parser/LexerAdapter.ts: -------------------------------------------------------------------------------- 1 | import { lineColFromIndex } from '../lexer/lineColFromIndex.js'; 2 | import { Token, TokenType } from '../lexer/token.js'; 3 | 4 | // Nearly type definitions say that Token must have a value field, 5 | // which however is wrong. Instead Nearley expects a text field. 6 | type NearleyToken = Token & { value: string }; 7 | 8 | export default class LexerAdapter { 9 | private index = 0; 10 | private tokens: Token[] = []; 11 | private input = ''; 12 | 13 | constructor(private tokenize: (chunk: string) => Token[]) {} 14 | 15 | reset(chunk: string, _info: any) { 16 | this.input = chunk; 17 | this.index = 0; 18 | this.tokens = this.tokenize(chunk); 19 | } 20 | 21 | next(): NearleyToken | undefined { 22 | return this.tokens[this.index++] as NearleyToken | undefined; 23 | } 24 | 25 | save(): any {} 26 | 27 | formatError(token: NearleyToken) { 28 | const { line, col } = lineColFromIndex(this.input, token.start); 29 | return `Parse error at token: ${token.text} at line ${line} column ${col}`; 30 | } 31 | 32 | has(name: string): boolean { 33 | return name in TokenType; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/parser/createParser.ts: -------------------------------------------------------------------------------- 1 | import nearley from 'nearley'; 2 | 3 | import Tokenizer from '../lexer/Tokenizer.js'; 4 | import { disambiguateTokens } from '../lexer/disambiguateTokens.js'; 5 | import { ParamTypes } from '../lexer/TokenizerOptions.js'; 6 | import { StatementNode } from './ast.js'; 7 | import grammar from './grammar.js'; 8 | import LexerAdapter from './LexerAdapter.js'; 9 | import { createEofToken } from '../lexer/token.js'; 10 | 11 | const { Parser: NearleyParser, Grammar } = nearley; 12 | 13 | export interface Parser { 14 | parse(sql: string, paramTypesOverrides: ParamTypes): StatementNode[]; 15 | } 16 | 17 | /** 18 | * Creates a parser object which wraps the setup of Nearley parser 19 | */ 20 | export function createParser(tokenizer: Tokenizer): Parser { 21 | let paramTypesOverrides: ParamTypes = {}; 22 | const lexer = new LexerAdapter(chunk => [ 23 | ...disambiguateTokens(tokenizer.tokenize(chunk, paramTypesOverrides)), 24 | createEofToken(chunk.length), 25 | ]); 26 | const parser = new NearleyParser(Grammar.fromCompiled(grammar), { lexer }); 27 | 28 | return { 29 | parse: (sql: string, paramTypes: ParamTypes) => { 30 | // share paramTypesOverrides with Tokenizer 31 | paramTypesOverrides = paramTypes; 32 | 33 | const { results } = parser.feed(sql); 34 | 35 | if (results.length === 1) { 36 | return results[0]; 37 | } else if (results.length === 0) { 38 | // Ideally we would report a line number where the parser failed, 39 | // but I haven't found a way to get this info from Nearley :( 40 | throw new Error('Parse error: Invalid SQL'); 41 | } else { 42 | throw new Error(`Parse error: Ambiguous grammar\n${JSON.stringify(results, undefined, 2)}`); 43 | } 44 | }, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/sqlFormatter.ts: -------------------------------------------------------------------------------- 1 | import * as allDialects from './allDialects.js'; 2 | 3 | import { FormatOptions } from './FormatOptions.js'; 4 | import { createDialect, DialectOptions } from './dialect.js'; 5 | import Formatter from './formatter/Formatter.js'; 6 | import { ConfigError, validateConfig } from './validateConfig.js'; 7 | 8 | const dialectNameMap: Record = { 9 | bigquery: 'bigquery', 10 | db2: 'db2', 11 | db2i: 'db2i', 12 | duckdb: 'duckdb', 13 | hive: 'hive', 14 | mariadb: 'mariadb', 15 | mysql: 'mysql', 16 | n1ql: 'n1ql', 17 | plsql: 'plsql', 18 | postgresql: 'postgresql', 19 | redshift: 'redshift', 20 | spark: 'spark', 21 | sqlite: 'sqlite', 22 | sql: 'sql', 23 | tidb: 'tidb', 24 | trino: 'trino', 25 | transactsql: 'transactsql', 26 | tsql: 'transactsql', // alias for transactsq 27 | singlestoredb: 'singlestoredb', 28 | snowflake: 'snowflake', 29 | }; 30 | 31 | export const supportedDialects = Object.keys(dialectNameMap); 32 | export type SqlLanguage = keyof typeof dialectNameMap; 33 | 34 | export type FormatOptionsWithLanguage = Partial & { 35 | language?: SqlLanguage; 36 | }; 37 | 38 | export type FormatOptionsWithDialect = Partial & { 39 | dialect: DialectOptions; 40 | }; 41 | 42 | const defaultOptions: FormatOptions = { 43 | tabWidth: 2, 44 | useTabs: false, 45 | keywordCase: 'preserve', 46 | identifierCase: 'preserve', 47 | dataTypeCase: 'preserve', 48 | functionCase: 'preserve', 49 | indentStyle: 'standard', 50 | logicalOperatorNewline: 'before', 51 | expressionWidth: 50, 52 | linesBetweenQueries: 1, 53 | denseOperators: false, 54 | newlineBeforeSemicolon: false, 55 | }; 56 | 57 | /** 58 | * Format whitespace in a query to make it easier to read. 59 | * 60 | * @param {string} query - input SQL query string 61 | * @param {FormatOptionsWithLanguage} cfg Configuration options (see docs in README) 62 | * @return {string} formatted query 63 | */ 64 | export const format = (query: string, cfg: FormatOptionsWithLanguage = {}): string => { 65 | if (typeof cfg.language === 'string' && !supportedDialects.includes(cfg.language)) { 66 | throw new ConfigError(`Unsupported SQL dialect: ${cfg.language}`); 67 | } 68 | 69 | const canonicalDialectName = dialectNameMap[cfg.language || 'sql']; 70 | 71 | return formatDialect(query, { 72 | ...cfg, 73 | dialect: allDialects[canonicalDialectName], 74 | }); 75 | }; 76 | 77 | /** 78 | * Like the above format(), but language parameter is mandatory 79 | * and must be a Dialect object instead of a string. 80 | * 81 | * @param {string} query - input SQL query string 82 | * @param {FormatOptionsWithDialect} cfg Configuration options (see docs in README) 83 | * @return {string} formatted query 84 | */ 85 | export const formatDialect = ( 86 | query: string, 87 | { dialect, ...cfg }: FormatOptionsWithDialect 88 | ): string => { 89 | if (typeof query !== 'string') { 90 | throw new Error('Invalid query argument. Expected string, instead got ' + typeof query); 91 | } 92 | 93 | const options = validateConfig({ 94 | ...defaultOptions, 95 | ...cfg, 96 | }); 97 | 98 | return new Formatter(createDialect(dialect), options).format(query); 99 | }; 100 | 101 | export type FormatFn = typeof format; 102 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const dedupe = (arr: string[]) => [...new Set(arr)]; 2 | 3 | // Last element from array 4 | export const last = (arr: T[]): T | undefined => arr[arr.length - 1]; 5 | 6 | // Sorts strings by length, so that longer ones are first 7 | // Also sorts alphabetically after sorting by length. 8 | export const sortByLengthDesc = (strings: string[]) => 9 | strings.sort((a, b) => b.length - a.length || a.localeCompare(b)); 10 | 11 | /** Get length of longest string in list of strings */ 12 | export const maxLength = (strings: string[]) => 13 | strings.reduce((max, cur) => Math.max(max, cur.length), 0); 14 | 15 | // replaces long whitespace sequences with just one space 16 | export const equalizeWhitespace = (s: string) => s.replace(/\s+/gu, ' '); 17 | 18 | // True when string contains multiple lines 19 | export const isMultiline = (text: string): boolean => /\n/.test(text); 20 | 21 | // Given a type and a field name, returns a type where this field is optional 22 | // 23 | // For example, these two type definitions are equivalent: 24 | // 25 | // type Foo = Optional<{ foo: string, bar: number }, 'foo'>; 26 | // type Foo = { foo?: string, bar: number }; 27 | // 28 | export type Optional = Pick, K> & Omit; 29 | -------------------------------------------------------------------------------- /src/validateConfig.ts: -------------------------------------------------------------------------------- 1 | import { FormatOptions } from './FormatOptions.js'; 2 | import { ParamItems } from './formatter/Params.js'; 3 | import { ParamTypes } from './lexer/TokenizerOptions.js'; 4 | 5 | export class ConfigError extends Error {} 6 | 7 | export function validateConfig(cfg: FormatOptions): FormatOptions { 8 | const removedOptions = [ 9 | 'multilineLists', 10 | 'newlineBeforeOpenParen', 11 | 'newlineBeforeCloseParen', 12 | 'aliasAs', 13 | 'commaPosition', 14 | 'tabulateAlias', 15 | ]; 16 | for (const optionName of removedOptions) { 17 | if (optionName in cfg) { 18 | throw new ConfigError(`${optionName} config is no more supported.`); 19 | } 20 | } 21 | 22 | if (cfg.expressionWidth <= 0) { 23 | throw new ConfigError( 24 | `expressionWidth config must be positive number. Received ${cfg.expressionWidth} instead.` 25 | ); 26 | } 27 | 28 | if (cfg.params && !validateParams(cfg.params)) { 29 | // eslint-disable-next-line no-console 30 | console.warn('WARNING: All "params" option values should be strings.'); 31 | } 32 | 33 | if (cfg.paramTypes && !validateParamTypes(cfg.paramTypes)) { 34 | throw new ConfigError( 35 | 'Empty regex given in custom paramTypes. That would result in matching infinite amount of parameters.' 36 | ); 37 | } 38 | 39 | return cfg; 40 | } 41 | 42 | function validateParams(params: ParamItems | string[]): boolean { 43 | const paramValues = params instanceof Array ? params : Object.values(params); 44 | return paramValues.every(p => typeof p === 'string'); 45 | } 46 | 47 | function validateParamTypes(paramTypes: ParamTypes): boolean { 48 | if (paramTypes.custom && Array.isArray(paramTypes.custom)) { 49 | return paramTypes.custom.every(p => p.regex !== ''); 50 | } 51 | return true; 52 | } 53 | -------------------------------------------------------------------------------- /static/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | width: 100%; 5 | margin: 0; 6 | padding: 0; 7 | overflow: hidden; 8 | font-family: 'Roboto Mono', monospace; 9 | } 10 | header { 11 | z-index: 2; 12 | position: relative; 13 | height: 4rem; 14 | padding: 10px 20px; 15 | border-bottom: 2px solid #8dc63f; 16 | box-sizing: border-box; 17 | } 18 | h1.title { 19 | margin: 0 1rem 0 0; 20 | display: inline-block; 21 | } 22 | .buttons { 23 | display: inline-block; 24 | } 25 | a { 26 | text-decoration: none; 27 | } 28 | main { 29 | overflow: hidden; 30 | height: calc(100% - 4rem); 31 | display: flex; 32 | flex-direction: row; 33 | } 34 | aside.options-menu { 35 | padding: 0.5rem; 36 | width: 11%; 37 | height: 100%; 38 | background-color: aliceblue; 39 | } 40 | article.config { 41 | margin: 0.25rem 0; 42 | padding-bottom: 0.5rem; 43 | border-bottom: 1px black solid; 44 | } 45 | article.config input[type='number'] { 46 | width: 3rem; 47 | } 48 | .staging { 49 | width: 89%; 50 | height: 100%; 51 | display: flex; 52 | flex-direction: row; 53 | align-items: stretch; 54 | } 55 | .input, 56 | .output { 57 | display: flex; 58 | align-items: stretch; 59 | width: 50%; 60 | height: 100%; 61 | border-left: 2px solid #8dc63f; 62 | } 63 | textarea { 64 | width: 100%; 65 | padding: 20px; 66 | border: 0; 67 | box-sizing: border-box; 68 | font-size: 1.3em; 69 | resize: none; 70 | outline: none; 71 | line-height: 1.3; 72 | font-family: 'Roboto Mono', monospace; 73 | } 74 | .error { 75 | width: 100%; 76 | padding: 20px; 77 | border: 4px solid salmon; 78 | background: rgb(255, 237, 230); 79 | } 80 | -------------------------------------------------------------------------------- /static/index.js: -------------------------------------------------------------------------------- 1 | const attachFormat = () => { 2 | const input = document.getElementById('input'); 3 | const output = document.getElementById('output'); 4 | const error = document.getElementById('error'); 5 | 6 | const language = document.getElementById('language'); 7 | const tabWidth = document.getElementById('tabWidth'); 8 | const useTabs = document.getElementById('useTabs'); 9 | const keywordCase = document.getElementById('keywordCase'); 10 | const dataTypeCase = document.getElementById('dataTypeCase'); 11 | const functionCase = document.getElementById('functionCase'); 12 | const identifierCase = document.getElementById('identifierCase'); 13 | const indentStyle = document.getElementById('indentStyle'); 14 | const logicalOperatorNewline = document.getElementById('logicalOperatorNewline'); 15 | const expressionWidth = document.getElementById('expressionWidth'); 16 | const lineBetweenQueries = document.getElementById('lineBetweenQueries'); 17 | const denseOperators = document.getElementById('denseOperators'); 18 | const newlineBeforeSemicolon = document.getElementById('newlineBeforeSemicolon'); 19 | 20 | function showOutput(text) { 21 | output.value = text; 22 | output.style.display = 'block'; 23 | error.style.display = 'none'; 24 | } 25 | 26 | function showError(text) { 27 | error.innerHTML = text; 28 | output.style.display = 'none'; 29 | error.style.display = 'block'; 30 | } 31 | 32 | function format() { 33 | try { 34 | const config = { 35 | language: language.options[language.selectedIndex].value, 36 | tabWidth: tabWidth.value, 37 | useTabs: useTabs.checked, 38 | keywordCase: keywordCase.options[keywordCase.selectedIndex].value, 39 | dataTypeCase: dataTypeCase.options[dataTypeCase.selectedIndex].value, 40 | functionCase: functionCase.options[functionCase.selectedIndex].value, 41 | identifierCase: identifierCase.options[identifierCase.selectedIndex].value, 42 | indentStyle: indentStyle.options[indentStyle.selectedIndex].value, 43 | logicalOperatorNewline: 44 | logicalOperatorNewline.options[logicalOperatorNewline.selectedIndex].value, 45 | expressionWidth: expressionWidth.value, 46 | lineBetweenQueries: lineBetweenQueries.value, 47 | denseOperators: denseOperators.checked, 48 | newlineBeforeSemicolon: newlineBeforeSemicolon.checked, 49 | }; 50 | showOutput(sqlFormatter.format(input.value, config)); 51 | } catch (e) { 52 | if (e instanceof sqlFormatter.ConfigError) { 53 | showError(`

Configuration error

${e.message}

`); 54 | } else { 55 | showError( 56 | ` 57 |

An Unexpected Error Occurred

58 |

${e.message}

59 |

60 | Please report this at 61 | Github issues page. 62 |

63 |

Stack Trace:

64 |
${e.stack.toString()}
65 | ` 66 | ); 67 | } 68 | } 69 | } 70 | 71 | input.addEventListener('input', format); 72 | [ 73 | language, 74 | tabWidth, 75 | useTabs, 76 | keywordCase, 77 | dataTypeCase, 78 | functionCase, 79 | identifierCase, 80 | indentStyle, 81 | logicalOperatorNewline, 82 | expressionWidth, 83 | lineBetweenQueries, 84 | denseOperators, 85 | newlineBeforeSemicolon, 86 | ].forEach(option => option.addEventListener('change', format)); 87 | 88 | format(); 89 | }; 90 | 91 | document.addEventListener('DOMContentLoaded', attachFormat); 92 | -------------------------------------------------------------------------------- /static/sql-formatter-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sql-formatter-org/sql-formatter/afc74616e35f56b83664e03a0544c6c85afb3e0d/static/sql-formatter-icon.png -------------------------------------------------------------------------------- /test/db2.test.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { format as originalFormat, FormatFn } from '../src/sqlFormatter.js'; 4 | import behavesLikeDb2Formatter from './behavesLikeDb2Formatter.js'; 5 | 6 | import supportsCreateTable from './features/createTable.js'; 7 | import supportsAlterTable from './features/alterTable.js'; 8 | import supportsDropTable from './features/dropTable.js'; 9 | import supportsJoin from './features/join.js'; 10 | import supportsStrings from './features/strings.js'; 11 | import supportsComments from './features/comments.js'; 12 | import supportsOperators from './features/operators.js'; 13 | import supportsLimiting from './features/limiting.js'; 14 | import supportsDataTypeCase from './options/dataTypeCase.js'; 15 | 16 | describe('Db2Formatter', () => { 17 | const language = 'db2'; 18 | const format: FormatFn = (query, cfg = {}) => originalFormat(query, { ...cfg, language }); 19 | 20 | behavesLikeDb2Formatter(format); 21 | 22 | supportsComments(format); 23 | supportsLimiting(format, { limit: true, fetchNext: true, offset: true }); 24 | supportsCreateTable(format); 25 | supportsAlterTable(format, { 26 | addColumn: true, 27 | dropColumn: true, 28 | renameColumn: true, 29 | }); 30 | supportsDropTable(format); 31 | supportsJoin(format, { without: ['NATURAL'] }); 32 | supportsOperators( 33 | format, 34 | [ 35 | '**', 36 | '%', 37 | '&', 38 | '|', 39 | '^', 40 | '~', 41 | '¬=', 42 | '¬>', 43 | '¬<', 44 | '!>', 45 | '!<', 46 | '^=', 47 | '^>', 48 | '^<', 49 | '||', 50 | '->', 51 | '=>', 52 | ], 53 | { any: true } 54 | ); 55 | // Additional U& string type in addition to others shared by all DB2 implementations 56 | supportsStrings(format, ["U&''"]); 57 | supportsDataTypeCase(format); 58 | 59 | it('supports non-standard FOR clause', () => { 60 | expect(format('SELECT * FROM tbl FOR UPDATE OF other_tbl FOR RS USE AND KEEP EXCLUSIVE LOCKS')) 61 | .toBe(dedent` 62 | SELECT 63 | * 64 | FROM 65 | tbl 66 | FOR UPDATE OF 67 | other_tbl 68 | FOR RS USE AND KEEP EXCLUSIVE LOCKS 69 | `); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/db2i.test.ts: -------------------------------------------------------------------------------- 1 | import { format as originalFormat, FormatFn } from '../src/sqlFormatter.js'; 2 | import behavesLikeDb2Formatter from './behavesLikeDb2Formatter.js'; 3 | import supportsComments from './features/comments.js'; 4 | 5 | import supportsCreateTable from './features/createTable.js'; 6 | import supportsAlterTable from './features/alterTable.js'; 7 | import supportsDropTable from './features/dropTable.js'; 8 | import supportsJoin from './features/join.js'; 9 | import supportsOperators from './features/operators.js'; 10 | import supportsLimiting from './features/limiting.js'; 11 | import supportsDataTypeCase from './options/dataTypeCase.js'; 12 | 13 | describe('Db2iFormatter', () => { 14 | const language = 'db2i'; 15 | const format: FormatFn = (query, cfg = {}) => originalFormat(query, { ...cfg, language }); 16 | 17 | behavesLikeDb2Formatter(format); 18 | 19 | supportsComments(format, { nestedBlockComments: true }); 20 | supportsLimiting(format, { limit: true, fetchNext: true, fetchFirst: true, offset: true }); 21 | supportsCreateTable(format, { orReplace: true }); 22 | supportsAlterTable(format, { 23 | addColumn: true, 24 | dropColumn: true, 25 | }); 26 | supportsDropTable(format, { ifExists: true }); 27 | supportsJoin(format, { 28 | without: ['NATURAL'], 29 | additionally: ['EXCEPTION JOIN', 'LEFT EXCEPTION JOIN', 'RIGHT EXCEPTION JOIN'], 30 | }); 31 | supportsOperators(format, ['**', '¬=', '¬>', '¬<', '!>', '!<', '||', '=>'], { any: true }); 32 | supportsDataTypeCase(format); 33 | }); 34 | -------------------------------------------------------------------------------- /test/features/alterTable.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | interface AlterTableConfig { 6 | addColumn?: boolean; 7 | dropColumn?: boolean; 8 | modify?: boolean; 9 | renameTo?: boolean; 10 | renameColumn?: boolean; 11 | } 12 | 13 | export default function supportsAlterTable(format: FormatFn, cfg: AlterTableConfig = {}) { 14 | if (cfg.addColumn) { 15 | it('formats ALTER TABLE ... ADD COLUMN query', () => { 16 | const result = format('ALTER TABLE supplier ADD COLUMN unit_price DECIMAL NOT NULL;'); 17 | expect(result).toBe(dedent` 18 | ALTER TABLE supplier 19 | ADD COLUMN unit_price DECIMAL NOT NULL; 20 | `); 21 | }); 22 | } 23 | 24 | if (cfg.dropColumn) { 25 | it('formats ALTER TABLE ... DROP COLUMN query', () => { 26 | const result = format('ALTER TABLE supplier DROP COLUMN unit_price;'); 27 | expect(result).toBe(dedent` 28 | ALTER TABLE supplier 29 | DROP COLUMN unit_price; 30 | `); 31 | }); 32 | } 33 | 34 | if (cfg.modify) { 35 | it('formats ALTER TABLE ... MODIFY statement', () => { 36 | const result = format('ALTER TABLE supplier MODIFY supplier_id DECIMAL NULL;'); 37 | expect(result).toBe(dedent` 38 | ALTER TABLE supplier 39 | MODIFY supplier_id DECIMAL NULL; 40 | `); 41 | }); 42 | } 43 | 44 | if (cfg.renameTo) { 45 | it('formats ALTER TABLE ... RENAME TO statement', () => { 46 | const result = format('ALTER TABLE supplier RENAME TO the_one_who_supplies;'); 47 | expect(result).toBe(dedent` 48 | ALTER TABLE supplier 49 | RENAME TO the_one_who_supplies; 50 | `); 51 | }); 52 | } 53 | 54 | if (cfg.renameColumn) { 55 | it('formats ALTER TABLE ... RENAME COLUMN statement', () => { 56 | const result = format('ALTER TABLE supplier RENAME COLUMN supplier_id TO id;'); 57 | expect(result).toBe(dedent` 58 | ALTER TABLE supplier 59 | RENAME COLUMN supplier_id TO id; 60 | `); 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/features/arrayAndMapAccessors.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsArrayAndMapAccessors(format: FormatFn) { 6 | it('supports square brackets for array indexing', () => { 7 | const result = format(`SELECT arr[1], order_lines[5].productId;`); 8 | expect(result).toBe(dedent` 9 | SELECT 10 | arr[1], 11 | order_lines[5].productId; 12 | `); 13 | }); 14 | 15 | // The check for yota['foo.bar-baz'] is for Issue #230 16 | it('supports square brackets for map lookup', () => { 17 | const result = format(`SELECT alpha['a'], beta['gamma'].zeta, yota['foo.bar-baz'];`); 18 | expect(result).toBe(dedent` 19 | SELECT 20 | alpha['a'], 21 | beta['gamma'].zeta, 22 | yota['foo.bar-baz']; 23 | `); 24 | }); 25 | 26 | it('supports square brackets for map lookup - uppercase', () => { 27 | const result = format(`SELECT Alpha['a'], Beta['gamma'].zeTa, yotA['foo.bar-baz'];`, { 28 | identifierCase: 'upper', 29 | }); 30 | expect(result).toBe(dedent` 31 | SELECT 32 | ALPHA['a'], 33 | BETA['gamma'].ZETA, 34 | YOTA['foo.bar-baz']; 35 | `); 36 | }); 37 | 38 | it('supports namespaced array identifiers', () => { 39 | const result = format(`SELECT foo.coalesce['blah'];`); 40 | expect(result).toBe(dedent` 41 | SELECT 42 | foo.coalesce['blah']; 43 | `); 44 | }); 45 | 46 | it('formats array accessor with comment in-between', () => { 47 | const result = format(`SELECT arr /* comment */ [1];`); 48 | expect(result).toBe(dedent` 49 | SELECT 50 | arr/* comment */ [1]; 51 | `); 52 | }); 53 | 54 | it('formats namespaced array accessor with comment in-between', () => { 55 | const result = format(`SELECT foo./* comment */arr[1];`); 56 | expect(result).toBe(dedent` 57 | SELECT 58 | foo./* comment */ arr[1]; 59 | `); 60 | }); 61 | 62 | it('changes case of array accessors when identifierCase option used', () => { 63 | expect(format(`SELECT arr[1];`, { identifierCase: 'upper' })).toBe(dedent` 64 | SELECT 65 | ARR[1]; 66 | `); 67 | expect(format(`SELECT NS.Arr[1];`, { identifierCase: 'lower' })).toBe(dedent` 68 | SELECT 69 | ns.arr[1]; 70 | `); 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /test/features/arrayLiterals.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | interface ArrayLiteralConfig { 6 | withArrayPrefix?: boolean; 7 | withoutArrayPrefix?: boolean; 8 | } 9 | 10 | export default function supportsArrayLiterals(format: FormatFn, cfg: ArrayLiteralConfig = {}) { 11 | if (cfg.withArrayPrefix) { 12 | it('supports ARRAY[] literals', () => { 13 | expect( 14 | format( 15 | `SELECT ARRAY[1, 2, 3] FROM ARRAY['come-on', 'seriously', 'this', 'is', 'a', 'very', 'very', 'long', 'array'];` 16 | ) 17 | ).toBe(dedent` 18 | SELECT 19 | ARRAY[1, 2, 3] 20 | FROM 21 | ARRAY[ 22 | 'come-on', 23 | 'seriously', 24 | 'this', 25 | 'is', 26 | 'a', 27 | 'very', 28 | 'very', 29 | 'long', 30 | 'array' 31 | ]; 32 | `); 33 | }); 34 | 35 | it('dataTypeCase option does NOT affect ARRAY[] literal case', () => { 36 | expect( 37 | format(`SELECT ArrAy[1, 2]`, { 38 | dataTypeCase: 'upper', 39 | }) 40 | ).toBe(dedent` 41 | SELECT 42 | ArrAy[1, 2] 43 | `); 44 | }); 45 | 46 | it('keywordCase option affects ARRAY[] literal case', () => { 47 | expect( 48 | format(`SELECT ArrAy[1, 2]`, { 49 | keywordCase: 'upper', 50 | }) 51 | ).toBe(dedent` 52 | SELECT 53 | ARRAY[1, 2] 54 | `); 55 | }); 56 | 57 | it('dataTypeCase option affects ARRAY type case', () => { 58 | expect( 59 | format(`CREATE TABLE foo ( items ArrAy )`, { 60 | dataTypeCase: 'upper', 61 | }) 62 | ).toBe(dedent` 63 | CREATE TABLE foo (items ARRAY) 64 | `); 65 | }); 66 | } 67 | 68 | if (cfg.withoutArrayPrefix) { 69 | it('supports array literals', () => { 70 | expect( 71 | format( 72 | `SELECT [1, 2, 3] FROM ['come-on', 'seriously', 'this', 'is', 'a', 'very', 'very', 'long', 'array'];` 73 | ) 74 | ).toBe(dedent` 75 | SELECT 76 | [1, 2, 3] 77 | FROM 78 | [ 79 | 'come-on', 80 | 'seriously', 81 | 'this', 82 | 'is', 83 | 'a', 84 | 'very', 85 | 'very', 86 | 'long', 87 | 'array' 88 | ]; 89 | `); 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /test/features/between.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | import { FormatFn } from '../../src/sqlFormatter.js'; 3 | 4 | export default function supportsBetween(format: FormatFn) { 5 | it('formats BETWEEN _ AND _ on single line', () => { 6 | expect(format('foo BETWEEN bar AND baz')).toBe('foo BETWEEN bar AND baz'); 7 | }); 8 | 9 | it('supports qualified.names as BETWEEN expression values', () => { 10 | expect(format('foo BETWEEN t.bar AND t.baz')).toBe('foo BETWEEN t.bar AND t.baz'); 11 | }); 12 | 13 | it('formats BETWEEN with comments inside', () => { 14 | expect(format('WHERE foo BETWEEN /*C1*/ t.bar /*C2*/ AND /*C3*/ t.baz')).toBe(dedent` 15 | WHERE 16 | foo BETWEEN /*C1*/ t.bar /*C2*/ AND /*C3*/ t.baz 17 | `); 18 | }); 19 | 20 | it('supports complex expressions inside BETWEEN', () => { 21 | // Not ideal, but better than crashing 22 | expect(format('foo BETWEEN 1+2 AND 3+4')).toBe('foo BETWEEN 1 + 2 AND 3 + 4'); 23 | }); 24 | 25 | it('supports CASE inside BETWEEN', () => { 26 | expect(format('foo BETWEEN CASE x WHEN 1 THEN 2 END AND 3')).toBe(dedent` 27 | foo BETWEEN CASE x 28 | WHEN 1 THEN 2 29 | END AND 3 30 | `); 31 | }); 32 | 33 | // Regression test for #534 34 | it('supports AND after BETWEEN', () => { 35 | expect(format('SELECT foo BETWEEN 1 AND 2 AND x > 10')).toBe(dedent` 36 | SELECT 37 | foo BETWEEN 1 AND 2 38 | AND x > 10 39 | `); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /test/features/commentOn.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsCommentOn(format: FormatFn) { 6 | it('formats COMMENT ON ...', () => { 7 | expect(format(`COMMENT ON TABLE my_table IS 'This is an awesome table.';`)).toBe(dedent` 8 | COMMENT ON TABLE my_table IS 'This is an awesome table.'; 9 | `); 10 | 11 | expect(format(`COMMENT ON COLUMN my_table.ssn IS 'Social Security Number';`)).toBe(dedent` 12 | COMMENT ON COLUMN my_table.ssn IS 'Social Security Number'; 13 | `); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /test/features/constraints.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsConstraints(format: FormatFn, actions: string[]) { 6 | actions.forEach(action => { 7 | it(`treats ON UPDATE & ON DELETE ${action} as distinct keywords from ON`, () => { 8 | expect( 9 | format(` 10 | CREATE TABLE foo ( 11 | update_time datetime ON UPDATE ${action}, 12 | delete_time datetime ON DELETE ${action}, 13 | ); 14 | `) 15 | ).toBe(dedent` 16 | CREATE TABLE foo ( 17 | update_time datetime ON UPDATE ${action}, 18 | delete_time datetime ON DELETE ${action}, 19 | ); 20 | `); 21 | }); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /test/features/createTable.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | interface CreateTableConfig { 6 | orReplace?: boolean; 7 | ifNotExists?: boolean; 8 | columnComment?: boolean; 9 | tableComment?: boolean; 10 | } 11 | 12 | export default function supportsCreateTable(format: FormatFn, cfg: CreateTableConfig = {}) { 13 | it('formats short CREATE TABLE', () => { 14 | expect(format('CREATE TABLE tbl (a INT PRIMARY KEY, b TEXT);')).toBe(dedent` 15 | CREATE TABLE tbl (a INT PRIMARY KEY, b TEXT); 16 | `); 17 | }); 18 | 19 | // The decision to place it to multiple lines is made based on the length of text inside braces 20 | // ignoring the whitespace. (Which is not quite right :P) 21 | it('formats long CREATE TABLE', () => { 22 | expect( 23 | format('CREATE TABLE tbl (a INT PRIMARY KEY, b TEXT, c INT NOT NULL, doggie INT NOT NULL);') 24 | ).toBe(dedent` 25 | CREATE TABLE tbl ( 26 | a INT PRIMARY KEY, 27 | b TEXT, 28 | c INT NOT NULL, 29 | doggie INT NOT NULL 30 | ); 31 | `); 32 | }); 33 | 34 | if (cfg.orReplace) { 35 | it('formats short CREATE OR REPLACE TABLE', () => { 36 | expect(format('CREATE OR REPLACE TABLE tbl (a INT PRIMARY KEY, b TEXT);')).toBe(dedent` 37 | CREATE OR REPLACE TABLE tbl (a INT PRIMARY KEY, b TEXT); 38 | `); 39 | }); 40 | } 41 | 42 | if (cfg.ifNotExists) { 43 | it('formats short CREATE TABLE IF NOT EXISTS', () => { 44 | expect(format('CREATE TABLE IF NOT EXISTS tbl (a INT PRIMARY KEY, b TEXT);')).toBe(dedent` 45 | CREATE TABLE IF NOT EXISTS tbl (a INT PRIMARY KEY, b TEXT); 46 | `); 47 | }); 48 | } 49 | 50 | if (cfg.columnComment) { 51 | it('formats short CREATE TABLE with column comments', () => { 52 | expect( 53 | format(`CREATE TABLE tbl (a INT COMMENT 'Hello world!', b TEXT COMMENT 'Here we are!');`) 54 | ).toBe(dedent` 55 | CREATE TABLE tbl ( 56 | a INT COMMENT 'Hello world!', 57 | b TEXT COMMENT 'Here we are!' 58 | ); 59 | `); 60 | }); 61 | } 62 | 63 | if (cfg.tableComment) { 64 | it('formats short CREATE TABLE with comment', () => { 65 | expect(format(`CREATE TABLE tbl (a INT, b TEXT) COMMENT = 'Hello, world!';`)).toBe(dedent` 66 | CREATE TABLE tbl (a INT, b TEXT) COMMENT = 'Hello, world!'; 67 | `); 68 | }); 69 | } 70 | 71 | it('correctly indents CREATE TABLE in tabular style', () => { 72 | expect( 73 | format( 74 | `CREATE TABLE foo ( 75 | id INT PRIMARY KEY NOT NULL, 76 | fname VARCHAR NOT NULL 77 | );`, 78 | { indentStyle: 'tabularLeft' } 79 | ) 80 | ).toBe(dedent` 81 | CREATE TABLE foo ( 82 | id INT PRIMARY KEY NOT NULL, 83 | fname VARCHAR NOT NULL 84 | ); 85 | `); 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /test/features/createView.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | interface CreateViewConfig { 6 | orReplace?: boolean; 7 | materialized?: boolean; 8 | ifNotExists?: boolean; 9 | } 10 | 11 | export default function supportsCreateView( 12 | format: FormatFn, 13 | { orReplace, materialized, ifNotExists }: CreateViewConfig = {} 14 | ) { 15 | it('formats CREATE VIEW', () => { 16 | expect(format('CREATE VIEW my_view AS SELECT id, fname, lname FROM tbl;')).toBe(dedent` 17 | CREATE VIEW my_view AS 18 | SELECT 19 | id, 20 | fname, 21 | lname 22 | FROM 23 | tbl; 24 | `); 25 | }); 26 | 27 | it('formats CREATE VIEW with columns', () => { 28 | expect(format('CREATE VIEW my_view (id, fname, lname) AS SELECT * FROM tbl;')).toBe(dedent` 29 | CREATE VIEW my_view (id, fname, lname) AS 30 | SELECT 31 | * 32 | FROM 33 | tbl; 34 | `); 35 | }); 36 | 37 | if (orReplace) { 38 | it('formats CREATE OR REPLACE VIEW', () => { 39 | expect(format('CREATE OR REPLACE VIEW v1 AS SELECT 42;')).toBe(dedent` 40 | CREATE OR REPLACE VIEW v1 AS 41 | SELECT 42 | 42; 43 | `); 44 | }); 45 | } 46 | 47 | if (materialized) { 48 | it('formats CREATE MATERIALIZED VIEW', () => { 49 | expect(format('CREATE MATERIALIZED VIEW mat_view AS SELECT 42;')).toBe(dedent` 50 | CREATE MATERIALIZED VIEW mat_view AS 51 | SELECT 52 | 42; 53 | `); 54 | }); 55 | } 56 | 57 | if (ifNotExists) { 58 | it('formats short CREATE VIEW IF NOT EXISTS', () => { 59 | expect(format('CREATE VIEW IF NOT EXISTS my_view AS SELECT 42;')).toBe(dedent` 60 | CREATE VIEW IF NOT EXISTS my_view AS 61 | SELECT 62 | 42; 63 | `); 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/features/deleteFrom.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | interface DeleteFromConfig { 6 | withoutFrom?: boolean; 7 | } 8 | 9 | export default function supportsDeleteFrom( 10 | format: FormatFn, 11 | { withoutFrom }: DeleteFromConfig = {} 12 | ) { 13 | it('formats DELETE FROM statement', () => { 14 | const result = format("DELETE FROM Customers WHERE CustomerName='Alfred' AND Phone=5002132;"); 15 | expect(result).toBe(dedent` 16 | DELETE FROM Customers 17 | WHERE 18 | CustomerName = 'Alfred' 19 | AND Phone = 5002132; 20 | `); 21 | }); 22 | 23 | if (withoutFrom) { 24 | it('formats DELETE statement (without FROM)', () => { 25 | const result = format("DELETE Customers WHERE CustomerName='Alfred';"); 26 | expect(result).toBe(dedent` 27 | DELETE Customers 28 | WHERE 29 | CustomerName = 'Alfred'; 30 | `); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/features/disableComment.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsDisableComment(format: FormatFn) { 6 | it('does not format text between /* sql-formatter-disable */ and /* sql-formatter-enable */', () => { 7 | const result = format(dedent` 8 | SELECT foo FROM bar; 9 | /* sql-formatter-disable */ 10 | SELECT foo FROM bar; 11 | /* sql-formatter-enable */ 12 | SELECT foo FROM bar; 13 | `); 14 | 15 | expect(result).toBe(dedent` 16 | SELECT 17 | foo 18 | FROM 19 | bar; 20 | 21 | /* sql-formatter-disable */ 22 | SELECT foo FROM bar; 23 | /* sql-formatter-enable */ 24 | SELECT 25 | foo 26 | FROM 27 | bar; 28 | `); 29 | }); 30 | 31 | it('does not format text after /* sql-formatter-disable */ until end of file', () => { 32 | const result = format(dedent` 33 | SELECT foo FROM bar; 34 | /* sql-formatter-disable */ 35 | SELECT foo FROM bar; 36 | 37 | SELECT foo FROM bar; 38 | `); 39 | 40 | expect(result).toBe(dedent` 41 | SELECT 42 | foo 43 | FROM 44 | bar; 45 | 46 | /* sql-formatter-disable */ 47 | SELECT foo FROM bar; 48 | 49 | SELECT foo FROM bar; 50 | `); 51 | }); 52 | 53 | it('does not parse code between disable/enable comments', () => { 54 | const result = format(dedent` 55 | SELECT /*sql-formatter-disable*/ ?!{}[] /*sql-formatter-enable*/ FROM bar; 56 | `); 57 | 58 | expect(result).toBe(dedent` 59 | SELECT 60 | /*sql-formatter-disable*/ ?!{}[] /*sql-formatter-enable*/ 61 | FROM 62 | bar; 63 | `); 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /test/features/dropTable.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | interface DropTableConfig { 6 | ifExists?: boolean; 7 | } 8 | 9 | export default function supportsDropTable(format: FormatFn, { ifExists }: DropTableConfig = {}) { 10 | it('formats DROP TABLE statement', () => { 11 | const result = format('DROP TABLE admin_role;'); 12 | expect(result).toBe(dedent` 13 | DROP TABLE admin_role; 14 | `); 15 | }); 16 | 17 | if (ifExists) { 18 | it('formats DROP TABLE IF EXISTS statement', () => { 19 | const result = format('DROP TABLE IF EXISTS admin_role;'); 20 | expect(result).toBe(dedent` 21 | DROP TABLE IF EXISTS admin_role; 22 | `); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/features/insertInto.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | interface InsertIntoConfig { 6 | withoutInto?: boolean; 7 | } 8 | 9 | export default function supportsInsertInto( 10 | format: FormatFn, 11 | { withoutInto }: InsertIntoConfig = {} 12 | ) { 13 | it('formats simple INSERT INTO', () => { 14 | const result = format( 15 | "INSERT INTO Customers (ID, MoneyBalance, Address, City) VALUES (12,-123.4, 'Skagen 2111','Stv');" 16 | ); 17 | expect(result).toBe(dedent` 18 | INSERT INTO 19 | Customers (ID, MoneyBalance, Address, City) 20 | VALUES 21 | (12, -123.4, 'Skagen 2111', 'Stv'); 22 | `); 23 | }); 24 | 25 | if (withoutInto) { 26 | it('formats INSERT without INTO', () => { 27 | const result = format( 28 | "INSERT Customers (ID, MoneyBalance, Address, City) VALUES (12,-123.4, 'Skagen 2111','Stv');" 29 | ); 30 | expect(result).toBe(dedent` 31 | INSERT 32 | Customers (ID, MoneyBalance, Address, City) 33 | VALUES 34 | (12, -123.4, 'Skagen 2111', 'Stv'); 35 | `); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/features/isDistinctFrom.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | import { FormatFn } from '../../src/sqlFormatter.js'; 3 | 4 | export default function supportsIsDistinctFrom(format: FormatFn) { 5 | // Regression test for #564 6 | it('formats IS [NOT] DISTINCT FROM operator', () => { 7 | expect(format('SELECT x IS DISTINCT FROM y, x IS NOT DISTINCT FROM y')).toBe(dedent` 8 | SELECT 9 | x IS DISTINCT FROM y, 10 | x IS NOT DISTINCT FROM y 11 | `); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /test/features/join.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | interface Options { 6 | without?: string[]; 7 | additionally?: string[]; 8 | supportsUsing?: boolean; 9 | supportsApply?: boolean; 10 | } 11 | 12 | export default function supportsJoin( 13 | format: FormatFn, 14 | { without, additionally, supportsUsing = true, supportsApply }: Options = {} 15 | ) { 16 | const unsupportedJoinRegex = without ? new RegExp(without.join('|'), 'u') : /^whateve_!%&$/u; 17 | const isSupportedJoin = (join: string) => !unsupportedJoinRegex.test(join); 18 | 19 | [ 20 | 'JOIN', 21 | 'INNER JOIN', 22 | 'CROSS JOIN', 23 | 'LEFT JOIN', 24 | 'LEFT OUTER JOIN', 25 | 'RIGHT JOIN', 26 | 'RIGHT OUTER JOIN', 27 | 'FULL JOIN', 28 | 'FULL OUTER JOIN', 29 | 'NATURAL JOIN', 30 | 'NATURAL INNER JOIN', 31 | 'NATURAL LEFT JOIN', 32 | 'NATURAL LEFT OUTER JOIN', 33 | 'NATURAL RIGHT JOIN', 34 | 'NATURAL RIGHT OUTER JOIN', 35 | 'NATURAL FULL JOIN', 36 | 'NATURAL FULL OUTER JOIN', 37 | ...(additionally || []), 38 | ] 39 | .filter(isSupportedJoin) 40 | .forEach(join => { 41 | it(`supports ${join}`, () => { 42 | const result = format(` 43 | SELECT * FROM customers 44 | ${join} orders ON customers.customer_id = orders.customer_id 45 | ${join} items ON items.id = orders.id; 46 | `); 47 | expect(result).toBe(dedent` 48 | SELECT 49 | * 50 | FROM 51 | customers 52 | ${join} orders ON customers.customer_id = orders.customer_id 53 | ${join} items ON items.id = orders.id; 54 | `); 55 | }); 56 | }); 57 | 58 | it('properly uppercases JOIN ... ON', () => { 59 | const result = format(`select * from customers join foo on foo.id = customers.id;`, { 60 | keywordCase: 'upper', 61 | }); 62 | expect(result).toBe(dedent` 63 | SELECT 64 | * 65 | FROM 66 | customers 67 | JOIN foo ON foo.id = customers.id; 68 | `); 69 | }); 70 | 71 | if (supportsUsing) { 72 | it('properly uppercases JOIN ... USING', () => { 73 | const result = format(`select * from customers join foo using (id);`, { 74 | keywordCase: 'upper', 75 | }); 76 | expect(result).toBe(dedent` 77 | SELECT 78 | * 79 | FROM 80 | customers 81 | JOIN foo USING (id); 82 | `); 83 | }); 84 | } 85 | 86 | if (supportsApply) { 87 | ['CROSS APPLY', 'OUTER APPLY'].forEach(apply => { 88 | // TODO: improve formatting of custom functions 89 | it(`supports ${apply}`, () => { 90 | const result = format(`SELECT * FROM customers ${apply} fn(customers.id)`); 91 | expect(result).toBe(dedent` 92 | SELECT 93 | * 94 | FROM 95 | customers 96 | ${apply} fn (customers.id) 97 | `); 98 | }); 99 | }); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/features/limiting.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | interface LimitingTypes { 6 | limit?: boolean; 7 | offset?: boolean; 8 | fetchFirst?: boolean; 9 | fetchNext?: boolean; 10 | } 11 | 12 | export default function supportsLimiting(format: FormatFn, types: LimitingTypes) { 13 | if (types.limit) { 14 | it('formats LIMIT with two comma-separated values on single line', () => { 15 | const result = format('SELECT * FROM tbl LIMIT 5, 10;'); 16 | expect(result).toBe(dedent` 17 | SELECT 18 | * 19 | FROM 20 | tbl 21 | LIMIT 22 | 5, 10; 23 | `); 24 | }); 25 | 26 | // Regression test for #303 27 | it('formats LIMIT with complex expressions', () => { 28 | const result = format('SELECT * FROM tbl LIMIT abs(-5) - 1, (2 + 3) * 5;'); 29 | expect(result).toBe(dedent` 30 | SELECT 31 | * 32 | FROM 33 | tbl 34 | LIMIT 35 | abs(-5) - 1, (2 + 3) * 5; 36 | `); 37 | }); 38 | 39 | // Regression test for #301 40 | it('formats LIMIT with comments', () => { 41 | const result = format('SELECT * FROM tbl LIMIT --comment\n 5,--comment\n6;'); 42 | expect(result).toBe(dedent` 43 | SELECT 44 | * 45 | FROM 46 | tbl 47 | LIMIT --comment 48 | 5, --comment 49 | 6; 50 | `); 51 | }); 52 | 53 | // Regression test for #412 54 | it('formats LIMIT in tabular style', () => { 55 | const result = format('SELECT * FROM tbl LIMIT 5, 6;', { indentStyle: 'tabularLeft' }); 56 | expect(result).toBe(dedent` 57 | SELECT * 58 | FROM tbl 59 | LIMIT 5, 6; 60 | `); 61 | }); 62 | } 63 | 64 | if (types.limit && types.offset) { 65 | it('formats LIMIT of single value and OFFSET', () => { 66 | const result = format('SELECT * FROM tbl LIMIT 5 OFFSET 8;'); 67 | expect(result).toBe(dedent` 68 | SELECT 69 | * 70 | FROM 71 | tbl 72 | LIMIT 73 | 5 74 | OFFSET 75 | 8; 76 | `); 77 | }); 78 | } 79 | 80 | if (types.fetchFirst) { 81 | it('formats FETCH FIRST', () => { 82 | const result = format('SELECT * FROM tbl FETCH FIRST 10 ROWS ONLY;'); 83 | expect(result).toBe(dedent` 84 | SELECT 85 | * 86 | FROM 87 | tbl 88 | FETCH FIRST 89 | 10 ROWS ONLY; 90 | `); 91 | }); 92 | } 93 | 94 | if (types.fetchNext) { 95 | it('formats FETCH NEXT', () => { 96 | const result = format('SELECT * FROM tbl FETCH NEXT 1 ROW ONLY;'); 97 | expect(result).toBe(dedent` 98 | SELECT 99 | * 100 | FROM 101 | tbl 102 | FETCH NEXT 103 | 1 ROW ONLY; 104 | `); 105 | }); 106 | } 107 | 108 | if (types.fetchFirst && types.offset) { 109 | it('formats OFFSET ... FETCH FIRST', () => { 110 | const result = format('SELECT * FROM tbl OFFSET 250 ROWS FETCH FIRST 5 ROWS ONLY;'); 111 | expect(result).toBe(dedent` 112 | SELECT 113 | * 114 | FROM 115 | tbl 116 | OFFSET 117 | 250 ROWS 118 | FETCH FIRST 119 | 5 ROWS ONLY; 120 | `); 121 | }); 122 | } 123 | 124 | if (types.fetchNext && types.offset) { 125 | it('formats OFFSET ... FETCH FIRST', () => { 126 | const result = format('SELECT * FROM tbl OFFSET 250 ROWS FETCH NEXT 5 ROWS ONLY;'); 127 | expect(result).toBe(dedent` 128 | SELECT 129 | * 130 | FROM 131 | tbl 132 | OFFSET 133 | 250 ROWS 134 | FETCH NEXT 135 | 5 ROWS ONLY; 136 | `); 137 | }); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /test/features/mergeInto.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsMergeInto(format: FormatFn) { 6 | it('formats MERGE INTO', () => { 7 | const result = format( 8 | `MERGE INTO DetailedInventory AS t 9 | USING Inventory AS i 10 | ON t.product = i.product 11 | WHEN MATCHED THEN 12 | UPDATE SET quantity = t.quantity + i.quantity 13 | WHEN NOT MATCHED THEN 14 | INSERT (product, quantity) VALUES ('Horse saddle', 12);` 15 | ); 16 | // The indentation here is not ideal, but at least it's not a complete crap 17 | expect(result).toBe(dedent` 18 | MERGE INTO 19 | DetailedInventory AS t USING Inventory AS i ON t.product = i.product 20 | WHEN MATCHED THEN 21 | UPDATE SET 22 | quantity = t.quantity + i.quantity 23 | WHEN NOT MATCHED THEN 24 | INSERT 25 | (product, quantity) 26 | VALUES 27 | ('Horse saddle', 12); 28 | `); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /test/features/numbers.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsNumbers(format: FormatFn) { 6 | it('supports decimal numbers', () => { 7 | const result = format('SELECT 42, -35.04, 105., 2.53E+3, 1.085E-5;'); 8 | expect(result).toBe(dedent` 9 | SELECT 10 | 42, 11 | -35.04, 12 | 105., 13 | 2.53E+3, 14 | 1.085E-5; 15 | `); 16 | }); 17 | 18 | it('supports hex and binary numbers', () => { 19 | const result = format('SELECT 0xAE, 0x10F, 0b1010001;'); 20 | expect(result).toBe(dedent` 21 | SELECT 22 | 0xAE, 23 | 0x10F, 24 | 0b1010001; 25 | `); 26 | }); 27 | 28 | it('correctly handles floats as single tokens', () => { 29 | const result = format('SELECT 1e-9 AS a, 1.5e+10 AS b, 3.5E12 AS c, 3.5e12 AS d;'); 30 | expect(result).toBe(dedent` 31 | SELECT 32 | 1e-9 AS a, 33 | 1.5e+10 AS b, 34 | 3.5E12 AS c, 35 | 3.5e12 AS d; 36 | `); 37 | }); 38 | 39 | it('correctly handles floats with trailing point', () => { 40 | let result = format('SELECT 1000. AS a;'); 41 | expect(result).toBe(dedent` 42 | SELECT 43 | 1000. AS a; 44 | `); 45 | 46 | result = format('SELECT a, b / 1000. AS a_s, 100. * b / SUM(a_s);'); 47 | expect(result).toBe(dedent` 48 | SELECT 49 | a, 50 | b / 1000. AS a_s, 51 | 100. * b / SUM(a_s); 52 | `); 53 | }); 54 | 55 | it('supports decimal values without leading digits', () => { 56 | const result = format(`SELECT .456 AS foo;`); 57 | expect(result).toBe(dedent` 58 | SELECT 59 | .456 AS foo; 60 | `); 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /test/features/onConflict.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsOnConflict(format: FormatFn) { 6 | // Regression test for issue #535 7 | it('supports INSERT .. ON CONFLICT syntax', () => { 8 | expect(format(`INSERT INTO tbl VALUES (1,'Blah') ON CONFLICT DO NOTHING;`)).toBe(dedent` 9 | INSERT INTO 10 | tbl 11 | VALUES 12 | (1, 'Blah') 13 | ON CONFLICT DO NOTHING; 14 | `); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /test/features/operators.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | type OperatorsConfig = { 6 | logicalOperators?: string[]; 7 | any?: boolean; 8 | }; 9 | 10 | export default function supportsOperators( 11 | format: FormatFn, 12 | operators: string[], 13 | cfg: OperatorsConfig = {} 14 | ) { 15 | // Always test for standard SQL operators 16 | const standardOperators = ['+', '-', '*', '/', '>', '<', '=', '<>', '<=', '>=', '!=']; 17 | operators = [...standardOperators, ...operators]; 18 | 19 | operators.forEach(op => { 20 | it(`supports ${op} operator`, () => { 21 | // Would be simpler to test with "foo${op}bar" 22 | // but this doesn't work with "-" operator in bigQuery, 23 | // where foo-bar is detected as identifier 24 | expect(format(`foo${op} bar ${op}zap`)).toBe(`foo ${op} bar ${op} zap`); 25 | }); 26 | }); 27 | 28 | operators.forEach(op => { 29 | it(`supports ${op} operator in dense mode`, () => { 30 | expect(format(`foo ${op} bar`, { denseOperators: true })).toBe(`foo${op}bar`); 31 | }); 32 | }); 33 | 34 | (cfg.logicalOperators || ['AND', 'OR']).forEach(op => { 35 | it(`supports ${op} operator`, () => { 36 | const result = format(`SELECT true ${op} false AS foo;`); 37 | expect(result).toBe(dedent` 38 | SELECT 39 | true 40 | ${op} false AS foo; 41 | `); 42 | }); 43 | }); 44 | 45 | it('supports set operators', () => { 46 | expect(format('foo ALL bar')).toBe('foo ALL bar'); 47 | expect(format('EXISTS bar')).toBe('EXISTS bar'); 48 | expect(format('foo IN (1, 2, 3)')).toBe('foo IN (1, 2, 3)'); 49 | expect(format("foo LIKE 'hello%'")).toBe("foo LIKE 'hello%'"); 50 | expect(format('foo IS NULL')).toBe('foo IS NULL'); 51 | expect(format('UNIQUE foo')).toBe('UNIQUE foo'); 52 | }); 53 | 54 | if (cfg.any) { 55 | it('supports ANY set-operator', () => { 56 | expect(format('foo = ANY (1, 2, 3)')).toBe('foo = ANY (1, 2, 3)'); 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/features/returning.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsReturning(format: FormatFn) { 6 | it('places RETURNING to new line', () => { 7 | const result = format( 8 | "INSERT INTO users (firstname, lastname) VALUES ('Joe', 'Cool') RETURNING id, firstname;" 9 | ); 10 | expect(result).toBe(dedent` 11 | INSERT INTO 12 | users (firstname, lastname) 13 | VALUES 14 | ('Joe', 'Cool') 15 | RETURNING 16 | id, 17 | firstname; 18 | `); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /test/features/schema.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsSchema(format: FormatFn) { 6 | it('formats simple SET SCHEMA statements', () => { 7 | const result = format('SET SCHEMA schema1;'); 8 | expect(result).toBe(dedent` 9 | SET SCHEMA schema1; 10 | `); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /test/features/setOperations.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export const standardSetOperations = [ 6 | 'UNION', 7 | 'UNION ALL', 8 | 'UNION DISTINCT', 9 | 'EXCEPT', 10 | 'EXCEPT ALL', 11 | 'EXCEPT DISTINCT', 12 | 'INTERSECT', 13 | 'INTERSECT ALL', 14 | 'INTERSECT DISTINCT', 15 | ]; 16 | 17 | export default function supportsSetOperations( 18 | format: FormatFn, 19 | operations: string[] = standardSetOperations 20 | ) { 21 | operations.forEach(op => { 22 | it(`formats ${op}`, () => { 23 | expect(format(`SELECT * FROM foo ${op} SELECT * FROM bar;`)).toBe(dedent` 24 | SELECT 25 | * 26 | FROM 27 | foo 28 | ${op} 29 | SELECT 30 | * 31 | FROM 32 | bar; 33 | `); 34 | }); 35 | 36 | it(`formats ${op} inside subquery`, () => { 37 | expect(format(`SELECT * FROM (SELECT * FROM foo ${op} SELECT * FROM bar) AS tbl;`)) 38 | .toBe(dedent` 39 | SELECT 40 | * 41 | FROM 42 | ( 43 | SELECT 44 | * 45 | FROM 46 | foo 47 | ${op} 48 | SELECT 49 | * 50 | FROM 51 | bar 52 | ) AS tbl; 53 | `); 54 | }); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /test/features/truncateTable.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | interface TruncateTableConfig { 6 | withTable?: boolean; 7 | withoutTable?: boolean; 8 | } 9 | 10 | export default function supportsTruncateTable( 11 | format: FormatFn, 12 | { withTable = true, withoutTable }: TruncateTableConfig = {} 13 | ) { 14 | if (withTable) { 15 | it('formats TRUNCATE TABLE statement', () => { 16 | const result = format('TRUNCATE TABLE Customers;'); 17 | expect(result).toBe(dedent` 18 | TRUNCATE TABLE Customers; 19 | `); 20 | }); 21 | } 22 | 23 | if (withoutTable) { 24 | it('formats TRUNCATE statement (without TABLE)', () => { 25 | const result = format('TRUNCATE Customers;'); 26 | expect(result).toBe(dedent` 27 | TRUNCATE Customers; 28 | `); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/features/update.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | interface UpdateConfig { 6 | whereCurrentOf?: boolean; 7 | } 8 | 9 | export default function supportsUpdate(format: FormatFn, { whereCurrentOf }: UpdateConfig = {}) { 10 | it('formats simple UPDATE statement', () => { 11 | const result = format( 12 | "UPDATE Customers SET ContactName='Alfred Schmidt', City='Hamburg' WHERE CustomerName='Alfreds Futterkiste';" 13 | ); 14 | expect(result).toBe(dedent` 15 | UPDATE Customers 16 | SET 17 | ContactName = 'Alfred Schmidt', 18 | City = 'Hamburg' 19 | WHERE 20 | CustomerName = 'Alfreds Futterkiste'; 21 | `); 22 | }); 23 | 24 | it('formats UPDATE statement with AS part', () => { 25 | const result = format( 26 | 'UPDATE customers SET total_orders = order_summary.total FROM ( SELECT * FROM bank) AS order_summary' 27 | ); 28 | expect(result).toBe(dedent` 29 | UPDATE customers 30 | SET 31 | total_orders = order_summary.total 32 | FROM 33 | ( 34 | SELECT 35 | * 36 | FROM 37 | bank 38 | ) AS order_summary 39 | `); 40 | }); 41 | 42 | if (whereCurrentOf) { 43 | it('formats UPDATE statement with cursor position', () => { 44 | const result = format("UPDATE Customers SET Name='John' WHERE CURRENT OF my_cursor;"); 45 | expect(result).toBe(dedent` 46 | UPDATE Customers 47 | SET 48 | Name = 'John' 49 | WHERE CURRENT OF my_cursor; 50 | `); 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/features/window.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsWindow(format: FormatFn) { 6 | it('formats WINDOW clause at top level', () => { 7 | const result = format( 8 | 'SELECT *, ROW_NUMBER() OVER wnd AS next_value FROM tbl WINDOW wnd AS (PARTITION BY id ORDER BY time);' 9 | ); 10 | expect(result).toBe(dedent` 11 | SELECT 12 | *, 13 | ROW_NUMBER() OVER wnd AS next_value 14 | FROM 15 | tbl 16 | WINDOW 17 | wnd AS ( 18 | PARTITION BY 19 | id 20 | ORDER BY 21 | time 22 | ); 23 | `); 24 | }); 25 | 26 | it('formats multiple WINDOW specifications', () => { 27 | const result = format( 28 | 'SELECT * FROM table1 WINDOW w1 AS (PARTITION BY col1), w2 AS (PARTITION BY col1, col2);' 29 | ); 30 | expect(result).toBe(dedent` 31 | SELECT 32 | * 33 | FROM 34 | table1 35 | WINDOW 36 | w1 AS ( 37 | PARTITION BY 38 | col1 39 | ), 40 | w2 AS ( 41 | PARTITION BY 42 | col1, 43 | col2 44 | ); 45 | `); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /test/features/windowFunctions.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsWindowFunctions(format: FormatFn) { 6 | it('supports ROWS BETWEEN in window functions', () => { 7 | expect( 8 | format(` 9 | SELECT 10 | RANK() OVER ( 11 | PARTITION BY explosion 12 | ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW 13 | ) AS amount 14 | FROM 15 | tbl 16 | `) 17 | ).toBe(dedent` 18 | SELECT 19 | RANK() OVER ( 20 | PARTITION BY 21 | explosion 22 | ORDER BY 23 | day ROWS BETWEEN 6 PRECEDING 24 | AND CURRENT ROW 25 | ) AS amount 26 | FROM 27 | tbl 28 | `); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /test/features/with.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsWith(format: FormatFn) { 6 | it('formats WITH clause with multiple Common Table Expressions (CTE)', () => { 7 | const result = format(` 8 | WITH 9 | cte_1 AS ( 10 | SELECT a FROM b WHERE c = 1 11 | ), 12 | cte_2 AS ( 13 | SELECT c FROM d WHERE e = 2 14 | ), 15 | final AS ( 16 | SELECT * FROM cte_1 LEFT JOIN cte_2 ON b = d 17 | ) 18 | SELECT * FROM final; 19 | `); 20 | expect(result).toBe(dedent` 21 | WITH 22 | cte_1 AS ( 23 | SELECT 24 | a 25 | FROM 26 | b 27 | WHERE 28 | c = 1 29 | ), 30 | cte_2 AS ( 31 | SELECT 32 | c 33 | FROM 34 | d 35 | WHERE 36 | e = 2 37 | ), 38 | final AS ( 39 | SELECT 40 | * 41 | FROM 42 | cte_1 43 | LEFT JOIN cte_2 ON b = d 44 | ) 45 | SELECT 46 | * 47 | FROM 48 | final; 49 | `); 50 | }); 51 | 52 | it('formats WITH clause with parameterized CTE', () => { 53 | const result = format(` 54 | WITH cte_1(id, parent_id) AS ( 55 | SELECT id, parent_id 56 | FROM tab1 57 | WHERE parent_id IS NULL 58 | ) 59 | SELECT id, parent_id FROM cte_1; 60 | `); 61 | expect(result).toBe(dedent` 62 | WITH 63 | cte_1 (id, parent_id) AS ( 64 | SELECT 65 | id, 66 | parent_id 67 | FROM 68 | tab1 69 | WHERE 70 | parent_id IS NULL 71 | ) 72 | SELECT 73 | id, 74 | parent_id 75 | FROM 76 | cte_1; 77 | `); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /test/hive.test.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { format as originalFormat, FormatFn } from '../src/sqlFormatter.js'; 4 | 5 | import behavesLikeSqlFormatter from './behavesLikeSqlFormatter.js'; 6 | 7 | import supportsCreateTable from './features/createTable.js'; 8 | import supportsDropTable from './features/dropTable.js'; 9 | import supportsAlterTable from './features/alterTable.js'; 10 | import supportsStrings from './features/strings.js'; 11 | import supportsBetween from './features/between.js'; 12 | import supportsJoin from './features/join.js'; 13 | import supportsOperators from './features/operators.js'; 14 | import supportsArrayAndMapAccessors from './features/arrayAndMapAccessors.js'; 15 | import supportsComments from './features/comments.js'; 16 | import supportsIdentifiers from './features/identifiers.js'; 17 | import supportsWindow from './features/window.js'; 18 | import supportsSetOperations from './features/setOperations.js'; 19 | import supportsLimiting from './features/limiting.js'; 20 | import supportsUpdate from './features/update.js'; 21 | import supportsDeleteFrom from './features/deleteFrom.js'; 22 | import supportsTruncateTable from './features/truncateTable.js'; 23 | import supportsMergeInto from './features/mergeInto.js'; 24 | import supportsCreateView from './features/createView.js'; 25 | import supportsDataTypeCase from './options/dataTypeCase.js'; 26 | 27 | describe('HiveFormatter', () => { 28 | const language = 'hive'; 29 | const format: FormatFn = (query, cfg = {}) => originalFormat(query, { ...cfg, language }); 30 | 31 | behavesLikeSqlFormatter(format); 32 | supportsComments(format); 33 | supportsCreateView(format, { materialized: true, ifNotExists: true }); 34 | supportsCreateTable(format, { ifNotExists: true }); 35 | supportsDropTable(format, { ifExists: true }); 36 | supportsAlterTable(format, { renameTo: true }); 37 | supportsUpdate(format); 38 | supportsDeleteFrom(format); 39 | supportsTruncateTable(format, { withoutTable: true }); 40 | supportsMergeInto(format); 41 | supportsStrings(format, ['""-bs', "''-bs"]); 42 | supportsIdentifiers(format, ['``']); 43 | supportsBetween(format); 44 | supportsJoin(format, { 45 | without: ['NATURAL'], 46 | additionally: ['LEFT SEMI JOIN'], 47 | supportsUsing: false, 48 | }); 49 | supportsSetOperations(format, ['UNION', 'UNION ALL', 'UNION DISTINCT']); 50 | supportsOperators(format, ['%', '~', '^', '|', '&', '<=>', '==', '!', '||'], { any: true }); 51 | supportsArrayAndMapAccessors(format); 52 | supportsWindow(format); 53 | supportsLimiting(format, { limit: true }); 54 | supportsDataTypeCase(format); 55 | 56 | // eslint-disable-next-line no-template-curly-in-string 57 | it('recognizes ${hivevar:name} substitution variables', () => { 58 | const result = format( 59 | // eslint-disable-next-line no-template-curly-in-string 60 | "SELECT ${var1}, ${ var 2 } FROM ${hivevar:table_name} WHERE name = '${hivevar:name}';" 61 | ); 62 | expect(result).toBe(dedent` 63 | SELECT 64 | \${var1}, 65 | \${ var 2 } 66 | FROM 67 | \${hivevar:table_name} 68 | WHERE 69 | name = '\${hivevar:name}'; 70 | `); 71 | }); 72 | 73 | it('supports SORT BY, CLUSTER BY, DISTRIBUTE BY', () => { 74 | const result = format( 75 | 'SELECT value, count DISTRIBUTE BY count CLUSTER BY value SORT BY value, count;' 76 | ); 77 | expect(result).toBe(dedent` 78 | SELECT 79 | value, 80 | count 81 | DISTRIBUTE BY 82 | count 83 | CLUSTER BY 84 | value 85 | SORT BY 86 | value, 87 | count; 88 | `); 89 | }); 90 | 91 | it('formats INSERT INTO TABLE', () => { 92 | const result = format("INSERT INTO TABLE Customers VALUES (12,-123.4, 'Skagen 2111','Stv');"); 93 | expect(result).toBe(dedent` 94 | INSERT INTO TABLE 95 | Customers 96 | VALUES 97 | (12, -123.4, 'Skagen 2111', 'Stv'); 98 | `); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /test/mariadb.test.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { format as originalFormat, FormatFn } from '../src/sqlFormatter.js'; 4 | import behavesLikeMariaDbFormatter from './behavesLikeMariaDbFormatter.js'; 5 | 6 | import supportsJoin from './features/join.js'; 7 | import supportsOperators from './features/operators.js'; 8 | import supportsReturning from './features/returning.js'; 9 | import supportsSetOperations, { standardSetOperations } from './features/setOperations.js'; 10 | import supportsLimiting from './features/limiting.js'; 11 | import supportsCreateTable from './features/createTable.js'; 12 | import supportsParams from './options/param.js'; 13 | import supportsCreateView from './features/createView.js'; 14 | import supportsAlterTable from './features/alterTable.js'; 15 | import supportsStrings from './features/strings.js'; 16 | import supportsConstraints from './features/constraints.js'; 17 | import supportsDataTypeCase from './options/dataTypeCase.js'; 18 | 19 | describe('MariaDbFormatter', () => { 20 | const language = 'mariadb'; 21 | const format: FormatFn = (query, cfg = {}) => originalFormat(query, { ...cfg, language }); 22 | 23 | behavesLikeMariaDbFormatter(format); 24 | 25 | // in addition to string types listed in behavesLikeMariaDbFormatter 26 | supportsStrings(format, ["B''"]); 27 | 28 | supportsJoin(format, { 29 | without: ['FULL', 'NATURAL INNER JOIN'], 30 | additionally: ['STRAIGHT_JOIN'], 31 | }); 32 | supportsSetOperations(format, [...standardSetOperations, 'MINUS', 'MINUS ALL', 'MINUS DISTINCT']); 33 | supportsOperators(format, ['%', ':=', '&', '|', '^', '~', '<<', '>>', '<=>', '&&', '||', '!'], { 34 | logicalOperators: ['AND', 'OR', 'XOR'], 35 | any: true, 36 | }); 37 | supportsReturning(format); 38 | supportsLimiting(format, { limit: true, offset: true, fetchFirst: true, fetchNext: true }); 39 | supportsCreateTable(format, { 40 | orReplace: true, 41 | ifNotExists: true, 42 | columnComment: true, 43 | tableComment: true, 44 | }); 45 | supportsConstraints(format, ['RESTRICT', 'CASCADE', 'SET NULL', 'NO ACTION', 'SET DEFAULT']); 46 | supportsParams(format, { positional: true }); 47 | supportsCreateView(format, { orReplace: true, ifNotExists: true }); 48 | supportsAlterTable(format, { 49 | addColumn: true, 50 | dropColumn: true, 51 | modify: true, 52 | renameTo: true, 53 | renameColumn: true, 54 | }); 55 | supportsDataTypeCase(format); 56 | 57 | it(`supports @"name" variables`, () => { 58 | expect(format(`SELECT @"foo fo", @"foo\\"x", @"foo""y" FROM tbl;`)).toBe(dedent` 59 | SELECT 60 | @"foo fo", 61 | @"foo\\"x", 62 | @"foo""y" 63 | FROM 64 | tbl; 65 | `); 66 | }); 67 | 68 | it(`supports @'name' variables`, () => { 69 | expect(format(`SELECT @'bar ar', @'bar\\'x', @'bar''y' FROM tbl;`)).toBe(dedent` 70 | SELECT 71 | @'bar ar', 72 | @'bar\\'x', 73 | @'bar''y' 74 | FROM 75 | tbl; 76 | `); 77 | }); 78 | 79 | it('formats ALTER TABLE ... ALTER COLUMN', () => { 80 | expect( 81 | format( 82 | `ALTER TABLE t ALTER COLUMN foo SET DEFAULT 10; 83 | ALTER TABLE t ALTER COLUMN foo DROP DEFAULT;` 84 | ) 85 | ).toBe(dedent` 86 | ALTER TABLE t 87 | ALTER COLUMN foo 88 | SET DEFAULT 10; 89 | 90 | ALTER TABLE t 91 | ALTER COLUMN foo 92 | DROP DEFAULT; 93 | `); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /test/mysql.test.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { format as originalFormat, FormatFn } from '../src/sqlFormatter.js'; 4 | import behavesLikeMariaDbFormatter from './behavesLikeMariaDbFormatter.js'; 5 | 6 | import supportsJoin from './features/join.js'; 7 | import supportsOperators from './features/operators.js'; 8 | import supportsWindow from './features/window.js'; 9 | import supportsSetOperations from './features/setOperations.js'; 10 | import supportsLimiting from './features/limiting.js'; 11 | import supportsCreateTable from './features/createTable.js'; 12 | import supportsParams from './options/param.js'; 13 | import supportsCreateView from './features/createView.js'; 14 | import supportsAlterTable from './features/alterTable.js'; 15 | import supportsStrings from './features/strings.js'; 16 | import supportsConstraints from './features/constraints.js'; 17 | import supportsDataTypeCase from './options/dataTypeCase.js'; 18 | 19 | describe('MySqlFormatter', () => { 20 | const language = 'mysql'; 21 | const format: FormatFn = (query, cfg = {}) => originalFormat(query, { ...cfg, language }); 22 | 23 | behavesLikeMariaDbFormatter(format); 24 | 25 | // in addition to string types listed in behavesLikeMariaDbFormatter 26 | supportsStrings(format, ["N''"]); 27 | 28 | supportsJoin(format, { 29 | without: ['FULL'], 30 | additionally: ['STRAIGHT_JOIN'], 31 | }); 32 | supportsSetOperations(format, ['UNION', 'UNION ALL', 'UNION DISTINCT']); 33 | supportsOperators( 34 | format, 35 | ['%', ':=', '&', '|', '^', '~', '<<', '>>', '<=>', '->', '->>', '&&', '||', '!'], 36 | { 37 | logicalOperators: ['AND', 'OR', 'XOR'], 38 | any: true, 39 | } 40 | ); 41 | supportsWindow(format); 42 | supportsLimiting(format, { limit: true, offset: true }); 43 | supportsCreateTable(format, { ifNotExists: true, columnComment: true, tableComment: true }); 44 | supportsConstraints(format, [ 45 | 'RESTRICT', 46 | 'CASCADE', 47 | 'SET NULL', 48 | 'NO ACTION', 49 | 'NOW', 50 | 'CURRENT_TIMESTAMP', 51 | ]); 52 | supportsParams(format, { positional: true }); 53 | supportsCreateView(format, { orReplace: true }); 54 | supportsAlterTable(format, { 55 | addColumn: true, 56 | dropColumn: true, 57 | modify: true, 58 | renameTo: true, 59 | renameColumn: true, 60 | }); 61 | supportsDataTypeCase(format); 62 | 63 | it(`supports @"name" variables`, () => { 64 | expect(format(`SELECT @"foo fo", @"foo\\"x", @"foo""y" FROM tbl;`)).toBe(dedent` 65 | SELECT 66 | @"foo fo", 67 | @"foo\\"x", 68 | @"foo""y" 69 | FROM 70 | tbl; 71 | `); 72 | }); 73 | 74 | it(`supports @'name' variables`, () => { 75 | expect(format(`SELECT @'bar ar', @'bar\\'x', @'bar''y' FROM tbl;`)).toBe(dedent` 76 | SELECT 77 | @'bar ar', 78 | @'bar\\'x', 79 | @'bar''y' 80 | FROM 81 | tbl; 82 | `); 83 | }); 84 | 85 | it('formats ALTER TABLE ... ALTER COLUMN', () => { 86 | expect( 87 | format( 88 | `ALTER TABLE t ALTER COLUMN foo SET DEFAULT 10; 89 | ALTER TABLE t ALTER COLUMN foo DROP DEFAULT;` 90 | ) 91 | ).toBe(dedent` 92 | ALTER TABLE t 93 | ALTER COLUMN foo 94 | SET DEFAULT 10; 95 | 96 | ALTER TABLE t 97 | ALTER COLUMN foo 98 | DROP DEFAULT; 99 | `); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /test/options/dataTypeCase.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsDataTypeCase(format: FormatFn) { 6 | it('preserves data type keyword case by default', () => { 7 | const result = format( 8 | 'CREATE TABLE users ( user_id iNt PRIMARY KEY, total_earnings Decimal(5, 2) NOT NULL )' 9 | ); 10 | expect(result).toBe(dedent` 11 | CREATE TABLE users ( 12 | user_id iNt PRIMARY KEY, 13 | total_earnings Decimal(5, 2) NOT NULL 14 | ) 15 | `); 16 | }); 17 | 18 | it('converts data type keyword case to uppercase', () => { 19 | const result = format( 20 | 'CREATE TABLE users ( user_id iNt PRIMARY KEY, total_earnings Decimal(5, 2) NOT NULL )', 21 | { 22 | dataTypeCase: 'upper', 23 | } 24 | ); 25 | expect(result).toBe(dedent` 26 | CREATE TABLE users ( 27 | user_id INT PRIMARY KEY, 28 | total_earnings DECIMAL(5, 2) NOT NULL 29 | ) 30 | `); 31 | }); 32 | 33 | it('converts data type keyword case to lowercase', () => { 34 | const result = format( 35 | 'CREATE TABLE users ( user_id iNt PRIMARY KEY, total_earnings Decimal(5, 2) NOT NULL )', 36 | { 37 | dataTypeCase: 'lower', 38 | } 39 | ); 40 | expect(result).toBe(dedent` 41 | CREATE TABLE users ( 42 | user_id int PRIMARY KEY, 43 | total_earnings decimal(5, 2) NOT NULL 44 | ) 45 | `); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /test/options/expressionWidth.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsExpressionWidth(format: FormatFn) { 6 | it('throws error when expressionWidth negative number', () => { 7 | expect(() => { 8 | format('SELECT *', { expressionWidth: -2 }); 9 | }).toThrowErrorMatchingInlineSnapshot( 10 | `"expressionWidth config must be positive number. Received -2 instead."` 11 | ); 12 | }); 13 | 14 | it('throws error when expressionWidth is zero', () => { 15 | expect(() => { 16 | format('SELECT *', { expressionWidth: 0 }); 17 | }).toThrowErrorMatchingInlineSnapshot( 18 | `"expressionWidth config must be positive number. Received 0 instead."` 19 | ); 20 | }); 21 | 22 | it('breaks paranthesized expressions to multiple lines when they exceed expressionWidth', () => { 23 | const result = format( 24 | 'SELECT product.price + (product.original_price * product.sales_tax) AS total FROM product;', 25 | { 26 | expressionWidth: 40, 27 | } 28 | ); 29 | expect(result).toBe(dedent` 30 | SELECT 31 | product.price + ( 32 | product.original_price * product.sales_tax 33 | ) AS total 34 | FROM 35 | product; 36 | `); 37 | }); 38 | 39 | it('keeps paranthesized expressions on single lines when they do not exceed expressionWidth', () => { 40 | const result = format( 41 | 'SELECT product.price + (product.original_price * product.sales_tax) AS total FROM product;', 42 | { 43 | expressionWidth: 50, 44 | } 45 | ); 46 | expect(result).toBe(dedent` 47 | SELECT 48 | product.price + (product.original_price * product.sales_tax) AS total 49 | FROM 50 | product; 51 | `); 52 | }); 53 | 54 | it('calculates parenthesized expression length (also considering spaces)', () => { 55 | const result = format('SELECT (price * tax) AS total FROM table_name WHERE (amount > 25);', { 56 | expressionWidth: 10, 57 | denseOperators: true, 58 | }); 59 | expect(result).toBe(dedent` 60 | SELECT 61 | (price*tax) AS total 62 | FROM 63 | table_name 64 | WHERE 65 | (amount>25); 66 | `); 67 | }); 68 | 69 | it('formats inline when length of substituted parameters < expressionWidth', () => { 70 | const result = format('SELECT (?, ?, ?) AS total;', { 71 | expressionWidth: 11, 72 | paramTypes: { positional: true }, 73 | params: ['10', '20', '30'], 74 | }); 75 | expect(result).toBe(dedent` 76 | SELECT 77 | (10, 20, 30) AS total; 78 | `); 79 | }); 80 | 81 | it('formats NOT-inline when length of substituted parameters > expressionWidth', () => { 82 | const result = format('SELECT (?, ?, ?) AS total;', { 83 | expressionWidth: 11, 84 | paramTypes: { positional: true }, 85 | params: ['100', '200', '300'], 86 | }); 87 | expect(result).toBe(dedent` 88 | SELECT 89 | ( 90 | 100, 91 | 200, 92 | 300 93 | ) AS total; 94 | `); 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /test/options/functionCase.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsFunctionCase(format: FormatFn) { 6 | it('preserves function name case by default', () => { 7 | const result = format('SELECT MiN(price) AS min_price, Cast(item_code AS INT) FROM products'); 8 | expect(result).toBe(dedent` 9 | SELECT 10 | MiN(price) AS min_price, 11 | Cast(item_code AS INT) 12 | FROM 13 | products 14 | `); 15 | }); 16 | 17 | it('converts function names to uppercase', () => { 18 | const result = format('SELECT MiN(price) AS min_price, Cast(item_code AS INT) FROM products', { 19 | functionCase: 'upper', 20 | }); 21 | expect(result).toBe(dedent` 22 | SELECT 23 | MIN(price) AS min_price, 24 | CAST(item_code AS INT) 25 | FROM 26 | products 27 | `); 28 | }); 29 | 30 | it('converts function names to lowercase', () => { 31 | const result = format('SELECT MiN(price) AS min_price, Cast(item_code AS INT) FROM products', { 32 | functionCase: 'lower', 33 | }); 34 | expect(result).toBe(dedent` 35 | SELECT 36 | min(price) AS min_price, 37 | cast(item_code AS INT) 38 | FROM 39 | products 40 | `); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /test/options/identifierCase.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsIdentifierCase(format: FormatFn) { 6 | it('preserves identifier case by default', () => { 7 | const result = format( 8 | dedent` 9 | select Abc, 'mytext' as MyText from tBl1 left join Tbl2 where colA > 1 and colB = 3` 10 | ); 11 | expect(result).toBe(dedent` 12 | select 13 | Abc, 14 | 'mytext' as MyText 15 | from 16 | tBl1 17 | left join Tbl2 18 | where 19 | colA > 1 20 | and colB = 3 21 | `); 22 | }); 23 | 24 | it('converts identifiers to uppercase', () => { 25 | const result = format( 26 | dedent` 27 | select Abc, 'mytext' as MyText from tBl1 left join Tbl2 where colA > 1 and colB = 3`, 28 | { identifierCase: 'upper' } 29 | ); 30 | expect(result).toBe(dedent` 31 | select 32 | ABC, 33 | 'mytext' as MYTEXT 34 | from 35 | TBL1 36 | left join TBL2 37 | where 38 | COLA > 1 39 | and COLB = 3 40 | `); 41 | }); 42 | 43 | it('converts identifiers to lowercase', () => { 44 | const result = format( 45 | dedent` 46 | select Abc, 'mytext' as MyText from tBl1 left join Tbl2 where colA > 1 and colB = 3`, 47 | { identifierCase: 'lower' } 48 | ); 49 | expect(result).toBe(dedent` 50 | select 51 | abc, 52 | 'mytext' as mytext 53 | from 54 | tbl1 55 | left join tbl2 56 | where 57 | cola > 1 58 | and colb = 3 59 | `); 60 | }); 61 | 62 | it('does not uppercase quoted identifiers', () => { 63 | const result = format(`select "abc" as foo`, { 64 | identifierCase: 'upper', 65 | }); 66 | expect(result).toBe(dedent` 67 | select 68 | "abc" as FOO 69 | `); 70 | }); 71 | 72 | it('converts multi-part identifiers to uppercase', () => { 73 | const result = format('select Abc from Part1.Part2.Part3', { identifierCase: 'upper' }); 74 | expect(result).toBe(dedent` 75 | select 76 | ABC 77 | from 78 | PART1.PART2.PART3 79 | `); 80 | }); 81 | 82 | it('function names are not effected by identifierCase option', () => { 83 | const result = format('select count(*) from tbl', { identifierCase: 'upper' }); 84 | expect(result).toBe(dedent` 85 | select 86 | count(*) 87 | from 88 | TBL 89 | `); 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /test/options/keywordCase.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsKeywordCase(format: FormatFn) { 6 | it('preserves keyword case by default', () => { 7 | const result = format('select distinct * frOM foo left JOIN bar WHERe cola > 1 and colb = 3'); 8 | expect(result).toBe(dedent` 9 | select distinct 10 | * 11 | frOM 12 | foo 13 | left JOIN bar 14 | WHERe 15 | cola > 1 16 | and colb = 3 17 | `); 18 | }); 19 | 20 | it('converts keywords to uppercase', () => { 21 | const result = format( 22 | 'select distinct * frOM foo left JOIN mycol WHERe cola > 1 and colb = 3', 23 | { 24 | keywordCase: 'upper', 25 | } 26 | ); 27 | expect(result).toBe(dedent` 28 | SELECT DISTINCT 29 | * 30 | FROM 31 | foo 32 | LEFT JOIN mycol 33 | WHERE 34 | cola > 1 35 | AND colb = 3 36 | `); 37 | }); 38 | 39 | it('converts keywords to lowercase', () => { 40 | const result = format('select distinct * frOM foo left JOIN bar WHERe cola > 1 and colb = 3', { 41 | keywordCase: 'lower', 42 | }); 43 | expect(result).toBe(dedent` 44 | select distinct 45 | * 46 | from 47 | foo 48 | left join bar 49 | where 50 | cola > 1 51 | and colb = 3 52 | `); 53 | }); 54 | 55 | it('does not uppercase keywords inside strings', () => { 56 | const result = format(`select 'distinct' as foo`, { 57 | keywordCase: 'upper', 58 | }); 59 | expect(result).toBe(dedent` 60 | SELECT 61 | 'distinct' AS foo 62 | `); 63 | }); 64 | 65 | it('treats dot-seperated keywords as plain identifiers', () => { 66 | const result = format('select table.and from set.select', { 67 | keywordCase: 'upper', 68 | }); 69 | expect(result).toBe(dedent` 70 | SELECT 71 | table.and 72 | FROM 73 | set.select 74 | `); 75 | }); 76 | 77 | // regression test for #356 78 | it('formats multi-word reserved clauses into single line', () => { 79 | const result = format( 80 | `select * from mytable 81 | inner 82 | join 83 | mytable2 on mytable1.col1 = mytable2.col1 84 | where mytable2.col1 = 5 85 | group 86 | bY mytable1.col2 87 | order 88 | by 89 | mytable2.col3;`, 90 | { keywordCase: 'upper' } 91 | ); 92 | expect(result).toBe(dedent` 93 | SELECT 94 | * 95 | FROM 96 | mytable 97 | INNER JOIN mytable2 ON mytable1.col1 = mytable2.col1 98 | WHERE 99 | mytable2.col1 = 5 100 | GROUP BY 101 | mytable1.col2 102 | ORDER BY 103 | mytable2.col3; 104 | `); 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /test/options/linesBetweenQueries.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsLinesBetweenQueries(format: FormatFn) { 6 | it('defaults to single empty line between queries', () => { 7 | const result = format('SELECT * FROM foo; SELECT * FROM bar;'); 8 | expect(result).toBe(dedent` 9 | SELECT 10 | * 11 | FROM 12 | foo; 13 | 14 | SELECT 15 | * 16 | FROM 17 | bar; 18 | `); 19 | }); 20 | 21 | it('supports more empty lines between queries', () => { 22 | const result = format('SELECT * FROM foo; SELECT * FROM bar;', { linesBetweenQueries: 2 }); 23 | expect(result).toBe(dedent` 24 | SELECT 25 | * 26 | FROM 27 | foo; 28 | 29 | 30 | SELECT 31 | * 32 | FROM 33 | bar; 34 | `); 35 | }); 36 | 37 | it('supports no empty lines between queries', () => { 38 | const result = format('SELECT * FROM foo; SELECT * FROM bar;', { linesBetweenQueries: 0 }); 39 | expect(result).toBe(dedent` 40 | SELECT 41 | * 42 | FROM 43 | foo; 44 | SELECT 45 | * 46 | FROM 47 | bar; 48 | `); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /test/options/logicalOperatorNewline.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsLogicalOperatorNewline(format: FormatFn) { 6 | it('by default adds newline before logical operator', () => { 7 | const result = format('SELECT a WHERE true AND false;'); 8 | expect(result).toBe(dedent` 9 | SELECT 10 | a 11 | WHERE 12 | true 13 | AND false; 14 | `); 15 | }); 16 | 17 | it('supports newline after logical operator', () => { 18 | const result = format('SELECT a WHERE true AND false;', { 19 | logicalOperatorNewline: 'after', 20 | }); 21 | expect(result).toBe(dedent` 22 | SELECT 23 | a 24 | WHERE 25 | true AND 26 | false; 27 | `); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /test/options/newlineBeforeSemicolon.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsNewlineBeforeSemicolon(format: FormatFn) { 6 | it('formats lonely semicolon', () => { 7 | expect(format(';')).toBe(';'); 8 | }); 9 | 10 | it('does not add newline before lonely semicolon when newlineBeforeSemicolon:true', () => { 11 | expect(format(';', { newlineBeforeSemicolon: true })).toBe(';'); 12 | }); 13 | 14 | it('defaults to semicolon on end of last line', () => { 15 | const result = format(`SELECT a FROM b;`); 16 | expect(result).toBe(dedent` 17 | SELECT 18 | a 19 | FROM 20 | b; 21 | `); 22 | }); 23 | 24 | // A regression test for semicolon placement in single-line clauses like: 25 | // 26 | // ALTER TABLE 27 | // my_table 28 | // ALTER COLUMN 29 | // foo 30 | // DROP DEFAULT; <-- here 31 | // 32 | // Unfortunately there's really no such single-line clause that exists in all dialects, 33 | // so our test resorts to using somewhat invalid SQL. 34 | it('places semicolon on the same line as a single-line clause', () => { 35 | const result = format(`SELECT a FROM;`); 36 | expect(result).toBe(dedent` 37 | SELECT 38 | a 39 | FROM; 40 | `); 41 | }); 42 | 43 | it('supports semicolon on separate line', () => { 44 | const result = format(`SELECT a FROM b;`, { newlineBeforeSemicolon: true }); 45 | expect(result).toBe(dedent` 46 | SELECT 47 | a 48 | FROM 49 | b 50 | ; 51 | `); 52 | }); 53 | 54 | // the nr of empty lines here depends on linesBetweenQueries option 55 | it('formats multiple lonely semicolons', () => { 56 | expect(format(';;;')).toBe(dedent` 57 | ; 58 | 59 | ; 60 | 61 | ; 62 | `); 63 | }); 64 | 65 | it('does not introduce extra empty lines between semicolons when newlineBeforeSemicolon:true', () => { 66 | expect(format(';;;', { newlineBeforeSemicolon: true })).toBe(dedent` 67 | ; 68 | 69 | ; 70 | 71 | ; 72 | `); 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /test/options/tabWidth.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { FormatFn } from '../../src/sqlFormatter.js'; 4 | 5 | export default function supportsTabWidth(format: FormatFn) { 6 | it('indents with 2 spaces by default', () => { 7 | const result = format('SELECT count(*),Column1 FROM Table1;'); 8 | 9 | expect(result).toBe(dedent` 10 | SELECT 11 | count(*), 12 | Column1 13 | FROM 14 | Table1; 15 | `); 16 | }); 17 | 18 | it('supports indenting with 4 spaces', () => { 19 | const result = format('SELECT count(*),Column1 FROM Table1;', { 20 | tabWidth: 4, 21 | }); 22 | 23 | expect(result).toBe(dedent` 24 | SELECT 25 | count(*), 26 | Column1 27 | FROM 28 | Table1; 29 | `); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /test/options/useTabs.ts: -------------------------------------------------------------------------------- 1 | import { FormatFn } from '../../src/sqlFormatter.js'; 2 | 3 | export default function supportsUseTabs(format: FormatFn) { 4 | it('supports indenting with tabs', () => { 5 | const result = format('SELECT count(*),Column1 FROM Table1;', { 6 | useTabs: true, 7 | }); 8 | 9 | expect(result).toBe(['SELECT', '\tcount(*),', '\tColumn1', 'FROM', '\tTable1;'].join('\n')); 10 | }); 11 | 12 | it('ignores tabWidth when useTabs is enabled', () => { 13 | const result = format('SELECT count(*),Column1 FROM Table1;', { 14 | useTabs: true, 15 | tabWidth: 10, 16 | }); 17 | 18 | expect(result).toBe(['SELECT', '\tcount(*),', '\tColumn1', 'FROM', '\tTable1;'].join('\n')); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /test/perftest.ts: -------------------------------------------------------------------------------- 1 | import { format } from '../src/sqlFormatter.js'; 2 | 3 | const BASELINE = 300; 4 | 5 | describe('Performance test', () => { 6 | it('uses about 300 MB of memory to format empty query', () => { 7 | format('', { language: 'sql' }); 8 | 9 | expect(memoryUsageInMB()).toBeLessThan(BASELINE); 10 | }); 11 | 12 | // Issue #840 13 | it.skip('should use less than 100 MB of additional memory to format ~100 KB of SQL', () => { 14 | // Long list of values 15 | const values = Array(10000).fill('myid'); 16 | const sql = `SELECT ${values.join(', ')}`; 17 | expect(sql.length).toBeGreaterThan(50000); 18 | expect(sql.length).toBeLessThan(100000); 19 | 20 | format(sql, { language: 'sql' }); 21 | 22 | expect(memoryUsageInMB()).toBeLessThan(BASELINE + 100); 23 | }); 24 | }); 25 | 26 | function memoryUsageInMB() { 27 | return Math.round(process.memoryUsage().heapUsed / 1024 / 1024); 28 | } 29 | -------------------------------------------------------------------------------- /test/singlestoredb.test.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | import { format as originalFormat, FormatFn } from '../src/sqlFormatter.js'; 3 | import behavesLikeMariaDbFormatter from './behavesLikeMariaDbFormatter.js'; 4 | 5 | import supportsJoin from './features/join.js'; 6 | import supportsOperators from './features/operators.js'; 7 | import supportsSetOperations from './features/setOperations.js'; 8 | import supportsLimiting from './features/limiting.js'; 9 | import supportsCreateTable from './features/createTable.js'; 10 | import supportsCreateView from './features/createView.js'; 11 | import supportsAlterTable from './features/alterTable.js'; 12 | import supportsStrings from './features/strings.js'; 13 | import supportsDataTypeCase from './options/dataTypeCase.js'; 14 | 15 | describe('SingleStoreDbFormatter', () => { 16 | const language = 'singlestoredb'; 17 | const format: FormatFn = (query, cfg = {}) => originalFormat(query, { ...cfg, language }); 18 | 19 | behavesLikeMariaDbFormatter(format); 20 | 21 | // in addition to string types listed in behavesLikeMariaDbFormatter 22 | supportsStrings(format, ["B''"]); 23 | 24 | supportsJoin(format, { 25 | without: ['NATURAL INNER JOIN', 'NATURAL FULL', 'NATURAL JOIN'], 26 | additionally: ['STRAIGHT_JOIN'], 27 | }); 28 | supportsSetOperations(format, [ 29 | 'UNION', 30 | 'UNION ALL', 31 | 'UNION DISTINCT', 32 | 'EXCEPT', 33 | 'INTERSECT', 34 | 'MINUS', 35 | ]); 36 | supportsOperators( 37 | format, 38 | [':=', '&', '|', '^', '~', '<<', '>>', '<=>', '&&', '||', ':>', '!:>'], 39 | { any: true } 40 | ); 41 | supportsLimiting(format, { limit: true, offset: true }); 42 | supportsCreateTable(format, { ifNotExists: true, columnComment: true, tableComment: true }); 43 | supportsCreateView(format); 44 | supportsAlterTable(format, { 45 | addColumn: true, 46 | dropColumn: true, 47 | modify: true, 48 | renameTo: true, 49 | }); 50 | supportsDataTypeCase(format); 51 | 52 | describe(`formats traversal of semi structured data`, () => { 53 | it(`formats '::' path-operator without spaces`, () => { 54 | expect(format(`SELECT * FROM foo WHERE json_foo::bar = 'foobar'`)).toBe(dedent` 55 | SELECT 56 | * 57 | FROM 58 | foo 59 | WHERE 60 | json_foo::bar = 'foobar' 61 | `); 62 | }); 63 | it(`formats '::$' conversion path-operator without spaces`, () => { 64 | expect(format(`SELECT * FROM foo WHERE json_foo::$bar = 'foobar'`)).toBe(dedent` 65 | SELECT 66 | * 67 | FROM 68 | foo 69 | WHERE 70 | json_foo::$bar = 'foobar' 71 | `); 72 | }); 73 | it(`formats '::%' conversion path-operator without spaces`, () => { 74 | expect(format(`SELECT * FROM foo WHERE json_foo::%bar = 'foobar'`)).toBe(dedent` 75 | SELECT 76 | * 77 | FROM 78 | foo 79 | WHERE 80 | json_foo::%bar = 'foobar' 81 | `); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/sqlite.test.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | import { format as originalFormat, FormatFn } from '../src/sqlFormatter.js'; 3 | import behavesLikeSqlFormatter from './behavesLikeSqlFormatter.js'; 4 | 5 | import supportsCreateTable from './features/createTable.js'; 6 | import supportsDropTable from './features/dropTable.js'; 7 | import supportsAlterTable from './features/alterTable.js'; 8 | import supportsSchema from './features/schema.js'; 9 | import supportsStrings from './features/strings.js'; 10 | import supportsBetween from './features/between.js'; 11 | import supportsJoin from './features/join.js'; 12 | import supportsOperators from './features/operators.js'; 13 | import supportsConstraints from './features/constraints.js'; 14 | import supportsDeleteFrom from './features/deleteFrom.js'; 15 | import supportsComments from './features/comments.js'; 16 | import supportsIdentifiers from './features/identifiers.js'; 17 | import supportsParams from './options/param.js'; 18 | import supportsWindow from './features/window.js'; 19 | import supportsSetOperations from './features/setOperations.js'; 20 | import supportsLimiting from './features/limiting.js'; 21 | import supportsInsertInto from './features/insertInto.js'; 22 | import supportsUpdate from './features/update.js'; 23 | import supportsCreateView from './features/createView.js'; 24 | import supportsOnConflict from './features/onConflict.js'; 25 | import supportsDataTypeCase from './options/dataTypeCase.js'; 26 | 27 | describe('SqliteFormatter', () => { 28 | const language = 'sqlite'; 29 | const format: FormatFn = (query, cfg = {}) => originalFormat(query, { ...cfg, language }); 30 | 31 | behavesLikeSqlFormatter(format); 32 | supportsComments(format); 33 | supportsCreateView(format, { ifNotExists: true }); 34 | supportsCreateTable(format, { ifNotExists: true }); 35 | supportsDropTable(format, { ifExists: true }); 36 | supportsConstraints(format, ['SET NULL', 'SET DEFAULT', 'CASCADE', 'RESTRICT', 'NO ACTION']); 37 | supportsAlterTable(format, { 38 | addColumn: true, 39 | dropColumn: true, 40 | renameTo: true, 41 | renameColumn: true, 42 | }); 43 | supportsDeleteFrom(format); 44 | supportsInsertInto(format); 45 | supportsOnConflict(format); 46 | supportsUpdate(format); 47 | supportsStrings(format, ["''-qq", "X''"]); 48 | supportsIdentifiers(format, [`""-qq`, '``', '[]']); 49 | supportsBetween(format); 50 | supportsSchema(format); 51 | supportsJoin(format); 52 | supportsSetOperations(format, ['UNION', 'UNION ALL', 'EXCEPT', 'INTERSECT']); 53 | supportsOperators(format, ['%', '~', '&', '|', '<<', '>>', '==', '->', '->>', '||']); 54 | supportsParams(format, { positional: true, numbered: ['?'], named: [':', '$', '@'] }); 55 | supportsWindow(format); 56 | supportsLimiting(format, { limit: true, offset: true }); 57 | supportsDataTypeCase(format); 58 | 59 | it('supports REPLACE INTO syntax', () => { 60 | expect(format(`REPLACE INTO tbl VALUES (1,'Leopard'),(2,'Dog');`)).toBe(dedent` 61 | REPLACE INTO 62 | tbl 63 | VALUES 64 | (1, 'Leopard'), 65 | (2, 'Dog'); 66 | `); 67 | }); 68 | 69 | it('supports ON CONFLICT .. DO UPDATE syntax', () => { 70 | expect(format(`INSERT INTO tbl VALUES (1,'Leopard') ON CONFLICT DO UPDATE SET foo=1;`)) 71 | .toBe(dedent` 72 | INSERT INTO 73 | tbl 74 | VALUES 75 | (1, 'Leopard') 76 | ON CONFLICT DO UPDATE 77 | SET 78 | foo = 1; 79 | `); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/tidb.test.ts: -------------------------------------------------------------------------------- 1 | import dedent from 'dedent-js'; 2 | 3 | import { format as originalFormat, FormatFn } from '../src/sqlFormatter.js'; 4 | import behavesLikeMariaDbFormatter from './behavesLikeMariaDbFormatter.js'; 5 | 6 | import supportsJoin from './features/join.js'; 7 | import supportsOperators from './features/operators.js'; 8 | import supportsWindow from './features/window.js'; 9 | import supportsSetOperations from './features/setOperations.js'; 10 | import supportsLimiting from './features/limiting.js'; 11 | import supportsCreateTable from './features/createTable.js'; 12 | import supportsParams from './options/param.js'; 13 | import supportsCreateView from './features/createView.js'; 14 | import supportsAlterTable from './features/alterTable.js'; 15 | import supportsStrings from './features/strings.js'; 16 | import supportsConstraints from './features/constraints.js'; 17 | import supportsDataTypeCase from './options/dataTypeCase.js'; 18 | 19 | // For now these tests are exactly the same as for MySQL 20 | describe('TiDBFormatter', () => { 21 | const language = 'tidb'; 22 | const format: FormatFn = (query, cfg = {}) => originalFormat(query, { ...cfg, language }); 23 | 24 | behavesLikeMariaDbFormatter(format); 25 | 26 | // in addition to string types listed in behavesLikeMariaDbFormatter 27 | supportsStrings(format, ["N''"]); 28 | 29 | supportsJoin(format, { 30 | without: ['FULL'], 31 | additionally: ['STRAIGHT_JOIN'], 32 | }); 33 | supportsSetOperations(format, ['UNION', 'UNION ALL', 'UNION DISTINCT']); 34 | supportsOperators( 35 | format, 36 | ['%', ':=', '&', '|', '^', '~', '<<', '>>', '<=>', '->', '->>', '&&', '||', '!'], 37 | { 38 | logicalOperators: ['AND', 'OR', 'XOR'], 39 | any: true, 40 | } 41 | ); 42 | supportsWindow(format); 43 | supportsLimiting(format, { limit: true, offset: true }); 44 | supportsCreateTable(format, { ifNotExists: true, columnComment: true, tableComment: true }); 45 | supportsConstraints(format, [ 46 | 'RESTRICT', 47 | 'CASCADE', 48 | 'SET NULL', 49 | 'NO ACTION', 50 | 'NOW', 51 | 'CURRENT_TIMESTAMP', 52 | ]); 53 | supportsParams(format, { positional: true }); 54 | supportsCreateView(format, { orReplace: true }); 55 | supportsAlterTable(format, { 56 | addColumn: true, 57 | dropColumn: true, 58 | modify: true, 59 | renameTo: true, 60 | renameColumn: true, 61 | }); 62 | supportsDataTypeCase(format); 63 | 64 | it(`supports @"name" variables`, () => { 65 | expect(format(`SELECT @"foo fo", @"foo\\"x", @"foo""y" FROM tbl;`)).toBe(dedent` 66 | SELECT 67 | @"foo fo", 68 | @"foo\\"x", 69 | @"foo""y" 70 | FROM 71 | tbl; 72 | `); 73 | }); 74 | 75 | it(`supports @'name' variables`, () => { 76 | expect(format(`SELECT @'bar ar', @'bar\\'x', @'bar''y' FROM tbl;`)).toBe(dedent` 77 | SELECT 78 | @'bar ar', 79 | @'bar\\'x', 80 | @'bar''y' 81 | FROM 82 | tbl; 83 | `); 84 | }); 85 | 86 | it('formats ALTER TABLE ... ALTER COLUMN', () => { 87 | expect( 88 | format( 89 | `ALTER TABLE t ALTER COLUMN foo SET DEFAULT 10; 90 | ALTER TABLE t ALTER COLUMN foo DROP DEFAULT;` 91 | ) 92 | ).toBe(dedent` 93 | ALTER TABLE t 94 | ALTER COLUMN foo 95 | SET DEFAULT 10; 96 | 97 | ALTER TABLE t 98 | ALTER COLUMN foo 99 | DROP DEFAULT; 100 | `); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /test/unit/NestedComment.test.ts: -------------------------------------------------------------------------------- 1 | import { NestedComment } from '../../src/lexer/NestedComment.js'; 2 | 3 | describe('NestedComment', () => { 4 | const match = (input: string, index: number) => { 5 | const re = new NestedComment(); 6 | re.lastIndex = index; 7 | return re.exec(input); 8 | }; 9 | 10 | it('matches comment at the start of a string', () => { 11 | expect(match('/* comment */ blah...', 0)).toEqual(['/* comment */']); 12 | }); 13 | 14 | it('matches empty comment block', () => { 15 | expect(match('/**/ blah...', 0)).toEqual(['/**/']); 16 | }); 17 | 18 | it('matches comment containing * and / characters', () => { 19 | expect(match('/** // */ blah...', 0)).toEqual(['/** // */']); 20 | }); 21 | 22 | it('matches only first comment, when two comments in row', () => { 23 | expect(match('/*com1*//*com2*/ blah...', 0)).toEqual(['/*com1*/']); 24 | }); 25 | 26 | it('matches comment in the middle of a string', () => { 27 | expect(match('hello /* comment */ blah...', 6)).toEqual(['/* comment */']); 28 | }); 29 | 30 | it('does not match a comment when index not set to its start position', () => { 31 | expect(match('hello /* comment */ blah...', 1)).toEqual(null); 32 | }); 33 | 34 | it('does not match unclosed comment', () => { 35 | expect(match('/* comment blah...', 0)).toEqual(null); 36 | }); 37 | 38 | it('does not match unopened comment', () => { 39 | expect(match(' comment */ blah...', 0)).toEqual(null); 40 | }); 41 | 42 | it('matches a nested comment', () => { 43 | expect(match('/* some /* nested */ comment */ blah...', 0)).toEqual([ 44 | '/* some /* nested */ comment */', 45 | ]); 46 | }); 47 | 48 | it('matches a multi-level nested comment', () => { 49 | expect(match('/* some /* /* nested */ */ comment */ blah...', 0)).toEqual([ 50 | '/* some /* /* nested */ */ comment */', 51 | ]); 52 | }); 53 | 54 | it('matches multiple nested comments', () => { 55 | expect(match('/* some /* n1 */ and /* n2 */ coms */ blah...', 0)).toEqual([ 56 | '/* some /* n1 */ and /* n2 */ coms */', 57 | ]); 58 | }); 59 | 60 | it('does not match an inproperly nested comment', () => { 61 | expect(match('/* some /* comment blah...', 0)).toEqual(null); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/unit/Parser.test.ts: -------------------------------------------------------------------------------- 1 | import Tokenizer from '../../src/lexer/Tokenizer.js'; 2 | import { createParser } from '../../src/parser/createParser.js'; 3 | 4 | describe('Parser', () => { 5 | const parse = (sql: string) => { 6 | const tokenizer = new Tokenizer( 7 | { 8 | reservedClauses: ['FROM', 'WHERE', 'LIMIT', 'CREATE TABLE'], 9 | reservedSelect: ['SELECT'], 10 | reservedSetOperations: ['UNION', 'UNION ALL'], 11 | reservedJoins: ['JOIN'], 12 | reservedFunctionNames: ['SQRT', 'CURRENT_TIME'], 13 | reservedKeywords: ['BETWEEN', 'LIKE', 'ON', 'USING'], 14 | reservedDataTypes: [], 15 | operators: [':'], 16 | extraParens: ['[]', '{}'], 17 | stringTypes: ["''-qq"], 18 | identTypes: ['""-qq'], 19 | }, 20 | 'sql' 21 | ); 22 | 23 | return createParser(tokenizer).parse(sql, {}); 24 | }; 25 | 26 | it('parses empty list of tokens', () => { 27 | expect(parse('')).toEqual([]); 28 | }); 29 | 30 | it('throws error when parsing invalid SQL expression', () => { 31 | expect(() => parse('SELECT (')).toThrow('Parse error at token: «EOF»'); 32 | }); 33 | 34 | it('parses list of statements', () => { 35 | expect(parse('foo; bar')).toMatchSnapshot(); 36 | }); 37 | 38 | it('parses array subscript', () => { 39 | expect(parse('SELECT my_array[5]')).toMatchSnapshot(); 40 | }); 41 | 42 | it('parses array subscript with comment', () => { 43 | expect(parse('SELECT my_array /*haha*/ [5]')).toMatchSnapshot(); 44 | }); 45 | 46 | it('parses parenthesized expressions', () => { 47 | expect(parse('SELECT (birth_year - (CURRENT_DATE + 1))')).toMatchSnapshot(); 48 | }); 49 | 50 | it('parses function call', () => { 51 | expect(parse('SELECT sqrt(2)')).toMatchSnapshot(); 52 | }); 53 | 54 | it('parses LIMIT clause with count', () => { 55 | expect(parse('LIMIT 15;')).toMatchSnapshot(); 56 | }); 57 | 58 | it('parses LIMIT clause with offset and count', () => { 59 | expect(parse('LIMIT 100, 15;')).toMatchSnapshot(); 60 | }); 61 | 62 | it('parses LIMIT clause with longer expressions', () => { 63 | expect(parse('LIMIT 50 + 50, 3 * 2;')).toMatchSnapshot(); 64 | }); 65 | 66 | it('parses BETWEEN expression', () => { 67 | expect(parse('WHERE age BETWEEN 18 AND 63')).toMatchSnapshot(); 68 | }); 69 | 70 | it('parses set operations', () => { 71 | expect(parse('SELECT foo FROM bar UNION ALL SELECT foo FROM baz')).toMatchSnapshot(); 72 | }); 73 | 74 | it('parses SELECT *', () => { 75 | expect(parse('SELECT *')).toMatchSnapshot(); 76 | }); 77 | 78 | it('parses SELECT ident.*', () => { 79 | expect(parse('SELECT ident.*')).toMatchSnapshot(); 80 | }); 81 | 82 | it('parses function name with and without parameters', () => { 83 | expect(parse('SELECT CURRENT_TIME a, CURRENT_TIME() b;')).toMatchSnapshot(); 84 | }); 85 | 86 | it('parses curly braces', () => { 87 | expect(parse('SELECT {foo: bar};')).toMatchSnapshot(); 88 | }); 89 | 90 | it('parses square brackets', () => { 91 | expect(parse('SELECT [1, 2, 3];')).toMatchSnapshot(); 92 | }); 93 | 94 | it('parses qualified.identifier.sequence', () => { 95 | expect(parse('SELECT foo.bar.baz;')).toMatchSnapshot(); 96 | }); 97 | 98 | it('parses CASE expression', () => { 99 | expect(parse('SELECT CASE foo WHEN 1+1 THEN 10 ELSE 20 END;')).toMatchSnapshot(); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /test/unit/Tokenizer.test.ts: -------------------------------------------------------------------------------- 1 | import Tokenizer from '../../src/lexer/Tokenizer.js'; 2 | 3 | describe('Tokenizer', () => { 4 | const tokenize = (sql: string) => 5 | new Tokenizer( 6 | { 7 | reservedClauses: ['FROM', 'WHERE', 'LIMIT', 'CREATE TABLE'], 8 | reservedSelect: ['SELECT'], 9 | reservedSetOperations: ['UNION', 'UNION ALL'], 10 | reservedJoins: ['JOIN'], 11 | reservedFunctionNames: ['SQRT', 'CURRENT_TIME'], 12 | reservedKeywords: ['BETWEEN', 'LIKE', 'ON', 'USING'], 13 | reservedDataTypes: [], 14 | stringTypes: ["''-qq"], 15 | identTypes: ['""-qq'], 16 | }, 17 | 'sql' 18 | ).tokenize(sql, {}); 19 | 20 | it('tokenizes whitespace to empty array', () => { 21 | expect(tokenize(' \t\n \n\r ')).toEqual([]); 22 | }); 23 | 24 | it('tokenizes single line SQL tokens', () => { 25 | expect(tokenize('SELECT * FROM foo;')).toMatchSnapshot(); 26 | }); 27 | 28 | it('tokenizes multiline SQL tokens', () => { 29 | expect(tokenize('SELECT "foo\n bar" /* \n\n\n */;')).toMatchSnapshot(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/unit/__snapshots__/Tokenizer.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Tokenizer tokenizes multiline SQL tokens 1`] = ` 4 | [ 5 | { 6 | "precedingWhitespace": undefined, 7 | "raw": "SELECT", 8 | "start": 0, 9 | "text": "SELECT", 10 | "type": "RESERVED_SELECT", 11 | }, 12 | { 13 | "precedingWhitespace": " ", 14 | "raw": ""foo 15 | bar"", 16 | "start": 7, 17 | "text": ""foo 18 | bar"", 19 | "type": "QUOTED_IDENTIFIER", 20 | }, 21 | { 22 | "precedingWhitespace": " ", 23 | "raw": "/* 24 | 25 | 26 | */", 27 | "start": 18, 28 | "text": "/* 29 | 30 | 31 | */", 32 | "type": "BLOCK_COMMENT", 33 | }, 34 | { 35 | "precedingWhitespace": undefined, 36 | "raw": ";", 37 | "start": 27, 38 | "text": ";", 39 | "type": "DELIMITER", 40 | }, 41 | ] 42 | `; 43 | 44 | exports[`Tokenizer tokenizes single line SQL tokens 1`] = ` 45 | [ 46 | { 47 | "precedingWhitespace": undefined, 48 | "raw": "SELECT", 49 | "start": 0, 50 | "text": "SELECT", 51 | "type": "RESERVED_SELECT", 52 | }, 53 | { 54 | "precedingWhitespace": " ", 55 | "raw": "*", 56 | "start": 7, 57 | "text": "*", 58 | "type": "ASTERISK", 59 | }, 60 | { 61 | "precedingWhitespace": " ", 62 | "raw": "FROM", 63 | "start": 9, 64 | "text": "FROM", 65 | "type": "RESERVED_CLAUSE", 66 | }, 67 | { 68 | "precedingWhitespace": " ", 69 | "raw": "foo", 70 | "start": 14, 71 | "text": "foo", 72 | "type": "IDENTIFIER", 73 | }, 74 | { 75 | "precedingWhitespace": undefined, 76 | "raw": ";", 77 | "start": 17, 78 | "text": ";", 79 | "type": "DELIMITER", 80 | }, 81 | ] 82 | `; 83 | -------------------------------------------------------------------------------- /test/unit/expandPhrases.test.ts: -------------------------------------------------------------------------------- 1 | import { expandSinglePhrase } from '../../src/expandPhrases.js'; 2 | 3 | describe('expandSinglePhrase()', () => { 4 | it('returns single item when no [optional blocks] found', () => { 5 | expect(expandSinglePhrase('INSERT INTO')).toEqual(['INSERT INTO']); 6 | }); 7 | 8 | it('expands expression with one [optional block] at the end', () => { 9 | expect(expandSinglePhrase('DROP TABLE [IF EXISTS]')).toEqual([ 10 | 'DROP TABLE', 11 | 'DROP TABLE IF EXISTS', 12 | ]); 13 | }); 14 | 15 | it('expands expression with one [optional block] at the middle', () => { 16 | expect(expandSinglePhrase('CREATE [TEMPORARY] TABLE')).toEqual([ 17 | 'CREATE TABLE', 18 | 'CREATE TEMPORARY TABLE', 19 | ]); 20 | }); 21 | 22 | it('expands expression with one [optional block] at the start', () => { 23 | expect(expandSinglePhrase('[EXPLAIN] SELECT')).toEqual(['SELECT', 'EXPLAIN SELECT']); 24 | }); 25 | 26 | it('expands multiple [optional] [blocks]', () => { 27 | expect(expandSinglePhrase('CREATE [OR REPLACE] [MATERIALIZED] VIEW')).toEqual([ 28 | 'CREATE VIEW', 29 | 'CREATE MATERIALIZED VIEW', 30 | 'CREATE OR REPLACE VIEW', 31 | 'CREATE OR REPLACE MATERIALIZED VIEW', 32 | ]); 33 | }); 34 | 35 | it('expands expression with optional [multi|choice|block]', () => { 36 | expect(expandSinglePhrase('CREATE [TEMP|TEMPORARY|VIRTUAL] TABLE')).toEqual([ 37 | 'CREATE TABLE', 38 | 'CREATE TEMP TABLE', 39 | 'CREATE TEMPORARY TABLE', 40 | 'CREATE VIRTUAL TABLE', 41 | ]); 42 | }); 43 | 44 | it('removes braces around {mandatory} {block}', () => { 45 | expect(expandSinglePhrase('CREATE {TEMP} {TABLE}')).toEqual(['CREATE TEMP TABLE']); 46 | }); 47 | 48 | it('expands expression with mandatory {multi|choice|block}', () => { 49 | expect(expandSinglePhrase('CREATE {TEMP|TEMPORARY|VIRTUAL} TABLE')).toEqual([ 50 | 'CREATE TEMP TABLE', 51 | 'CREATE TEMPORARY TABLE', 52 | 'CREATE VIRTUAL TABLE', 53 | ]); 54 | }); 55 | 56 | it('expands nested []-block inside []-block', () => { 57 | expect(expandSinglePhrase('CREATE [[OR] REPLACE] TABLE')).toEqual([ 58 | 'CREATE TABLE', 59 | 'CREATE REPLACE TABLE', 60 | 'CREATE OR REPLACE TABLE', 61 | ]); 62 | }); 63 | 64 | it('expands nested {}-block inside {}-block', () => { 65 | expect(expandSinglePhrase('CREATE {{OR} REPLACE} TABLE')).toEqual(['CREATE OR REPLACE TABLE']); 66 | }); 67 | 68 | it('expands nested {}-block inside []-block', () => { 69 | expect(expandSinglePhrase('FOR RS [USE AND KEEP {UPDATE | EXCLUSIVE} LOCKS]')).toEqual([ 70 | 'FOR RS', 71 | 'FOR RS USE AND KEEP UPDATE LOCKS', 72 | 'FOR RS USE AND KEEP EXCLUSIVE LOCKS', 73 | ]); 74 | }); 75 | 76 | it('throws error when encountering unbalanced ][-braces', () => { 77 | expect(() => expandSinglePhrase('CREATE [TABLE')).toThrowErrorMatchingInlineSnapshot( 78 | `"Unbalanced parenthesis in: CREATE [TABLE"` 79 | ); 80 | expect(() => expandSinglePhrase('CREATE TABLE]')).toThrowErrorMatchingInlineSnapshot( 81 | `"Unbalanced parenthesis in: CREATE TABLE]"` 82 | ); 83 | }); 84 | 85 | it('throws error when encountering unbalanced }{-braces', () => { 86 | expect(() => expandSinglePhrase('CREATE {TABLE')).toThrowErrorMatchingInlineSnapshot( 87 | `"Unbalanced parenthesis in: CREATE {TABLE"` 88 | ); 89 | expect(() => expandSinglePhrase('CREATE TABLE}')).toThrowErrorMatchingInlineSnapshot( 90 | `"Unbalanced parenthesis in: CREATE TABLE}"` 91 | ); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /test/unit/tabularStyle.test.ts: -------------------------------------------------------------------------------- 1 | import toTabularFormat from '../../src/formatter/tabularStyle.js'; 2 | 3 | describe('toTabularFormat()', () => { 4 | it('does nothing in standard style', () => { 5 | expect(toTabularFormat('FROM', 'standard')).toBe('FROM'); 6 | expect(toTabularFormat('INNER JOIN', 'standard')).toBe('INNER JOIN'); 7 | expect(toTabularFormat('INSERT INTO', 'standard')).toBe('INSERT INTO'); 8 | }); 9 | 10 | it('formats in tabularLeft style', () => { 11 | expect(toTabularFormat('FROM', 'tabularLeft')).toBe('FROM '); 12 | expect(toTabularFormat('INNER JOIN', 'tabularLeft')).toBe('INNER JOIN'); 13 | expect(toTabularFormat('INSERT INTO', 'tabularLeft')).toBe('INSERT INTO'); 14 | }); 15 | 16 | it('formats in tabularRight style', () => { 17 | expect(toTabularFormat('FROM', 'tabularRight')).toBe(' FROM'); 18 | expect(toTabularFormat('INNER JOIN', 'tabularRight')).toBe(' INNER JOIN'); 19 | expect(toTabularFormat('INSERT INTO', 'tabularRight')).toBe(' INSERT INTO'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist/cjs", 5 | "module": "CommonJS" 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist/esm", 5 | "module": "NodeNext" 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "moduleResolution": "NodeNext", 5 | "target": "es6", 6 | "lib": ["es6", "dom"], 7 | "rootDirs": ["src"], 8 | "baseUrl": "./", 9 | "sourceMap": true, 10 | "declaration": true, 11 | "esModuleInterop": true, 12 | // Enable all strict type-checking options. 13 | "strict": true, 14 | // Report error when not all code paths in function return a value. 15 | "noImplicitReturns": true 16 | }, 17 | "include": ["src", "test", "static"], 18 | "exclude": ["node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default { 4 | entry: './src/index.ts', 5 | output: { 6 | path: path.resolve('./dist'), 7 | filename: 'sql-formatter.cjs', 8 | library: 'sqlFormatter', 9 | libraryTarget: 'umd', 10 | globalObject: 'this', 11 | }, 12 | resolve: { 13 | extensions: ['.js', '.ts'], 14 | extensionAlias: { 15 | '.js': ['.ts', '.js'], 16 | }, 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.ts$/u, 22 | exclude: /node_modules/u, 23 | use: [ 24 | 'babel-loader', 25 | { 26 | loader: 'ts-loader', 27 | options: { 28 | // Prevent `ts-loader` from emitting types to the `lib` directory. 29 | // This also disables type-checking, which is already performed 30 | // independently of the webpack build process. 31 | transpileOnly: true, 32 | }, 33 | }, 34 | ], 35 | }, 36 | { 37 | test: /\.js$/u, 38 | exclude: /node_modules/u, 39 | use: ['babel-loader'], 40 | }, 41 | ], 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | import { merge } from 'webpack-merge'; 2 | 3 | import common from './webpack.common.js'; 4 | 5 | export default merge(common, { 6 | mode: 'production', 7 | devtool: 'source-map', 8 | output: { 9 | filename: 'sql-formatter.min.cjs', 10 | }, 11 | }); 12 | --------------------------------------------------------------------------------