mirror of
https://github.com/Fabio286/antares.git
synced 2025-06-05 21:59:22 +02:00
feat: implement a better query splitter for SQL queries, fixes #926
This commit is contained in:
@ -34,6 +34,7 @@ export interface ClientParams {
|
|||||||
| { databasePath: string; readonly: boolean };
|
| { databasePath: string; readonly: boolean };
|
||||||
poolSize?: number;
|
poolSize?: number;
|
||||||
logger?: () => void;
|
logger?: () => void;
|
||||||
|
querySplitter?: (sql: string, clieng?: string) => string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
86
src/common/libs/querySplitter.ts
Normal file
86
src/common/libs/querySplitter.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { ClientCode } from 'common/interfaces/antares';
|
||||||
|
|
||||||
|
export const querySplitter =(sql: string, dbType: ClientCode): string[] => {
|
||||||
|
const queries: string[] = [];
|
||||||
|
let currentQuery = '';
|
||||||
|
let insideBlock = false;
|
||||||
|
let insideString = false;
|
||||||
|
let stringDelimiter: string | null = null;
|
||||||
|
let insideDollarTag = false;
|
||||||
|
let dollarTagDelimiter: string | null = null;
|
||||||
|
|
||||||
|
// Regex patterns for BEGIN-END blocks, dollar tags in PostgreSQL, and semicolons
|
||||||
|
const beginRegex = /\bBEGIN\b/i;
|
||||||
|
const endRegex = /\bEND\b;/i;
|
||||||
|
const dollarTagRegex = /\$(\w+)?\$/; // Matches $tag$ or $$
|
||||||
|
|
||||||
|
// Split on semicolons, keeping semicolons attached to the lines
|
||||||
|
const lines = sql.split(/(?<=;)/);
|
||||||
|
|
||||||
|
for (let line of lines) {
|
||||||
|
line = line.trim();
|
||||||
|
|
||||||
|
if (!line) continue;
|
||||||
|
|
||||||
|
for (let i = 0; i < line.length; i++) {
|
||||||
|
const char = line[i];
|
||||||
|
|
||||||
|
// Handle string boundaries
|
||||||
|
if ((char === '\'' || char === '"') && (!insideString || char === stringDelimiter)) {
|
||||||
|
if (!insideString) {
|
||||||
|
insideString = true;
|
||||||
|
stringDelimiter = char;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
insideString = false;
|
||||||
|
stringDelimiter = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentQuery += char;
|
||||||
|
|
||||||
|
if (dbType === 'pg') {
|
||||||
|
// Handle dollar-quoted blocks in PostgreSQL
|
||||||
|
if (!insideString && line.slice(i).match(dollarTagRegex)) {
|
||||||
|
const match = line.slice(i).match(dollarTagRegex);
|
||||||
|
if (match) {
|
||||||
|
const tag = match[0];
|
||||||
|
if (!insideDollarTag) {
|
||||||
|
insideDollarTag = true;
|
||||||
|
dollarTagDelimiter = tag;
|
||||||
|
currentQuery += tag;
|
||||||
|
i += tag.length - 1;
|
||||||
|
}
|
||||||
|
else if (dollarTagDelimiter === tag) {
|
||||||
|
insideDollarTag = false;
|
||||||
|
dollarTagDelimiter = null;
|
||||||
|
currentQuery += tag;
|
||||||
|
i += tag.length - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check BEGIN-END blocks
|
||||||
|
if (!insideString && !insideDollarTag) {
|
||||||
|
if (beginRegex.test(line))
|
||||||
|
insideBlock = true;
|
||||||
|
|
||||||
|
if (insideBlock && endRegex.test(line))
|
||||||
|
insideBlock = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the query if we encounter a semicolon outside a BEGIN-END block, outside a string, and outside dollar tags
|
||||||
|
if (!insideBlock && !insideString && !insideDollarTag && /;\s*$/.test(line)) {
|
||||||
|
queries.push(currentQuery.trim());
|
||||||
|
currentQuery = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any remaining query
|
||||||
|
if (currentQuery.trim())
|
||||||
|
queries.push(currentQuery.trim());
|
||||||
|
|
||||||
|
return queries;
|
||||||
|
};
|
@ -2,27 +2,9 @@ import * as antares from 'common/interfaces/antares';
|
|||||||
import mysql from 'mysql2/promise';
|
import mysql from 'mysql2/promise';
|
||||||
import * as pg from 'pg';
|
import * as pg from 'pg';
|
||||||
import SSH2Promise = require('@fabio286/ssh2-promise');
|
import SSH2Promise = require('@fabio286/ssh2-promise');
|
||||||
|
import { querySplitter } from 'common/libs/querySplitter';
|
||||||
|
|
||||||
export type LoggerLevel = 'query' | 'error'
|
import { ipcLogger, LoggerLevel } from '../misc/ipcLogger';
|
||||||
|
|
||||||
const ipcLogger = ({ content, cUid, level }: {content: string; cUid: string; level: LoggerLevel}) => {
|
|
||||||
if (level === 'error') {
|
|
||||||
if (process.type !== undefined) {
|
|
||||||
const mainWindow = require('electron').webContents.fromId(1);
|
|
||||||
mainWindow.send('non-blocking-exception', { cUid, message: content, date: new Date() });
|
|
||||||
}
|
|
||||||
if (process.env.NODE_ENV === 'development' && process.type === 'browser') console.log(content);
|
|
||||||
}
|
|
||||||
else if (level === 'query') {
|
|
||||||
// Remove comments, newlines and multiple spaces
|
|
||||||
const escapedSql = content.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '').replace(/\s\s+/g, ' ');
|
|
||||||
if (process.type !== undefined) {
|
|
||||||
const mainWindow = require('electron').webContents.fromId(1);
|
|
||||||
mainWindow.send('query-log', { cUid, sql: escapedSql, date: new Date() });
|
|
||||||
}
|
|
||||||
if (process.env.NODE_ENV === 'development' && process.type === 'browser') console.log(escapedSql);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* As Simple As Possible Query Builder Core
|
* As Simple As Possible Query Builder Core
|
||||||
@ -34,6 +16,7 @@ export abstract class BaseClient {
|
|||||||
protected _poolSize: number;
|
protected _poolSize: number;
|
||||||
protected _ssh?: SSH2Promise;
|
protected _ssh?: SSH2Promise;
|
||||||
protected _logger: (args: {content: string; cUid: string; level: LoggerLevel}) => void;
|
protected _logger: (args: {content: string; cUid: string; level: LoggerLevel}) => void;
|
||||||
|
protected _querySplitter: (sql: string, client: antares.ClientCode) => string[];
|
||||||
protected _queryDefaults: antares.QueryBuilderObject;
|
protected _queryDefaults: antares.QueryBuilderObject;
|
||||||
protected _query: antares.QueryBuilderObject;
|
protected _query: antares.QueryBuilderObject;
|
||||||
|
|
||||||
@ -43,6 +26,7 @@ export abstract class BaseClient {
|
|||||||
this._params = args.params;
|
this._params = args.params;
|
||||||
this._poolSize = args.poolSize || undefined;
|
this._poolSize = args.poolSize || undefined;
|
||||||
this._logger = args.logger || ipcLogger;
|
this._logger = args.logger || ipcLogger;
|
||||||
|
this._querySplitter = args.querySplitter || querySplitter;
|
||||||
|
|
||||||
this._queryDefaults = {
|
this._queryDefaults = {
|
||||||
schema: '',
|
schema: '',
|
||||||
|
@ -245,10 +245,10 @@ export class FirebirdSQLClient extends BaseClient {
|
|||||||
name: db.name,
|
name: db.name,
|
||||||
size: schemaSize,
|
size: schemaSize,
|
||||||
tables: remappedTables,
|
tables: remappedTables,
|
||||||
functions: [],
|
functions: [] as null[],
|
||||||
procedures: remappedProcedures,
|
procedures: remappedProcedures,
|
||||||
triggers: remappedTriggers,
|
triggers: remappedTriggers,
|
||||||
schedulers: []
|
schedulers: [] as null[]
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -337,7 +337,7 @@ export class FirebirdSQLClient extends BaseClient {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
name: field.FIELD_NAME.trim(),
|
name: field.FIELD_NAME.trim(),
|
||||||
key: null,
|
key: null as null,
|
||||||
type: fieldType,
|
type: fieldType,
|
||||||
schema: schema,
|
schema: schema,
|
||||||
table: table,
|
table: table,
|
||||||
@ -346,14 +346,14 @@ export class FirebirdSQLClient extends BaseClient {
|
|||||||
datePrecision: field.FIELD_NAME.trim() === 'TIMESTAMP' ? 4 : null,
|
datePrecision: field.FIELD_NAME.trim() === 'TIMESTAMP' ? 4 : null,
|
||||||
charLength: ![...NUMBER, ...FLOAT].includes(fieldType) ? field.FIELD_LENGTH : null,
|
charLength: ![...NUMBER, ...FLOAT].includes(fieldType) ? field.FIELD_LENGTH : null,
|
||||||
nullable: !field.NOT_NULL,
|
nullable: !field.NOT_NULL,
|
||||||
unsigned: null,
|
unsigned: null as null,
|
||||||
zerofill: null,
|
zerofill: null as null,
|
||||||
order: field.FIELD_POSITION+1,
|
order: field.FIELD_POSITION+1,
|
||||||
default: defaultValue,
|
default: defaultValue,
|
||||||
charset: field.CHARSET,
|
charset: field.CHARSET,
|
||||||
collation: null,
|
collation: null as null,
|
||||||
autoIncrement: false,
|
autoIncrement: false,
|
||||||
onUpdate: null,
|
onUpdate: null as null,
|
||||||
comment: field.DESCRIPTION?.trim()
|
comment: field.DESCRIPTION?.trim()
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -457,7 +457,7 @@ export class FirebirdSQLClient extends BaseClient {
|
|||||||
table: table,
|
table: table,
|
||||||
field: field.FKCOLUMN_NAME.trim(),
|
field: field.FKCOLUMN_NAME.trim(),
|
||||||
position: field.KEY_SEQ,
|
position: field.KEY_SEQ,
|
||||||
constraintPosition: null,
|
constraintPosition: null as null,
|
||||||
constraintName: field.FK_NAME.trim(),
|
constraintName: field.FK_NAME.trim(),
|
||||||
refSchema: schema,
|
refSchema: schema,
|
||||||
refTable: field.PKTABLE_NAME.trim(),
|
refTable: field.PKTABLE_NAME.trim(),
|
||||||
@ -1041,9 +1041,7 @@ export class FirebirdSQLClient extends BaseClient {
|
|||||||
const resultsArr = [];
|
const resultsArr = [];
|
||||||
let paramsArr = [];
|
let paramsArr = [];
|
||||||
const queries = args.split
|
const queries = args.split
|
||||||
? sql.split(/((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/gm)
|
? this._querySplitter(sql, 'firebird')
|
||||||
.filter(Boolean)
|
|
||||||
.map(q => q.trim())
|
|
||||||
: [sql];
|
: [sql];
|
||||||
|
|
||||||
let connection: firebird.Database | firebird.Transaction;
|
let connection: firebird.Database | firebird.Transaction;
|
||||||
|
@ -1755,9 +1755,7 @@ export class MySQLClient extends BaseClient {
|
|||||||
const resultsArr: antares.QueryResult[] = [];
|
const resultsArr: antares.QueryResult[] = [];
|
||||||
let paramsArr = [];
|
let paramsArr = [];
|
||||||
const queries = args.split
|
const queries = args.split
|
||||||
? sql.split(/((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/gm)
|
? this._querySplitter(sql, 'mysql')
|
||||||
.filter(Boolean)
|
|
||||||
.map(q => q.trim())
|
|
||||||
: [sql];
|
: [sql];
|
||||||
|
|
||||||
const connection = await this.getConnection(args);
|
const connection = await this.getConnection(args);
|
||||||
|
@ -466,7 +466,7 @@ export class PostgreSQLClient extends BaseClient {
|
|||||||
procedures: remappedProcedures,
|
procedures: remappedProcedures,
|
||||||
triggers: remappedTriggers,
|
triggers: remappedTriggers,
|
||||||
triggerFunctions: remappedTriggerFunctions,
|
triggerFunctions: remappedTriggerFunctions,
|
||||||
schedulers: []
|
schedulers: [] as null[]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -532,7 +532,7 @@ export class PostgreSQLClient extends BaseClient {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
name: field.column_name,
|
name: field.column_name,
|
||||||
key: null,
|
key: null as null,
|
||||||
type: type.toUpperCase(),
|
type: type.toUpperCase(),
|
||||||
isArray,
|
isArray,
|
||||||
schema: field.table_schema,
|
schema: field.table_schema,
|
||||||
@ -542,14 +542,14 @@ export class PostgreSQLClient extends BaseClient {
|
|||||||
datePrecision: field.datetime_precision,
|
datePrecision: field.datetime_precision,
|
||||||
charLength: field.character_maximum_length,
|
charLength: field.character_maximum_length,
|
||||||
nullable: field.is_nullable.includes('YES'),
|
nullable: field.is_nullable.includes('YES'),
|
||||||
unsigned: null,
|
unsigned: null as null,
|
||||||
zerofill: null,
|
zerofill: null as null,
|
||||||
order: field.ordinal_position,
|
order: field.ordinal_position,
|
||||||
default: field.column_default,
|
default: field.column_default,
|
||||||
charset: field.character_set_name,
|
charset: field.character_set_name,
|
||||||
collation: field.collation_name,
|
collation: field.collation_name,
|
||||||
autoIncrement: false,
|
autoIncrement: false,
|
||||||
onUpdate: null,
|
onUpdate: null as null,
|
||||||
comment: field.column_comment
|
comment: field.column_comment
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -1252,9 +1252,9 @@ export class PostgreSQLClient extends BaseClient {
|
|||||||
return results.rows.map(async row => {
|
return results.rows.map(async row => {
|
||||||
if (!row.pg_get_functiondef) {
|
if (!row.pg_get_functiondef) {
|
||||||
return {
|
return {
|
||||||
definer: null,
|
definer: null as null,
|
||||||
sql: '',
|
sql: '',
|
||||||
parameters: [],
|
parameters: [] as null[],
|
||||||
name: routine,
|
name: routine,
|
||||||
comment: '',
|
comment: '',
|
||||||
security: 'DEFINER',
|
security: 'DEFINER',
|
||||||
@ -1303,8 +1303,8 @@ export class PostgreSQLClient extends BaseClient {
|
|||||||
name: routine,
|
name: routine,
|
||||||
comment: '',
|
comment: '',
|
||||||
security: row.pg_get_functiondef.includes('SECURITY DEFINER') ? 'DEFINER' : 'INVOKER',
|
security: row.pg_get_functiondef.includes('SECURITY DEFINER') ? 'DEFINER' : 'INVOKER',
|
||||||
deterministic: null,
|
deterministic: null as null,
|
||||||
dataAccess: null,
|
dataAccess: null as null,
|
||||||
language: row.pg_get_functiondef.match(/(?<=LANGUAGE )(.*)(?<=[\S+\n\r\s])/gm)[0]
|
language: row.pg_get_functiondef.match(/(?<=LANGUAGE )(.*)(?<=[\S+\n\r\s])/gm)[0]
|
||||||
};
|
};
|
||||||
})[0];
|
})[0];
|
||||||
@ -1368,9 +1368,9 @@ export class PostgreSQLClient extends BaseClient {
|
|||||||
return results.rows.map(async row => {
|
return results.rows.map(async row => {
|
||||||
if (!row.pg_get_functiondef) {
|
if (!row.pg_get_functiondef) {
|
||||||
return {
|
return {
|
||||||
definer: null,
|
definer: null as null,
|
||||||
sql: '',
|
sql: '',
|
||||||
parameters: [],
|
parameters: [] as null[],
|
||||||
name: func,
|
name: func,
|
||||||
comment: '',
|
comment: '',
|
||||||
security: 'DEFINER',
|
security: 'DEFINER',
|
||||||
@ -1418,8 +1418,8 @@ export class PostgreSQLClient extends BaseClient {
|
|||||||
name: func,
|
name: func,
|
||||||
comment: '',
|
comment: '',
|
||||||
security: row.pg_get_functiondef.includes('SECURITY DEFINER') ? 'DEFINER' : 'INVOKER',
|
security: row.pg_get_functiondef.includes('SECURITY DEFINER') ? 'DEFINER' : 'INVOKER',
|
||||||
deterministic: null,
|
deterministic: null as null,
|
||||||
dataAccess: null,
|
dataAccess: null as null,
|
||||||
language: row.pg_get_functiondef.match(/(?<=LANGUAGE )(.*)(?<=[\S+\n\r\s])/gm)[0],
|
language: row.pg_get_functiondef.match(/(?<=LANGUAGE )(.*)(?<=[\S+\n\r\s])/gm)[0],
|
||||||
returns: row.pg_get_functiondef.match(/(?<=RETURNS )(.*)(?<=[\S+\n\r\s])/gm)[0].replace('SETOF ', '').toUpperCase()
|
returns: row.pg_get_functiondef.match(/(?<=RETURNS )(.*)(?<=[\S+\n\r\s])/gm)[0].replace('SETOF ', '').toUpperCase()
|
||||||
};
|
};
|
||||||
@ -1665,9 +1665,7 @@ export class PostgreSQLClient extends BaseClient {
|
|||||||
const resultsArr: antares.QueryResult[] = [];
|
const resultsArr: antares.QueryResult[] = [];
|
||||||
let paramsArr = [];
|
let paramsArr = [];
|
||||||
const queries = args.split
|
const queries = args.split
|
||||||
? sql.split(/(?!\B'[^']*);(?![^']*'\B)/gm)
|
? this._querySplitter(sql, 'pg')
|
||||||
.filter(Boolean)
|
|
||||||
.map(q => q.trim())
|
|
||||||
: [sql];
|
: [sql];
|
||||||
|
|
||||||
let connection: pg.Client | pg.PoolClient;
|
let connection: pg.Client | pg.PoolClient;
|
||||||
|
@ -124,10 +124,10 @@ export class SQLiteClient extends BaseClient {
|
|||||||
name: db.name,
|
name: db.name,
|
||||||
size: schemaSize,
|
size: schemaSize,
|
||||||
tables: remappedTables,
|
tables: remappedTables,
|
||||||
functions: [],
|
functions: [] as null[],
|
||||||
procedures: [],
|
procedures: [] as null[],
|
||||||
triggers: remappedTriggers,
|
triggers: remappedTriggers,
|
||||||
schedulers: []
|
schedulers: [] as null[]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -166,22 +166,22 @@ export class SQLiteClient extends BaseClient {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
name: field.name,
|
name: field.name,
|
||||||
key: null,
|
key: null as null,
|
||||||
type: type.trim(),
|
type: type.trim(),
|
||||||
schema: schema,
|
schema: schema,
|
||||||
table: table,
|
table: table,
|
||||||
numLength: [...NUMBER, ...FLOAT].includes(type) ? length : null,
|
numLength: [...NUMBER, ...FLOAT].includes(type) ? length : null,
|
||||||
datePrecision: null,
|
datePrecision: null as null,
|
||||||
charLength: ![...NUMBER, ...FLOAT].includes(type) ? length : null,
|
charLength: ![...NUMBER, ...FLOAT].includes(type) ? length : null,
|
||||||
nullable: !field.notnull,
|
nullable: !field.notnull,
|
||||||
unsigned: null,
|
unsigned: null as null,
|
||||||
zerofill: null,
|
zerofill: null as null,
|
||||||
order: typeof field.cid === 'string' ? +field.cid + 1 : field.cid + 1,
|
order: typeof field.cid === 'string' ? +field.cid + 1 : field.cid + 1,
|
||||||
default: field.dflt_value,
|
default: field.dflt_value,
|
||||||
charset: null,
|
charset: null as null,
|
||||||
collation: null,
|
collation: null as null,
|
||||||
autoIncrement: false,
|
autoIncrement: false,
|
||||||
onUpdate: null,
|
onUpdate: null as null,
|
||||||
comment: ''
|
comment: ''
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -267,7 +267,7 @@ export class SQLiteClient extends BaseClient {
|
|||||||
table: table,
|
table: table,
|
||||||
field: field.from,
|
field: field.from,
|
||||||
position: field.id + 1,
|
position: field.id + 1,
|
||||||
constraintPosition: null,
|
constraintPosition: null as null,
|
||||||
constraintName: field.id,
|
constraintName: field.id,
|
||||||
refSchema: schema,
|
refSchema: schema,
|
||||||
refTable: field.table,
|
refTable: field.table,
|
||||||
@ -629,9 +629,7 @@ export class SQLiteClient extends BaseClient {
|
|||||||
const resultsArr = [];
|
const resultsArr = [];
|
||||||
let paramsArr = [];
|
let paramsArr = [];
|
||||||
const queries = args.split
|
const queries = args.split
|
||||||
? sql.split(/((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/gm)
|
? this._querySplitter(sql, 'sqlite')
|
||||||
.filter(Boolean)
|
|
||||||
.map(q => q.trim())
|
|
||||||
: [sql];
|
: [sql];
|
||||||
|
|
||||||
let connection: sqlite.Database;
|
let connection: sqlite.Database;
|
||||||
|
20
src/main/libs/misc/ipcLogger.ts
Normal file
20
src/main/libs/misc/ipcLogger.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export type LoggerLevel = 'query' | 'error'
|
||||||
|
|
||||||
|
export const ipcLogger = ({ content, cUid, level }: {content: string; cUid: string; level: LoggerLevel}) => {
|
||||||
|
if (level === 'error') {
|
||||||
|
if (process.type !== undefined) {
|
||||||
|
const mainWindow = require('electron').webContents.fromId(1);
|
||||||
|
mainWindow.send('non-blocking-exception', { cUid, message: content, date: new Date() });
|
||||||
|
}
|
||||||
|
if (process.env.NODE_ENV === 'development' && process.type === 'browser') console.log(content);
|
||||||
|
}
|
||||||
|
else if (level === 'query') {
|
||||||
|
// Remove comments, newlines and multiple spaces
|
||||||
|
const escapedSql = content.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '').replace(/\s\s+/g, ' ');
|
||||||
|
if (process.type !== undefined) {
|
||||||
|
const mainWindow = require('electron').webContents.fromId(1);
|
||||||
|
mainWindow.send('query-log', { cUid, sql: escapedSql, date: new Date() });
|
||||||
|
}
|
||||||
|
if (process.env.NODE_ENV === 'development' && process.type === 'browser') console.log(escapedSql);
|
||||||
|
}
|
||||||
|
};
|
Reference in New Issue
Block a user