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,45 @@
# Custom Firebird Configuration
# Wire encryption settings
# Options: Enabled, Required, Disabled
WireCrypt = Disabled
# Authentication settings
# Add Legacy_Auth to support older clients
AuthServer = Legacy_Auth
# User manager plugin
UserManager = Legacy_UserManager
# Default character set
DefaultCharSet = UTF8
# Buffer settings for better performance
DefaultDbCachePages = 2048
TempCacheLimit = 512M
# Connection settings
ConnectionTimeout = 180
DatabaseGrowthIncrement = 128M
# TCP Protocol settings
TcpRemoteBufferSize = 8192
TcpNoNagle = 1
# Security settings
RemoteServiceName = gds_db
RemoteServicePort = 3050
RemoteAuxPort = 0
RemotePipeName = firebird
# Lock settings
LockMemSize = 1M
LockHashSlots = 8191
LockAcquireSpins = 0
# Log settings
FileSystemCacheThreshold = 65536
FileSystemCacheSize = 0
# Compatibility settings for older clients
CompatiblityDialect = 3

View File

@@ -0,0 +1,46 @@
{
"name": "dbgate-plugin-firebird",
"main": "dist/backend.js",
"version": "6.0.0-alpha.1",
"license": "GPL-3.0",
"description": "firebirdQL connector plugin for DbGate",
"homepage": "https://dbgate.org",
"repository": {
"type": "git",
"url": "https://github.com/dbgate/dbgate"
},
"author": "Jan Prochazka",
"keywords": [
"dbgate",
"firebird",
"dbgatebuiltin"
],
"files": [
"dist",
"icon.svg"
],
"scripts": {
"build:frontend": "webpack --config webpack-frontend.config",
"build:frontend:watch": "webpack --watch --config webpack-frontend.config",
"build:backend": "webpack --config webpack-backend.config.js",
"build": "yarn build:frontend && yarn build:backend",
"plugin": "yarn build && yarn pack && dbgate-plugin dbgate-plugin-firebird",
"copydist": "yarn build && yarn pack && dbgate-copydist ../dist/dbgate-plugin-firebird",
"plugout": "dbgate-plugout dbgate-plugin-firebird",
"prepublishOnly": "yarn build"
},
"devDependencies": {
"dbgate-plugin-tools": "^1.0.7",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"wkx": "^0.5.0",
"pg-copy-streams": "^6.0.6",
"node-firebird": "^1.1.9",
"dbgate-query-splitter": "^4.11.3",
"dbgate-tools": "^6.0.0-alpha.1",
"lodash": "^4.17.21",
"pg": "^8.11.5"
}
}

View File

@@ -0,0 +1,9 @@
module.exports = {
trailingComma: 'es5',
tabWidth: 2,
semi: true,
singleQuote: true,
arrowParen: 'avoid',
arrowParens: 'avoid',
printWidth: 120,
};

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],
};

View File

@@ -0,0 +1,46 @@
var webpack = require('webpack');
var path = require('path');
const packageJson = require('./package.json');
const buildPluginExternals = require('../../common/buildPluginExternals');
const externals = buildPluginExternals(packageJson);
var config = {
context: __dirname + '/src/backend',
entry: {
app: './index.js',
},
target: 'node',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'backend.js',
libraryTarget: 'commonjs2',
},
// uncomment for disable minimalization
// optimization: {
// minimize: false,
// },
plugins: [
new webpack.IgnorePlugin({
checkResource(resource) {
const lazyImports = ['pg-native', 'uws'];
if (!lazyImports.includes(resource)) {
return false;
}
try {
require.resolve(resource);
} catch (err) {
return true;
}
return false;
},
}),
],
externals,
};
module.exports = config;

View File

@@ -0,0 +1,30 @@
var webpack = require('webpack');
var path = require('path');
var config = {
context: __dirname + '/src/frontend',
entry: {
app: './index.js',
},
target: 'web',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'frontend.js',
libraryTarget: 'var',
library: 'plugin',
},
plugins: [
new webpack.DefinePlugin({
'global.DBGATE_PACKAGES': 'window.DBGATE_PACKAGES',
}),
],
// uncomment for disable minimalization
// optimization: {
// minimize: false,
// },
};
module.exports = config;

View File

@@ -3310,6 +3310,11 @@ better-sqlite3@11.8.1:
bindings "^1.5.0"
prebuild-install "^7.1.1"
big-integer@^1.6.51:
version "1.6.52"
resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85"
integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==
big.js@^5.2.2:
version "5.2.2"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
@@ -7873,6 +7878,11 @@ long@*, long@^5.2.1, long@~5.2.3:
resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1"
integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==
long@^5.2.3:
version "5.3.2"
resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83"
integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==
lru-cache@^10.2.0:
version "10.4.3"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
@@ -8437,6 +8447,14 @@ node-cron@^2.0.3:
opencollective-postinstall "^2.0.0"
tz-offset "0.0.1"
node-firebird@^1.1.9:
version "1.1.9"
resolved "https://registry.yarnpkg.com/node-firebird/-/node-firebird-1.1.9.tgz#0e6815b4e209812a4c85b71227e40e268bedeb8b"
integrity sha512-6Ol+Koide1WbfUp4BJ1dSA4wm091jAgCwwSoihxO/RRdcfR+dMVDE9jd2Z2ixjk7q/vSNJUYORXv7jmRfvwdrw==
dependencies:
big-integer "^1.6.51"
long "^5.2.3"
node-gyp@^7.1.0:
version "7.1.2"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-7.1.2.tgz#21a810aebb187120251c3bcec979af1587b188ae"