Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions src/common/datasource/pg/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*
* Copyright 2023 OceanBase
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Unit tests for src/common/datasource/pg/index.tsx
*
* 覆盖目标(design.md §3.5 / §4.2 / §10.3,compat-RISK-3):
* 1. PG `features.task` 经收敛后等于 [TaskType.ASYNC]
* 2. PG 其它 features 字段保持原状(sessionManage / sessionParams / sqlExplain /
* plRun / groupResourceTree / sqlconsole = true;obclient / recycleBin = false)
* 3. 横向回归:MySQL / Oracle / OceanBase 各自 features.task 数组未受波及
*
* 测试形式:map case(数组定义所有用例 + forEach 循环生成 it,
* 等价于 it.each;选 forEach 是因为 odc-client 的 @types/jest@22 不识别 it.each 签名)。
* Refs: dms-ee#850, compat-RISK-3
*/

// haveOCP() 依赖 webpack DefinePlugin 注入的 HAVEOCP 常量,jest 运行时未注入,
// 因此显式 mock 让 haveOCP() 返回 false,避免触发 pg/index.tsx 末尾的 delete 分支。
jest.mock('@/util/env', () => ({
haveOCP: () => false,
isClient: () => false,
hasEventTrackingPermission: () => false
}));

// pg/index.tsx 静态 import MySQLColumnExtra(来自 oceanbase),而该组件的依赖链
// 引入了 src/util/intl.tsx -> @umijs/max -> esbuild 运行时检查,在 jsdom 测试环境
// 下会失败。本测试只断言 PG datasource 静态导出,不渲染 React 组件,因此 mock 之。
jest.mock('@/common/datasource/oceanbase/MySQLColumnExtra', () => ({
__esModule: true,
default: () => null
}));

// mysql/oracle/oceanbase 三套 lateral 基线引用 ColumnExtra 同链路,全部以最小 mock 代替。
jest.mock('@/common/datasource/oceanbase/OracleColumnExtra', () => ({
__esModule: true,
default: () => null
}));

import { ConnectType, TaskType } from '@/d.ts';
import pgItems from '@/common/datasource/pg';
import mysqlItems from '@/common/datasource/mysql';
import oracleItems from '@/common/datasource/oracle';
import obItems from '@/common/datasource/oceanbase/obmysql';

const pgConfig = pgItems[ConnectType.PG];

describe('datasource/pg/index features.task collapse (compat-RISK-3)', () => {
it('exports a config keyed by ConnectType.PG', () => {
expect(pgConfig).toBeDefined();
});

it('features.task equals [TaskType.ASYNC] (only ASYNC kept)', () => {
expect(pgConfig.features.task).toEqual([TaskType.ASYNC]);
expect(pgConfig.features.task).toHaveLength(1);
expect(pgConfig.features.task[0]).toBe(TaskType.ASYNC);
});

// map case · 旧任务类型必须全部移除
const removedTaskCases: Array<{ name: string; taskType: TaskType }> = [
{ name: 'SQL_PLAN removed', taskType: TaskType.SQL_PLAN },
{ name: 'DATA_ARCHIVE removed', taskType: TaskType.DATA_ARCHIVE },
{ name: 'DATA_DELETE removed', taskType: TaskType.DATA_DELETE },
{ name: 'IMPORT removed', taskType: TaskType.IMPORT },
{ name: 'EXPORT removed', taskType: TaskType.EXPORT },
{ name: 'EXPORT_RESULT_SET removed', taskType: TaskType.EXPORT_RESULT_SET },
{
name: 'STRUCTURE_COMPARISON removed',
taskType: TaskType.STRUCTURE_COMPARISON
},
{ name: 'MULTIPLE_ASYNC removed', taskType: TaskType.MULTIPLE_ASYNC }
];

removedTaskCases.forEach(({ name, taskType }) => {
it(`features.task should NOT contain ${name}`, () => {
expect(pgConfig.features.task).not.toContain(taskType);
});
});
});

