├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src ├── core │ ├── Formatter.js │ ├── Indentation.js │ ├── InlineBlock.js │ ├── Params.js │ ├── Tokenizer.js │ └── tokenTypes.js ├── languages │ ├── Db2Formatter.js │ ├── N1qlFormatter.js │ ├── PlSqlFormatter.js │ └── StandardSqlFormatter.js └── sqlFormatter.js ├── test ├── Db2FormatterTest.js ├── N1qlFormatterTest.js ├── PlSqlFormatterTest.js ├── StandardSqlFormatterTest.js ├── behavesLikeSqlFormatter.js └── sqlFormatterTest.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env"], 3 | "plugins": ["add-module-exports"] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lib 2 | /dist 3 | /coverage 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["adpyke-es6", "prettier"], 3 | "parserOptions": { 4 | "sourceType": "module" 5 | }, 6 | "env": { 7 | "jest": true, 8 | "browser": false, 9 | "node": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | lib 3 | node_modules 4 | .DS_Store 5 | coverage 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /lib 2 | /dist 3 | /coverage 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 100, 4 | "tabWidth": 2 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: node 3 | 4 | install: 5 | - npm install 6 | - npm install coveralls 7 | script: 8 | - npm run check 9 | - npm run build 10 | after_script: 11 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-present ZeroTurnaround LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQL Formatter Plus 2 | 3 | A fork of [SQL Formatter](https://github.com/zeroturnaround/sql-formatter) with some extra bug fixes and features. 4 | 5 | Fixes: 6 | 7 | - Fixed formatting issue with unicode characters 8 | - Fixed comment formatting for non-unix line endings 9 | - Fixed null reference on input tokenization 10 | - Fixed indentation of multiple statements 11 | 12 | New Features: 13 | 14 | - Convert keywords to uppercase with the `uppercase` config option 15 | - Configurable number of line breaks between queries with the `linesBetweenQueries` config option 16 | 17 | **SQL Formatter** is a JavaScript library for pretty-printing SQL queries. 18 | It started as a port of a [PHP Library][], but has since considerably diverged. 19 | It supports [Standard SQL][], [Couchbase N1QL][], [IBM DB2][] and [Oracle PL/SQL][] dialects. 20 | 21 | [Try the demo.](https://kufii.github.io/sql-formatter-plus//) 22 | 23 | ## Install 24 | 25 | Get the latest version from NPM: 26 | 27 | ```shell 28 | npm install sql-formatter 29 | ``` 30 | 31 | ## Usage 32 | 33 | ```javascript 34 | import sqlFormatter from 'sql-formatter-plus'; 35 | 36 | console.log(sqlFormatter.format('SELECT * FROM table1')); 37 | ``` 38 | 39 | This will output: 40 | 41 | ```sql 42 | SELECT 43 | * 44 | FROM 45 | table1 46 | ``` 47 | 48 | You can also pass in configuration options: 49 | 50 | ```javascript 51 | sqlFormatter.format('SELECT *', { 52 | language: 'n1ql', // Defaults to "sql" 53 | indent: ' ', // Defaults to two spaces, 54 | uppercase: true, // Defaults to false 55 | linesBetweenQueries: 2 // Defaults to 1 56 | }); 57 | ``` 58 | 59 | Currently just four SQL dialects are supported: 60 | 61 | - **sql** - [Standard SQL][] 62 | - **n1ql** - [Couchbase N1QL][] 63 | - **db2** - [IBM DB2][] 64 | - **pl/sql** - [Oracle PL/SQL][] 65 | 66 | ### Placeholders replacement 67 | 68 | ```javascript 69 | // Named placeholders 70 | sqlFormatter.format("SELECT * FROM tbl WHERE foo = @foo", { 71 | params: {foo: "'bar'"} 72 | })); 73 | 74 | // Indexed placeholders 75 | sqlFormatter.format("SELECT * FROM tbl WHERE foo = ?", { 76 | params: ["'bar'"] 77 | })); 78 | ``` 79 | 80 | Both result in: 81 | 82 | ```sql 83 | SELECT 84 | * 85 | FROM 86 | tbl 87 | WHERE 88 | foo = 'bar' 89 | ``` 90 | 91 | ## Usage without NPM 92 | 93 | If you don't use a module bundler, clone the repository, run `npm install` and grab a file from `/dist` directory to use inside a ` 133 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sql-formatter-plus", 3 | "version": "1.3.6", 4 | "description": "Formats whitespace in a SQL query to make it more readable", 5 | "license": "MIT", 6 | "main": "lib/sqlFormatter.js", 7 | "keywords": [ 8 | "sql", 9 | "formatter", 10 | "format", 11 | "n1ql", 12 | "whitespace" 13 | ], 14 | "authors": [ 15 | "Rene Saarsoo", 16 | "Uku Pattak" 17 | ], 18 | "files": [ 19 | "dist", 20 | "lib", 21 | "src" 22 | ], 23 | "scripts": { 24 | "clean": "rimraf lib dist", 25 | "lint": "eslint .", 26 | "format": "prettier --write \"**/*.{js,jsx,md,json,css,html,prettierrc,eslintrc,babelrc}\"", 27 | "check:format": "prettier --check \"**/*.{js,jsx,md,json,css,html,prettierrc,eslintrc,babelrc}\"", 28 | "test": "jest", 29 | "test:watch": "npm run test -- --watch", 30 | "check": "npm run lint && npm run check:format && npm run test", 31 | "build": "npm run build:commonjs && npm run build:umd && npm run build:umd:min", 32 | "build:commonjs": "babel src --out-dir lib", 33 | "build:umd": "webpack --mode development src/sqlFormatter.js --output dist/sql-formatter.js", 34 | "build:umd:min": "webpack --mode production src/sqlFormatter.js --output dist/sql-formatter.min.js", 35 | "prepublish": "npm run clean && npm run check && npm run build" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/kufii/sql-formatter-plus.git" 40 | }, 41 | "bugs": { 42 | "url": "https://github.com/kufii/sql-formatter-plus/issues" 43 | }, 44 | "dependencies": { 45 | "@babel/polyfill": "^7.7.0", 46 | "lodash": "^4.17.15" 47 | }, 48 | "devDependencies": { 49 | "@babel/cli": "^7.7.0", 50 | "@babel/core": "^7.7.2", 51 | "@babel/preset-env": "^7.7.1", 52 | "babel-eslint": "^10.0.3", 53 | "babel-plugin-add-module-exports": "^1.0.2", 54 | "dedent-js": "^1.0.1", 55 | "eslint": "^6.6.0", 56 | "eslint-config-adpyke-es6": "^1.4.13", 57 | "eslint-config-prettier": "^6.5.0", 58 | "jest": "^24.9.0", 59 | "prettier": "^1.19.1", 60 | "rimraf": "^3.0.0", 61 | "webpack": "^4.41.2", 62 | "webpack-cli": "^3.3.10" 63 | }, 64 | "jest": { 65 | "roots": [ 66 | "test" 67 | ], 68 | "testRegex": ".*Test", 69 | "collectCoverage": true 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/core/Formatter.js: -------------------------------------------------------------------------------- 1 | import includes from 'lodash/includes'; 2 | import tokenTypes from './tokenTypes'; 3 | import Indentation from './Indentation'; 4 | import InlineBlock from './InlineBlock'; 5 | import Params from './Params'; 6 | 7 | const trimSpacesEnd = str => str.replace(/[ \t]+$/u, ''); 8 | 9 | export default class Formatter { 10 | /** 11 | * @param {Object} cfg 12 | * @param {String} cfg.language 13 | * @param {String} cfg.indent 14 | * @param {Bool} cfg.uppercase 15 | * @param {Integer} cfg.linesBetweenQueries 16 | * @param {Object} cfg.params 17 | * @param {Tokenizer} tokenizer 18 | */ 19 | constructor(cfg, tokenizer, tokenOverride) { 20 | this.cfg = cfg || {}; 21 | this.indentation = new Indentation(this.cfg.indent); 22 | this.inlineBlock = new InlineBlock(); 23 | this.params = new Params(this.cfg.params); 24 | this.tokenizer = tokenizer; 25 | this.tokenOverride = tokenOverride; 26 | this.previousReservedWord = {}; 27 | this.tokens = []; 28 | this.index = 0; 29 | } 30 | 31 | /** 32 | * Formats whitespace in a SQL string to make it easier to read. 33 | * 34 | * @param {String} query The SQL query string 35 | * @return {String} formatted query 36 | */ 37 | format(query) { 38 | this.tokens = this.tokenizer.tokenize(query); 39 | const formattedQuery = this.getFormattedQueryFromTokens(); 40 | 41 | return formattedQuery.trim(); 42 | } 43 | 44 | getFormattedQueryFromTokens() { 45 | let formattedQuery = ''; 46 | 47 | this.tokens.forEach((token, index) => { 48 | this.index = index; 49 | 50 | if (this.tokenOverride) token = this.tokenOverride(token, this.previousReservedWord) || token; 51 | 52 | if (token.type === tokenTypes.WHITESPACE) { 53 | // ignore (we do our own whitespace formatting) 54 | } else if (token.type === tokenTypes.LINE_COMMENT) { 55 | formattedQuery = this.formatLineComment(token, formattedQuery); 56 | } else if (token.type === tokenTypes.BLOCK_COMMENT) { 57 | formattedQuery = this.formatBlockComment(token, formattedQuery); 58 | } else if (token.type === tokenTypes.RESERVED_TOP_LEVEL) { 59 | formattedQuery = this.formatTopLevelReservedWord(token, formattedQuery); 60 | this.previousReservedWord = token; 61 | } else if (token.type === tokenTypes.RESERVED_TOP_LEVEL_NO_INDENT) { 62 | formattedQuery = this.formatTopLevelReservedWordNoIndent(token, formattedQuery); 63 | this.previousReservedWord = token; 64 | } else if (token.type === tokenTypes.RESERVED_NEWLINE) { 65 | formattedQuery = this.formatNewlineReservedWord(token, formattedQuery); 66 | this.previousReservedWord = token; 67 | } else if (token.type === tokenTypes.RESERVED) { 68 | formattedQuery = this.formatWithSpaces(token, formattedQuery); 69 | this.previousReservedWord = token; 70 | } else if (token.type === tokenTypes.OPEN_PAREN) { 71 | formattedQuery = this.formatOpeningParentheses(token, formattedQuery); 72 | } else if (token.type === tokenTypes.CLOSE_PAREN) { 73 | formattedQuery = this.formatClosingParentheses(token, formattedQuery); 74 | } else if (token.type === tokenTypes.PLACEHOLDER) { 75 | formattedQuery = this.formatPlaceholder(token, formattedQuery); 76 | } else if (token.value === ',') { 77 | formattedQuery = this.formatComma(token, formattedQuery); 78 | } else if (token.value === ':') { 79 | formattedQuery = this.formatWithSpaceAfter(token, formattedQuery); 80 | } else if (token.value === '.') { 81 | formattedQuery = this.formatWithoutSpaces(token, formattedQuery); 82 | } else if (token.value === ';') { 83 | formattedQuery = this.formatQuerySeparator(token, formattedQuery); 84 | } else { 85 | formattedQuery = this.formatWithSpaces(token, formattedQuery); 86 | } 87 | }); 88 | return formattedQuery; 89 | } 90 | 91 | formatLineComment(token, query) { 92 | return this.addNewline(query + token.value); 93 | } 94 | 95 | formatBlockComment(token, query) { 96 | return this.addNewline(this.addNewline(query) + this.indentComment(token.value)); 97 | } 98 | 99 | indentComment(comment) { 100 | return comment.replace(/\n[ \t]*/gu, '\n' + this.indentation.getIndent() + ' '); 101 | } 102 | 103 | formatTopLevelReservedWordNoIndent(token, query) { 104 | this.indentation.decreaseTopLevel(); 105 | query = this.addNewline(query) + this.equalizeWhitespace(this.formatReservedWord(token.value)); 106 | return this.addNewline(query); 107 | } 108 | 109 | formatTopLevelReservedWord(token, query) { 110 | this.indentation.decreaseTopLevel(); 111 | 112 | query = this.addNewline(query); 113 | 114 | this.indentation.increaseTopLevel(); 115 | 116 | query += this.equalizeWhitespace(this.formatReservedWord(token.value)); 117 | return this.addNewline(query); 118 | } 119 | 120 | formatNewlineReservedWord(token, query) { 121 | return ( 122 | this.addNewline(query) + this.equalizeWhitespace(this.formatReservedWord(token.value)) + ' ' 123 | ); 124 | } 125 | 126 | // Replace any sequence of whitespace characters with single space 127 | equalizeWhitespace(string) { 128 | return string.replace(/\s+/gu, ' '); 129 | } 130 | 131 | // Opening parentheses increase the block indent level and start a new line 132 | formatOpeningParentheses(token, query) { 133 | // Take out the preceding space unless there was whitespace there in the original query 134 | // or another opening parens or line comment 135 | const preserveWhitespaceFor = [ 136 | tokenTypes.WHITESPACE, 137 | tokenTypes.OPEN_PAREN, 138 | tokenTypes.LINE_COMMENT 139 | ]; 140 | if (!includes(preserveWhitespaceFor, this.previousToken().type)) { 141 | query = trimSpacesEnd(query); 142 | } 143 | query += this.cfg.uppercase ? token.value.toUpperCase() : token.value; 144 | 145 | this.inlineBlock.beginIfPossible(this.tokens, this.index); 146 | 147 | if (!this.inlineBlock.isActive()) { 148 | this.indentation.increaseBlockLevel(); 149 | query = this.addNewline(query); 150 | } 151 | return query; 152 | } 153 | 154 | // Closing parentheses decrease the block indent level 155 | formatClosingParentheses(token, query) { 156 | token.value = this.cfg.uppercase ? token.value.toUpperCase() : token.value; 157 | if (this.inlineBlock.isActive()) { 158 | this.inlineBlock.end(); 159 | return this.formatWithSpaceAfter(token, query); 160 | } else { 161 | this.indentation.decreaseBlockLevel(); 162 | return this.formatWithSpaces(token, this.addNewline(query)); 163 | } 164 | } 165 | 166 | formatPlaceholder(token, query) { 167 | return query + this.params.get(token) + ' '; 168 | } 169 | 170 | // Commas start a new line (unless within inline parentheses or SQL "LIMIT" clause) 171 | formatComma(token, query) { 172 | query = trimSpacesEnd(query) + token.value + ' '; 173 | 174 | if (this.inlineBlock.isActive()) { 175 | return query; 176 | } else if (/^LIMIT$/iu.test(this.previousReservedWord.value)) { 177 | return query; 178 | } else { 179 | return this.addNewline(query); 180 | } 181 | } 182 | 183 | formatWithSpaceAfter(token, query) { 184 | return trimSpacesEnd(query) + token.value + ' '; 185 | } 186 | 187 | formatWithoutSpaces(token, query) { 188 | return trimSpacesEnd(query) + token.value; 189 | } 190 | 191 | formatWithSpaces(token, query) { 192 | const value = token.type === 'reserved' ? this.formatReservedWord(token.value) : token.value; 193 | return query + value + ' '; 194 | } 195 | 196 | formatReservedWord(value) { 197 | return this.cfg.uppercase ? value.toUpperCase() : value; 198 | } 199 | 200 | formatQuerySeparator(token, query) { 201 | this.indentation.resetIndentation(); 202 | return trimSpacesEnd(query) + token.value + '\n'.repeat(this.cfg.linesBetweenQueries || 1); 203 | } 204 | 205 | addNewline(query) { 206 | query = trimSpacesEnd(query); 207 | if (!query.endsWith('\n')) query += '\n'; 208 | return query + this.indentation.getIndent(); 209 | } 210 | 211 | previousToken(offset = 1) { 212 | return this.tokens[this.index - offset] || {}; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/core/Indentation.js: -------------------------------------------------------------------------------- 1 | import repeat from 'lodash/repeat'; 2 | import last from 'lodash/last'; 3 | 4 | const INDENT_TYPE_TOP_LEVEL = 'top-level'; 5 | const INDENT_TYPE_BLOCK_LEVEL = 'block-level'; 6 | 7 | /** 8 | * Manages indentation levels. 9 | * 10 | * There are two types of indentation levels: 11 | * 12 | * - BLOCK_LEVEL : increased by open-parenthesis 13 | * - TOP_LEVEL : increased by RESERVED_TOP_LEVEL words 14 | */ 15 | export default class Indentation { 16 | /** 17 | * @param {String} indent Indent value, default is " " (2 spaces) 18 | */ 19 | constructor(indent) { 20 | this.indent = indent || ' '; 21 | this.indentTypes = []; 22 | } 23 | 24 | /** 25 | * Returns current indentation string. 26 | * @return {String} 27 | */ 28 | getIndent() { 29 | return repeat(this.indent, this.indentTypes.length); 30 | } 31 | 32 | /** 33 | * Increases indentation by one top-level indent. 34 | */ 35 | increaseTopLevel() { 36 | this.indentTypes.push(INDENT_TYPE_TOP_LEVEL); 37 | } 38 | 39 | /** 40 | * Increases indentation by one block-level indent. 41 | */ 42 | increaseBlockLevel() { 43 | this.indentTypes.push(INDENT_TYPE_BLOCK_LEVEL); 44 | } 45 | 46 | /** 47 | * Decreases indentation by one top-level indent. 48 | * Does nothing when the previous indent is not top-level. 49 | */ 50 | decreaseTopLevel() { 51 | if (last(this.indentTypes) === INDENT_TYPE_TOP_LEVEL) { 52 | this.indentTypes.pop(); 53 | } 54 | } 55 | 56 | /** 57 | * Decreases indentation by one block-level indent. 58 | * If there are top-level indents within the block-level indent, 59 | * throws away these as well. 60 | */ 61 | decreaseBlockLevel() { 62 | while (this.indentTypes.length > 0) { 63 | const type = this.indentTypes.pop(); 64 | if (type !== INDENT_TYPE_TOP_LEVEL) { 65 | break; 66 | } 67 | } 68 | } 69 | 70 | resetIndentation() { 71 | this.indentTypes = []; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/core/InlineBlock.js: -------------------------------------------------------------------------------- 1 | import tokenTypes from './tokenTypes'; 2 | 3 | const INLINE_MAX_LENGTH = 50; 4 | 5 | /** 6 | * Bookkeeper for inline blocks. 7 | * 8 | * Inline blocks are parenthesized expressions that are shorter than INLINE_MAX_LENGTH. 9 | * These blocks are formatted on a single line, unlike longer parenthesized 10 | * expressions where open-parenthesis causes newline and increase of indentation. 11 | */ 12 | export default class InlineBlock { 13 | constructor() { 14 | this.level = 0; 15 | } 16 | 17 | /** 18 | * Begins inline block when lookahead through upcoming tokens determines 19 | * that the block would be smaller than INLINE_MAX_LENGTH. 20 | * @param {Object[]} tokens Array of all tokens 21 | * @param {Number} index Current token position 22 | */ 23 | beginIfPossible(tokens, index) { 24 | if (this.level === 0 && this.isInlineBlock(tokens, index)) { 25 | this.level = 1; 26 | } else if (this.level > 0) { 27 | this.level++; 28 | } else { 29 | this.level = 0; 30 | } 31 | } 32 | 33 | /** 34 | * Finishes current inline block. 35 | * There might be several nested ones. 36 | */ 37 | end() { 38 | this.level--; 39 | } 40 | 41 | /** 42 | * True when inside an inline block 43 | * @return {Boolean} 44 | */ 45 | isActive() { 46 | return this.level > 0; 47 | } 48 | 49 | // Check if this should be an inline parentheses block 50 | // Examples are "NOW()", "COUNT(*)", "int(10)", key(`some_column`), DECIMAL(7,2) 51 | isInlineBlock(tokens, index) { 52 | let length = 0; 53 | let level = 0; 54 | 55 | for (let i = index; i < tokens.length; i++) { 56 | const token = tokens[i]; 57 | length += token.value.length; 58 | 59 | // Overran max length 60 | if (length > INLINE_MAX_LENGTH) { 61 | return false; 62 | } 63 | 64 | if (token.type === tokenTypes.OPEN_PAREN) { 65 | level++; 66 | } else if (token.type === tokenTypes.CLOSE_PAREN) { 67 | level--; 68 | if (level === 0) { 69 | return true; 70 | } 71 | } 72 | 73 | if (this.isForbiddenToken(token)) { 74 | return false; 75 | } 76 | } 77 | return false; 78 | } 79 | 80 | // Reserved words that cause newlines, comments and semicolons 81 | // are not allowed inside inline parentheses block 82 | isForbiddenToken({ type, value }) { 83 | return ( 84 | type === tokenTypes.RESERVED_TOP_LEVEL || 85 | type === tokenTypes.RESERVED_NEWLINE || 86 | type === tokenTypes.COMMENT || 87 | type === tokenTypes.BLOCK_COMMENT || 88 | value === ';' 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/core/Params.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Handles placeholder replacement with given params. 3 | */ 4 | export default class Params { 5 | /** 6 | * @param {Object} params 7 | */ 8 | constructor(params) { 9 | this.params = params; 10 | this.index = 0; 11 | } 12 | 13 | /** 14 | * Returns param value that matches given placeholder with param key. 15 | * @param {Object} token 16 | * @param {String} token.key Placeholder key 17 | * @param {String} token.value Placeholder value 18 | * @return {String} param or token.value when params are missing 19 | */ 20 | get({ key, value }) { 21 | if (!this.params) { 22 | return value; 23 | } 24 | if (key) { 25 | return this.params[key]; 26 | } 27 | return this.params[this.index++]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/core/Tokenizer.js: -------------------------------------------------------------------------------- 1 | import isEmpty from 'lodash/isEmpty'; 2 | import escapeRegExp from 'lodash/escapeRegExp'; 3 | import tokenTypes from './tokenTypes'; 4 | 5 | export default class Tokenizer { 6 | /** 7 | * @param {Object} cfg 8 | * @param {String[]} cfg.reservedWords Reserved words in SQL 9 | * @param {String[]} cfg.reservedTopLevelWords Words that are set to new line separately 10 | * @param {String[]} cfg.reservedNewlineWords Words that are set to newline 11 | * @param {String[]} cfg.reservedTopLevelWordsNoIndent Words that are top level but have no indentation 12 | * @param {String[]} cfg.stringTypes String types to enable: "", '', ``, [], N'' 13 | * @param {String[]} cfg.openParens Opening parentheses to enable, like (, [ 14 | * @param {String[]} cfg.closeParens Closing parentheses to enable, like ), ] 15 | * @param {String[]} cfg.indexedPlaceholderTypes Prefixes for indexed placeholders, like ? 16 | * @param {String[]} cfg.namedPlaceholderTypes Prefixes for named placeholders, like @ and : 17 | * @param {String[]} cfg.lineCommentTypes Line comments to enable, like # and -- 18 | * @param {String[]} cfg.specialWordChars Special chars that can be found inside of words, like @ and # 19 | */ 20 | constructor(cfg) { 21 | this.WHITESPACE_REGEX = /^(\s+)/u; 22 | this.NUMBER_REGEX = /^((-\s*)?[0-9]+(\.[0-9]+)?|0x[0-9a-fA-F]+|0b[01]+)\b/u; 23 | this.OPERATOR_REGEX = /^(!=|<>|==|<=|>=|!<|!>|\|\||::|->>|->|~~\*|~~|!~~\*|!~~|~\*|!~\*|!~|:=|.)/u; 24 | 25 | this.BLOCK_COMMENT_REGEX = /^(\/\*[^]*?(?:\*\/|$))/u; 26 | this.LINE_COMMENT_REGEX = this.createLineCommentRegex(cfg.lineCommentTypes); 27 | 28 | this.RESERVED_TOP_LEVEL_REGEX = this.createReservedWordRegex(cfg.reservedTopLevelWords); 29 | this.RESERVED_TOP_LEVEL_NO_INDENT_REGEX = this.createReservedWordRegex( 30 | cfg.reservedTopLevelWordsNoIndent 31 | ); 32 | this.RESERVED_NEWLINE_REGEX = this.createReservedWordRegex(cfg.reservedNewlineWords); 33 | this.RESERVED_PLAIN_REGEX = this.createReservedWordRegex(cfg.reservedWords); 34 | 35 | this.WORD_REGEX = this.createWordRegex(cfg.specialWordChars); 36 | this.STRING_REGEX = this.createStringRegex(cfg.stringTypes); 37 | 38 | this.OPEN_PAREN_REGEX = this.createParenRegex(cfg.openParens); 39 | this.CLOSE_PAREN_REGEX = this.createParenRegex(cfg.closeParens); 40 | 41 | this.INDEXED_PLACEHOLDER_REGEX = this.createPlaceholderRegex( 42 | cfg.indexedPlaceholderTypes, 43 | '[0-9]*' 44 | ); 45 | this.IDENT_NAMED_PLACEHOLDER_REGEX = this.createPlaceholderRegex( 46 | cfg.namedPlaceholderTypes, 47 | '[a-zA-Z0-9._$]+' 48 | ); 49 | this.STRING_NAMED_PLACEHOLDER_REGEX = this.createPlaceholderRegex( 50 | cfg.namedPlaceholderTypes, 51 | this.createStringPattern(cfg.stringTypes) 52 | ); 53 | } 54 | 55 | createLineCommentRegex(lineCommentTypes) { 56 | return new RegExp( 57 | `^((?:${lineCommentTypes.map(c => escapeRegExp(c)).join('|')}).*?(?:\r\n|\r|\n|$))`, 58 | 'u' 59 | ); 60 | } 61 | 62 | createReservedWordRegex(reservedWords) { 63 | const reservedWordsPattern = reservedWords.join('|').replace(/ /gu, '\\s+'); 64 | return new RegExp(`^(${reservedWordsPattern})\\b`, 'iu'); 65 | } 66 | 67 | createWordRegex(specialChars = []) { 68 | return new RegExp( 69 | `^([\\p{Alphabetic}\\p{Mark}\\p{Decimal_Number}\\p{Connector_Punctuation}\\p{Join_Control}${specialChars.join( 70 | '' 71 | )}]+)`, 72 | 'u' 73 | ); 74 | } 75 | 76 | createStringRegex(stringTypes) { 77 | return new RegExp('^(' + this.createStringPattern(stringTypes) + ')', 'u'); 78 | } 79 | 80 | // This enables the following string patterns: 81 | // 1. backtick quoted string using `` to escape 82 | // 2. square bracket quoted string (SQL Server) using ]] to escape 83 | // 3. double quoted string using "" or \" to escape 84 | // 4. single quoted string using '' or \' to escape 85 | // 5. national character quoted string using N'' or N\' to escape 86 | createStringPattern(stringTypes) { 87 | const patterns = { 88 | '``': '((`[^`]*($|`))+)', 89 | '[]': '((\\[[^\\]]*($|\\]))(\\][^\\]]*($|\\]))*)', 90 | '""': '(("[^"\\\\]*(?:\\\\.[^"\\\\]*)*("|$))+)', 91 | "''": "(('[^'\\\\]*(?:\\\\.[^'\\\\]*)*('|$))+)", 92 | "N''": "((N'[^N'\\\\]*(?:\\\\.[^N'\\\\]*)*('|$))+)" 93 | }; 94 | 95 | return stringTypes.map(t => patterns[t]).join('|'); 96 | } 97 | 98 | createParenRegex(parens) { 99 | return new RegExp('^(' + parens.map(p => this.escapeParen(p)).join('|') + ')', 'iu'); 100 | } 101 | 102 | escapeParen(paren) { 103 | if (paren.length === 1) { 104 | // A single punctuation character 105 | return escapeRegExp(paren); 106 | } else { 107 | // longer word 108 | return '\\b' + paren + '\\b'; 109 | } 110 | } 111 | 112 | createPlaceholderRegex(types, pattern) { 113 | if (isEmpty(types)) { 114 | return false; 115 | } 116 | const typesRegex = types.map(escapeRegExp).join('|'); 117 | 118 | return new RegExp(`^((?:${typesRegex})(?:${pattern}))`, 'u'); 119 | } 120 | 121 | /** 122 | * Takes a SQL string and breaks it into tokens. 123 | * Each token is an object with type and value. 124 | * 125 | * @param {String} input The SQL string 126 | * @return {Object[]} tokens An array of tokens. 127 | * @return {String} token.type 128 | * @return {String} token.value 129 | */ 130 | tokenize(input) { 131 | if (!input) return []; 132 | 133 | const tokens = []; 134 | let token; 135 | 136 | // Keep processing the string until it is empty 137 | while (input.length) { 138 | // Get the next token and the token type 139 | token = this.getNextToken(input, token); 140 | // Advance the string 141 | input = input.substring(token.value.length); 142 | 143 | tokens.push(token); 144 | } 145 | return tokens; 146 | } 147 | 148 | getNextToken(input, previousToken) { 149 | return ( 150 | this.getWhitespaceToken(input) || 151 | this.getCommentToken(input) || 152 | this.getStringToken(input) || 153 | this.getOpenParenToken(input) || 154 | this.getCloseParenToken(input) || 155 | this.getPlaceholderToken(input) || 156 | this.getNumberToken(input) || 157 | this.getReservedWordToken(input, previousToken) || 158 | this.getWordToken(input) || 159 | this.getOperatorToken(input) 160 | ); 161 | } 162 | 163 | getWhitespaceToken(input) { 164 | return this.getTokenOnFirstMatch({ 165 | input, 166 | type: tokenTypes.WHITESPACE, 167 | regex: this.WHITESPACE_REGEX 168 | }); 169 | } 170 | 171 | getCommentToken(input) { 172 | return this.getLineCommentToken(input) || this.getBlockCommentToken(input); 173 | } 174 | 175 | getLineCommentToken(input) { 176 | return this.getTokenOnFirstMatch({ 177 | input, 178 | type: tokenTypes.LINE_COMMENT, 179 | regex: this.LINE_COMMENT_REGEX 180 | }); 181 | } 182 | 183 | getBlockCommentToken(input) { 184 | return this.getTokenOnFirstMatch({ 185 | input, 186 | type: tokenTypes.BLOCK_COMMENT, 187 | regex: this.BLOCK_COMMENT_REGEX 188 | }); 189 | } 190 | 191 | getStringToken(input) { 192 | return this.getTokenOnFirstMatch({ 193 | input, 194 | type: tokenTypes.STRING, 195 | regex: this.STRING_REGEX 196 | }); 197 | } 198 | 199 | getOpenParenToken(input) { 200 | return this.getTokenOnFirstMatch({ 201 | input, 202 | type: tokenTypes.OPEN_PAREN, 203 | regex: this.OPEN_PAREN_REGEX 204 | }); 205 | } 206 | 207 | getCloseParenToken(input) { 208 | return this.getTokenOnFirstMatch({ 209 | input, 210 | type: tokenTypes.CLOSE_PAREN, 211 | regex: this.CLOSE_PAREN_REGEX 212 | }); 213 | } 214 | 215 | getPlaceholderToken(input) { 216 | return ( 217 | this.getIdentNamedPlaceholderToken(input) || 218 | this.getStringNamedPlaceholderToken(input) || 219 | this.getIndexedPlaceholderToken(input) 220 | ); 221 | } 222 | 223 | getIdentNamedPlaceholderToken(input) { 224 | return this.getPlaceholderTokenWithKey({ 225 | input, 226 | regex: this.IDENT_NAMED_PLACEHOLDER_REGEX, 227 | parseKey: v => v.slice(1) 228 | }); 229 | } 230 | 231 | getStringNamedPlaceholderToken(input) { 232 | return this.getPlaceholderTokenWithKey({ 233 | input, 234 | regex: this.STRING_NAMED_PLACEHOLDER_REGEX, 235 | parseKey: v => this.getEscapedPlaceholderKey({ key: v.slice(2, -1), quoteChar: v.slice(-1) }) 236 | }); 237 | } 238 | 239 | getIndexedPlaceholderToken(input) { 240 | return this.getPlaceholderTokenWithKey({ 241 | input, 242 | regex: this.INDEXED_PLACEHOLDER_REGEX, 243 | parseKey: v => v.slice(1) 244 | }); 245 | } 246 | 247 | getPlaceholderTokenWithKey({ input, regex, parseKey }) { 248 | const token = this.getTokenOnFirstMatch({ input, regex, type: tokenTypes.PLACEHOLDER }); 249 | if (token) { 250 | token.key = parseKey(token.value); 251 | } 252 | return token; 253 | } 254 | 255 | getEscapedPlaceholderKey({ key, quoteChar }) { 256 | return key.replace(new RegExp(escapeRegExp('\\' + quoteChar), 'gu'), quoteChar); 257 | } 258 | 259 | // Decimal, binary, or hex numbers 260 | getNumberToken(input) { 261 | return this.getTokenOnFirstMatch({ 262 | input, 263 | type: tokenTypes.NUMBER, 264 | regex: this.NUMBER_REGEX 265 | }); 266 | } 267 | 268 | // Punctuation and symbols 269 | getOperatorToken(input) { 270 | return this.getTokenOnFirstMatch({ 271 | input, 272 | type: tokenTypes.OPERATOR, 273 | regex: this.OPERATOR_REGEX 274 | }); 275 | } 276 | 277 | getReservedWordToken(input, previousToken) { 278 | // A reserved word cannot be preceded by a "." 279 | // this makes it so in "my_table.from", "from" is not considered a reserved word 280 | if (previousToken && previousToken.value && previousToken.value === '.') { 281 | return; 282 | } 283 | return ( 284 | this.getTopLevelReservedToken(input) || 285 | this.getNewlineReservedToken(input) || 286 | this.getTopLevelReservedTokenNoIndent(input) || 287 | this.getPlainReservedToken(input) 288 | ); 289 | } 290 | 291 | getTopLevelReservedToken(input) { 292 | return this.getTokenOnFirstMatch({ 293 | input, 294 | type: tokenTypes.RESERVED_TOP_LEVEL, 295 | regex: this.RESERVED_TOP_LEVEL_REGEX 296 | }); 297 | } 298 | 299 | getNewlineReservedToken(input) { 300 | return this.getTokenOnFirstMatch({ 301 | input, 302 | type: tokenTypes.RESERVED_NEWLINE, 303 | regex: this.RESERVED_NEWLINE_REGEX 304 | }); 305 | } 306 | 307 | getTopLevelReservedTokenNoIndent(input) { 308 | return this.getTokenOnFirstMatch({ 309 | input, 310 | type: tokenTypes.RESERVED_TOP_LEVEL_NO_INDENT, 311 | regex: this.RESERVED_TOP_LEVEL_NO_INDENT_REGEX 312 | }); 313 | } 314 | 315 | getPlainReservedToken(input) { 316 | return this.getTokenOnFirstMatch({ 317 | input, 318 | type: tokenTypes.RESERVED, 319 | regex: this.RESERVED_PLAIN_REGEX 320 | }); 321 | } 322 | 323 | getWordToken(input) { 324 | return this.getTokenOnFirstMatch({ 325 | input, 326 | type: tokenTypes.WORD, 327 | regex: this.WORD_REGEX 328 | }); 329 | } 330 | 331 | getTokenOnFirstMatch({ input, type, regex }) { 332 | const matches = input.match(regex); 333 | 334 | if (matches) { 335 | return { type, value: matches[1] }; 336 | } 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/core/tokenTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Constants for token types 3 | */ 4 | export default { 5 | WHITESPACE: 'whitespace', 6 | WORD: 'word', 7 | STRING: 'string', 8 | RESERVED: 'reserved', 9 | RESERVED_TOP_LEVEL: 'reserved-top-level', 10 | RESERVED_TOP_LEVEL_NO_INDENT: 'reserved-top-level-no-indent', 11 | RESERVED_NEWLINE: 'reserved-newline', 12 | OPERATOR: 'operator', 13 | OPEN_PAREN: 'open-paren', 14 | CLOSE_PAREN: 'close-paren', 15 | LINE_COMMENT: 'line-comment', 16 | BLOCK_COMMENT: 'block-comment', 17 | NUMBER: 'number', 18 | PLACEHOLDER: 'placeholder' 19 | }; 20 | -------------------------------------------------------------------------------- /src/languages/Db2Formatter.js: -------------------------------------------------------------------------------- 1 | import Formatter from '../core/Formatter'; 2 | import Tokenizer from '../core/Tokenizer'; 3 | 4 | const reservedWords = [ 5 | 'ABS', 6 | 'ACTIVATE', 7 | 'ALIAS', 8 | 'ALL', 9 | 'ALLOCATE', 10 | 'ALLOW', 11 | 'ALTER', 12 | 'ANY', 13 | 'ARE', 14 | 'ARRAY', 15 | 'AS', 16 | 'ASC', 17 | 'ASENSITIVE', 18 | 'ASSOCIATE', 19 | 'ASUTIME', 20 | 'ASYMMETRIC', 21 | 'AT', 22 | 'ATOMIC', 23 | 'ATTRIBUTES', 24 | 'AUDIT', 25 | 'AUTHORIZATION', 26 | 'AUX', 27 | 'AUXILIARY', 28 | 'AVG', 29 | 'BEFORE', 30 | 'BEGIN', 31 | 'BETWEEN', 32 | 'BIGINT', 33 | 'BINARY', 34 | 'BLOB', 35 | 'BOOLEAN', 36 | 'BOTH', 37 | 'BUFFERPOOL', 38 | 'BY', 39 | 'CACHE', 40 | 'CALL', 41 | 'CALLED', 42 | 'CAPTURE', 43 | 'CARDINALITY', 44 | 'CASCADED', 45 | 'CASE', 46 | 'CAST', 47 | 'CCSID', 48 | 'CEIL', 49 | 'CEILING', 50 | 'CHAR', 51 | 'CHARACTER', 52 | 'CHARACTER_LENGTH', 53 | 'CHAR_LENGTH', 54 | 'CHECK', 55 | 'CLOB', 56 | 'CLONE', 57 | 'CLOSE', 58 | 'CLUSTER', 59 | 'COALESCE', 60 | 'COLLATE', 61 | 'COLLECT', 62 | 'COLLECTION', 63 | 'COLLID', 64 | 'COLUMN', 65 | 'COMMENT', 66 | 'COMMIT', 67 | 'CONCAT', 68 | 'CONDITION', 69 | 'CONNECT', 70 | 'CONNECTION', 71 | 'CONSTRAINT', 72 | 'CONTAINS', 73 | 'CONTINUE', 74 | 'CONVERT', 75 | 'CORR', 76 | 'CORRESPONDING', 77 | 'COUNT', 78 | 'COUNT_BIG', 79 | 'COVAR_POP', 80 | 'COVAR_SAMP', 81 | 'CREATE', 82 | 'CROSS', 83 | 'CUBE', 84 | 'CUME_DIST', 85 | 'CURRENT', 86 | 'CURRENT_DATE', 87 | 'CURRENT_DEFAULT_TRANSFORM_GROUP', 88 | 'CURRENT_LC_CTYPE', 89 | 'CURRENT_PATH', 90 | 'CURRENT_ROLE', 91 | 'CURRENT_SCHEMA', 92 | 'CURRENT_SERVER', 93 | 'CURRENT_TIME', 94 | 'CURRENT_TIMESTAMP', 95 | 'CURRENT_TIMEZONE', 96 | 'CURRENT_TRANSFORM_GROUP_FOR_TYPE', 97 | 'CURRENT_USER', 98 | 'CURSOR', 99 | 'CYCLE', 100 | 'DATA', 101 | 'DATABASE', 102 | 'DATAPARTITIONNAME', 103 | 'DATAPARTITIONNUM', 104 | 'DATE', 105 | 'DAY', 106 | 'DAYS', 107 | 'DB2GENERAL', 108 | 'DB2GENRL', 109 | 'DB2SQL', 110 | 'DBINFO', 111 | 'DBPARTITIONNAME', 112 | 'DBPARTITIONNUM', 113 | 'DEALLOCATE', 114 | 'DEC', 115 | 'DECIMAL', 116 | 'DECLARE', 117 | 'DEFAULT', 118 | 'DEFAULTS', 119 | 'DEFINITION', 120 | 'DELETE', 121 | 'DENSERANK', 122 | 'DENSE_RANK', 123 | 'DEREF', 124 | 'DESCRIBE', 125 | 'DESCRIPTOR', 126 | 'DETERMINISTIC', 127 | 'DIAGNOSTICS', 128 | 'DISABLE', 129 | 'DISALLOW', 130 | 'DISCONNECT', 131 | 'DISTINCT', 132 | 'DO', 133 | 'DOCUMENT', 134 | 'DOUBLE', 135 | 'DROP', 136 | 'DSSIZE', 137 | 'DYNAMIC', 138 | 'EACH', 139 | 'EDITPROC', 140 | 'ELEMENT', 141 | 'ELSE', 142 | 'ELSEIF', 143 | 'ENABLE', 144 | 'ENCODING', 145 | 'ENCRYPTION', 146 | 'END', 147 | 'END-EXEC', 148 | 'ENDING', 149 | 'ERASE', 150 | 'ESCAPE', 151 | 'EVERY', 152 | 'EXCEPTION', 153 | 'EXCLUDING', 154 | 'EXCLUSIVE', 155 | 'EXEC', 156 | 'EXECUTE', 157 | 'EXISTS', 158 | 'EXIT', 159 | 'EXP', 160 | 'EXPLAIN', 161 | 'EXTENDED', 162 | 'EXTERNAL', 163 | 'EXTRACT', 164 | 'FALSE', 165 | 'FENCED', 166 | 'FETCH', 167 | 'FIELDPROC', 168 | 'FILE', 169 | 'FILTER', 170 | 'FINAL', 171 | 'FIRST', 172 | 'FLOAT', 173 | 'FLOOR', 174 | 'FOR', 175 | 'FOREIGN', 176 | 'FREE', 177 | 'FULL', 178 | 'FUNCTION', 179 | 'FUSION', 180 | 'GENERAL', 181 | 'GENERATED', 182 | 'GET', 183 | 'GLOBAL', 184 | 'GOTO', 185 | 'GRANT', 186 | 'GRAPHIC', 187 | 'GROUP', 188 | 'GROUPING', 189 | 'HANDLER', 190 | 'HASH', 191 | 'HASHED_VALUE', 192 | 'HINT', 193 | 'HOLD', 194 | 'HOUR', 195 | 'HOURS', 196 | 'IDENTITY', 197 | 'IF', 198 | 'IMMEDIATE', 199 | 'IN', 200 | 'INCLUDING', 201 | 'INCLUSIVE', 202 | 'INCREMENT', 203 | 'INDEX', 204 | 'INDICATOR', 205 | 'INDICATORS', 206 | 'INF', 207 | 'INFINITY', 208 | 'INHERIT', 209 | 'INNER', 210 | 'INOUT', 211 | 'INSENSITIVE', 212 | 'INSERT', 213 | 'INT', 214 | 'INTEGER', 215 | 'INTEGRITY', 216 | 'INTERSECTION', 217 | 'INTERVAL', 218 | 'INTO', 219 | 'IS', 220 | 'ISOBID', 221 | 'ISOLATION', 222 | 'ITERATE', 223 | 'JAR', 224 | 'JAVA', 225 | 'KEEP', 226 | 'KEY', 227 | 'LABEL', 228 | 'LANGUAGE', 229 | 'LARGE', 230 | 'LATERAL', 231 | 'LC_CTYPE', 232 | 'LEADING', 233 | 'LEAVE', 234 | 'LEFT', 235 | 'LIKE', 236 | 'LINKTYPE', 237 | 'LN', 238 | 'LOCAL', 239 | 'LOCALDATE', 240 | 'LOCALE', 241 | 'LOCALTIME', 242 | 'LOCALTIMESTAMP', 243 | 'LOCATOR', 244 | 'LOCATORS', 245 | 'LOCK', 246 | 'LOCKMAX', 247 | 'LOCKSIZE', 248 | 'LONG', 249 | 'LOOP', 250 | 'LOWER', 251 | 'MAINTAINED', 252 | 'MATCH', 253 | 'MATERIALIZED', 254 | 'MAX', 255 | 'MAXVALUE', 256 | 'MEMBER', 257 | 'MERGE', 258 | 'METHOD', 259 | 'MICROSECOND', 260 | 'MICROSECONDS', 261 | 'MIN', 262 | 'MINUTE', 263 | 'MINUTES', 264 | 'MINVALUE', 265 | 'MOD', 266 | 'MODE', 267 | 'MODIFIES', 268 | 'MODULE', 269 | 'MONTH', 270 | 'MONTHS', 271 | 'MULTISET', 272 | 'NAN', 273 | 'NATIONAL', 274 | 'NATURAL', 275 | 'NCHAR', 276 | 'NCLOB', 277 | 'NEW', 278 | 'NEW_TABLE', 279 | 'NEXTVAL', 280 | 'NO', 281 | 'NOCACHE', 282 | 'NOCYCLE', 283 | 'NODENAME', 284 | 'NODENUMBER', 285 | 'NOMAXVALUE', 286 | 'NOMINVALUE', 287 | 'NONE', 288 | 'NOORDER', 289 | 'NORMALIZE', 290 | 'NORMALIZED', 291 | 'NOT', 292 | 'NULL', 293 | 'NULLIF', 294 | 'NULLS', 295 | 'NUMERIC', 296 | 'NUMPARTS', 297 | 'OBID', 298 | 'OCTET_LENGTH', 299 | 'OF', 300 | 'OFFSET', 301 | 'OLD', 302 | 'OLD_TABLE', 303 | 'ON', 304 | 'ONLY', 305 | 'OPEN', 306 | 'OPTIMIZATION', 307 | 'OPTIMIZE', 308 | 'OPTION', 309 | 'ORDER', 310 | 'OUT', 311 | 'OUTER', 312 | 'OVER', 313 | 'OVERLAPS', 314 | 'OVERLAY', 315 | 'OVERRIDING', 316 | 'PACKAGE', 317 | 'PADDED', 318 | 'PAGESIZE', 319 | 'PARAMETER', 320 | 'PART', 321 | 'PARTITION', 322 | 'PARTITIONED', 323 | 'PARTITIONING', 324 | 'PARTITIONS', 325 | 'PASSWORD', 326 | 'PATH', 327 | 'PERCENTILE_CONT', 328 | 'PERCENTILE_DISC', 329 | 'PERCENT_RANK', 330 | 'PIECESIZE', 331 | 'PLAN', 332 | 'POSITION', 333 | 'POWER', 334 | 'PRECISION', 335 | 'PREPARE', 336 | 'PREVVAL', 337 | 'PRIMARY', 338 | 'PRIQTY', 339 | 'PRIVILEGES', 340 | 'PROCEDURE', 341 | 'PROGRAM', 342 | 'PSID', 343 | 'PUBLIC', 344 | 'QUERY', 345 | 'QUERYNO', 346 | 'RANGE', 347 | 'RANK', 348 | 'READ', 349 | 'READS', 350 | 'REAL', 351 | 'RECOVERY', 352 | 'RECURSIVE', 353 | 'REF', 354 | 'REFERENCES', 355 | 'REFERENCING', 356 | 'REFRESH', 357 | 'REGR_AVGX', 358 | 'REGR_AVGY', 359 | 'REGR_COUNT', 360 | 'REGR_INTERCEPT', 361 | 'REGR_R2', 362 | 'REGR_SLOPE', 363 | 'REGR_SXX', 364 | 'REGR_SXY', 365 | 'REGR_SYY', 366 | 'RELEASE', 367 | 'RENAME', 368 | 'REPEAT', 369 | 'RESET', 370 | 'RESIGNAL', 371 | 'RESTART', 372 | 'RESTRICT', 373 | 'RESULT', 374 | 'RESULT_SET_LOCATOR', 375 | 'RETURN', 376 | 'RETURNS', 377 | 'REVOKE', 378 | 'RIGHT', 379 | 'ROLE', 380 | 'ROLLBACK', 381 | 'ROLLUP', 382 | 'ROUND_CEILING', 383 | 'ROUND_DOWN', 384 | 'ROUND_FLOOR', 385 | 'ROUND_HALF_DOWN', 386 | 'ROUND_HALF_EVEN', 387 | 'ROUND_HALF_UP', 388 | 'ROUND_UP', 389 | 'ROUTINE', 390 | 'ROW', 391 | 'ROWNUMBER', 392 | 'ROWS', 393 | 'ROWSET', 394 | 'ROW_NUMBER', 395 | 'RRN', 396 | 'RUN', 397 | 'SAVEPOINT', 398 | 'SCHEMA', 399 | 'SCOPE', 400 | 'SCRATCHPAD', 401 | 'SCROLL', 402 | 'SEARCH', 403 | 'SECOND', 404 | 'SECONDS', 405 | 'SECQTY', 406 | 'SECURITY', 407 | 'SENSITIVE', 408 | 'SEQUENCE', 409 | 'SESSION', 410 | 'SESSION_USER', 411 | 'SIGNAL', 412 | 'SIMILAR', 413 | 'SIMPLE', 414 | 'SMALLINT', 415 | 'SNAN', 416 | 'SOME', 417 | 'SOURCE', 418 | 'SPECIFIC', 419 | 'SPECIFICTYPE', 420 | 'SQL', 421 | 'SQLEXCEPTION', 422 | 'SQLID', 423 | 'SQLSTATE', 424 | 'SQLWARNING', 425 | 'SQRT', 426 | 'STACKED', 427 | 'STANDARD', 428 | 'START', 429 | 'STARTING', 430 | 'STATEMENT', 431 | 'STATIC', 432 | 'STATMENT', 433 | 'STAY', 434 | 'STDDEV_POP', 435 | 'STDDEV_SAMP', 436 | 'STOGROUP', 437 | 'STORES', 438 | 'STYLE', 439 | 'SUBMULTISET', 440 | 'SUBSTRING', 441 | 'SUM', 442 | 'SUMMARY', 443 | 'SYMMETRIC', 444 | 'SYNONYM', 445 | 'SYSFUN', 446 | 'SYSIBM', 447 | 'SYSPROC', 448 | 'SYSTEM', 449 | 'SYSTEM_USER', 450 | 'TABLE', 451 | 'TABLESAMPLE', 452 | 'TABLESPACE', 453 | 'THEN', 454 | 'TIME', 455 | 'TIMESTAMP', 456 | 'TIMEZONE_HOUR', 457 | 'TIMEZONE_MINUTE', 458 | 'TO', 459 | 'TRAILING', 460 | 'TRANSACTION', 461 | 'TRANSLATE', 462 | 'TRANSLATION', 463 | 'TREAT', 464 | 'TRIGGER', 465 | 'TRIM', 466 | 'TRUE', 467 | 'TRUNCATE', 468 | 'TYPE', 469 | 'UESCAPE', 470 | 'UNDO', 471 | 'UNIQUE', 472 | 'UNKNOWN', 473 | 'UNNEST', 474 | 'UNTIL', 475 | 'UPPER', 476 | 'USAGE', 477 | 'USER', 478 | 'USING', 479 | 'VALIDPROC', 480 | 'VALUE', 481 | 'VARCHAR', 482 | 'VARIABLE', 483 | 'VARIANT', 484 | 'VARYING', 485 | 'VAR_POP', 486 | 'VAR_SAMP', 487 | 'VCAT', 488 | 'VERSION', 489 | 'VIEW', 490 | 'VOLATILE', 491 | 'VOLUMES', 492 | 'WHEN', 493 | 'WHENEVER', 494 | 'WHILE', 495 | 'WIDTH_BUCKET', 496 | 'WINDOW', 497 | 'WITH', 498 | 'WITHIN', 499 | 'WITHOUT', 500 | 'WLM', 501 | 'WRITE', 502 | 'XMLELEMENT', 503 | 'XMLEXISTS', 504 | 'XMLNAMESPACES', 505 | 'YEAR', 506 | 'YEARS' 507 | ]; 508 | 509 | const reservedTopLevelWords = [ 510 | 'ADD', 511 | 'AFTER', 512 | 'ALTER COLUMN', 513 | 'ALTER TABLE', 514 | 'DELETE FROM', 515 | 'EXCEPT', 516 | 'FETCH FIRST', 517 | 'FROM', 518 | 'GROUP BY', 519 | 'GO', 520 | 'HAVING', 521 | 'INSERT INTO', 522 | 'INTERSECT', 523 | 'LIMIT', 524 | 'ORDER BY', 525 | 'SELECT', 526 | 'SET CURRENT SCHEMA', 527 | 'SET SCHEMA', 528 | 'SET', 529 | 'UPDATE', 530 | 'VALUES', 531 | 'WHERE' 532 | ]; 533 | 534 | const reservedTopLevelWordsNoIndent = ['INTERSECT', 'INTERSECT ALL', 'MINUS', 'UNION', 'UNION ALL']; 535 | 536 | const reservedNewlineWords = [ 537 | 'AND', 538 | 'CROSS JOIN', 539 | 'INNER JOIN', 540 | 'JOIN', 541 | 'LEFT JOIN', 542 | 'LEFT OUTER JOIN', 543 | 'OR', 544 | 'OUTER JOIN', 545 | 'RIGHT JOIN', 546 | 'RIGHT OUTER JOIN' 547 | ]; 548 | 549 | let tokenizer; 550 | 551 | export default class Db2Formatter { 552 | /** 553 | * @param {Object} cfg Different set of configurations 554 | */ 555 | constructor(cfg) { 556 | this.cfg = cfg; 557 | } 558 | 559 | /** 560 | * Formats DB2 query to make it easier to read 561 | * 562 | * @param {String} query The DB2 query string 563 | * @return {String} formatted string 564 | */ 565 | format(query) { 566 | if (!tokenizer) { 567 | tokenizer = new Tokenizer({ 568 | reservedWords, 569 | reservedTopLevelWords, 570 | reservedNewlineWords, 571 | reservedTopLevelWordsNoIndent, 572 | stringTypes: [`""`, "''", '``', '[]'], 573 | openParens: ['('], 574 | closeParens: [')'], 575 | indexedPlaceholderTypes: ['?'], 576 | namedPlaceholderTypes: [':'], 577 | lineCommentTypes: ['--'], 578 | specialWordChars: ['#', '@'] 579 | }); 580 | } 581 | return new Formatter(this.cfg, tokenizer).format(query); 582 | } 583 | } 584 | -------------------------------------------------------------------------------- /src/languages/N1qlFormatter.js: -------------------------------------------------------------------------------- 1 | import Formatter from '../core/Formatter'; 2 | import Tokenizer from '../core/Tokenizer'; 3 | 4 | const reservedWords = [ 5 | 'ALL', 6 | 'ALTER', 7 | 'ANALYZE', 8 | 'AND', 9 | 'ANY', 10 | 'ARRAY', 11 | 'AS', 12 | 'ASC', 13 | 'BEGIN', 14 | 'BETWEEN', 15 | 'BINARY', 16 | 'BOOLEAN', 17 | 'BREAK', 18 | 'BUCKET', 19 | 'BUILD', 20 | 'BY', 21 | 'CALL', 22 | 'CASE', 23 | 'CAST', 24 | 'CLUSTER', 25 | 'COLLATE', 26 | 'COLLECTION', 27 | 'COMMIT', 28 | 'CONNECT', 29 | 'CONTINUE', 30 | 'CORRELATE', 31 | 'COVER', 32 | 'CREATE', 33 | 'DATABASE', 34 | 'DATASET', 35 | 'DATASTORE', 36 | 'DECLARE', 37 | 'DECREMENT', 38 | 'DELETE', 39 | 'DERIVED', 40 | 'DESC', 41 | 'DESCRIBE', 42 | 'DISTINCT', 43 | 'DO', 44 | 'DROP', 45 | 'EACH', 46 | 'ELEMENT', 47 | 'ELSE', 48 | 'END', 49 | 'EVERY', 50 | 'EXCEPT', 51 | 'EXCLUDE', 52 | 'EXECUTE', 53 | 'EXISTS', 54 | 'EXPLAIN', 55 | 'FALSE', 56 | 'FETCH', 57 | 'FIRST', 58 | 'FLATTEN', 59 | 'FOR', 60 | 'FORCE', 61 | 'FROM', 62 | 'FUNCTION', 63 | 'GRANT', 64 | 'GROUP', 65 | 'GSI', 66 | 'HAVING', 67 | 'IF', 68 | 'IGNORE', 69 | 'ILIKE', 70 | 'IN', 71 | 'INCLUDE', 72 | 'INCREMENT', 73 | 'INDEX', 74 | 'INFER', 75 | 'INLINE', 76 | 'INNER', 77 | 'INSERT', 78 | 'INTERSECT', 79 | 'INTO', 80 | 'IS', 81 | 'JOIN', 82 | 'KEY', 83 | 'KEYS', 84 | 'KEYSPACE', 85 | 'KNOWN', 86 | 'LAST', 87 | 'LEFT', 88 | 'LET', 89 | 'LETTING', 90 | 'LIKE', 91 | 'LIMIT', 92 | 'LSM', 93 | 'MAP', 94 | 'MAPPING', 95 | 'MATCHED', 96 | 'MATERIALIZED', 97 | 'MERGE', 98 | 'MISSING', 99 | 'NAMESPACE', 100 | 'NEST', 101 | 'NOT', 102 | 'NULL', 103 | 'NUMBER', 104 | 'OBJECT', 105 | 'OFFSET', 106 | 'ON', 107 | 'OPTION', 108 | 'OR', 109 | 'ORDER', 110 | 'OUTER', 111 | 'OVER', 112 | 'PARSE', 113 | 'PARTITION', 114 | 'PASSWORD', 115 | 'PATH', 116 | 'POOL', 117 | 'PREPARE', 118 | 'PRIMARY', 119 | 'PRIVATE', 120 | 'PRIVILEGE', 121 | 'PROCEDURE', 122 | 'PUBLIC', 123 | 'RAW', 124 | 'REALM', 125 | 'REDUCE', 126 | 'RENAME', 127 | 'RETURN', 128 | 'RETURNING', 129 | 'REVOKE', 130 | 'RIGHT', 131 | 'ROLE', 132 | 'ROLLBACK', 133 | 'SATISFIES', 134 | 'SCHEMA', 135 | 'SELECT', 136 | 'SELF', 137 | 'SEMI', 138 | 'SET', 139 | 'SHOW', 140 | 'SOME', 141 | 'START', 142 | 'STATISTICS', 143 | 'STRING', 144 | 'SYSTEM', 145 | 'THEN', 146 | 'TO', 147 | 'TRANSACTION', 148 | 'TRIGGER', 149 | 'TRUE', 150 | 'TRUNCATE', 151 | 'UNDER', 152 | 'UNION', 153 | 'UNIQUE', 154 | 'UNKNOWN', 155 | 'UNNEST', 156 | 'UNSET', 157 | 'UPDATE', 158 | 'UPSERT', 159 | 'USE', 160 | 'USER', 161 | 'USING', 162 | 'VALIDATE', 163 | 'VALUE', 164 | 'VALUED', 165 | 'VALUES', 166 | 'VIA', 167 | 'VIEW', 168 | 'WHEN', 169 | 'WHERE', 170 | 'WHILE', 171 | 'WITH', 172 | 'WITHIN', 173 | 'WORK', 174 | 'XOR' 175 | ]; 176 | 177 | const reservedTopLevelWords = [ 178 | 'DELETE FROM', 179 | 'EXCEPT ALL', 180 | 'EXCEPT', 181 | 'EXPLAIN DELETE FROM', 182 | 'EXPLAIN UPDATE', 183 | 'EXPLAIN UPSERT', 184 | 'FROM', 185 | 'GROUP BY', 186 | 'HAVING', 187 | 'INFER', 188 | 'INSERT INTO', 189 | 'LET', 190 | 'LIMIT', 191 | 'MERGE', 192 | 'NEST', 193 | 'ORDER BY', 194 | 'PREPARE', 195 | 'SELECT', 196 | 'SET CURRENT SCHEMA', 197 | 'SET SCHEMA', 198 | 'SET', 199 | 'UNNEST', 200 | 'UPDATE', 201 | 'UPSERT', 202 | 'USE KEYS', 203 | 'VALUES', 204 | 'WHERE' 205 | ]; 206 | 207 | const reservedTopLevelWordsNoIndent = ['INTERSECT', 'INTERSECT ALL', 'MINUS', 'UNION', 'UNION ALL']; 208 | 209 | const reservedNewlineWords = [ 210 | 'AND', 211 | 'INNER JOIN', 212 | 'JOIN', 213 | 'LEFT JOIN', 214 | 'LEFT OUTER JOIN', 215 | 'OR', 216 | 'OUTER JOIN', 217 | 'RIGHT JOIN', 218 | 'RIGHT OUTER JOIN', 219 | 'XOR' 220 | ]; 221 | 222 | let tokenizer; 223 | 224 | export default class N1qlFormatter { 225 | /** 226 | * @param {Object} cfg Different set of configurations 227 | */ 228 | constructor(cfg) { 229 | this.cfg = cfg; 230 | } 231 | 232 | /** 233 | * Format the whitespace in a N1QL string to make it easier to read 234 | * 235 | * @param {String} query The N1QL string 236 | * @return {String} formatted string 237 | */ 238 | format(query) { 239 | if (!tokenizer) { 240 | tokenizer = new Tokenizer({ 241 | reservedWords, 242 | reservedTopLevelWords, 243 | reservedNewlineWords, 244 | reservedTopLevelWordsNoIndent, 245 | stringTypes: [`""`, "''", '``'], 246 | openParens: ['(', '[', '{'], 247 | closeParens: [')', ']', '}'], 248 | namedPlaceholderTypes: ['$'], 249 | lineCommentTypes: ['#', '--'] 250 | }); 251 | } 252 | return new Formatter(this.cfg, tokenizer).format(query); 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/languages/PlSqlFormatter.js: -------------------------------------------------------------------------------- 1 | import Formatter from '../core/Formatter'; 2 | import Tokenizer from '../core/Tokenizer'; 3 | import tokenTypes from '../core/tokenTypes'; 4 | 5 | const reservedWords = [ 6 | 'A', 7 | 'ACCESSIBLE', 8 | 'AGENT', 9 | 'AGGREGATE', 10 | 'ALL', 11 | 'ALTER', 12 | 'ANY', 13 | 'ARRAY', 14 | 'AS', 15 | 'ASC', 16 | 'AT', 17 | 'ATTRIBUTE', 18 | 'AUTHID', 19 | 'AVG', 20 | 'BETWEEN', 21 | 'BFILE_BASE', 22 | 'BINARY_INTEGER', 23 | 'BINARY', 24 | 'BLOB_BASE', 25 | 'BLOCK', 26 | 'BODY', 27 | 'BOOLEAN', 28 | 'BOTH', 29 | 'BOUND', 30 | 'BREADTH', 31 | 'BULK', 32 | 'BY', 33 | 'BYTE', 34 | 'C', 35 | 'CALL', 36 | 'CALLING', 37 | 'CASCADE', 38 | 'CASE', 39 | 'CHAR_BASE', 40 | 'CHAR', 41 | 'CHARACTER', 42 | 'CHARSET', 43 | 'CHARSETFORM', 44 | 'CHARSETID', 45 | 'CHECK', 46 | 'CLOB_BASE', 47 | 'CLONE', 48 | 'CLOSE', 49 | 'CLUSTER', 50 | 'CLUSTERS', 51 | 'COALESCE', 52 | 'COLAUTH', 53 | 'COLLECT', 54 | 'COLUMNS', 55 | 'COMMENT', 56 | 'COMMIT', 57 | 'COMMITTED', 58 | 'COMPILED', 59 | 'COMPRESS', 60 | 'CONNECT', 61 | 'CONSTANT', 62 | 'CONSTRUCTOR', 63 | 'CONTEXT', 64 | 'CONTINUE', 65 | 'CONVERT', 66 | 'COUNT', 67 | 'CRASH', 68 | 'CREATE', 69 | 'CREDENTIAL', 70 | 'CURRENT', 71 | 'CURRVAL', 72 | 'CURSOR', 73 | 'CUSTOMDATUM', 74 | 'DANGLING', 75 | 'DATA', 76 | 'DATE_BASE', 77 | 'DATE', 78 | 'DAY', 79 | 'DECIMAL', 80 | 'DEFAULT', 81 | 'DEFINE', 82 | 'DELETE', 83 | 'DEPTH', 84 | 'DESC', 85 | 'DETERMINISTIC', 86 | 'DIRECTORY', 87 | 'DISTINCT', 88 | 'DO', 89 | 'DOUBLE', 90 | 'DROP', 91 | 'DURATION', 92 | 'ELEMENT', 93 | 'ELSIF', 94 | 'EMPTY', 95 | 'END', 96 | 'ESCAPE', 97 | 'EXCEPTIONS', 98 | 'EXCLUSIVE', 99 | 'EXECUTE', 100 | 'EXISTS', 101 | 'EXIT', 102 | 'EXTENDS', 103 | 'EXTERNAL', 104 | 'EXTRACT', 105 | 'FALSE', 106 | 'FETCH', 107 | 'FINAL', 108 | 'FIRST', 109 | 'FIXED', 110 | 'FLOAT', 111 | 'FOR', 112 | 'FORALL', 113 | 'FORCE', 114 | 'FROM', 115 | 'FUNCTION', 116 | 'GENERAL', 117 | 'GOTO', 118 | 'GRANT', 119 | 'GROUP', 120 | 'HASH', 121 | 'HEAP', 122 | 'HIDDEN', 123 | 'HOUR', 124 | 'IDENTIFIED', 125 | 'IF', 126 | 'IMMEDIATE', 127 | 'IN', 128 | 'INCLUDING', 129 | 'INDEX', 130 | 'INDEXES', 131 | 'INDICATOR', 132 | 'INDICES', 133 | 'INFINITE', 134 | 'INSTANTIABLE', 135 | 'INT', 136 | 'INTEGER', 137 | 'INTERFACE', 138 | 'INTERVAL', 139 | 'INTO', 140 | 'INVALIDATE', 141 | 'IS', 142 | 'ISOLATION', 143 | 'JAVA', 144 | 'LANGUAGE', 145 | 'LARGE', 146 | 'LEADING', 147 | 'LENGTH', 148 | 'LEVEL', 149 | 'LIBRARY', 150 | 'LIKE', 151 | 'LIKE2', 152 | 'LIKE4', 153 | 'LIKEC', 154 | 'LIMITED', 155 | 'LOCAL', 156 | 'LOCK', 157 | 'LONG', 158 | 'MAP', 159 | 'MAX', 160 | 'MAXLEN', 161 | 'MEMBER', 162 | 'MERGE', 163 | 'MIN', 164 | 'MINUTE', 165 | 'MLSLABEL', 166 | 'MOD', 167 | 'MODE', 168 | 'MONTH', 169 | 'MULTISET', 170 | 'NAME', 171 | 'NAN', 172 | 'NATIONAL', 173 | 'NATIVE', 174 | 'NATURAL', 175 | 'NATURALN', 176 | 'NCHAR', 177 | 'NEW', 178 | 'NEXTVAL', 179 | 'NOCOMPRESS', 180 | 'NOCOPY', 181 | 'NOT', 182 | 'NOWAIT', 183 | 'NULL', 184 | 'NULLIF', 185 | 'NUMBER_BASE', 186 | 'NUMBER', 187 | 'OBJECT', 188 | 'OCICOLL', 189 | 'OCIDATE', 190 | 'OCIDATETIME', 191 | 'OCIDURATION', 192 | 'OCIINTERVAL', 193 | 'OCILOBLOCATOR', 194 | 'OCINUMBER', 195 | 'OCIRAW', 196 | 'OCIREF', 197 | 'OCIREFCURSOR', 198 | 'OCIROWID', 199 | 'OCISTRING', 200 | 'OCITYPE', 201 | 'OF', 202 | 'OLD', 203 | 'ON', 204 | 'ONLY', 205 | 'OPAQUE', 206 | 'OPEN', 207 | 'OPERATOR', 208 | 'OPTION', 209 | 'ORACLE', 210 | 'ORADATA', 211 | 'ORDER', 212 | 'ORGANIZATION', 213 | 'ORLANY', 214 | 'ORLVARY', 215 | 'OTHERS', 216 | 'OUT', 217 | 'OVERLAPS', 218 | 'OVERRIDING', 219 | 'PACKAGE', 220 | 'PARALLEL_ENABLE', 221 | 'PARAMETER', 222 | 'PARAMETERS', 223 | 'PARENT', 224 | 'PARTITION', 225 | 'PASCAL', 226 | 'PCTFREE', 227 | 'PIPE', 228 | 'PIPELINED', 229 | 'PLS_INTEGER', 230 | 'PLUGGABLE', 231 | 'POSITIVE', 232 | 'POSITIVEN', 233 | 'PRAGMA', 234 | 'PRECISION', 235 | 'PRIOR', 236 | 'PRIVATE', 237 | 'PROCEDURE', 238 | 'PUBLIC', 239 | 'RAISE', 240 | 'RANGE', 241 | 'RAW', 242 | 'READ', 243 | 'REAL', 244 | 'RECORD', 245 | 'REF', 246 | 'REFERENCE', 247 | 'RELEASE', 248 | 'RELIES_ON', 249 | 'REM', 250 | 'REMAINDER', 251 | 'RENAME', 252 | 'RESOURCE', 253 | 'RESULT_CACHE', 254 | 'RESULT', 255 | 'RETURN', 256 | 'RETURNING', 257 | 'REVERSE', 258 | 'REVOKE', 259 | 'ROLLBACK', 260 | 'ROW', 261 | 'ROWID', 262 | 'ROWNUM', 263 | 'ROWTYPE', 264 | 'SAMPLE', 265 | 'SAVE', 266 | 'SAVEPOINT', 267 | 'SB1', 268 | 'SB2', 269 | 'SB4', 270 | 'SEARCH', 271 | 'SECOND', 272 | 'SEGMENT', 273 | 'SELF', 274 | 'SEPARATE', 275 | 'SEQUENCE', 276 | 'SERIALIZABLE', 277 | 'SHARE', 278 | 'SHORT', 279 | 'SIZE_T', 280 | 'SIZE', 281 | 'SMALLINT', 282 | 'SOME', 283 | 'SPACE', 284 | 'SPARSE', 285 | 'SQL', 286 | 'SQLCODE', 287 | 'SQLDATA', 288 | 'SQLERRM', 289 | 'SQLNAME', 290 | 'SQLSTATE', 291 | 'STANDARD', 292 | 'START', 293 | 'STATIC', 294 | 'STDDEV', 295 | 'STORED', 296 | 'STRING', 297 | 'STRUCT', 298 | 'STYLE', 299 | 'SUBMULTISET', 300 | 'SUBPARTITION', 301 | 'SUBSTITUTABLE', 302 | 'SUBTYPE', 303 | 'SUCCESSFUL', 304 | 'SUM', 305 | 'SYNONYM', 306 | 'SYSDATE', 307 | 'TABAUTH', 308 | 'TABLE', 309 | 'TDO', 310 | 'THE', 311 | 'THEN', 312 | 'TIME', 313 | 'TIMESTAMP', 314 | 'TIMEZONE_ABBR', 315 | 'TIMEZONE_HOUR', 316 | 'TIMEZONE_MINUTE', 317 | 'TIMEZONE_REGION', 318 | 'TO', 319 | 'TRAILING', 320 | 'TRANSACTION', 321 | 'TRANSACTIONAL', 322 | 'TRIGGER', 323 | 'TRUE', 324 | 'TRUSTED', 325 | 'TYPE', 326 | 'UB1', 327 | 'UB2', 328 | 'UB4', 329 | 'UID', 330 | 'UNDER', 331 | 'UNIQUE', 332 | 'UNPLUG', 333 | 'UNSIGNED', 334 | 'UNTRUSTED', 335 | 'USE', 336 | 'USER', 337 | 'USING', 338 | 'VALIDATE', 339 | 'VALIST', 340 | 'VALUE', 341 | 'VARCHAR', 342 | 'VARCHAR2', 343 | 'VARIABLE', 344 | 'VARIANCE', 345 | 'VARRAY', 346 | 'VARYING', 347 | 'VIEW', 348 | 'VIEWS', 349 | 'VOID', 350 | 'WHENEVER', 351 | 'WHILE', 352 | 'WITH', 353 | 'WORK', 354 | 'WRAPPED', 355 | 'WRITE', 356 | 'YEAR', 357 | 'ZONE' 358 | ]; 359 | 360 | const reservedTopLevelWords = [ 361 | 'ADD', 362 | 'ALTER COLUMN', 363 | 'ALTER TABLE', 364 | 'BEGIN', 365 | 'CONNECT BY', 366 | 'DECLARE', 367 | 'DELETE FROM', 368 | 'DELETE', 369 | 'END', 370 | 'EXCEPT', 371 | 'EXCEPTION', 372 | 'FETCH FIRST', 373 | 'FROM', 374 | 'GROUP BY', 375 | 'HAVING', 376 | 'INSERT INTO', 377 | 'INSERT', 378 | 'LIMIT', 379 | 'LOOP', 380 | 'MODIFY', 381 | 'ORDER BY', 382 | 'SELECT', 383 | 'SET CURRENT SCHEMA', 384 | 'SET SCHEMA', 385 | 'SET', 386 | 'START WITH', 387 | 'UPDATE', 388 | 'VALUES', 389 | 'WHERE' 390 | ]; 391 | 392 | const reservedTopLevelWordsNoIndent = ['INTERSECT', 'INTERSECT ALL', 'MINUS', 'UNION', 'UNION ALL']; 393 | 394 | const reservedNewlineWords = [ 395 | 'AND', 396 | 'CROSS APPLY', 397 | 'CROSS JOIN', 398 | 'ELSE', 399 | 'END', 400 | 'INNER JOIN', 401 | 'JOIN', 402 | 'LEFT JOIN', 403 | 'LEFT OUTER JOIN', 404 | 'OR', 405 | 'OUTER APPLY', 406 | 'OUTER JOIN', 407 | 'RIGHT JOIN', 408 | 'RIGHT OUTER JOIN', 409 | 'WHEN', 410 | 'XOR' 411 | ]; 412 | 413 | const tokenOverride = (token, previousReservedToken) => { 414 | if ( 415 | token.type === tokenTypes.RESERVED_TOP_LEVEL && 416 | token.value === 'SET' && 417 | previousReservedToken.value === 'BY' 418 | ) { 419 | token.type = tokenTypes.RESERVED; 420 | return token; 421 | } 422 | }; 423 | 424 | let tokenizer; 425 | 426 | export default class PlSqlFormatter { 427 | /** 428 | * @param {Object} cfg Different set of configurations 429 | */ 430 | constructor(cfg) { 431 | this.cfg = cfg; 432 | } 433 | 434 | /** 435 | * Format the whitespace in a PL/SQL string to make it easier to read 436 | * 437 | * @param {String} query The PL/SQL string 438 | * @return {String} formatted string 439 | */ 440 | format(query) { 441 | if (!tokenizer) { 442 | tokenizer = new Tokenizer({ 443 | reservedWords, 444 | reservedTopLevelWords, 445 | reservedNewlineWords, 446 | reservedTopLevelWordsNoIndent, 447 | stringTypes: [`""`, "N''", "''", '``'], 448 | openParens: ['(', 'CASE'], 449 | closeParens: [')', 'END'], 450 | indexedPlaceholderTypes: ['?'], 451 | namedPlaceholderTypes: [':'], 452 | lineCommentTypes: ['--'], 453 | specialWordChars: ['_', '$', '#', '.', '@'] 454 | }); 455 | } 456 | return new Formatter(this.cfg, tokenizer, tokenOverride).format(query); 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /src/languages/StandardSqlFormatter.js: -------------------------------------------------------------------------------- 1 | import Formatter from '../core/Formatter'; 2 | import Tokenizer from '../core/Tokenizer'; 3 | 4 | const reservedWords = [ 5 | 'ACCESSIBLE', 6 | 'ACTION', 7 | 'AGAINST', 8 | 'AGGREGATE', 9 | 'ALGORITHM', 10 | 'ALL', 11 | 'ALTER', 12 | 'ANALYSE', 13 | 'ANALYZE', 14 | 'AS', 15 | 'ASC', 16 | 'AUTOCOMMIT', 17 | 'AUTO_INCREMENT', 18 | 'BACKUP', 19 | 'BEGIN', 20 | 'BETWEEN', 21 | 'BINLOG', 22 | 'BOTH', 23 | 'CASCADE', 24 | 'CASE', 25 | 'CHANGE', 26 | 'CHANGED', 27 | 'CHARACTER SET', 28 | 'CHARSET', 29 | 'CHECK', 30 | 'CHECKSUM', 31 | 'COLLATE', 32 | 'COLLATION', 33 | 'COLUMN', 34 | 'COLUMNS', 35 | 'COMMENT', 36 | 'COMMIT', 37 | 'COMMITTED', 38 | 'COMPRESSED', 39 | 'CONCURRENT', 40 | 'CONSTRAINT', 41 | 'CONTAINS', 42 | 'CONVERT', 43 | 'CREATE', 44 | 'CROSS', 45 | 'CURRENT_TIMESTAMP', 46 | 'DATABASE', 47 | 'DATABASES', 48 | 'DAY', 49 | 'DAY_HOUR', 50 | 'DAY_MINUTE', 51 | 'DAY_SECOND', 52 | 'DEFAULT', 53 | 'DEFINER', 54 | 'DELAYED', 55 | 'DELETE', 56 | 'DESC', 57 | 'DESCRIBE', 58 | 'DETERMINISTIC', 59 | 'DISTINCT', 60 | 'DISTINCTROW', 61 | 'DIV', 62 | 'DO', 63 | 'DROP', 64 | 'DUMPFILE', 65 | 'DUPLICATE', 66 | 'DYNAMIC', 67 | 'ELSE', 68 | 'ENCLOSED', 69 | 'END', 70 | 'ENGINE', 71 | 'ENGINES', 72 | 'ENGINE_TYPE', 73 | 'ESCAPE', 74 | 'ESCAPED', 75 | 'EVENTS', 76 | 'EXEC', 77 | 'EXECUTE', 78 | 'EXISTS', 79 | 'EXPLAIN', 80 | 'EXTENDED', 81 | 'FAST', 82 | 'FETCH', 83 | 'FIELDS', 84 | 'FILE', 85 | 'FIRST', 86 | 'FIXED', 87 | 'FLUSH', 88 | 'FOR', 89 | 'FORCE', 90 | 'FOREIGN', 91 | 'FULL', 92 | 'FULLTEXT', 93 | 'FUNCTION', 94 | 'GLOBAL', 95 | 'GRANT', 96 | 'GRANTS', 97 | 'GROUP_CONCAT', 98 | 'HEAP', 99 | 'HIGH_PRIORITY', 100 | 'HOSTS', 101 | 'HOUR', 102 | 'HOUR_MINUTE', 103 | 'HOUR_SECOND', 104 | 'IDENTIFIED', 105 | 'IF', 106 | 'IFNULL', 107 | 'IGNORE', 108 | 'IN', 109 | 'INDEX', 110 | 'INDEXES', 111 | 'INFILE', 112 | 'INSERT', 113 | 'INSERT_ID', 114 | 'INSERT_METHOD', 115 | 'INTERVAL', 116 | 'INTO', 117 | 'INVOKER', 118 | 'IS', 119 | 'ISOLATION', 120 | 'KEY', 121 | 'KEYS', 122 | 'KILL', 123 | 'LAST_INSERT_ID', 124 | 'LEADING', 125 | 'LEVEL', 126 | 'LIKE', 127 | 'LINEAR', 128 | 'LINES', 129 | 'LOAD', 130 | 'LOCAL', 131 | 'LOCK', 132 | 'LOCKS', 133 | 'LOGS', 134 | 'LOW_PRIORITY', 135 | 'MARIA', 136 | 'MASTER', 137 | 'MASTER_CONNECT_RETRY', 138 | 'MASTER_HOST', 139 | 'MASTER_LOG_FILE', 140 | 'MATCH', 141 | 'MAX_CONNECTIONS_PER_HOUR', 142 | 'MAX_QUERIES_PER_HOUR', 143 | 'MAX_ROWS', 144 | 'MAX_UPDATES_PER_HOUR', 145 | 'MAX_USER_CONNECTIONS', 146 | 'MEDIUM', 147 | 'MERGE', 148 | 'MINUTE', 149 | 'MINUTE_SECOND', 150 | 'MIN_ROWS', 151 | 'MODE', 152 | 'MODIFY', 153 | 'MONTH', 154 | 'MRG_MYISAM', 155 | 'MYISAM', 156 | 'NAMES', 157 | 'NATURAL', 158 | 'NOT', 159 | 'NOW()', 160 | 'NULL', 161 | 'OFFSET', 162 | 'ON DELETE', 163 | 'ON UPDATE', 164 | 'ON', 165 | 'ONLY', 166 | 'OPEN', 167 | 'OPTIMIZE', 168 | 'OPTION', 169 | 'OPTIONALLY', 170 | 'OUTFILE', 171 | 'PACK_KEYS', 172 | 'PAGE', 173 | 'PARTIAL', 174 | 'PARTITION', 175 | 'PARTITIONS', 176 | 'PASSWORD', 177 | 'PRIMARY', 178 | 'PRIVILEGES', 179 | 'PROCEDURE', 180 | 'PROCESS', 181 | 'PROCESSLIST', 182 | 'PURGE', 183 | 'QUICK', 184 | 'RAID0', 185 | 'RAID_CHUNKS', 186 | 'RAID_CHUNKSIZE', 187 | 'RAID_TYPE', 188 | 'RANGE', 189 | 'READ', 190 | 'READ_ONLY', 191 | 'READ_WRITE', 192 | 'REFERENCES', 193 | 'REGEXP', 194 | 'RELOAD', 195 | 'RENAME', 196 | 'REPAIR', 197 | 'REPEATABLE', 198 | 'REPLACE', 199 | 'REPLICATION', 200 | 'RESET', 201 | 'RESTORE', 202 | 'RESTRICT', 203 | 'RETURN', 204 | 'RETURNS', 205 | 'REVOKE', 206 | 'RLIKE', 207 | 'ROLLBACK', 208 | 'ROW', 209 | 'ROWS', 210 | 'ROW_FORMAT', 211 | 'SECOND', 212 | 'SECURITY', 213 | 'SEPARATOR', 214 | 'SERIALIZABLE', 215 | 'SESSION', 216 | 'SHARE', 217 | 'SHOW', 218 | 'SHUTDOWN', 219 | 'SLAVE', 220 | 'SONAME', 221 | 'SOUNDS', 222 | 'SQL', 223 | 'SQL_AUTO_IS_NULL', 224 | 'SQL_BIG_RESULT', 225 | 'SQL_BIG_SELECTS', 226 | 'SQL_BIG_TABLES', 227 | 'SQL_BUFFER_RESULT', 228 | 'SQL_CACHE', 229 | 'SQL_CALC_FOUND_ROWS', 230 | 'SQL_LOG_BIN', 231 | 'SQL_LOG_OFF', 232 | 'SQL_LOG_UPDATE', 233 | 'SQL_LOW_PRIORITY_UPDATES', 234 | 'SQL_MAX_JOIN_SIZE', 235 | 'SQL_NO_CACHE', 236 | 'SQL_QUOTE_SHOW_CREATE', 237 | 'SQL_SAFE_UPDATES', 238 | 'SQL_SELECT_LIMIT', 239 | 'SQL_SLAVE_SKIP_COUNTER', 240 | 'SQL_SMALL_RESULT', 241 | 'SQL_WARNINGS', 242 | 'START', 243 | 'STARTING', 244 | 'STATUS', 245 | 'STOP', 246 | 'STORAGE', 247 | 'STRAIGHT_JOIN', 248 | 'STRING', 249 | 'STRIPED', 250 | 'SUPER', 251 | 'TABLE', 252 | 'TABLES', 253 | 'TEMPORARY', 254 | 'TERMINATED', 255 | 'THEN', 256 | 'TO', 257 | 'TRAILING', 258 | 'TRANSACTIONAL', 259 | 'TRUE', 260 | 'TRUNCATE', 261 | 'TYPE', 262 | 'TYPES', 263 | 'UNCOMMITTED', 264 | 'UNIQUE', 265 | 'UNLOCK', 266 | 'UNSIGNED', 267 | 'USAGE', 268 | 'USE', 269 | 'USING', 270 | 'VARIABLES', 271 | 'VIEW', 272 | 'WHEN', 273 | 'WITH', 274 | 'WORK', 275 | 'WRITE', 276 | 'YEAR_MONTH' 277 | ]; 278 | 279 | const reservedTopLevelWords = [ 280 | 'ADD', 281 | 'AFTER', 282 | 'ALTER COLUMN', 283 | 'ALTER TABLE', 284 | 'DELETE FROM', 285 | 'EXCEPT', 286 | 'FETCH FIRST', 287 | 'FROM', 288 | 'GROUP BY', 289 | 'GO', 290 | 'HAVING', 291 | 'INSERT INTO', 292 | 'INSERT', 293 | 'LIMIT', 294 | 'MODIFY', 295 | 'ORDER BY', 296 | 'SELECT', 297 | 'SET CURRENT SCHEMA', 298 | 'SET SCHEMA', 299 | 'SET', 300 | 'UPDATE', 301 | 'VALUES', 302 | 'WHERE' 303 | ]; 304 | 305 | const reservedTopLevelWordsNoIndent = ['INTERSECT', 'INTERSECT ALL', 'MINUS', 'UNION', 'UNION ALL']; 306 | 307 | const reservedNewlineWords = [ 308 | 'AND', 309 | 'CROSS APPLY', 310 | 'CROSS JOIN', 311 | 'ELSE', 312 | 'INNER JOIN', 313 | 'JOIN', 314 | 'LEFT JOIN', 315 | 'LEFT OUTER JOIN', 316 | 'OR', 317 | 'OUTER APPLY', 318 | 'OUTER JOIN', 319 | 'RIGHT JOIN', 320 | 'RIGHT OUTER JOIN', 321 | 'WHEN', 322 | 'XOR' 323 | ]; 324 | 325 | let tokenizer; 326 | 327 | export default class StandardSqlFormatter { 328 | /** 329 | * @param {Object} cfg Different set of configurations 330 | */ 331 | constructor(cfg) { 332 | this.cfg = cfg; 333 | } 334 | 335 | /** 336 | * Format the whitespace in a Standard SQL string to make it easier to read 337 | * 338 | * @param {String} query The Standard SQL string 339 | * @return {String} formatted string 340 | */ 341 | format(query) { 342 | if (!tokenizer) { 343 | tokenizer = new Tokenizer({ 344 | reservedWords, 345 | reservedTopLevelWords, 346 | reservedNewlineWords, 347 | reservedTopLevelWordsNoIndent, 348 | stringTypes: [`""`, "N''", "''", '``', '[]'], 349 | openParens: ['(', 'CASE'], 350 | closeParens: [')', 'END'], 351 | indexedPlaceholderTypes: ['?'], 352 | namedPlaceholderTypes: ['@', ':'], 353 | lineCommentTypes: ['#', '--'] 354 | }); 355 | } 356 | return new Formatter(this.cfg, tokenizer).format(query); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/sqlFormatter.js: -------------------------------------------------------------------------------- 1 | import Db2Formatter from './languages/Db2Formatter'; 2 | import N1qlFormatter from './languages/N1qlFormatter'; 3 | import PlSqlFormatter from './languages/PlSqlFormatter'; 4 | import StandardSqlFormatter from './languages/StandardSqlFormatter'; 5 | 6 | /** 7 | * Format whitespace in a query to make it easier to read. 8 | * 9 | * @param {String} query 10 | * @param {Object} cfg 11 | * @param {String} cfg.language Query language, default is Standard SQL 12 | * @param {String} cfg.indent Characters used for indentation, default is " " (2 spaces) 13 | * @param {Bool} cfg.uppercase Converts keywords to uppercase 14 | * @param {Integer} cfg.linesBetweenQueries How many line breaks between queries 15 | * @param {Object} cfg.params Collection of params for placeholder replacement 16 | * @return {String} 17 | */ 18 | export const format = (query, cfg = {}) => { 19 | switch (cfg.language) { 20 | case 'db2': 21 | return new Db2Formatter(cfg).format(query); 22 | case 'n1ql': 23 | return new N1qlFormatter(cfg).format(query); 24 | case 'pl/sql': 25 | return new PlSqlFormatter(cfg).format(query); 26 | case 'sql': 27 | case undefined: 28 | return new StandardSqlFormatter(cfg).format(query); 29 | default: 30 | throw Error(`Unsupported SQL dialect: ${cfg.language}`); 31 | } 32 | }; 33 | 34 | export default { format }; 35 | -------------------------------------------------------------------------------- /test/Db2FormatterTest.js: -------------------------------------------------------------------------------- 1 | import sqlFormatter from './../src/sqlFormatter'; 2 | import behavesLikeSqlFormatter from './behavesLikeSqlFormatter'; 3 | import dedent from 'dedent-js'; 4 | 5 | describe('Db2Formatter', () => { 6 | behavesLikeSqlFormatter('db2'); 7 | 8 | const format = (query, cfg = {}) => sqlFormatter.format(query, { ...cfg, language: 'db2' }); 9 | 10 | it('formats FETCH FIRST like LIMIT', () => { 11 | expect(format('SELECT col1 FROM tbl ORDER BY col2 DESC FETCH FIRST 20 ROWS ONLY;')) 12 | .toBe(dedent/* sql */ ` 13 | SELECT 14 | col1 15 | FROM 16 | tbl 17 | ORDER BY 18 | col2 DESC 19 | FETCH FIRST 20 | 20 ROWS ONLY; 21 | `); 22 | }); 23 | 24 | it('formats only -- as a line comment', () => { 25 | const result = format(` 26 | SELECT col FROM 27 | -- This is a comment 28 | MyTable; 29 | `); 30 | expect(result).toBe(dedent/* sql */ ` 31 | SELECT 32 | col 33 | FROM 34 | -- This is a comment 35 | MyTable; 36 | `); 37 | }); 38 | 39 | it('recognizes @ and # as part of identifiers', () => { 40 | const result = format('SELECT col#1, @col2 FROM tbl'); 41 | expect(result).toBe(dedent/* sql */ ` 42 | SELECT 43 | col#1, 44 | @col2 45 | FROM 46 | tbl 47 | `); 48 | }); 49 | 50 | it('recognizes :variables', () => { 51 | expect(format('SELECT :variable;')).toBe(dedent/* sql */ ` 52 | SELECT 53 | :variable; 54 | `); 55 | }); 56 | 57 | it('replaces :variables with param values', () => { 58 | const result = format('SELECT :variable', { 59 | params: { variable: '"variable value"' } 60 | }); 61 | expect(result).toBe(dedent/* sql */ ` 62 | SELECT 63 | "variable value" 64 | `); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/N1qlFormatterTest.js: -------------------------------------------------------------------------------- 1 | import sqlFormatter from './../src/sqlFormatter'; 2 | import behavesLikeSqlFormatter from './behavesLikeSqlFormatter'; 3 | import dedent from 'dedent-js'; 4 | 5 | describe('N1qlFormatter', () => { 6 | behavesLikeSqlFormatter('n1ql'); 7 | 8 | const format = (query, cfg = {}) => sqlFormatter.format(query, { ...cfg, language: 'n1ql' }); 9 | 10 | it('formats SELECT query with element selection expression', () => { 11 | const result = format('SELECT order_lines[0].productId FROM orders;'); 12 | expect(result).toBe(dedent/* sql */ ` 13 | SELECT 14 | order_lines[0].productId 15 | FROM 16 | orders; 17 | `); 18 | }); 19 | 20 | it('formats SELECT query with primary key querying', () => { 21 | const result = format("SELECT fname, email FROM tutorial USE KEYS ['dave', 'ian'];"); 22 | expect(result).toBe(dedent/* sql */ ` 23 | SELECT 24 | fname, 25 | email 26 | FROM 27 | tutorial 28 | USE KEYS 29 | ['dave', 'ian']; 30 | `); 31 | }); 32 | 33 | it('formats INSERT with {} object literal', () => { 34 | const result = format( 35 | "INSERT INTO heroes (KEY, VALUE) VALUES ('123', {'id':1,'type':'Tarzan'});" 36 | ); 37 | expect(result).toBe(dedent/* sql */ ` 38 | INSERT INTO 39 | heroes (KEY, VALUE) 40 | VALUES 41 | ('123', {'id': 1, 'type': 'Tarzan'}); 42 | `); 43 | }); 44 | 45 | it('formats INSERT with large object and array literals', () => { 46 | const result = format(` 47 | INSERT INTO heroes (KEY, VALUE) VALUES ('123', {'id': 1, 'type': 'Tarzan', 48 | 'array': [123456789, 123456789, 123456789, 123456789, 123456789], 'hello': 'world'}); 49 | `); 50 | expect(result).toBe(dedent/* sql */ ` 51 | INSERT INTO 52 | heroes (KEY, VALUE) 53 | VALUES 54 | ( 55 | '123', 56 | { 57 | 'id': 1, 58 | 'type': 'Tarzan', 59 | 'array': [ 60 | 123456789, 61 | 123456789, 62 | 123456789, 63 | 123456789, 64 | 123456789 65 | ], 66 | 'hello': 'world' 67 | } 68 | ); 69 | `); 70 | }); 71 | 72 | it('formats SELECT query with UNNEST top level reserver word', () => { 73 | const result = format('SELECT * FROM tutorial UNNEST tutorial.children c;'); 74 | expect(result).toBe(dedent/* sql */ ` 75 | SELECT 76 | * 77 | FROM 78 | tutorial 79 | UNNEST 80 | tutorial.children c; 81 | `); 82 | }); 83 | 84 | it('formats SELECT query with NEST and USE KEYS', () => { 85 | const result = format(` 86 | SELECT * FROM usr 87 | USE KEYS 'Elinor_33313792' NEST orders_with_users orders 88 | ON KEYS ARRAY s.order_id FOR s IN usr.shipped_order_history END; 89 | `); 90 | expect(result).toBe(dedent/* sql */ ` 91 | SELECT 92 | * 93 | FROM 94 | usr 95 | USE KEYS 96 | 'Elinor_33313792' 97 | NEST 98 | orders_with_users orders ON KEYS ARRAY s.order_id FOR s IN usr.shipped_order_history END; 99 | `); 100 | }); 101 | 102 | it('formats explained DELETE query with USE KEYS and RETURNING', () => { 103 | const result = format("EXPLAIN DELETE FROM tutorial t USE KEYS 'baldwin' RETURNING t"); 104 | expect(result).toBe(dedent/* sql */ ` 105 | EXPLAIN DELETE FROM 106 | tutorial t 107 | USE KEYS 108 | 'baldwin' RETURNING t 109 | `); 110 | }); 111 | 112 | it('formats UPDATE query with USE KEYS and RETURNING', () => { 113 | const result = format( 114 | "UPDATE tutorial USE KEYS 'baldwin' SET type = 'actor' RETURNING tutorial.type" 115 | ); 116 | expect(result).toBe(dedent/* sql */ ` 117 | UPDATE 118 | tutorial 119 | USE KEYS 120 | 'baldwin' 121 | SET 122 | type = 'actor' RETURNING tutorial.type 123 | `); 124 | }); 125 | 126 | it('recognizes $variables', () => { 127 | const result = format('SELECT $variable, $\'var name\', $"var name", $`var name`;'); 128 | expect(result).toBe(dedent/* sql */ ` 129 | SELECT 130 | $variable, 131 | $'var name', 132 | $"var name", 133 | $\`var name\`; 134 | `); 135 | }); 136 | 137 | it('replaces $variables with param values', () => { 138 | const result = format('SELECT $variable, $\'var name\', $"var name", $`var name`;', { 139 | params: { 140 | variable: '"variable value"', 141 | 'var name': "'var value'" 142 | } 143 | }); 144 | expect(result).toBe(dedent/* sql */ ` 145 | SELECT 146 | "variable value", 147 | 'var value', 148 | 'var value', 149 | 'var value'; 150 | `); 151 | }); 152 | 153 | it('replaces $ numbered placeholders with param values', () => { 154 | const result = format('SELECT $1, $2, $0;', { 155 | params: { 156 | 0: 'first', 157 | 1: 'second', 158 | 2: 'third' 159 | } 160 | }); 161 | expect(result).toBe(dedent/* sql */ ` 162 | SELECT 163 | second, 164 | third, 165 | first; 166 | `); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /test/PlSqlFormatterTest.js: -------------------------------------------------------------------------------- 1 | import sqlFormatter from './../src/sqlFormatter'; 2 | import behavesLikeSqlFormatter from './behavesLikeSqlFormatter'; 3 | import dedent from 'dedent-js'; 4 | 5 | describe('PlSqlFormatter', () => { 6 | behavesLikeSqlFormatter('pl/sql'); 7 | 8 | const format = (query, cfg = {}) => sqlFormatter.format(query, { ...cfg, language: 'pl/sql' }); 9 | 10 | it('formats FETCH FIRST like LIMIT', () => { 11 | expect(format('SELECT col1 FROM tbl ORDER BY col2 DESC FETCH FIRST 20 ROWS ONLY;')) 12 | .toBe(dedent/* sql */ ` 13 | SELECT 14 | col1 15 | FROM 16 | tbl 17 | ORDER BY 18 | col2 DESC 19 | FETCH FIRST 20 | 20 ROWS ONLY; 21 | `); 22 | }); 23 | 24 | it('formats only -- as a line comment', () => { 25 | const result = format('SELECT col FROM\n-- This is a comment\nMyTable;\n'); 26 | expect(result).toBe(dedent/* sql */ ` 27 | SELECT 28 | col 29 | FROM 30 | -- This is a comment 31 | MyTable; 32 | `); 33 | }); 34 | 35 | it('recognizes _, $, #, . and @ as part of identifiers', () => { 36 | const result = format('SELECT my_col$1#, col.2@ FROM tbl\n'); 37 | expect(result).toBe(dedent/* sql */ ` 38 | SELECT 39 | my_col$1#, 40 | col.2@ 41 | FROM 42 | tbl 43 | `); 44 | }); 45 | 46 | it('formats short CREATE TABLE', () => { 47 | expect(format('CREATE TABLE items (a INT PRIMARY KEY, b TEXT);')).toBe( 48 | 'CREATE TABLE items (a INT PRIMARY KEY, b TEXT);' 49 | ); 50 | }); 51 | 52 | it('formats long CREATE TABLE', () => { 53 | expect( 54 | format('CREATE TABLE items (a INT PRIMARY KEY, b TEXT, c INT NOT NULL, d INT NOT NULL);') 55 | ).toBe(dedent/* sql */ ` 56 | CREATE TABLE items ( 57 | a INT PRIMARY KEY, 58 | b TEXT, 59 | c INT NOT NULL, 60 | d INT NOT NULL 61 | ); 62 | `); 63 | }); 64 | 65 | it('formats INSERT without INTO', () => { 66 | const result = format( 67 | "INSERT Customers (ID, MoneyBalance, Address, City) VALUES (12,-123.4, 'Skagen 2111','Stv');" 68 | ); 69 | expect(result).toBe(dedent/* sql */ ` 70 | INSERT 71 | Customers (ID, MoneyBalance, Address, City) 72 | VALUES 73 | (12, -123.4, 'Skagen 2111', 'Stv'); 74 | `); 75 | }); 76 | 77 | it('formats ALTER TABLE ... MODIFY query', () => { 78 | const result = format('ALTER TABLE supplier MODIFY supplier_name char(100) NOT NULL;'); 79 | expect(result).toBe(dedent/* sql */ ` 80 | ALTER TABLE 81 | supplier 82 | MODIFY 83 | supplier_name char(100) NOT NULL; 84 | `); 85 | }); 86 | 87 | it('formats ALTER TABLE ... ALTER COLUMN query', () => { 88 | const result = format('ALTER TABLE supplier ALTER COLUMN supplier_name VARCHAR(100) NOT NULL;'); 89 | expect(result).toBe(dedent/* sql */ ` 90 | ALTER TABLE 91 | supplier 92 | ALTER COLUMN 93 | supplier_name VARCHAR(100) NOT NULL; 94 | `); 95 | }); 96 | 97 | it('recognizes ?[0-9]* placeholders', () => { 98 | const result = format('SELECT ?1, ?25, ?;'); 99 | expect(result).toBe(dedent/* sql */ ` 100 | SELECT 101 | ?1, 102 | ?25, 103 | ?; 104 | `); 105 | }); 106 | 107 | it('replaces ? numbered placeholders with param values', () => { 108 | const result = format('SELECT ?1, ?2, ?0;', { 109 | params: { 110 | 0: 'first', 111 | 1: 'second', 112 | 2: 'third' 113 | } 114 | }); 115 | expect(result).toBe('SELECT\n second,\n third,\n first;'); 116 | }); 117 | 118 | it('replaces ? indexed placeholders with param values', () => { 119 | const result = format('SELECT ?, ?, ?;', { 120 | params: ['first', 'second', 'third'] 121 | }); 122 | expect(result).toBe('SELECT\n first,\n second,\n third;'); 123 | }); 124 | 125 | it('formats SELECT query with CROSS JOIN', () => { 126 | const result = format('SELECT a, b FROM t CROSS JOIN t2 on t.id = t2.id_t'); 127 | expect(result).toBe(dedent/* sql */ ` 128 | SELECT 129 | a, 130 | b 131 | FROM 132 | t 133 | CROSS JOIN t2 on t.id = t2.id_t 134 | `); 135 | }); 136 | 137 | it('formats SELECT query with CROSS APPLY', () => { 138 | const result = format('SELECT a, b FROM t CROSS APPLY fn(t.id)'); 139 | expect(result).toBe(dedent/* sql */ ` 140 | SELECT 141 | a, 142 | b 143 | FROM 144 | t 145 | CROSS APPLY fn(t.id) 146 | `); 147 | }); 148 | 149 | it('formats simple SELECT', () => { 150 | const result = format('SELECT N, M FROM t'); 151 | expect(result).toBe(dedent/* sql */ ` 152 | SELECT 153 | N, 154 | M 155 | FROM 156 | t 157 | `); 158 | }); 159 | 160 | it('formats simple SELECT with national characters', () => { 161 | const result = format("SELECT N'value'"); 162 | expect(result).toBe(dedent/* sql */ ` 163 | SELECT 164 | N'value' 165 | `); 166 | }); 167 | 168 | it('formats SELECT query with OUTER APPLY', () => { 169 | const result = format('SELECT a, b FROM t OUTER APPLY fn(t.id)'); 170 | expect(result).toBe(dedent/* sql */ ` 171 | SELECT 172 | a, 173 | b 174 | FROM 175 | t 176 | OUTER APPLY fn(t.id) 177 | `); 178 | }); 179 | 180 | it('formats CASE ... WHEN with a blank expression', () => { 181 | const result = format( 182 | "CASE WHEN option = 'foo' THEN 1 WHEN option = 'bar' THEN 2 WHEN option = 'baz' THEN 3 ELSE 4 END;" 183 | ); 184 | 185 | expect(result).toBe(dedent/* sql */ ` 186 | CASE 187 | WHEN option = 'foo' THEN 1 188 | WHEN option = 'bar' THEN 2 189 | WHEN option = 'baz' THEN 3 190 | ELSE 4 191 | END; 192 | `); 193 | }); 194 | 195 | it('formats CASE ... WHEN inside SELECT', () => { 196 | const result = format( 197 | "SELECT foo, bar, CASE baz WHEN 'one' THEN 1 WHEN 'two' THEN 2 ELSE 3 END FROM table" 198 | ); 199 | 200 | expect(result).toBe(dedent/* sql */ ` 201 | SELECT 202 | foo, 203 | bar, 204 | CASE 205 | baz 206 | WHEN 'one' THEN 1 207 | WHEN 'two' THEN 2 208 | ELSE 3 209 | END 210 | FROM 211 | table 212 | `); 213 | }); 214 | 215 | it('formats CASE ... WHEN with an expression', () => { 216 | const result = format( 217 | "CASE toString(getNumber()) WHEN 'one' THEN 1 WHEN 'two' THEN 2 WHEN 'three' THEN 3 ELSE 4 END;" 218 | ); 219 | 220 | expect(result).toBe(dedent/* sql */ ` 221 | CASE 222 | toString(getNumber()) 223 | WHEN 'one' THEN 1 224 | WHEN 'two' THEN 2 225 | WHEN 'three' THEN 3 226 | ELSE 4 227 | END; 228 | `); 229 | }); 230 | 231 | it('properly converts to uppercase in case statements', () => { 232 | const result = format( 233 | "case toString(getNumber()) when 'one' then 1 when 'two' then 2 when 'three' then 3 else 4 end;", 234 | { uppercase: true } 235 | ); 236 | expect(result).toBe(dedent/* sql */ ` 237 | CASE 238 | toString(getNumber()) 239 | WHEN 'one' THEN 1 240 | WHEN 'two' THEN 2 241 | WHEN 'three' THEN 3 242 | ELSE 4 243 | END; 244 | `); 245 | }); 246 | 247 | it('formats Oracle recursive sub queries', () => { 248 | const result = format(/* sql */ ` 249 | WITH t1(id, parent_id) AS ( 250 | -- Anchor member. 251 | SELECT 252 | id, 253 | parent_id 254 | FROM 255 | tab1 256 | WHERE 257 | parent_id IS NULL 258 | MINUS 259 | -- Recursive member. 260 | SELECT 261 | t2.id, 262 | t2.parent_id 263 | FROM 264 | tab1 t2, 265 | t1 266 | WHERE 267 | t2.parent_id = t1.id 268 | ) SEARCH BREADTH FIRST BY id SET order1, 269 | another AS (SELECT * FROM dual) 270 | SELECT id, parent_id FROM t1 ORDER BY order1; 271 | `); 272 | expect(result).toBe(dedent/* sql */ ` 273 | WITH t1(id, parent_id) AS ( 274 | -- Anchor member. 275 | SELECT 276 | id, 277 | parent_id 278 | FROM 279 | tab1 280 | WHERE 281 | parent_id IS NULL 282 | MINUS 283 | -- Recursive member. 284 | SELECT 285 | t2.id, 286 | t2.parent_id 287 | FROM 288 | tab1 t2, 289 | t1 290 | WHERE 291 | t2.parent_id = t1.id 292 | ) SEARCH BREADTH FIRST BY id SET order1, 293 | another AS ( 294 | SELECT 295 | * 296 | FROM 297 | dual 298 | ) 299 | SELECT 300 | id, 301 | parent_id 302 | FROM 303 | t1 304 | ORDER BY 305 | order1; 306 | `); 307 | }); 308 | }); 309 | -------------------------------------------------------------------------------- /test/StandardSqlFormatterTest.js: -------------------------------------------------------------------------------- 1 | import sqlFormatter from './../src/sqlFormatter'; 2 | import behavesLikeSqlFormatter from './behavesLikeSqlFormatter'; 3 | import dedent from 'dedent-js'; 4 | 5 | describe('StandardSqlFormatter', () => { 6 | behavesLikeSqlFormatter(); 7 | 8 | const format = (query, cfg = {}) => sqlFormatter.format(query, { ...cfg, language: 'sql' }); 9 | 10 | it('formats short CREATE TABLE', () => { 11 | expect(format('CREATE TABLE items (a INT PRIMARY KEY, b TEXT);')).toBe( 12 | 'CREATE TABLE items (a INT PRIMARY KEY, b TEXT);' 13 | ); 14 | }); 15 | 16 | it('formats long CREATE TABLE', () => { 17 | expect( 18 | format('CREATE TABLE items (a INT PRIMARY KEY, b TEXT, c INT NOT NULL, d INT NOT NULL);') 19 | ).toBe(dedent/* sql */ ` 20 | CREATE TABLE items ( 21 | a INT PRIMARY KEY, 22 | b TEXT, 23 | c INT NOT NULL, 24 | d INT NOT NULL 25 | ); 26 | `); 27 | }); 28 | 29 | it('formats INSERT without INTO', () => { 30 | const result = format( 31 | "INSERT Customers (ID, MoneyBalance, Address, City) VALUES (12,-123.4, 'Skagen 2111','Stv');" 32 | ); 33 | expect(result).toBe(dedent/* sql */ ` 34 | INSERT 35 | Customers (ID, MoneyBalance, Address, City) 36 | VALUES 37 | (12, -123.4, 'Skagen 2111', 'Stv'); 38 | `); 39 | }); 40 | 41 | it('formats ALTER TABLE ... MODIFY query', () => { 42 | const result = format('ALTER TABLE supplier MODIFY supplier_name char(100) NOT NULL;'); 43 | expect(result).toBe(dedent/* sql */ ` 44 | ALTER TABLE 45 | supplier 46 | MODIFY 47 | supplier_name char(100) NOT NULL; 48 | `); 49 | }); 50 | 51 | it('formats ALTER TABLE ... ALTER COLUMN query', () => { 52 | const result = format('ALTER TABLE supplier ALTER COLUMN supplier_name VARCHAR(100) NOT NULL;'); 53 | expect(result).toBe(dedent/* sql */ ` 54 | ALTER TABLE 55 | supplier 56 | ALTER COLUMN 57 | supplier_name VARCHAR(100) NOT NULL; 58 | `); 59 | }); 60 | 61 | it('recognizes [] strings', () => { 62 | expect(format('[foo JOIN bar]')).toBe('[foo JOIN bar]'); 63 | expect(format('[foo ]] JOIN bar]')).toBe('[foo ]] JOIN bar]'); 64 | }); 65 | 66 | it('recognizes @variables', () => { 67 | const result = format( 68 | 'SELECT @variable, @a1_2.3$, @\'var name\', @"var name", @`var name`, @[var name];' 69 | ); 70 | expect(result).toBe(dedent/* sql */ ` 71 | SELECT 72 | @variable, 73 | @a1_2.3$, 74 | @'var name', 75 | @"var name", 76 | @\`var name\`, 77 | @[var name]; 78 | `); 79 | }); 80 | 81 | it('replaces @variables with param values', () => { 82 | const result = format( 83 | "SELECT @variable, @a1_2.3$, @'var name', @\"var name\", @`var name`, @[var name], @'var\\name';", 84 | { 85 | params: { 86 | variable: '"variable value"', 87 | 'a1_2.3$': "'weird value'", 88 | 'var name': "'var value'", 89 | 'var\\name': `'var\\ value'` 90 | } 91 | } 92 | ); 93 | expect(result).toBe(dedent/* sql */ ` 94 | SELECT 95 | "variable value", 96 | 'weird value', 97 | 'var value', 98 | 'var value', 99 | 'var value', 100 | 'var value', 101 | 'var\\ value'; 102 | `); 103 | }); 104 | 105 | it('recognizes :variables', () => { 106 | const result = format( 107 | 'SELECT :variable, :a1_2.3$, :\'var name\', :"var name", :`var name`, :[var name];' 108 | ); 109 | expect(result).toBe(dedent/* sql */ ` 110 | SELECT 111 | :variable, 112 | :a1_2.3$, 113 | :'var name', 114 | :"var name", 115 | :\`var name\`, 116 | :[var name]; 117 | `); 118 | }); 119 | 120 | it('replaces :variables with param values', () => { 121 | const result = format( 122 | 'SELECT :variable, :a1_2.3$, :\'var name\', :"var name", :`var name`,' + 123 | " :[var name], :'escaped \\'var\\'', :\"^*& weird \\\" var \";", 124 | { 125 | params: { 126 | variable: '"variable value"', 127 | 'a1_2.3$': "'weird value'", 128 | 'var name': "'var value'", 129 | "escaped 'var'": "'weirder value'", 130 | '^*& weird " var ': "'super weird value'" 131 | } 132 | } 133 | ); 134 | expect(result).toBe(dedent/* sql */ ` 135 | SELECT 136 | "variable value", 137 | 'weird value', 138 | 'var value', 139 | 'var value', 140 | 'var value', 141 | 'var value', 142 | 'weirder value', 143 | 'super weird value'; 144 | `); 145 | }); 146 | 147 | it('recognizes ?[0-9]* placeholders', () => { 148 | const result = format('SELECT ?1, ?25, ?;'); 149 | expect(result).toBe(dedent/* sql */ ` 150 | SELECT 151 | ?1, 152 | ?25, 153 | ?; 154 | `); 155 | }); 156 | 157 | it('replaces ? numbered placeholders with param values', () => { 158 | const result = format('SELECT ?1, ?2, ?0;', { 159 | params: { 160 | 0: 'first', 161 | 1: 'second', 162 | 2: 'third' 163 | } 164 | }); 165 | expect(result).toBe(dedent/* sql */ ` 166 | SELECT 167 | second, 168 | third, 169 | first; 170 | `); 171 | }); 172 | 173 | it('replaces ? indexed placeholders with param values', () => { 174 | const result = format('SELECT ?, ?, ?;', { 175 | params: ['first', 'second', 'third'] 176 | }); 177 | expect(result).toBe(dedent/* sql */ ` 178 | SELECT 179 | first, 180 | second, 181 | third; 182 | `); 183 | }); 184 | 185 | it('formats query with GO batch separator', () => { 186 | const result = format('SELECT 1 GO SELECT 2', { 187 | params: ['first', 'second', 'third'] 188 | }); 189 | expect(result).toBe(dedent/* sql */ ` 190 | SELECT 191 | 1 192 | GO 193 | SELECT 194 | 2 195 | `); 196 | }); 197 | 198 | it('formats SELECT query with CROSS JOIN', () => { 199 | const result = format('SELECT a, b FROM t CROSS JOIN t2 on t.id = t2.id_t'); 200 | expect(result).toBe(dedent/* sql */ ` 201 | SELECT 202 | a, 203 | b 204 | FROM 205 | t 206 | CROSS JOIN t2 on t.id = t2.id_t 207 | `); 208 | }); 209 | 210 | it('formats SELECT query with CROSS APPLY', () => { 211 | const result = format('SELECT a, b FROM t CROSS APPLY fn(t.id)'); 212 | expect(result).toBe(dedent/* sql */ ` 213 | SELECT 214 | a, 215 | b 216 | FROM 217 | t 218 | CROSS APPLY fn(t.id) 219 | `); 220 | }); 221 | 222 | it('formats simple SELECT', () => { 223 | const result = format('SELECT N, M FROM t'); 224 | expect(result).toBe(dedent/* sql */ ` 225 | SELECT 226 | N, 227 | M 228 | FROM 229 | t 230 | `); 231 | }); 232 | 233 | it('formats simple SELECT with national characters (MSSQL)', () => { 234 | const result = format("SELECT N'value'"); 235 | expect(result).toBe(dedent/* sql */ ` 236 | SELECT 237 | N'value' 238 | `); 239 | }); 240 | 241 | it('formats SELECT query with OUTER APPLY', () => { 242 | const result = format('SELECT a, b FROM t OUTER APPLY fn(t.id)'); 243 | expect(result).toBe(dedent/* sql */ ` 244 | SELECT 245 | a, 246 | b 247 | FROM 248 | t 249 | OUTER APPLY fn(t.id) 250 | `); 251 | }); 252 | 253 | it('formats FETCH FIRST like LIMIT', () => { 254 | const result = format('SELECT * FETCH FIRST 2 ROWS ONLY;'); 255 | expect(result).toBe(dedent/* sql */ ` 256 | SELECT 257 | * 258 | FETCH FIRST 259 | 2 ROWS ONLY; 260 | `); 261 | }); 262 | 263 | it('formats CASE ... WHEN with a blank expression', () => { 264 | const result = format( 265 | "CASE WHEN option = 'foo' THEN 1 WHEN option = 'bar' THEN 2 WHEN option = 'baz' THEN 3 ELSE 4 END;" 266 | ); 267 | 268 | expect(result).toBe(dedent/* sql */ ` 269 | CASE 270 | WHEN option = 'foo' THEN 1 271 | WHEN option = 'bar' THEN 2 272 | WHEN option = 'baz' THEN 3 273 | ELSE 4 274 | END; 275 | `); 276 | }); 277 | 278 | it('formats CASE ... WHEN inside SELECT', () => { 279 | const result = format( 280 | "SELECT foo, bar, CASE baz WHEN 'one' THEN 1 WHEN 'two' THEN 2 ELSE 3 END FROM table" 281 | ); 282 | 283 | expect(result).toBe(dedent/* sql */ ` 284 | SELECT 285 | foo, 286 | bar, 287 | CASE 288 | baz 289 | WHEN 'one' THEN 1 290 | WHEN 'two' THEN 2 291 | ELSE 3 292 | END 293 | FROM 294 | table 295 | `); 296 | }); 297 | 298 | it('formats CASE ... WHEN with an expression', () => { 299 | const result = format( 300 | "CASE toString(getNumber()) WHEN 'one' THEN 1 WHEN 'two' THEN 2 WHEN 'three' THEN 3 ELSE 4 END;" 301 | ); 302 | 303 | expect(result).toBe(dedent/* sql */ ` 304 | CASE 305 | toString(getNumber()) 306 | WHEN 'one' THEN 1 307 | WHEN 'two' THEN 2 308 | WHEN 'three' THEN 3 309 | ELSE 4 310 | END; 311 | `); 312 | }); 313 | 314 | it('recognizes lowercase CASE ... END', () => { 315 | const result = format("case when option = 'foo' then 1 else 2 end;"); 316 | 317 | expect(result).toBe(dedent/* sql */ ` 318 | case 319 | when option = 'foo' then 1 320 | else 2 321 | end; 322 | `); 323 | }); 324 | 325 | // Regression test for issue #43 326 | it('ignores words CASE and END inside other strings', () => { 327 | const result = format('SELECT CASEDATE, ENDDATE FROM table1;'); 328 | 329 | expect(result).toBe(dedent/* sql */ ` 330 | SELECT 331 | CASEDATE, 332 | ENDDATE 333 | FROM 334 | table1; 335 | `); 336 | }); 337 | 338 | it('formats tricky line comments', () => { 339 | expect(format('SELECT a#comment, here\nFROM b--comment')).toBe(dedent/* sql */ ` 340 | SELECT 341 | a #comment, here 342 | FROM 343 | b --comment 344 | `); 345 | }); 346 | 347 | it('formats line comments followed by semicolon', () => { 348 | expect( 349 | format(` 350 | SELECT a FROM b 351 | --comment 352 | ; 353 | `) 354 | ).toBe(dedent/* sql */ ` 355 | SELECT 356 | a 357 | FROM 358 | b --comment 359 | ; 360 | `); 361 | }); 362 | 363 | it('formats line comments followed by comma', () => { 364 | expect( 365 | format(dedent/* sql */ ` 366 | SELECT a --comment 367 | , b 368 | `) 369 | ).toBe(dedent/* sql */ ` 370 | SELECT 371 | a --comment 372 | , 373 | b 374 | `); 375 | }); 376 | 377 | it('formats line comments followed by close-paren', () => { 378 | expect(format('SELECT ( a --comment\n )')).toBe(dedent/* sql */ ` 379 | SELECT 380 | (a --comment 381 | ) 382 | `); 383 | }); 384 | 385 | it('formats line comments followed by open-paren', () => { 386 | expect(format('SELECT a --comment\n()')).toBe(dedent/* sql */ ` 387 | SELECT 388 | a --comment 389 | () 390 | `); 391 | }); 392 | 393 | it('formats lonely semicolon', () => { 394 | expect(format(';')).toBe(';'); 395 | }); 396 | }); 397 | -------------------------------------------------------------------------------- /test/behavesLikeSqlFormatter.js: -------------------------------------------------------------------------------- 1 | import sqlFormatter from './../src/sqlFormatter'; 2 | import dedent from 'dedent-js'; 3 | 4 | /** 5 | * Core tests for all SQL formatters 6 | * @param {String} language 7 | */ 8 | export default function behavesLikeSqlFormatter(language) { 9 | const format = (query, cfg = {}) => sqlFormatter.format(query, { ...cfg, language }); 10 | 11 | it('uses given indent config for indention', () => { 12 | const result = format('SELECT count(*),Column1 FROM Table1;', { 13 | indent: ' ' 14 | }); 15 | 16 | expect(result).toBe(dedent/* sql */ ` 17 | SELECT 18 | count(*), 19 | Column1 20 | FROM 21 | Table1; 22 | `); 23 | }); 24 | 25 | it('formats simple SET SCHEMA queries', () => { 26 | const result = format('SET SCHEMA schema1; SET CURRENT SCHEMA schema2;'); 27 | expect(result).toBe(dedent/* sql */ ` 28 | SET SCHEMA 29 | schema1; 30 | SET CURRENT SCHEMA 31 | schema2; 32 | `); 33 | }); 34 | 35 | it('formats simple SELECT query', () => { 36 | const result = format('SELECT count(*),Column1 FROM Table1;'); 37 | expect(result).toBe(dedent/* sql */ ` 38 | SELECT 39 | count(*), 40 | Column1 41 | FROM 42 | Table1; 43 | `); 44 | }); 45 | 46 | it('formats complex SELECT', () => { 47 | const result = format( 48 | "SELECT DISTINCT name, ROUND(age/7) field1, 18 + 20 AS field2, 'some string' FROM foo;" 49 | ); 50 | expect(result).toBe(dedent/* sql */ ` 51 | SELECT 52 | DISTINCT name, 53 | ROUND(age / 7) field1, 54 | 18 + 20 AS field2, 55 | 'some string' 56 | FROM 57 | foo; 58 | `); 59 | }); 60 | 61 | it('formats SELECT with complex WHERE', () => { 62 | const result = sqlFormatter.format(` 63 | SELECT * FROM foo WHERE Column1 = 'testing' 64 | AND ( (Column2 = Column3 OR Column4 >= NOW()) ); 65 | `); 66 | expect(result).toBe(dedent/* sql */ ` 67 | SELECT 68 | * 69 | FROM 70 | foo 71 | WHERE 72 | Column1 = 'testing' 73 | AND ( 74 | ( 75 | Column2 = Column3 76 | OR Column4 >= NOW() 77 | ) 78 | ); 79 | `); 80 | }); 81 | 82 | it('formats SELECT with top level reserved words', () => { 83 | const result = format(` 84 | SELECT * FROM foo WHERE name = 'John' GROUP BY some_column 85 | HAVING column > 10 ORDER BY other_column LIMIT 5; 86 | `); 87 | expect(result).toBe(dedent/* sql */ ` 88 | SELECT 89 | * 90 | FROM 91 | foo 92 | WHERE 93 | name = 'John' 94 | GROUP BY 95 | some_column 96 | HAVING 97 | column > 10 98 | ORDER BY 99 | other_column 100 | LIMIT 101 | 5; 102 | `); 103 | }); 104 | 105 | it('formats LIMIT with two comma-separated values on single line', () => { 106 | const result = format('LIMIT 5, 10;'); 107 | expect(result).toBe(dedent/* sql */ ` 108 | LIMIT 109 | 5, 10; 110 | `); 111 | }); 112 | 113 | it('formats LIMIT of single value followed by another SELECT using commas', () => { 114 | const result = format('LIMIT 5; SELECT foo, bar;'); 115 | expect(result).toBe(dedent/* sql */ ` 116 | LIMIT 117 | 5; 118 | SELECT 119 | foo, 120 | bar; 121 | `); 122 | }); 123 | 124 | it('formats LIMIT of single value and OFFSET', () => { 125 | const result = format('LIMIT 5 OFFSET 8;'); 126 | expect(result).toBe(dedent/* sql */ ` 127 | LIMIT 128 | 5 OFFSET 8; 129 | `); 130 | }); 131 | 132 | it('recognizes LIMIT in lowercase', () => { 133 | const result = format('limit 5, 10;'); 134 | expect(result).toBe(dedent/* sql */ ` 135 | limit 136 | 5, 10; 137 | `); 138 | }); 139 | 140 | it('preserves case of keywords', () => { 141 | const result = format('select distinct * frOM foo left join bar WHERe a > 1 and b = 3'); 142 | expect(result).toBe(dedent/* sql */ ` 143 | select 144 | distinct * 145 | frOM 146 | foo 147 | left join bar 148 | WHERe 149 | a > 1 150 | and b = 3 151 | `); 152 | }); 153 | 154 | it('formats SELECT query with SELECT query inside it', () => { 155 | const result = format( 156 | 'SELECT *, SUM(*) AS sum FROM (SELECT * FROM Posts LIMIT 30) WHERE a > b' 157 | ); 158 | expect(result).toBe(dedent/* sql */ ` 159 | SELECT 160 | *, 161 | SUM(*) AS sum 162 | FROM 163 | ( 164 | SELECT 165 | * 166 | FROM 167 | Posts 168 | LIMIT 169 | 30 170 | ) 171 | WHERE 172 | a > b 173 | `); 174 | }); 175 | 176 | it('formats SELECT query with INNER JOIN', () => { 177 | const result = format(` 178 | SELECT customer_id.from, COUNT(order_id) AS total FROM customers 179 | INNER JOIN orders ON customers.customer_id = orders.customer_id; 180 | `); 181 | expect(result).toBe(dedent/* sql */ ` 182 | SELECT 183 | customer_id.from, 184 | COUNT(order_id) AS total 185 | FROM 186 | customers 187 | INNER JOIN orders ON customers.customer_id = orders.customer_id; 188 | `); 189 | }); 190 | 191 | it('formats SELECT query with different comments', () => { 192 | const result = format(dedent/* sql */ ` 193 | SELECT 194 | /* 195 | * This is a block comment 196 | */ 197 | * FROM 198 | -- This is another comment 199 | MyTable # One final comment 200 | WHERE 1 = 2; 201 | `); 202 | expect(result).toBe(dedent/* sql */ ` 203 | SELECT 204 | /* 205 | * This is a block comment 206 | */ 207 | * 208 | FROM 209 | -- This is another comment 210 | MyTable # One final comment 211 | WHERE 212 | 1 = 2; 213 | `); 214 | }); 215 | 216 | it('maintains block comment indentation', () => { 217 | const sql = dedent/* sql */ ` 218 | SELECT 219 | /* 220 | * This is a block comment 221 | */ 222 | * 223 | FROM 224 | MyTable 225 | WHERE 226 | 1 = 2; 227 | `; 228 | expect(format(sql)).toBe(sql); 229 | }); 230 | 231 | it('formats simple INSERT query', () => { 232 | const result = format( 233 | "INSERT INTO Customers (ID, MoneyBalance, Address, City) VALUES (12,-123.4, 'Skagen 2111','Stv');" 234 | ); 235 | expect(result).toBe(dedent/* sql */ ` 236 | INSERT INTO 237 | Customers (ID, MoneyBalance, Address, City) 238 | VALUES 239 | (12, -123.4, 'Skagen 2111', 'Stv'); 240 | `); 241 | }); 242 | 243 | it('keeps short parenthesized list with nested parenthesis on single line', () => { 244 | const result = format('SELECT (a + b * (c - NOW()));'); 245 | expect(result).toBe(dedent/* sql */ ` 246 | SELECT 247 | (a + b * (c - NOW())); 248 | `); 249 | }); 250 | 251 | it('breaks long parenthesized lists to multiple lines', () => { 252 | const result = format(` 253 | INSERT INTO some_table (id_product, id_shop, id_currency, id_country, id_registration) ( 254 | SELECT IF(dq.id_discounter_shopping = 2, dq.value, dq.value / 100), 255 | IF (dq.id_discounter_shopping = 2, 'amount', 'percentage') FROM foo); 256 | `); 257 | expect(result).toBe(dedent/* sql */ ` 258 | INSERT INTO 259 | some_table ( 260 | id_product, 261 | id_shop, 262 | id_currency, 263 | id_country, 264 | id_registration 265 | ) ( 266 | SELECT 267 | IF( 268 | dq.id_discounter_shopping = 2, 269 | dq.value, 270 | dq.value / 100 271 | ), 272 | IF ( 273 | dq.id_discounter_shopping = 2, 274 | 'amount', 275 | 'percentage' 276 | ) 277 | FROM 278 | foo 279 | ); 280 | `); 281 | }); 282 | 283 | it('formats simple UPDATE query', () => { 284 | const result = format( 285 | "UPDATE Customers SET ContactName='Alfred Schmidt', City='Hamburg' WHERE CustomerName='Alfreds Futterkiste';" 286 | ); 287 | expect(result).toBe(dedent/* sql */ ` 288 | UPDATE 289 | Customers 290 | SET 291 | ContactName = 'Alfred Schmidt', 292 | City = 'Hamburg' 293 | WHERE 294 | CustomerName = 'Alfreds Futterkiste'; 295 | `); 296 | }); 297 | 298 | it('formats simple DELETE query', () => { 299 | const result = format("DELETE FROM Customers WHERE CustomerName='Alfred' AND Phone=5002132;"); 300 | expect(result).toBe(dedent/* sql */ ` 301 | DELETE FROM 302 | Customers 303 | WHERE 304 | CustomerName = 'Alfred' 305 | AND Phone = 5002132; 306 | `); 307 | }); 308 | 309 | it('formats simple DROP query', () => { 310 | const result = format('DROP TABLE IF EXISTS admin_role;'); 311 | expect(result).toBe('DROP TABLE IF EXISTS admin_role;'); 312 | }); 313 | 314 | it('formats incomplete query', () => { 315 | const result = format('SELECT count('); 316 | expect(result).toBe(dedent/* sql */ ` 317 | SELECT 318 | count( 319 | `); 320 | }); 321 | 322 | it('formats query that ends with open comment', () => { 323 | const result = format(` 324 | SELECT count(*) 325 | /*Comment 326 | `); 327 | expect(result).toBe(dedent` 328 | SELECT 329 | count(*) 330 | /*Comment 331 | `); 332 | }); 333 | 334 | it('formats UPDATE query with AS part', () => { 335 | const result = format( 336 | 'UPDATE customers SET total_orders = order_summary.total FROM ( SELECT * FROM bank) AS order_summary' 337 | ); 338 | expect(result).toBe(dedent/* sql */ ` 339 | UPDATE 340 | customers 341 | SET 342 | total_orders = order_summary.total 343 | FROM 344 | ( 345 | SELECT 346 | * 347 | FROM 348 | bank 349 | ) AS order_summary 350 | `); 351 | }); 352 | 353 | it('formats top-level and newline multi-word reserved words with inconsistent spacing', () => { 354 | const result = format('SELECT * FROM foo LEFT \t OUTER \n JOIN bar ORDER \n BY blah'); 355 | expect(result).toBe(dedent/* sql */ ` 356 | SELECT 357 | * 358 | FROM 359 | foo 360 | LEFT OUTER JOIN bar 361 | ORDER BY 362 | blah 363 | `); 364 | }); 365 | 366 | it('formats long double parenthesized queries to multiple lines', () => { 367 | const result = format("((foo = '0123456789-0123456789-0123456789-0123456789'))"); 368 | expect(result).toBe(dedent/* sql */ ` 369 | ( 370 | ( 371 | foo = '0123456789-0123456789-0123456789-0123456789' 372 | ) 373 | ) 374 | `); 375 | }); 376 | 377 | it('formats short double parenthesized queries to one line', () => { 378 | const result = format("((foo = 'bar'))"); 379 | expect(result).toBe("((foo = 'bar'))"); 380 | }); 381 | 382 | it('formats single-char operators', () => { 383 | expect(format('foo = bar')).toBe('foo = bar'); 384 | expect(format('foo < bar')).toBe('foo < bar'); 385 | expect(format('foo > bar')).toBe('foo > bar'); 386 | expect(format('foo + bar')).toBe('foo + bar'); 387 | expect(format('foo - bar')).toBe('foo - bar'); 388 | expect(format('foo * bar')).toBe('foo * bar'); 389 | expect(format('foo / bar')).toBe('foo / bar'); 390 | expect(format('foo % bar')).toBe('foo % bar'); 391 | }); 392 | 393 | it('formats multi-char operators', () => { 394 | expect(format('foo != bar')).toBe('foo != bar'); 395 | expect(format('foo <> bar')).toBe('foo <> bar'); 396 | expect(format('foo == bar')).toBe('foo == bar'); // N1QL 397 | expect(format('foo || bar')).toBe('foo || bar'); // Oracle, Postgre, N1QL string concat 398 | 399 | expect(format('foo <= bar')).toBe('foo <= bar'); 400 | expect(format('foo >= bar')).toBe('foo >= bar'); 401 | 402 | expect(format('foo !< bar')).toBe('foo !< bar'); 403 | expect(format('foo !> bar')).toBe('foo !> bar'); 404 | }); 405 | 406 | it('formats logical operators', () => { 407 | expect(format('foo ALL bar')).toBe('foo ALL bar'); 408 | expect(format('foo = ANY (1, 2, 3)')).toBe('foo = ANY (1, 2, 3)'); 409 | expect(format('EXISTS bar')).toBe('EXISTS bar'); 410 | expect(format('foo IN (1, 2, 3)')).toBe('foo IN (1, 2, 3)'); 411 | expect(format("foo LIKE 'hello%'")).toBe("foo LIKE 'hello%'"); 412 | expect(format('foo IS NULL')).toBe('foo IS NULL'); 413 | expect(format('UNIQUE foo')).toBe('UNIQUE foo'); 414 | }); 415 | 416 | it('formats AND/OR operators', () => { 417 | expect(format('foo BETWEEN bar AND baz')).toBe('foo BETWEEN bar\nAND baz'); 418 | expect(format('foo AND bar')).toBe('foo\nAND bar'); 419 | expect(format('foo OR bar')).toBe('foo\nOR bar'); 420 | }); 421 | 422 | it('recognizes strings', () => { 423 | expect(format('"foo JOIN bar"')).toBe('"foo JOIN bar"'); 424 | expect(format("'foo JOIN bar'")).toBe("'foo JOIN bar'"); 425 | expect(format('`foo JOIN bar`')).toBe('`foo JOIN bar`'); 426 | }); 427 | 428 | it('recognizes escaped strings', () => { 429 | expect(format('"foo \\" JOIN bar"')).toBe('"foo \\" JOIN bar"'); 430 | expect(format("'foo \\' JOIN bar'")).toBe("'foo \\' JOIN bar'"); 431 | expect(format('`foo `` JOIN bar`')).toBe('`foo `` JOIN bar`'); 432 | }); 433 | 434 | it('formats postgre specific operators', () => { 435 | expect(format('column::int')).toBe('column :: int'); 436 | expect(format('v->2')).toBe('v -> 2'); 437 | expect(format('v->>2')).toBe('v ->> 2'); 438 | expect(format("foo ~~ 'hello'")).toBe("foo ~~ 'hello'"); 439 | expect(format("foo !~ 'hello'")).toBe("foo !~ 'hello'"); 440 | expect(format("foo ~* 'hello'")).toBe("foo ~* 'hello'"); 441 | expect(format("foo ~~* 'hello'")).toBe("foo ~~* 'hello'"); 442 | expect(format("foo !~~ 'hello'")).toBe("foo !~~ 'hello'"); 443 | expect(format("foo !~* 'hello'")).toBe("foo !~* 'hello'"); 444 | expect(format("foo !~~* 'hello'")).toBe("foo !~~* 'hello'"); 445 | }); 446 | 447 | it('keeps separation between multiple statements', () => { 448 | expect(format('foo;bar;')).toBe('foo;\nbar;'); 449 | expect(format('foo\n;bar;')).toBe('foo;\nbar;'); 450 | expect(format('foo\n\n\n;bar;\n\n')).toBe('foo;\nbar;'); 451 | 452 | const result = format(` 453 | SELECT count(*),Column1 FROM Table1; 454 | SELECT count(*),Column1 FROM Table2; 455 | `); 456 | expect(result).toBe(dedent/* sql */ ` 457 | SELECT 458 | count(*), 459 | Column1 460 | FROM 461 | Table1; 462 | SELECT 463 | count(*), 464 | Column1 465 | FROM 466 | Table2; 467 | `); 468 | }); 469 | 470 | it('formats unicode correctly', () => { 471 | const result = format('SELECT test, тест FROM table;'); 472 | expect(result).toBe(dedent/* sql */ ` 473 | SELECT 474 | test, 475 | тест 476 | FROM 477 | table; 478 | `); 479 | }); 480 | 481 | it('converts keywords to uppercase when option passed in', () => { 482 | const result = format('select distinct * frOM foo left join bar WHERe cola > 1 and colb = 3', { 483 | uppercase: true 484 | }); 485 | expect(result).toBe(dedent/* sql */ ` 486 | SELECT 487 | DISTINCT * 488 | FROM 489 | foo 490 | LEFT JOIN bar 491 | WHERE 492 | cola > 1 493 | AND colb = 3 494 | `); 495 | }); 496 | 497 | it('line breaks between queries change with config', () => { 498 | const result = format('SELECT * FROM foo; SELECT * FROM bar;', { linesBetweenQueries: 2 }); 499 | expect(result).toBe(dedent/* sql */ ` 500 | SELECT 501 | * 502 | FROM 503 | foo; 504 | 505 | SELECT 506 | * 507 | FROM 508 | bar; 509 | `); 510 | }); 511 | 512 | it('correctly indents create statement after select', () => { 513 | const result = sqlFormatter.format(` 514 | SELECT * FROM test; 515 | CREATE TABLE TEST(id NUMBER NOT NULL, col1 VARCHAR2(20), col2 VARCHAR2(20)); 516 | `); 517 | expect(result).toBe(dedent/* sql */ ` 518 | SELECT 519 | * 520 | FROM 521 | test; 522 | CREATE TABLE TEST( 523 | id NUMBER NOT NULL, 524 | col1 VARCHAR2(20), 525 | col2 VARCHAR2(20) 526 | ); 527 | `); 528 | }); 529 | } 530 | -------------------------------------------------------------------------------- /test/sqlFormatterTest.js: -------------------------------------------------------------------------------- 1 | import sqlFormatter from './../src/sqlFormatter'; 2 | 3 | describe('sqlFormatter', () => { 4 | it('throws error when unsupported language parameter specified', () => { 5 | expect(() => { 6 | sqlFormatter.format('SELECT *', { language: 'blah' }); 7 | }).toThrow('Unsupported SQL dialect: blah'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | output: { 3 | library: 'sqlFormatter', 4 | libraryTarget: 'umd' 5 | } 6 | }; 7 | --------------------------------------------------------------------------------