dialect, dumper

This commit is contained in:
Jan Prochazka 2020-02-02 19:27:25 +01:00
parent 9f3679fefb
commit 6188e90340
18 changed files with 294 additions and 10 deletions

13
api/src/dmlf/command.js Normal file
View File

@ -0,0 +1,13 @@
class Command {
/** @param driver {import('dbgate').EngineDriver} */
toSql(driver) {
const dumper = driver.createDumper();
this.dumpSql(dumper);
return dumper.s;
}
/** @param dumper {import('dbgate').SqlDumper} */
dumpSql(dumper) {}
}
module.exports = Command;

37
api/src/dmlf/select.js Normal file
View File

@ -0,0 +1,37 @@
const Command = require('./command');
class Select extends Command {
constructor() {
super();
/** @type {number} */
this.topRecords = undefined;
/** @type {import('dbgate').NamedObjectInfo} */
this.from = undefined;
/** @type {import('dbgate').RangeDefinition} */
this.range = undefined;
this.distinct = false;
this.selectAll = false;
}
/** @param dumper {import('dbgate').SqlDumper} */
dumpSql(dumper) {
dumper.put('^select ');
if (this.topRecords) {
dumper.put('^top %s ', this.topRecords);
}
if (this.distinct) {
dumper.put('^distinct ');
}
if (this.selectAll) {
dumper.put('* ');
} else {
// TODO
}
dumper.put('^from %f ', this.from);
if (this.range) {
dumper.put('^limit %s ^offset %s ', this.range.limit, this.range.offset);
}
}
}
module.exports = Select;

View File

@ -0,0 +1,64 @@
class SqlDumper {
/** @param driver {import('dbgate').EngineDriver} */
constructor(driver) {
this.s = '';
this.driver = driver;
this.dialect = driver.dialect;
}
putRaw(text) {
this.s += text;
}
putCmd(text) {
this.putRaw(text);
this.putRaw(';\n');
}
putFormattedValue(c, value) {
switch (c) {
case 's':
if (value != null) {
this.putRaw(value.toString());
}
break;
case 'f':
{
const { schemaName, pureName } = value;
if (schemaName) {
this.putRaw(this.dialect.quoteIdentifier(schemaName));
this.putRaw('.');
}
this.putRaw(this.dialect.quoteIdentifier(pureName));
}
break;
}
}
/** @param format {string} */
put(format, ...args) {
let i = 0;
let argIndex = 0;
const length = format.length;
while (i < length) {
let c = format[i];
i++;
switch (c) {
case '^':
while (i < length && format[i].match(/[a-z]/i)) {
this.putRaw(format[i].toUpperCase());
i++;
}
break;
case '%':
c = format[i];
i++;
this.putFormattedValue(c, args[argIndex]);
argIndex++;
break;
default:
this.putRaw(c);
break;
}
}
}
}
module.exports = SqlDumper;

View File

@ -0,0 +1,5 @@
const SqlDumper = require('../default/SqlDumper');
class MsSqlDumper extends SqlDumper {}
module.exports = MsSqlDumper;

View File

