1
1
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:
2025-01-31 17:28:58 +01:00
parent e3b30359bf
commit 96ae09feca
8 changed files with 147 additions and 64 deletions

View File

@ -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[];
} }
/** /**

View 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;
};

View File

@ -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: '',

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;

View 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);
}
};