expression parser

This commit is contained in:
Jan Prochazka 2020-03-12 12:46:07 +01:00
parent e2c5c8163c
commit 064121376f
15 changed files with 1306 additions and 100 deletions

View File

@ -1,57 +1,83 @@
const _ = require('lodash');
const moment = require('moment');
class SqlDumper { class SqlDumper {
/** @param driver {import('@dbgate/types').EngineDriver} */ /** @param driver {import('@dbgate/types').EngineDriver} */
constructor(driver) { constructor(driver) {
this.s = ""; this.s = '';
this.driver = driver; this.driver = driver;
this.dialect = driver.dialect; this.dialect = driver.dialect;
this.indentLevel = 0; this.indentLevel = 0;
} }
endCommand() { endCommand() {
this.putRaw(";\n"); this.putRaw(';\n');
} }
putRaw(text) { putRaw(text) {
this.s += text; this.s += text;
} }
escapeString(value) {
const esc = this.dialect.stringEscapeChar;
let res = '';
for (let i = 0; i < value.length; i++) {
const c = value[i];
if (c == esc || c == "'") {
res += esc;
}
res += c;
}
return res;
}
putStringValue(value) {
this.putRaw("'");
this.putRaw(this.escapeString(value));
this.putRaw("'");
}
putValue(value) {
if (_.isString(value)) this.putStringValue(value);
if (_.isNumber(value)) this.putRaw(value.toString());
if (_.isDate(value)) this.putStringValue(moment(value).toISOString());
}
putCmd(format, ...args) { putCmd(format, ...args) {
this.put(format, ...args); this.put(format, ...args);
this.endCommand(); this.endCommand();
} }
putFormattedValue(c, value) { putFormattedValue(c, value) {
switch (c) { switch (c) {
case "s": case 's':
if (value != null) { if (value != null) {
this.putRaw(value.toString()); this.putRaw(value.toString());
} }
break; break;
case "i": case 'i':
{ {
this.putRaw(this.dialect.quoteIdentifier(value)); this.putRaw(this.dialect.quoteIdentifier(value));
} }
break; break;
case "k": case 'k':
{ {
if (value) { if (value) {
this.putRaw(value.toUpperCase()); this.putRaw(value.toUpperCase());
} }
} }
break; break;
case "f": case 'f':
{ {
const { schemaName, pureName } = value; const { schemaName, pureName } = value;
if (schemaName) { if (schemaName) {
this.putRaw(this.dialect.quoteIdentifier(schemaName)); this.putRaw(this.dialect.quoteIdentifier(schemaName));
this.putRaw("."); this.putRaw('.');
} }
this.putRaw(this.dialect.quoteIdentifier(pureName)); this.putRaw(this.dialect.quoteIdentifier(pureName));
} }
break; break;
case 'v':
this.putValue(value);
break;
} }
} }
putFormattedList(c, collection) { putFormattedList(c, collection) {
if (!collection) return; if (!collection) return;
this.putCollection(", ", collection, item => this.putCollection(', ', collection, item => this.putFormattedValue(c, item));
this.putFormattedValue(c, item)
);
} }
/** @param format {string} */ /** @param format {string} */
put(format, ...args) { put(format, ...args) {
@ -62,20 +88,20 @@ class SqlDumper {
let c = format[i]; let c = format[i];
i++; i++;
switch (c) { switch (c) {
case "^": case '^':
while (i < length && format[i].match(/[a-z0-9_]/i)) { while (i < length && format[i].match(/[a-z0-9_]/i)) {
this.putRaw(format[i].toUpperCase()); this.putRaw(format[i].toUpperCase());
i++; i++;
} }
break; break;
case "%": case '%':
c = format[i]; c = format[i];
i++; i++;
switch (c) { switch (c) {
case "%": case '%':
this.putRaw("%"); this.putRaw('%');
break; break;
case ",": case ',':
c = format[i]; c = format[i];
i++; i++;
this.putFormattedList(c, args[argIndex]); this.putFormattedList(c, args[argIndex]);
@ -87,22 +113,22 @@ class SqlDumper {
argIndex++; argIndex++;
break; break;
case "&": case '&':
c = format[i]; c = format[i];
i++; i++;
switch (c) { switch (c) {
case "&": case '&':
this.putRaw("&"); this.putRaw('&');
break; break;
case ">": case '>':
this.indentLevel++; this.indentLevel++;
break; break;
case "<": case '<':
this.indentLevel--; this.indentLevel--;
break; break;
case "n": case 'n':
this.putRaw("\n"); this.putRaw('\n');
this.putRaw(" ".repeat(2 * this.indentLevel)); this.putRaw(' '.repeat(2 * this.indentLevel));
break; break;
} }
break; break;
@ -114,36 +140,29 @@ class SqlDumper {
} }
} }
autoIncrement() { autoIncrement() {
this.put(" ^auto_increment"); this.put(' ^auto_increment');
} }
/** /**
* @param column {import('@dbgate/types').ColumnInfo} * @param column {import('@dbgate/types').ColumnInfo}
*/ */
columnDefinition( columnDefinition(column, { includeDefault = true, includeNullable = true, includeCollate = true } = {}) {
column,
{
includeDefault = true,
includeNullable = true,
includeCollate = true
} = {}
) {
if (column.computedExpression) { if (column.computedExpression) {
this.put("^as %s", column.computedExpression); this.put('^as %s', column.computedExpression);
if (column.isPersisted) this.put(" ^persisted"); if (column.isPersisted) this.put(' ^persisted');
return; return;
} }
this.put("%k", column.dataType); this.put('%k', column.dataType);
if (column.autoIncrement) { if (column.autoIncrement) {
this.autoIncrement(); this.autoIncrement();
} }
this.putRaw(" "); this.putRaw(' ');
if (column.isSparse) { if (column.isSparse) {
this.put(" ^sparse "); this.put(' ^sparse ');
} }
if (includeNullable) { if (includeNullable) {
this.put(column.notNull ? "^not ^null" : "^null"); this.put(column.notNull ? '^not ^null' : '^null');
} }
if (includeDefault && column.defaultValue != null) { if (includeDefault && column.defaultValue != null) {
this.columnDefault(column); this.columnDefault(column);
@ -155,13 +174,9 @@ class SqlDumper {
*/ */
columnDefault(column) { columnDefault(column) {
if (column.defaultConstraint != null) { if (column.defaultConstraint != null) {
this.put( this.put(' ^constraint %i ^default %s ', column.defaultConstraint, column.defaultValue);
" ^constraint %i ^default %s ",
column.defaultConstraint,
column.defaultValue
);
} else { } else {
this.put(" ^default %s ", column.defaultValue); this.put(' ^default %s ', column.defaultValue);
} }
} }
@ -182,24 +197,24 @@ class SqlDumper {
} }
/** @param table {import('@dbgate/types').TableInfo} */ /** @param table {import('@dbgate/types').TableInfo} */
createTable(table) { createTable(table) {
this.put("^create ^table %f ( &>&n", table); this.put('^create ^table %f ( &>&n', table);
this.putCollection(",&n", table.columns, col => { this.putCollection(',&n', table.columns, col => {
this.put("%i ", col.columnName); this.put('%i ', col.columnName);
this.columnDefinition(col); this.columnDefinition(col);
}); });
if (table.primaryKey) { if (table.primaryKey) {
this.put(",&n"); this.put(',&n');
if (table.primaryKey.constraintName) { if (table.primaryKey.constraintName) {
this.put("^constraint %i", table.primaryKey.constraintName); this.put('^constraint %i', table.primaryKey.constraintName);
} }
this.put( this.put(
" ^primary ^key (%,i)", ' ^primary ^key (%,i)',
table.primaryKey.columns.map(x => x.columnName) table.primaryKey.columns.map(x => x.columnName)
); );
} }
if (table.foreignKeys) { if (table.foreignKeys) {
table.foreignKeys.forEach(fk => { table.foreignKeys.forEach(fk => {
this.put(",&n"); this.put(',&n');
this.createForeignKeyFore(fk); this.createForeignKeyFore(fk);
}); });
} }
@ -215,7 +230,7 @@ class SqlDumper {
// first = false; // first = false;
// CreateCheckCore(cnt); // CreateCheckCore(cnt);
// } // }
this.put("&<&n)"); this.put('&<&n)');
this.endCommand(); this.endCommand();
// foreach (var ix in table.Indexes) // foreach (var ix in table.Indexes)
// { // {
@ -225,16 +240,15 @@ class SqlDumper {
/** @param fk {import('@dbgate/types').ForeignKeyInfo} */ /** @param fk {import('@dbgate/types').ForeignKeyInfo} */
createForeignKeyFore(fk) { createForeignKeyFore(fk) {
if (fk.constraintName != null) if (fk.constraintName != null) this.put('^constraint %i ', fk.constraintName);
this.put("^constraint %i ", fk.constraintName);
this.put( this.put(
"^foreign ^key (%,i) ^references %f (%,i)", '^foreign ^key (%,i) ^references %f (%,i)',
fk.columns.map(x => x.columnName), fk.columns.map(x => x.columnName),
{ schemaName: fk.refSchemaName, pureName: fk.refTableName }, { schemaName: fk.refSchemaName, pureName: fk.refTableName },
fk.columns.map(x => x.refColumnName) fk.columns.map(x => x.refColumnName)
); );
if (fk.deleteAction) this.put(" ^on ^delete %k", fk.deleteAction); if (fk.deleteAction) this.put(' ^on ^delete %k', fk.deleteAction);
if (fk.updateAction) this.put(" ^on ^update %k", fk.updateAction); if (fk.updateAction) this.put(' ^on ^update %k', fk.updateAction);
} }
} }

View File

@ -1,8 +1,15 @@
const SqlDumper = require("../default/SqlDumper"); const SqlDumper = require('../default/SqlDumper');
class MsSqlDumper extends SqlDumper { class MsSqlDumper extends SqlDumper {
autoIncrement() { autoIncrement() {
this.put(" ^identity"); this.put(' ^identity');
}
putStringValue(value) {
if (/[^\u0000-\u00ff]/.test(value)) {
this.putRaw('N');
}
super.putStringValue(value);
} }
} }

View File

@ -7,6 +7,7 @@ const dialect = {
limitSelect: true, limitSelect: true,
rangeSelect: true, rangeSelect: true,
offsetFetchRangeSyntax: true, offsetFetchRangeSyntax: true,
stringEscapeChar: "'",
quoteIdentifier(s) { quoteIdentifier(s) {
return `[${s}]`; return `[${s}]`;
} }

View File

@ -4,6 +4,7 @@ const MySqlDumper = require("./MySqlDumper");
/** @type {import('@dbgate/types').SqlDialect} */ /** @type {import('@dbgate/types').SqlDialect} */
const dialect = { const dialect = {
rangeSelect: true, rangeSelect: true,
stringEscapeChar: "\\",
quoteIdentifier(s) { quoteIdentifier(s) {
return "`" + s + "`"; return "`" + s + "`";
} }

View File

@ -4,6 +4,7 @@ const PostgreDumper = require('./PostgreDumper');
/** @type {import('@dbgate/types').SqlDialect} */ /** @type {import('@dbgate/types').SqlDialect} */
const dialect = { const dialect = {
rangeSelect: true, rangeSelect: true,
stringEscapeChar: "\\",
quoteIdentifier(s) { quoteIdentifier(s) {
return '"' + s + '"'; return '"' + s + '"';
}, },

View File

@ -0,0 +1,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleFileExtensions: ['js'],
};

View File

@ -14,12 +14,16 @@
], ],
"devDependencies": { "devDependencies": {
"@dbgate/types": "^0.1.0", "@dbgate/types": "^0.1.0",
"@types/jest": "^25.1.4",
"@types/node": "^13.7.0", "@types/node": "^13.7.0",
"jest": "^25.1.0",
"ts-jest": "^25.2.1",
"typescript": "^3.7.5" "typescript": "^3.7.5"
}, },
"dependencies": { "dependencies": {
"@types/parsimmon": "^1.10.1", "@types/parsimmon": "^1.10.1",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"moment": "^2.24.0",
"parsimmon": "^1.13.0" "parsimmon": "^1.13.0"
} }
} }

View File

@ -0,0 +1,17 @@
import { parseFilter } from './parseFilter';
import { FilterType } from './types';
import engines from '@dbgate/engines';
import { dumpSqlCondition, treeToSql } from '@dbgate/sqltree';
const ast = parseFilter(process.argv[2], process.argv[3] as FilterType);
console.log(JSON.stringify(ast, null, ' '));
console.log('***************** MS SQL ******************');
console.log(treeToSql(engines('mssql'), ast, dumpSqlCondition));
console.log('***************** MySql *******************');
console.log(treeToSql(engines('mysql'), ast, dumpSqlCondition));
console.log('***************** Postgre *****************');
console.log(treeToSql(engines('postgres'), ast, dumpSqlCondition));

View File

@ -7,6 +7,10 @@ function token(parser) {
return parser.skip(whitespace); return parser.skip(whitespace);
} }
function word(str) {
return P.string(str).thru(token);
}
function interpretEscapes(str) { function interpretEscapes(str) {
let escapes = { let escapes = {
b: '\b', b: '\b',
@ -28,17 +32,61 @@ function interpretEscapes(str) {
}); });
} }
const parser = P.createLanguage({ const binaryCondition = operator => value => ({
expr: r => P.alt(r.string), conditionType: 'binary',
operator,
left: {
exprType: 'placeholder',
},
right: {
exprType: 'value',
value,
},
});
string: () => const compoudCondition = conditionType => conditions => {
if (conditions.length == 1) return conditions[0];
return {
conditionType,
conditions,
};
};
const parser = P.createLanguage({
string1: () =>
token(P.regexp(/"((?:\\.|.)*?)"/, 1)) token(P.regexp(/"((?:\\.|.)*?)"/, 1))
.map(interpretEscapes) .map(interpretEscapes)
.desc('string'), .map(binaryCondition('='))
.desc('string quoted'),
string2: () =>
token(P.regexp(/'((?:\\.|.)*?)'/, 1))
.map(interpretEscapes)
.map(binaryCondition('='))
.desc('string quoted'),
number: () =>
token(P.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/))
.map(Number)
.map(binaryCondition('='))
.desc('number'),
noQuotedString: () =>
P.regexp(/[^\s]+/)
.desc('string unquoted')
.map(binaryCondition('=')),
comma: () => word(','),
not: () => word('NOT'),
notNull: r => r.not.then(r.null).map(() => 'NOT_NULL'),
null: () => word('NULL'),
element: r => P.alt(r.string1, r.string2, r.null, r.notNull, r.number, r.noQuotedString).trim(whitespace),
factor: r => r.element.sepBy(whitespace).map(compoudCondition('and')),
list: r => r.factor.sepBy(r.comma).map(compoudCondition('or')),
}); });
export function parseFilter(value: string, filterType: FilterType) { export function parseFilter(value: string, filterType: FilterType) {
const ast = parser.expr.tryParse(value); const ast = parser.list.tryParse(value);
console.log(ast);
return ast; return ast;
} }

View File

@ -1,3 +1,7 @@
import parserFilter, { parseFilter } from './parseFilter'; import { parseFilter } from './parseFilter';
test('parse string', parseFilter('"123"', 'string')); test('parse string', () => {
const ast = parseFilter('"123"', 'string');
console.log(JSON.stringify(ast));
expect(ast).toBe(3);
});

View File

@ -1,3 +1,4 @@
import _ from 'lodash';
import { SqlDumper } from '@dbgate/types'; import { SqlDumper } from '@dbgate/types';
import { Expression, ColumnRefExpression } from './types'; import { Expression, ColumnRefExpression } from './types';
import { dumpSqlSourceRef } from './dumpSqlSource'; import { dumpSqlSourceRef } from './dumpSqlSource';
@ -14,5 +15,13 @@ export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
dmp.put('%i', expr.columnName); dmp.put('%i', expr.columnName);
} }
break; break;
case 'placeholder':
dmp.putRaw('{PLACEHOLDER}');
break;
case 'value':
dmp.put('%v', expr.value);
break;
} }
} }

View File

@ -70,7 +70,11 @@ export interface ValueExpression {
value: any; value: any;
} }
export type Expression = ColumnRefExpression | ValueExpression; export interface PlaceholderExpression {
exprType: 'placeholder';
}
export type Expression = ColumnRefExpression | ValueExpression | PlaceholderExpression;
export type OrderByExpression = Expression & { direction: 'ASC' | 'DESC' }; export type OrderByExpression = Expression & { direction: 'ASC' | 'DESC' };
export type ResultField = Expression & { alias?: string }; export type ResultField = Expression & { alias?: string };

View File

@ -1,6 +1,7 @@
export interface SqlDialect { export interface SqlDialect {
rangeSelect?: boolean; rangeSelect?: boolean;
limitSelect?: boolean; limitSelect?: boolean;
stringEscapeChar: string;
offsetFetchRangeSyntax?: boolean; offsetFetchRangeSyntax?: boolean;
quoteIdentifier(s: string): string; quoteIdentifier(s: string): string;
} }

View File

@ -8,6 +8,7 @@ export interface SqlDumper {
putRaw(s: string); putRaw(s: string);
put(format: string, ...args); put(format: string, ...args);
putCmd(format: string, ...args); putCmd(format: string, ...args);
putValue(value: string | number | Date);
putCollection<T>( putCollection<T>(
delimiter: string, delimiter: string,
collection: T[], collection: T[],

1153
yarn.lock

File diff suppressed because it is too large Load Diff