From 3ae2fbca87b381093787630cf4bcc81dcfb3c829 Mon Sep 17 00:00:00 2001 From: Adel Date: Tue, 9 Jun 2020 23:00:25 +0300 Subject: [PATCH] WIP --- packages/cli/src/generator.test.ts | 6 +- packages/cli/src/generator.ts | 104 +- packages/query/package.json | 3 +- packages/query/src/index.ts | 8 +- .../query/src/loader/sql/grammar/SQLParser.g4 | 17 - packages/query/src/loader/sql/index.ts | 7 +- .../__snapshots__/query.test.ts.snap | 115 ++ .../loader/typescript/grammar/QueryLexer.g4 | 28 + .../loader/typescript/grammar/QueryParser.g4 | 32 + packages/query/src/loader/typescript/index.ts | 34 +- .../loader/typescript/parser/QueryLexer.ts | 131 +++ .../loader/typescript/parser/QueryParser.ts | 1019 +++++++++++++++++ .../typescript/parser/QueryParserListener.ts | 133 +++ .../typescript/parser/QueryParserVisitor.ts | 96 ++ .../query/src/loader/typescript/query.test.ts | 30 + packages/query/src/loader/typescript/query.ts | 177 +++ packages/query/src/preprocessor.test.ts | 150 +-- packages/query/src/preprocessor.ts | 146 +-- packages/query/src/tag.ts | 4 +- 19 files changed, 1944 insertions(+), 296 deletions(-) create mode 100644 packages/query/src/loader/typescript/__snapshots__/query.test.ts.snap create mode 100644 packages/query/src/loader/typescript/grammar/QueryLexer.g4 create mode 100644 packages/query/src/loader/typescript/grammar/QueryParser.g4 create mode 100644 packages/query/src/loader/typescript/parser/QueryLexer.ts create mode 100644 packages/query/src/loader/typescript/parser/QueryParser.ts create mode 100644 packages/query/src/loader/typescript/parser/QueryParserListener.ts create mode 100644 packages/query/src/loader/typescript/parser/QueryParserVisitor.ts create mode 100644 packages/query/src/loader/typescript/query.test.ts create mode 100644 packages/query/src/loader/typescript/query.ts diff --git a/packages/cli/src/generator.test.ts b/packages/cli/src/generator.test.ts index d624028d..b0f75708 100644 --- a/packages/cli/src/generator.test.ts +++ b/packages/cli/src/generator.test.ts @@ -1,5 +1,5 @@ import * as queryModule from '@pgtyped/query'; -import { parseSQLFile } from '@pgtyped/query'; +import { parseSQLFile, parseTypeScriptFile } from "@pgtyped/query"; import { IQueryTypes } from '@pgtyped/query/lib/actions'; import { generateInterface, queryToTypeDeclarations } from './generator'; import { ProcessingMode } from './index'; @@ -13,8 +13,8 @@ function parsedQuery( queryString: string, ): Parameters[0] { return mode === ProcessingMode.SQL - ? { mode, ast: parseSQLFile(queryString).parseTree.queries[0] } - : { mode, name, body: queryString }; + ? { mode, ast: parseSQLFile(queryString).queries[0] } + : { mode, ast: parseTypeScriptFile(queryString).queries[0] } } describe('query-to-interface translation', () => { diff --git a/packages/cli/src/generator.ts b/packages/cli/src/generator.ts index b9554c20..ec40b5a1 100644 --- a/packages/cli/src/generator.ts +++ b/packages/cli/src/generator.ts @@ -4,10 +4,10 @@ import { parseSQLFile, parseTypeScriptFile, prettyPrintEvents, - processQueryAST, - processQueryString, - QueryAST, -} from '@pgtyped/query'; + processTSQueryAST, processSQLQueryAST, + SQLQueryAST, + TSQueryAST, +} from "@pgtyped/query"; import { camelCase } from 'camel-case'; import { pascalCase } from 'pascal-case'; import { ProcessingMode } from './index'; @@ -35,12 +35,11 @@ export const generateTypeAlias = (typeName: string, alias: string) => type ParsedQuery = | { - name: string; - body: string; - mode: ProcessingMode.TS; + ast: TSQueryAST; + mode: ProcessingMode.TS; } | { - ast: QueryAST; + ast: SQLQueryAST; mode: ProcessingMode.SQL; }; @@ -52,11 +51,11 @@ export async function queryToTypeDeclarations( let queryData; let queryName; if (parsedQuery.mode === ProcessingMode.TS) { - queryName = parsedQuery.name; - queryData = processQueryString(parsedQuery.body); + queryName = parsedQuery.ast.name; + queryData = processTSQueryAST(parsedQuery.ast); } else { queryName = parsedQuery.ast.name; - queryData = processQueryAST(parsedQuery.ast); + queryData = processSQLQueryAST(parsedQuery.ast); } const typeData = await getTypes(queryData, queryName, connection); @@ -158,11 +157,20 @@ export async function queryToTypeDeclarations( ); } -interface ITypedQuery { +type ITypedQuery = { + mode: 'ts'; + fileName: string; + query: { + name: string; + ast: TSQueryAST; + }; + typeDeclaration: string; +} | { + mode: 'sql'; fileName: string; - query?: { + query: { name: string; - ast: QueryAST; + ast: SQLQueryAST; paramTypeAlias: string; returnTypeAlias: string; }; @@ -177,55 +185,55 @@ async function generateTypedecsFromFile( types: TypeAllocator = new TypeAllocator(DefaultTypeMapping), ): Promise { const results: ITypedQuery[] = []; - if (mode === 'ts') { - const queries = parseTypeScriptFile(contents, fileName); - for (const query of queries) { + + const { queries, events } = mode === 'ts' ? parseTypeScriptFile(contents, fileName) : parseSQLFile(contents, fileName); + if (events.length > 0) { + prettyPrintEvents(contents, events); + if (events.find((e) => 'critical' in e)) { + return results; + } + } + for (const queryAST of queries) { + let typedQuery: ITypedQuery; + if (mode === 'sql') { + const sqlQueryAST = queryAST as SQLQueryAST; const result = await queryToTypeDeclarations( - { - body: query.tagContent, - name: query.queryName, - mode: ProcessingMode.TS, - }, + { ast: sqlQueryAST, mode: ProcessingMode.SQL }, connection, types, ); - const typedQuery = { + typedQuery = { + mode: 'sql' as const, + query: { + name: camelCase(sqlQueryAST.name), + ast: sqlQueryAST, + paramTypeAlias: `I${pascalCase(sqlQueryAST.name)}Params`, + returnTypeAlias: `I${pascalCase(sqlQueryAST.name)}Result`, + }, fileName, - queryName: query.queryName, typeDeclaration: result, }; - results.push(typedQuery); - } - } else { - const { - parseTree: { queries }, - events, - } = parseSQLFile(contents, fileName); - if (events.length > 0) { - prettyPrintEvents(contents, events); - if (events.find((e) => 'critical' in e)) { - return results; - } - } - for (const query of queries) { + } else { + const tsQueryAST = queryAST as TSQueryAST; const result = await queryToTypeDeclarations( - { ast: query, mode: ProcessingMode.SQL }, + { + ast: tsQueryAST, + mode: ProcessingMode.TS, + }, connection, types, ); - const typedQuery = { + typedQuery = { + mode: 'ts' as const, + fileName, query: { - name: camelCase(query.name), - ast: query, - paramTypeAlias: `I${pascalCase(query.name)}Params`, - returnTypeAlias: `I${pascalCase(query.name)}Result`, + name: tsQueryAST.name, + ast: tsQueryAST, }, - fileName, - queryName: query.name, typeDeclaration: result, }; - results.push(typedQuery); } + results.push(typedQuery); } return results; } @@ -253,7 +261,7 @@ export async function generateDeclarationFile( declarationFileContents += '\n'; for (const typeDec of typeDecs) { declarationFileContents += typeDec.typeDeclaration; - if (!typeDec.query) { + if (typeDec.mode === 'ts') { continue; } const queryPP = typeDec.query.ast.statement.body diff --git a/packages/query/package.json b/packages/query/package.json index d4b5c5e4..fe9fbbed 100644 --- a/packages/query/package.json +++ b/packages/query/package.json @@ -12,7 +12,8 @@ "build": "tsc --declaration", "check": "tsc --noEmit", "watch": "tsc --declaration --watch --preserveWatchOutput", - "parsegen": "antlr4ts -visitor -Xexact-output-dir -o src/loader/sql/parser src/loader/sql/grammar/*.g4" + "parsegen-sql": "antlr4ts -visitor -Xexact-output-dir -o src/loader/sql/parser src/loader/sql/grammar/*.g4", + "parsegen-ts": "antlr4ts -visitor -Xexact-output-dir -o src/loader/typescript/parser src/loader/typescript/grammar/*.g4" }, "jest": { "roots": [ diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts index b94ca250..9e183de0 100644 --- a/packages/query/src/index.ts +++ b/packages/query/src/index.ts @@ -4,17 +4,17 @@ export { ParamTransform, IQueryParameters, IInterpolatedQuery, - processQueryAST, - processQueryString, + processSQLQueryAST, + processTSQueryAST, } from './preprocessor'; export { AsyncQueue } from '@pgtyped/wire'; -export { default as parseTypeScriptFile } from './loader/typescript'; +export { default as parseTypeScriptFile, TSQueryAST } from './loader/typescript'; export { default as parseSQLFile, - Query as QueryAST, + SQLQueryAST, prettyPrintEvents, } from './loader/sql'; diff --git a/packages/query/src/loader/sql/grammar/SQLParser.g4 b/packages/query/src/loader/sql/grammar/SQLParser.g4 index 1f761e9e..4ffc59b2 100644 --- a/packages/query/src/loader/sql/grammar/SQLParser.g4 +++ b/packages/query/src/loader/sql/grammar/SQLParser.g4 @@ -1,20 +1,3 @@ -/* --- @name GetAllUsers --- @param userNames -> (...) --- @param user -> (name,age) --- @param users -> ((name,age)...) -select * from $userNames; -select * from $books $filterById; -*/ - - -/* - @name GetAllUsers - @param userNames -> (...) - @param user -> (name,age) - @param users -> ((name,age)...) -*/ - parser grammar SQLParser; options { tokenVocab = SQLLexer; } diff --git a/packages/query/src/loader/sql/index.ts b/packages/query/src/loader/sql/index.ts index 68d2dc84..66e1bcd7 100644 --- a/packages/query/src/loader/sql/index.ts +++ b/packages/query/src/loader/sql/index.ts @@ -212,10 +212,12 @@ class ParseListener implements SQLParserListener { } } +export type SQLParseResult = { queries: Query[]; events: ParseEvent[] }; + function parseText( text: string, fileName: string = 'undefined.sql', -): { parseTree: ParseTree; events: ParseEvent[] } { +): SQLParseResult { const logger = new Logger(text); const inputStream = CharStreams.fromString(text); const lexer = new SQLLexer(inputStream); @@ -231,10 +233,11 @@ function parseText( const listener = new ParseListener(logger); ParseTreeWalker.DEFAULT.walk(listener as SQLParserListener, tree); return { - parseTree: listener.parseTree, + queries: listener.parseTree.queries, events: logger.parseEvents, }; } export { prettyPrintEvents } from './logger'; +export type SQLQueryAST = Query; export default parseText; diff --git a/packages/query/src/loader/typescript/__snapshots__/query.test.ts.snap b/packages/query/src/loader/typescript/__snapshots__/query.test.ts.snap new file mode 100644 index 00000000..df7a70df --- /dev/null +++ b/packages/query/src/loader/typescript/__snapshots__/query.test.ts.snap @@ -0,0 +1,115 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`array param 1`] = ` +Object { + "events": Array [], + "query": Object { + "params": Array [ + Object { + "location": Object { + "a": 34, + "b": 36, + "col": 34, + "line": 1, + }, + "name": "ids", + "selection": Object { + "type": "scalar_array", + }, + }, + ], + "text": "select * from users where id in $$ids", + }, +} +`; + +exports[`array spread param 1`] = ` +Object { + "events": Array [], + "query": Object { + "params": Array [ + Object { + "location": Object { + "a": 72, + "b": 80, + "col": 11, + "line": 2, + }, + "name": "customers", + "selection": Object { + "keys": Array [ + "customerName", + "contactName", + "address", + ], + "type": "object_array", + }, + }, + ], + "text": "INSERT INTO customers (customer_name, contact_name, address) + VALUES $$customers(customerName, contactName, address)", + }, +} +`; + +exports[`pick param 1`] = ` +Object { + "events": Array [], + "query": Object { + "params": Array [ + Object { + "location": Object { + "a": 33, + "b": 43, + "col": 33, + "line": 1, + }, + "name": "activeUsers", + "selection": Object { + "keys": Array [ + "userOne", + "userTwo", + ], + "type": "object", + }, + }, + ], + "text": "select * from users where id in $activeUsers(userOne, userTwo)", + }, +} +`; + +exports[`scalar param 1`] = ` +Object { + "events": Array [], + "query": Object { + "params": Array [ + Object { + "location": Object { + "a": 32, + "b": 33, + "col": 32, + "line": 1, + }, + "name": "id", + "selection": Object { + "type": "scalar", + }, + }, + Object { + "location": Object { + "a": 47, + "b": 51, + "col": 47, + "line": 1, + }, + "name": "title", + "selection": Object { + "type": "scalar", + }, + }, + ], + "text": "select * from users where id = $id and title= $title", + }, +} +`; diff --git a/packages/query/src/loader/typescript/grammar/QueryLexer.g4 b/packages/query/src/loader/typescript/grammar/QueryLexer.g4 new file mode 100644 index 00000000..7e506e95 --- /dev/null +++ b/packages/query/src/loader/typescript/grammar/QueryLexer.g4 @@ -0,0 +1,28 @@ +/* +-- @name GetAllUsers +-- @param userNames -> (...) +-- @param user -> (name,age) +-- @param users -> ((name,age)...) +select * from $userNames; +select * from $books $filterById; +*/ + +lexer grammar QueryLexer; + +tokens { ID } + +fragment QUOT: '\''; +fragment ID: [a-zA-Z][a-zA-Z_0-9]*; + +SID: ID -> type(ID); +SINGULAR_PARAM_MARK: '$'; +PLURAL_PARAM_MARK: '$$'; +COMMA: ','; +OB: '('; +CB: ')'; +WORD: [a-zA-Z_0-9]+; +SPECIAL: [\-+*/<>=~!@#%^&|`?${}.[\]":]+; +EOF_STATEMENT: ';'; +WSL : [ \t\r\n]+ -> skip; +// parse strings and recognize escaped quotes +STRING: QUOT (QUOT | .*? ~([\\]) QUOT); diff --git a/packages/query/src/loader/typescript/grammar/QueryParser.g4 b/packages/query/src/loader/typescript/grammar/QueryParser.g4 new file mode 100644 index 00000000..529beca9 --- /dev/null +++ b/packages/query/src/loader/typescript/grammar/QueryParser.g4 @@ -0,0 +1,32 @@ +parser grammar QueryParser; + +options { tokenVocab = QueryLexer; } + +input + : query EOF + ; + +query + : (ignored* param ignored*)* + ; + +ignored: (ID | WORD | STRING | COMMA | OB | CB | SPECIAL)+; + +param + : pickParam + | arrayPickParam + | scalarParam + | arrayParam + ; + +scalarParam: SINGULAR_PARAM_MARK paramName; + +pickParam: SINGULAR_PARAM_MARK paramName OB pickKey (COMMA pickKey)* COMMA? CB; + +arrayPickParam: PLURAL_PARAM_MARK paramName OB pickKey (COMMA pickKey)* COMMA? CB; + +arrayParam: PLURAL_PARAM_MARK paramName; + +paramName: ID; + +pickKey: ID; diff --git a/packages/query/src/loader/typescript/index.ts b/packages/query/src/loader/typescript/index.ts index dbee9297..550fa6c1 100644 --- a/packages/query/src/loader/typescript/index.ts +++ b/packages/query/src/loader/typescript/index.ts @@ -1,12 +1,17 @@ import ts from 'typescript'; +import parseQuery, { Query } from "./query"; +import { ParseEvent } from "../sql/logger"; + +export type TSQueryAST = Query; interface INode { queryName: string; - tagName: string; - tagContent: string; + queryText: string; } -export function parseFile(sourceFile: ts.SourceFile): INode[] { +export type TSParseResult = { queries: TSQueryAST[]; events: ParseEvent[] }; + +export function parseFile(sourceFile: ts.SourceFile): TSParseResult { const foundNodes: INode[] = []; parseNode(sourceFile); @@ -15,22 +20,31 @@ export function parseFile(sourceFile: ts.SourceFile): INode[] { const queryName = node.parent.getChildren()[0].getText(); const taggedTemplateNode = node as ts.TaggedTemplateExpression; const tagName = taggedTemplateNode.tag.getText(); - const tagContent = taggedTemplateNode.template + const queryText = taggedTemplateNode.template .getText() .replace('\n', '') .slice(1, -1) .trim(); - foundNodes.push({ - queryName, - tagName, - tagContent, - }); + if (tagName === 'sql') { + foundNodes.push({ + queryName, + queryText, + }); + } } ts.forEachChild(node, parseNode); } - return foundNodes; + const queries: TSQueryAST[] = []; + const events: ParseEvent[] = []; + for (const node of foundNodes) { + const {query, events: qEvents} = parseQuery(node.queryText, node.queryName); + queries.push(query); + events.push(...qEvents); + } + + return { queries, events }; } const parseCode = (fileContent: string, fileName = 'unnamed.ts') => { diff --git a/packages/query/src/loader/typescript/parser/QueryLexer.ts b/packages/query/src/loader/typescript/parser/QueryLexer.ts new file mode 100644 index 00000000..60d39330 --- /dev/null +++ b/packages/query/src/loader/typescript/parser/QueryLexer.ts @@ -0,0 +1,131 @@ +// Generated from src/loader/typescript/grammar/QueryLexer.g4 by ANTLR 4.7.3-SNAPSHOT + + +import { ATN } from "antlr4ts/atn/ATN"; +import { ATNDeserializer } from "antlr4ts/atn/ATNDeserializer"; +import { CharStream } from "antlr4ts/CharStream"; +import { Lexer } from "antlr4ts/Lexer"; +import { LexerATNSimulator } from "antlr4ts/atn/LexerATNSimulator"; +import { NotNull } from "antlr4ts/Decorators"; +import { Override } from "antlr4ts/Decorators"; +import { RuleContext } from "antlr4ts/RuleContext"; +import { Vocabulary } from "antlr4ts/Vocabulary"; +import { VocabularyImpl } from "antlr4ts/VocabularyImpl"; + +import * as Utils from "antlr4ts/misc/Utils"; + + +export class QueryLexer extends Lexer { + public static readonly ID = 1; + public static readonly SINGULAR_PARAM_MARK = 2; + public static readonly PLURAL_PARAM_MARK = 3; + public static readonly COMMA = 4; + public static readonly OB = 5; + public static readonly CB = 6; + public static readonly WORD = 7; + public static readonly SPECIAL = 8; + public static readonly EOF_STATEMENT = 9; + public static readonly WSL = 10; + public static readonly STRING = 11; + + // tslint:disable:no-trailing-whitespace + public static readonly channelNames: string[] = [ + "DEFAULT_TOKEN_CHANNEL", "HIDDEN", + ]; + + // tslint:disable:no-trailing-whitespace + public static readonly modeNames: string[] = [ + "DEFAULT_MODE", + ]; + + public static readonly ruleNames: string[] = [ + "QUOT", "ID", "SID", "SINGULAR_PARAM_MARK", "PLURAL_PARAM_MARK", "COMMA", + "OB", "CB", "WORD", "SPECIAL", "EOF_STATEMENT", "WSL", "STRING", + ]; + + private static readonly _LITERAL_NAMES: Array = [ + undefined, undefined, "'$'", "'$$'", "','", "'('", "')'", undefined, undefined, + "';'", + ]; + private static readonly _SYMBOLIC_NAMES: Array = [ + undefined, "ID", "SINGULAR_PARAM_MARK", "PLURAL_PARAM_MARK", "COMMA", + "OB", "CB", "WORD", "SPECIAL", "EOF_STATEMENT", "WSL", "STRING", + ]; + public static readonly VOCABULARY: Vocabulary = new VocabularyImpl(QueryLexer._LITERAL_NAMES, QueryLexer._SYMBOLIC_NAMES, []); + + // @Override + // @NotNull + public get vocabulary(): Vocabulary { + return QueryLexer.VOCABULARY; + } + // tslint:enable:no-trailing-whitespace + + + constructor(input: CharStream) { + super(input); + this._interp = new LexerATNSimulator(QueryLexer._ATN, this); + } + + // @Override + public get grammarFileName(): string { return "QueryLexer.g4"; } + + // @Override + public get ruleNames(): string[] { return QueryLexer.ruleNames; } + + // @Override + public get serializedATN(): string { return QueryLexer._serializedATN; } + + // @Override + public get channelNames(): string[] { return QueryLexer.channelNames; } + + // @Override + public get modeNames(): string[] { return QueryLexer.modeNames; } + + public static readonly _serializedATN: string = + "\x03\uC91D\uCABA\u058D\uAFBA\u4F53\u0607\uEA8B\uC241\x02\rT\b\x01\x04" + + "\x02\t\x02\x04\x03\t\x03\x04\x04\t\x04\x04\x05\t\x05\x04\x06\t\x06\x04" + + "\x07\t\x07\x04\b\t\b\x04\t\t\t\x04\n\t\n\x04\v\t\v\x04\f\t\f\x04\r\t\r" + + "\x04\x0E\t\x0E\x03\x02\x03\x02\x03\x03\x03\x03\x07\x03\"\n\x03\f\x03\x0E" + + "\x03%\v\x03\x03\x04\x03\x04\x03\x04\x03\x04\x03\x05\x03\x05\x03\x06\x03" + + "\x06\x03\x06\x03\x07\x03\x07\x03\b\x03\b\x03\t\x03\t\x03\n\x06\n7\n\n" + + "\r\n\x0E\n8\x03\v\x06\v<\n\v\r\v\x0E\v=\x03\f\x03\f\x03\r\x06\rC\n\r\r" + + "\r\x0E\rD\x03\r\x03\r\x03\x0E\x03\x0E\x03\x0E\x07\x0EL\n\x0E\f\x0E\x0E" + + "\x0EO\v\x0E\x03\x0E\x03\x0E\x05\x0ES\n\x0E\x03M\x02\x02\x0F\x03\x02\x02" + + "\x05\x02\x03\x07\x02\x02\t\x02\x04\v\x02\x05\r\x02\x06\x0F\x02\x07\x11" + + "\x02\b\x13\x02\t\x15\x02\n\x17\x02\v\x19\x02\f\x1B\x02\r\x03\x02\x07\x04" + + "\x02C\\c|\x06\x022;C\\aac|\v\x02#(,-/1<<>B]]_`bb}\x80\x05\x02\v\f\x0F" + + "\x0F\"\"\x03\x02^^\x02W\x02\x07\x03\x02\x02\x02\x02\t\x03\x02\x02\x02" + + "\x02\v\x03\x02\x02\x02\x02\r\x03\x02\x02\x02\x02\x0F\x03\x02\x02\x02\x02" + + "\x11\x03\x02\x02\x02\x02\x13\x03\x02\x02\x02\x02\x15\x03\x02\x02\x02\x02" + + "\x17\x03\x02\x02\x02\x02\x19\x03\x02\x02\x02\x02\x1B\x03\x02\x02\x02\x03" + + "\x1D\x03\x02\x02\x02\x05\x1F\x03\x02\x02\x02\x07&\x03\x02\x02\x02\t*\x03" + + "\x02\x02\x02\v,\x03\x02\x02\x02\r/\x03\x02\x02\x02\x0F1\x03\x02\x02\x02" + + "\x113\x03\x02\x02\x02\x136\x03\x02\x02\x02\x15;\x03\x02\x02\x02\x17?\x03" + + "\x02\x02\x02\x19B\x03\x02\x02\x02\x1BH\x03\x02\x02\x02\x1D\x1E\x07)\x02" + + "\x02\x1E\x04\x03\x02\x02\x02\x1F#\t\x02\x02\x02 \"\t\x03\x02\x02! \x03" + + "\x02\x02\x02\"%\x03\x02\x02\x02#!\x03\x02\x02\x02#$\x03\x02\x02\x02$\x06" + + "\x03\x02\x02\x02%#\x03\x02\x02\x02&\'\x05\x05\x03\x02\'(\x03\x02\x02\x02" + + "()\b\x04\x02\x02)\b\x03\x02\x02\x02*+\x07&\x02\x02+\n\x03\x02\x02\x02" + + ",-\x07&\x02\x02-.\x07&\x02\x02.\f\x03\x02\x02\x02/0\x07.\x02\x020\x0E" + + "\x03\x02\x02\x0212\x07*\x02\x022\x10\x03\x02\x02\x0234\x07+\x02\x024\x12" + + "\x03\x02\x02\x0257\t\x03\x02\x0265\x03\x02\x02\x0278\x03\x02\x02\x028" + + "6\x03\x02\x02\x0289\x03\x02\x02\x029\x14\x03\x02\x02\x02:<\t\x04\x02\x02" + + ";:\x03\x02\x02\x02<=\x03\x02\x02\x02=;\x03\x02\x02\x02=>\x03\x02\x02\x02" + + ">\x16\x03\x02\x02\x02?@\x07=\x02\x02@\x18\x03\x02\x02\x02AC\t\x05\x02" + + "\x02BA\x03\x02\x02\x02CD\x03\x02\x02\x02DB\x03\x02\x02\x02DE\x03\x02\x02" + + "\x02EF\x03\x02\x02\x02FG\b\r\x03\x02G\x1A\x03\x02\x02\x02HR\x05\x03\x02" + + "\x02IS\x05\x03\x02\x02JL\v\x02\x02\x02KJ\x03\x02\x02\x02LO\x03\x02\x02" + + "\x02MN\x03\x02\x02\x02MK\x03\x02\x02\x02NP\x03\x02\x02\x02OM\x03\x02\x02" + + "\x02PQ\n\x06\x02\x02QS\x05\x03\x02\x02RI\x03\x02\x02\x02RM\x03\x02\x02" + + "\x02S\x1C\x03\x02\x02\x02\t\x02#8=DMR\x04\t\x03\x02\b\x02\x02"; + public static __ATN: ATN; + public static get _ATN(): ATN { + if (!QueryLexer.__ATN) { + QueryLexer.__ATN = new ATNDeserializer().deserialize(Utils.toCharArray(QueryLexer._serializedATN)); + } + + return QueryLexer.__ATN; + } + +} + diff --git a/packages/query/src/loader/typescript/parser/QueryParser.ts b/packages/query/src/loader/typescript/parser/QueryParser.ts new file mode 100644 index 00000000..e8e077a8 --- /dev/null +++ b/packages/query/src/loader/typescript/parser/QueryParser.ts @@ -0,0 +1,1019 @@ +// Generated from src/loader/typescript/grammar/QueryParser.g4 by ANTLR 4.7.3-SNAPSHOT + + +import { ATN } from "antlr4ts/atn/ATN"; +import { ATNDeserializer } from "antlr4ts/atn/ATNDeserializer"; +import { FailedPredicateException } from "antlr4ts/FailedPredicateException"; +import { NotNull } from "antlr4ts/Decorators"; +import { NoViableAltException } from "antlr4ts/NoViableAltException"; +import { Override } from "antlr4ts/Decorators"; +import { Parser } from "antlr4ts/Parser"; +import { ParserRuleContext } from "antlr4ts/ParserRuleContext"; +import { ParserATNSimulator } from "antlr4ts/atn/ParserATNSimulator"; +import { ParseTreeListener } from "antlr4ts/tree/ParseTreeListener"; +import { ParseTreeVisitor } from "antlr4ts/tree/ParseTreeVisitor"; +import { RecognitionException } from "antlr4ts/RecognitionException"; +import { RuleContext } from "antlr4ts/RuleContext"; +//import { RuleVersion } from "antlr4ts/RuleVersion"; +import { TerminalNode } from "antlr4ts/tree/TerminalNode"; +import { Token } from "antlr4ts/Token"; +import { TokenStream } from "antlr4ts/TokenStream"; +import { Vocabulary } from "antlr4ts/Vocabulary"; +import { VocabularyImpl } from "antlr4ts/VocabularyImpl"; + +import * as Utils from "antlr4ts/misc/Utils"; + +import { QueryParserListener } from "./QueryParserListener"; +import { QueryParserVisitor } from "./QueryParserVisitor"; + + +export class QueryParser extends Parser { + public static readonly ID = 1; + public static readonly SINGULAR_PARAM_MARK = 2; + public static readonly PLURAL_PARAM_MARK = 3; + public static readonly COMMA = 4; + public static readonly OB = 5; + public static readonly CB = 6; + public static readonly WORD = 7; + public static readonly SPECIAL = 8; + public static readonly EOF_STATEMENT = 9; + public static readonly WSL = 10; + public static readonly STRING = 11; + public static readonly RULE_input = 0; + public static readonly RULE_query = 1; + public static readonly RULE_ignored = 2; + public static readonly RULE_param = 3; + public static readonly RULE_scalarParam = 4; + public static readonly RULE_pickParam = 5; + public static readonly RULE_arrayPickParam = 6; + public static readonly RULE_arrayParam = 7; + public static readonly RULE_paramName = 8; + public static readonly RULE_pickKey = 9; + // tslint:disable:no-trailing-whitespace + public static readonly ruleNames: string[] = [ + "input", "query", "ignored", "param", "scalarParam", "pickParam", "arrayPickParam", + "arrayParam", "paramName", "pickKey", + ]; + + private static readonly _LITERAL_NAMES: Array = [ + undefined, undefined, "'$'", "'$$'", "','", "'('", "')'", undefined, undefined, + "';'", + ]; + private static readonly _SYMBOLIC_NAMES: Array = [ + undefined, "ID", "SINGULAR_PARAM_MARK", "PLURAL_PARAM_MARK", "COMMA", + "OB", "CB", "WORD", "SPECIAL", "EOF_STATEMENT", "WSL", "STRING", + ]; + public static readonly VOCABULARY: Vocabulary = new VocabularyImpl(QueryParser._LITERAL_NAMES, QueryParser._SYMBOLIC_NAMES, []); + + // @Override + // @NotNull + public get vocabulary(): Vocabulary { + return QueryParser.VOCABULARY; + } + // tslint:enable:no-trailing-whitespace + + // @Override + public get grammarFileName(): string { return "QueryParser.g4"; } + + // @Override + public get ruleNames(): string[] { return QueryParser.ruleNames; } + + // @Override + public get serializedATN(): string { return QueryParser._serializedATN; } + + constructor(input: TokenStream) { + super(input); + this._interp = new ParserATNSimulator(QueryParser._ATN, this); + } + // @RuleVersion(0) + public input(): InputContext { + let _localctx: InputContext = new InputContext(this._ctx, this.state); + this.enterRule(_localctx, 0, QueryParser.RULE_input); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 20; + this.query(); + this.state = 21; + this.match(QueryParser.EOF); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public query(): QueryContext { + let _localctx: QueryContext = new QueryContext(this._ctx, this.state); + this.enterRule(_localctx, 2, QueryParser.RULE_query); + let _la: number; + try { + let _alt: number; + this.enterOuterAlt(_localctx, 1); + { + this.state = 38; + this._errHandler.sync(this); + _la = this._input.LA(1); + while ((((_la) & ~0x1F) === 0 && ((1 << _la) & ((1 << QueryParser.ID) | (1 << QueryParser.SINGULAR_PARAM_MARK) | (1 << QueryParser.PLURAL_PARAM_MARK) | (1 << QueryParser.COMMA) | (1 << QueryParser.OB) | (1 << QueryParser.CB) | (1 << QueryParser.WORD) | (1 << QueryParser.SPECIAL) | (1 << QueryParser.STRING))) !== 0)) { + { + { + this.state = 26; + this._errHandler.sync(this); + _la = this._input.LA(1); + while ((((_la) & ~0x1F) === 0 && ((1 << _la) & ((1 << QueryParser.ID) | (1 << QueryParser.COMMA) | (1 << QueryParser.OB) | (1 << QueryParser.CB) | (1 << QueryParser.WORD) | (1 << QueryParser.SPECIAL) | (1 << QueryParser.STRING))) !== 0)) { + { + { + this.state = 23; + this.ignored(); + } + } + this.state = 28; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + this.state = 29; + this.param(); + this.state = 33; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 1, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 30; + this.ignored(); + } + } + } + this.state = 35; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 1, this._ctx); + } + } + } + this.state = 40; + this._errHandler.sync(this); + _la = this._input.LA(1); + } + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public ignored(): IgnoredContext { + let _localctx: IgnoredContext = new IgnoredContext(this._ctx, this.state); + this.enterRule(_localctx, 4, QueryParser.RULE_ignored); + let _la: number; + try { + let _alt: number; + this.enterOuterAlt(_localctx, 1); + { + this.state = 42; + this._errHandler.sync(this); + _alt = 1; + do { + switch (_alt) { + case 1: + { + { + this.state = 41; + _la = this._input.LA(1); + if (!((((_la) & ~0x1F) === 0 && ((1 << _la) & ((1 << QueryParser.ID) | (1 << QueryParser.COMMA) | (1 << QueryParser.OB) | (1 << QueryParser.CB) | (1 << QueryParser.WORD) | (1 << QueryParser.SPECIAL) | (1 << QueryParser.STRING))) !== 0))) { + this._errHandler.recoverInline(this); + } else { + if (this._input.LA(1) === Token.EOF) { + this.matchedEOF = true; + } + + this._errHandler.reportMatch(this); + this.consume(); + } + } + } + break; + default: + throw new NoViableAltException(this); + } + this.state = 44; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 3, this._ctx); + } while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public param(): ParamContext { + let _localctx: ParamContext = new ParamContext(this._ctx, this.state); + this.enterRule(_localctx, 6, QueryParser.RULE_param); + try { + this.state = 50; + this._errHandler.sync(this); + switch ( this.interpreter.adaptivePredict(this._input, 4, this._ctx) ) { + case 1: + this.enterOuterAlt(_localctx, 1); + { + this.state = 46; + this.pickParam(); + } + break; + + case 2: + this.enterOuterAlt(_localctx, 2); + { + this.state = 47; + this.arrayPickParam(); + } + break; + + case 3: + this.enterOuterAlt(_localctx, 3); + { + this.state = 48; + this.scalarParam(); + } + break; + + case 4: + this.enterOuterAlt(_localctx, 4); + { + this.state = 49; + this.arrayParam(); + } + break; + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public scalarParam(): ScalarParamContext { + let _localctx: ScalarParamContext = new ScalarParamContext(this._ctx, this.state); + this.enterRule(_localctx, 8, QueryParser.RULE_scalarParam); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 52; + this.match(QueryParser.SINGULAR_PARAM_MARK); + this.state = 53; + this.paramName(); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public pickParam(): PickParamContext { + let _localctx: PickParamContext = new PickParamContext(this._ctx, this.state); + this.enterRule(_localctx, 10, QueryParser.RULE_pickParam); + let _la: number; + try { + let _alt: number; + this.enterOuterAlt(_localctx, 1); + { + this.state = 55; + this.match(QueryParser.SINGULAR_PARAM_MARK); + this.state = 56; + this.paramName(); + this.state = 57; + this.match(QueryParser.OB); + this.state = 58; + this.pickKey(); + this.state = 63; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 5, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 59; + this.match(QueryParser.COMMA); + this.state = 60; + this.pickKey(); + } + } + } + this.state = 65; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 5, this._ctx); + } + this.state = 67; + this._errHandler.sync(this); + _la = this._input.LA(1); + if (_la === QueryParser.COMMA) { + { + this.state = 66; + this.match(QueryParser.COMMA); + } + } + + this.state = 69; + this.match(QueryParser.CB); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public arrayPickParam(): ArrayPickParamContext { + let _localctx: ArrayPickParamContext = new ArrayPickParamContext(this._ctx, this.state); + this.enterRule(_localctx, 12, QueryParser.RULE_arrayPickParam); + let _la: number; + try { + let _alt: number; + this.enterOuterAlt(_localctx, 1); + { + this.state = 71; + this.match(QueryParser.PLURAL_PARAM_MARK); + this.state = 72; + this.paramName(); + this.state = 73; + this.match(QueryParser.OB); + this.state = 74; + this.pickKey(); + this.state = 79; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 7, this._ctx); + while (_alt !== 2 && _alt !== ATN.INVALID_ALT_NUMBER) { + if (_alt === 1) { + { + { + this.state = 75; + this.match(QueryParser.COMMA); + this.state = 76; + this.pickKey(); + } + } + } + this.state = 81; + this._errHandler.sync(this); + _alt = this.interpreter.adaptivePredict(this._input, 7, this._ctx); + } + this.state = 83; + this._errHandler.sync(this); + _la = this._input.LA(1); + if (_la === QueryParser.COMMA) { + { + this.state = 82; + this.match(QueryParser.COMMA); + } + } + + this.state = 85; + this.match(QueryParser.CB); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public arrayParam(): ArrayParamContext { + let _localctx: ArrayParamContext = new ArrayParamContext(this._ctx, this.state); + this.enterRule(_localctx, 14, QueryParser.RULE_arrayParam); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 87; + this.match(QueryParser.PLURAL_PARAM_MARK); + this.state = 88; + this.paramName(); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public paramName(): ParamNameContext { + let _localctx: ParamNameContext = new ParamNameContext(this._ctx, this.state); + this.enterRule(_localctx, 16, QueryParser.RULE_paramName); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 90; + this.match(QueryParser.ID); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + // @RuleVersion(0) + public pickKey(): PickKeyContext { + let _localctx: PickKeyContext = new PickKeyContext(this._ctx, this.state); + this.enterRule(_localctx, 18, QueryParser.RULE_pickKey); + try { + this.enterOuterAlt(_localctx, 1); + { + this.state = 92; + this.match(QueryParser.ID); + } + } + catch (re) { + if (re instanceof RecognitionException) { + _localctx.exception = re; + this._errHandler.reportError(this, re); + this._errHandler.recover(this, re); + } else { + throw re; + } + } + finally { + this.exitRule(); + } + return _localctx; + } + + public static readonly _serializedATN: string = + "\x03\uC91D\uCABA\u058D\uAFBA\u4F53\u0607\uEA8B\uC241\x03\ra\x04\x02\t" + + "\x02\x04\x03\t\x03\x04\x04\t\x04\x04\x05\t\x05\x04\x06\t\x06\x04\x07\t" + + "\x07\x04\b\t\b\x04\t\t\t\x04\n\t\n\x04\v\t\v\x03\x02\x03\x02\x03\x02\x03" + + "\x03\x07\x03\x1B\n\x03\f\x03\x0E\x03\x1E\v\x03\x03\x03\x03\x03\x07\x03" + + "\"\n\x03\f\x03\x0E\x03%\v\x03\x07\x03\'\n\x03\f\x03\x0E\x03*\v\x03\x03" + + "\x04\x06\x04-\n\x04\r\x04\x0E\x04.\x03\x05\x03\x05\x03\x05\x03\x05\x05" + + "\x055\n\x05\x03\x06\x03\x06\x03\x06\x03\x07\x03\x07\x03\x07\x03\x07\x03" + + "\x07\x03\x07\x07\x07@\n\x07\f\x07\x0E\x07C\v\x07\x03\x07\x05\x07F\n\x07" + + "\x03\x07\x03\x07\x03\b\x03\b\x03\b\x03\b\x03\b\x03\b\x07\bP\n\b\f\b\x0E" + + "\bS\v\b\x03\b\x05\bV\n\b\x03\b\x03\b\x03\t\x03\t\x03\t\x03\n\x03\n\x03" + + "\v\x03\v\x03\v\x02\x02\x02\f\x02\x02\x04\x02\x06\x02\b\x02\n\x02\f\x02" + + "\x0E\x02\x10\x02\x12\x02\x14\x02\x02\x03\x05\x02\x03\x03\x06\n\r\r\x02" + + "a\x02\x16\x03\x02\x02\x02\x04(\x03\x02\x02\x02\x06,\x03\x02\x02\x02\b" + + "4\x03\x02\x02\x02\n6\x03\x02\x02\x02\f9\x03\x02\x02\x02\x0EI\x03\x02\x02" + + "\x02\x10Y\x03\x02\x02\x02\x12\\\x03\x02\x02\x02\x14^\x03\x02\x02\x02\x16" + + "\x17\x05\x04\x03\x02\x17\x18\x07\x02\x02\x03\x18\x03\x03\x02\x02\x02\x19" + + "\x1B\x05\x06\x04\x02\x1A\x19\x03\x02\x02\x02\x1B\x1E\x03\x02\x02\x02\x1C" + + "\x1A\x03\x02\x02\x02\x1C\x1D\x03\x02\x02\x02\x1D\x1F\x03\x02\x02\x02\x1E" + + "\x1C\x03\x02\x02\x02\x1F#\x05\b\x05\x02 \"\x05\x06\x04\x02! \x03\x02\x02" + + "\x02\"%\x03\x02\x02\x02#!\x03\x02\x02\x02#$\x03\x02\x02\x02$\'\x03\x02" + + "\x02\x02%#\x03\x02\x02\x02&\x1C\x03\x02\x02\x02\'*\x03\x02\x02\x02(&\x03" + + "\x02\x02\x02()\x03\x02\x02\x02)\x05\x03\x02\x02\x02*(\x03\x02\x02\x02" + + "+-\t\x02\x02\x02,+\x03\x02\x02\x02-.\x03\x02\x02\x02.,\x03\x02\x02\x02" + + "./\x03\x02\x02\x02/\x07\x03\x02\x02\x0205\x05\f\x07\x0215\x05\x0E\b\x02" + + "25\x05\n\x06\x0235\x05\x10\t\x0240\x03\x02\x02\x0241\x03\x02\x02\x024" + + "2\x03\x02\x02\x0243\x03\x02\x02\x025\t\x03\x02\x02\x0267\x07\x04\x02\x02" + + "78\x05\x12\n\x028\v\x03\x02\x02\x029:\x07\x04\x02\x02:;\x05\x12\n\x02" + + ";<\x07\x07\x02\x02\x07\x06\x02\x02>@\x05\x14\v\x02?" + + "=\x03\x02\x02\x02@C\x03\x02\x02\x02A?\x03\x02\x02\x02AB\x03\x02\x02\x02" + + "BE\x03\x02\x02\x02CA\x03\x02\x02\x02DF\x07\x06\x02\x02ED\x03\x02\x02\x02" + + "EF\x03\x02\x02\x02FG\x03\x02\x02\x02GH\x07\b\x02\x02H\r\x03\x02\x02\x02" + + "IJ\x07\x05\x02\x02JK\x05\x12\n\x02KL\x07\x07\x02\x02LQ\x05\x14\v\x02M" + + "N\x07\x06\x02\x02NP\x05\x14\v\x02OM\x03\x02\x02\x02PS\x03\x02\x02\x02" + + "QO\x03\x02\x02\x02QR\x03\x02\x02\x02RU\x03\x02\x02\x02SQ\x03\x02\x02\x02" + + "TV\x07\x06\x02\x02UT\x03\x02\x02\x02UV\x03\x02\x02\x02VW\x03\x02\x02\x02" + + "WX\x07\b\x02\x02X\x0F\x03\x02\x02\x02YZ\x07\x05\x02\x02Z[\x05\x12\n\x02" + + "[\x11\x03\x02\x02\x02\\]\x07\x03\x02\x02]\x13\x03\x02\x02\x02^_\x07\x03" + + "\x02\x02_\x15\x03\x02\x02\x02\v\x1C#(.4AEQU"; + public static __ATN: ATN; + public static get _ATN(): ATN { + if (!QueryParser.__ATN) { + QueryParser.__ATN = new ATNDeserializer().deserialize(Utils.toCharArray(QueryParser._serializedATN)); + } + + return QueryParser.__ATN; + } + +} + +export class InputContext extends ParserRuleContext { + public query(): QueryContext { + return this.getRuleContext(0, QueryContext); + } + public EOF(): TerminalNode { return this.getToken(QueryParser.EOF, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return QueryParser.RULE_input; } + // @Override + public enterRule(listener: QueryParserListener): void { + if (listener.enterInput) { + listener.enterInput(this); + } + } + // @Override + public exitRule(listener: QueryParserListener): void { + if (listener.exitInput) { + listener.exitInput(this); + } + } + // @Override + public accept(visitor: QueryParserVisitor): Result { + if (visitor.visitInput) { + return visitor.visitInput(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class QueryContext extends ParserRuleContext { + public param(): ParamContext[]; + public param(i: number): ParamContext; + public param(i?: number): ParamContext | ParamContext[] { + if (i === undefined) { + return this.getRuleContexts(ParamContext); + } else { + return this.getRuleContext(i, ParamContext); + } + } + public ignored(): IgnoredContext[]; + public ignored(i: number): IgnoredContext; + public ignored(i?: number): IgnoredContext | IgnoredContext[] { + if (i === undefined) { + return this.getRuleContexts(IgnoredContext); + } else { + return this.getRuleContext(i, IgnoredContext); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return QueryParser.RULE_query; } + // @Override + public enterRule(listener: QueryParserListener): void { + if (listener.enterQuery) { + listener.enterQuery(this); + } + } + // @Override + public exitRule(listener: QueryParserListener): void { + if (listener.exitQuery) { + listener.exitQuery(this); + } + } + // @Override + public accept(visitor: QueryParserVisitor): Result { + if (visitor.visitQuery) { + return visitor.visitQuery(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class IgnoredContext extends ParserRuleContext { + public ID(): TerminalNode[]; + public ID(i: number): TerminalNode; + public ID(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(QueryParser.ID); + } else { + return this.getToken(QueryParser.ID, i); + } + } + public WORD(): TerminalNode[]; + public WORD(i: number): TerminalNode; + public WORD(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(QueryParser.WORD); + } else { + return this.getToken(QueryParser.WORD, i); + } + } + public STRING(): TerminalNode[]; + public STRING(i: number): TerminalNode; + public STRING(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(QueryParser.STRING); + } else { + return this.getToken(QueryParser.STRING, i); + } + } + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(QueryParser.COMMA); + } else { + return this.getToken(QueryParser.COMMA, i); + } + } + public OB(): TerminalNode[]; + public OB(i: number): TerminalNode; + public OB(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(QueryParser.OB); + } else { + return this.getToken(QueryParser.OB, i); + } + } + public CB(): TerminalNode[]; + public CB(i: number): TerminalNode; + public CB(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(QueryParser.CB); + } else { + return this.getToken(QueryParser.CB, i); + } + } + public SPECIAL(): TerminalNode[]; + public SPECIAL(i: number): TerminalNode; + public SPECIAL(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(QueryParser.SPECIAL); + } else { + return this.getToken(QueryParser.SPECIAL, i); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return QueryParser.RULE_ignored; } + // @Override + public enterRule(listener: QueryParserListener): void { + if (listener.enterIgnored) { + listener.enterIgnored(this); + } + } + // @Override + public exitRule(listener: QueryParserListener): void { + if (listener.exitIgnored) { + listener.exitIgnored(this); + } + } + // @Override + public accept(visitor: QueryParserVisitor): Result { + if (visitor.visitIgnored) { + return visitor.visitIgnored(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class ParamContext extends ParserRuleContext { + public pickParam(): PickParamContext | undefined { + return this.tryGetRuleContext(0, PickParamContext); + } + public arrayPickParam(): ArrayPickParamContext | undefined { + return this.tryGetRuleContext(0, ArrayPickParamContext); + } + public scalarParam(): ScalarParamContext | undefined { + return this.tryGetRuleContext(0, ScalarParamContext); + } + public arrayParam(): ArrayParamContext | undefined { + return this.tryGetRuleContext(0, ArrayParamContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return QueryParser.RULE_param; } + // @Override + public enterRule(listener: QueryParserListener): void { + if (listener.enterParam) { + listener.enterParam(this); + } + } + // @Override + public exitRule(listener: QueryParserListener): void { + if (listener.exitParam) { + listener.exitParam(this); + } + } + // @Override + public accept(visitor: QueryParserVisitor): Result { + if (visitor.visitParam) { + return visitor.visitParam(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class ScalarParamContext extends ParserRuleContext { + public SINGULAR_PARAM_MARK(): TerminalNode { return this.getToken(QueryParser.SINGULAR_PARAM_MARK, 0); } + public paramName(): ParamNameContext { + return this.getRuleContext(0, ParamNameContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return QueryParser.RULE_scalarParam; } + // @Override + public enterRule(listener: QueryParserListener): void { + if (listener.enterScalarParam) { + listener.enterScalarParam(this); + } + } + // @Override + public exitRule(listener: QueryParserListener): void { + if (listener.exitScalarParam) { + listener.exitScalarParam(this); + } + } + // @Override + public accept(visitor: QueryParserVisitor): Result { + if (visitor.visitScalarParam) { + return visitor.visitScalarParam(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class PickParamContext extends ParserRuleContext { + public SINGULAR_PARAM_MARK(): TerminalNode { return this.getToken(QueryParser.SINGULAR_PARAM_MARK, 0); } + public paramName(): ParamNameContext { + return this.getRuleContext(0, ParamNameContext); + } + public OB(): TerminalNode { return this.getToken(QueryParser.OB, 0); } + public pickKey(): PickKeyContext[]; + public pickKey(i: number): PickKeyContext; + public pickKey(i?: number): PickKeyContext | PickKeyContext[] { + if (i === undefined) { + return this.getRuleContexts(PickKeyContext); + } else { + return this.getRuleContext(i, PickKeyContext); + } + } + public CB(): TerminalNode { return this.getToken(QueryParser.CB, 0); } + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(QueryParser.COMMA); + } else { + return this.getToken(QueryParser.COMMA, i); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return QueryParser.RULE_pickParam; } + // @Override + public enterRule(listener: QueryParserListener): void { + if (listener.enterPickParam) { + listener.enterPickParam(this); + } + } + // @Override + public exitRule(listener: QueryParserListener): void { + if (listener.exitPickParam) { + listener.exitPickParam(this); + } + } + // @Override + public accept(visitor: QueryParserVisitor): Result { + if (visitor.visitPickParam) { + return visitor.visitPickParam(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class ArrayPickParamContext extends ParserRuleContext { + public PLURAL_PARAM_MARK(): TerminalNode { return this.getToken(QueryParser.PLURAL_PARAM_MARK, 0); } + public paramName(): ParamNameContext { + return this.getRuleContext(0, ParamNameContext); + } + public OB(): TerminalNode { return this.getToken(QueryParser.OB, 0); } + public pickKey(): PickKeyContext[]; + public pickKey(i: number): PickKeyContext; + public pickKey(i?: number): PickKeyContext | PickKeyContext[] { + if (i === undefined) { + return this.getRuleContexts(PickKeyContext); + } else { + return this.getRuleContext(i, PickKeyContext); + } + } + public CB(): TerminalNode { return this.getToken(QueryParser.CB, 0); } + public COMMA(): TerminalNode[]; + public COMMA(i: number): TerminalNode; + public COMMA(i?: number): TerminalNode | TerminalNode[] { + if (i === undefined) { + return this.getTokens(QueryParser.COMMA); + } else { + return this.getToken(QueryParser.COMMA, i); + } + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return QueryParser.RULE_arrayPickParam; } + // @Override + public enterRule(listener: QueryParserListener): void { + if (listener.enterArrayPickParam) { + listener.enterArrayPickParam(this); + } + } + // @Override + public exitRule(listener: QueryParserListener): void { + if (listener.exitArrayPickParam) { + listener.exitArrayPickParam(this); + } + } + // @Override + public accept(visitor: QueryParserVisitor): Result { + if (visitor.visitArrayPickParam) { + return visitor.visitArrayPickParam(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class ArrayParamContext extends ParserRuleContext { + public PLURAL_PARAM_MARK(): TerminalNode { return this.getToken(QueryParser.PLURAL_PARAM_MARK, 0); } + public paramName(): ParamNameContext { + return this.getRuleContext(0, ParamNameContext); + } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return QueryParser.RULE_arrayParam; } + // @Override + public enterRule(listener: QueryParserListener): void { + if (listener.enterArrayParam) { + listener.enterArrayParam(this); + } + } + // @Override + public exitRule(listener: QueryParserListener): void { + if (listener.exitArrayParam) { + listener.exitArrayParam(this); + } + } + // @Override + public accept(visitor: QueryParserVisitor): Result { + if (visitor.visitArrayParam) { + return visitor.visitArrayParam(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class ParamNameContext extends ParserRuleContext { + public ID(): TerminalNode { return this.getToken(QueryParser.ID, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return QueryParser.RULE_paramName; } + // @Override + public enterRule(listener: QueryParserListener): void { + if (listener.enterParamName) { + listener.enterParamName(this); + } + } + // @Override + public exitRule(listener: QueryParserListener): void { + if (listener.exitParamName) { + listener.exitParamName(this); + } + } + // @Override + public accept(visitor: QueryParserVisitor): Result { + if (visitor.visitParamName) { + return visitor.visitParamName(this); + } else { + return visitor.visitChildren(this); + } + } +} + + +export class PickKeyContext extends ParserRuleContext { + public ID(): TerminalNode { return this.getToken(QueryParser.ID, 0); } + constructor(parent: ParserRuleContext | undefined, invokingState: number) { + super(parent, invokingState); + } + // @Override + public get ruleIndex(): number { return QueryParser.RULE_pickKey; } + // @Override + public enterRule(listener: QueryParserListener): void { + if (listener.enterPickKey) { + listener.enterPickKey(this); + } + } + // @Override + public exitRule(listener: QueryParserListener): void { + if (listener.exitPickKey) { + listener.exitPickKey(this); + } + } + // @Override + public accept(visitor: QueryParserVisitor): Result { + if (visitor.visitPickKey) { + return visitor.visitPickKey(this); + } else { + return visitor.visitChildren(this); + } + } +} + + diff --git a/packages/query/src/loader/typescript/parser/QueryParserListener.ts b/packages/query/src/loader/typescript/parser/QueryParserListener.ts new file mode 100644 index 00000000..3175fe91 --- /dev/null +++ b/packages/query/src/loader/typescript/parser/QueryParserListener.ts @@ -0,0 +1,133 @@ +// Generated from src/loader/typescript/grammar/QueryParser.g4 by ANTLR 4.7.3-SNAPSHOT + + +import { ParseTreeListener } from "antlr4ts/tree/ParseTreeListener"; + +import { InputContext } from "./QueryParser"; +import { QueryContext } from "./QueryParser"; +import { IgnoredContext } from "./QueryParser"; +import { ParamContext } from "./QueryParser"; +import { ScalarParamContext } from "./QueryParser"; +import { PickParamContext } from "./QueryParser"; +import { ArrayPickParamContext } from "./QueryParser"; +import { ArrayParamContext } from "./QueryParser"; +import { ParamNameContext } from "./QueryParser"; +import { PickKeyContext } from "./QueryParser"; + + +/** + * This interface defines a complete listener for a parse tree produced by + * `QueryParser`. + */ +export interface QueryParserListener extends ParseTreeListener { + /** + * Enter a parse tree produced by `QueryParser.input`. + * @param ctx the parse tree + */ + enterInput?: (ctx: InputContext) => void; + /** + * Exit a parse tree produced by `QueryParser.input`. + * @param ctx the parse tree + */ + exitInput?: (ctx: InputContext) => void; + + /** + * Enter a parse tree produced by `QueryParser.query`. + * @param ctx the parse tree + */ + enterQuery?: (ctx: QueryContext) => void; + /** + * Exit a parse tree produced by `QueryParser.query`. + * @param ctx the parse tree + */ + exitQuery?: (ctx: QueryContext) => void; + + /** + * Enter a parse tree produced by `QueryParser.ignored`. + * @param ctx the parse tree + */ + enterIgnored?: (ctx: IgnoredContext) => void; + /** + * Exit a parse tree produced by `QueryParser.ignored`. + * @param ctx the parse tree + */ + exitIgnored?: (ctx: IgnoredContext) => void; + + /** + * Enter a parse tree produced by `QueryParser.param`. + * @param ctx the parse tree + */ + enterParam?: (ctx: ParamContext) => void; + /** + * Exit a parse tree produced by `QueryParser.param`. + * @param ctx the parse tree + */ + exitParam?: (ctx: ParamContext) => void; + + /** + * Enter a parse tree produced by `QueryParser.scalarParam`. + * @param ctx the parse tree + */ + enterScalarParam?: (ctx: ScalarParamContext) => void; + /** + * Exit a parse tree produced by `QueryParser.scalarParam`. + * @param ctx the parse tree + */ + exitScalarParam?: (ctx: ScalarParamContext) => void; + + /** + * Enter a parse tree produced by `QueryParser.pickParam`. + * @param ctx the parse tree + */ + enterPickParam?: (ctx: PickParamContext) => void; + /** + * Exit a parse tree produced by `QueryParser.pickParam`. + * @param ctx the parse tree + */ + exitPickParam?: (ctx: PickParamContext) => void; + + /** + * Enter a parse tree produced by `QueryParser.arrayPickParam`. + * @param ctx the parse tree + */ + enterArrayPickParam?: (ctx: ArrayPickParamContext) => void; + /** + * Exit a parse tree produced by `QueryParser.arrayPickParam`. + * @param ctx the parse tree + */ + exitArrayPickParam?: (ctx: ArrayPickParamContext) => void; + + /** + * Enter a parse tree produced by `QueryParser.arrayParam`. + * @param ctx the parse tree + */ + enterArrayParam?: (ctx: ArrayParamContext) => void; + /** + * Exit a parse tree produced by `QueryParser.arrayParam`. + * @param ctx the parse tree + */ + exitArrayParam?: (ctx: ArrayParamContext) => void; + + /** + * Enter a parse tree produced by `QueryParser.paramName`. + * @param ctx the parse tree + */ + enterParamName?: (ctx: ParamNameContext) => void; + /** + * Exit a parse tree produced by `QueryParser.paramName`. + * @param ctx the parse tree + */ + exitParamName?: (ctx: ParamNameContext) => void; + + /** + * Enter a parse tree produced by `QueryParser.pickKey`. + * @param ctx the parse tree + */ + enterPickKey?: (ctx: PickKeyContext) => void; + /** + * Exit a parse tree produced by `QueryParser.pickKey`. + * @param ctx the parse tree + */ + exitPickKey?: (ctx: PickKeyContext) => void; +} + diff --git a/packages/query/src/loader/typescript/parser/QueryParserVisitor.ts b/packages/query/src/loader/typescript/parser/QueryParserVisitor.ts new file mode 100644 index 00000000..5b5ac0b7 --- /dev/null +++ b/packages/query/src/loader/typescript/parser/QueryParserVisitor.ts @@ -0,0 +1,96 @@ +// Generated from src/loader/typescript/grammar/QueryParser.g4 by ANTLR 4.7.3-SNAPSHOT + + +import { ParseTreeVisitor } from "antlr4ts/tree/ParseTreeVisitor"; + +import { InputContext } from "./QueryParser"; +import { QueryContext } from "./QueryParser"; +import { IgnoredContext } from "./QueryParser"; +import { ParamContext } from "./QueryParser"; +import { ScalarParamContext } from "./QueryParser"; +import { PickParamContext } from "./QueryParser"; +import { ArrayPickParamContext } from "./QueryParser"; +import { ArrayParamContext } from "./QueryParser"; +import { ParamNameContext } from "./QueryParser"; +import { PickKeyContext } from "./QueryParser"; + + +/** + * This interface defines a complete generic visitor for a parse tree produced + * by `QueryParser`. + * + * @param The return type of the visit operation. Use `void` for + * operations with no return type. + */ +export interface QueryParserVisitor extends ParseTreeVisitor { + /** + * Visit a parse tree produced by `QueryParser.input`. + * @param ctx the parse tree + * @return the visitor result + */ + visitInput?: (ctx: InputContext) => Result; + + /** + * Visit a parse tree produced by `QueryParser.query`. + * @param ctx the parse tree + * @return the visitor result + */ + visitQuery?: (ctx: QueryContext) => Result; + + /** + * Visit a parse tree produced by `QueryParser.ignored`. + * @param ctx the parse tree + * @return the visitor result + */ + visitIgnored?: (ctx: IgnoredContext) => Result; + + /** + * Visit a parse tree produced by `QueryParser.param`. + * @param ctx the parse tree + * @return the visitor result + */ + visitParam?: (ctx: ParamContext) => Result; + + /** + * Visit a parse tree produced by `QueryParser.scalarParam`. + * @param ctx the parse tree + * @return the visitor result + */ + visitScalarParam?: (ctx: ScalarParamContext) => Result; + + /** + * Visit a parse tree produced by `QueryParser.pickParam`. + * @param ctx the parse tree + * @return the visitor result + */ + visitPickParam?: (ctx: PickParamContext) => Result; + + /** + * Visit a parse tree produced by `QueryParser.arrayPickParam`. + * @param ctx the parse tree + * @return the visitor result + */ + visitArrayPickParam?: (ctx: ArrayPickParamContext) => Result; + + /** + * Visit a parse tree produced by `QueryParser.arrayParam`. + * @param ctx the parse tree + * @return the visitor result + */ + visitArrayParam?: (ctx: ArrayParamContext) => Result; + + /** + * Visit a parse tree produced by `QueryParser.paramName`. + * @param ctx the parse tree + * @return the visitor result + */ + visitParamName?: (ctx: ParamNameContext) => Result; + + /** + * Visit a parse tree produced by `QueryParser.pickKey`. + * @param ctx the parse tree + * @return the visitor result + */ + visitPickKey?: (ctx: PickKeyContext) => Result; +} + diff --git a/packages/query/src/loader/typescript/query.test.ts b/packages/query/src/loader/typescript/query.test.ts new file mode 100644 index 00000000..385f1b0a --- /dev/null +++ b/packages/query/src/loader/typescript/query.test.ts @@ -0,0 +1,30 @@ +import parse from "./query"; + +test("scalar param", () => { + const query = `select * from users where id = $id and title= $title`; + + const result = parse(query); + expect(result).toMatchSnapshot(); +}); + +test("pick param", () => { + const query = `select * from users where id in $activeUsers(userOne, userTwo)`; + + const result = parse(query); + expect(result).toMatchSnapshot(); +}); + +test("array param", () => { + const query = `select * from users where id in $$ids`; + + const result = parse(query); + expect(result).toMatchSnapshot(); +}); + +test("array spread param", () => { + const query = `INSERT INTO customers (customer_name, contact_name, address) + VALUES $$customers(customerName, contactName, address)`; + + const result = parse(query); + expect(result).toMatchSnapshot(); +}); diff --git a/packages/query/src/loader/typescript/query.ts b/packages/query/src/loader/typescript/query.ts new file mode 100644 index 00000000..13d2453e --- /dev/null +++ b/packages/query/src/loader/typescript/query.ts @@ -0,0 +1,177 @@ +import { QueryParserListener } from './parser/QueryParserListener'; +import { CharStreams, CommonTokenStream } from 'antlr4ts'; +import { ParseTreeWalker } from 'antlr4ts/tree/ParseTreeWalker'; +import { QueryLexer } from './parser/QueryLexer'; +import { + ParamNameContext, PickKeyContext, QueryContext, + QueryParser +} from "./parser/QueryParser"; +import { Logger, ParseEvent, ParseEventType, ParseWarningType } from '../sql/logger'; +import { Interval } from 'antlr4ts/misc'; + +export enum ParamType { + Scalar = 'scalar', + Object = 'object', + ScalarArray = 'scalar_array', + ObjectArray = 'object_array', +} + +export type ParamSelection = + | { + type: ParamType.Scalar; +} | { + type: ParamType.ScalarArray; +} + | { + type: + | ParamType.Object + | ParamType.ObjectArray; + keys: string[]; +}; + +export interface Param { + name: string; + selection: ParamSelection; + location: CodeInterval; +} + +interface CodeInterval { + a: number; + b: number; + line: number; + col: number; +} + +export interface Query { + name: string; + params: Param[]; + text: string; +} + +export function assert(condition: any): asserts condition { + if (!condition) { + throw new Error('Assertion Failed'); + } +} + +class ParseListener implements QueryParserListener { + logger: Logger; + query: Partial = {}; + private currentParam: Partial = {}; + private currentSelection: Partial = {}; + + constructor(queryName: string, logger: Logger) { + this.query.name = queryName; + this.logger = logger; + } + + exitQuery() { +/* + const currentQuery = this.currentQuery as Query; + currentQuery.params.forEach((p) => { + const paramUsed = p.name in currentQuery.usedParamSet; + if (!paramUsed) { + this.logger.logEvent({ + type: ParseEventType.Warning, + message: { + type: ParseWarningType.ParamNeverUsed, + text: `Parameter "${p.name}" is defined but never used`, + }, + location: p.codeRefs.defined, + }); + } + }); + this.parseTree.queries.push(currentQuery); +*/ + } + + enterQuery(ctx: QueryContext) { + const { inputStream } = ctx.start; + const a = ctx.start.startIndex; + const b = ctx.stop!.stopIndex; + const interval = new Interval(a, b); + const text = inputStream!.getText(interval); + this.query = { + text, + params: [], + }; + } + + enterParamName(ctx: ParamNameContext) { + const defLoc = { + a: ctx.start.startIndex, + b: ctx.start.stopIndex, + line: ctx.start.line, + col: ctx.start.charPositionInLine, + }; + this.currentParam = { + name: ctx.text, + location: defLoc, + selection: undefined, + }; + } + + exitParam() { + this.currentParam.selection = this.currentSelection as ParamSelection; + this.query.params!.push(this.currentParam as Param); + this.currentSelection = {}; + this.currentParam = {}; + } + + enterScalarParam() { + this.currentSelection = { + type: ParamType.Scalar, + }; + } + + enterPickParam() { + this.currentSelection = { + type: ParamType.Object, + keys: [], + }; + } + + enterArrayPickParam() { + this.currentSelection = { + type: ParamType.ObjectArray, + keys: [], + }; + } + + enterArrayParam() { + this.currentSelection = { + type: ParamType.ScalarArray, + }; + } + + enterPickKey(ctx: PickKeyContext) { + assert('keys' in this.currentSelection); + this.currentSelection.keys!.push(ctx.text); + } +} + +function parseText( + text: string, + queryName: string = 'query', +): { query: Query; events: ParseEvent[] } { + const logger = new Logger(text); + const inputStream = CharStreams.fromString(text); + const lexer = new QueryLexer(inputStream); + lexer.removeErrorListeners(); + lexer.addErrorListener(logger); + const tokenStream = new CommonTokenStream(lexer); + const parser = new QueryParser(tokenStream); + parser.removeErrorListeners(); + parser.addErrorListener(logger); + + const tree = parser.input(); + + const listener = new ParseListener(queryName, logger); + ParseTreeWalker.DEFAULT.walk(listener as QueryParserListener, tree); + return { + query: listener.query as any, + events: logger.parseEvents, + }; +} + +export default parseText; diff --git a/packages/query/src/preprocessor.test.ts b/packages/query/src/preprocessor.test.ts index 1c10782c..3940510d 100644 --- a/packages/query/src/preprocessor.test.ts +++ b/packages/query/src/preprocessor.test.ts @@ -1,12 +1,14 @@ import { ParamTransform, - processQueryAST, - processQueryString, + processSQLQueryAST, + processTSQueryAST, } from './preprocessor'; -import parseText from './loader/sql'; +import parseSQLQuery from './loader/sql'; +import parseTSQuery from './loader/typescript'; -test('name parameter interpolation', () => { +test('(TS) name parameter interpolation', () => { const query = 'SELECT id, name from users where id = $id and age > $age'; + const parsedQuery = parseTSQuery(query); const parameters = { id: '123', age: 12, @@ -18,13 +20,14 @@ test('name parameter interpolation', () => { bindings: ['123', 12], }; - const result = processQueryString(query, parameters); + const result = processTSQueryAST(parsedQuery.queries[0], parameters); expect(result).toEqual(expectedResult); }); test('(TS) scalar param used twice', () => { const query = 'SELECT id, name from users where id = $id and parent_id = $id'; + const parsedQuery = parseTSQuery(query); const parameters = { id: '123', }; @@ -35,13 +38,14 @@ test('(TS) scalar param used twice', () => { bindings: ['123'], }; - const result = processQueryString(query, parameters); + const result = processTSQueryAST(parsedQuery.queries[0], parameters); expect(result).toEqual(expectedResult); }); -test('name parameter mapping', () => { +test('(TS) name parameter mapping', () => { const query = 'SELECT id, name from users where id = $id and age > $age'; + const parsedQuery = parseTSQuery(query); const expectedResult = { query: 'SELECT id, name from users where id = $1 and age > $2', @@ -60,14 +64,15 @@ test('name parameter mapping', () => { bindings: [], }; - const result = processQueryString(query); + const result = processTSQueryAST(parsedQuery.queries[0]); expect(result).toEqual(expectedResult); }); -test('single value list parameter interpolation', () => { +test('(TS) single value list parameter interpolation', () => { const query = 'INSERT INTO users (name, age) VALUES $user(name, age) RETURNING id'; + const parsedQuery = parseTSQuery(query); const parameters = { user: { @@ -99,14 +104,15 @@ test('single value list parameter interpolation', () => { bindings: [], }; - const result = processQueryString(query); + const result = processTSQueryAST(parsedQuery.queries[0]); expect(result).toEqual(expectedResult); }); -test('multiple value list (array) parameter mapping', () => { +test('(TS) multiple value list (array) parameter mapping', () => { const query = 'SELECT FROM users where (age in $$ages) or (age in $$otherAges)'; + const parsedQuery = parseTSQuery(query); const expectedResult = { query: 'SELECT FROM users where (age in ($1)) or (age in ($2))', @@ -125,13 +131,14 @@ test('multiple value list (array) parameter mapping', () => { bindings: [], }; - const result = processQueryString(query); + const result = processTSQueryAST(parsedQuery.queries[0]); expect(result).toEqual(expectedResult); }); -test('multiple value list (array) parameter interpolation', () => { +test('(TS) multiple value list (array) parameter interpolation', () => { const query = 'SELECT FROM users where age in $$ages'; + const parsedQuery = parseTSQuery(query); const parameters = { ages: [23, 27, 50], @@ -143,13 +150,14 @@ test('multiple value list (array) parameter interpolation', () => { mapping: [], }; - const result = processQueryString(query, parameters); + const result = processTSQueryAST(parsedQuery.queries[0], parameters); expect(result).toEqual(expectedResult); }); -test('multiple value list (array) parameter used twice interpolation', () => { +test('(TS) multiple value list (array) parameter used twice interpolation', () => { const query = 'SELECT FROM users where age in $$ages or age in $$ages'; + const parsedQuery = parseTSQuery(query); const parameters = { ages: [23, 27, 50], @@ -161,14 +169,15 @@ test('multiple value list (array) parameter used twice interpolation', () => { mapping: [], }; - const result = processQueryString(query, parameters); + const result = processTSQueryAST(parsedQuery.queries[0], parameters); expect(result).toEqual(expectedResult); }); -test('multiple value list parameter mapping', () => { +test('(TS) multiple value list parameter mapping', () => { const query = 'INSERT INTO users (name, age) VALUES $$users(name, age) RETURNING id'; + const parsedQuery = parseTSQuery(query); const expectedResult = { query: 'INSERT INTO users (name, age) VALUES ($1, $2) RETURNING id', @@ -193,14 +202,15 @@ test('multiple value list parameter mapping', () => { ], }; - const result = processQueryString(query); + const result = processTSQueryAST(parsedQuery.queries[0]); expect(result).toEqual(expectedResult); }); -test('multiple value list parameter interpolation', () => { +test('(TS) multiple value list parameter interpolation', () => { const query = 'INSERT INTO users (name, age) VALUES $$users(name, age) RETURNING id'; + const parsedQuery = parseTSQuery(query); const parameters = { users: [ @@ -216,17 +226,17 @@ test('multiple value list parameter interpolation', () => { mapping: [], }; - const result = processQueryString(query, parameters); + const result = processTSQueryAST(parsedQuery.queries[0], parameters); expect(result).toEqual(expectedResult); }); -test('(AST) no params', () => { +test('(SQL) no params', () => { const query = ` /* @name selectSomeUsers */ SELECT id, name FROM users;`; - const fileAST = parseText(query); + const fileAST = parseSQLQuery(query); const parameters = {}; const expectedResult = { @@ -235,22 +245,22 @@ test('(AST) no params', () => { bindings: [], }; - const interpolationResult = processQueryAST( - fileAST.parseTree.queries[0], + const interpolationResult = processSQLQueryAST( + fileAST.queries[0], parameters, ); - const mappingResult = processQueryAST(fileAST.parseTree.queries[0]); + const mappingResult = processSQLQueryAST(fileAST.queries[0]); expect(interpolationResult).toEqual(expectedResult); expect(mappingResult).toEqual(expectedResult); }); -test('(AST) two scalar params', () => { +test('(SQL) two scalar params', () => { const query = ` /* @name selectSomeUsers */ SELECT id, name from users where id = :id and age > :age;`; - const fileAST = parseText(query); + const fileAST = parseSQLQuery(query); const parameters = { id: '123', age: 12, @@ -279,22 +289,22 @@ test('(AST) two scalar params', () => { bindings: [], }; - const interpolationResult = processQueryAST( - fileAST.parseTree.queries[0], + const interpolationResult = processSQLQueryAST( + fileAST.queries[0], parameters, ); - const mappingResult = processQueryAST(fileAST.parseTree.queries[0]); + const mappingResult = processSQLQueryAST(fileAST.queries[0]); expect(interpolationResult).toEqual(expectedInterpolationResult); expect(mappingResult).toEqual(expectedMappingResult); }); -test('(AST) one param used twice', () => { +test('(SQL) one param used twice', () => { const query = ` /* @name selectUsersAndParents */ SELECT id, name from users where id = :id or parent_id = :id;`; - const fileAST = parseText(query); + const fileAST = parseSQLQuery(query); const parameters = { id: '123', }; @@ -317,24 +327,24 @@ test('(AST) one param used twice', () => { bindings: [], }; - const interpolationResult = processQueryAST( - fileAST.parseTree.queries[0], + const interpolationResult = processSQLQueryAST( + fileAST.queries[0], parameters, ); - const mappingResult = processQueryAST(fileAST.parseTree.queries[0]); + const mappingResult = processSQLQueryAST(fileAST.queries[0]); expect(interpolationResult).toEqual(expectedInterpolationResult); expect(mappingResult).toEqual(expectedMappingResult); }); -test('(AST) array param', () => { +test('(SQL) array param', () => { const query = ` /* @name selectSomeUsers @param ages -> (...) */ SELECT FROM users WHERE age in :ages;`; - const fileAST = parseText(query); + const fileAST = parseSQLQuery(query); const parameters = { ages: [23, 27, 50], @@ -358,24 +368,24 @@ test('(AST) array param', () => { ], }; - const interpolationResult = processQueryAST( - fileAST.parseTree.queries[0], + const interpolationResult = processSQLQueryAST( + fileAST.queries[0], parameters, ); - const mappingResult = processQueryAST(fileAST.parseTree.queries[0]); + const mappingResult = processSQLQueryAST(fileAST.queries[0]); expect(interpolationResult).toEqual(expectedInterpolationResult); expect(mappingResult).toEqual(expectedMappingResult); }); -test('(AST) array param used twice', () => { +test('(SQL) array param used twice', () => { const query = ` /* @name selectSomeUsers @param ages -> (...) */ SELECT FROM users WHERE age in :ages or age in :ages;`; - const fileAST = parseText(query); + const fileAST = parseSQLQuery(query); const parameters = { ages: [23, 27, 50], @@ -399,24 +409,24 @@ test('(AST) array param used twice', () => { ], }; - const interpolationResult = processQueryAST( - fileAST.parseTree.queries[0], + const interpolationResult = processSQLQueryAST( + fileAST.queries[0], parameters, ); - const mappingResult = processQueryAST(fileAST.parseTree.queries[0]); + const mappingResult = processSQLQueryAST(fileAST.queries[0]); expect(interpolationResult).toEqual(expectedInterpolationResult); expect(mappingResult).toEqual(expectedMappingResult); }); -test('(AST) array and scalar param', () => { +test('(SQL) array and scalar param', () => { const query = ` /* @name selectSomeUsers @param ages -> (...) */ SELECT FROM users WHERE age in :ages and id = :userId;`; - const fileAST = parseText(query); + const fileAST = parseSQLQuery(query); const parameters = { ages: [23, 27, 50], @@ -446,24 +456,24 @@ test('(AST) array and scalar param', () => { ], }; - const interpolationResult = processQueryAST( - fileAST.parseTree.queries[0], + const interpolationResult = processSQLQueryAST( + fileAST.queries[0], parameters, ); - const mappingResult = processQueryAST(fileAST.parseTree.queries[0]); + const mappingResult = processSQLQueryAST(fileAST.queries[0]); expect(interpolationResult).toEqual(expectedInterpolationResult); expect(mappingResult).toEqual(expectedMappingResult); }); -test('(AST) pick param', () => { +test('(SQL) pick param', () => { const query = ` /* @name insertUsers @param user -> (name, age) */ INSERT INTO users (name, age) VALUES :user RETURNING id;`; - const fileAST = parseText(query); + const fileAST = parseSQLQuery(query); const parameters = { user: { name: 'Bob', age: 12 }, @@ -498,24 +508,24 @@ test('(AST) pick param', () => { ], }; - const interpolationResult = processQueryAST( - fileAST.parseTree.queries[0], + const interpolationResult = processSQLQueryAST( + fileAST.queries[0], parameters, ); expect(interpolationResult).toEqual(expectedInterpolationResult); - const mappingResult = processQueryAST(fileAST.parseTree.queries[0]); + const mappingResult = processSQLQueryAST(fileAST.queries[0]); expect(mappingResult).toEqual(expectedMappingResult); }); -test('(AST) pick param used twice', () => { +test('(SQL) pick param used twice', () => { const query = ` /* @name insertUsersTwice @param user -> (name, age) */ INSERT INTO users (name, age) VALUES :user, :user RETURNING id;`; - const fileAST = parseText(query); + const fileAST = parseSQLQuery(query); const parameters = { user: { name: 'Bob', age: 12 }, @@ -550,24 +560,24 @@ test('(AST) pick param used twice', () => { ], }; - const interpolationResult = processQueryAST( - fileAST.parseTree.queries[0], + const interpolationResult = processSQLQueryAST( + fileAST.queries[0], parameters, ); expect(interpolationResult).toEqual(expectedInterpolationResult); - const mappingResult = processQueryAST(fileAST.parseTree.queries[0]); + const mappingResult = processSQLQueryAST(fileAST.queries[0]); expect(mappingResult).toEqual(expectedMappingResult); }); -test('(AST) pickSpread param', () => { +test('(SQL) pickSpread param', () => { const query = ` /* @name insertUsers @param users -> ((name, age)...) */ INSERT INTO users (name, age) VALUES :users RETURNING id;`; - const fileAST = parseText(query); + const fileAST = parseSQLQuery(query); const parameters = { users: [ @@ -607,24 +617,24 @@ test('(AST) pickSpread param', () => { mapping: expectedMapping, }; - const interpolationResult = processQueryAST( - fileAST.parseTree.queries[0], + const interpolationResult = processSQLQueryAST( + fileAST.queries[0], parameters, ); - const mappingResult = processQueryAST(fileAST.parseTree.queries[0]); + const mappingResult = processSQLQueryAST(fileAST.queries[0]); expect(interpolationResult).toEqual(expectedInterpolationResult); expect(mappingResult).toEqual(expectedMappingResult); }); -test('(AST) pickSpread param used twice', () => { +test('(SQL) pickSpread param used twice', () => { const query = ` /* @name insertUsers @param users -> ((name, age)...) */ INSERT INTO users (name, age) VALUES :users, :users RETURNING id;`; - const fileAST = parseText(query); + const fileAST = parseSQLQuery(query); const parameters = { users: [ @@ -665,11 +675,11 @@ test('(AST) pickSpread param used twice', () => { mapping: expectedMapping, }; - const interpolationResult = processQueryAST( - fileAST.parseTree.queries[0], + const interpolationResult = processSQLQueryAST( + fileAST.queries[0], parameters, ); - const mappingResult = processQueryAST(fileAST.parseTree.queries[0]); + const mappingResult = processSQLQueryAST(fileAST.queries[0]); expect(interpolationResult).toEqual(expectedInterpolationResult); expect(mappingResult).toEqual(expectedMappingResult); diff --git a/packages/query/src/preprocessor.ts b/packages/query/src/preprocessor.ts index f9475c37..557519f1 100644 --- a/packages/query/src/preprocessor.ts +++ b/packages/query/src/preprocessor.ts @@ -1,4 +1,5 @@ -import { assert, Query as QueryAST, TransformType } from './loader/sql'; +import { assert, SQLQueryAST, TransformType } from './loader/sql'; +import { TSQueryAST } from "./loader/typescript"; type Scalar = string | number | null; @@ -103,8 +104,8 @@ function replaceIntervals( } /* Processes query AST formed by new parser from pure SQL files */ -export const processQueryAST = ( - query: QueryAST, +export const processSQLQueryAST = ( + query: SQLQueryAST, passedParams?: IQueryParameters, ): IInterpolatedQuery => { const bindings: Scalar[] = []; @@ -266,142 +267,9 @@ export const processQueryAST = ( }; /* Processes query strings produced by old parser from SQL-in-TS statements */ -export const processQueryString = ( - query: string, +export const processTSQueryAST = ( + query: TSQueryAST, parameters?: IQueryParameters, ): IInterpolatedQuery => { - const bindings: Scalar[] = []; - const params: QueryParam[] = []; - let index = 0; - const flatQuery = query.replace( - rootRegex, - (_1, prefix, paramName: string, nestedExp: string): string => { - let param: QueryParam | undefined; - let replacement = '$bad'; - if (prefix === Prefix.Singular) { - if (nestedExp) { - const dict: { [key: string]: IScalarParam } = {}; - const replacementContents = nestedExp.replace( - leafRegex, - (_2, leafParamName: string): string => { - dict[leafParamName] = { - type: ParamTransform.Scalar, - name: leafParamName, - assignedIndex: ++index, - }; - return `$${index}`; - }, - ); - replacement = `(${replacementContents})`; - param = { - type: ParamTransform.Pick, - name: paramName, - dict, - }; - } else { - const existingParam = params.find((p) => p.name === paramName); - let assignedIndex; - if (existingParam) { - assert(existingParam?.type === ParamTransform.Scalar); - assignedIndex = existingParam.assignedIndex; - } else { - assignedIndex = ++index; - param = { - type: ParamTransform.Scalar, - name: paramName, - assignedIndex, - }; - - if (parameters) { - const scalar = parameters[paramName]; - if (assertScalar(scalar)) { - bindings.push(scalar); - } else { - throw new Error(`Bad parameter ${paramName} expected scalar`); - } - } - } - replacement = `$${assignedIndex}`; - } - } else if (prefix === Prefix.Plural) { - if (nestedExp) { - const dict: any = {}; - const keys: string[] = []; - nestedExp.replace(leafRegex, (_, key) => { - keys.push(key); - return ''; - }); - if (parameters) { - const dictArray = parameters[paramName]; - if (assertDictArray(dictArray)) { - replacement = dictArray - .map((d) => { - const tupleStr = keys - .map((key) => { - const value = d[key]; - bindings.push(value); - return `$${++index}`; - }) - .join(', '); - return `(${tupleStr})`; - }) - .join(', '); - } - } else { - const repl = keys - .map((key) => { - const i = ++index; - dict[key] = { - name: key, - assignedIndex: i, - type: ParamTransform.Scalar, - }; - return `$${i}`; - }) - .join(', '); - param = { - type: ParamTransform.PickSpread, - name: paramName, - dict, - }; - replacement = `(${repl})`; - } - } else { - if (parameters) { - const scalars = parameters[paramName]; - if (assertScalarArray(scalars)) { - replacement = scalars - .map((value) => { - bindings.push(value); - return `$${++index}`; - }) - .join(', '); - } else { - throw new Error( - `Bad parameter ${paramName} expected array of scalars`, - ); - } - } else { - param = { - type: ParamTransform.Spread, - name: paramName, - assignedIndex: ++index, - }; - replacement = `$${index}`; - } - replacement = `(${replacement})`; - } - } - if (param) { - params.push(param); - } - return replacement; - }, - ); - - return { - mapping: parameters ? [] : params, - query: flatQuery, - bindings, - }; + return null as any; }; diff --git a/packages/query/src/tag.ts b/packages/query/src/tag.ts index 53ca7e5e..f11cfef4 100644 --- a/packages/query/src/tag.ts +++ b/packages/query/src/tag.ts @@ -2,7 +2,7 @@ import { processQueryString, ParamTransform, IQueryParameters, - processQueryAST, + processSQLQueryAST, } from './preprocessor'; import { Query as QueryAST } from './loader/sql'; @@ -52,7 +52,7 @@ export class PreparedQuery { constructor(query: QueryAST) { this.query = query; this.run = async (params, connection) => { - const { query: processedQuery, bindings } = processQueryAST( + const { query: processedQuery, bindings } = processSQLQueryAST( this.query, params as any, );