├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── SQLAutocomplete.ts └── models │ ├── AutocompleteOption.ts │ ├── AutocompleteOptionType.ts │ └── SimpleSQLTokenizer.ts ├── test ├── autocomplete.test.ts └── simplesqltokenizer.test.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .antlr 2 | 3 | ## Node: https://raw.githubusercontent.com/github/gitignore/master/Node.gitignore 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # Snowpack dependency directory (https://snowpack.dev/) 49 | web_modules/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | .parcel-cache 82 | 83 | # Next.js build output 84 | .next 85 | out 86 | 87 | # Nuxt.js build / generate output 88 | .nuxt 89 | dist 90 | 91 | # Gatsby files 92 | .cache/ 93 | # Comment in the public line in if your project uses Gatsby and not Next.js 94 | # https://nextjs.org/blog/next-9-1#public-directory-support 95 | # public 96 | 97 | # vuepress build output 98 | .vuepress/dist 99 | 100 | # Serverless directories 101 | .serverless/ 102 | 103 | # FuseBox cache 104 | .fusebox/ 105 | 106 | # DynamoDB Local files 107 | .dynamodb/ 108 | 109 | # TernJS port file 110 | .tern-port 111 | 112 | # Stores VSCode versions used for testing VSCode extensions 113 | .vscode-test 114 | 115 | # yarn v2 116 | .yarn/cache 117 | .yarn/unplugged 118 | .yarn/build-state.yml 119 | .yarn/install-state.gz 120 | .pnp.* 121 | 122 | ## macOS: https://raw.githubusercontent.com/github/gitignore/master/Global/macOS.gitignore 123 | 124 | # General 125 | .DS_Store 126 | .AppleDouble 127 | .LSOverride 128 | 129 | # Icon must end with two \r 130 | Icon 131 | 132 | 133 | # Thumbnails 134 | ._* 135 | 136 | # Files that might appear in the root of a volume 137 | .DocumentRevisions-V100 138 | .fseventsd 139 | .Spotlight-V100 140 | .TemporaryItems 141 | .Trashes 142 | .VolumeIcon.icns 143 | .com.apple.timemachine.donotpresent 144 | 145 | # Directories potentially created on remote AFP share 146 | .AppleDB 147 | .AppleDesktop 148 | Network Trash Folder 149 | Temporary Items 150 | .apdisk 151 | 152 | ## Windows: https://raw.githubusercontent.com/github/gitignore/master/Global/Windows.gitignore 153 | 154 | # Windows thumbnail cache files 155 | Thumbs.db 156 | Thumbs.db:encryptable 157 | ehthumbs.db 158 | ehthumbs_vista.db 159 | 160 | # Dump file 161 | *.stackdump 162 | 163 | # Folder config file 164 | [Dd]esktop.ini 165 | 166 | # Recycle Bin used on file shares 167 | $RECYCLE.BIN/ 168 | 169 | # Windows Installer files 170 | *.cab 171 | *.msi 172 | *.msix 173 | *.msm 174 | *.msp 175 | 176 | # Windows shortcuts 177 | *.lnk -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | test/ 3 | jest.config.js 4 | index.ts 5 | tsconfig.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Matt @ modelDBA 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-autocomplete 2 | 3 | Generate valid autocomplete suggestions for keywords, tables, or columns. 4 | 5 | Supports MySQL, T-SQL (SQL Server), PL/pgSQL (PostgreSQL) and PL/SQL (Oracle) dialects. 6 | 7 | ## Install 8 | ```shell 9 | npm install sql-autocomplete 10 | ``` 11 | 12 | ## [Full documentation can be found here](https://modeldba.com/sql-autocomplete/docs/) 13 | 14 | ## Get Started 15 | 16 | ```typescript 17 | import { SQLAutocomplete, SQLDialect } from 'sql-autocomplete'; 18 | 19 | const sqlAutocomplete = new SQLAutocomplete(SQLDialect.MYSQL, 20 | ['myDatabaseTableName'], // Optional 21 | ['aColumnName']); // Optional 22 | const sql1 = 'SELECT * FR'; 23 | const options1 = sqlAutocomplete.autocomplete(sql1); 24 | console.dir(options1); 25 | 26 | // [ AutocompleteOption { value: 'FROM', optionType: 'KEYWORD' } ] 27 | 28 | const sql2 = 'SELECT * FROM myDatab'; 29 | const options2 = sqlAutocomplete.autocomplete(sql2); 30 | console.dir(options2); 31 | 32 | // [ AutocompleteOption { value: 'myDatabaseTableName', optionType: 'TABLE' } ] 33 | ``` 34 | 35 | ## Created By 36 | 37 | [![modelDBA logo](https://modeldba.com/sql-autocomplete/modelDBA128x128.png "modelDBA")](https://modeldba.com) 38 | 39 | sql-autocomplete is a project created and maintained by [modelDBA](https://modeldba.com), a database IDE for modern developers. 40 | modelDBA lets you visualize SQL as you type and edit tables easily with a no-code table editor. 41 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export { SQLDialect } from 'antlr4ts-sql'; 2 | 3 | export * from './src/SQLAutocomplete'; 4 | 5 | export * from './src/models/AutocompleteOption'; 6 | export * from './src/models/AutocompleteOptionType'; 7 | export * from './src/models/SimpleSQLTokenizer'; 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sql-autocomplete", 3 | "version": "1.1.1", 4 | "description": "Autocomplete recommendations for SQL statements. Supports PostgreSQL, MySQL, SQL Server and Oracle (PL/SQL) dialects.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc --build --clean && tsc", 9 | "test": "npx jest" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/modeldba/sql-autocomplete.git" 14 | }, 15 | "keywords": [ 16 | "sql", 17 | "autocomplete", 18 | "database", 19 | "postgresql", 20 | "mysql", 21 | "sqlserver", 22 | "oracle", 23 | "plpgsql", 24 | "plsql", 25 | "tsql" 26 | ], 27 | "author": "modelDBA", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/modeldba/sql-autocomplete/issues" 31 | }, 32 | "homepage": "https://modeldba.com/sql-autocomplete", 33 | "dependencies": { 34 | "antlr4-c3": "^1.1.15", 35 | "antlr4ts-sql": "^1.1.0" 36 | }, 37 | "devDependencies": { 38 | "@types/jest": "^26.0.20", 39 | "jest": "^26.6.3", 40 | "ts-jest": "^26.5.3", 41 | "typescript": "^4.2.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/SQLAutocomplete.ts: -------------------------------------------------------------------------------- 1 | import { antlr4tsSQL, CommonTokenStream, PredictionMode, MySQLGrammar, Parser, PLpgSQLGrammar, PlSQLGrammar, SQLDialect, Token, TSQLGrammar } from 'antlr4ts-sql'; 2 | import { CodeCompletionCore } from "antlr4-c3"; 3 | import { AutocompleteOption } from "./models/AutocompleteOption"; 4 | import { AutocompleteOptionType } from "./models/AutocompleteOptionType"; 5 | import { SimpleSQLTokenizer } from "./models/SimpleSQLTokenizer"; 6 | 7 | export class SQLAutocomplete { 8 | 9 | dialect: SQLDialect; 10 | antlr4tssql: antlr4tsSQL; 11 | 12 | tableNames: string[] = []; 13 | columnNames: string[] = []; 14 | 15 | constructor(dialect: SQLDialect, tableNames?: string[], columnNames?: string[]) { 16 | this.dialect = dialect; 17 | this.antlr4tssql = new antlr4tsSQL(this.dialect); 18 | if (tableNames !== null && tableNames !== undefined) { 19 | this.tableNames.push(...tableNames); 20 | } 21 | if (columnNames !== null && columnNames !== undefined) { 22 | this.columnNames.push(...columnNames); 23 | } 24 | } 25 | 26 | autocomplete(sqlScript: string, atIndex?: number): AutocompleteOption[] { 27 | if (atIndex !== undefined && atIndex !== null) { 28 | // Remove everything after the index we want to get suggestions for, 29 | // it's not needed and keeping it in may impact which token gets selected for prediction 30 | sqlScript = sqlScript.substring(0, atIndex); 31 | } 32 | const tokens = this._getTokens(sqlScript); 33 | const parser = this._getParser(tokens); 34 | const core = new CodeCompletionCore(parser); 35 | const preferredRulesTable = this._getPreferredRulesForTable(); 36 | const preferredRulesColumn = this._getPreferredRulesForColumn(); 37 | const preferredRuleOptions = [preferredRulesTable, preferredRulesColumn]; 38 | const ignoreTokens = this._getTokensToIgnore(); 39 | core.ignoredTokens = new Set(ignoreTokens); 40 | let indexToAutocomplete = sqlScript.length; 41 | if (atIndex !== null && atIndex !== undefined) { 42 | indexToAutocomplete = atIndex; 43 | } 44 | const simpleSQLTokenizer = new SimpleSQLTokenizer(sqlScript, this._tokenizeWhitespace()); 45 | const allTokens = new CommonTokenStream(simpleSQLTokenizer); 46 | const tokenIndex = this._getTokenIndexAt(allTokens.getTokens(), sqlScript, indexToAutocomplete); 47 | if (tokenIndex === null) { 48 | return null; 49 | } 50 | const token: any = allTokens.getTokens()[tokenIndex]; 51 | const tokenString = this._getTokenString(token, sqlScript, indexToAutocomplete); 52 | tokens.fill(); // Needed for CoreCompletionCore to process correctly, see: https://github.com/mike-lischke/antlr4-c3/issues/42 53 | const autocompleteOptions: AutocompleteOption[] = []; 54 | // Depending on the SQL grammar, we may not get both Tables and Column rules, 55 | // even if both are viable options for autocompletion 56 | // So, instead of using all preferredRules at once, we'll do them separate 57 | let isTableCandidatePosition = false; 58 | let isColumnCandidatePosition = false; 59 | for (const preferredRules of preferredRuleOptions) { 60 | core.preferredRules = new Set(preferredRules); 61 | const candidates = core.collectCandidates(tokenIndex); 62 | for (const candidateToken of candidates.tokens) { 63 | let candidateTokenValue = parser.vocabulary.getDisplayName(candidateToken[0]); 64 | if (this.dialect === SQLDialect.MYSQL && candidateTokenValue.endsWith('_SYMBOL')) { 65 | candidateTokenValue = candidateTokenValue.substring(0, candidateTokenValue.length - 7); 66 | } 67 | if (candidateTokenValue.startsWith("'") && candidateTokenValue.endsWith("'")) { 68 | candidateTokenValue = candidateTokenValue.substring(1, candidateTokenValue.length - 1); 69 | } 70 | let followOnTokens = candidateToken[1]; 71 | for (const followOnToken of followOnTokens) { 72 | let followOnTokenValue = parser.vocabulary.getDisplayName(followOnToken); 73 | if (followOnTokenValue.startsWith("'") && followOnTokenValue.endsWith("'")) { 74 | followOnTokenValue = followOnTokenValue.substring(1, followOnTokenValue.length - 1); 75 | } 76 | if (!(followOnTokenValue.length === 1 && /[^\w\s]/.test(followOnTokenValue))) { 77 | candidateTokenValue += ' '; 78 | } 79 | candidateTokenValue += followOnTokenValue; 80 | } 81 | if (tokenString.length === 0 || (candidateTokenValue.startsWith(tokenString.toUpperCase()) && autocompleteOptions.find(option => option.value === candidateTokenValue) === undefined)) { 82 | autocompleteOptions.push(new AutocompleteOption(candidateTokenValue, AutocompleteOptionType.KEYWORD)); 83 | } 84 | } 85 | for (const rule of candidates.rules) { 86 | if (preferredRulesTable.includes(rule[0])) { 87 | isTableCandidatePosition = true; 88 | } 89 | if (preferredRulesColumn.includes(rule[0])) { 90 | isColumnCandidatePosition = true; 91 | } 92 | } 93 | } 94 | if (isTableCandidatePosition) { 95 | for (const tableName of this.tableNames) { 96 | if (tableName.toUpperCase().startsWith(tokenString.toUpperCase())) { 97 | autocompleteOptions.unshift(new AutocompleteOption(tableName, AutocompleteOptionType.TABLE)); 98 | } 99 | } 100 | if (autocompleteOptions.length === 0 || autocompleteOptions[0].optionType !== AutocompleteOptionType.TABLE) { 101 | // If none of the table options match, still identify this as a potential table location 102 | autocompleteOptions.unshift(new AutocompleteOption(null, AutocompleteOptionType.TABLE)); 103 | } 104 | } 105 | if (isColumnCandidatePosition) { 106 | for (const columnName of this.columnNames) { 107 | if (columnName.toUpperCase().startsWith(tokenString.toUpperCase())) { 108 | autocompleteOptions.unshift(new AutocompleteOption(columnName, AutocompleteOptionType.COLUMN)); 109 | } 110 | } 111 | if (autocompleteOptions.length === 0 || autocompleteOptions[0].optionType !== AutocompleteOptionType.COLUMN) { 112 | // If none of the column options match, still identify this as a potential column location 113 | autocompleteOptions.unshift(new AutocompleteOption(null, AutocompleteOptionType.COLUMN)); 114 | } 115 | } 116 | return autocompleteOptions; 117 | } 118 | 119 | setTableNames(tableNames: string[]): void { 120 | if (tableNames !== null && tableNames !== undefined) { 121 | this.tableNames = [...tableNames]; 122 | } 123 | } 124 | 125 | setColumnNames(columnNames: string[]): void { 126 | if (columnNames !== null && columnNames !== undefined) { 127 | this.columnNames = [...columnNames]; 128 | } 129 | } 130 | 131 | _getTokens(sqlScript: string): CommonTokenStream { 132 | const tokens = this.antlr4tssql.getTokens(sqlScript, []); 133 | return tokens; 134 | } 135 | 136 | _getParser(tokens: CommonTokenStream): Parser { 137 | let parser = this.antlr4tssql.getParser(tokens, []); 138 | parser.interpreter.setPredictionMode(PredictionMode.LL); 139 | return parser; 140 | } 141 | 142 | _tokenizeWhitespace() { 143 | if (this.dialect === SQLDialect.TSQL) { 144 | return false; // TSQL grammar SKIPs whitespace 145 | } else if (this.dialect === SQLDialect.PLSQL) { 146 | return true; 147 | } else if (this.dialect === SQLDialect.PLpgSQL) { 148 | return true; 149 | } else if (this.dialect === SQLDialect.MYSQL) { 150 | return true; 151 | } 152 | return true; 153 | } 154 | 155 | _getPreferredRulesForTable(): number[] { 156 | if (this.dialect === SQLDialect.TSQL) { 157 | return [ 158 | TSQLGrammar.TSqlParser.RULE_table_name, 159 | TSQLGrammar.TSqlParser.RULE_table_name_with_hint, 160 | TSQLGrammar.TSqlParser.RULE_full_table_name, 161 | TSQLGrammar.TSqlParser.RULE_table_source 162 | ]; 163 | } else if (this.dialect === SQLDialect.MYSQL) { 164 | return [ 165 | MySQLGrammar.MultiQueryMySQLParser.RULE_tableRef, 166 | MySQLGrammar.MultiQueryMySQLParser.RULE_fieldIdentifier 167 | ] 168 | } else if (this.dialect === SQLDialect.PLSQL) { 169 | return [ 170 | PlSQLGrammar.PlSqlParser.RULE_tableview_name, 171 | PlSQLGrammar.PlSqlParser.RULE_table_element 172 | ] 173 | } else if (this.dialect === SQLDialect.PLpgSQL) { 174 | return [ 175 | PLpgSQLGrammar.PLpgSQLParser.RULE_schema_qualified_name, 176 | PLpgSQLGrammar.PLpgSQLParser.RULE_indirection_var 177 | ]; 178 | } 179 | return []; 180 | } 181 | 182 | _getPreferredRulesForColumn(): number[] { 183 | if (this.dialect === SQLDialect.TSQL) { 184 | return [ 185 | TSQLGrammar.TSqlParser.RULE_column_elem, 186 | TSQLGrammar.TSqlParser.RULE_column_alias, 187 | TSQLGrammar.TSqlParser.RULE_full_column_name, 188 | TSQLGrammar.TSqlParser.RULE_output_column_name, 189 | TSQLGrammar.TSqlParser.RULE_column_declaration 190 | ]; 191 | } else if (this.dialect === SQLDialect.MYSQL) { 192 | return [ 193 | MySQLGrammar.MultiQueryMySQLParser.RULE_columnRef 194 | ]; 195 | } else if (this.dialect === SQLDialect.PLSQL) { 196 | return [ 197 | PlSQLGrammar.PlSqlParser.RULE_column_name, 198 | PlSQLGrammar.PlSqlParser.RULE_general_element 199 | ]; 200 | } else if (this.dialect === SQLDialect.PLpgSQL) { 201 | return [ 202 | PLpgSQLGrammar.PLpgSQLParser.RULE_indirection_var, 203 | PLpgSQLGrammar.PLpgSQLParser.RULE_indirection_identifier 204 | ]; 205 | } 206 | return []; 207 | } 208 | 209 | _getTokensToIgnore(): number[] { 210 | if (this.dialect === SQLDialect.TSQL) { 211 | return [ 212 | TSQLGrammar.TSqlParser.DOT, 213 | TSQLGrammar.TSqlParser.COMMA, 214 | TSQLGrammar.TSqlParser.ID, 215 | TSQLGrammar.TSqlParser.LR_BRACKET, 216 | TSQLGrammar.TSqlParser.RR_BRACKET 217 | ]; 218 | } else if (this.dialect === SQLDialect.MYSQL) { 219 | return [ 220 | MySQLGrammar.MultiQueryMySQLParser.DOT_SYMBOL, 221 | MySQLGrammar.MultiQueryMySQLParser.COMMA_SYMBOL, 222 | MySQLGrammar.MultiQueryMySQLParser.SEMICOLON_SYMBOL, 223 | MySQLGrammar.MultiQueryMySQLParser.IDENTIFIER, 224 | MySQLGrammar.MultiQueryMySQLParser.OPEN_PAR_SYMBOL, 225 | MySQLGrammar.MultiQueryMySQLParser.CLOSE_PAR_SYMBOL, 226 | MySQLGrammar.MultiQueryMySQLParser.OPEN_CURLY_SYMBOL, 227 | MySQLGrammar.MultiQueryMySQLParser.CLOSE_CURLY_SYMBOL 228 | ]; 229 | } else if (this.dialect === SQLDialect.PLSQL) { 230 | return [ 231 | PlSQLGrammar.PlSqlParser.PERIOD, 232 | PlSQLGrammar.PlSqlParser.COMMA, 233 | PlSQLGrammar.PlSqlParser.SEMICOLON, 234 | PlSQLGrammar.PlSqlParser.DOUBLE_PERIOD, 235 | PlSQLGrammar.PlSqlParser.IDENTIFIER, 236 | PlSQLGrammar.PlSqlParser.LEFT_PAREN, 237 | PlSQLGrammar.PlSqlParser.RIGHT_PAREN 238 | ]; 239 | } else if (this.dialect === SQLDialect.PLpgSQL) { 240 | return [ 241 | PLpgSQLGrammar.PLpgSQLParser.DOT, 242 | PLpgSQLGrammar.PLpgSQLParser.COMMA, 243 | PLpgSQLGrammar.PLpgSQLParser.SEMI_COLON, 244 | PLpgSQLGrammar.PLpgSQLParser.DOUBLE_DOT, 245 | PLpgSQLGrammar.PLpgSQLParser.Identifier, 246 | PLpgSQLGrammar.PLpgSQLParser.LEFT_PAREN, 247 | PLpgSQLGrammar.PLpgSQLParser.RIGHT_PAREN, 248 | PLpgSQLGrammar.PLpgSQLParser.LEFT_BRACKET, 249 | PLpgSQLGrammar.PLpgSQLParser.RIGHT_BRACKET 250 | ]; 251 | } 252 | return []; 253 | } 254 | 255 | _getTokenIndexAt(tokens: any[], fullString: string, offset: number): number { 256 | if (tokens.length === 0) { 257 | return null; 258 | } 259 | let i: number = 0 260 | let lastNonEOFToken: number = null; 261 | while (i < tokens.length) { 262 | const token = tokens[i]; 263 | if (token.type !== Token.EOF) { 264 | lastNonEOFToken = i; 265 | } 266 | if (token.start > offset) { 267 | if (i === 0) { 268 | return null; 269 | } 270 | return i - 1; 271 | } 272 | i++; 273 | } 274 | // If we didn't find the token above and the last 275 | // character in the autocomplete is whitespace, 276 | // start autocompleting for the next token 277 | if (/\s$/.test(fullString)) { 278 | return i - 1; 279 | } 280 | return lastNonEOFToken; 281 | } 282 | 283 | _getTokenString(token: any, fullString: string, offset: number): string { 284 | if (token !== null && token.type !== Token.EOF) { 285 | let stop = token.stop; 286 | if (offset < stop) { 287 | stop = offset; 288 | } 289 | return fullString.substring(token.start, stop + 1); 290 | } 291 | return ''; 292 | } 293 | 294 | } -------------------------------------------------------------------------------- /src/models/AutocompleteOption.ts: -------------------------------------------------------------------------------- 1 | import { AutocompleteOptionType } from "./AutocompleteOptionType"; 2 | 3 | export class AutocompleteOption { 4 | 5 | value: string; 6 | optionType: AutocompleteOptionType; 7 | 8 | constructor(value: string, optionType: AutocompleteOptionType) { 9 | this.value = value; 10 | this.optionType = optionType; 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /src/models/AutocompleteOptionType.ts: -------------------------------------------------------------------------------- 1 | export enum AutocompleteOptionType { 2 | KEYWORD = 'KEYWORD', 3 | COLUMN = 'COLUMN', 4 | TABLE = 'TABLE' 5 | } -------------------------------------------------------------------------------- /src/models/SimpleSQLTokenizer.ts: -------------------------------------------------------------------------------- 1 | import { TokenSource, Token, CharStream, TokenFactory, CommonToken } from "antlr4ts-sql"; 2 | 3 | /** 4 | * A very simple tokenizer for splitting a string into tokens based on 5 | * whitespace. Handles SQL quote characters (i.e. '). Also splits ; and . as separate tokens 6 | * 7 | * THIS SHOULD NOT BE USED FOR MOST ANTLR4 TASKS. 8 | * 9 | * It is designed to be used to find the correct token index at 10 | * any string location, regardless of the validity of the SQL string. 11 | * See SQLAutocomplete.getTokenIndexAt for usage. 12 | */ 13 | export class SimpleSQLTokenizer implements TokenSource { 14 | 15 | value: string; 16 | _currentIndex: number; 17 | _insideQuote: boolean; 18 | 19 | specialCharacters: string[] = [';', '.', '(', ')']; 20 | whitespaceCharacters: string[] = [' ', '\f', '\n', '\r', '\t', '\v', '\u00A0', '\u2028', '\u2029']; 21 | 22 | constructor(value: string, tokenizeWhitespace: boolean) { 23 | this.value = value; 24 | this._currentIndex = 0; 25 | this._insideQuote = false; 26 | if (tokenizeWhitespace) { 27 | this.specialCharacters.push(...this.whitespaceCharacters); 28 | } 29 | } 30 | 31 | nextToken(): Token { 32 | let start = null; 33 | let stop = null; 34 | const notWhitespaceRegex = /[^\s]/; 35 | while (this._currentIndex < this.value.length) { 36 | const currentChar = this.value[this._currentIndex]; 37 | if (currentChar === "'" && this.value[this._currentIndex - 1] !== '\\') { 38 | this._insideQuote = !this._insideQuote; 39 | } 40 | if ((notWhitespaceRegex.test(currentChar) && !this.specialCharacters.includes(currentChar)) || this._insideQuote) { 41 | if (start === null) { 42 | start = this._currentIndex; 43 | } 44 | if (this._currentIndex === this.value.length - 1) { 45 | stop = this._currentIndex; 46 | } 47 | } else if (start !== null) { 48 | stop = this._currentIndex - 1; 49 | if (this.specialCharacters.includes(currentChar)) { 50 | // The next block will iterate past the current special character 51 | // Need to back up so that on the next call to nextToken, the special character will be identified again 52 | this._currentIndex--; 53 | } 54 | } 55 | if (start !== null && stop !== null) { 56 | this._currentIndex++; 57 | return new CommonToken(Token.DEFAULT_CHANNEL, this.value.substring(start, stop + 1), {}, null, start, stop); 58 | } 59 | if (this.specialCharacters.includes(currentChar) && !this._insideQuote) { 60 | this._currentIndex++; 61 | return new CommonToken(Token.DEFAULT_CHANNEL, currentChar, {}, null, this._currentIndex - 1, this._currentIndex - 1); 62 | } 63 | this._currentIndex++; 64 | } 65 | return new CommonToken(Token.EOF); 66 | } 67 | 68 | line: number; 69 | charPositionInLine: number; 70 | inputStream: CharStream; 71 | sourceName: string; 72 | tokenFactory: TokenFactory; 73 | 74 | } -------------------------------------------------------------------------------- /test/autocomplete.test.ts: -------------------------------------------------------------------------------- 1 | import { SQLAutocomplete, SQLDialect, AutocompleteOption, AutocompleteOptionType } from '../dist/index'; 2 | 3 | let mysqlAutocomplete: SQLAutocomplete = null; 4 | let plsqlAutocomplete: SQLAutocomplete = null; 5 | let plpgsqlAutocomplete: SQLAutocomplete = null; 6 | let tsqlAutocomplete: SQLAutocomplete = null; 7 | beforeAll(() => { 8 | mysqlAutocomplete = new SQLAutocomplete(SQLDialect.MYSQL); 9 | plsqlAutocomplete = new SQLAutocomplete(SQLDialect.PLSQL); 10 | plpgsqlAutocomplete = new SQLAutocomplete(SQLDialect.PLpgSQL); 11 | tsqlAutocomplete = new SQLAutocomplete(SQLDialect.TSQL); 12 | }); 13 | 14 | function containsOptionType(options: AutocompleteOption[], type: AutocompleteOptionType): boolean { 15 | for (const option of options) { 16 | if (option.optionType === type) { 17 | return true; 18 | } 19 | } 20 | return false; 21 | } 22 | 23 | function containsOption(options: AutocompleteOption[], type: AutocompleteOptionType, value: string): boolean { 24 | for (const option of options) { 25 | if (option.optionType === type && option.value === value) { 26 | return true; 27 | } 28 | } 29 | return false; 30 | } 31 | 32 | function allKeywordsBeginWith(options: AutocompleteOption[], value: string): boolean { 33 | value = value.toUpperCase(); 34 | for (const option of options) { 35 | if (option.optionType === AutocompleteOptionType.KEYWORD && !option.value.startsWith(value)) { 36 | return false; 37 | } 38 | } 39 | return true; 40 | } 41 | 42 | test('autocomplete constructor options', () => { 43 | const autocompleterWithoutNames = new SQLAutocomplete(SQLDialect.MYSQL); 44 | expect(autocompleterWithoutNames.tableNames.length).toBe(0); 45 | expect(autocompleterWithoutNames.columnNames.length).toBe(0); 46 | 47 | const autocompleterWithNames = new SQLAutocomplete(SQLDialect.MYSQL, ['table1'], ['columnA']); 48 | expect(autocompleterWithNames.tableNames.length).toBe(1); 49 | expect(autocompleterWithNames.columnNames.length).toBe(1); 50 | 51 | // Test for a table location 52 | const sqlWithTable = 'SELECT * FROM t'; 53 | const options = autocompleterWithoutNames.autocomplete(sqlWithTable, sqlWithTable.length); 54 | expect(containsOptionType(options, AutocompleteOptionType.TABLE)).toBeTruthy(); 55 | expect(containsOption(options, AutocompleteOptionType.TABLE, null)).toBeTruthy(); 56 | expect(containsOptionType(options, AutocompleteOptionType.COLUMN)).toBeFalsy(); 57 | expect(allKeywordsBeginWith(options, 't')).toBeTruthy(); 58 | 59 | const options2 = autocompleterWithNames.autocomplete(sqlWithTable, sqlWithTable.length); 60 | expect(containsOptionType(options2, AutocompleteOptionType.TABLE)).toBeTruthy(); 61 | expect(containsOption(options2, AutocompleteOptionType.TABLE, null)).toBeFalsy(); 62 | expect(containsOption(options2, AutocompleteOptionType.TABLE, 'table1')).toBeTruthy(); 63 | expect(containsOptionType(options2, AutocompleteOptionType.COLUMN)).toBeFalsy(); 64 | expect(allKeywordsBeginWith(options2, 't')).toBeTruthy(); 65 | 66 | // Test for a table or column location 67 | const sqlWithColumn = 'SELECT * FROM table1 WHERE c'; 68 | const options3 = autocompleterWithoutNames.autocomplete(sqlWithColumn, sqlWithColumn.length); 69 | expect(containsOptionType(options3, AutocompleteOptionType.TABLE)).toBeTruthy(); 70 | expect(containsOption(options3, AutocompleteOptionType.TABLE, null)).toBeTruthy(); 71 | expect(containsOptionType(options3, AutocompleteOptionType.COLUMN)).toBeTruthy(); 72 | expect(containsOption(options3, AutocompleteOptionType.COLUMN, null)).toBeTruthy(); 73 | expect(allKeywordsBeginWith(options3, 'c')).toBeTruthy(); 74 | 75 | const options4 = autocompleterWithNames.autocomplete(sqlWithColumn, sqlWithColumn.length); 76 | expect(containsOptionType(options4, AutocompleteOptionType.TABLE)).toBeTruthy(); 77 | expect(containsOption(options4, AutocompleteOptionType.TABLE, 'table1')).toBeFalsy(); 78 | expect(containsOption(options4, AutocompleteOptionType.TABLE, null)).toBeTruthy(); 79 | expect(containsOptionType(options4, AutocompleteOptionType.COLUMN)).toBeTruthy(); 80 | expect(containsOption(options4, AutocompleteOptionType.COLUMN, 'columnA')).toBeTruthy(); 81 | expect(allKeywordsBeginWith(options4, 'c')).toBeTruthy(); 82 | }) 83 | 84 | test('autocomplete detects table location', () => { 85 | const sql = 'SELECT * FROM t'; 86 | const tsqlOptions = tsqlAutocomplete.autocomplete(sql, sql.length); 87 | expect(containsOptionType(tsqlOptions, AutocompleteOptionType.TABLE)).toBeTruthy(); 88 | expect(containsOptionType(tsqlOptions, AutocompleteOptionType.COLUMN)).toBeFalsy(); 89 | expect(allKeywordsBeginWith(tsqlOptions, 't')).toBeTruthy(); 90 | const mysqlOptions = mysqlAutocomplete.autocomplete(sql, sql.length); 91 | expect(containsOptionType(mysqlOptions, AutocompleteOptionType.TABLE)).toBeTruthy(); 92 | expect(containsOptionType(mysqlOptions, AutocompleteOptionType.COLUMN)).toBeFalsy(); 93 | expect(allKeywordsBeginWith(mysqlOptions, 't')).toBeTruthy(); 94 | const plsqlOptions = plsqlAutocomplete.autocomplete(sql, sql.length); 95 | expect(containsOptionType(plsqlOptions, AutocompleteOptionType.TABLE)).toBeTruthy(); 96 | expect(containsOptionType(plsqlOptions, AutocompleteOptionType.COLUMN)).toBeFalsy(); 97 | expect(allKeywordsBeginWith(plsqlOptions, 't')).toBeTruthy(); 98 | const plpgsqlOptions = plpgsqlAutocomplete.autocomplete(sql, sql.length); 99 | expect(containsOptionType(plpgsqlOptions, AutocompleteOptionType.TABLE)).toBeTruthy(); 100 | expect(containsOptionType(plpgsqlOptions, AutocompleteOptionType.COLUMN)).toBeFalsy(); 101 | expect(allKeywordsBeginWith(plpgsqlOptions, 't')).toBeTruthy(); 102 | }); 103 | 104 | test('autocomplete detects column location', () => { 105 | const sql = 'SELECT * FROM table1 WHERE c'; 106 | const tsqlOptions = tsqlAutocomplete.autocomplete(sql, sql.length); 107 | expect(containsOptionType(tsqlOptions, AutocompleteOptionType.TABLE)).toBeTruthy(); 108 | expect(containsOptionType(tsqlOptions, AutocompleteOptionType.COLUMN)).toBeTruthy(); 109 | expect(allKeywordsBeginWith(tsqlOptions, 'c')).toBeTruthy(); 110 | const mysqlOptions = mysqlAutocomplete.autocomplete(sql, sql.length); 111 | expect(containsOptionType(mysqlOptions, AutocompleteOptionType.TABLE)).toBeTruthy(); 112 | expect(containsOptionType(mysqlOptions, AutocompleteOptionType.COLUMN)).toBeTruthy(); 113 | expect(allKeywordsBeginWith(mysqlOptions, 'c')).toBeTruthy(); 114 | const plsqlOptions = plsqlAutocomplete.autocomplete(sql, sql.length); 115 | expect(containsOptionType(plsqlOptions, AutocompleteOptionType.TABLE)).toBeTruthy(); 116 | expect(containsOptionType(plsqlOptions, AutocompleteOptionType.COLUMN)).toBeTruthy(); 117 | expect(allKeywordsBeginWith(plsqlOptions, 'c')).toBeTruthy(); 118 | const plpgsqlOptions = plpgsqlAutocomplete.autocomplete(sql, sql.length); 119 | expect(containsOptionType(plpgsqlOptions, AutocompleteOptionType.TABLE)).toBeTruthy(); 120 | expect(containsOptionType(plpgsqlOptions, AutocompleteOptionType.COLUMN)).toBeTruthy(); 121 | expect(allKeywordsBeginWith(plpgsqlOptions, 'c')).toBeTruthy(); 122 | }); 123 | 124 | test('autocomplete next word', () => { 125 | const sql = 'SELECT '; 126 | const tsqlOptions = tsqlAutocomplete.autocomplete(sql); 127 | expect(containsOptionType(tsqlOptions, AutocompleteOptionType.TABLE)).toBeTruthy(); 128 | expect(containsOptionType(tsqlOptions, AutocompleteOptionType.COLUMN)).toBeTruthy(); 129 | const mysqlOptions = mysqlAutocomplete.autocomplete(sql); 130 | expect(containsOptionType(mysqlOptions, AutocompleteOptionType.TABLE)).toBeTruthy(); 131 | expect(containsOptionType(mysqlOptions, AutocompleteOptionType.COLUMN)).toBeTruthy(); 132 | const plsqlOptions = plsqlAutocomplete.autocomplete(sql); 133 | expect(containsOptionType(plsqlOptions, AutocompleteOptionType.TABLE)).toBeTruthy(); 134 | expect(containsOptionType(plsqlOptions, AutocompleteOptionType.COLUMN)).toBeTruthy(); 135 | const plpgsqlOptions = plpgsqlAutocomplete.autocomplete(sql); 136 | expect(containsOptionType(plpgsqlOptions, AutocompleteOptionType.TABLE)).toBeTruthy(); 137 | expect(containsOptionType(plpgsqlOptions, AutocompleteOptionType.COLUMN)).toBeTruthy(); 138 | }); 139 | 140 | test('autocomplete when position is not provided', () => { 141 | const sql = 'SELECT * FR'; 142 | const tsqlOptions = tsqlAutocomplete.autocomplete(sql); 143 | expect(containsOptionType(tsqlOptions, AutocompleteOptionType.TABLE)).toBeFalsy(); 144 | expect(containsOptionType(tsqlOptions, AutocompleteOptionType.COLUMN)).toBeFalsy(); 145 | expect(allKeywordsBeginWith(tsqlOptions, 'FR')).toBeTruthy(); 146 | const mysqlOptions = mysqlAutocomplete.autocomplete(sql); 147 | expect(containsOptionType(mysqlOptions, AutocompleteOptionType.TABLE)).toBeFalsy(); 148 | expect(containsOptionType(mysqlOptions, AutocompleteOptionType.COLUMN)).toBeFalsy(); 149 | expect(allKeywordsBeginWith(mysqlOptions, 'FR')).toBeTruthy(); 150 | const plsqlOptions = plsqlAutocomplete.autocomplete(sql); 151 | expect(containsOptionType(plsqlOptions, AutocompleteOptionType.TABLE)).toBeFalsy(); 152 | expect(containsOptionType(plsqlOptions, AutocompleteOptionType.COLUMN)).toBeFalsy(); 153 | expect(allKeywordsBeginWith(plsqlOptions, 'FR')).toBeTruthy(); 154 | const plpgsqlOptions = plpgsqlAutocomplete.autocomplete(sql); 155 | expect(containsOptionType(plpgsqlOptions, AutocompleteOptionType.TABLE)).toBeFalsy(); 156 | expect(containsOptionType(plpgsqlOptions, AutocompleteOptionType.COLUMN)).toBeFalsy(); 157 | expect(allKeywordsBeginWith(plpgsqlOptions, 'FR')).toBeTruthy(); 158 | }); -------------------------------------------------------------------------------- /test/simplesqltokenizer.test.ts: -------------------------------------------------------------------------------- 1 | import { SimpleSQLTokenizer } from '../dist/index'; 2 | import { Token } from 'antlr4ts-sql'; 3 | 4 | test('SimpleSQLTokenizer correctly parses SQL queries', () => { 5 | let sqlString = 'SELECT * FROM table'; 6 | let tokenizer = new SimpleSQLTokenizer(sqlString, false); 7 | expect(tokenizer.nextToken().text).toBe('SELECT'); 8 | expect(tokenizer.nextToken().text).toBe('*'); 9 | expect(tokenizer.nextToken().text).toBe('FROM'); 10 | expect(tokenizer.nextToken().text).toBe('table'); 11 | expect(tokenizer.nextToken().type).toBe(Token.EOF); 12 | 13 | sqlString = ' SELECT \t* \r\n FROM table '; 14 | tokenizer = new SimpleSQLTokenizer(sqlString, false); 15 | expect(tokenizer.nextToken().text).toBe('SELECT'); 16 | expect(tokenizer.nextToken().text).toBe('*'); 17 | expect(tokenizer.nextToken().text).toBe('FROM'); 18 | expect(tokenizer.nextToken().text).toBe('table'); 19 | expect(tokenizer.nextToken().type).toBe(Token.EOF); 20 | 21 | sqlString = "SELECT * FROM table WHERE column = 'test'"; 22 | tokenizer = new SimpleSQLTokenizer(sqlString, false); 23 | expect(tokenizer.nextToken().text).toBe('SELECT'); 24 | expect(tokenizer.nextToken().text).toBe('*'); 25 | expect(tokenizer.nextToken().text).toBe('FROM'); 26 | expect(tokenizer.nextToken().text).toBe('table'); 27 | expect(tokenizer.nextToken().text).toBe('WHERE'); 28 | expect(tokenizer.nextToken().text).toBe('column'); 29 | expect(tokenizer.nextToken().text).toBe('='); 30 | expect(tokenizer.nextToken().text).toBe("'test'"); 31 | expect(tokenizer.nextToken().type).toBe(Token.EOF); 32 | 33 | sqlString = "SELECT * FROM table WHERE column = 'test and a missing quote"; 34 | tokenizer = new SimpleSQLTokenizer(sqlString, false); 35 | expect(tokenizer.nextToken().text).toBe('SELECT'); 36 | expect(tokenizer.nextToken().text).toBe('*'); 37 | expect(tokenizer.nextToken().text).toBe('FROM'); 38 | expect(tokenizer.nextToken().text).toBe('table'); 39 | expect(tokenizer.nextToken().text).toBe('WHERE'); 40 | expect(tokenizer.nextToken().text).toBe('column'); 41 | expect(tokenizer.nextToken().text).toBe('='); 42 | expect(tokenizer.nextToken().text).toBe("'test and a missing quote"); 43 | expect(tokenizer.nextToken().type !== Token.EOF); 44 | 45 | sqlString = "SELECT * FROM table WHERE column = 'test and a \r\n newline' "; 46 | tokenizer = new SimpleSQLTokenizer(sqlString, false); 47 | expect(tokenizer.nextToken().text).toBe('SELECT'); 48 | expect(tokenizer.nextToken().text).toBe('*'); 49 | expect(tokenizer.nextToken().text).toBe('FROM'); 50 | expect(tokenizer.nextToken().text).toBe('table'); 51 | expect(tokenizer.nextToken().text).toBe('WHERE'); 52 | expect(tokenizer.nextToken().text).toBe('column'); 53 | expect(tokenizer.nextToken().text).toBe('='); 54 | expect(tokenizer.nextToken().text).toBe("'test and a \r\n newline'"); 55 | expect(tokenizer.nextToken().type).toBe(Token.EOF); 56 | 57 | sqlString = "SELECT * FROM table; SELECT * FROM table2 ;SELECT * FROM table3;SELECT * FROM table4 ; "; 58 | tokenizer = new SimpleSQLTokenizer(sqlString, false); 59 | expect(tokenizer.nextToken().text).toBe('SELECT'); 60 | expect(tokenizer.nextToken().text).toBe('*'); 61 | expect(tokenizer.nextToken().text).toBe('FROM'); 62 | expect(tokenizer.nextToken().text).toBe('table'); 63 | expect(tokenizer.nextToken().text).toBe(';'); 64 | expect(tokenizer.nextToken().text).toBe('SELECT'); 65 | expect(tokenizer.nextToken().text).toBe('*'); 66 | expect(tokenizer.nextToken().text).toBe('FROM'); 67 | expect(tokenizer.nextToken().text).toBe('table2'); 68 | expect(tokenizer.nextToken().text).toBe(';'); 69 | expect(tokenizer.nextToken().text).toBe('SELECT'); 70 | expect(tokenizer.nextToken().text).toBe('*'); 71 | expect(tokenizer.nextToken().text).toBe('FROM'); 72 | expect(tokenizer.nextToken().text).toBe('table3'); 73 | expect(tokenizer.nextToken().text).toBe(';'); 74 | expect(tokenizer.nextToken().text).toBe('SELECT'); 75 | expect(tokenizer.nextToken().text).toBe('*'); 76 | expect(tokenizer.nextToken().text).toBe('FROM'); 77 | expect(tokenizer.nextToken().text).toBe('table4'); 78 | expect(tokenizer.nextToken().text).toBe(';'); 79 | expect(tokenizer.nextToken().type).toBe(Token.EOF); 80 | 81 | sqlString = "SELECT * FROM table1 t1 where t1.column1 = 0;"; 82 | tokenizer = new SimpleSQLTokenizer(sqlString, false); 83 | expect(tokenizer.nextToken().text).toBe('SELECT'); 84 | expect(tokenizer.nextToken().text).toBe('*'); 85 | expect(tokenizer.nextToken().text).toBe('FROM'); 86 | expect(tokenizer.nextToken().text).toBe('table1'); 87 | expect(tokenizer.nextToken().text).toBe('t1'); 88 | expect(tokenizer.nextToken().text).toBe('where'); 89 | expect(tokenizer.nextToken().text).toBe('t1'); 90 | expect(tokenizer.nextToken().text).toBe('.'); 91 | expect(tokenizer.nextToken().text).toBe('column1'); 92 | expect(tokenizer.nextToken().text).toBe('='); 93 | expect(tokenizer.nextToken().text).toBe('0'); 94 | expect(tokenizer.nextToken().text).toBe(';'); 95 | expect(tokenizer.nextToken().type).toBe(Token.EOF); 96 | 97 | sqlString = "SELECT * FROM table1 t1 where (t1.column1 = 0);"; 98 | tokenizer = new SimpleSQLTokenizer(sqlString, false); 99 | expect(tokenizer.nextToken().text).toBe('SELECT'); 100 | expect(tokenizer.nextToken().text).toBe('*'); 101 | expect(tokenizer.nextToken().text).toBe('FROM'); 102 | expect(tokenizer.nextToken().text).toBe('table1'); 103 | expect(tokenizer.nextToken().text).toBe('t1'); 104 | expect(tokenizer.nextToken().text).toBe('where'); 105 | expect(tokenizer.nextToken().text).toBe('('); 106 | expect(tokenizer.nextToken().text).toBe('t1'); 107 | expect(tokenizer.nextToken().text).toBe('.'); 108 | expect(tokenizer.nextToken().text).toBe('column1'); 109 | expect(tokenizer.nextToken().text).toBe('='); 110 | expect(tokenizer.nextToken().text).toBe('0'); 111 | expect(tokenizer.nextToken().text).toBe(')'); 112 | expect(tokenizer.nextToken().text).toBe(';'); 113 | expect(tokenizer.nextToken().type).toBe(Token.EOF); 114 | 115 | }); 116 | 117 | test('SimpleSQLTokenizer can parse whitespace', () => { 118 | let sqlString = ' SELECT \t* \r\n FROM table '; 119 | let tokenizer = new SimpleSQLTokenizer(sqlString, true); 120 | expect(tokenizer.nextToken().text).toBe(' '); 121 | expect(tokenizer.nextToken().text).toBe(' '); 122 | expect(tokenizer.nextToken().text).toBe('SELECT'); 123 | expect(tokenizer.nextToken().text).toBe(' '); 124 | expect(tokenizer.nextToken().text).toBe('\t'); 125 | expect(tokenizer.nextToken().text).toBe('*'); 126 | expect(tokenizer.nextToken().text).toBe(' '); 127 | expect(tokenizer.nextToken().text).toBe('\r'); 128 | expect(tokenizer.nextToken().text).toBe('\n'); 129 | expect(tokenizer.nextToken().text).toBe(' '); 130 | expect(tokenizer.nextToken().text).toBe('FROM'); 131 | expect(tokenizer.nextToken().text).toBe(' '); 132 | expect(tokenizer.nextToken().text).toBe(' '); 133 | expect(tokenizer.nextToken().text).toBe('table'); 134 | expect(tokenizer.nextToken().text).toBe(' '); 135 | expect(tokenizer.nextToken().text).toBe(' '); 136 | expect(tokenizer.nextToken().text).toBe(' '); 137 | expect(tokenizer.nextToken().type).toBe(Token.EOF); 138 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true, 8 | "sourceMap": true, 9 | "allowJs": false, 10 | "incremental": true, 11 | "declaration": true, 12 | "outDir": "dist", 13 | "experimentalDecorators": true, 14 | "baseUrl": "./src", 15 | "paths": { 16 | "@/*": ["./*"] 17 | }, 18 | "strictPropertyInitialization": false 19 | }, 20 | "include": ["**/*.ts"], 21 | "exclude": ["node_modules", "dist", "**/*.d.ts", "test", "parse.ts"] 22 | } --------------------------------------------------------------------------------