mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 02:19:58 +08:00
🐛 fix(frontend/ci): 修复对象修改卡顿与 Windows ARM 驱动校验失败
- QueryEditor 为对象修改标签增加 object-edit 轻量模式,跳过重型元数据抓取和对象装饰扫描 - DefinitionViewer 与 TriggerViewer 打开的对象修改标签统一透传 queryMode,避免重新进入普通查询链路 - TriggerViewer 补全 MySQL/Oracle 类触发器 DDL 重建逻辑,修复对象修改打开语法不完整 - 补充对象修改与触发器 DDL 回归测试,覆盖轻量模式和元数据补全场景 - verify-driver-agent-revisions 脚本改为跨架构校验,避免在 x64 runner 直接执行 windows/arm64 二进制 - 新增 Windows ARM CI 校验追踪文档,保留架构校验与 host-native probe 证据
This commit is contained in:
@@ -114,6 +114,7 @@ describe('DefinitionViewer object edit entry', () => {
|
||||
type: 'query',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'main',
|
||||
queryMode: 'object-edit',
|
||||
query: expect.stringContaining('CREATE OR REPLACE VIEW reporting.active_users AS'),
|
||||
}));
|
||||
expect(storeState.addTab.mock.calls[0][0].query).toContain('SELECT id, name FROM users;');
|
||||
@@ -172,6 +173,7 @@ describe('DefinitionViewer object edit entry', () => {
|
||||
expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: '修改函数/存储过程: reporting.refresh_stats',
|
||||
type: 'query',
|
||||
queryMode: 'object-edit',
|
||||
query: expect.stringContaining('CREATE OR REPLACE FUNCTION reporting.refresh_stats()'),
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -612,6 +612,7 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
connectionId: tab.connectionId,
|
||||
dbName,
|
||||
query,
|
||||
queryMode: 'object-edit',
|
||||
});
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
|
||||
@@ -1331,6 +1331,30 @@ describe('QueryEditor external SQL save', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it('skips heavy autocomplete metadata fetch for object edit query tabs', async () => {
|
||||
autoFetchState.visible = true;
|
||||
backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }, { Database: 'analytics' }] });
|
||||
|
||||
await act(async () => {
|
||||
create(<QueryEditor tab={createTab({
|
||||
query: 'CREATE OR REPLACE VIEW reporting.active_users AS SELECT * FROM users;',
|
||||
dbName: 'main',
|
||||
queryMode: 'object-edit',
|
||||
})} />);
|
||||
});
|
||||
await act(async () => {
|
||||
for (let i = 0; i < 6; i += 1) {
|
||||
await Promise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
expect(backendApp.DBGetDatabases).toHaveBeenCalledTimes(1);
|
||||
expect(backendApp.DBGetTables).not.toHaveBeenCalled();
|
||||
expect(backendApp.DBGetAllColumns).not.toHaveBeenCalled();
|
||||
expect(backendApp.DBQuery).not.toHaveBeenCalled();
|
||||
expect(editorState.editor.deltaDecorations).toHaveBeenCalledWith([], []);
|
||||
});
|
||||
|
||||
it('keeps the editor empty when a tab draft is externally synced to an empty query', async () => {
|
||||
let renderer!: ReactTestRenderer;
|
||||
|
||||
|
||||
@@ -1912,6 +1912,7 @@ const resolveQueryLocatorPlan = async ({
|
||||
const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isActive = true }) => {
|
||||
const [query, setQuery] = useState(getInitialEditorQuery(tab));
|
||||
const isExternalSQLFileTab = Boolean(String(tab.filePath || '').trim());
|
||||
const isObjectEditQueryTab = tab.type === 'query' && tab.queryMode === 'object-edit';
|
||||
|
||||
type ResultSet = {
|
||||
key: string;
|
||||
@@ -2127,6 +2128,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
return;
|
||||
}
|
||||
|
||||
if (isObjectEditQueryTab) {
|
||||
objectDecorationIdsRef.current = editor.deltaDecorations(objectDecorationIdsRef.current, []);
|
||||
return;
|
||||
}
|
||||
|
||||
const text = getQueryEditorDecorationModelTextIfLightweight(model, maxTextLength);
|
||||
if (text === null) {
|
||||
objectDecorationIdsRef.current = editor.deltaDecorations(objectDecorationIdsRef.current, []);
|
||||
@@ -2173,7 +2179,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
}
|
||||
|
||||
objectDecorationIdsRef.current = editor.deltaDecorations(objectDecorationIdsRef.current, decorations);
|
||||
}, []);
|
||||
}, [isObjectEditQueryTab]);
|
||||
|
||||
const showObjectInfoAtPosition = useCallback((position?: { lineNumber: number; column: number } | null) => {
|
||||
const editor = editorRef.current;
|
||||
@@ -2380,7 +2386,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
|
||||
// Fetch Metadata for Autocomplete (Cross-database)
|
||||
useEffect(() => {
|
||||
if (!autoFetchVisible) {
|
||||
if (!autoFetchVisible || isObjectEditQueryTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2584,7 +2590,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
refreshObjectDecorations();
|
||||
};
|
||||
void fetchMetadata();
|
||||
}, [autoFetchVisible, currentConnectionId, connections, dbList, isActive, refreshObjectDecorations]); // dbList 变化时触发重新加载
|
||||
}, [autoFetchVisible, currentConnectionId, connections, dbList, isActive, isObjectEditQueryTab, refreshObjectDecorations]); // dbList 变化时触发重新加载
|
||||
|
||||
// Query ID management helpers
|
||||
const setQueryId = (id: string) => {
|
||||
|
||||
@@ -112,6 +112,7 @@ describe('TriggerViewer object edit entry', () => {
|
||||
type: 'query',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'main',
|
||||
queryMode: 'object-edit',
|
||||
query: expect.stringContaining('CREATE TRIGGER users_bi BEFORE INSERT'),
|
||||
}));
|
||||
});
|
||||
@@ -172,6 +173,99 @@ describe('TriggerViewer object edit entry', () => {
|
||||
expect(query).not.toContain('请补全 CREATE TRIGGER 语句');
|
||||
});
|
||||
|
||||
it('rebuilds mysql trigger ddl from metadata rows when only action statement is available', async () => {
|
||||
storeState.connections[0].config.type = 'mysql';
|
||||
backendApp.DBQuery.mockResolvedValue({
|
||||
success: true,
|
||||
data: [{
|
||||
TRIGGER_NAME: 'users_bi',
|
||||
TRIGGER_SCHEMA: 'main',
|
||||
EVENT_OBJECT_SCHEMA: 'audit',
|
||||
EVENT_OBJECT_TABLE: 'users',
|
||||
ACTION_TIMING: 'BEFORE',
|
||||
EVENT_MANIPULATION: 'INSERT',
|
||||
ACTION_ORIENTATION: 'ROW',
|
||||
ACTION_STATEMENT: 'SET NEW.created_at = NOW()',
|
||||
}],
|
||||
});
|
||||
|
||||
let renderer: any;
|
||||
await act(async () => {
|
||||
renderer = create(<TriggerViewer tab={tab} />);
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('对象修改'))[0];
|
||||
|
||||
await act(async () => {
|
||||
button.props.onClick();
|
||||
});
|
||||
|
||||
const query = storeState.addTab.mock.calls[0][0].query;
|
||||
expect(query).toContain('CREATE TRIGGER `main`.`users_bi`');
|
||||
expect(query).toContain('BEFORE INSERT ON `audit`.`users`');
|
||||
expect(query).toContain('FOR EACH ROW');
|
||||
expect(query).toContain('SET NEW.created_at = NOW();');
|
||||
expect(query).not.toContain('请补全 CREATE TRIGGER 语句');
|
||||
});
|
||||
|
||||
it('rebuilds oracle trigger ddl from metadata rows when body query returns fragments only', async () => {
|
||||
storeState.connections[0].config.type = 'oracle';
|
||||
backendApp.DBQuery
|
||||
.mockResolvedValueOnce({ success: true, data: [] })
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{
|
||||
OWNER: 'AUDIT',
|
||||
TABLE_OWNER: 'AUDIT',
|
||||
TABLE_NAME: 'USERS',
|
||||
TRIGGER_NAME: 'USERS_BU',
|
||||
TRIGGER_TYPE: 'BEFORE EACH ROW',
|
||||
TRIGGERING_EVENT: 'UPDATE',
|
||||
WHEN_CLAUSE: 'NEW.UPDATED_AT IS NULL',
|
||||
TRIGGER_BODY: 'BEGIN\n :NEW.UPDATED_AT := SYSDATE;\nEND;',
|
||||
}],
|
||||
})
|
||||
.mockResolvedValueOnce({ success: true, data: [] })
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{
|
||||
OWNER: 'AUDIT',
|
||||
TABLE_OWNER: 'AUDIT',
|
||||
TABLE_NAME: 'USERS',
|
||||
TRIGGER_NAME: 'USERS_BU',
|
||||
TRIGGER_TYPE: 'BEFORE EACH ROW',
|
||||
TRIGGERING_EVENT: 'UPDATE',
|
||||
WHEN_CLAUSE: 'NEW.UPDATED_AT IS NULL',
|
||||
TRIGGER_BODY: 'BEGIN\n :NEW.UPDATED_AT := SYSDATE;\nEND;',
|
||||
}],
|
||||
});
|
||||
|
||||
let renderer: any;
|
||||
await act(async () => {
|
||||
renderer = create(<TriggerViewer tab={{
|
||||
...tab,
|
||||
id: 'trigger-conn-1-main-audit.users_bu',
|
||||
title: '触发器: audit.users_bu',
|
||||
triggerName: 'audit.users_bu',
|
||||
}} />);
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('对象修改'))[0];
|
||||
|
||||
await act(async () => {
|
||||
button.props.onClick();
|
||||
});
|
||||
|
||||
const query = storeState.addTab.mock.calls[0][0].query;
|
||||
expect(query).toContain('CREATE OR REPLACE TRIGGER AUDIT.USERS_BU');
|
||||
expect(query).toContain('BEFORE UPDATE ON AUDIT.USERS');
|
||||
expect(query).toContain('FOR EACH ROW');
|
||||
expect(query).toContain('WHEN (NEW.UPDATED_AT IS NULL)');
|
||||
expect(query).toContain(':NEW.UPDATED_AT := SYSDATE;');
|
||||
});
|
||||
|
||||
it('reloads the latest trigger definition before opening object edit', async () => {
|
||||
backendApp.DBQuery
|
||||
.mockResolvedValueOnce({
|
||||
|
||||
@@ -19,6 +19,92 @@ const ensureSqlStatementTerminator = (sql: string): string => {
|
||||
return /;\s*$/.test(normalized) ? normalized : `${normalized};`;
|
||||
};
|
||||
|
||||
const getCaseInsensitiveRawValue = (row: Record<string, any>, keys: string[]): any => {
|
||||
const normalizedKeyMap = new Map<string, string>();
|
||||
Object.keys(row || {}).forEach((key) => normalizedKeyMap.set(key.toLowerCase(), key));
|
||||
for (const key of keys) {
|
||||
const matchedKey = normalizedKeyMap.get(String(key || '').toLowerCase());
|
||||
if (!matchedKey) continue;
|
||||
const value = row[matchedKey];
|
||||
if (value !== undefined && value !== null && String(value).trim() !== '') {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const buildMySQLTriggerDDLFromMetadata = (
|
||||
row: Record<string, any>,
|
||||
fallbackTriggerName: string,
|
||||
fallbackTableName: string,
|
||||
): string => {
|
||||
const triggerName = String(
|
||||
getCaseInsensitiveRawValue(row, ['trigger_name', 'Trigger', 'TRIGGER_NAME'])
|
||||
|| splitQualifiedNameLast(fallbackTriggerName).objectName
|
||||
|| fallbackTriggerName,
|
||||
).trim();
|
||||
const triggerSchema = String(getCaseInsensitiveRawValue(row, ['trigger_schema', 'TRIGGER_SCHEMA']) || '').trim();
|
||||
const eventSchema = String(getCaseInsensitiveRawValue(row, ['event_object_schema', 'EVENT_OBJECT_SCHEMA']) || '').trim();
|
||||
const eventTable = String(
|
||||
getCaseInsensitiveRawValue(row, ['event_object_table', 'EVENT_OBJECT_TABLE', 'table_name', 'TABLE_NAME'])
|
||||
|| splitQualifiedNameLast(fallbackTableName).objectName
|
||||
|| fallbackTableName,
|
||||
).trim();
|
||||
const actionTiming = String(getCaseInsensitiveRawValue(row, ['action_timing', 'ACTION_TIMING']) || '').trim().toUpperCase();
|
||||
const eventManipulation = String(getCaseInsensitiveRawValue(row, ['event_manipulation', 'EVENT_MANIPULATION']) || '').trim().toUpperCase();
|
||||
const actionOrientation = String(getCaseInsensitiveRawValue(row, ['action_orientation', 'ACTION_ORIENTATION']) || '').trim().toUpperCase();
|
||||
const actionStatement = String(getCaseInsensitiveRawValue(row, ['action_statement', 'ACTION_STATEMENT']) || '').trim();
|
||||
|
||||
if (!triggerName || !eventTable || !actionTiming || !eventManipulation || !actionStatement) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const qualifiedTriggerName = triggerSchema ? `\`${triggerSchema.replace(/`/g, '``')}\`.\`${triggerName.replace(/`/g, '``')}\`` : `\`${triggerName.replace(/`/g, '``')}\``;
|
||||
const qualifiedTableName = eventSchema ? `\`${eventSchema.replace(/`/g, '``')}\`.\`${eventTable.replace(/`/g, '``')}\`` : `\`${eventTable.replace(/`/g, '``')}\``;
|
||||
const orientationClause = actionOrientation === 'ROW' ? '\nFOR EACH ROW' : '';
|
||||
return `CREATE TRIGGER ${qualifiedTriggerName}\n${actionTiming} ${eventManipulation} ON ${qualifiedTableName}${orientationClause}\n${actionStatement}`;
|
||||
};
|
||||
|
||||
const buildOracleLikeTriggerDDLFromMetadata = (
|
||||
row: Record<string, any>,
|
||||
fallbackTriggerName: string,
|
||||
fallbackTableName: string,
|
||||
): string => {
|
||||
const triggerName = String(
|
||||
getCaseInsensitiveRawValue(row, ['trigger_name', 'TRIGGER_NAME'])
|
||||
|| splitQualifiedNameLast(fallbackTriggerName).objectName
|
||||
|| fallbackTriggerName,
|
||||
).trim();
|
||||
const owner = String(getCaseInsensitiveRawValue(row, ['owner', 'OWNER']) || splitQualifiedNameLast(fallbackTriggerName).parentPath || '').trim();
|
||||
const tableOwner = String(getCaseInsensitiveRawValue(row, ['table_owner', 'TABLE_OWNER']) || splitQualifiedNameLast(fallbackTableName).parentPath || '').trim();
|
||||
const tableName = String(
|
||||
getCaseInsensitiveRawValue(row, ['table_name', 'TABLE_NAME'])
|
||||
|| splitQualifiedNameLast(fallbackTableName).objectName
|
||||
|| fallbackTableName,
|
||||
).trim();
|
||||
const triggerType = String(getCaseInsensitiveRawValue(row, ['trigger_type', 'TRIGGER_TYPE']) || '').trim();
|
||||
const triggeringEvent = String(getCaseInsensitiveRawValue(row, ['triggering_event', 'TRIGGERING_EVENT']) || '').trim();
|
||||
const whenClause = String(getCaseInsensitiveRawValue(row, ['when_clause', 'WHEN_CLAUSE']) || '').trim();
|
||||
const triggerBody = String(getCaseInsensitiveRawValue(row, ['trigger_body', 'TRIGGER_BODY']) || '').trim();
|
||||
|
||||
if (!triggerName || !tableName || !triggerType || !triggeringEvent || !triggerBody) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const qualifiedTriggerName = owner ? `${owner}.${triggerName}` : triggerName;
|
||||
const qualifiedTableName = tableOwner ? `${tableOwner}.${tableName}` : tableName;
|
||||
const normalizedWhenClause = whenClause ? `\nWHEN (${whenClause.replace(/^\((.*)\)$/s, '$1')})` : '';
|
||||
const normalizedTriggerType = triggerType.replace(/\s+/g, ' ').trim();
|
||||
const triggerTypeMatch = normalizedTriggerType.match(/^(BEFORE|AFTER|INSTEAD OF)(?:\s+(EACH ROW|STATEMENT))?$/i);
|
||||
if (triggerTypeMatch) {
|
||||
const timing = String(triggerTypeMatch[1] || '').toUpperCase();
|
||||
const firingLevel = String(triggerTypeMatch[2] || '').toUpperCase();
|
||||
const forEachRowClause = firingLevel === 'EACH ROW' ? '\nFOR EACH ROW' : '';
|
||||
return `CREATE OR REPLACE TRIGGER ${qualifiedTriggerName}\n${timing} ${triggeringEvent} ON ${qualifiedTableName}${forEachRowClause}${normalizedWhenClause}\n${triggerBody}`;
|
||||
}
|
||||
return `CREATE OR REPLACE TRIGGER ${qualifiedTriggerName}\n${triggerType} ${triggeringEvent} ON ${qualifiedTableName}${normalizedWhenClause}\n${triggerBody}`;
|
||||
};
|
||||
|
||||
const buildEditableTriggerSql = (triggerName: string, triggerDefinition: string): string => {
|
||||
const normalizedName = String(triggerName || '').trim();
|
||||
const normalizedDefinition = String(triggerDefinition || '').trim();
|
||||
@@ -102,7 +188,7 @@ const TriggerViewer: React.FC<TriggerViewerProps> = ({ tab }) => {
|
||||
return [
|
||||
`SHOW CREATE TRIGGER \`${name.replace(/`/g, '``')}\``,
|
||||
safeDbName
|
||||
? `SELECT ACTION_STATEMENT AS trigger_definition FROM information_schema.triggers WHERE trigger_schema = '${safeDbName}' AND trigger_name = '${safeTriggerName}' LIMIT 1`
|
||||
? `SELECT TRIGGER_NAME, TRIGGER_SCHEMA, EVENT_OBJECT_SCHEMA, EVENT_OBJECT_TABLE, ACTION_TIMING, EVENT_MANIPULATION, ACTION_ORIENTATION, ACTION_STATEMENT FROM information_schema.triggers WHERE trigger_schema = '${safeDbName}' AND trigger_name = '${safeTriggerName}' LIMIT 1`
|
||||
: '',
|
||||
safeDbName
|
||||
? `SHOW TRIGGERS FROM \`${dbName.replace(/`/g, '``')}\` LIKE '${safeTriggerName}'`
|
||||
@@ -125,12 +211,21 @@ LIMIT 1`];
|
||||
case 'oracle':
|
||||
case 'dm':
|
||||
if (schema) {
|
||||
return [`SELECT TRIGGER_BODY FROM ALL_TRIGGERS WHERE OWNER = '${escapeSQLLiteral(schema).toUpperCase()}' AND TRIGGER_NAME = '${safeTriggerName.toUpperCase()}'`];
|
||||
return [
|
||||
`SELECT DBMS_METADATA.GET_DDL('TRIGGER', '${safeTriggerName.toUpperCase()}', '${escapeSQLLiteral(schema).toUpperCase()}') AS trigger_definition FROM DUAL`,
|
||||
`SELECT OWNER, TABLE_OWNER, TABLE_NAME, TRIGGER_NAME, TRIGGER_TYPE, TRIGGERING_EVENT, WHEN_CLAUSE, TRIGGER_BODY FROM ALL_TRIGGERS WHERE OWNER = '${escapeSQLLiteral(schema).toUpperCase()}' AND TRIGGER_NAME = '${safeTriggerName.toUpperCase()}'`,
|
||||
];
|
||||
}
|
||||
if (!safeDbName) {
|
||||
return [`SELECT TRIGGER_BODY FROM USER_TRIGGERS WHERE TRIGGER_NAME = '${safeTriggerName.toUpperCase()}'`];
|
||||
return [
|
||||
`SELECT DBMS_METADATA.GET_DDL('TRIGGER', '${safeTriggerName.toUpperCase()}') AS trigger_definition FROM DUAL`,
|
||||
`SELECT TABLE_NAME, TRIGGER_NAME, TRIGGER_TYPE, TRIGGERING_EVENT, WHEN_CLAUSE, TRIGGER_BODY FROM USER_TRIGGERS WHERE TRIGGER_NAME = '${safeTriggerName.toUpperCase()}'`,
|
||||
];
|
||||
}
|
||||
return [`SELECT TRIGGER_BODY FROM ALL_TRIGGERS WHERE OWNER = '${safeDbName.toUpperCase()}' AND TRIGGER_NAME = '${safeTriggerName.toUpperCase()}'`];
|
||||
return [
|
||||
`SELECT DBMS_METADATA.GET_DDL('TRIGGER', '${safeTriggerName.toUpperCase()}', '${safeDbName.toUpperCase()}') AS trigger_definition FROM DUAL`,
|
||||
`SELECT OWNER, TABLE_OWNER, TABLE_NAME, TRIGGER_NAME, TRIGGER_TYPE, TRIGGERING_EVENT, WHEN_CLAUSE, TRIGGER_BODY FROM ALL_TRIGGERS WHERE OWNER = '${safeDbName.toUpperCase()}' AND TRIGGER_NAME = '${safeTriggerName.toUpperCase()}'`,
|
||||
];
|
||||
case 'sqlite':
|
||||
return [`SELECT sql FROM sqlite_master WHERE type = 'trigger' AND name = '${safeTriggerName}'`];
|
||||
case 'duckdb':
|
||||
@@ -202,21 +297,25 @@ LIMIT 1`];
|
||||
return '';
|
||||
};
|
||||
|
||||
const extractTriggerDefinition = (dialect: string, data: any[]): string => {
|
||||
const extractTriggerDefinition = (dialect: string, data: any[], fallbackTriggerName: string, fallbackTableName: string): string => {
|
||||
if (!data || data.length === 0) {
|
||||
return '-- 未找到触发器定义';
|
||||
}
|
||||
|
||||
const row = data[0];
|
||||
const row = data[0] as Record<string, any>;
|
||||
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks': {
|
||||
// MySQL SHOW CREATE TRIGGER returns: Trigger, sql_mode, SQL Original Statement, ...
|
||||
const keys = Object.keys(row);
|
||||
const metadataDDL = buildMySQLTriggerDDLFromMetadata(row, fallbackTriggerName, fallbackTableName);
|
||||
if (row.trigger_definition || row.TRIGGER_DEFINITION) {
|
||||
return String(row.trigger_definition || row.TRIGGER_DEFINITION);
|
||||
}
|
||||
if (metadataDDL) {
|
||||
return metadataDDL;
|
||||
}
|
||||
if (row.ACTION_STATEMENT || row.action_statement) {
|
||||
return String(row.ACTION_STATEMENT || row.action_statement);
|
||||
}
|
||||
@@ -243,6 +342,14 @@ LIMIT 1`];
|
||||
}
|
||||
case 'oracle':
|
||||
case 'dm': {
|
||||
const ddl = String(row.trigger_definition || row.TRIGGER_DEFINITION || '').trim();
|
||||
if (ddl) {
|
||||
return ddl;
|
||||
}
|
||||
const metadataDDL = buildOracleLikeTriggerDDLFromMetadata(row, fallbackTriggerName, fallbackTableName);
|
||||
if (metadataDDL) {
|
||||
return metadataDDL;
|
||||
}
|
||||
return row.trigger_body || row.TRIGGER_BODY || Object.values(row)[0] || '';
|
||||
}
|
||||
case 'sqlite': {
|
||||
@@ -287,7 +394,10 @@ LIMIT 1`];
|
||||
const result = await runQueryCandidates(config, dbName, queries);
|
||||
|
||||
if (result.success && Array.isArray(result.data) && result.data.length > 0) {
|
||||
return { success: true, definition: extractTriggerDefinition(dialect, result.data) };
|
||||
return {
|
||||
success: true,
|
||||
definition: extractTriggerDefinition(dialect, result.data, triggerName, String(tab.triggerTableName || '')),
|
||||
};
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
@@ -391,6 +501,7 @@ LIMIT 1`];
|
||||
connectionId: tab.connectionId,
|
||||
dbName,
|
||||
query: buildEditableTriggerSql(triggerName, latestDefinition),
|
||||
queryMode: 'object-edit',
|
||||
});
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
|
||||
@@ -412,6 +412,7 @@ export interface TabData {
|
||||
dbName?: string;
|
||||
tableName?: string;
|
||||
query?: string;
|
||||
queryMode?: "standard" | "object-edit";
|
||||
filePath?: string;
|
||||
initialTab?: string;
|
||||
readOnly?: boolean;
|
||||
|
||||
@@ -99,6 +99,15 @@ expected_revision_for() {
|
||||
' internal/db/driver_agent_revisions_gen.go
|
||||
}
|
||||
|
||||
build_tags_for_driver() {
|
||||
local driver="$1"
|
||||
local tags="gonavi_${driver}_driver"
|
||||
if [[ "$driver" == "duckdb" && "$host_goos" == "windows" && "$host_goarch" == "amd64" ]]; then
|
||||
tags="${tags} duckdb_use_lib"
|
||||
fi
|
||||
printf '%s\n' "$tags"
|
||||
}
|
||||
|
||||
agent_path_for() {
|
||||
local driver="$1"
|
||||
local public_name asset
|
||||
@@ -125,9 +134,97 @@ print(data.get("agentRevision", ""))
|
||||
'
|
||||
}
|
||||
|
||||
probe_host_agent_revision() {
|
||||
local driver="$1"
|
||||
local build_tags probe_dir probe_path revision
|
||||
build_tags="$(build_tags_for_driver "$driver")"
|
||||
probe_dir="$(mktemp -d)"
|
||||
probe_path="${probe_dir}/probe-agent"
|
||||
if [[ "$host_goos" == "windows" ]]; then
|
||||
probe_path="${probe_path}.exe"
|
||||
fi
|
||||
|
||||
CGO_ENABLED=0 go build \
|
||||
-tags "${build_tags}" \
|
||||
-trimpath \
|
||||
-ldflags "-s -w" \
|
||||
-o "${probe_path}" \
|
||||
./cmd/optional-driver-agent >/dev/null
|
||||
|
||||
chmod +x "$probe_path" 2>/dev/null || true
|
||||
revision="$(probe_agent_revision "$probe_path")"
|
||||
rm -rf "$probe_dir"
|
||||
printf '%s\n' "$revision"
|
||||
}
|
||||
|
||||
can_execute_target_binary() {
|
||||
[[ "$target_platform" == "$host_platform" ]]
|
||||
}
|
||||
|
||||
validate_windows_pe_machine() {
|
||||
local agent_path="$1"
|
||||
local expected_goarch="$2"
|
||||
python3 - "$agent_path" "$expected_goarch" <<'PY'
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
path = sys.argv[1]
|
||||
goarch = sys.argv[2].strip().lower()
|
||||
expected = {
|
||||
"386": 0x014C,
|
||||
"amd64": 0x8664,
|
||||
"arm64": 0xAA64,
|
||||
}
|
||||
labels = {
|
||||
0x014C: "windows-386",
|
||||
0x8664: "windows-amd64",
|
||||
0xAA64: "windows-arm64",
|
||||
}
|
||||
|
||||
if goarch not in expected:
|
||||
sys.exit(0)
|
||||
|
||||
with open(path, "rb") as fh:
|
||||
fh.seek(0, os.SEEK_END)
|
||||
size = fh.tell()
|
||||
if size < 0x40:
|
||||
raise SystemExit("文件头不完整")
|
||||
|
||||
fh.seek(0)
|
||||
if fh.read(2) != b"MZ":
|
||||
raise SystemExit("缺少 MZ 头")
|
||||
|
||||
fh.seek(0x3C)
|
||||
pe_offset_raw = fh.read(4)
|
||||
if len(pe_offset_raw) != 4:
|
||||
raise SystemExit("读取 PE 头偏移失败")
|
||||
pe_offset = struct.unpack("<I", pe_offset_raw)[0]
|
||||
if pe_offset < 0x40 or pe_offset + 24 > size:
|
||||
raise SystemExit("PE 头不完整")
|
||||
|
||||
fh.seek(pe_offset)
|
||||
if fh.read(4) != b"PE\0\0":
|
||||
raise SystemExit("缺少 PE 签名")
|
||||
|
||||
machine_raw = fh.read(2)
|
||||
if len(machine_raw) != 2:
|
||||
raise SystemExit("读取 PE 架构失败")
|
||||
machine = struct.unpack("<H", machine_raw)[0]
|
||||
|
||||
expected_machine = expected[goarch]
|
||||
if machine != expected_machine:
|
||||
raise SystemExit(f"可执行文件架构不兼容(文件={labels.get(machine, hex(machine))},期望={labels[expected_machine]})")
|
||||
PY
|
||||
}
|
||||
|
||||
declare -a raw_drivers=()
|
||||
IFS=',' read -r -a raw_drivers <<<"$driver_csv"
|
||||
|
||||
host_goos="$(go env GOOS)"
|
||||
host_goarch="$(go env GOARCH)"
|
||||
host_platform="${host_goos}/${host_goarch}"
|
||||
|
||||
failed=0
|
||||
for raw_driver in "${raw_drivers[@]}"; do
|
||||
[[ -n "$raw_driver" ]] || continue
|
||||
@@ -152,7 +249,22 @@ for raw_driver in "${raw_drivers[@]}"; do
|
||||
fi
|
||||
chmod +x "$agent_path" 2>/dev/null || true
|
||||
|
||||
actual="$(probe_agent_revision "$agent_path" || true)"
|
||||
if [[ "$goos" == "windows" ]]; then
|
||||
if ! validate_windows_pe_machine "$agent_path" "$goarch"; then
|
||||
echo "❌ $driver Windows driver-agent 架构校验失败:asset=$agent_path target=$target_platform"
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
actual=""
|
||||
if can_execute_target_binary; then
|
||||
actual="$(probe_agent_revision "$agent_path" || true)"
|
||||
else
|
||||
echo "ℹ️ runner 平台 ${host_platform} 无法直接执行目标二进制 ${target_platform},已先完成目标资产架构校验,再用 host-native probe 校验相同 build tags 的 revision"
|
||||
actual="$(probe_host_agent_revision "$driver" || true)"
|
||||
fi
|
||||
|
||||
if [[ "$actual" != "$expected" ]]; then
|
||||
echo "❌ $driver driver-agent revision 不匹配:asset=$agent_path actual=${actual:-空} expected=$expected"
|
||||
failed=1
|
||||
|
||||
Reference in New Issue
Block a user