UNPKG

@04l3x4ndr3/microbase-orm

Version:

Um micro ORM em JavaScript ES6 inspirado no CodeIgniter 3 Query Builder com suporte para MySQL/MariaDB e PostgreSQL

1,530 lines (1,213 loc) 54.4 kB
// QueryBuilder.js import MySQLDriver from './drivers/MySQLDriver.js'; import PostgreSQLDriver from './drivers/PostgreSQLDriver.js'; import MariadbDriver from "./drivers/MariadbDriver.js"; class QueryBuilder { constructor(connection, driverType, config = {}) { this.connection = connection; this.config = config; this.driverType = driverType; this.driver = this.createDriver(driverType, config); // ✅ Sistema de ID único para rastreamento this.queryBuilderId = this.generateBuilderId(); // ✅ Cache de queries compiladas this.queryCache = new Map(); this.maxCacheSize = config.maxQueryCache || 50; // ✅ Métricas do QueryBuilder this.metrics = { queriesBuilt: 0, cacheHits: 0, cacheMisses: 0, avgBuildTime: 0, complexQueries: 0, errors: 0 }; // ✅ Configurações de validação this.validation = { enabled: config.validation !== false, maxWhereConditions: config.maxWhereConditions || 50, maxJoins: config.maxJoins || 20, maxSelectFields: config.maxSelectFields || 100, maxGroupByFields: config.maxGroupByFields || 20, maxOrderByFields: config.maxOrderByFields || 10 }; // ✅ Sistema de debug avançado this.DEBUG = config.debug || false; this.debugLevel = config.debugLevel || 'info'; // info, warn, error // ✅ Estado de transação this.inTransaction = false; this.transactionDepth = 0; this.reset(); if (this.DEBUG) { console.log(`🔧 QueryBuilder inicializado [${this.queryBuilderId}] - Driver: ${driverType}`); } } generateBuilderId() { return `qb_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`; } createDriver(driverType, config) { const drivers = { 'mysql': MySQLDriver, 'mariadb': MariadbDriver, 'postgres': PostgreSQLDriver, 'postgresql': PostgreSQLDriver }; const DriverClass = drivers[driverType.toLowerCase()]; if (!DriverClass) { throw new Error(`Driver não suportado: ${driverType}. Drivers disponíveis: ${Object.keys(drivers).join(', ')}`); } return new DriverClass(this.connection, config); } // ✅ Reset melhorado com validação de estado reset() { this.selectFields = ['*']; this.fromTable = ''; this.joinClauses = []; this.whereClauses = []; this.groupByFields = []; this.havingClauses = []; this.orderByFields = []; this.limitValue = null; this.offsetValue = null; this.distinctFlag = false; this.params = []; this.updateData = null; this.lastQuery = null; this.lastExecutionTime = null; this.currentOperation = null; // ✅ Estado de subconsultas this.subqueries = []; this.isSubquery = false; // ✅ Estado de CTE (Common Table Expressions) this.cteQueries = []; return this; } // =============================== // ✅ MÉTODOS SELECT MELHORADOS // =============================== select(fields = '*') { this._validateSelectFields(fields); if (typeof fields === 'string') { this.selectFields = fields === '*' ? ['*'] : [fields]; } else if (Array.isArray(fields)) { this.selectFields = fields; } else if (typeof fields === 'object' && fields !== null) { // ✅ Suporte a objeto para alias: { nome: 'name', idade: 'age' } this.selectFields = Object.entries(fields).map(([field, alias]) => `${this.driver.escapeIdentifier(field)} AS ${this.driver.escapeIdentifier(alias)}` ); } this.currentOperation = 'SELECT'; return this; } // ✅ Seleção com expressão SQL raw selectRaw(expression, alias = null) { this._validateNotEmpty(expression, 'Expressão SQL'); if (alias) { this.selectFields = [`${expression} AS ${this.driver.escapeIdentifier(alias)}`]; } else { this.selectFields = [expression]; } return this; } // ✅ Funções de agregação melhoradas selectMax(field, alias = null) { this._validateFieldName(field); const maxField = `MAX(${this.driver.escapeIdentifier(field)})`; this.selectFields = [alias ? `${maxField} AS ${this.driver.escapeIdentifier(alias)}` : maxField]; return this; } selectMin(field, alias = null) { this._validateFieldName(field); const minField = `MIN(${this.driver.escapeIdentifier(field)})`; this.selectFields = [alias ? `${minField} AS ${this.driver.escapeIdentifier(alias)}` : minField]; return this; } selectAvg(field, alias = null) { this._validateFieldName(field); const avgField = `AVG(${this.driver.escapeIdentifier(field)})`; this.selectFields = [alias ? `${avgField} AS ${this.driver.escapeIdentifier(alias)}` : avgField]; return this; } selectSum(field, alias = null) { this._validateFieldName(field); const sumField = `SUM(${this.driver.escapeIdentifier(field)})`; this.selectFields = [alias ? `${sumField} AS ${this.driver.escapeIdentifier(alias)}` : sumField]; return this; } selectCount(field = '*', alias = 'count') { const fieldToCount = field === '*' ? '*' : this.driver.escapeIdentifier(field); const countField = `COUNT(${fieldToCount})`; this.selectFields = [alias ? `${countField} AS ${this.driver.escapeIdentifier(alias)}` : countField]; return this; } // ✅ Suporte a window functions (PostgreSQL/MySQL 8.0+) selectWindow(expression, windowSpec, alias = null) { this._validateNotEmpty(expression, 'Expressão window'); this._validateNotEmpty(windowSpec, 'Especificação window'); const windowField = `${expression} OVER (${windowSpec})`; this.selectFields = [alias ? `${windowField} AS ${this.driver.escapeIdentifier(alias)}` : windowField]; return this; } distinct() { this.distinctFlag = true; return this; } // =============================== // ✅ FROM MELHORADO // =============================== from(table, alias = null) { this._validateTableName(table); let tableName = table; // ✅ CORREÇÃO: Auto-schema para PostgreSQL if (this.driverType === 'postgres' && !table.includes('.') && !table.includes(' ') && !table.includes('(')) { const schema = this.config.schema || this.driver.schema || 'public'; tableName = `${schema}.${table}`; } this.fromTable = alias ? `${this.driver.escapeIdentifier(tableName)} AS ${this.driver.escapeIdentifier(alias)}` : this.driver.escapeIdentifier(tableName); return this; } // ✅ FROM com subconsulta fromSubquery(subquery, alias) { this._validateNotEmpty(alias, 'Alias da subconsulta'); if (typeof subquery === 'function') { const subBuilder = new QueryBuilder(this.connection, this.driverType, this.config); subBuilder.isSubquery = true; subquery(subBuilder); const subSql = subBuilder.buildSelectQuery(); this.fromTable = `(${subSql}) AS ${this.driver.escapeIdentifier(alias)}`; this.params = [...this.params, ...subBuilder.params]; } else if (typeof subquery === 'string') { this.fromTable = `(${subquery}) AS ${this.driver.escapeIdentifier(alias)}`; } return this; } // =============================== // ✅ JOIN METHODS MELHORADOS // =============================== join(table, condition, type = 'INNER', alias = null) { this._validateJoinCount(); this._validateTableName(table); this._validateNotEmpty(condition, 'Condição do JOIN'); const validJoinTypes = ['INNER', 'LEFT', 'RIGHT', 'FULL', 'CROSS']; if (!validJoinTypes.includes(type.toUpperCase())) { throw new Error(`Tipo de JOIN inválido: ${type}. Tipos válidos: ${validJoinTypes.join(', ')}`); } let escapedTable = this.driver.escapeIdentifier(table); if (alias) { escapedTable += ` AS ${this.driver.escapeIdentifier(alias)}`; } this.joinClauses.push(`${type.toUpperCase()} JOIN ${escapedTable} ON ${condition}`); return this; } leftJoin(table, condition, alias = null) { return this.join(table, condition, 'LEFT', alias); } rightJoin(table, condition, alias = null) { return this.join(table, condition, 'RIGHT', alias); } fullJoin(table, condition, alias = null) { return this.join(table, condition, 'FULL', alias); } crossJoin(table, alias = null) { this._validateJoinCount(); this._validateTableName(table); let escapedTable = this.driver.escapeIdentifier(table); if (alias) { escapedTable += ` AS ${this.driver.escapeIdentifier(alias)}`; } this.joinClauses.push(`CROSS JOIN ${escapedTable}`); return this; } // ✅ JOIN com subconsulta joinSubquery(subquery, alias, condition, type = 'INNER') { this._validateJoinCount(); this._validateNotEmpty(alias, 'Alias do JOIN'); this._validateNotEmpty(condition, 'Condição do JOIN'); if (typeof subquery === 'function') { const subBuilder = new QueryBuilder(this.connection, this.driverType, this.config); subBuilder.isSubquery = true; subquery(subBuilder); const subSql = subBuilder.buildSelectQuery(); this.joinClauses.push(`${type.toUpperCase()} JOIN (${subSql}) AS ${this.driver.escapeIdentifier(alias)} ON ${condition}`); this.params = [...this.params, ...subBuilder.params]; } return this; } // =============================== // ✅ WHERE METHODS MELHORADOS // =============================== where(field, value = null, operator = '=') { this._validateWhereCount(); if (typeof field === 'object' && field !== null) { Object.entries(field).forEach(([key, val]) => { this._addWhereClause(`${this.driver.escapeIdentifier(key)} = ?`, val); }); } else if (typeof field === 'function') { // ✅ Suporte a closures para agrupamento this._addWhereGroup(field); } else if (value !== null && value !== undefined) { this._validateOperator(operator); this._addWhereClause(`${this.driver.escapeIdentifier(field)} ${operator} ?`, value); } else { // Raw condition this._addWhereClause(field); } return this; } orWhere(field, value = null, operator = '=') { if (this.whereClauses.length === 0) { return this.where(field, value, operator); } if (typeof field === 'object' && field !== null) { const conditions = []; const values = []; Object.entries(field).forEach(([key, val]) => { conditions.push(`${this.driver.escapeIdentifier(key)} = ?`); values.push(val); }); this.whereClauses.push(`OR (${conditions.join(' AND ')})`); this.params.push(...values); } else if (typeof field === 'function') { this._addOrWhereGroup(field); } else if (value !== null && value !== undefined) { this._validateOperator(operator); this.whereClauses.push(`OR ${this.driver.escapeIdentifier(field)} ${operator} ?`); this.params.push(value); } else { this.whereClauses.push(`OR ${field}`); } return this; } // ✅ WHERE com subconsulta whereSubquery(field, operator, subquery) { this._validateWhereCount(); this._validateFieldName(field); this._validateOperator(operator); if (typeof subquery === 'function') { const subBuilder = new QueryBuilder(this.connection, this.driverType, this.config); subBuilder.isSubquery = true; subquery(subBuilder); const subSql = subBuilder.buildSelectQuery(); this._addWhereClause(`${this.driver.escapeIdentifier(field)} ${operator} (${subSql})`); this.params = [...this.params, ...subBuilder.params]; } return this; } whereExists(subquery) { this._validateWhereCount(); if (typeof subquery === 'function') { const subBuilder = new QueryBuilder(this.connection, this.driverType, this.config); subBuilder.isSubquery = true; subquery(subBuilder); const subSql = subBuilder.buildSelectQuery(); this._addWhereClause(`EXISTS (${subSql})`); this.params = [...this.params, ...subBuilder.params]; } return this; } whereNotExists(subquery) { this._validateWhereCount(); if (typeof subquery === 'function') { const subBuilder = new QueryBuilder(this.connection, this.driverType, this.config); subBuilder.isSubquery = true; subquery(subBuilder); const subSql = subBuilder.buildSelectQuery(); this._addWhereClause(`NOT EXISTS (${subSql})`); this.params = [...this.params, ...subBuilder.params]; } return this; } whereIn(field, values) { this._validateWhereCount(); this._validateFieldName(field); this._validateArrayValues(values, 'whereIn'); const placeholders = values.map(() => '?').join(', '); this._addWhereClause(`${this.driver.escapeIdentifier(field)} IN (${placeholders})`); this.params.push(...values); return this; } whereNotIn(field, values) { this._validateWhereCount(); this._validateFieldName(field); this._validateArrayValues(values, 'whereNotIn'); const placeholders = values.map(() => '?').join(', '); this._addWhereClause(`${this.driver.escapeIdentifier(field)} NOT IN (${placeholders})`); this.params.push(...values); return this; } whereBetween(field, min, max) { this._validateWhereCount(); this._validateFieldName(field); this._addWhereClause(`${this.driver.escapeIdentifier(field)} BETWEEN ? AND ?`, [min, max]); return this; } whereNotBetween(field, min, max) { this._validateWhereCount(); this._validateFieldName(field); this._addWhereClause(`${this.driver.escapeIdentifier(field)} NOT BETWEEN ? AND ?`, [min, max]); return this; } whereNull(field) { this._validateWhereCount(); this._validateFieldName(field); this._addWhereClause(`${this.driver.escapeIdentifier(field)} IS NULL`); return this; } whereNotNull(field) { this._validateWhereCount(); this._validateFieldName(field); this._addWhereClause(`${this.driver.escapeIdentifier(field)} IS NOT NULL`); return this; } whereLike(field, value) { this._validateWhereCount(); this._validateFieldName(field); this._addWhereClause(`${this.driver.escapeIdentifier(field)} LIKE ?`, value); return this; } orWhereLike(field, value) { if (this.whereClauses.length === 0) { return this.whereLike(field, value); } this.whereClauses.push(`OR ${this.driver.escapeIdentifier(field)} LIKE ?`); this.params.push(value); return this; } whereNotLike(field, value) { this._validateWhereCount(); this._validateFieldName(field); this._addWhereClause(`${this.driver.escapeIdentifier(field)} NOT LIKE ?`, value); return this; } // ✅ WHERE com expressão raw whereRaw(expression, bindings = []) { this._validateWhereCount(); this._validateNotEmpty(expression, 'Expressão WHERE'); this._addWhereClause(expression); if (Array.isArray(bindings)) { this.params.push(...bindings); } else if (bindings !== null && bindings !== undefined) { this.params.push(bindings); } return this; } // =============================== // ✅ GROUP BY E HAVING MELHORADOS // =============================== groupBy(fields) { this._validateGroupByCount(); if (typeof fields === 'string') { this.groupByFields.push(this.driver.escapeIdentifier(fields)); } else if (Array.isArray(fields)) { fields.forEach(field => { this._validateFieldName(field); this.groupByFields.push(this.driver.escapeIdentifier(field)); }); } return this; } groupByRaw(expression) { this._validateNotEmpty(expression, 'Expressão GROUP BY'); this.groupByFields.push(expression); return this; } having(field, value = null, operator = '=') { if (typeof field === 'object' && field !== null) { Object.entries(field).forEach(([key, val]) => { // ✅ CORREÇÃO: Verificar se já existem cláusulas HAVING if (this.havingClauses.length > 0) { this.havingClauses.push(`AND ${this.driver.escapeIdentifier(key)} = ?`); } else { this.havingClauses.push(`${this.driver.escapeIdentifier(key)} = ?`); } this.params.push(val); }); } else if (value !== null && value !== undefined) { this._validateOperator(operator); if (this.havingClauses.length > 0) { this.havingClauses.push(`AND ${this.driver.escapeIdentifier(field)} ${operator} ?`); } else { this.havingClauses.push(`${this.driver.escapeIdentifier(field)} ${operator} ?`); } this.params.push(value); } else { if (this.havingClauses.length > 0) { this.havingClauses.push(`AND ${field}`); } else { this.havingClauses.push(field); } } return this; } orHaving(field, value = null, operator = '=') { if (this.havingClauses.length === 0) { return this.having(field, value, operator); } if (typeof field === 'object' && field !== null) { const conditions = []; const values = []; Object.entries(field).forEach(([key, val]) => { conditions.push(`${this.driver.escapeIdentifier(key)} = ?`); values.push(val); }); this.havingClauses.push(`OR (${conditions.join(' AND ')})`); this.params.push(...values); } else if (value !== null && value !== undefined) { this._validateOperator(operator); this.havingClauses.push(`OR ${this.driver.escapeIdentifier(field)} ${operator} ?`); this.params.push(value); } else { this.havingClauses.push(`OR ${field}`); } return this; } havingRaw(expression, bindings = []) { this._validateNotEmpty(expression, 'Expressão HAVING'); if (this.havingClauses.length > 0) { this.havingClauses.push(`AND ${expression}`); } else { this.havingClauses.push(expression); } if (Array.isArray(bindings)) { this.params.push(...bindings); } return this; } // =============================== // ✅ ORDER BY MELHORADO // =============================== orderBy(field, direction = 'ASC') { this._validateOrderByCount(); this._validateFieldName(field); const validDirections = ['ASC', 'DESC']; const upperDirection = direction.toUpperCase(); if (!validDirections.includes(upperDirection)) { throw new Error(`Direção deve ser ASC ou DESC, recebido: ${direction}`); } this.orderByFields.push(`${this.driver.escapeIdentifier(field)} ${upperDirection}`); return this; } orderByRaw(expression) { this._validateOrderByCount(); this._validateNotEmpty(expression, 'Expressão ORDER BY'); this.orderByFields.push(expression); return this; } orderByRandom() { this._validateOrderByCount(); this.orderByFields.push(this.driver.getRandomFunction()); return this; } // ✅ ORDER BY com NULLS FIRST/LAST (PostgreSQL) orderByNulls(field, direction = 'ASC', nullsPosition = 'LAST') { this._validateOrderByCount(); this._validateFieldName(field); const validDirections = ['ASC', 'DESC']; const validNullsPositions = ['FIRST', 'LAST']; if (!validDirections.includes(direction.toUpperCase())) { throw new Error(`Direção deve ser ASC ou DESC, recebido: ${direction}`); } if (!validNullsPositions.includes(nullsPosition.toUpperCase())) { throw new Error(`Posição de NULLs deve ser FIRST ou LAST, recebido: ${nullsPosition}`); } if (this.driverType === 'postgres') { this.orderByFields.push(`${this.driver.escapeIdentifier(field)} ${direction.toUpperCase()} NULLS ${nullsPosition.toUpperCase()}`); } else { // Fallback para outros bancos this.orderByFields.push(`${this.driver.escapeIdentifier(field)} ${direction.toUpperCase()}`); } return this; } // =============================== // ✅ LIMIT E OFFSET MELHORADOS // =============================== limit(count, offset = null) { this._validateLimit(count); this.limitValue = count; if (offset !== null) { this._validateOffset(offset); this.offsetValue = offset; } return this; } offset(count) { this._validateOffset(count); this.offsetValue = count; return this; } // ✅ Paginação simplificada paginate(page, perPage = 15) { this._validatePagination(page, perPage); const offset = (page - 1) * perPage; return this.limit(perPage, offset); } // =============================== // ✅ COMMON TABLE EXPRESSIONS (CTE) // =============================== with(name, query) { this._validateNotEmpty(name, 'Nome da CTE'); if (this.driverType !== 'postgres' && this.driverType !== 'postgresql') { throw new Error('CTEs são suportadas apenas no PostgreSQL'); } if (typeof query === 'function') { const cteBuilder = new QueryBuilder(this.connection, this.driverType, this.config); cteBuilder.isSubquery = true; query(cteBuilder); const cteSql = cteBuilder.buildSelectQuery(); this.cteQueries.push(`${this.driver.escapeIdentifier(name)} AS (${cteSql})`); this.params = [...cteBuilder.params, ...this.params]; } return this; } withRecursive(name, query) { this._validateNotEmpty(name, 'Nome da CTE recursiva'); if (this.driverType !== 'postgres' && this.driverType !== 'postgresql') { throw new Error('CTEs recursivas são suportadas apenas no PostgreSQL'); } if (typeof query === 'function') { const cteBuilder = new QueryBuilder(this.connection, this.driverType, this.config); cteBuilder.isSubquery = true; query(cteBuilder); const cteSql = cteBuilder.buildSelectQuery(); this.cteQueries.push(`${this.driver.escapeIdentifier(name)} AS (${cteSql})`); this.params = [...cteBuilder.params, ...this.params]; } return this; } // =============================== // ✅ UNION OPERATIONS // =============================== union(query, all = false) { if (typeof query === 'function') { const unionBuilder = new QueryBuilder(this.connection, this.driverType, this.config); unionBuilder.isSubquery = true; query(unionBuilder); const unionSql = unionBuilder.buildSelectQuery(); const unionType = all ? 'UNION ALL' : 'UNION'; this.subqueries.push(`${unionType} (${unionSql})`); this.params = [...this.params, ...unionBuilder.params]; } return this; } unionAll(query) { return this.union(query, true); } // =============================== // ✅ BUILD E EXECUTE METHODS // =============================== buildSelectQuery() { const startTime = Date.now(); try { // ✅ Cache de query const cacheKey = this._generateCacheKey(); if (this.queryCache.has(cacheKey)) { this.metrics.cacheHits++; return this.queryCache.get(cacheKey); } else { this.metrics.cacheMisses++; } let sql = ''; // ✅ CTEs (PostgreSQL) if (this.cteQueries.length > 0) { sql += `WITH ${this.cteQueries.join(', ')} `; } sql += 'SELECT '; if (this.distinctFlag) { sql += 'DISTINCT '; } sql += this.selectFields.join(', '); if (this.fromTable) { sql += ` FROM ${this.fromTable}`; } if (this.joinClauses.length > 0) { sql += ` ${this.joinClauses.join(' ')}`; } if (this.whereClauses.length > 0) { sql += ` WHERE ${this.whereClauses.join(' ')}`; } if (this.groupByFields.length > 0) { sql += ` GROUP BY ${this.groupByFields.join(', ')}`; } if (this.havingClauses.length > 0) { sql += ` HAVING ${this.havingClauses.join(' ')}`; } if (this.orderByFields.length > 0) { sql += ` ORDER BY ${this.orderByFields.join(', ')}`; } if (this.limitValue !== null) { sql += ` ${this.driver.getLimitSyntax(this.limitValue, this.offsetValue || 0)}`; } // ✅ UNIONs if (this.subqueries.length > 0) { sql += ` ${this.subqueries.join(' ')}`; } // ✅ Adicionar ao cache if (this.queryCache.size < this.maxCacheSize) { this.queryCache.set(cacheKey, sql); } const buildTime = Date.now() - startTime; this._updateBuildMetrics(buildTime); return sql; } catch (error) { this.metrics.errors++; throw error; } } // ✅ Salvar a última query executada com métricas async executeQuery(sql, params) { const startTime = Date.now(); try { const result = await this.driver.execute(sql, params); const executionTime = Date.now() - startTime; this.lastQuery = { sql, params, executionTime, timestamp: Date.now() }; this.lastExecutionTime = executionTime; if (this.DEBUG) { this._logQueryExecution(sql, params, executionTime); } return result; } catch (error) { this.metrics.errors++; if (this.DEBUG) { this._logQueryError(sql, params, error); } throw error; } } // =============================== // ✅ MÉTODOS DE EXECUÇÃO MELHORADOS // =============================== async get() { this._validateQueryState(); const sql = this.buildSelectQuery(); const results = await this.executeQuery(sql, this.params); this.reset(); return results; } async getWhere(table, where) { this._validateTableName(table); return this.from(table).where(where).get(); } async first() { const results = await this.limit(1).get(); return results.length > 0 ? results[0] : null; } async count(field = '*') { const originalSelect = [...this.selectFields]; const originalLimit = this.limitValue; const originalOffset = this.offsetValue; const countField = field === '*' ? '*' : this.driver.escapeIdentifier(field); this.selectFields = [`COUNT(${countField}) as count`]; this.limitValue = null; this.offsetValue = null; const result = await this.get(); this.selectFields = originalSelect; this.limitValue = originalLimit; this.offsetValue = originalOffset; return parseInt(result[0].count) || 0; } // ✅ Exists check async exists() { const originalSelect = [...this.selectFields]; const originalLimit = this.limitValue; this.selectFields = ['1']; this.limitValue = 1; const result = await this.get(); this.selectFields = originalSelect; this.limitValue = originalLimit; return result.length > 0; } // ✅ Chunk processing para grandes datasets async chunk(size, callback) { this._validateChunkSize(size); this._validateCallback(callback); let offset = 0; let processedRows = 0; while (true) { const builderClone = this.clone(); builderClone.limit(size, offset); const chunk = await builderClone.get(); if (chunk.length === 0) { break; } const shouldContinue = await callback(chunk, Math.floor(offset / size) + 1); processedRows += chunk.length; if (shouldContinue === false) { break; } if (chunk.length < size) { break; } offset += size; } return processedRows; } // =============================== // ✅ INSERT METHODS MELHORADOS // =============================== async insert(table, data) { this._validateTableName(table); this._validateInsertData(data); if (Array.isArray(data)) { return this.insertBatch(table, data); } const fields = Object.keys(data); const values = Object.values(data); const escapedFields = fields.map(field => this.driver.escapeIdentifier(field)); const placeholders = fields.map(() => '?').join(', '); let sql = `INSERT INTO ${this.driver.escapeIdentifier(table)} (${escapedFields.join(', ')}) VALUES (${placeholders})`; // ✅ RETURNING para PostgreSQL if (this.driverType === 'postgres' || this.driverType === 'postgresql') { sql += ' RETURNING *'; } const result = await this.executeQuery(sql, values); this.reset(); return result; } async insertBatch(table, data) { this._validateTableName(table); this._validateBatchData(data); const fields = Object.keys(data[0]); const escapedFields = fields.map(field => this.driver.escapeIdentifier(field)); const values = []; const placeholders = []; data.forEach(row => { // ✅ Validar que todas as linhas têm os mesmos campos const rowFields = Object.keys(row); if (!this._arraysEqual(fields, rowFields)) { throw new Error('Todas as linhas do batch devem ter os mesmos campos'); } const rowValues = fields.map(field => row[field]); values.push(...rowValues); placeholders.push(`(${fields.map(() => '?').join(', ')})`); }); const sql = `INSERT INTO ${this.driver.escapeIdentifier(table)} (${escapedFields.join(', ')}) VALUES ${placeholders.join(', ')}`; const result = await this.executeQuery(sql, values); this.reset(); return result; } // ✅ INSERT com ON DUPLICATE KEY UPDATE (MySQL) / ON CONFLICT (PostgreSQL) async insertOrUpdate(table, data, conflictColumns = null) { this._validateTableName(table); this._validateInsertData(data); if (this.driverType === 'mysql' || this.driverType === 'mariadb') { return this._insertOrUpdateMySQL(table, data); } else if (this.driverType === 'postgres' || this.driverType === 'postgresql') { return this._insertOrUpdatePostgreSQL(table, data, conflictColumns); } else { throw new Error('insertOrUpdate não suportado para este driver'); } } async _insertOrUpdateMySQL(table, data) { const fields = Object.keys(data); const values = Object.values(data); const escapedFields = fields.map(field => this.driver.escapeIdentifier(field)); const placeholders = fields.map(() => '?').join(', '); const updateClauses = fields.map(field => `${this.driver.escapeIdentifier(field)} = VALUES(${this.driver.escapeIdentifier(field)})` ); const sql = `INSERT INTO ${this.driver.escapeIdentifier(table)} (${escapedFields.join(', ')}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${updateClauses.join(', ')}`; const result = await this.executeQuery(sql, values); this.reset(); return result; } async _insertOrUpdatePostgreSQL(table, data, conflictColumns) { if (!conflictColumns || !Array.isArray(conflictColumns)) { throw new Error('conflictColumns é obrigatório para PostgreSQL'); } const fields = Object.keys(data); const values = Object.values(data); const escapedFields = fields.map(field => this.driver.escapeIdentifier(field)); const placeholders = fields.map(() => '?').join(', '); const updateClauses = fields .filter(field => !conflictColumns.includes(field)) .map(field => `${this.driver.escapeIdentifier(field)} = EXCLUDED.${this.driver.escapeIdentifier(field)}`); const escapedConflictColumns = conflictColumns.map(col => this.driver.escapeIdentifier(col)); let sql = `INSERT INTO ${this.driver.escapeIdentifier(table)} (${escapedFields.join(', ')}) VALUES (${placeholders}) ON CONFLICT (${escapedConflictColumns.join(', ')})`; if (updateClauses.length > 0) { sql += ` DO UPDATE SET ${updateClauses.join(', ')}`; } else { sql += ' DO NOTHING'; } sql += ' RETURNING *'; const result = await this.executeQuery(sql, values); this.reset(); return result; } async replace(table, data) { this._validateTableName(table); this._validateInsertData(data); if (this.driverType !== 'mysql' && this.driverType !== 'mariadb') { throw new Error('REPLACE só é suportado no MySQL/MariaDB'); } const fields = Object.keys(data); const values = Object.values(data); const escapedFields = fields.map(field => this.driver.escapeIdentifier(field)); const placeholders = fields.map(() => '?').join(', '); const sql = `REPLACE INTO ${this.driver.escapeIdentifier(table)} (${escapedFields.join(', ')}) VALUES (${placeholders})`; const result = await this.executeQuery(sql, values); this.reset(); return result; } // =============================== // ✅ UPDATE METHODS MELHORADOS // =============================== set(field, value = null) { if (typeof field === 'object' && field !== null) { this.updateData = { ...this.updateData, ...field }; } else { this._validateFieldName(field); this.updateData = this.updateData || {}; this.updateData[field] = value; } return this; } // ✅ SET com expressão raw setRaw(field, expression, bindings = []) { this._validateFieldName(field); this._validateNotEmpty(expression, 'Expressão SET'); this.updateData = this.updateData || {}; this.updateData[field] = { __raw: expression, __bindings: bindings }; return this; } async update(table, data = null, where = null) { this._validateTableName(table); const updateData = data || this.updateData; this._validateUpdateData(updateData); if (where) { this.where(where); } const setClauses = []; const setValues = []; Object.entries(updateData).forEach(([field, value]) => { if (typeof value === 'object' && value !== null && value.__raw) { // ✅ Suporte a expressões raw setClauses.push(`${this.driver.escapeIdentifier(field)} = ${value.__raw}`); if (value.__bindings && Array.isArray(value.__bindings)) { setValues.push(...value.__bindings); } } else { setClauses.push(`${this.driver.escapeIdentifier(field)} = ?`); setValues.push(value); } }); let sql = `UPDATE ${this.driver.escapeIdentifier(table)} SET ${setClauses.join(', ')}`; if (this.whereClauses.length > 0) { sql += ` WHERE ${this.whereClauses.join(' ')}`; } const allParams = [...setValues, ...this.params]; // ✅ RETURNING para PostgreSQL if (this.driverType === 'postgres' || this.driverType === 'postgresql') { sql += ' RETURNING *'; } const result = await this.executeQuery(sql, allParams); this.reset(); return result; } // ✅ Increment/Decrement async increment(table, field, amount = 1, where = null) { this._validateTableName(table); this._validateFieldName(field); this._validateNumber(amount, 'Amount'); if (where) { this.where(where); } let sql = `UPDATE ${this.driver.escapeIdentifier(table)} SET ${this.driver.escapeIdentifier(field)} = ${this.driver.escapeIdentifier(field)} + ?`; if (this.whereClauses.length > 0) { sql += ` WHERE ${this.whereClauses.join(' ')}`; } const params = [amount, ...this.params]; const result = await this.executeQuery(sql, params); this.reset(); return result; } async decrement(table, field, amount = 1, where = null) { return this.increment(table, field, -Math.abs(amount), where); } // =============================== // ✅ DELETE METHODS MELHORADOS // =============================== async delete(table = null, where = null) { if (table) { this._validateTableName(table); this.fromTable = this.driver.escapeIdentifier(table); } if (where) { this.where(where); } if (!this.fromTable) { throw new Error('Tabela é obrigatória para operação DELETE'); } let sql = `DELETE FROM ${this.fromTable}`; if (this.whereClauses.length > 0) { sql += ` WHERE ${this.whereClauses.join(' ')}`; } // ✅ RETURNING para PostgreSQL if (this.driverType === 'postgres' || this.driverType === 'postgresql') { sql += ' RETURNING *'; } const result = await this.executeQuery(sql, this.params); this.reset(); return result; } async emptyTable(table) { this._validateTableName(table); let sql; if (this.driverType === 'postgres' || this.driverType === 'postgresql') { sql = `TRUNCATE TABLE ${this.driver.escapeIdentifier(table)} RESTART IDENTITY CASCADE`; } else { sql = `TRUNCATE TABLE ${this.driver.escapeIdentifier(table)}`; } const result = await this.executeQuery(sql, []); this.reset(); return result; } // =============================== // ✅ TRANSACTION SUPPORT // =============================== async transaction(callback) { this._validateCallback(callback); const isNestedTransaction = this.inTransaction; if (!isNestedTransaction) { await this.beginTransaction(); } try { this.transactionDepth++; const result = await callback(this); if (!isNestedTransaction) { await this.commit(); } return result; } catch (error) { if (!isNestedTransaction) { await this.rollback(); } throw error; } finally { this.transactionDepth--; if (this.transactionDepth === 0) { this.inTransaction = false; } } } async beginTransaction() { const sql = this.driverType === 'postgres' || this.driverType === 'postgresql' ? 'BEGIN' : 'START TRANSACTION'; await this.executeQuery(sql, []); this.inTransaction = true; this.transactionDepth = 1; if (this.DEBUG) { console.log(`🔄 Transação iniciada [${this.queryBuilderId}]`); } } async commit() { if (!this.inTransaction) { throw new Error('Nenhuma transação ativa para commit'); } await this.executeQuery('COMMIT', []); this.inTransaction = false; this.transactionDepth = 0; if (this.DEBUG) { console.log(`✅ Transação commitada [${this.queryBuilderId}]`); } } async rollback() { if (!this.inTransaction) { throw new Error('Nenhuma transação ativa para rollback'); } await this.executeQuery('ROLLBACK', []); this.inTransaction = false; this.transactionDepth = 0; if (this.DEBUG) { console.log(`🔄 Transação revertida [${this.queryBuilderId}]`); } } // =============================== // ✅ UTILITY METHODS MELHORADOS // =============================== async query(sql, params = []) { this._validateNotEmpty(sql, 'SQL'); if (!Array.isArray(params)) { params = [params]; } return await this.executeQuery(sql, params); } getCompiledSelect() { return this.buildSelectQuery(); } getLastQuery() { return this.lastQuery; } // ✅ Clone do QueryBuilder clone() { const cloned = new QueryBuilder(this.connection, this.driverType, this.config); cloned.selectFields = [...this.selectFields]; cloned.fromTable = this.fromTable; cloned.joinClauses = [...this.joinClauses]; cloned.whereClauses = [...this.whereClauses]; cloned.groupByFields = [...this.groupByFields]; cloned.havingClauses = [...this.havingClauses]; cloned.orderByFields = [...this.orderByFields]; cloned.limitValue = this.limitValue; cloned.offsetValue = this.offsetValue; cloned.distinctFlag = this.distinctFlag; cloned.params = [...this.params]; cloned.updateData = this.updateData ? { ...this.updateData } : null; cloned.cteQueries = [...this.cteQueries]; cloned.subqueries = [...this.subqueries]; return cloned; } // ✅ Builder pattern para reutilização newQuery() { return this.builder(); } // =============================== // ✅ MÉTODOS DE VALIDAÇÃO // =============================== _validateQueryState() { if (!this.fromTable && this.currentOperation === 'SELECT') { throw new Error('FROM é obrigatório para queries SELECT'); } } _validateSelectFields(fields) { if (!this.validation.enabled) return; if (Array.isArray(fields) && fields.length > this.validation.maxSelectFields) { throw new Error(`Muitos campos SELECT (máximo: ${this.validation.maxSelectFields})`); } } _validateTableName(table) { if (!table || typeof table !== 'string' || table.trim().length === 0) { throw new Error('Nome da tabela é obrigatório e deve ser uma string não vazia'); } } _validateFieldName(field) { if (!field || typeof field !== 'string' || field.trim().length === 0) { throw new Error('Nome do campo é obrigatório e deve ser uma string não vazia'); } } _validateNotEmpty(value, name) { if (!value || (typeof value === 'string' && value.trim().length === 0)) { throw new Error(`${name} não pode estar vazio`); } } _validateOperator(operator) { const validOperators = ['=', '!=', '<>', '<', '>', '<=', '>=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN']; if (!validOperators.includes(operator.toUpperCase())) { throw new Error(`Operador inválido: ${operator}. Operadores válidos: ${validOperators.join(', ')}`); } } _validateArrayValues(values, method) { if (!Array.isArray(values) || values.length === 0) { throw new Error(`${method} requer um array não vazio`); } } _validateWhereCount() { if (!this.validation.enabled) return; if (this.whereClauses.length >= this.validation.maxWhereConditions) { throw new Error(`Muitas condições WHERE (máximo: ${this.validation.maxWhereConditions})`); } } _validateJoinCount() { if (!this.validation.enabled) return; if (this.joinClauses.length >= this.validation.maxJoins) { throw new Error(`Muitos JOINs (máximo: ${this.validation.maxJoins})`); } } _validateGroupByCount() { if (!this.validation.enabled) return; if (this.groupByFields.length >= this.validation.maxGroupByFields) { throw new Error(`Muitos campos GROUP BY (máximo: ${this.validation.maxGroupByFields})`); } } _validateOrderByCount() { if (!this.validation.enabled) return; if (this.orderByFields.length >= this.validation.maxOrderByFields) { throw new Error(`Muitos campos ORDER BY (máximo: ${this.validation.maxOrderByFields})`); } } _validateLimit(limit) { if (!Number.isInteger(limit) || limit < 0) { throw new Error('LIMIT deve ser um número inteiro não negativo'); } } _validateOffset(offset) { if (!Number.isInteger(offset) || offset < 0) { throw new Error('OFFSET deve ser um número inteiro não negativo'); } } _validatePagination(page, perPage) { if (!Number.isInteger(page) || page < 1) { throw new Error('Página deve ser um número inteiro maior que 0'); } if (!Number.isInteger(perPage) || perPage < 1 || perPage > 1000) { throw new Error('Itens por página deve ser um número inteiro entre 1 e 1000'); } } _validateInsertData(data) { if (!data || (typeof data !== 'object' && !Array.isArray(data))) { throw new Error('Dados para inserção são obrigatórios'); } if (typeof data === 'object' && !Array.isArray(data) && Object.keys(data).length === 0) { throw new Error('Dados para inserção não podem estar vazios'); } } _validateBatchData(data) { if (!Array.isArray(data) || data.length === 0) { throw new Error('insertBatch requer um array não vazio'); } const firstRowFields = Object.keys(data[0]); if (firstRowFields.length === 0) { throw new Error('Primeira linha do batch não pode estar vazia'); } } _validateUpdateData(data) { if (!data || typeof data !== 'object' || Object.keys(data).length === 0) { throw new Error('Dados para atualização são obrigatórios'); } } _validateNumber(value, name) { if (typeof value !== 'number' || isNaN(value)) { throw new Error(`${name} deve ser um número válido`); } } _validateChunkSize(size) { if (!Number.isInteger(size) || size < 1 || size > 10000) { throw new Error('Tamanho do chunk deve ser um número inteiro entre 1 e 10000'); } } _validateCallback(callback) { if (typeof callback !== 'function') { throw new Error('Callback deve ser uma função'); } } // =============================== // ✅ MÉTODOS AUXILIARES // =============================== _addWhereClause(condition, value = null) { const connector = this.whereClauses.length > 0 ? 'AND' : ''; if (connector) { this.whereClauses.push(`${connector} ${condition}`); } else { this.whereClauses.push(condition); } if (value !== null && value !== undefined) { if (Array.isArray(value)) { this.params.push(...value); } else { this.params.push(value); } } } _addWhereGroup(callback) { const groupBuilder = this.clone(); groupBuilder.whereClauses = []; groupBuilder.params = []; callback(groupBuilder); if (groupBuilder.whereClauses.length > 0) { const groupCondition = groupBuilder.whereClauses.join(' '); this._addWhereClause(`(${groupCondition})`); this.params.push(...groupBuilder.params); } } _addOrWhereGroup(callbac