From be9a3516b6a69ff6442b5bcb459f07ab1d87128d Mon Sep 17 00:00:00 2001 From: Adel Date: Mon, 22 Jun 2020 13:40:08 +0300 Subject: [PATCH] Better ts parser (WIP) --- .gitignore | 8 +- packages/query/src/index.ts | 5 +- packages/query/src/loader/typescript/index.ts | 2 + packages/query/src/loader/typescript/query.ts | 17 +- ...essor.test.ts => preprocessor-sql.test.ts} | 235 +----------- packages/query/src/preprocessor-sql.ts | 172 +++++++++ packages/query/src/preprocessor-ts.test.ts | 349 ++++++++++++++++++ packages/query/src/preprocessor-ts.ts | 197 ++++++++++ packages/query/src/preprocessor.ts | 209 +---------- packages/query/src/tag.ts | 18 +- 10 files changed, 755 insertions(+), 457 deletions(-) rename packages/query/src/{preprocessor.test.ts => preprocessor-sql.test.ts} (65%) create mode 100644 packages/query/src/preprocessor-sql.ts create mode 100644 packages/query/src/preprocessor-ts.test.ts create mode 100644 packages/query/src/preprocessor-ts.ts diff --git a/.gitignore b/.gitignore index 54167f51..58546cfb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,12 @@ node_modules/* .idea/ # Ignore build files -*.js -*.d.ts -*.js.map +packages/**/*.js +packages/**/*.d.ts +packages/**/*.js.map *.java *.interp *.tokens *.log -.vercel \ No newline at end of file +.vercel diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts index 9e183de0..8f0bf657 100644 --- a/packages/query/src/index.ts +++ b/packages/query/src/index.ts @@ -4,10 +4,11 @@ export { ParamTransform, IQueryParameters, IInterpolatedQuery, - processSQLQueryAST, - processTSQueryAST, } from './preprocessor'; +export { processTSQueryAST } from './preprocessor-ts'; +export { processSQLQueryAST } from './preprocessor-sql'; + export { AsyncQueue } from '@pgtyped/wire'; export { default as parseTypeScriptFile, TSQueryAST } from './loader/typescript'; diff --git a/packages/query/src/loader/typescript/index.ts b/packages/query/src/loader/typescript/index.ts index 550fa6c1..f1c2b73b 100644 --- a/packages/query/src/loader/typescript/index.ts +++ b/packages/query/src/loader/typescript/index.ts @@ -2,6 +2,8 @@ import ts from 'typescript'; import parseQuery, { Query } from "./query"; import { ParseEvent } from "../sql/logger"; +export const parseTSQuery = parseQuery; + export type TSQueryAST = Query; interface INode { diff --git a/packages/query/src/loader/typescript/query.ts b/packages/query/src/loader/typescript/query.ts index 13d2453e..81518be0 100644 --- a/packages/query/src/loader/typescript/query.ts +++ b/packages/query/src/loader/typescript/query.ts @@ -3,6 +3,7 @@ import { CharStreams, CommonTokenStream } from 'antlr4ts'; import { ParseTreeWalker } from 'antlr4ts/tree/ParseTreeWalker'; import { QueryLexer } from './parser/QueryLexer'; import { + ParamContext, ParamNameContext, PickKeyContext, QueryContext, QueryParser } from "./parser/QueryParser"; @@ -98,20 +99,20 @@ class ParseListener implements QueryParserListener { } 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() { + exitParam(ctx: ParamContext) { + const defLoc = { + a: ctx.start.startIndex, + b: ctx.stop!.stopIndex, + line: ctx.start.line, + col: ctx.start.charPositionInLine, + }; + this.currentParam.location = defLoc; this.currentParam.selection = this.currentSelection as ParamSelection; this.query.params!.push(this.currentParam as Param); this.currentSelection = {}; diff --git a/packages/query/src/preprocessor.test.ts b/packages/query/src/preprocessor-sql.test.ts similarity index 65% rename from packages/query/src/preprocessor.test.ts rename to packages/query/src/preprocessor-sql.test.ts index 3940510d..2178d9df 100644 --- a/packages/query/src/preprocessor.test.ts +++ b/packages/query/src/preprocessor-sql.test.ts @@ -1,235 +1,6 @@ -import { - ParamTransform, - processSQLQueryAST, - processTSQueryAST, -} from './preprocessor'; -import parseSQLQuery from './loader/sql'; -import parseTSQuery from './loader/typescript'; - -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, - }; - - const expectedResult = { - query: 'SELECT id, name from users where id = $1 and age > $2', - mapping: [], - bindings: ['123', 12], - }; - - 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', - }; - - const expectedResult = { - query: 'SELECT id, name from users where id = $1 and parent_id = $1', - mapping: [], - bindings: ['123'], - }; - - const result = processTSQueryAST(parsedQuery.queries[0], parameters); - - expect(result).toEqual(expectedResult); -}); - -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', - mapping: [ - { - assignedIndex: 1, - name: 'id', - type: ParamTransform.Scalar, - }, - { - assignedIndex: 2, - name: 'age', - type: ParamTransform.Scalar, - }, - ], - bindings: [], - }; - - const result = processTSQueryAST(parsedQuery.queries[0]); - - expect(result).toEqual(expectedResult); -}); - -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: { - name: 'Bob', - age: 12, - }, - }; - - const expectedResult = { - query: 'INSERT INTO users (name, age) VALUES ($1, $2) RETURNING id', - mapping: [ - { - name: 'user', - type: ParamTransform.Pick, - dict: { - name: { - assignedIndex: 1, - name: 'name', - type: ParamTransform.Scalar, - }, - age: { - assignedIndex: 2, - name: 'age', - type: ParamTransform.Scalar, - }, - }, - }, - ], - bindings: [], - }; - - const result = processTSQueryAST(parsedQuery.queries[0]); - - expect(result).toEqual(expectedResult); -}); - -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))', - mapping: [ - { - name: 'ages', - type: ParamTransform.Spread, - assignedIndex: 1, - }, - { - name: 'otherAges', - type: ParamTransform.Spread, - assignedIndex: 2, - }, - ], - bindings: [], - }; - - const result = processTSQueryAST(parsedQuery.queries[0]); - - expect(result).toEqual(expectedResult); -}); - -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], - }; - - const expectedResult = { - query: 'SELECT FROM users where age in ($1, $2, $3)', - bindings: [23, 27, 50], - mapping: [], - }; - - const result = processTSQueryAST(parsedQuery.queries[0], parameters); - - expect(result).toEqual(expectedResult); -}); - -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], - }; - - const expectedResult = { - query: 'SELECT FROM users where age in ($1, $2, $3) or age in ($4, $5, $6)', - bindings: [23, 27, 50, 23, 27, 50], - mapping: [], - }; - - const result = processTSQueryAST(parsedQuery.queries[0], parameters); - - expect(result).toEqual(expectedResult); -}); - -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', - bindings: [], - mapping: [ - { - name: 'users', - type: ParamTransform.PickSpread, - dict: { - name: { - name: 'name', - type: ParamTransform.Scalar, - assignedIndex: 1, - }, - age: { - name: 'age', - type: ParamTransform.Scalar, - assignedIndex: 2, - }, - }, - }, - ], - }; - - const result = processTSQueryAST(parsedQuery.queries[0]); - - expect(result).toEqual(expectedResult); -}); - -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: [ - { name: 'Bob', age: 12 }, - { name: 'Tom', age: 22 }, - ], - }; - - const expectedResult = { - query: - 'INSERT INTO users (name, age) VALUES ($1, $2), ($3, $4) RETURNING id', - bindings: ['Bob', 12, 'Tom', 22], - mapping: [], - }; - - const result = processTSQueryAST(parsedQuery.queries[0], parameters); - - expect(result).toEqual(expectedResult); -}); +import parseSQLQuery from "./loader/sql"; +import { processSQLQueryAST } from "./preprocessor-sql"; +import { ParamTransform } from "./preprocessor"; test('(SQL) no params', () => { const query = ` diff --git a/packages/query/src/preprocessor-sql.ts b/packages/query/src/preprocessor-sql.ts new file mode 100644 index 00000000..3000b113 --- /dev/null +++ b/packages/query/src/preprocessor-sql.ts @@ -0,0 +1,172 @@ +import { assert, SQLQueryAST, TransformType } from "./loader/sql"; +import { + IInterpolatedQuery, + INestedParameters, + IQueryParameters, IScalarArrayParam, + IScalarParam, + ParamTransform, + QueryParam, replaceIntervals, Scalar +} from "./preprocessor"; + +/* Processes query AST formed by new parser from pure SQL files */ +export const processSQLQueryAST = ( + query: SQLQueryAST, + passedParams?: IQueryParameters, +): IInterpolatedQuery => { + const bindings: Scalar[] = []; + const paramMapping: QueryParam[] = []; + const usedParams = query.params.filter((p) => p.name in query.usedParamSet); + const { a: statementStart } = query.statement.loc; + let i = 1; + const intervals: { a: number; b: number; sub: string }[] = []; + for (const usedParam of usedParams) { + const paramLocs = usedParam.codeRefs.used.map(({ a, b }) => ({ + a: a - statementStart - 1, + b: b - statementStart, + })); + + // Handle spread transform + if (usedParam.transform.type === TransformType.ArraySpread) { + let sub: string; + if (passedParams) { + const paramValue = passedParams[usedParam.name]; + sub = (paramValue as Scalar[]) + .map((val) => { + bindings.push(val); + return `$${i++}`; + }) + .join(','); + } else { + const idx = i++; + paramMapping.push({ + name: usedParam.name, + type: ParamTransform.Spread, + assignedIndex: idx, + } as IScalarArrayParam); + sub = `$${idx}`; + } + paramLocs.forEach((pl) => + intervals.push({ + ...pl, + sub: `(${sub})`, + }), + ); + continue; + } + + // Handle pick transform + if (usedParam.transform.type === TransformType.PickTuple) { + const dict: { + [key: string]: IScalarParam; + } = {}; + const sub = usedParam.transform.keys + .map((pickKey) => { + const idx = i++; + dict[pickKey] = { + name: pickKey, + type: ParamTransform.Scalar, + assignedIndex: idx, + } as IScalarParam; + if (passedParams) { + const paramValue = passedParams[ + usedParam.name + ] as INestedParameters; + const val = paramValue[pickKey]; + bindings.push(val); + } + return `$${idx}`; + }) + .join(','); + if (!passedParams) { + paramMapping.push({ + name: usedParam.name, + type: ParamTransform.Pick, + dict, + }); + } + + paramLocs.forEach((pl) => + intervals.push({ + ...pl, + sub: `(${sub})`, + }), + ); + continue; + } + + // Handle spreadPick transform + if (usedParam.transform.type === TransformType.PickArraySpread) { + let sub: string; + if (passedParams) { + const passedParam = passedParams[usedParam.name] as INestedParameters[]; + sub = passedParam + .map((entity) => { + assert(usedParam.transform.type === TransformType.PickArraySpread); + const ssub = usedParam.transform.keys + .map((pickKey) => { + const val = entity[pickKey]; + bindings.push(val); + return `$${i++}`; + }) + .join(','); + return ssub; + }) + .join('),('); + } else { + const dict: { + [key: string]: IScalarParam; + } = {}; + sub = usedParam.transform.keys + .map((pickKey) => { + const idx = i++; + dict[pickKey] = { + name: pickKey, + type: ParamTransform.Scalar, + assignedIndex: idx, + } as IScalarParam; + return `$${idx}`; + }) + .join(','); + paramMapping.push({ + name: usedParam.name, + type: ParamTransform.PickSpread, + dict, + }); + } + + paramLocs.forEach((pl) => + intervals.push({ + ...pl, + sub: `(${sub})`, + }), + ); + continue; + } + + // Handle scalar transform + const assignedIndex = i++; + if (passedParams) { + const paramValue = passedParams[usedParam.name] as Scalar; + bindings.push(paramValue); + } else { + paramMapping.push({ + name: usedParam.name, + type: ParamTransform.Scalar, + assignedIndex, + } as IScalarParam); + } + + paramLocs.forEach((pl) => + intervals.push({ + ...pl, + sub: `$${assignedIndex}`, + }), + ); + } + const flatStr = replaceIntervals(query.statement.body, intervals); + return { + mapping: paramMapping, + query: flatStr, + bindings, + }; +}; diff --git a/packages/query/src/preprocessor-ts.test.ts b/packages/query/src/preprocessor-ts.test.ts new file mode 100644 index 00000000..69361a57 --- /dev/null +++ b/packages/query/src/preprocessor-ts.test.ts @@ -0,0 +1,349 @@ +import { parseTSQuery } from "./loader/typescript"; +import { processTSQueryAST } from "./preprocessor-ts"; +import { ParamTransform } from "./preprocessor"; + +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, + }; + + const expectedResult = { + query: 'SELECT id, name from users where id = $1 and age > $2', + mapping: [], + bindings: ['123', 12], + }; + + const result = processTSQueryAST(parsedQuery.query, 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', + }; + + const expectedResult = { + query: 'SELECT id, name from users where id = $1 and parent_id = $1', + mapping: [], + bindings: ['123'], + }; + + const result = processTSQueryAST(parsedQuery.query, parameters); + + expect(result).toEqual(expectedResult); +}); + +test('(TS) name parameter mapping', () => { + const query = 'SELECT id, name from users where id = $id and age > $age and parent_id = $id'; + const parsedQuery = parseTSQuery(query); + + const expectedResult = { + query: 'SELECT id, name from users where id = $1 and age > $2 and parent_id = $1', + mapping: [], + bindings: ['1234-1235', 33], + }; + + const result = processTSQueryAST(parsedQuery.query, {id: '1234-1235', age: 33}); + + expect(result).toEqual(expectedResult); +}); + +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: { + name: 'Bob', + age: 12, + }, + }; + + const expectedResult = { + query: 'INSERT INTO users (name, age) VALUES ($1, $2) RETURNING id', + mapping: [ + { + name: 'user', + type: ParamTransform.Pick, + dict: { + name: { + assignedIndex: 1, + name: 'name', + type: ParamTransform.Scalar, + }, + age: { + assignedIndex: 2, + name: 'age', + type: ParamTransform.Scalar, + }, + }, + }, + ], + bindings: [], + }; + + const result = processTSQueryAST(parsedQuery.query); + + expect(result).toEqual(expectedResult); +}); + +test('(TS) single value list parameter interpolation twice', () => { + const query = + 'INSERT INTO users (name, age) VALUES $user(name, age) BOGUS $user(name, id) RETURNING id'; + const parsedQuery = parseTSQuery(query); + + const parameters = { + user: { + id: '1234-123-1233', + name: 'Bob', + age: 12, + }, + }; + + const expectedResult = { + query: 'INSERT INTO users (name, age) VALUES ($1, $2) BOGUS ($1, $3) RETURNING id', + mapping: [], + bindings: ['Bob', 12, '1234-123-1233'], + }; + + const result = processTSQueryAST(parsedQuery.query, parameters); + + expect(result).toEqual(expectedResult); +}); + +test('(TS) multiple value list (array) parameter mapping', () => { + const query = + 'SELECT FROM users where (age in $$ages and age in $$ages) or (age in $$otherAges)'; + const parsedQuery = parseTSQuery(query); + + const expectedResult = { + query: 'SELECT FROM users where (age in ($1) and age in ($1)) or (age in ($2))', + mapping: [ + { + name: 'ages', + type: ParamTransform.Spread, + assignedIndex: [1], + }, + { + name: 'otherAges', + type: ParamTransform.Spread, + assignedIndex: [2], + }, + ], + bindings: [], + }; + + const result = processTSQueryAST(parsedQuery.query); + + expect(result).toEqual(expectedResult); +}); + +test('(TS) multiple value list (array) parameter interpolation', () => { + const query = 'SELECT FROM users where age in $$ages or parent_age in $$ages'; + const parsedQuery = parseTSQuery(query); + + const parameters = { + ages: [23, 27, 50], + }; + + const expectedResult = { + query: 'SELECT FROM users where age in ($1, $2, $3) or parent_age in ($1, $2, $3)', + bindings: [23, 27, 50], + mapping: [], + }; + + const result = processTSQueryAST(parsedQuery.query, parameters); + + expect(result).toEqual(expectedResult); +}); + +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', + bindings: [], + mapping: [ + { + name: 'users', + type: ParamTransform.PickSpread, + dict: { + name: { + name: 'name', + type: ParamTransform.Scalar, + assignedIndex: 1, + }, + age: { + name: 'age', + type: ParamTransform.Scalar, + assignedIndex: 2, + }, + }, + }, + ], + }; + + const result = processTSQueryAST(parsedQuery.query); + + expect(result).toEqual(expectedResult); +}); + +test('(TS) multiple value list parameter mapping twice', () => { + const query = + 'INSERT INTO users (name, age) VALUES $$users(name, age), $$users(name) RETURNING id'; + const parsedQuery = parseTSQuery(query); + + const expectedResult = { + query: 'INSERT INTO users (name, age) VALUES ($1, $2), ($1) RETURNING id', + bindings: [], + mapping: [ + { + name: 'users', + type: ParamTransform.PickSpread, + dict: { + name: { + name: 'name', + type: ParamTransform.Scalar, + assignedIndex: 1, + }, + age: { + name: 'age', + type: ParamTransform.Scalar, + assignedIndex: 2, + }, + }, + }, + ], + }; + + const result = processTSQueryAST(parsedQuery.query); + + expect(result).toEqual(expectedResult); +}); + +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: [ + { name: 'Bob', age: 12 }, + { name: 'Tom', age: 22 }, + ], + }; + + const expectedResult = { + query: + 'INSERT INTO users (name, age) VALUES ($1, $2), ($3, $4) RETURNING id', + bindings: ['Bob', 12, 'Tom', 22], + mapping: [], + }; + + const result = processTSQueryAST(parsedQuery.query, parameters); + + expect(result).toEqual(expectedResult); +}); + +test('(TS) multiple value list parameter interpolation twice', () => { + const query = + 'INSERT INTO users (name, age) VALUES $$users(name, age), $$users(name, age) RETURNING id'; + const parsedQuery = parseTSQuery(query); + + const parameters = { + users: [ + { name: 'Bob', age: 12 }, + { name: 'Tom', age: 22 }, + ], + }; + + const expectedResult = { + query: + 'INSERT INTO users (name, age) VALUES ($1, $2), ($3, $4), ($5, $6), ($7, $8) RETURNING id', + bindings: ['Bob', 12, 'Tom', 22, 'Bob', 12, 'Tom', 22], + mapping: [], + }; + + const result = processTSQueryAST(parsedQuery.query, parameters); + + expect(result).toEqual(expectedResult); +}); + + +test('(TS) all kinds mapping ', () => { + const query = + '$userId $age $userId $$users $age $user(id) $$users $user(id, parentId) $$comments(id, text) $user(age)'; + const parsedQuery = parseTSQuery(query); + + const expectedResult = { + query: '$1 $2 $1 ($3) $2 ($4) ($3) ($4, $5) ($6, $7) ($8)', + bindings: [], + mapping: [ + { + name: 'userId', + type: ParamTransform.Scalar, + assignedIndex: 1, + }, + { + name: 'age', + type: ParamTransform.Scalar, + assignedIndex: 2, + }, + { + name: 'users', + type: ParamTransform.Spread, + assignedIndex: [3], + }, + { + name: 'user', + type: ParamTransform.Pick, + dict: { + id: { + name: 'id', + type: ParamTransform.Scalar, + assignedIndex: 4, + }, + age: { + name: 'age', + type: ParamTransform.Scalar, + assignedIndex: 8, + }, + parentId: { + name: 'parentId', + type: ParamTransform.Scalar, + assignedIndex: 5, + }, + }, + }, + { + name: 'comments', + type: ParamTransform.PickSpread, + dict: { + id: { + name: 'id', + type: ParamTransform.Scalar, + assignedIndex: 6, + }, + text: { + name: 'text', + type: ParamTransform.Scalar, + assignedIndex: 7, + }, + }, + }, + ], + }; + + const result = processTSQueryAST(parsedQuery.query); + + expect(result).toEqual(expectedResult); +}); diff --git a/packages/query/src/preprocessor-ts.ts b/packages/query/src/preprocessor-ts.ts new file mode 100644 index 00000000..d0c12616 --- /dev/null +++ b/packages/query/src/preprocessor-ts.ts @@ -0,0 +1,197 @@ +import { TSQueryAST } from "./loader/typescript"; +import { ParamType } from "./loader/typescript/query"; +import { assert } from "./loader/sql"; +import { + IDictArrayParam, + IDictParam, + IInterpolatedQuery, + INestedParameters, + IQueryParameters, IScalarArrayParam, + IScalarParam, + ParamTransform, + QueryParam, replaceIntervals, Scalar +} from "./preprocessor"; + +function processScalar( + paramName: string, + nextIndex: number, + existingConfig?: IScalarParam, + parameters?: IQueryParameters, +): { + replacement: string; + bindings: Scalar[], + nextIndex: number; + config: IScalarParam, +} { + let index = nextIndex; + const bindings = []; + let replacement; + let config = existingConfig; + if (config) { + replacement = `$${config.assignedIndex}`; + } else { + const assignedIndex = ++index; + replacement = `$${assignedIndex}`; + config = {assignedIndex, type: ParamTransform.Scalar, name: paramName}; + + if (parameters) { + const value = parameters[paramName] as Scalar; + bindings.push(value); + } + } + return { bindings, replacement, nextIndex: index, config }; +} + +function processScalarArray( + paramName: string, + nextIndex: number, + existingConfig?: IScalarArrayParam, + parameters?: IQueryParameters, +): { + replacement: string; + bindings: Scalar[], + nextIndex: number; + config: IScalarArrayParam, +} { + let index = nextIndex; + const bindings: Scalar[] = []; + let config = existingConfig; + + let assignedIndex: number[] = []; + if (config) { + assignedIndex = config.assignedIndex as number[]; + } else { + if (parameters) { + const values = parameters[paramName] as Scalar[]; + assignedIndex = values.map(val => { + bindings.push(val); + return ++index; + }); + } else { + assignedIndex = [++index]; + } + config = {assignedIndex, type: ParamTransform.Spread, name: paramName}; + } + const replacement = '(' + assignedIndex.map(v => `$${v}`).join(', ') + ')'; + + return { bindings, replacement, nextIndex: index, config }; +} + +function processObject( + paramName: string, + keys: string[], + nextIndex: number, + existingConfig?: IDictParam, + parameters?: IQueryParameters, +): { + replacement: string; + bindings: Scalar[], + nextIndex: number; + config: IDictParam, +} { + let index = nextIndex; + const bindings: Scalar[] = []; + let config = existingConfig || { name: paramName, type: ParamTransform.Pick, dict: {}} as IDictParam; + + const keyIndices = keys.map(key => { + if (key in config.dict) { + // reuse index if parameter was seen before + return `$${config.dict[key].assignedIndex}`; + } + + const assignedIndex = ++index; + config.dict[key] = {assignedIndex, type: ParamTransform.Scalar, name: key}; + if (parameters) { + const value = (parameters[paramName] as INestedParameters)[key]; + bindings.push(value); + } + return `$${assignedIndex}`; + }); + const replacement = '(' + keyIndices.join(', ') + ')'; + + return { bindings, replacement, nextIndex: index, config }; +} + +function processObjectArray( + paramName: string, + keys: string[], + nextIndex: number, + existingConfig?: IDictArrayParam, + parameters?: IQueryParameters, +): { + replacement: string; + bindings: Scalar[], + nextIndex: number; + config: IDictArrayParam, +} { + let index = nextIndex; + const bindings: Scalar[] = []; + let config = existingConfig || { name: paramName, type: ParamTransform.PickSpread, dict: {}} as IDictArrayParam; + + let replacement; + if (parameters) { + const values = parameters[paramName] as INestedParameters[]; + replacement = values.map(val => Object.values(val).map(v => { + bindings.push(v); + return `$${++index}`; + }).join(', ')).map(pk => `(${pk})`).join(', '); + } else { + const keyIndices = keys.map( key => { + if (key in config.dict) { + // reuse index if parameter was seen before + return `$${config.dict[key].assignedIndex}`; + } + + const assignedIndex = ++index; + config.dict[key] = {assignedIndex, type: ParamTransform.Scalar, name: key}; + return `$${assignedIndex}`; + }); + replacement = '(' + keyIndices.join(', ') + ')'; + } + + return { bindings, replacement, nextIndex: index, config }; +} + +/* Processes query strings produced by old parser from SQL-in-TS statements */ +export const processTSQueryAST = ( + query: TSQueryAST, + parameters?: IQueryParameters, +): IInterpolatedQuery => { + const bindings: Scalar[] = []; + const baseMap: { [param: string]: QueryParam } = {}; + let i = 0; + const intervals: { a: number; b: number; sub: string }[] = []; + for (const param of query.params) { + let sub: string; + let paramBindings: Scalar[] = []; + let config: QueryParam; + let result; + if (param.selection.type === ParamType.Scalar) { + const prevConfig = baseMap[param.name] as IScalarParam | undefined; + result = processScalar(param.name, i, prevConfig, parameters); + } + if (param.selection.type === ParamType.ScalarArray) { + const prevConfig = baseMap[param.name] as IScalarArrayParam | undefined; + result = processScalarArray(param.name, i, prevConfig, parameters); + } + if (param.selection.type === ParamType.Object) { + const prevConfig: IDictParam = baseMap[param.name] as IDictParam || { name: param.name, type: ParamTransform.Pick, dict: {}}; + result = processObject(param.name, param.selection.keys, i, prevConfig, parameters); + } + if (param.selection.type === ParamType.ObjectArray) { + const prevConfig: IDictArrayParam = baseMap[param.name] as IDictArrayParam || { name: param.name, type: ParamTransform.PickSpread, dict: {}}; + result = processObjectArray(param.name, param.selection.keys, i, prevConfig, parameters); + } + assert(result); + ({config, nextIndex: i, replacement: sub, bindings: paramBindings } = result); + baseMap[param.name] = config!; + bindings.push(...paramBindings); + intervals.push({ a: param.location.a, b: param.location.b, sub }); + } + const flatStr = replaceIntervals(query.text, intervals); + return { + mapping: parameters ? [] : Object.values(baseMap), + query: flatStr, + bindings, + }; +}; diff --git a/packages/query/src/preprocessor.ts b/packages/query/src/preprocessor.ts index 557519f1..7cf16a20 100644 --- a/packages/query/src/preprocessor.ts +++ b/packages/query/src/preprocessor.ts @@ -1,7 +1,4 @@ -import { assert, SQLQueryAST, TransformType } from './loader/sql'; -import { TSQueryAST } from "./loader/typescript"; - -type Scalar = string | number | null; +export type Scalar = string | number | null; export enum ParamTransform { Scalar, @@ -10,13 +7,13 @@ export enum ParamTransform { PickSpread, } -interface IScalarParam { +export interface IScalarParam { name: string; type: ParamTransform.Scalar; assignedIndex: number; } -interface IDictParam { +export interface IDictParam { name: string; type: ParamTransform.Pick; dict: { @@ -24,13 +21,13 @@ interface IDictParam { }; } -interface IScalarArrayParam { +export interface IScalarArrayParam { name: string; type: ParamTransform.Spread; - assignedIndex: number; + assignedIndex: number | number[]; } -interface IDictArrayParam { +export interface IDictArrayParam { name: string; type: ParamTransform.PickSpread; dict: { @@ -50,7 +47,7 @@ export interface IInterpolatedQuery { bindings: Scalar[]; } -interface INestedParameters { +export interface INestedParameters { [subParamName: string]: Scalar; } @@ -62,27 +59,7 @@ export interface IQueryParameters { | INestedParameters[]; } -function assertScalar(obj: any): obj is Scalar { - return true; -} - -function assertScalarArray(obj: any): obj is Scalar[] { - return true; -} - -function assertDictArray(obj: any): obj is INestedParameters[] { - return true; -} - -const rootRegex = /(\$\$?)(\w+)(?:\((.+?)\))?/gm; -const leafRegex = /(\w+)/gm; - -enum Prefix { - Singular = '$', - Plural = '$$', -} - -function replaceIntervals( +export function replaceIntervals( str: string, intervals: { a: number; b: number; sub: string }[], ) { @@ -103,173 +80,3 @@ function replaceIntervals( return result; } -/* Processes query AST formed by new parser from pure SQL files */ -export const processSQLQueryAST = ( - query: SQLQueryAST, - passedParams?: IQueryParameters, -): IInterpolatedQuery => { - const bindings: Scalar[] = []; - const paramMapping: QueryParam[] = []; - const usedParams = query.params.filter((p) => p.name in query.usedParamSet); - const { a: statementStart } = query.statement.loc; - let i = 1; - const intervals: { a: number; b: number; sub: string }[] = []; - for (const usedParam of usedParams) { - const paramLocs = usedParam.codeRefs.used.map(({ a, b }) => ({ - a: a - statementStart - 1, - b: b - statementStart, - })); - - // Handle spread transform - if (usedParam.transform.type === TransformType.ArraySpread) { - let sub: string; - if (passedParams) { - const paramValue = passedParams[usedParam.name]; - sub = (paramValue as Scalar[]) - .map((val) => { - bindings.push(val); - return `$${i++}`; - }) - .join(','); - } else { - const idx = i++; - paramMapping.push({ - name: usedParam.name, - type: ParamTransform.Spread, - assignedIndex: idx, - } as IScalarArrayParam); - sub = `$${idx}`; - } - paramLocs.forEach((pl) => - intervals.push({ - ...pl, - sub: `(${sub})`, - }), - ); - continue; - } - - // Handle pick transform - if (usedParam.transform.type === TransformType.PickTuple) { - const dict: { - [key: string]: IScalarParam; - } = {}; - const sub = usedParam.transform.keys - .map((pickKey) => { - const idx = i++; - dict[pickKey] = { - name: pickKey, - type: ParamTransform.Scalar, - assignedIndex: idx, - } as IScalarParam; - if (passedParams) { - const paramValue = passedParams[ - usedParam.name - ] as INestedParameters; - const val = paramValue[pickKey]; - bindings.push(val); - } - return `$${idx}`; - }) - .join(','); - if (!passedParams) { - paramMapping.push({ - name: usedParam.name, - type: ParamTransform.Pick, - dict, - }); - } - - paramLocs.forEach((pl) => - intervals.push({ - ...pl, - sub: `(${sub})`, - }), - ); - continue; - } - - // Handle spreadPick transform - if (usedParam.transform.type === TransformType.PickArraySpread) { - let sub: string; - if (passedParams) { - const passedParam = passedParams[usedParam.name] as INestedParameters[]; - sub = passedParam - .map((entity) => { - assert(usedParam.transform.type === TransformType.PickArraySpread); - const ssub = usedParam.transform.keys - .map((pickKey) => { - const val = entity[pickKey]; - bindings.push(val); - return `$${i++}`; - }) - .join(','); - return ssub; - }) - .join('),('); - } else { - const dict: { - [key: string]: IScalarParam; - } = {}; - sub = usedParam.transform.keys - .map((pickKey) => { - const idx = i++; - dict[pickKey] = { - name: pickKey, - type: ParamTransform.Scalar, - assignedIndex: idx, - } as IScalarParam; - return `$${idx}`; - }) - .join(','); - paramMapping.push({ - name: usedParam.name, - type: ParamTransform.PickSpread, - dict, - }); - } - - paramLocs.forEach((pl) => - intervals.push({ - ...pl, - sub: `(${sub})`, - }), - ); - continue; - } - - // Handle scalar transform - const assignedIndex = i++; - if (passedParams) { - const paramValue = passedParams[usedParam.name] as Scalar; - bindings.push(paramValue); - } else { - paramMapping.push({ - name: usedParam.name, - type: ParamTransform.Scalar, - assignedIndex, - } as IScalarParam); - } - - paramLocs.forEach((pl) => - intervals.push({ - ...pl, - sub: `$${assignedIndex}`, - }), - ); - } - const flatStr = replaceIntervals(query.statement.body, intervals); - return { - mapping: paramMapping, - query: flatStr, - bindings, - }; -}; - -/* Processes query strings produced by old parser from SQL-in-TS statements */ -export const processTSQueryAST = ( - query: TSQueryAST, - parameters?: IQueryParameters, -): IInterpolatedQuery => { - return null as any; -}; diff --git a/packages/query/src/tag.ts b/packages/query/src/tag.ts index f11cfef4..7ce8a893 100644 --- a/packages/query/src/tag.ts +++ b/packages/query/src/tag.ts @@ -1,10 +1,8 @@ -import { - processQueryString, - ParamTransform, - IQueryParameters, - processSQLQueryAST, -} from './preprocessor'; +import { processTSQueryAST } from './preprocessor-ts'; +import { processSQLQueryAST } from './preprocessor-sql'; import { Query as QueryAST } from './loader/sql'; +import { TSQueryAST } from "./loader/typescript"; +import { parseTypeScriptFile } from "./index"; interface IDatabaseConnection { query: (query: string, bindings: any[]) => Promise<{ rows: any[] }>; @@ -17,12 +15,12 @@ export class TaggedQuery { dbConnection: IDatabaseConnection, ) => Promise>; - private readonly query: string; + private readonly query: TSQueryAST; - constructor(query: string) { + constructor(query: TSQueryAST) { this.query = query; this.run = async (params, connection) => { - const { query: processedQuery, bindings } = processQueryString( + const { query: processedQuery, bindings } = processTSQueryAST( this.query, params as any, ); @@ -38,7 +36,7 @@ interface ITypePair { } const sql = (stringsArray: TemplateStringsArray) => - new TaggedQuery(stringsArray[0]); + new TaggedQuery(parseTypeScriptFile(stringsArray[0]).queries[0]); /* Used for pure SQL */ export class PreparedQuery {