diff --git a/src/common/libs/querySplitter.ts b/src/common/libs/querySplitter.ts deleted file mode 100644 index b86882fb..00000000 --- a/src/common/libs/querySplitter.ts +++ /dev/null @@ -1,86 +0,0 @@ -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/common/libs/sqlUtils.ts b/src/common/libs/sqlUtils.ts index 52e058c6..c2ea41a3 100644 --- a/src/common/libs/sqlUtils.ts +++ b/src/common/libs/sqlUtils.ts @@ -3,18 +3,161 @@ import { lineString, point, polygon } from '@turf/helpers'; import { BIT, BLOB, DATE, DATETIME, FLOAT, IS_MULTI_SPATIAL, NUMBER, SPATIAL, TEXT_SEARCH } from 'common/fieldTypes'; import * as antares from 'common/interfaces/antares'; +import { ClientCode } from 'common/interfaces/antares'; import * as moment from 'moment'; import customizations from '../customizations'; -import { ClientCode } from '../interfaces/antares'; import { getArrayDepth } from './getArrayDepth'; import hexToBinary, { HexChar } from './hexToBinary'; /** - * Escapes a string fo SQL use + * Splits a SQL string into multiple queries based on semicolons (;). + * Handles BEGIN-END blocks, strings, comments, and PostgreSQL dollar-quoted tags. * - * @param { String } string - * @returns { String } Escaped string + * @param {string} sql - The SQL string to split. + * @param {ClientCode} dbType - The database type (e.g., 'pg', 'mysql'). + * @returns {string[]} - An array of separated SQL queries. + */ +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; +}; + +/** + * Removes all comments (both single-line and multi-line) from a SQL string. + * + * @param {string} sql - The SQL string to process. + * @returns {string} - The SQL string without comments. + */ +export const removeComments = (sql: string): string => { + let result = ''; + let insideSingleLineComment = false; + let insideMultiLineComment = false; + + for (let i = 0; i < sql.length; i++) { + const char = sql[i]; + const nextChar = sql[i + 1] || ''; + + // Handle single-line comments (--) + if (!insideMultiLineComment && char === '-' && nextChar === '-') + insideSingleLineComment = true; + + // Handle multi-line comments (/* */) + if (!insideSingleLineComment && char === '/' && nextChar === '*') { + insideMultiLineComment = true; + i++; // Skip the '*' character + continue; + } + + if (insideMultiLineComment && char === '*' && nextChar === '/') { + insideMultiLineComment = false; + i++; // Skip the '/' character + continue; + } + + // Skip characters inside comments + if (insideSingleLineComment) { + if (char === '\n') + insideSingleLineComment = false; + + continue; + } + + if (insideMultiLineComment) + continue; + + // Append non-comment characters to the result + result += char; + } + + return result; +}; + +/** + * Escapes a string for safe use in SQL queries. + * + * @param {string} string - The string to escape. + * @returns {string} - The escaped string. */ export const sqlEscaper = (string: string): string => { // eslint-disable-next-line no-control-regex @@ -27,6 +170,12 @@ export const sqlEscaper = (string: string): string => { }); }; +/** + * Converts a value into a GeoJSON object based on its type. + * + * @param {any} val - The value to convert. + * @returns {object} - The generated GeoJSON object. + */ export const objectToGeoJSON = (val: any) => { if (Array.isArray(val)) { if (getArrayDepth(val) === 1) @@ -38,6 +187,13 @@ export const objectToGeoJSON = (val: any) => { return point([val.x, val.y]); }; +/** + * Escapes and wraps a string in quotes for safe use in SQL queries. + * + * @param {string} val - The string to process. + * @param {ClientCode} client - The database type (e.g., 'pg', 'mysql'). + * @returns {string} - The escaped and quoted string. + */ export const escapeAndQuote = (val: string, client: ClientCode) => { const { stringsWrapper: sw } = customizations[client]; // eslint-disable-next-line no-control-regex @@ -74,11 +230,17 @@ export const escapeAndQuote = (val: string, client: ClientCode) => { return `${sw}${escapedVal}${sw}`; }; +/** + * Converts a value into a SQL string based on the field type and database type. + * + * @param {object} args - Arguments containing the value, database type, and field type. + * @returns {string} - The generated SQL string. + */ export const valueToSqlString = (args: { - val: any; - client: ClientCode; - field: {type: string; datePrecision?: number; precision?: number | false; isArray?: boolean}; - }): string => { + val: any; + client: ClientCode; + field: { type: string; datePrecision?: number; precision?: number | false; isArray?: boolean }; +}): string => { let parsedValue; const { val, client, field } = args; const { stringsWrapper: sw } = customizations[client]; @@ -165,13 +327,19 @@ export const valueToSqlString = (args: { return parsedValue; }; +/** + * Converts a JSON array into an SQL INSERT query. + * + * @param {object} args - Arguments containing the JSON data, database type, fields, and options. + * @returns {string} - The generated SQL INSERT query. + */ export const jsonToSqlInsert = (args: { - json: Record[]; - client: ClientCode; - fields: Record; - table: string; - options?: {sqlInsertAfter: number; sqlInsertDivider: 'bytes' | 'rows'}; - }) => { + json: Record[]; + client: ClientCode; + fields: Record; + table: string; + options?: { sqlInsertAfter: number; sqlInsertDivider: 'bytes' | 'rows' }; +}) => { const { client, json, fields, table, options } = args; const sqlInsertAfter = options && options.sqlInsertAfter ? options.sqlInsertAfter : 1; const sqlInsertDivider = options && options.sqlInsertDivider ? options.sqlInsertDivider : 'rows'; @@ -193,7 +361,7 @@ export const jsonToSqlInsert = (args: { (sqlInsertDivider === 'bytes' && queryLength >= sqlInsertAfter * 1024) || (sqlInsertDivider === 'rows' && rowsWritten === sqlInsertAfter) ) { - insertsString += insertStmt+';'; + insertsString += insertStmt + ';'; insertStmt = `\nINSERT INTO ${ew}${table}${ew} (${fieldNames.join(', ')}) VALUES `; rowsWritten = 0; } @@ -206,11 +374,18 @@ export const jsonToSqlInsert = (args: { } if (rowsWritten > 0) - insertsString += insertStmt+';'; + insertsString += insertStmt + ';'; return insertsString; }; +/** + * Formats a JSON value for use in an SQL WHERE clause. + * + * @param {object} jsonValue - The JSON value to format. + * @param {ClientCode} clientType - The database type (e.g., 'pg', 'mysql'). + * @returns {string} - The formatted SQL WHERE clause. + */ export const formatJsonForSqlWhere = (jsonValue: object, clientType: antares.ClientCode) => { const formattedValue = JSON.stringify(jsonValue); diff --git a/src/main/ipc-handlers/schema.ts b/src/main/ipc-handlers/schema.ts index 7ef2d1cb..df4e930f 100644 --- a/src/main/ipc-handlers/schema.ts +++ b/src/main/ipc-handlers/schema.ts @@ -183,6 +183,7 @@ export default (connections: Record) => { const result = await connections[uid].raw(query, { nest: true, details: true, + comments: false, schema, tabUid, autocommit diff --git a/src/main/libs/clients/BaseClient.ts b/src/main/libs/clients/BaseClient.ts index e13fde6c..6281575b 100644 --- a/src/main/libs/clients/BaseClient.ts +++ b/src/main/libs/clients/BaseClient.ts @@ -1,8 +1,8 @@ import * as antares from 'common/interfaces/antares'; +import { querySplitter } from 'common/libs/sqlUtils'; import mysql from 'mysql2/promise'; import * as pg from 'pg'; import SSH2Promise = require('@fabio286/ssh2-promise'); -import { querySplitter } from 'common/libs/querySplitter'; import { ipcLogger, LoggerLevel } from '../misc/ipcLogger'; diff --git a/src/main/libs/clients/FirebirdSQLClient.ts b/src/main/libs/clients/FirebirdSQLClient.ts index b2b5d77e..85378740 100644 --- a/src/main/libs/clients/FirebirdSQLClient.ts +++ b/src/main/libs/clients/FirebirdSQLClient.ts @@ -1,6 +1,7 @@ import dataTypes from 'common/data-types/firebird'; import { FLOAT, NUMBER } from 'common/fieldTypes'; import * as antares from 'common/interfaces/antares'; +import { removeComments } from 'common/libs/sqlUtils'; import * as firebird from 'node-firebird'; import * as path from 'path'; @@ -1036,7 +1037,7 @@ export class FirebirdSQLClient extends BaseClient { }; if (!args.comments) - sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');// Remove comments + sql = removeComments(sql); const resultsArr = []; let paramsArr = []; diff --git a/src/main/libs/clients/MySQLClient.ts b/src/main/libs/clients/MySQLClient.ts index e54a6dde..b2b49f83 100644 --- a/src/main/libs/clients/MySQLClient.ts +++ b/src/main/libs/clients/MySQLClient.ts @@ -2,6 +2,7 @@ import SSH2Promise = require('@fabio286/ssh2-promise'); import SSHConfig from '@fabio286/ssh2-promise/lib/sshConfig'; import dataTypes from 'common/data-types/mysql'; import * as antares from 'common/interfaces/antares'; +import { removeComments } from 'common/libs/sqlUtils'; import * as mysql from 'mysql2/promise'; import * as EncodingToCharset from '../../../../node_modules/mysql2/lib/constants/encoding_charset.js'; @@ -1751,7 +1752,7 @@ export class MySQLClient extends BaseClient { }; if (!args.comments) - sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');// Remove comments + sql = removeComments(sql); const nestTables = args.nest ? '.' : false; const resultsArr: antares.QueryResult[] = []; diff --git a/src/main/libs/clients/PostgreSQLClient.ts b/src/main/libs/clients/PostgreSQLClient.ts index 04f567dd..fe250f8e 100644 --- a/src/main/libs/clients/PostgreSQLClient.ts +++ b/src/main/libs/clients/PostgreSQLClient.ts @@ -2,6 +2,7 @@ import SSH2Promise = require('@fabio286/ssh2-promise'); import SSHConfig from '@fabio286/ssh2-promise/lib/sshConfig'; import dataTypes from 'common/data-types/postgresql'; import * as antares from 'common/interfaces/antares'; +import { removeComments } from 'common/libs/sqlUtils'; import * as pg from 'pg'; import * as pgAst from 'pgsql-ast-parser'; import { ConnectionOptions } from 'tls'; @@ -1660,7 +1661,7 @@ export class PostgreSQLClient extends BaseClient { }; if (!args.comments) - sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');// Remove comments + sql = removeComments(sql); const resultsArr: antares.QueryResult[] = []; let paramsArr = []; diff --git a/src/main/libs/clients/SQLiteClient.ts b/src/main/libs/clients/SQLiteClient.ts index cd23ba15..b941dbad 100644 --- a/src/main/libs/clients/SQLiteClient.ts +++ b/src/main/libs/clients/SQLiteClient.ts @@ -2,6 +2,7 @@ import * as sqlite from 'better-sqlite3'; import dataTypes from 'common/data-types/sqlite'; import { DATETIME, FLOAT, NUMBER, TIME } from 'common/fieldTypes'; import * as antares from 'common/interfaces/antares'; +import { removeComments } from 'common/libs/sqlUtils'; import { BaseClient } from './BaseClient'; @@ -624,7 +625,7 @@ export class SQLiteClient extends BaseClient { }; if (!args.comments) - sql = sql.replace(/(\/\*(.|[\r\n])*?\*\/)|(--(.*|[\r\n]))/gm, '');// Remove comments + sql = removeComments(sql); const resultsArr = []; let paramsArr = [];