alterações de melhorias
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
Reference in New Issue
Block a user