describe('datasource/pg/index other features unchanged (compat-RISK-3)', () => {
// map case · 其它 features 字段保持现状(与 product.md §5 / design.md §3.5 一致)
const featuresCases: Array<{ name: string; key: string; expected: boolean }> =
[
{
name: 'sessionManage stays true',
key: 'sessionManage',
expected: true
},
{
name: 'sessionParams stays true',
key: 'sessionParams',
expected: true
},
{ name: 'sqlExplain stays true', key: 'sqlExplain', expected: true },
{ name: 'plRun stays true', key: 'plRun', expected: true },
{
name: 'groupResourceTree stays true',
key: 'groupResourceTree',
expected: true
},
{ name: 'sqlconsole stays true', key: 'sqlconsole', expected: true },
{ name: 'obclient stays false', key: 'obclient', expected: false },
{ name: 'recycleBin stays false', key: 'recycleBin', expected: false }
];

featuresCases.forEach(({ name, key, expected }) => {
it(`${name} (features.${key} === ${expected})`, () => {
expect(pgConfig.features[key]).toBe(expected);
});
});

it('features.export object preserves PG defaults (fileLimit/snapshot=false)', () => {
expect(pgConfig.features.export).toEqual({
fileLimit: false,
snapshot: false
});
});

it('schema.innerSchema preserves PG built-in schema list', () => {
expect(pgConfig.schema.innerSchema).toEqual([
'postgres',
'information_schema',
'pg_catalog'
]);
});

it('sql.language stays "pg"', () => {
expect(pgConfig.sql.language).toBe('pg');
});
});

describe('datasource/pg/index lateral regression baseline (compat-RISK-3)', () => {
// 横向回归 · MySQL / Oracle / OceanBase 的 features.task 必须与 support-pg 基线一致:
// - MySQL:12 项(含 LOGICAL_DATABASE_CHANGE / DATAMOCK)
// - Oracle:8 项(含 IMPORT / EXPORT / EXPORT_RESULT_SET / SQL_PLAN / ASYNC /
// DATA_DELETE / DATA_ARCHIVE / MULTIPLE_ASYNC)
// - OB-MySQL:Object.values(TaskType) 全集
const lateralCases: Array<{
name: string;
actual: TaskType[];
expected: TaskType[];
}> = [
{
name: 'MySQL features.task baseline (12 items, contains DATAMOCK)',
actual: mysqlItems[ConnectType.MYSQL].features.task,
expected: [
TaskType.ASYNC,
TaskType.DATAMOCK,
TaskType.SQL_PLAN,
TaskType.DATA_ARCHIVE,
TaskType.DATA_DELETE,
TaskType.IMPORT,
TaskType.EXPORT,
TaskType.EXPORT_RESULT_SET,
TaskType.STRUCTURE_COMPARISON,
TaskType.MULTIPLE_ASYNC,
TaskType.LOGICAL_DATABASE_CHANGE
]
},
{
name: 'Oracle features.task baseline (8 items, no STRUCTURE_COMPARISON)',
actual: oracleItems[ConnectType.ORACLE].features.task,
expected: [
TaskType.IMPORT,
TaskType.EXPORT,
TaskType.EXPORT_RESULT_SET,
TaskType.SQL_PLAN,
TaskType.ASYNC,
TaskType.DATA_DELETE,
TaskType.DATA_ARCHIVE,
TaskType.MULTIPLE_ASYNC
]
},
{
name: 'OB-MySQL features.task baseline (Object.values(TaskType) 全集)',
actual: obItems[ConnectType.OB_MYSQL].features.task,
expected: Object.values(TaskType)
}
];

lateralCases.forEach(({ name, actual, expected }) => {
it(name, () => {
expect(actual).toEqual(expected);
});
});

it('MySQL features.task NOT polluted by PG collapse (length unchanged)', () => {
expect(mysqlItems[ConnectType.MYSQL].features.task.length).toBeGreaterThan(
1
);
});

it('Oracle features.task NOT polluted by PG collapse (length unchanged)', () => {
expect(
oracleItems[ConnectType.ORACLE].features.task.length
).toBeGreaterThan(1);
});
});
30 changes: 18 additions & 12 deletions src/common/datasource/pg/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ import MySQLColumnExtra from '../oceanbase/MySQLColumnExtra';
import { haveOCP } from '@/util/env';

