feat: basic firebird analyser

This commit is contained in:
Nybkox
2025-05-06 15:52:15 +02:00
parent bac8bd0006
commit 839ec9a456
17 changed files with 610 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
const _ = require('lodash');
const sql = require('./sql');
const { getDataTypeString } = require('./helpers');
const { DatabaseAnalyser } = require('dbgate-tools');
class Analyser extends DatabaseAnalyser {
constructor(dbhan, driver, version) {
super(dbhan, driver, version);
}
async _runAnalysis() {
const tablesResult = await this.driver.query(this.dbhan, sql.tables);
const columnsResult = await this.driver.query(this.dbhan, sql.columns);
const columns = columnsResult.rows.map(i => ({
tableName: i.TABLENAME,
columnName: i.COLUMNNAME,
notNull: i.NOTNULL,
isPrimaryKey: i.ISPRIMARYKEY,
dataType: getDataTypeString(i),
precision: i.NUMBERPRECISION,
scale: i.SCALE,
length: i.LENGTH,
defaultValue: i.DEFAULTVALUE,
columnComment: i.COLUMNCOMMENT,
isUnsigned: i.ISUNSIGNED,
pureName: i.PURENAME,
schemaName: i.SCHEMANAME,
}));
const tables = tablesResult.rows.map(i => ({
pureName: i.PURENAME,
objectId: i.OBJECTID,
schemaName: i.SCHEMANAME,
objectComment: i.OBJECTCOMMENT,
}));
return {
tables: tables.map(table => ({
...table,
columns: columns.filter(column => column.tableName === table.pureName),
})),
};
}
async _getFastSnapshot() {
return this._runAnalysis();
}
}
module.exports = Analyser;

View File

@@ -0,0 +1,146 @@
const _ = require('lodash');
const driverBase = require('../frontend/driver');
const Analyser = require('./Analyser');
const Firebird = require('node-firebird');
const { getLogger, extractErrorLogData, createBulkInsertStreamBase } = require('dbgate-tools');
const sql = require('./sql');
const logger = getLogger('firebird');
/** @type {import('dbgate-types').EngineDriver<Firebird.Database>} */
const driver = {
...driverBase,
analyserClass: Analyser,
async connect({ port, user, password, server, databaseFile }) {
const options = {
host: server,
port,
database: databaseFile,
user,
password,
};
/**@type {Firebird.Database} */
const db = await new Promise((resolve, reject) => {
Firebird.attach(options, (err, db) => {
if (err) {
reject(err);
return;
}
resolve(db);
});
});
return {
client: db,
};
},
async query(dbhan, sql) {
const res = await new Promise((resolve, reject) => {
dbhan.client.query(sql, (err, result) => {
if (err) {
reject(err);
return;
}
resolve(result);
});
});
const columns = res[0] ? Object.keys(res[0]).map(i => ({ columnName: i })) : [];
return {
rows: res,
columns,
};
},
async script(dbhan, sql) {
throw new Error('Not implemented');
},
async stream(dbhan, sql, options) {
try {
await new Promise((resolve, reject) => {
let hasSentColumns = false;
dbhan.client.sequentially(
sql,
[],
(row, index) => {
if (!hasSentColumns) {
hasSentColumns = true;
const columns = Object.keys(row).map(i => ({ columnName: i }));
options.recordset(columns);
}
options.row(row);
},
err => {
if (err) {
reject(err);
return;
}
resolve();
}
);
});
options.done();
} catch (err) {
logger.error(extractErrorLogData(err), 'Stream error');
options.info({
message: err.message,
line: err.line,
// procedure: procName,
time: new Date(),
severity: 'error',
});
options.done();
}
},
async readQuery(dbhan, sql, structure) {
throw new Error('Not implemented');
},
async writeTable(dbhan, name, options) {
return createBulkInsertStream(this, stream, dbhan, name, options);
},
async getVersion(dbhan) {
const res = await this.query(dbhan, sql.version);
const version = res.rows?.[0]?.VERSION;
return {
version,
versionText: `Firebird ${version}`,
};
},
async listDatabases(dbhan) {
return [
{
name: 'default',
},
];
},
async createDatabase(dbhan, name) {},
async dropDatabase(dbhan, name) {},
async close(dbhan) {
return new Promise((resolve, reject) => {
dbhan.client.detach(err => {
if (err) {
reject(err);
return;
}
resolve();
});
});
},
};
module.exports = driver;

View File

@@ -0,0 +1,54 @@
function getDataTypeString(column) {
if (!column) {
return null;
}
const { DATATYPECODE, SCALE, LENGTH, NUMBERPRECISION } = column;
switch (DATATYPECODE) {
case 7:
return 'SMALLINT';
case 8:
return 'INTEGER';
case 9:
return 'BIGINT';
case 10:
return 'FLOAT';
case 11:
return 'DOUBLE PRECISION';
case 12:
return 'DATE';
case 13:
return 'TIME';
case 14:
return `CHAR(${LENGTH})`;
case 16:
return `DECIMAL(${NUMBERPRECISION}, ${SCALE})`;
case 27:
return 'DOUBLE PRECISION';
case 35:
return 'BLOB';
case 37:
return `VARCHAR(${LENGTH})`;
case 261:
return 'CSTRING';
default:
return `UNKNOWN (${DATATYPECODE})`;
}
}
module.exports = {
getDataTypeString,
};

View File

@@ -0,0 +1,7 @@
const driver = require('./driver');
module.exports = {
packageName: 'dbgate-plugin-firebird',
drivers: [driver],
initialize(dbgateEnv) {},
};

View File

