mirror of
https://github.com/dbgate/dbgate
synced 2024-11-07 12:13:57 +00:00
expression parser
This commit is contained in:
parent
e2c5c8163c
commit
064121376f
@ -1,57 +1,83 @@
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
class SqlDumper {
|
||||
/** @param driver {import('@dbgate/types').EngineDriver} */
|
||||
constructor(driver) {
|
||||
this.s = "";
|
||||
this.s = '';
|
||||
this.driver = driver;
|
||||
this.dialect = driver.dialect;
|
||||
this.indentLevel = 0;
|
||||
}
|
||||
endCommand() {
|
||||
this.putRaw(";\n");
|
||||
this.putRaw(';\n');
|
||||
}
|
||||
putRaw(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) {
|
||||
this.put(format, ...args);
|
||||
this.endCommand();
|
||||
}
|
||||
putFormattedValue(c, value) {
|
||||
switch (c) {
|
||||
case "s":
|
||||
case 's':
|
||||
if (value != null) {
|
||||
this.putRaw(value.toString());
|
||||
}
|
||||
break;
|
||||
case "i":
|
||||
case 'i':
|
||||
{
|
||||
this.putRaw(this.dialect.quoteIdentifier(value));
|
||||
}
|
||||
break;
|
||||
case "k":
|
||||
case 'k':
|
||||
{
|
||||
if (value) {
|
||||
this.putRaw(value.toUpperCase());
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "f":
|
||||
case 'f':
|
||||
{
|
||||
const { schemaName, pureName } = value;
|
||||
if (schemaName) {
|
||||
this.putRaw(this.dialect.quoteIdentifier(schemaName));
|
||||
this.putRaw(".");
|
||||
this.putRaw('.');
|
||||
}
|
||||
this.putRaw(this.dialect.quoteIdentifier(pureName));
|
||||
}
|
||||
break;
|
||||
case 'v':
|
||||
this.putValue(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
putFormattedList(c, collection) {
|
||||
if (!collection) return;
|
||||
this.putCollection(", ", collection, item =>
|
||||
this.putFormattedValue(c, item)
|
||||
);
|
||||
this.putCollection(', ', collection, item => this.putFormattedValue(c, item));
|
||||
}
|
||||
/** @param format {string} */
|
||||
put(format, ...args) {
|
||||
@ -62,20 +88,20 @@ class SqlDumper {
|
||||
let c = format[i];
|
||||
i++;
|
||||
switch (c) {
|
||||
case "^":
|
||||
case '^':
|
||||
while (i < length && format[i].match(/[a-z0-9_]/i)) {
|
||||
this.putRaw(format[i].toUpperCase());
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
case "%":
|
||||
case '%':
|
||||
c = format[i];
|
||||
i++;
|
||||
switch (c) {
|
||||
case "%":
|
||||
this.putRaw("%");
|
||||
case '%':
|
||||
this.putRaw('%');
|
||||
break;
|
||||
case ",":
|
||||
case ',':
|
||||
c = format[i];
|
||||
i++;
|
||||
this.putFormattedList(c, args[argIndex]);
|
||||
@ -87,22 +113,22 @@ class SqlDumper {
|
||||
|
||||
argIndex++;
|
||||
break;
|
||||
case "&":
|
||||
case '&':
|
||||
c = format[i];
|
||||
i++;
|
||||
switch (c) {
|
||||
case "&":
|
||||
this.putRaw("&");
|
||||
case '&':
|
||||
this.putRaw('&');
|
||||
break;
|
||||
case ">":
|
||||
case '>':
|
||||
this.indentLevel++;
|
||||
break;
|
||||
case "<":
|
||||
case '<':
|
||||
this.indentLevel--;
|
||||
break;
|
||||
case "n":
|
||||
this.putRaw("\n");
|
||||
this.putRaw(" ".repeat(2 * this.indentLevel));
|
||||
case 'n':
|
||||
this.putRaw('\n');
|
||||
this.putRaw(' '.repeat(2 * this.indentLevel));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
@ -114,36 +140,29 @@ class SqlDumper {
|
||||
}
|
||||
}
|
||||
autoIncrement() {
|
||||
this.put(" ^auto_increment");
|
||||
this.put(' ^auto_increment');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param column {import('@dbgate/types').ColumnInfo}
|
||||
*/
|
||||
columnDefinition(
|
||||
column,
|
||||
{
|
||||
includeDefault = true,
|
||||
includeNullable = true,
|
||||
includeCollate = true
|
||||
} = {}
|
||||
) {
|
||||
columnDefinition(column, { includeDefault = true, includeNullable = true, includeCollate = true } = {}) {
|
||||
if (column.computedExpression) {
|
||||
this.put("^as %s", column.computedExpression);
|
||||
if (column.isPersisted) this.put(" ^persisted");
|
||||
this.put('^as %s', column.computedExpression);
|
||||
if (column.isPersisted) this.put(' ^persisted');
|
||||
return;
|
||||
}
|
||||
this.put("%k", column.dataType);
|
||||
this.put('%k', column.dataType);
|
||||
if (column.autoIncrement) {
|
||||
this.autoIncrement();
|
||||
}
|
||||
|
||||
this.putRaw(" ");
|
||||
this.putRaw(' ');
|
||||
if (column.isSparse) {
|
||||
this.put(" ^sparse ");
|
||||
this.put(' ^sparse ');
|
||||
}
|
||||
if (includeNullable) {
|
||||
this.put(column.notNull ? "^not ^null" : "^null");
|
||||
this.put(column.notNull ? '^not ^null' : '^null');
|
||||
}
|
||||
if (includeDefault && column.defaultValue != null) {
|
||||
this.columnDefault(column);
|
||||
@ -155,13 +174,9 @@ class SqlDumper {
|
||||
*/
|
||||
columnDefault(column) {
|
||||
if (column.defaultConstraint != null) {
|
||||
this.put(
|
||||
" ^constraint %i ^default %s ",
|
||||
column.defaultConstraint,
|
||||
column.defaultValue
|
||||
);
|
||||
this.put(' ^constraint %i ^default %s ', column.defaultConstraint, column.defaultValue);
|
||||
} 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} */
|
||||
createTable(table) {
|
||||
this.put("^create ^table %f ( &>&n", table);
|
||||
this.putCollection(",&n", table.columns, col => {
|
||||
this.put("%i ", col.columnName);
|
||||
this.put('^create ^table %f ( &>&n', table);
|
||||
this.putCollection(',&n', table.columns, col => {
|
||||
this.put('%i ', col.columnName);
|
||||
this.columnDefinition(col);
|
||||
});
|
||||
if (table.primaryKey) {
|
||||
this.put(",&n");
|
||||
this.put(',&n');
|
||||
if (table.primaryKey.constraintName) {
|
||||
this.put("^constraint %i", table.primaryKey.constraintName);
|
||||
this.put('^constraint %i', table.primaryKey.constraintName);
|
||||
}
|
||||
this.put(
|
||||
" ^primary ^key (%,i)",
|
||||
' ^primary ^key (%,i)',
|
||||
table.primaryKey.columns.map(x => x.columnName)
|
||||
);
|
||||
}
|
||||
if (table.foreignKeys) {
|
||||
table.foreignKeys.forEach(fk => {
|
||||
this.put(",&n");
|
||||
this.put(',&n');
|
||||
this.createForeignKeyFore(fk);
|
||||
});
|
||||
}
|
||||
@ -215,7 +230,7 @@ class SqlDumper {
|
||||
// first = false;
|
||||
// CreateCheckCore(cnt);
|
||||
// }
|
||||
this.put("&<&n)");
|
||||
this.put('&<&n)');
|
||||
this.endCommand();
|
||||
// foreach (var ix in table.Indexes)
|
||||
// {
|
||||
@ -225,16 +240,15 @@ class SqlDumper {
|
||||
|
||||
/** @param fk {import('@dbgate/types').ForeignKeyInfo} */
|
||||
createForeignKeyFore(fk) {
|
||||
if (fk.constraintName != null)
|
||||
this.put("^constraint %i ", fk.constraintName);
|
||||
if (fk.constraintName != null) this.put('^constraint %i ', fk.constraintName);
|
||||
this.put(
|
||||
"^foreign ^key (%,i) ^references %f (%,i)",
|
||||
'^foreign ^key (%,i) ^references %f (%,i)',
|
||||
fk.columns.map(x => x.columnName),
|
||||
{ schemaName: fk.refSchemaName, pureName: fk.refTableName },
|
||||
fk.columns.map(x => x.refColumnName)
|
||||
);
|
||||
if (fk.deleteAction) this.put(" ^on ^delete %k", fk.deleteAction);
|
||||
if (fk.updateAction) this.put(" ^on ^update %k", fk.updateAction);
|
||||
if (fk.deleteAction) this.put(' ^on ^delete %k', fk.deleteAction);
|
||||
if (fk.updateAction) this.put(' ^on ^update %k', fk.updateAction);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,15 @@
|
||||
const SqlDumper = require("../default/SqlDumper");
|
||||
const SqlDumper = require('../default/SqlDumper');
|
||||
|
||||
class MsSqlDumper extends SqlDumper {
|
||||
autoIncrement() {
|
||||
this.put(" ^identity");
|
||||
this.put(' ^identity');
|
||||
}
|
||||
|
||||
putStringValue(value) {
|
||||
if (/[^\u0000-\u00ff]/.test(value)) {
|
||||
this.putRaw('N');
|
||||
}
|
||||
super.putStringValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ const dialect = {
|
||||
limitSelect: true,
|
||||
rangeSelect: true,
|
||||
offsetFetchRangeSyntax: true,
|
||||
stringEscapeChar: "'",
|
||||
quoteIdentifier(s) {
|
||||
return `[${s}]`;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ const MySqlDumper = require("./MySqlDumper");
|
||||
/** @type {import('@dbgate/types').SqlDialect} */
|
||||
const dialect = {
|
||||
rangeSelect: true,
|
||||
stringEscapeChar: "\\",
|
||||
quoteIdentifier(s) {
|
||||
return "`" + s + "`";
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ const PostgreDumper = require('./PostgreDumper');
|
||||
/** @type {import('@dbgate/types').SqlDialect} */
|
||||
const dialect = {
|
||||
rangeSelect: true,
|
||||
stringEscapeChar: "\\",
|
||||
quoteIdentifier(s) {
|
||||
return '"' + s + '"';
|
||||
},
|
||||
|
5
packages/filterparser/jest.config.js
Normal file
5
packages/filterparser/jest.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
moduleFileExtensions: ['js'],
|
||||
};
|
@ -14,12 +14,16 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@dbgate/types": "^0.1.0",
|
||||
"@types/jest": "^25.1.4",
|
||||
"@types/node": "^13.7.0",
|
||||
"jest": "^25.1.0",
|
||||
"ts-jest": "^25.2.1",
|
||||
"typescript": "^3.7.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/parsimmon": "^1.10.1",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.24.0",
|
||||
"parsimmon": "^1.13.0"
|
||||
}
|
||||
}
|
||||
|
17
packages/filterparser/src/cli.ts
Normal file
17
packages/filterparser/src/cli.ts
Normal 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));
|
@ -7,6 +7,10 @@ function token(parser) {
|
||||
return parser.skip(whitespace);
|
||||
}
|
||||
|
||||
function word(str) {
|
||||
return P.string(str).thru(token);
|
||||
}
|
||||
|
||||
function interpretEscapes(str) {
|
||||
let escapes = {
|
||||
b: '\b',
|
||||
@ -28,17 +32,61 @@ function interpretEscapes(str) {
|
||||
});
|
||||
}
|
||||
|
||||
const parser = P.createLanguage({
|
||||
expr: r => P.alt(r.string),
|
||||
const binaryCondition = operator => value => ({
|
||||
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))
|
||||
.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) {
|
||||
const ast = parser.expr.tryParse(value);
|
||||
console.log(ast);
|
||||
const ast = parser.list.tryParse(value);
|
||||
return ast;
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import { SqlDumper } from '@dbgate/types';
|
||||
import { Expression, ColumnRefExpression } from './types';
|
||||
import { dumpSqlSourceRef } from './dumpSqlSource';
|
||||
@ -14,5 +15,13 @@ export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
|
||||
dmp.put('%i', expr.columnName);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'placeholder':
|
||||
dmp.putRaw('{PLACEHOLDER}');
|
||||
break;
|
||||
|
||||
case 'value':
|
||||
dmp.put('%v', expr.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +70,11 @@ export interface ValueExpression {
|
||||
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 ResultField = Expression & { alias?: string };
|
||||
|
1
packages/types/dialect.d.ts
vendored
1
packages/types/dialect.d.ts
vendored
@ -1,6 +1,7 @@
|
||||
export interface SqlDialect {
|
||||
rangeSelect?: boolean;
|
||||
limitSelect?: boolean;
|
||||
stringEscapeChar: string;
|
||||
offsetFetchRangeSyntax?: boolean;
|
||||
quoteIdentifier(s: string): string;
|
||||
}
|
||||
|
1
packages/types/dumper.d.ts
vendored
1
packages/types/dumper.d.ts
vendored
@ -8,6 +8,7 @@ export interface SqlDumper {
|
||||
putRaw(s: string);
|
||||
put(format: string, ...args);
|
||||
putCmd(format: string, ...args);
|
||||
putValue(value: string | number | Date);
|
||||
putCollection<T>(
|
||||
delimiter: string,
|
||||
collection: T[],
|
||||
|
Loading…
Reference in New Issue
Block a user