diff --git a/src/common/interfaces/antares.ts b/src/common/interfaces/antares.ts index ab17e709..fa94e9f1 100644 --- a/src/common/interfaces/antares.ts +++ b/src/common/interfaces/antares.ts @@ -34,6 +34,7 @@ export interface ClientParams { | { databasePath: string; readonly: boolean }; poolSize?: number; logger?: () => void; + querySplitter?: (sql: string, clieng?: string) => string[]; } /** diff --git a/src/common/libs/querySplitter.ts b/src/common/libs/querySplitter.ts new file mode 100644 index 00000000..b86882fb --- /dev/null +++ b/src/common/libs/querySplitter.ts @@ -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; +}; diff --git a/src/main/libs/clients/BaseClient.ts b/src/main/libs/clients/BaseClient.ts index e0426dda..e13fde6c 100644 --- a/src/main/libs/clients/BaseClient.ts +++ b/src/main/libs/clients/BaseClient.ts @@ -2,27 +2,9 @@ import * as antares from 'common/interfaces/antares'; import mysql from 'mysql2/promise'; import * as pg from 'pg'; import SSH2Promise = require('@fabio286/ssh2-promise'); +import { querySplitter } from 'common/libs/querySplitter'; -export type LoggerLevel = 'query' | 'error' - -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); - } -}; +import { ipcLogger, LoggerLevel } from '../misc/ipcLogger'; /** * As Simple As Possible Query Builder Core @@ -34,6 +16,7 @@ export abstract class BaseClient { protected _poolSize: number; protected _ssh?: SSH2Promise; protected _logger: (args: {content: string; cUid: string; level: LoggerLevel}) => void; + protected _querySplitter: (sql: string, client: antares.ClientCode) => string[]; protected _queryDefaults: antares.QueryBuilderObject; protected _query: antares.QueryBuilderObject; @@ -43,6 +26,7 @@ export abstract class BaseClient { this._params = args.params; this._poolSize = args.poolSize || undefined; this._logger = args.logger || ipcLogger; + this._querySplitter = args.querySplitter || querySplitter; this._queryDefaults = { schema: '', diff --git a/src/main/libs/clients/FirebirdSQLClient.ts b/src/main/libs/clients/FirebirdSQLClient.ts index 181d7e74..b2b5d77e 100644 --- a/src/main/libs/clients/FirebirdSQLClient.ts +++ b/src/main/libs/clients/FirebirdSQLClient.ts @@ -245,10 +245,10 @@ export class FirebirdSQLClient extends BaseClient { name: db.name, size: schemaSize, tables: remappedTables, - functions: [], + functions: [] as null[], procedures: remappedProcedures, triggers: remappedTriggers, - schedulers: [] + schedulers: [] as null[] }; }); } @@ -337,7 +337,7 @@ export class FirebirdSQLClient extends BaseClient { return { name: field.FIELD_NAME.trim(), - key: null, + key: null as null, type: fieldType, schema: schema, table: table, @@ -346,14 +346,14 @@ export class FirebirdSQLClient extends BaseClient { datePrecision: field.FIELD_NAME.trim() === 'TIMESTAMP' ? 4 : null, charLength: ![...NUMBER, ...FLOAT].includes(fieldType) ? field.FIELD_LENGTH : null, nullable: !field.NOT_NULL, - unsigned: null, - zerofill: null, + unsigned: null as null, + zerofill: null as null, order: field.FIELD_POSITION+1, default: defaultValue, charset: field.CHARSET, - collation: null, + collation: null as null, autoIncrement: false, - onUpdate: null, + onUpdate: null as null, comment: field.DESCRIPTION?.trim() }; }); @@ -457,7 +457,7 @@ export class FirebirdSQLClient extends BaseClient { table: table, field: field.FKCOLUMN_NAME.trim(), position: field.KEY_SEQ, - constraintPosition: null, + constraintPosition: null as null, constraintName: field.FK_NAME.trim(), refSchema: schema, refTable: field.PKTABLE_NAME.trim(), @@ -1041,9 +1041,7 @@ export class FirebirdSQLClient extends BaseClient { const resultsArr = []; let paramsArr = []; const queries = args.split - ? sql.split(/((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/gm) - .filter(Boolean) - .map(q => q.trim()) + ? this._querySplitter(sql, 'firebird') : [sql]; let connection: firebird.Database | firebird.Transaction; diff --git a/src/main/libs/clients/MySQLClient.ts b/src/main/libs/clients/MySQLClient.ts index b04b6dc0..24825e07 100644 --- a/src/main/libs/clients/MySQLClient.ts +++ b/src/main/libs/clients/MySQLClient.ts @@ -1755,9 +1755,7 @@ export class MySQLClient extends BaseClient { const resultsArr: antares.QueryResult[] = []; let paramsArr = []; const queries = args.split - ? sql.split(/((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/gm) - .filter(Boolean) - .map(q => q.trim()) + ? this._querySplitter(sql, 'mysql') : [sql]; const connection = await this.getConnection(args); diff --git a/src/main/libs/clients/PostgreSQLClient.ts b/src/main/libs/clients/PostgreSQLClient.ts index 9c7d86d5..3a2d9495 100644 --- a/src/main/libs/clients/PostgreSQLClient.ts +++ b/src/main/libs/clients/PostgreSQLClient.ts @@ -466,7 +466,7 @@ export class PostgreSQLClient extends BaseClient { procedures: remappedProcedures, triggers: remappedTriggers, triggerFunctions: remappedTriggerFunctions, - schedulers: [] + schedulers: [] as null[] }; } else { @@ -532,7 +532,7 @@ export class PostgreSQLClient extends BaseClient { return { name: field.column_name, - key: null, + key: null as null, type: type.toUpperCase(), isArray, schema: field.table_schema, @@ -542,14 +542,14 @@ export class PostgreSQLClient extends BaseClient { datePrecision: field.datetime_precision, charLength: field.character_maximum_length, nullable: field.is_nullable.includes('YES'), - unsigned: null, - zerofill: null, + unsigned: null as null, + zerofill: null as null, order: field.ordinal_position, default: field.column_default, charset: field.character_set_name, collation: field.collation_name, autoIncrement: false, - onUpdate: null, + onUpdate: null as null, comment: field.column_comment }; }); @@ -1252,9 +1252,9 @@ export class PostgreSQLClient extends BaseClient { return results.rows.map(async row => { if (!row.pg_get_functiondef) { return { - definer: null, + definer: null as null, sql: '', - parameters: [], + parameters: [] as null[], name: routine, comment: '', security: 'DEFINER', @@ -1303,8 +1303,8 @@ export class PostgreSQLClient extends BaseClient { name: routine, comment: '', security: row.pg_get_functiondef.includes('SECURITY DEFINER') ? 'DEFINER' : 'INVOKER', - deterministic: null, - dataAccess: null, + deterministic: null as null, + dataAccess: null as null, language: row.pg_get_functiondef.match(/(?<=LANGUAGE )(.*)(?<=[\S+\n\r\s])/gm)[0] }; })[0]; @@ -1368,9 +1368,9 @@ export class PostgreSQLClient extends BaseClient { return results.rows.map(async row => { if (!row.pg_get_functiondef) { return { - definer: null, + definer: null as null, sql: '', - parameters: [], + parameters: [] as null[], name: func, comment: '', security: 'DEFINER', @@ -1418,8 +1418,8 @@ export class PostgreSQLClient extends BaseClient { name: func, comment: '', security: row.pg_get_functiondef.includes('SECURITY DEFINER') ? 'DEFINER' : 'INVOKER', - deterministic: null, - dataAccess: null, + deterministic: null as null, + dataAccess: null as null, 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() }; @@ -1665,9 +1665,7 @@ export class PostgreSQLClient extends BaseClient { const resultsArr: antares.QueryResult[] = []; let paramsArr = []; const queries = args.split - ? sql.split(/(?!\B'[^']*);(?![^']*'\B)/gm) - .filter(Boolean) - .map(q => q.trim()) + ? this._querySplitter(sql, 'pg') : [sql]; let connection: pg.Client | pg.PoolClient; diff --git a/src/main/libs/clients/SQLiteClient.ts b/src/main/libs/clients/SQLiteClient.ts index 73969085..cd23ba15 100644 --- a/src/main/libs/clients/SQLiteClient.ts +++ b/src/main/libs/clients/SQLiteClient.ts @@ -124,10 +124,10 @@ export class SQLiteClient extends BaseClient { name: db.name, size: schemaSize, tables: remappedTables, - functions: [], - procedures: [], + functions: [] as null[], + procedures: [] as null[], triggers: remappedTriggers, - schedulers: [] + schedulers: [] as null[] }; } else { @@ -166,22 +166,22 @@ export class SQLiteClient extends BaseClient { return { name: field.name, - key: null, + key: null as null, type: type.trim(), schema: schema, table: table, numLength: [...NUMBER, ...FLOAT].includes(type) ? length : null, - datePrecision: null, + datePrecision: null as null, charLength: ![...NUMBER, ...FLOAT].includes(type) ? length : null, nullable: !field.notnull, - unsigned: null, - zerofill: null, + unsigned: null as null, + zerofill: null as null, order: typeof field.cid === 'string' ? +field.cid + 1 : field.cid + 1, default: field.dflt_value, - charset: null, - collation: null, + charset: null as null, + collation: null as null, autoIncrement: false, - onUpdate: null, + onUpdate: null as null, comment: '' }; }); @@ -267,7 +267,7 @@ export class SQLiteClient extends BaseClient { table: table, field: field.from, position: field.id + 1, - constraintPosition: null, + constraintPosition: null as null, constraintName: field.id, refSchema: schema, refTable: field.table, @@ -629,9 +629,7 @@ export class SQLiteClient extends BaseClient { const resultsArr = []; let paramsArr = []; const queries = args.split - ? sql.split(/((?:[^;'"]*(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*')[^;'"]*)+)|;/gm) - .filter(Boolean) - .map(q => q.trim()) + ? this._querySplitter(sql, 'sqlite') : [sql]; let connection: sqlite.Database; diff --git a/src/main/libs/misc/ipcLogger.ts b/src/main/libs/misc/ipcLogger.ts new file mode 100644 index 00000000..b8ab4d1e --- /dev/null +++ b/src/main/libs/misc/ipcLogger.ts @@ -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); + } +}; \ No newline at end of file