324 lines
9.5 KiB
JavaScript
324 lines
9.5 KiB
JavaScript
/**
|
|
* 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,
|
|
};
|