UNPKG

@04l3x4ndr3/microbase-orm

Version:

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

934 lines (778 loc) 36.6 kB
class PostgreSQLDriver { constructor(connection, config = {}) { this.connection = connection; this.config = config; this.isPool = !!config.max; this.DEBUG = config.debug || false; this.driverId = this.generateDriverId(); this.schema = this.extractSchemaFromOptions(config.options); // ✅ Proteção contra recursão melhorada this.errorDepth = 0; this.maxErrorDepth = 10; // ✅ Cache de prepared statements this.preparedStatements = new Map(); this.maxPreparedStatements = config.maxPreparedStatements || 100; // ✅ Métricas específicas do PostgreSQL this.metrics = { queriesExecuted: 0, preparedStatementsUsed: 0, errorsCount: 0, avgQueryTime: 0, lastQueryTime: null, slowQueries: 0, slowQueryThreshold: config.slowQueryThreshold || 1000, transactionOperations: 0, schemaOperations: 0, indexOperations: 0 }; // ✅ Rate limiting para logs this.logRateLimit = new Map(); this.maxLogsPerMinute = config.maxLogsPerMinute || 10; // ✅ Query timeout específico para PostgreSQL this.queryTimeout = config.queryTimeout || 30000; // ✅ Configurações específicas do PostgreSQL this.postgresFeatures = { supportsJSONB: true, supportsArrays: true, supportsWindowFunctions: true, supportsCommonTableExpressions: true, supportsPartitioning: true, supportsFullTextSearch: true, supportsUUIDs: true, supportsEnums: true, supportsRangeTypes: true }; // ✅ Cache de conversão de placeholders this.placeholderCache = new Map(); console.log(`🔧 PostgreSQLDriver inicializado [ID: ${this.driverId}] - Schema: ${this.schema} - Pool: ${this.isPool}`); } // ✅ Gerador de ID único generateDriverId() { return `postgres_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`; } // ✅ Extração de schema melhorada extractSchemaFromOptions(options) { if (!options) return 'public'; // Suportar múltiplos formatos de search_path const patterns = [ /--search_path=([^,\s]+)/, /search_path\s*=\s*([^,\s]+)/, /-c\s+search_path=([^,\s]+)/ ]; for (const pattern of patterns) { const match = options.match(pattern); if (match) return match[1]; } return 'public'; } // ✅ Escape melhorado com validação escapeIdentifier(identifier) { if (!identifier) { throw new Error('Identifier não pode ser vazio'); } // Cache para identifiers comuns const cacheKey = `ident_${identifier}`; if (this.preparedStatements.has(cacheKey)) { return this.preparedStatements.get(cacheKey); } let escaped; // Se o identificador já contém schema, não modificar if (identifier.includes('.')) { const parts = identifier.split('.'); parts.forEach(part => this._validateIdentifier(part)); escaped = parts.map(part => `"${part.replace(/"/g, '""')}"`).join('.'); } else { this._validateIdentifier(identifier); escaped = `"${identifier.replace(/"/g, '""')}"`; } // Adicionar ao cache se não estiver cheio if (this.preparedStatements.size < this.maxPreparedStatements) { this.preparedStatements.set(cacheKey, escaped); } return escaped; } // ✅ Validação de identificadores PostgreSQL _validateIdentifier(identifier) { // PostgreSQL tem limite de 63 caracteres para identificadores if (identifier.length > 63) { throw new Error(`Nome de identificador muito longo (máximo 63 caracteres): ${identifier}`); } // Verificar se começa com letra ou underscore if (!/^[a-zA-Z_]/.test(identifier)) { throw new Error(`Identificador deve começar com letra ou underscore: ${identifier}`); } // Verificar caracteres válidos (letras, números, underscore, $) if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(identifier)) { throw new Error(`Identificador contém caracteres inválidos: ${identifier}`); } } // ✅ Escape de valores com suporte a tipos específicos do PostgreSQL escapeValue(value) { if (value === null || value === undefined) return 'NULL'; if (typeof value === 'string') { // Escape específico para PostgreSQL return `'${value.replace(/'/g, "''")}'`; } if (typeof value === 'number') { if (Number.isNaN(value)) return 'NULL'; if (!Number.isFinite(value)) return 'NULL'; return value.toString(); } if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE'; if (value instanceof Date) { if (isNaN(value.getTime())) return 'NULL'; return `'${value.toISOString()}'`; } if (Buffer.isBuffer(value)) { return `'\\x${value.toString('hex')}'`; } // ✅ Suporte nativo a arrays PostgreSQL if (Array.isArray(value)) { const escapedValues = value.map(v => this.escapeValue(v)); return `ARRAY[${escapedValues.join(', ')}]`; } // ✅ Suporte a JSONB nativo do PostgreSQL if (typeof value === 'object' && value.constructor === Object) { return `'${JSON.stringify(value).replace(/'/g, "''")}'::jsonb`; } if (typeof value === 'object') { return `'${JSON.stringify(value).replace(/'/g, "''")}'`; } return `'${String(value).replace(/'/g, "''")}'`; } // ✅ Conversão de placeholders com cache _convertPlaceholders(sql) { // Verificar cache primeiro if (this.placeholderCache.has(sql)) { return this.placeholderCache.get(sql); } let paramIndex = 1; const convertedSql = sql.replace(/\?/g, () => `$${paramIndex++}`); // Adicionar ao cache se não estiver cheio if (this.placeholderCache.size < this.maxPreparedStatements) { this.placeholderCache.set(sql, convertedSql); } return convertedSql; } // ✅ Execute com melhorias específicas do PostgreSQL async execute(sql, params = []) { if (this.errorDepth > this.maxErrorDepth) { throw new Error('Stack overflow detectado - muitos erros aninhados'); } const queryId = this.generateQueryId(); const startTime = Date.now(); try { // ✅ Converter placeholders MySQL (?) para PostgreSQL ($1, $2, etc.) const convertedSql = this._convertPlaceholders(sql); if (this.DEBUG && this._shouldLog('debug')) { console.log(`🔍 PostgreSQL SQL Debug [${queryId}]:`, this._sanitizeForLog(convertedSql)); console.log(`📝 PostgreSQL Params [${queryId}]:`, this._sanitizeParams(params)); console.log(`🗄️ Schema [${queryId}]:`, this.schema); console.log(`🏊‍♂️ Using Pool [${queryId}]:`, this.isPool); } // ✅ Detecção de operações específicas this._detectOperationType(sql); // ✅ Timeout específico para PostgreSQL const queryPromise = this._executeWithPostgreSQLOptimizations(convertedSql, params); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error(`PostgreSQL Query timeout após ${this.queryTimeout}ms`)), this.queryTimeout); }); const result = await Promise.race([queryPromise, timeoutPromise]); const duration = Date.now() - startTime; this._updateMetrics(duration); if (duration > this.metrics.slowQueryThreshold) { this.metrics.slowQueries++; if (this._shouldLog('slow')) { console.warn(`🐌 Slow PostgreSQL query detected [${queryId}] - ${duration}ms: ${this._sanitizeForLog(convertedSql)}`); } } return result.rows; } catch (error) { const duration = Date.now() - startTime; this._updateMetrics(duration, true); throw this.handlePostgreSQLError(error, sql, queryId); } } // ✅ Otimizações específicas do PostgreSQL async _executeWithPostgreSQLOptimizations(sql, params) { let result; // ✅ Usar prepared statements quando possível if (params.length > 0 && this._shouldUsePreparedStatement(sql)) { this.metrics.preparedStatementsUsed++; } // Verificar se é um pool ou conexão única if (this.connection.query) { // Pool ou cliente direto result = await this.connection.query(sql, params); } else { // Pool que precisa de getConnection const client = await this.connection.connect(); try { result = await client.query(sql, params); } finally { client.release(); } } return result; } _shouldUsePreparedStatement(sql) { // PostgreSQL se beneficia de prepared statements para queries repetitivas return /^(INSERT|UPDATE|DELETE|SELECT)\s/i.test(sql.trim()); } _detectOperationType(sql) { const trimmedSql = sql.trim().toUpperCase(); if (trimmedSql.includes('BEGIN') || trimmedSql.includes('COMMIT') || trimmedSql.includes('ROLLBACK')) { this.metrics.transactionOperations++; } if (trimmedSql.includes('CREATE SCHEMA') || trimmedSql.includes('DROP SCHEMA')) { this.metrics.schemaOperations++; } if (trimmedSql.includes('CREATE INDEX') || trimmedSql.includes('DROP INDEX') || trimmedSql.includes('REINDEX')) { this.metrics.indexOperations++; } } generateQueryId() { return `pg_q_${Date.now()}_${Math.random().toString(36).substr(2, 4)}`; } _shouldLog(type) { const now = Date.now(); const minute = Math.floor(now / 60000); const key = `${type}_${minute}`; if (!this.logRateLimit.has(key)) { this.logRateLimit.set(key, 0); } const count = this.logRateLimit.get(key); if (count < this.maxLogsPerMinute) { this.logRateLimit.set(key, count + 1); return true; } return false; } _sanitizeForLog(sql) { if (!sql) return sql; let sanitized = sql.length > 400 ? sql.substring(0, 400) + '...' : sql; sanitized = sanitized.replace(/password\s*=\s*['"][^'"]*['"]/gi, 'password=***'); return sanitized; } _sanitizeParams(params) { if (!Array.isArray(params)) return params; return params.map((param, index) => { if (typeof param === 'string' && param.length > 200) { return `${param.substring(0, 200)}... [${param.length} chars]`; } return param; }); } _updateMetrics(duration, isError = false) { this.metrics.queriesExecuted++; this.metrics.lastQueryTime = duration; if (isError) { this.metrics.errorsCount++; } else { if (this.metrics.avgQueryTime === 0) { this.metrics.avgQueryTime = duration; } else { this.metrics.avgQueryTime = (this.metrics.avgQueryTime * 0.9) + (duration * 0.1); } } } // ✅ Tratamento de erro específico e melhorado para PostgreSQL handlePostgreSQLError(error, sql, queryId = null) { this.errorDepth++; try { const errorCode = error.code || 'UNKNOWN'; const errorMessage = String(error.message || error.toString() || 'Erro desconhecido'); const severity = error.severity || 'ERROR'; const detail = error.detail || ''; const hint = error.hint || ''; const logPrefix = queryId ? `[${queryId}]` : ''; switch (errorCode) { case '42P01': // undefined_table return new Error(`Tabela não encontrada ${logPrefix}: Schema '${this.schema}' - ${errorMessage}`); case '42703': // undefined_column const columnMatch = errorMessage.match(/column "(.+)" does not exist/); if (columnMatch) { return new Error(`Coluna não encontrada ${logPrefix}: '${columnMatch[1]}' - ${hint || 'Verifique o nome da coluna'}`); } return new Error(`Coluna não encontrada ${logPrefix}: ${errorMessage}`); case '3F000': // invalid_schema_name return new Error(`Schema inválido ${logPrefix}: '${this.schema}' não encontrado - ${errorMessage}`); case '23505': // unique_violation const uniqueMatch = errorMessage.match(/Key \((.+)\)=\((.+)\) already exists/); if (uniqueMatch) { return new Error(`Violação de chave única ${logPrefix}: Campo(s) '${uniqueMatch[1]}' com valor '${uniqueMatch[2]}' já existe`); } return new Error(`Violação de chave única ${logPrefix}: ${errorMessage}`); case '23503': // foreign_key_violation const fkMatch = errorMessage.match(/Key \((.+)\)=\((.+)\) is not present in table "(.+)"/); if (fkMatch) { return new Error(`Violação de chave estrangeira ${logPrefix}: Valor '${fkMatch[2]}' para '${fkMatch[1]}' não existe na tabela '${fkMatch[3]}'`); } return new Error(`Violação de chave estrangeira ${logPrefix}: ${errorMessage}`); case '23502': // not_null_violation const nullMatch = errorMessage.match(/null value in column "(.+)" violates not-null constraint/); if (nullMatch) { return new Error(`Campo obrigatório ${logPrefix}: '${nullMatch[1]}' não pode ser NULL`); } return new Error(`Violação de NOT NULL ${logPrefix}: ${errorMessage}`); case '22001': // string_data_right_truncation return new Error(`Dados muito longos ${logPrefix}: ${errorMessage} - ${hint}`); case '22P02': // invalid_text_representation return new Error(`Formato de dados inválido ${logPrefix}: ${errorMessage}`); case '23514': // check_violation return new Error(`Violação de constraint CHECK ${logPrefix}: ${errorMessage}`); // Erros de conexão case '08006': // connection_failure return new Error(`Falha na conexão PostgreSQL ${logPrefix}: ${errorMessage}`); case '08001': // sqlclient_unable_to_establish_sqlconnection return new Error(`Não foi possível estabelecer conexão PostgreSQL ${logPrefix}: ${errorMessage}`); case '08003': // connection_does_not_exist return new Error(`Conexão PostgreSQL não existe ${logPrefix}: ${errorMessage}`); case '08004': // sqlserver_rejected_establishment_of_sqlconnection return new Error(`Servidor PostgreSQL rejeitou a conexão ${logPrefix}: ${errorMessage}`); // Erros de autenticação case '28P01': // invalid_password return new Error(`Senha inválida PostgreSQL ${logPrefix}: ${errorMessage}`); case '28000': // invalid_authorization_specification return new Error(`Especificação de autorização inválida ${logPrefix}: ${errorMessage}`); // Erros de banco/schema case '3D000': // invalid_catalog_name return new Error(`Banco de dados não encontrado ${logPrefix}: ${errorMessage}`); case '42P06': // duplicate_schema return new Error(`Schema já existe ${logPrefix}: ${errorMessage}`); case '2BP01': // dependent_objects_still_exist return new Error(`Não é possível remover: objetos dependentes ainda existem ${logPrefix}: ${errorMessage}`); // Erros de sintaxe case '42601': // syntax_error return new Error(`Erro de sintaxe SQL ${logPrefix}: ${errorMessage}\nSQL: ${this._sanitizeForLog(sql)}`); case '42804': // datatype_mismatch return new Error(`Incompatibilidade de tipos ${logPrefix}: ${errorMessage}`); case '42883': // undefined_function return new Error(`Função não definida ${logPrefix}: ${errorMessage}`); // Erros de transação case '25P02': // in_failed_sql_transaction return new Error(`Transação em estado falho ${logPrefix}: Execute ROLLBACK primeiro`); case '40001': // serialization_failure return new Error(`Falha de serialização ${logPrefix}: Deadlock detectado - ${errorMessage}`); case '40P01': // deadlock_detected return new Error(`Deadlock detectado ${logPrefix}: ${errorMessage}`); // Erros de permissão case '42501': // insufficient_privilege return new Error(`Privilégios insuficientes ${logPrefix}: ${errorMessage}`); // Erros específicos do PostgreSQL case '22012': // division_by_zero return new Error(`Divisão por zero ${logPrefix}: ${errorMessage}`); case '2200C': // invalid_use_of_escape_character return new Error(`Uso inválido de caractere de escape ${logPrefix}: ${errorMessage}`); case '22025': // invalid_escape_sequence return new Error(`Sequência de escape inválida ${logPrefix}: ${errorMessage}`); case '22008': // datetime_field_overflow return new Error(`Overflow em campo de data/hora ${logPrefix}: ${errorMessage}`); case '22007': // invalid_datetime_format return new Error(`Formato de data/hora inválido ${logPrefix}: ${errorMessage}`); // Erros de JSON case '22032': // invalid_json_text return new Error(`JSON inválido ${logPrefix}: ${errorMessage}`); case '22033': // invalid_sql_json_subscript return new Error(`Subscript JSON inválido ${logPrefix}: ${errorMessage}`); // Erros de array case '2202E': // array_subscript_error return new Error(`Erro de subscript de array ${logPrefix}: ${errorMessage}`); default: if (this._shouldLog('unknown_error')) { console.error(`❌ PostgreSQL Error desconhecido ${logPrefix}:`, { code: errorCode, severity, message: errorMessage, detail, hint, sql: this._sanitizeForLog(sql) }); } let fullMessage = `PostgreSQL Error ${logPrefix} [${errorCode}/${severity}]: ${errorMessage}`; if (detail) fullMessage += `\nDetalhe: ${detail}`; if (hint) fullMessage += `\nDica: ${hint}`; return new Error(fullMessage); } } finally { this.errorDepth--; } } getLimitSyntax(limit, offset = 0) { const limitNum = parseInt(limit); const offsetNum = parseInt(offset); if (isNaN(limitNum) || limitNum < 0) { throw new Error('LIMIT deve ser um número não negativo'); } if (isNaN(offsetNum) || offsetNum < 0) { throw new Error('OFFSET deve ser um número não negativo'); } return `LIMIT ${limitNum} OFFSET ${offsetNum}`; } getRandomFunction() { return 'RANDOM()'; } // ✅ Verificação de tabela com cache e schema específico async tableExists(tableName) { try { const result = await this.execute(` SELECT EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2 AND table_type = 'BASE TABLE' )`, [this.schema, tableName]); return result[0]?.exists; } catch (error) { if (this._shouldLog('table_check_error')) { console.error(`❌ Erro ao verificar existência da tabela ${this.schema}.${tableName}:`, error.message); } return false; } } // ✅ Listar tabelas com informações extras async listTables() { try { const result = await this.execute(` SELECT t.table_name, t.table_type, obj_description(c.oid) as table_comment, pg_size_pretty(pg_total_relation_size(c.oid)) as size, pg_stat_get_numscans(c.oid) as seq_scans, pg_stat_get_tuples_returned(c.oid) as tuples_returned FROM information_schema.tables t LEFT JOIN pg_class c ON c.relname = t.table_name LEFT JOIN pg_namespace n ON n.oid = c.relnamespace WHERE t.table_schema = $1 AND t.table_type = 'BASE TABLE' AND n.nspname = $1 ORDER BY t.table_name `, [this.schema]); return result.map(row => ({ name: row.table_name, type: row.table_type, comment: row.table_comment || null, size: row.size || '0 bytes', seqScans: row.seq_scans || 0, tuplesReturned: row.tuples_returned || 0 })); } catch (error) { throw new Error(`Erro ao listar tabelas no schema '${this.schema}': ${error.message}`); } } // ✅ Descrição detalhada de tabela async describeTable(tableName) { try { const result = await this.execute(` SELECT column_name as field, data_type as type, is_nullable as "null", column_default as "default", character_maximum_length as max_length, numeric_precision, numeric_scale, udt_name as udt_type, col_description(pgc.oid, ordinal_position) as comment, ordinal_position as position, CASE WHEN pk.column_name IS NOT NULL THEN 'PRI' WHEN fk.column_name IS NOT NULL THEN 'MUL' ELSE '' END as key FROM information_schema.columns c LEFT JOIN pg_class pgc ON pgc.relname = c.table_name LEFT JOIN pg_namespace pgn ON pgn.oid = pgc.relnamespace AND pgn.nspname = c.table_schema LEFT JOIN ( SELECT ku.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage ku ON tc.constraint_name = ku.constraint_name WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = $1 AND tc.table_name = $2 ) pk ON pk.column_name = c.column_name LEFT JOIN ( SELECT ku.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage ku ON tc.constraint_name = ku.constraint_name WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1 AND tc.table_name = $2 ) fk ON fk.column_name = c.column_name WHERE c.table_schema = $1 AND c.table_name = $2 ORDER BY c.ordinal_position `, [this.schema, tableName]); return result.map(col => ({ Field: col.field, Type: col.type, UDTType: col.udt_type, Null: col.null === 'YES' ? 'YES' : 'NO', Key: col.key || '', Default: col.default, MaxLength: col.max_length, NumericPrecision: col.numeric_precision, NumericScale: col.numeric_scale, Comment: col.comment || '', Position: col.position })); } catch (error) { throw new Error(`Erro ao descrever tabela ${this.schema}.${tableName}: ${error.message}`); } } // ✅ Informações do banco específicas do PostgreSQL async getDatabaseInfo() { try { const [version] = await this.execute('SELECT version() as version'); const [encoding] = await this.execute('SELECT pg_encoding_to_char(encoding) as encoding FROM pg_database WHERE datname = current_database()'); const [collation] = await this.execute('SELECT datcollate as collation FROM pg_database WHERE datname = current_database()'); const [timezone] = await this.execute('SELECT current_setting(\'timezone\') as timezone'); const [maxConnections] = await this.execute('SELECT current_setting(\'max_connections\') as max_connections'); return { version: version.version, encoding: encoding.encoding, collation: collation.collation, timezone: timezone.timezone, maxConnections: maxConnections.max_connections, currentSchema: this.schema, driverId: this.driverId, features: this.postgresFeatures }; } catch (error) { throw new Error(`Erro ao obter informações do banco: ${error.message}`); } } // ✅ Estatísticas específicas do PostgreSQL async getPerformanceStats() { try { const stats = await this.execute(` SELECT (SELECT setting FROM pg_settings WHERE name = 'max_connections') as max_connections, (SELECT count(*) FROM pg_stat_activity) as current_connections, (SELECT count(*) FROM pg_stat_activity WHERE state = 'active') as active_connections, (SELECT count(*) FROM pg_stat_activity WHERE state = 'idle') as idle_connections, (SELECT extract(epoch from now() - pg_postmaster_start_time())) as uptime_seconds, (SELECT sum(numbackends) FROM pg_stat_database) as total_backends, (SELECT sum(xact_commit) FROM pg_stat_database) as total_commits, (SELECT sum(xact_rollback) FROM pg_stat_database) as total_rollbacks, (SELECT sum(tup_returned) FROM pg_stat_database) as tuples_returned, (SELECT sum(tup_fetched) FROM pg_stat_database) as tuples_fetched, (SELECT sum(tup_inserted) FROM pg_stat_database) as tuples_inserted, (SELECT sum(tup_updated) FROM pg_stat_database) as tuples_updated, (SELECT sum(tup_deleted) FROM pg_stat_database) as tuples_deleted `); const result = stats[0] || {}; // ✅ Adicionar métricas do driver result.driver_metrics = this.getDriverMetrics(); return result; } catch (error) { throw new Error(`Erro ao obter estatísticas: ${error.message}`); } } getDriverMetrics() { return { ...this.metrics, driverId: this.driverId, currentSchema: this.schema, preparedStatementsCount: this.preparedStatements.size, placeholderCacheSize: this.placeholderCache.size, isPool: this.isPool, features: this.postgresFeatures }; } // ✅ Ping melhorado para PostgreSQL async ping() { try { const startTime = Date.now(); await this.execute('SELECT 1 as ping'); const responseTime = Date.now() - startTime; return { status: 'ok', responseTime, timestamp: Date.now(), driver: 'PostgreSQL', schema: this.schema }; } catch (error) { return { status: 'error', error: error.message, timestamp: Date.now(), driver: 'PostgreSQL', schema: this.schema }; } } // ✅ Verificação de schema melhorada async schemaExists() { try { const result = await this.execute(` SELECT EXISTS ( SELECT 1 FROM information_schema.schemata WHERE schema_name = $1 )`, [this.schema]); return result[0]?.exists; } catch (error) { if (this._shouldLog('schema_check_error')) { console.error(`❌ Erro ao verificar existência do schema ${this.schema}:`, error.message); } return false; } } // ✅ Criação de schema melhorada async createSchemaIfNotExists() { try { const exists = await this.schemaExists(); if (!exists) { await this.execute(`CREATE SCHEMA IF NOT EXISTS "${this.schema}"`); console.log(`✅ Schema '${this.schema}' criado com sucesso`); } else { console.log(`ℹ️ Schema '${this.schema}' já existe`); } return true; } catch (error) { throw new Error(`Erro ao criar schema '${this.schema}': ${error.message}`); } } // ✅ Backup de tabela com opções específicas do PostgreSQL async backupTable(tableName, options = {}) { try { const timestamp = options.timestamp || Date.now(); const suffix = options.suffix || 'backup'; const backupTableName = `${tableName}_${suffix}_${timestamp}`; const targetSchema = options.targetSchema || this.schema; const escapedBackupName = this.escapeIdentifier(`${targetSchema}.${backupTableName}`); const escapedTableName = this.escapeIdentifier(`${this.schema}.${tableName}`); const tableExists = await this.tableExists(tableName); if (!tableExists) { throw new Error(`Tabela original ${this.schema}.${tableName} não existe`); } if (options.structureOnly) { await this.execute(`CREATE TABLE ${escapedBackupName} (LIKE ${escapedTableName} INCLUDING ALL)`); } else { let createSql = `CREATE TABLE ${escapedBackupName} AS SELECT * FROM ${escapedTableName}`; if (options.whereClause) { createSql += ` WHERE ${options.whereClause}`; } await this.execute(createSql); } console.log(`✅ Backup PostgreSQL da tabela ${this.schema}.${tableName} criado como ${targetSchema}.${backupTableName}`); return { originalTable: `${this.schema}.${tableName}`, backupTable: `${targetSchema}.${backupTableName}`, timestamp: Date.now(), structureOnly: !!options.structureOnly, targetSchema, whereClause: options.whereClause || null }; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); throw new Error(`Erro ao criar backup da tabela ${this.schema}.${tableName}: ${errorMsg}`); } } // ✅ Métodos específicos do PostgreSQL async analyzeTable(tableName) { try { const fullTableName = `"${this.schema}"."${tableName}"`; await this.execute(`ANALYZE ${fullTableName}`); return { table: `${this.schema}.${tableName}`, operation: 'analyze', timestamp: Date.now(), driver: 'PostgreSQL' }; } catch (error) { throw new Error(`Erro ao analisar tabela ${this.schema}.${tableName}: ${error.message}`); } } async vacuumTable(tableName, options = {}) { try { const fullTableName = `"${this.schema}"."${tableName}"`; let vacuumSql = 'VACUUM'; if (options.full) vacuumSql += ' FULL'; if (options.verbose) vacuumSql += ' VERBOSE'; if (options.analyze !== false) vacuumSql += ' ANALYZE'; vacuumSql += ` ${fullTableName}`; await this.execute(vacuumSql); return { table: `${this.schema}.${tableName}`, operation: 'vacuum', options, timestamp: Date.now(), driver: 'PostgreSQL' }; } catch (error) { throw new Error(`Erro ao executar VACUUM na tabela ${this.schema}.${tableName}: ${error.message}`); } } async reindexTable(tableName) { try { const fullTableName = `"${this.schema}"."${tableName}"`; await this.execute(`REINDEX TABLE ${fullTableName}`); return { table: `${this.schema}.${tableName}`, operation: 'reindex', timestamp: Date.now(), driver: 'PostgreSQL' }; } catch (error) { throw new Error(`Erro ao reindexar tabela ${this.schema}.${tableName}: ${error.message}`); } } // ✅ Teste de funcionalidades específicas do PostgreSQL async supportsJSONB() { try { await this.execute("SELECT '{}'::jsonb as test"); return true; } catch (error) { return false; } } async supportsArrays() { try { await this.execute("SELECT ARRAY[1,2,3] as test"); return true; } catch (error) { return false; } } async supportsWindowFunctions() { try { await this.execute('SELECT ROW_NUMBER() OVER (ORDER BY 1) as test FROM (SELECT 1) as t'); return true; } catch (error) { return false; } } async supportsCommonTableExpressions() { try { await this.execute('WITH cte AS (SELECT 1 as test) SELECT test FROM cte'); return true; } catch (error) { return false; } } // ✅ Limpeza de recursos clearCache() { this.preparedStatements.clear(); this.placeholderCache.clear(); this.logRateLimit.clear(); console.log(`🧹 Cache limpo para PostgreSQLDriver [${this.driverId}]`); } getDriverInfo() { return { driverId: this.driverId, type: 'PostgreSQL', schema: this.schema, isPool: this.isPool, debug: this.DEBUG, metrics: this.getDriverMetrics(), cacheSize: this.preparedStatements.size, placeholderCacheSize: this.placeholderCache.size, maxCacheSize: this.maxPreparedStatements, features: this.postgresFeatures }; } } export default PostgreSQLDriver;