@ -1,8 +1,18 @@
const _ = require('lodash');
const mssql = require('mssql');
const MsSqlAnalyser = require('./MsSqlAnalyser');
const MsSqlDumper = require('./MsSqlDumper');
module.exports = {
/** @type {import('dbgate').SqlDialect} */
const dialect = {
limitSelect: true,
quoteIdentifier(s) {
return `[${s}]`;
},
};
/** @type {import('dbgate').EngineDriver} */
const driver = {
async connect({ server, port, user, password, database }) {
const pool = await mssql.connect({ server, port, user, password, database });
return pool;
@ -26,5 +36,11 @@ module.exports = {
await analyser.runAnalysis();
return analyser.result;
},
async analyseIncremental(pool) {},
// async analyseIncremental(pool) {},
createDumper() {
return new MsSqlDumper(this);
},
dialect,
};
module.exports = driver;

View File

@ -0,0 +1,54 @@
const fs = require('fs-extra');
const fp = require('lodash/fp');
const path = require('path');
const _ = require('lodash');
const DatabaseAnalayser = require('../default/DatabaseAnalyser');
/** @returns {Promise<string>} */
async function loadQuery(name) {
return await fs.readFile(path.join(__dirname, name), 'utf-8');
}
class MySqlAnalyser extends DatabaseAnalayser {
constructor(pool, driver) {
super(pool, driver);
}
async createQuery(
resFileName,
tables = false,
views = false,
procedures = false,
functions = false,
triggers = false
) {
console.log('DB', this.pool._database_name);
let res = await loadQuery(resFileName);
res = res.replace('=[OBJECT_NAME_CONDITION]', ' is not null');
res = res.replace('#DATABASE#', this.pool._database_name);
return res;
}
async runAnalysis() {
const tables = await this.driver.query(this.pool, await this.createQuery('tables.sql'));
const columns = await this.driver.query(this.pool, await this.createQuery('columns.sql'));
// const pkColumns = await this.driver.query(this.pool, await this.createQuery('primary_keys.sql'));
// const fkColumns = await this.driver.query(this.pool, await this.createQuery('foreign_keys.sql'));
this.result.tables = tables.rows.map(table => ({
...table,
columns: columns.rows
.filter(col => col.objectId == table.objectId)
.map(({ isNullable, extra, ...col }) => ({
...col,
notNull: !isNullable,
autoIncrement: extra && extra.toLowerCase().includes('auto_increment'),
})),
foreignKeys: [],
// primaryKey: extractPrimaryKeys(table, pkColumns.rows),
// foreignKeys: extractForeignKeys(table, fkColumns.rows),
}));
}
}
module.exports = MySqlAnalyser;

View File

@ -0,0 +1,5 @@
const SqlDumper = require('../default/SqlDumper');
class MySqlDumper extends SqlDumper {}
module.exports = MySqlDumper;

View File

@ -0,0 +1,13 @@
select
TABLE_NAME as pureName,
COLUMN_NAME as columnName,
IS_NULLABLE as isNullable,
DATA_TYPE as dataType,
CHARACTER_MAXIMUM_LENGTH,
NUMERIC_PRECISION,
NUMERIC_SCALE,
COLUMN_DEFAULT,
EXTRA as extra
from INFORMATION_SCHEMA.COLUMNS
where TABLE_SCHEMA = '#DATABASE#' and TABLE_NAME =[OBJECT_NAME_CONDITION]
order by ORDINAL_POSITION

View File

@ -1,8 +1,20 @@
const mysql = require('mysql');
const MySqlAnalyser = require('./MySqlAnalyser');
const MySqlDumper = require('./MySqlDumper');
module.exports = {
/** @type {import('dbgate').SqlDialect} */
const dialect = {
rangeSelect: true,
quoteIdentifier(s) {
return '`' + s + '`';
},
};
/** @type {import('dbgate').EngineDriver} */
const driver = {
async connect({ server, port, user, password, database }) {
const connection = mysql.createConnection({ host: server, port, user, password, database });
connection._database_name = database;
return connection;
},
async query(connection, sql) {
@ -18,8 +30,19 @@ module.exports = {
const version = rows[0].Value;
return { version };
},
async analyseFull(pool) {
const analyser = new MySqlAnalyser(pool, this);
await analyser.runAnalysis();
return analyser.result;
},
async listDatabases(connection) {
const { rows } = await this.query(connection, 'show databases');
return rows.map(x => ({ name: x.Database }));
},
createDumper() {
return new MySqlDumper(this);
},
dialect,
};
module.exports = driver;

View File

@ -0,0 +1,5 @@
select
TABLE_NAME as pureName,
case when ENGINE='InnoDB' then CREATE_TIME else coalesce(UPDATE_TIME, CREATE_TIME) end as alterTime
from information_schema.tables
where TABLE_SCHEMA = '#DATABASE#' and TABLE_NAME =[OBJECT_NAME_CONDITION];

View File

@ -0,0 +1,5 @@
const SqlDumper = require('../default/SqlDumper');
class PostgreDumper extends SqlDumper {}
module.exports = PostgreDumper;

View File

@ -1,5 +1,13 @@
const { Client } = require('pg');
/** @type {import('dbgate').SqlDialect} */
const dialect = {
rangeSelect: true,
quoteIdentifier(s) {
return '"' + s + '"';
},
};
module.exports = {
async connect({ server, port, user, password, database }) {
const client = new Client({ host: server, port, user, password, database: database || 'postgres' });
@ -19,4 +27,5 @@ module.exports = {
const { rows } = await this.query(client, 'SELECT datname AS name FROM pg_database WHERE datistemplate = false');
return rows;
},
dialect,
};

View File

@ -1,4 +1,5 @@
const engines = require('../engines');
const Select = require('../dmlf/select');
let systemConnection;
let storedConnection;
@ -33,7 +34,16 @@ function waitConnected() {
async function handleTableData({ msgid, schemaName, pureName }) {
await waitConnected();
const driver = engines(storedConnection);
const res = await driver.query(systemConnection, `SELECT TOP(100) * FROM ${pureName}`);
const select = new Select();
if (driver.dialect.limitSelect) select.topRecords = 100;
if (driver.dialect.rangeSelect) select.range = { offset: 0, limit: 100 };
select.from = { schemaName, pureName };
select.selectAll = true;
const sql = select.toSql(driver);
console.log('SQL', sql);
const res = await driver.query(systemConnection, sql);
process.send({ msgtype: 'response', msgid, ...res });
}

5
types/dialect.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
export interface SqlDialect {
rangeSelect?: boolean;
limitSelect?: boolean;
quoteIdentifier(s: string): string;
}

5
types/dumper.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
export interface SqlDumper {
s: string;
put(format: string, ...args);
putCmd(format: string, ...args);
}

16
types/engines.d.ts vendored
View File

@ -1,18 +1,24 @@
import { QueryResult } from "./query";
import { SqlDialect } from "./dialect";
import { SqlDumper } from "./dumper";
import { DatabaseInfo } from "./dbinfo";
export interface EngineDriver {
connect({
server,
port,
user,
password
password,
database
}: {
server: any;
port: any;
user: any;
password: any;
database: any;
}): any;
query(pool: any, sql: string): Promise<QueryResult>;
getVersion(pool: any): Promise<string>;
getVersion(pool: any): Promise<{ version: string }>;
listDatabases(
pool: any
): Promise<
@ -20,6 +26,8 @@ export interface EngineDriver {
name: string;
}[]
>;
analyseFull(pool: any): Promise<void>;
analyseIncremental(pool: any): Promise<void>;
analyseFull(pool: any): Promise<DatabaseInfo>;
// analyseIncremental(pool: any): Promise<void>;
dialect: SqlDialect;
createDumper(): SqlDumper;
}

2
types/index.d.ts vendored
View File

@ -9,3 +9,5 @@ export interface OpenedDatabaseConnection {
export * from "./engines";
export * from "./dbinfo";
export * from "./query";
export * from "./dialect";
export * from "./dumper";

9
types/query.d.ts vendored
View File

@ -1,3 +1,8 @@
export interface QueryResult {
rows: any[];
export interface RangeDefinition {
offset: number;
limit: number;
}
export interface QueryResult {
rows: any[];
}