/** * Driver Firebird com connection pooling. * * Mantém um pool de conexões pré-abertas por configuração de banco. * Elimina o overhead de attach/detach a cada query (~18ms por query). * * API pública idêntica à versão anterior — transparente para os controllers. */ const Firebird = require('node-firebird'); // ============================================================ // Configuração do Pool // ============================================================ const POOL_SIZE = parseInt(process.env.FB_POOL_SIZE, 10) || 5; const POOL_MAX = POOL_SIZE * 3; // máximo de conexões simultâneas // Um pool por config (key = host:port:database) const pools = new Map(); function configKey(config) { return `${config.host}:${config.port}:${config.database}`; } // ============================================================ // Pool de Conexões // ============================================================ class Pool { constructor(config, size) { this.config = config; this.size = size; this.maxSize = POOL_MAX; this.available = []; this.busy = 0; this.waiting = []; this.ready = false; this.initPromise = null; } async _init() { if (this.ready) return; if (this.initPromise) return this.initPromise; this.initPromise = (async () => { const errors = []; for (let i = 0; i < this.size; i++) { try { const db = await this._attach(); this.available.push(db); } catch (e) { errors.push(e.message); } } if (this.available.length === 0) { throw new Error(`Pool: falha ao abrir conexões: ${errors.join('; ')}`); } this.ready = true; console.log(`[Pool] ${this.available.length} conexões abertas para ${configKey(this.config)}`); })(); return this.initPromise; } _attach() { return new Promise((resolve, reject) => { Firebird.attach(this.config, (err, db) => { if (err) return reject(err); resolve(db); }); }); } async acquire() { if (!this.ready) await this._init(); // Conexão disponível no pool if (this.available.length > 0) { this.busy++; return this.available.pop(); } // Pool esgotado mas dentro do limite máximo: abre nova if (this.busy < this.maxSize) { try { const db = await this._attach(); this.busy++; return db; } catch (e) { console.error('[Pool] Falha ao abrir conexão extra:', e.message); throw e; } } // Limite máximo atingido: entra na fila de espera return new Promise((resolve) => { this.waiting.push(resolve); }); } release(db) { if (this.waiting.length > 0) { // Entrega para o próximo da fila const resolve = this.waiting.shift(); resolve(db); } else { // Devolve ao pool se ainda há espaço if (this.available.length + this.busy <= this.maxSize) { this.available.push(db); } else { try { db.detach(); } catch (_) {} } this.busy--; } } async close() { this.ready = false; const all = [...this.available]; this.available = []; for (const db of all) { try { db.detach(); } catch (_) {} } } } function getPool(config) { const key = configKey(config); if (!pools.has(key)) { pools.set(key, new Pool(config, POOL_SIZE)); } return pools.get(key); } // ============================================================ // Leitura de BLOBs // ============================================================ function readBlob(blobFunc) { return new Promise((resolve, reject) => { if (typeof blobFunc !== 'function') return resolve(blobFunc); blobFunc((err, name, emitter) => { if (err) return reject(err); if (!emitter || typeof emitter.on !== 'function') return resolve(null); const chunks = []; let total = 0; emitter.on('data', (c) => { chunks.push(c); total += c.length; }); emitter.on('end', () => resolve(Buffer.concat(chunks, total))); emitter.on('error', reject); }); }); } // ============================================================ // Query (SELECT) — via pool // ============================================================ function query(config, sql, params = []) { return new Promise((resolve, reject) => { getPool(config).acquire().then((db) => { const allRows = []; db.sequentially(sql, params, (row) => { const keys = Object.keys(row); const blobPromises = keys.map((key) => { const val = row[key]; if (typeof val === 'function') { return readBlob(val).then((data) => { row[key] = data; }); } return Promise.resolve(); }); return Promise.all(blobPromises).then(() => { allRows.push(row); }); }, (queryErr) => { getPool(config).release(db); if (queryErr) return reject(new Error(`Erro na consulta (firebird): ${queryErr.message}`)); resolve(allRows); }); }).catch((err) => { reject(new Error(`Erro ao conectar (firebird): ${err.message}`)); }); }); } // ============================================================ // Execute (INSERT/UPDATE/DELETE/DDL) — via pool // ============================================================ function execute(config, sql, params = []) { return new Promise((resolve, reject) => { getPool(config).acquire().then((db) => { const pool = getPool(config); db.transaction(Firebird.ISOLATION_READ_COMMITTED, (transErr, transaction) => { if (transErr) { pool.release(db); return reject(new Error(`Erro ao iniciar transação (firebird): ${transErr.message}`)); } transaction.query(sql, params, (queryErr, result) => { if (queryErr) { try { transaction.rollback(); } catch (_) {} pool.release(db); return reject(new Error(`Erro na execução (firebird): ${queryErr.message}`)); } transaction.commit((commitErr) => { pool.release(db); if (commitErr) return reject(new Error(`Erro ao commitar (firebird): ${commitErr.message}`)); resolve({ affectedRows: result ? result.length : 0, result }); }); }); }); }).catch((err) => { reject(new Error(`Erro ao conectar (firebird): ${err.message}`)); }); }); } // ============================================================ // Teste de conexão (usa pool) // ============================================================ async function testConnection(config) { await query(config, 'SELECT 1 FROM RDB$DATABASE'); return true; } // ============================================================ // Introspecção // ============================================================ async function listTables(config) { const rows = await query(config, ` SELECT TRIM(RDB$RELATION_NAME) AS TABLE_NAME FROM RDB$RELATIONS WHERE RDB$SYSTEM_FLAG = 0 AND RDB$RELATION_TYPE = 0 ORDER BY RDB$RELATION_NAME `); return rows.map((r) => (r.TABLE_NAME || '').trim()).filter(Boolean); } async function tableInfo(config, tableName) { const rows = await query(config, ` SELECT rf.RDB$FIELD_NAME AS COLUMN_NAME, rf.RDB$FIELD_POSITION AS ORDINAL_POSITION, CASE f.RDB$FIELD_TYPE WHEN 7 THEN 'SMALLINT' WHEN 8 THEN 'INTEGER' WHEN 16 THEN 'BIGINT' WHEN 9 THEN 'QUAD' WHEN 10 THEN 'FLOAT' WHEN 27 THEN 'DOUBLE PRECISION' WHEN 12 THEN 'DATE' WHEN 13 THEN 'TIME' WHEN 35 THEN 'TIMESTAMP' WHEN 37 THEN 'VARCHAR' WHEN 40 THEN 'CSTRING' WHEN 45 THEN 'BLOB_ID' WHEN 261 THEN 'BLOB' WHEN 14 THEN 'CHAR' WHEN 41 THEN 'NUMERIC' ELSE 'UNKNOWN' END AS DATA_TYPE, f.RDB$FIELD_LENGTH AS FIELD_LENGTH, f.RDB$FIELD_SCALE AS FIELD_SCALE, f.RDB$FIELD_PRECISION AS FIELD_PRECISION, rf.RDB$NULL_FLAG AS NULL_FLAG FROM RDB$RELATION_FIELDS rf INNER JOIN RDB$FIELDS f ON rf.RDB$FIELD_SOURCE = f.RDB$FIELD_NAME WHERE rf.RDB$RELATION_NAME = ? ORDER BY rf.RDB$FIELD_POSITION `, [String(tableName).toUpperCase()]); return rows.map((row) => ({ name: (row.COLUMN_NAME || '').trim(), position: row.ORDINAL_POSITION, type: (row.DATA_TYPE || '').trim(), length: row.FIELD_LENGTH, precision: row.FIELD_PRECISION, scale: row.FIELD_SCALE, nullable: row.NULL_FLAG !== 1, })); } async function tableExists(config, tableName) { const r = await query(config, "SELECT COUNT(*) AS CT FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = ?", [String(tableName).toUpperCase()]); return (r[0] && r[0].CT > 0) || false; } async function columnExists(config, tableName, columnName) { const r = await query(config, "SELECT COUNT(*) AS CT FROM RDB$RELATION_FIELDS WHERE RDB$RELATION_NAME = ? AND RDB$FIELD_NAME = ?", [String(tableName).toUpperCase(), String(columnName).toUpperCase()]); return (r[0] && r[0].CT > 0) || false; } /** * Encerra todos os pools de conexão. */ async function close() { const promises = []; for (const pool of pools.values()) { promises.push(pool.close().catch(() => {})); } await Promise.all(promises); pools.clear(); } /** * Retorna estatísticas dos pools para debug. */ function poolStats() { const result = {}; for (const [key, pool] of pools) { result[key] = { available: pool.available.length, busy: pool.busy, waiting: pool.waiting.length, maxSize: pool.maxSize, }; } return result; } module.exports = { query, execute, testConnection, listTables, tableInfo, tableExists, columnExists, close, poolStats, };