@@ -0,0 +1,43 @@
module.exports = `
SELECT DISTINCT
CAST(TRIM(rf.rdb$relation_name) AS VARCHAR(255)) AS tableName,
CAST(TRIM(rf.rdb$field_name) AS VARCHAR(255)) AS columnName,
CASE rf.rdb$null_flag WHEN 1 THEN FALSE ELSE TRUE END AS notNull,
CASE
WHEN EXISTS (
SELECT 1
FROM rdb$relation_constraints rc
JOIN rdb$index_segments idx ON rc.rdb$index_name = idx.rdb$index_name
WHERE rc.rdb$relation_name = rf.rdb$relation_name
AND idx.rdb$field_name = rf.rdb$field_name
AND rc.rdb$constraint_type = 'PRIMARY KEY'
) THEN TRUE
ELSE FALSE
END AS isPrimaryKey,
f.rdb$field_type AS dataTypeCode,
f.rdb$field_precision AS numberprecision,
f.rdb$field_scale AS scale,
f.rdb$field_length AS length,
CAST(TRIM(rf.rdb$default_value) AS VARCHAR(255)) AS defaultValue,
CAST(TRIM(rf.rdb$description) AS VARCHAR(255)) AS columnComment,
CASE
WHEN f.rdb$field_type IN (8, 9, 16) AND f.rdb$field_scale < 0 THEN TRUE
ELSE FALSE
END AS isUnsigned,
CAST(TRIM(rf.rdb$field_name) AS VARCHAR(255)) AS pureName,
CAST(TRIM(r.rdb$owner_name) AS VARCHAR(255)) AS schemaName
FROM
rdb$relation_fields rf
JOIN
rdb$relations r ON rf.rdb$relation_name = r.rdb$relation_name
LEFT JOIN
rdb$fields f ON rf.rdb$field_source = f.rdb$field_name
LEFT JOIN
rdb$character_sets cs ON f.rdb$character_set_id = cs.rdb$character_set_id
LEFT JOIN
rdb$collations co ON f.rdb$collation_id = co.rdb$collation_id
WHERE
r.rdb$system_flag = 0
ORDER BY
tableName, rf.rdb$field_position;
`;

View File

@@ -0,0 +1,9 @@
const version = require('./version');
const tables = require('./tables');
const columns = require('./columns');
module.exports = {
version,
columns,
tables,
};

View File

@@ -0,0 +1,9 @@
module.exports = `
SELECT
TRIM(RDB$RELATION_NAME) AS pureName,
RDB$DESCRIPTION AS objectComment,
RDB$FORMAT AS objectTypeField
FROM RDB$RELATIONS
WHERE RDB$SYSTEM_FLAG = 0 -- only user-defined tables
ORDER BY pureName;
`;

View File

@@ -0,0 +1 @@
module.exports = `SELECT rdb$get_context('SYSTEM', 'ENGINE_VERSION') as version from rdb$database;`;

View File

@@ -0,0 +1,5 @@
const { SqlDumper } = global.DBGATE_PACKAGES['dbgate-tools'];
class Dumper extends SqlDumper {}
module.exports = Dumper;

View File

@@ -0,0 +1,85 @@
const { driverBase } = global.DBGATE_PACKAGES['dbgate-tools'];
const Dumper = require('./Dumper');
/** @type {import('dbgate-types').SqlDialect} */
const dialect = {
rangeSelect: true,
ilike: true,
defaultSchemaName: 'public',
multipleSchema: true,
stringEscapeChar: "'",
fallbackDataType: 'varchar',
anonymousPrimaryKey: false,
enableConstraintsPerTable: true,
stringAgg: true,
createColumn: true,
dropColumn: true,
changeColumn: true,
createIndex: true,
dropIndex: true,
createForeignKey: true,
dropForeignKey: true,
createPrimaryKey: true,
dropPrimaryKey: true,
createUnique: true,
dropUnique: true,
createCheck: true,
dropCheck: true,
allowMultipleValuesInsert: true,
renameSqlObject: true,
filteredIndexes: true,
};
const firebirdSplitterOptions = {
stringsBegins: ["'", '"'],
stringsEnds: {
"'": "'",
'"': '"',
},
stringEscapes: {
"'": "'", // Single quote is escaped by another single quote
'"': '"', // Double quote is escaped by another double quote
},
allowSemicolon: true,
allowCustomDelimiter: false,
allowCustomSqlTerminator: false,
allowGoDelimiter: false,
allowSlashDelimiter: false,
allowDollarDollarString: false,
noSplit: false,
doubleDashComments: true,
multilineComments: true,
javaScriptComments: false,
skipSeparatorBeginEnd: false,
ignoreComments: false,
preventSingleLineSplit: false,
adaptiveGoSplit: false,
returnRichInfo: false,
splitByLines: false,
splitByEmptyLine: false,
copyFromStdin: false,
queryParameterStyle: ':', // Firebird uses colon-prefixed parameters (:param_name)
};
/** @type {import('dbgate-types').EngineDriver} */
const firebirdDriverBase = {
...driverBase,
defaultPort: 3050,
showConnectionField: field => ['port', 'user', 'password', 'server', 'databaseFile'].includes(field),
getQuerySplitterOptions: () => firebirdSplitterOptions,
// beforeConnectionSave: connection => {
// const { databaseFile } = connection;
// return {
// singleDatabase: true,
// defaultDatabase: databaseFile,
// };
// },
engine: 'firebird@dbgate-plugin-firebird',
title: 'Firebird',
supportsTransactions: true,
dumperClass: Dumper,
dialect,
};
module.exports = firebirdDriverBase;

View File

@@ -0,0 +1,6 @@
import driver from './driver';
export default {
packageName: 'dbgate-plugin-firebird',
drivers: [driver],
};