diff --git a/CHANGELOG.md b/CHANGELOG.md index d1c2f15d7..915b1cb47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,15 @@ Builds: - linux - application for linux - win - application for Windows +## 6.6.1 +- ADDED: Support for Mongo shell (Premium) - #1114 +- FIXED: Support for BLOB in Oracle #1181 +- ADDED: Connect to named SQL Server instance #340 +- ADDED: Support for SQL Server descriptions #1137 +- ADDED: Application log viewer +- FIXED: Selecting default database in connection dialog +- CHANGED: Improved logging system, added related database and connection to logs metadata + ## 6.6.0 - ADDED: Database chat - AI powered chatbot, which knows your database (Premium) - ADDED: Firestore support (Premium) diff --git a/integration-tests/__tests__/alter-table.spec.js b/integration-tests/__tests__/alter-table.spec.js index c6883543b..65e1b2d3f 100644 --- a/integration-tests/__tests__/alter-table.spec.js +++ b/integration-tests/__tests__/alter-table.spec.js @@ -118,6 +118,31 @@ describe('Alter table', () => { }) ); + test.each(engines.filter(i => i.supportTableComments).map(engine => [engine.label, engine]))( + 'Add comment to table - %s', + testWrapper(async (conn, driver, engine) => { + await testTableDiff(engine, conn, driver, tbl => { + tbl.objectComment = 'Added table comment'; + }); + }) + ); + + test.each(engines.filter(i => i.supportColumnComments).map(engine => [engine.label, engine]))( + 'Add comment to column - %s', + testWrapper(async (conn, driver, engine) => { + await testTableDiff(engine, conn, driver, tbl => { + tbl.columns.push({ + columnName: 'added', + columnComment: 'Added column comment', + dataType: 'int', + pairingId: crypto.randomUUID(), + notNull: false, + autoIncrement: false, + }); + }); + }) + ); + test.each( createEnginesColumnsSource(engines.filter(x => !x.skipDropColumn)).filter( ([_label, col, engine]) => !engine.skipPkDrop || !col.endsWith('_pk') diff --git a/integration-tests/__tests__/table-create.spec.js b/integration-tests/__tests__/table-create.spec.js index fd5ffe0df..7e0357d68 100644 --- a/integration-tests/__tests__/table-create.spec.js +++ b/integration-tests/__tests__/table-create.spec.js @@ -64,6 +64,40 @@ describe('Table create', () => { }) ); + test.each( + engines.filter(i => i.supportTableComments || i.supportColumnComments).map(engine => [engine.label, engine]) + )( + 'Simple table with comment - %s', + testWrapper(async (conn, driver, engine) => { + await testTableCreate(engine, conn, driver, { + ...(engine.supportTableComments && { + schemaName: 'dbo', + objectComment: 'table comment', + }), + ...(engine.defaultSchemaName && { + schemaName: engine.defaultSchemaName, + }), + columns: [ + { + columnName: 'col1', + dataType: 'int', + pureName: 'tested', + ...(engine.skipNullability ? {} : { notNull: true }), + ...(engine.supportColumnComments && { + columnComment: 'column comment', + }), + ...(engine.defaultSchemaName && { + schemaName: engine.defaultSchemaName, + }), + }, + ], + primaryKey: { + columns: [{ columnName: 'col1' }], + }, + }); + }) + ); + test.each(engines.filter(x => !x.skipIndexes).map(engine => [engine.label, engine]))( 'Table with index - %s', testWrapper(async (conn, driver, engine) => { diff --git a/integration-tests/engines.js b/integration-tests/engines.js index 1fb4f28dd..6c16040a9 100644 --- a/integration-tests/engines.js +++ b/integration-tests/engines.js @@ -443,6 +443,8 @@ const sqlServerEngine = { supportSchemas: true, supportRenameSqlObject: true, defaultSchemaName: 'dbo', + supportTableComments: true, + supportColumnComments: true, // skipSeparateSchemas: true, triggers: [ { diff --git a/package.json b/package.json index 48fcdb715..240a4d409 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "6.6.1-premium-beta.15", + "version": "6.6.1", "name": "dbgate-all", "workspaces": [ "packages/*", diff --git a/packages/api/src/controllers/storage.js b/packages/api/src/controllers/storage.js index f839bd01b..d69323eb9 100644 --- a/packages/api/src/controllers/storage.js +++ b/packages/api/src/controllers/storage.js @@ -13,10 +13,6 @@ module.exports = { return null; }, - async loadSuperadminPermissions() { - return []; - }, - getConnectionsForLoginPage_meta: true, async getConnectionsForLoginPage() { return null; diff --git a/packages/tools/src/alterPlan.ts b/packages/tools/src/alterPlan.ts index d049fa464..5462efab8 100644 --- a/packages/tools/src/alterPlan.ts +++ b/packages/tools/src/alterPlan.ts @@ -292,6 +292,16 @@ export class AlterPlan { } } + _hasOnlyCommentChange(op: AlterOperation): boolean { + if (op.operationType === 'changeColumn') { + return _.isEqual( + _.omit(op.oldObject, ['columnComment', 'ordinal']), + _.omit(op.newObject, ['columnComment', 'ordinal']) + ); + } + return false; + } + _getDependendColumnConstraints(column: ColumnInfo, dependencyDefinition) { const table = this.wholeOldDb.tables.find(x => x.pureName == column.pureName && x.schemaName == column.schemaName); if (!table) return []; @@ -337,31 +347,42 @@ export class AlterPlan { ]) { if (op.operationType == testedOperationType) { const constraints = this._getDependendColumnConstraints(testedObject as ColumnInfo, testedDependencies); + const ignoreContraints = this.dialect.safeCommentChanges && this._hasOnlyCommentChange(op); // if (constraints.length > 0 && this.opts.noDropConstraint) { // return []; // } - const res: AlterOperation[] = [ - ...constraints.map(oldObject => { - const opRes: AlterOperation = { - operationType: 'dropConstraint', - oldObject, - isRecreate: true, - }; - return opRes; - }), - op, - ..._.reverse([...constraints]).map(newObject => { - const opRes: AlterOperation = { - operationType: 'createConstraint', - newObject, - }; - return opRes; - }), - ]; + const res: AlterOperation[] = []; - if (constraints.length > 0) { + if (!ignoreContraints) { + res.push( + ...constraints.map(oldObject => { + const opRes: AlterOperation = { + operationType: 'dropConstraint', + oldObject, + isRecreate: true, + }; + return opRes; + }) + ); + } + + res.push(op); + + if (!ignoreContraints) { + res.push( + ..._.reverse([...constraints]).map(newObject => { + const opRes: AlterOperation = { + operationType: 'createConstraint', + newObject, + }; + return opRes; + }) + ); + } + + if (!ignoreContraints && constraints.length > 0) { this.recreates.constraints += 1; } return res; diff --git a/packages/tools/src/diffTools.ts b/packages/tools/src/diffTools.ts index 1bb7ee7d5..6093ad666 100644 --- a/packages/tools/src/diffTools.ts +++ b/packages/tools/src/diffTools.ts @@ -59,6 +59,7 @@ export interface DbDiffOptions { ignoreForeignKeyActions?: boolean; ignoreDataTypes?: boolean; + ignoreComments?: boolean; } export function generateTablePairingId(table: TableInfo): TableInfo { @@ -322,11 +323,14 @@ export function testEqualColumns( ); return false; } - if ((a.columnComment || '') != (b.columnComment || '')) { - console.debug( - `Column ${a.pureName}.${a.columnName}, ${b.pureName}.${b.columnName}: different comment: ${a.columnComment}, ${b.columnComment}` - ); - return false; + + if (!opts.ignoreComments) { + if ((a.columnComment || '') != (b.columnComment || '')) { + console.debug( + `Column ${a.pureName}.${a.columnName}, ${b.pureName}.${b.columnName}: different comment: ${a.columnComment}, ${b.columnComment}` + ); + return false; + } } if (!testEqualTypes(a, b, opts)) { diff --git a/packages/types/dialect.d.ts b/packages/types/dialect.d.ts index 017b6f98b..b06a30071 100644 --- a/packages/types/dialect.d.ts +++ b/packages/types/dialect.d.ts @@ -74,6 +74,14 @@ export interface SqlDialect { predefinedDataTypes: string[]; + columnProperties?: { + columnName?: boolean; + isSparse?: true; + isPersisted?: true; + }; + + safeCommentChanges?: boolean; + // create sql-tree expression createColumnViewExpression( columnName: string, diff --git a/packages/types/test-engines.d.ts b/packages/types/test-engines.d.ts index 4388e5c85..9f4e2e3e9 100644 --- a/packages/types/test-engines.d.ts +++ b/packages/types/test-engines.d.ts @@ -56,6 +56,9 @@ export type TestEngineInfo = { useTextTypeForStrings?: boolean; + supportTableComments?: boolean; + supportColumnComments?: boolean; + supportRenameSqlObject?: boolean; supportSchemas?: boolean; diff --git a/plugins/dbgate-plugin-mssql/src/backend/MsSqlAnalyser.js b/plugins/dbgate-plugin-mssql/src/backend/MsSqlAnalyser.js index c6b923de5..9025808a5 100644 --- a/plugins/dbgate-plugin-mssql/src/backend/MsSqlAnalyser.js +++ b/plugins/dbgate-plugin-mssql/src/backend/MsSqlAnalyser.js @@ -1,4 +1,5 @@ const _ = require('lodash'); +const crypto = require('crypto'); const sql = require('./sql'); const { DatabaseAnalyser, isTypeString, isTypeNumeric } = global.DBGATE_PACKAGES['dbgate-tools']; @@ -54,6 +55,8 @@ function getColumnInfo({ defaultValue, defaultConstraint, computedExpression, + columnComment, + objectId, }) { const fullDataType = getFullDataTypeName({ dataType, @@ -71,6 +74,7 @@ function getColumnInfo({ } return { + objectId, columnName, dataType: fullDataType, notNull: !isNullable, @@ -79,9 +83,36 @@ function getColumnInfo({ defaultConstraint, computedExpression: simplifyComutedExpression(computedExpression), hasAutoValue: !!(dataType == 'timestamp' || dataType == 'rowversion' || computedExpression), + columnComment, }; } +/** + * @param {ReturnType} fieldType + * @param {any} item + * @param {Array<{ objectId: string; columnId: number, columnComment: string }>} columns + * @returns {string|null} + */ +function createObjectContentHash(fieldType, item, columns) { + if (!fieldType) return null; + const { modifyDate } = item; + + if ((columns?.length && fieldType === 'tables') || fieldType === 'views') { + const modifyDateStr = modifyDate ? modifyDate.toISOString() : ''; + const objectColumns = columns.filter(col => col.objectId == item.objectId); + const colsComments = objectColumns + .filter(i => i.columnComment) + .map(i => `${i.columnId}/${i.columnComment}`) + .join('||'); + const objectComment = item.objectComment || ''; + + return crypto.createHash('sha256').update(`${modifyDateStr}:${colsComments}:${objectComment}`).digest('hex'); + } + + if (!modifyDate) return null; + return modifyDate.toISOString(); +} + class MsSqlAnalyser extends DatabaseAnalyser { constructor(dbhan, driver, version) { super(dbhan, driver, version); @@ -104,6 +135,9 @@ class MsSqlAnalyser extends DatabaseAnalyser { const tablesRows = await this.analyserQuery('tables', ['tables']); this.feedback({ analysingMessage: 'DBGM-00206 Loading columns' }); const columnsRows = await this.analyserQuery('columns', ['tables']); + const columns = columnsRows.rows.map(getColumnInfo); + const baseColumnsRows = await this.analyserQuery('baseColumns', ['tables']); + const baseColumns = baseColumnsRows.rows.map(getColumnInfo); this.feedback({ analysingMessage: 'DBGM-00207 Loading primary keys' }); const pkColumnsRows = await this.analyserQuery('primaryKeys', ['tables']); this.feedback({ analysingMessage: 'DBGM-00208 Loading foreign keys' }); @@ -142,8 +176,8 @@ class MsSqlAnalyser extends DatabaseAnalyser { this.feedback({ analysingMessage: 'DBGM-00217 Finalizing DB structure' }); const tables = tablesRows.rows.map(row => ({ ...row, - contentHash: row.modifyDate && row.modifyDate.toISOString(), - columns: columnsRows.rows.filter(col => col.objectId == row.objectId).map(getColumnInfo), + contentHash: createObjectContentHash('tables', row, baseColumns), + columns: columns.filter(col => col.objectId == row.objectId), primaryKey: DatabaseAnalyser.extractPrimaryKeys(row, pkColumnsRows.rows), foreignKeys: DatabaseAnalyser.extractForeignKeys(row, fkColumnsRows.rows), indexes: indexesRows.rows @@ -171,7 +205,7 @@ class MsSqlAnalyser extends DatabaseAnalyser { const views = viewsRows.rows.map(row => ({ ...row, - contentHash: row.modifyDate && row.modifyDate.toISOString(), + contentHash: createObjectContentHash('views', row, baseColumns), createSql: getCreateSql(row), columns: viewColumnRows.rows.filter(col => col.objectId == row.objectId).map(getColumnInfo), })); @@ -192,7 +226,7 @@ class MsSqlAnalyser extends DatabaseAnalyser { .filter(x => x.sqlObjectType.trim() == 'P') .map(row => ({ ...row, - contentHash: row.modifyDate && row.modifyDate.toISOString(), + contentHash: createObjectContentHash('procedures', row), createSql: getCreateSql(row), parameters: prodceureToParameters[row.objectId], })); @@ -213,14 +247,14 @@ class MsSqlAnalyser extends DatabaseAnalyser { .filter(x => ['FN', 'IF', 'TF'].includes(x.sqlObjectType.trim())) .map(row => ({ ...row, - contentHash: row.modifyDate && row.modifyDate.toISOString(), + contentHash: createObjectContentHash('functions', row), createSql: getCreateSql(row), parameters: functionToParameters[row.objectId], })); const triggers = triggerRows.rows.map(row => ({ objectId: `triggers:${row.objectId}`, - contentHash: row.modifyDate && row.modifyDate.toISOString(), + contentHash: createObjectContentHash('triggers', row), createSql: row.definition, triggerTiming: row.triggerTiming, eventType: row.eventType, @@ -241,17 +275,19 @@ class MsSqlAnalyser extends DatabaseAnalyser { async _getFastSnapshot() { const modificationsQueryData = await this.analyserQuery('modifications'); + const baseColumnsRows = await this.analyserQuery('baseColumns', ['tables']); + const baseColumns = baseColumnsRows.rows; const tableSizes = await this.analyserQuery('tableSizes'); const res = DatabaseAnalyser.createEmptyStructure(); for (const item of modificationsQueryData.rows) { - const { type, objectId, modifyDate, schemaName, pureName } = item; + const { type, objectId, schemaName, pureName } = item; const field = objectTypeToField(type); if (!field || !res[field]) continue; res[field].push({ objectId, - contentHash: modifyDate && modifyDate.toISOString(), + contentHash: createObjectContentHash(field, item, baseColumns), schemaName, pureName, }); diff --git a/plugins/dbgate-plugin-mssql/src/backend/sql/baseColumns.js b/plugins/dbgate-plugin-mssql/src/backend/sql/baseColumns.js new file mode 100644 index 000000000..389f2f747 --- /dev/null +++ b/plugins/dbgate-plugin-mssql/src/backend/sql/baseColumns.js @@ -0,0 +1,11 @@ +module.exports = ` +select c.object_id as objectId, + ep.value as columnComment, + c.column_id as columnId +from sys.columns c +inner join sys.objects o on c.object_id = o.object_id +INNER JOIN sys.schemas u ON u.schema_id=o.schema_id +INNER JOIN sys.extended_properties ep on ep.major_id = c.object_id and ep.minor_id = c.column_id and ep.name = 'MS_Description' +where o.type IN ('U', 'V') and o.object_id =OBJECT_ID_CONDITION and u.name =SCHEMA_NAME_CONDITION +order by c.column_id +`; diff --git a/plugins/dbgate-plugin-mssql/src/backend/sql/columns.js b/plugins/dbgate-plugin-mssql/src/backend/sql/columns.js index 7a6ee8cf2..ad4b67781 100644 --- a/plugins/dbgate-plugin-mssql/src/backend/sql/columns.js +++ b/plugins/dbgate-plugin-mssql/src/backend/sql/columns.js @@ -7,7 +7,8 @@ select c.name as columnName, t.name as dataType, c.object_id as objectId, c.is_i col.NUMERIC_PRECISION as numericPrecision, col.NUMERIC_SCALE as numericScale, -- TODO only if version >= 2008 - c.is_sparse as isSparse + c.is_sparse as isSparse, + ep.value as columnComment from sys.columns c inner join sys.types t on c.system_type_id = t.system_type_id and c.user_type_id = t.user_type_id inner join sys.objects o on c.object_id = o.object_id @@ -15,6 +16,7 @@ INNER JOIN sys.schemas u ON u.schema_id=o.schema_id INNER JOIN INFORMATION_SCHEMA.COLUMNS col ON col.TABLE_NAME = o.name AND col.TABLE_SCHEMA = u.name and col.COLUMN_NAME = c.name left join sys.default_constraints d on c.default_object_id = d.object_id left join sys.computed_columns m on m.object_id = c.object_id and m.column_id = c.column_id +left join sys.extended_properties ep on ep.major_id = c.object_id and ep.minor_id = c.column_id and ep.name = 'MS_Description' where o.type = 'U' and o.object_id =OBJECT_ID_CONDITION and u.name =SCHEMA_NAME_CONDITION order by c.column_id `; diff --git a/plugins/dbgate-plugin-mssql/src/backend/sql/index.js b/plugins/dbgate-plugin-mssql/src/backend/sql/index.js index f9426419f..a281b48e0 100644 --- a/plugins/dbgate-plugin-mssql/src/backend/sql/index.js +++ b/plugins/dbgate-plugin-mssql/src/backend/sql/index.js @@ -16,6 +16,7 @@ const triggers = require('./triggers'); const listVariables = require('./listVariables'); const listDatabases = require('./listDatabases'); const listProcesses = require('./listProcesses'); +const baseColumns = require('./baseColumns'); module.exports = { columns, @@ -36,4 +37,5 @@ module.exports = { listVariables, listDatabases, listProcesses, + baseColumns, }; diff --git a/plugins/dbgate-plugin-mssql/src/backend/sql/tables.js b/plugins/dbgate-plugin-mssql/src/backend/sql/tables.js index 32dace94d..17887761a 100644 --- a/plugins/dbgate-plugin-mssql/src/backend/sql/tables.js +++ b/plugins/dbgate-plugin-mssql/src/backend/sql/tables.js @@ -1,8 +1,14 @@ module.exports = ` select - o.name as pureName, s.name as schemaName, o.object_id as objectId, - o.create_date as createDate, o.modify_date as modifyDate + o.name as pureName, + s.name as schemaName, + o.object_id as objectId, + o.create_date as createDate, + o.modify_date as modifyDate, + ep.value as objectComment from sys.tables o inner join sys.schemas s on o.schema_id = s.schema_id -where o.object_id =OBJECT_ID_CONDITION and s.name =SCHEMA_NAME_CONDITION -`; +left join sys.extended_properties ep on ep.major_id = o.object_id + and ep.minor_id = 0 + and ep.name = 'MS_Description' +where o.object_id =OBJECT_ID_CONDITION and s.name =SCHEMA_NAME_CONDITION`; diff --git a/plugins/dbgate-plugin-mssql/src/backend/tediousDriver.js b/plugins/dbgate-plugin-mssql/src/backend/tediousDriver.js index 5f531c7ab..f4a8a4cfa 100644 --- a/plugins/dbgate-plugin-mssql/src/backend/tediousDriver.js +++ b/plugins/dbgate-plugin-mssql/src/backend/tediousDriver.js @@ -66,14 +66,16 @@ async function tediousConnect(storedConnection) { const authentication = await getAuthentication(storedConnection); return new Promise((resolve, reject) => { + const [host, instance] = (server || '').split('\\'); const connectionOptions = { + instanceName: instance, encrypt: !!ssl || authType == 'msentra' || authType == 'azureManagedIdentity', cryptoCredentialsDetails: ssl ? _.pick(ssl, ['ca', 'cert', 'key']) : undefined, trustServerCertificate: ssl ? (!ssl.ca && !ssl.cert && !ssl.key ? true : ssl.rejectUnauthorized) : undefined, enableArithAbort: true, validateBulkLoadParameters: false, requestTimeout: 1000 * 3600, - port: port ? parseInt(port) : undefined, + port: port && !instance ? parseInt(port) : undefined, trustServerCertificate: !!trustServerCertificate, appName: 'DbGate', }; @@ -83,7 +85,7 @@ async function tediousConnect(storedConnection) { } const connection = new tedious.Connection({ - server, + server: host, authentication, options: connectionOptions, }); diff --git a/plugins/dbgate-plugin-mssql/src/frontend/MsSqlDumper.js b/plugins/dbgate-plugin-mssql/src/frontend/MsSqlDumper.js index 2cb9ee526..fd731ab64 100644 --- a/plugins/dbgate-plugin-mssql/src/frontend/MsSqlDumper.js +++ b/plugins/dbgate-plugin-mssql/src/frontend/MsSqlDumper.js @@ -124,8 +124,110 @@ class MsSqlDumper extends SqlDumper { this.putCmd("^execute sp_rename '%f.%i', '%s', 'COLUMN'", column, column.columnName, newcol); } + /** + * @param {import('dbgate-types').TableInfo} table + */ + dropTableCommentIfExists(table) { + const { schemaName, pureName } = table; + + const fullName = `${schemaName && schemaName + '.'}${pureName}`; + + this.put('&>^if ^exists (&n'); + this.put('&>^select 1 ^from sys.extended_properties&n'); + this.put("^where major_id = OBJECT_ID('%s')&n", fullName); + this.put('^and minor_id = 0&n'); + this.put("^and name = N'MS_Description'&<&<&n"); + this.put(')&n'); + this.put('&>^begin&n'); + this.put('&>^exec sp_dropextendedproperty&n'); + this.put("@name = N'MS_Description',&n"); + this.put("@level0type = N'SCHEMA', @level0name = '%s',&n", schemaName); + this.put("@level1type = N'TABLE', @level1name = '%s'&<&n", pureName); + this.put('^end'); + this.endCommand(); + } + + /** + * @param {import('dbgate-types').TableInfo} table + */ + createTableComment(table) { + const { schemaName, pureName, objectComment } = table; + if (!objectComment) return; + + this.put('&>^exec sp_addextendedproperty&n'); + this.put("@name = N'MS_Description', @value = N'%s',&n", objectComment); + this.put("@level0type = N'SCHEMA', @level0name = '%s',&n", schemaName || 'dbo'); + this.put("@level1type = N'TABLE', @level1name = '%s&<'", pureName); + this.endCommand(); + } + + /** + * @param {import('dbgate-types').ColumnInfo} oldcol + * @param {import('dbgate-types').ColumnInfo} newcol + */ + changeColumnComment(oldcol, newcol) { + if (oldcol.columnComment === newcol.columnComment) return; + + if (oldcol.columnComment) this.dropColumnCommentIfExists(newcol); + if (newcol.columnComment) this.createColumnComment(newcol); + } + + /** + * @param {import('dbgate-types').ColumnInfo} column + */ + dropColumnCommentIfExists(column) { + const { schemaName, columnName, pureName } = column; + const fullName = `${schemaName && schemaName + '.'}${pureName}`; + + this.put('&>^if ^exists (&n'); + this.put('&>^select 1 ^from sys.extended_properties&n'); + this.put("^where major_id = OBJECT_ID('%s')&n", fullName); + this.put( + "^and minor_id = (^select column_id ^from sys.columns ^where object_id = OBJECT_ID('%s') ^and name = '%s')&n", + fullName, + columnName + ); + this.put("^and name = N'MS_Description'&<&<&n"); + this.put(')&n'); + this.put('&>^begin&n'); + this.put('&>^exec sp_dropextendedproperty&n'); + this.put("@name = N'MS_Description',&n"); + this.put("@level0type = N'SCHEMA', @level0name = '%s',&n", schemaName); + this.put("@level1type = N'TABLE', @level1name = '%s',&n", pureName); + this.put("@level2type = N'COLUMN', @level2name = '%s'&<&n", columnName); + this.put('^end'); + this.endCommand(); + } + + /** + * @param {import('dbgate-types').ColumnInfo} column + */ + createColumnComment(column) { + const { schemaName, columnName, pureName, columnComment } = column; + if (!columnComment) return; + + this.put('&>^exec sp_addextendedproperty&n'); + this.put("@name = N'MS_Description', "); + this.put(`@value = N'%s',&n`, columnComment); + this.put("@level0type = N'SCHEMA', @level0name = '%s',&n", schemaName); + this.put("@level1type = N'TABLE', @level1name = '%s',&n", pureName); + this.put("@level2type = N'COLUMN', @level2name = '%s&<'", columnName); + this.endCommand(); + } + + /** + * @param {import('dbgate-types').TableInfo} table + */ + createTable(table) { + super.createTable(table); + + for (const column of table.columns || []) { + this.createColumnComment(column); + } + } + changeColumn(oldcol, newcol, constraints) { - if (testEqualColumns(oldcol, newcol, false, false)) { + if (testEqualColumns(oldcol, newcol, false, false, { ignoreComments: true })) { this.dropDefault(oldcol); if (oldcol.columnName != newcol.columnName) this.renameColumn(oldcol, newcol.columnName); this.createDefault(newcol); @@ -140,6 +242,8 @@ class MsSqlDumper extends SqlDumper { this.endCommand(); this.createDefault(newcol); } + + this.changeColumnComment(oldcol, newcol); } specialColumnOptions(column) { @@ -163,6 +267,44 @@ class MsSqlDumper extends SqlDumper { this.put('^select ^scope_identity()'); } + /** + * @param {import('dbgate-types').TableInfo} table + */ + tableOptions(table) { + this.endCommand(); + + const options = this.driver?.dialect?.getTableFormOptions?.('sqlCreateTable') || []; + for (const option of options) { + const { name, sqlFormatString } = option; + const value = table[name]; + + if (name == 'objectComment') { + this.createTableComment(table); + return; + } + + if (value) { + this.put('&n'); + this.put(sqlFormatString, value); + } + } + } + + /** + * @param {import('dbgate-types').TableInfo} table + * @param {string} optionName + * @param {string} optionValue + */ + setTableOption(table, optionName, optionValue) { + if (optionName == 'objectComment') { + this.dropTableCommentIfExists(table); + if (optionValue) this.createTableComment(table); + return; + } + + super.setTableOption(table, optionName, optionValue); + } + callableTemplate(func) { const putParameters = (parameters, delimiter) => { this.putCollection(delimiter, parameters || [], param => { @@ -207,8 +349,8 @@ MsSqlDumper.prototype.changeProcedureSchema = MsSqlDumper.prototype.changeObject MsSqlDumper.prototype.renameFunction = MsSqlDumper.prototype.renameObject; MsSqlDumper.prototype.changeFunctionSchema = MsSqlDumper.prototype.changeObjectSchema; - MsSqlDumper.prototype.renameTrigger = MsSqlDumper.prototype.renameObject; + MsSqlDumper.prototype.changeTriggerSchema = MsSqlDumper.prototype.changeObjectSchema; MsSqlDumper.prototype.renameTable = MsSqlDumper.prototype.renameObject; diff --git a/plugins/dbgate-plugin-mssql/src/frontend/driver.js b/plugins/dbgate-plugin-mssql/src/frontend/driver.js index fd979122e..13520b105 100644 --- a/plugins/dbgate-plugin-mssql/src/frontend/driver.js +++ b/plugins/dbgate-plugin-mssql/src/frontend/driver.js @@ -45,10 +45,13 @@ const dialect = { namedDefaultConstraint: true, columnProperties: { + columnComment: true, isSparse: true, isPersisted: true, }, + safeCommentChanges: true, + predefinedDataTypes: [ 'bigint', 'bit', @@ -111,6 +114,18 @@ const dialect = { }; } }, + + getTableFormOptions(intent) { + return [ + { + type: 'text', + label: 'Comment', + name: 'objectComment', + sqlFormatString: '^comment = %v', + allowEmptyValue: true, + }, + ]; + }, }; /** @type {import('dbgate-types').EngineDriver} */ diff --git a/plugins/dbgate-plugin-oracle/src/backend/driver.js b/plugins/dbgate-plugin-oracle/src/backend/driver.js index fda8e074a..0fb226a0d 100644 --- a/plugins/dbgate-plugin-oracle/src/backend/driver.js +++ b/plugins/dbgate-plugin-oracle/src/backend/driver.js @@ -13,6 +13,7 @@ function getOracledb() { if (!oracledbValue) { oracledbValue = require('oracledb'); oracledbValue.fetchAsString = [oracledbValue.CLOB, oracledbValue.NCLOB]; + oracledbValue.fetchAsBuffer = [oracledbValue.BLOB]; } return oracledbValue; }