const tableConfig = {
enableTableCharsetsAndCollations: true,
enableTableCharsetsAndCollations: false, // PG 字符集在数据库级别设置
enableConstraintOnUpdate: true,
ColumnExtraComponent: MySQLColumnExtra,
paritionNameCaseSensitivity: true,
enableIndexesFullTextType: true,
enableAutoIncrement: true,
enableAutoIncrement: false, // PG 使用 SERIAL/IDENTITY
type2ColumnType: {
id: 'int',
name: 'varchar',
date: 'datetime',
date: 'timestamp',
time: 'timestamp'
}
};
Expand Down Expand Up @@ -67,14 +67,20 @@ const items: Record<ConnectType.PG, IDataSourceModeConfig> = {
disableURLParse: true
},
features: {
task: [TaskType.DATA_ARCHIVE, TaskType.DATA_DELETE],
// Align with ODC backend PostgreSQLFeatures + odc_version_diff_config:
// PG 后端在本期仅承诺 ASYNC(数据库变更)任务;SQL_PLAN / DATA_ARCHIVE /
// DATA_DELETE / IMPORT / EXPORT / EXPORT_RESULT_SET / STRUCTURE_COMPARISON /
// MULTIPLE_ASYNC 均未在后端开放,前端入口同步收敛以避免点击后 unsupported。
// Refs: dms-ee#850, compat-RISK-3
task: [TaskType.ASYNC],
obclient: false,
recycleBin: false,
sessionManage: false,
sessionParams: false,
sqlExplain: false,
groupResourceTree: false,
sqlconsole: false,
recycleBin: false, // PG 无回收站
sessionManage: true,
sessionParams: true,
sqlExplain: true,
plRun: true,
groupResourceTree: true,
sqlconsole: true,
export: {
fileLimit: false,
snapshot: false
Expand All @@ -84,10 +90,10 @@ const items: Record<ConnectType.PG, IDataSourceModeConfig> = {
table: tableConfig,
func: functionConfig,
proc: procedureConfig,
innerSchema: ['postgres']
innerSchema: ['postgres', 'information_schema', 'pg_catalog']
},
sql: {
language: 'mysql',
language: 'pg',
escapeChar: '"',
caseSensitivity: true
}
Expand Down
34 changes: 30 additions & 4 deletions src/component/MonacoEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ const MonacoEditor: React.FC<IProps> = function (props) {
'./plugins/sqlserver-language/service'
);
getModelServiceFunc = serviceModule.getModelService;
} else if (language === 'pg') {
pluginModule = await import('./plugins/pg-language/index');
const serviceModule = await import('./plugins/pg-language/service');
getModelServiceFunc = serviceModule.getModelService;
} else {
pluginModule = await import('./plugins/ob-language/index');
getModelServiceFunc = getModelService;
Expand Down Expand Up @@ -187,8 +191,8 @@ const MonacoEditor: React.FC<IProps> = function (props) {
}

async function initEditor() {
// SQL Server 使用自定义的 'sqlserver' 语言支持
const monacoLanguage = language === 'sqlserver' ? 'sqlserver' : language;
// SQL Server 使用自定义的 'sqlserver' 语言支持, PostgreSQL 使用 'pg'
const monacoLanguage = language === 'sqlserver' ? 'sqlserver' : language === 'pg' ? 'pg' : language;
const currentTheme = theme || settingTheme;

editorRef.current = monaco.editor?.create(domRef.current, {
Expand Down Expand Up @@ -308,8 +312,8 @@ const MonacoEditor: React.FC<IProps> = function (props) {
language &&
language !== editorRef.current?.getModel().getLanguageId()
) {
// SQL Server 使用自定义的 'sqlserver' 语言支持
const monacoLanguage = language === 'sqlserver' ? 'sqlserver' : language;
// SQL Server 使用自定义的 'sqlserver' 语言支持, PostgreSQL 使用 'pg'
const monacoLanguage = language === 'sqlserver' ? 'sqlserver' : language === 'pg' ? 'pg' : language;
monaco.editor.setModelLanguage(
editorRef.current?.getModel(),
monacoLanguage
Expand Down Expand Up @@ -338,6 +342,28 @@ const MonacoEditor: React.FC<IProps> = function (props) {
);
}
});
} else if (language === 'pg') {
Promise.all([
import('./plugins/pg-language/index'),
import('./plugins/pg-language/service')
]).then(([pluginModule, serviceModule]) => {
const plugin = pluginModule.register(language);
// 重新设置模型选项
if (editorRef.current?.getModel()) {
plugin.setModelOptions(
editorRef.current.getModel().id,
serviceModule.getModelService(
{
modelId: editorRef.current.getModel().id,
delimiter() {
return sessionRef.current?.params?.delimiter;
}
},
() => sessionRef.current
)
);
}
});
} else {
import('./plugins/ob-language/index').then((module) =>
module.register(language)
Expand Down
Loading