🐛 fix(query-editor): 修复 OceanBase 视图查询与输入丢字

- 仅对元数据确认的 OceanBase Oracle 基表注入 ROWID
- 视图和同义词查询改为只读,避免触发 OBE-01445
- 在 SQL 补全中加入普通视图和物化视图候选
- 修复 beforeinput 已拦截时首个可打印字符被跳过的问题
- 补充视图、同义词和输入回归测试
This commit is contained in:
Syngnat
2026-07-14 17:11:07 +08:00
parent 8a1c1842ee
commit 3fa4f8e900
8 changed files with 342 additions and 19 deletions

View File

@@ -444,6 +444,33 @@ describe('DataViewer safe editing locator', () => {
renderer.unmount();
});
it('queries Oracle views without injecting ROWID and keeps the result read-only', async () => {
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ name: 'ID', key: '' }, { name: 'NAME', key: '' }],
});
backendApp.DBQuery.mockResolvedValue({
success: true,
fields: ['ID', 'NAME'],
data: [{ ID: 7, NAME: 'view-row' }],
});
const renderer = await renderAndReload(createTab({
id: 'tab-oracle-view-rowid',
tableName: 'PERSON_VIEW',
title: 'PERSON_VIEW',
objectType: 'view',
}));
const viewQueries = backendApp.DBQuery.mock.calls
.map((call: any[]) => String(call[2] || ''))
.filter((sql: string) => sql.includes('PERSON_VIEW'));
expect(viewQueries.length).toBeGreaterThan(0);
expect(viewQueries.every((sql: string) => !/\bROWID\b/i.test(sql))).toBe(true);
expect(dataGridState.latestProps?.readOnly).toBe(true);
renderer.unmount();
});
it('does not add fallback ORDER BY for DuckDB table preview when a primary key is available', async () => {
storeState.connections[0].config.type = 'duckdb';
storeState.connections[0].config.database = 'main';

View File

@@ -411,7 +411,9 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = React.memo(({
const duckdbSafeSelectCacheRef = useRef<Record<string, string>>({});
const currentConnConfig = connections.find(c => c.id === tab.connectionId)?.config;
const currentConnCaps = getDataSourceCapabilities(currentConnConfig);
const forceReadOnly = currentConnCaps.forceReadOnlyQueryResult;
// Ordinary views can reject physical ROWID (for example Oracle join views), so
// browse them without attempting the editable-table locator optimization.
const forceReadOnly = currentConnCaps.forceReadOnlyQueryResult || tab.objectType === 'view';
const preferManualTotalCount = currentConnCaps.preferManualTotalCount;
const supportsApproximateTableCount = currentConnCaps.supportsApproximateTableCount;
const supportsApproximateTotalPages = currentConnCaps.supportsApproximateTotalPages;
@@ -491,7 +493,7 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = React.memo(({
totalCountLoading: false,
totalCountCancelled: false,
}));
}, [tab.id, tab.connectionId, tab.dbName, tab.tableName]);
}, [tab.id, tab.connectionId, tab.dbName, tab.tableName, tab.objectType]);
const handleTableScrollSnapshotChange = useCallback((snapshot: ViewerScrollSnapshot) => {
scrollSnapshotRef.current = snapshot;
@@ -1226,7 +1228,7 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = React.memo(({
return;
}
fetchData(1, pagination.pageSize);
}, [tab.id, tab.connectionId, tab.dbName, tab.tableName, sortInfo, filterConditions, quickWhereCondition]); // Initial load and re-load on sort/filter
}, [tab.id, tab.connectionId, tab.dbName, tab.tableName, tab.objectType, sortInfo, filterConditions, quickWhereCondition]); // Initial load and re-load on sort/filter
return (
<div className={isV2Ui ? 'gn-v2-data-viewer' : undefined} style={{ flex: '1 1 auto', minHeight: 0, minWidth: 0, height: '100%', width: '100%', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>

View File

@@ -23,12 +23,12 @@ class FakeTextAreaElement extends FakeHTMLElement {
this.listeners.get(type)?.delete(listener);
}
dispatchPrintableBeforeInput(text: string): void {
dispatchPrintableBeforeInput(text: string, defaultPrevented = false): void {
const event = {
data: text,
inputType: 'insertText',
isComposing: false,
defaultPrevented: false,
defaultPrevented,
} as InputEvent;
this.listeners.get('beforeinput')?.forEach((listener) => listener(event));
}
@@ -169,6 +169,29 @@ describe('MonacoEditor printable input fallback', () => {
expect(position).toEqual({ lineNumber: 1, column: 3 });
});
it('recovers a default-prevented leading character when later input reaches the model', () => {
installPrintableInputFallback(editor, {
editor: { EditorOption: { readOnly: 1 } },
});
value = 'SELECT * FROM person';
position = { lineNumber: 1, column: value.length + 1 };
input.dispatchPrintableBeforeInput('s', true);
input.dispatchPrintableBeforeInput('f');
value = 'SELECT * FROM personf';
position = { lineNumber: 1, column: value.length + 1 };
modelContentListener?.();
vi.advanceTimersByTime(80);
expect(editor.executeEdits).toHaveBeenCalledWith(
'gonavi-printable-input-fallback',
[expect.objectContaining({ text: 'sf' })],
);
expect(value).toBe('SELECT * FROM personsf');
expect(position).toEqual({ lineNumber: 1, column: value.length + 1 });
});
it('settles the previous missing input before buffering text after a cursor move', () => {
installPrintableInputFallback(editor, {
editor: { EditorOption: { readOnly: 1 } },

View File

@@ -420,8 +420,7 @@ export const installPrintableInputFallback = (editor: any, monaco: any) => {
const handleBeforeInput = (event: InputEvent) => {
const text = String(event.data || '');
if (
event.defaultPrevented
|| event.isComposing
event.isComposing
|| event.inputType !== 'insertText'
|| !text
|| text.length > 8

View File

@@ -2461,6 +2461,52 @@ describe('QueryEditor external SQL save', () => {
});
});
it('suggests Oracle views after their metadata has loaded', async () => {
let renderer!: ReactTestRenderer;
autoFetchState.visible = true;
storeState.connections[0].config.type = 'oracle';
storeState.connections[0].config.database = 'APP';
backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'APP' }] });
backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [] });
backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => {
if (String(sql || '').includes('USER_VIEWS')) {
return { success: true, data: [{ view_name: 'PERSON_VIEW' }] };
}
return { success: true, data: [] };
});
await act(async () => {
renderer = create(<QueryEditor tab={createTab({ query: '', dbName: 'APP' })} />);
});
await act(async () => {
for (let i = 0; i < 12; i += 1) {
await Promise.resolve();
}
});
const sqlProvider = findSqlCompletionProvider();
expect(sqlProvider).toBeTruthy();
editorState.value = 'SELECT * FROM person';
editorState.latestOnChange?.(editorState.value);
const result = await sqlProvider.provideCompletionItems(
editorState.editor.getModel(),
{ lineNumber: 1, column: editorState.value.length + 1 },
);
expect(result.suggestions).toEqual(expect.arrayContaining([
expect.objectContaining({
label: 'PERSON_VIEW',
insertText: 'PERSON_VIEW',
detail: '视图 (APP)',
}),
]));
await act(async () => {
renderer.unmount();
});
});
it('fuzzy matches table names in FROM completion before column candidates', async () => {
let renderer!: ReactTestRenderer;
autoFetchState.visible = true;
@@ -8612,6 +8658,10 @@ describe('QueryEditor external SQL save', () => {
(storeState.connections[0].config as any).oceanBaseProtocol = 'oracle';
storeState.connections[0].config.user = 'dev';
storeState.connections[0].config.database = 'ORCLPDB1';
backendApp.DBGetTables.mockResolvedValueOnce({
success: true,
data: [{ Table: 'DEV.EDC_LOG' }],
});
backendApp.DBQueryMulti.mockResolvedValueOnce({
success: true,
data: [{ columns: ['WAFER_ID', ORACLE_ROWID_LOCATOR_COLUMN], rows: [{ WAFER_ID: 'R015Z10F08', [ORACLE_ROWID_LOCATOR_COLUMN]: 'AAAA' }] }],
@@ -8657,6 +8707,48 @@ describe('QueryEditor external SQL save', () => {
renderer?.unmount();
});
it('does not inject ROWID for an OceanBase Oracle synonym that is not a base table', async () => {
storeState.connections[0].config.type = 'oceanbase';
(storeState.connections[0].config as any).oceanBaseProtocol = 'oracle';
storeState.connections[0].config.user = 'B';
storeState.connections[0].config.database = 'ORCLPDB1';
backendApp.DBGetTables.mockResolvedValue({
success: true,
data: [{ Table: 'B.OTHER_TABLE' }],
});
backendApp.DBGetColumns.mockResolvedValueOnce({
success: true,
data: [{ name: 'ID', key: '' }, { name: 'NAME', key: '' }],
});
backendApp.DBQueryMulti.mockResolvedValueOnce({
success: true,
data: [{ columns: ['ID', 'NAME'], rows: [{ ID: 7, NAME: 'synonym-row' }] }],
});
let renderer!: ReactTestRenderer;
await act(async () => {
renderer = create(<QueryEditor tab={createTab({ dbName: 'ORCLPDB1', query: 'SELECT * FROM person' })} />);
});
await act(async () => {
await findButton(renderer!, '运行').props.onClick();
});
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
const executedSql = String(backendApp.DBQueryMulti.mock.calls[0][2]);
expect(executedSql).toMatch(/FROM\s+person/i);
expect(executedSql).not.toMatch(/\bROWID\b/i);
expect(dataGridState.latestProps?.editLocator).toMatchObject({
strategy: 'all-columns',
columns: ['ID', 'NAME'],
readOnly: false,
});
renderer?.unmount();
});
it('keeps OceanBase Oracle sequence queries out of the ROWNUM auto-limit wrapper', async () => {
const sql = 'SELECT IMP_BASICINFO.SEQ_HIS_AZA7.nextval FROM dual';
storeState.connections[0].config.type = 'oceanbase';

View File

@@ -17,6 +17,7 @@ import { getShortcutDisplayLabel, getShortcutPlatform, getShortcutPrimaryModifie
import { useAutoFetchVisibility } from '../utils/autoFetchVisibility';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import { isPostgresSchemaDialect } from '../utils/connectionDriverType';
import { resolveOceanBaseProtocolFromConfig } from '../utils/oceanBaseProtocol';
import { isOracleLikeDialect, resolveSqlDialect, resolveSqlFunctions, resolveSqlKeywords } from '../utils/sqlDialect';
import { applyQueryAutoLimit } from '../utils/queryAutoLimit';
import {
@@ -135,6 +136,7 @@ import {
getQueryEditorDecorationModelTextIfLightweight,
getQueryEditorObjectResolveText,
getTabQueryValue,
isOracleBaseTableReference,
isDocumentLevelShortcutTarget,
isQueryEditorPrimaryMouseButton,
normalizeCommentText,
@@ -199,6 +201,17 @@ const QUERY_EDITOR_AI_INLINE_DEBOUNCE_MS = 220;
const QUERY_EDITOR_AI_INLINE_CONTEXT_KEY = 'gonaviAiInlineSuggestionVisible';
const QUERY_EDITOR_IME_FALLBACK_DELAY_MS = 80;
const isOceanBaseOracleConnection = (config: any): boolean => {
const type = String(config?.type || '').trim().toLowerCase();
const driver = String(config?.driver || '').trim().toLowerCase();
if (type !== 'oceanbase' && driver !== 'oceanbase') return false;
try {
return resolveOceanBaseProtocolFromConfig(config || {}) === 'oracle';
} catch {
return false;
}
};
const normalizeQueryEditorInlineMemorySqlKey = (sql: string): string => (
String(sql || '')
.replace(/\r\n?/g, '\n')
@@ -4895,6 +4908,49 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
routineType: normalizeRoutineType(routine.routineType),
};
};
const getViewTypeLabel = (materialized: boolean) => (
materialized
? translate('query_editor.object_info.materialized_view')
: translate('sidebar.object.view')
);
const buildViewSuggestionMeta = (view: CompletionViewMeta) => {
const rawDbName = String(view.dbName || '').trim();
const rawViewName = String(view.viewName || '').trim();
const parsed = splitSchemaAndTable(rawViewName);
const schemaName = String(view.schemaName || parsed.schema || '').trim();
const objectName = String(parsed.table || rawViewName).trim();
const schemaMatchesDb = !!schemaName
&& !!rawDbName
&& schemaName.toLowerCase() === rawDbName.toLowerCase();
const isCurrentDb = rawDbName.toLowerCase() === getActiveCompletionDbName().toLowerCase();
const schemaQualifiedName = parsed.schema
? rawViewName
: (schemaName && !schemaMatchesDb ? `${schemaName}.${objectName}` : objectName);
const displayName = isCurrentDb && schemaMatchesDb
? objectName
: schemaQualifiedName;
const dbQualifiedLabel = rawDbName && !isCurrentDb
? `${rawDbName}.${displayName}`
: displayName;
const insertName = rawDbName && !isCurrentDb
? dbQualifiedLabel
: displayName;
return {
displayName,
dbQualifiedLabel,
insertText: quoteCompletionPath(insertName),
objectName,
schemaName,
};
};
const getViewSuggestionScope = (view: CompletionViewMeta, meta: ReturnType<typeof buildViewSuggestionMeta>) => {
const dbName = String(view.dbName || '').trim();
const schemaName = String(meta.schemaName || '').trim();
if (!schemaName || schemaName.toLowerCase() === dbName.toLowerCase()) {
return dbName;
}
return dbName ? `${dbName}.${schemaName}` : schemaName;
};
const buildConnConfig = () => {
const connId = sharedCurrentConnectionId;
@@ -5127,6 +5183,26 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
sortText: '0' + suggestionMeta.displayName
};
});
const viewSuggestions = [
...sharedViewsData.map((view) => ({ view, materialized: false })),
...sharedMaterializedViewsData.map((view) => ({ view, materialized: true })),
]
.filter(({ view }) => String(view.dbName || '').toLowerCase() === qualifierLower)
.map(({ view, materialized }) => ({ view, materialized, meta: buildViewSuggestionMeta(view) }))
.filter(({ view, meta }) => (
!prefix
|| meta.displayName.toLowerCase().startsWith(prefix)
|| meta.objectName.toLowerCase().startsWith(prefix)
|| String(view.viewName || '').toLowerCase().startsWith(prefix)
))
.map(({ view, materialized, meta }) => ({
label: meta.displayName,
kind: monaco.languages.CompletionItemKind.Class,
insertText: quoteCompletionPath(meta.displayName),
detail: `${getViewTypeLabel(materialized)} (${view.dbName})`,
range,
sortText: '05' + meta.displayName,
}));
const routineSuggestions = sharedRoutinesData
.filter((routine) => String(routine.dbName || '').toLowerCase() === qualifierLower)
.map((routine) => ({ routine, meta: buildRoutineSuggestionMeta(routine) }))
@@ -5145,7 +5221,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
range,
sortText: '1' + meta.displayName,
}));
return { suggestions: [...suggestions, ...routineSuggestions] };
return { suggestions: [...suggestions, ...viewSuggestions, ...routineSuggestions] };
}
// qualifier 是 schema如 dbo/public仅补全表名避免输入 dbo. 后再补成 dbo.dbo.table
@@ -5160,21 +5236,39 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
};
})
.filter(t => t.schema.toLowerCase() === qualifierLower && !!t.table);
const schemaViews = [
...sharedViewsData.map((view) => ({ view, materialized: false })),
...sharedMaterializedViewsData.map((view) => ({ view, materialized: true })),
]
.map(({ view, materialized }) => ({ view, materialized, meta: buildViewSuggestionMeta(view) }))
.filter(({ meta }) => meta.schemaName.toLowerCase() === qualifierLower && !!meta.objectName);
if (schemaTables.length > 0) {
if (schemaTables.length > 0 || schemaViews.length > 0) {
const filtered = prefix
? schemaTables.filter(t => t.table.toLowerCase().startsWith(prefix))
: schemaTables;
const suggestions = filtered.map(t => ({
label: t.table,
kind: monaco.languages.CompletionItemKind.Class,
insertText: quoteCompletionPart(t.table),
detail: appendCommentToDetail(`${translate('query_editor.object_info.table')} (${t.dbName}${t.schema ? '.' + t.schema : ''})`, t.comment),
documentation: buildCompletionDocumentation(t.comment),
range,
sortText: '0' + t.table
}));
const suggestions = [
...filtered.map(t => ({
label: t.table,
kind: monaco.languages.CompletionItemKind.Class,
insertText: quoteCompletionPart(t.table),
detail: appendCommentToDetail(`${translate('query_editor.object_info.table')} (${t.dbName}${t.schema ? '.' + t.schema : ''})`, t.comment),
documentation: buildCompletionDocumentation(t.comment),
range,
sortText: '0' + t.table
})),
...schemaViews
.filter(({ meta }) => !prefix || meta.objectName.toLowerCase().startsWith(prefix))
.map(({ view, materialized, meta }) => ({
label: meta.objectName,
kind: monaco.languages.CompletionItemKind.Class,
insertText: quoteCompletionPart(meta.objectName),
detail: `${getViewTypeLabel(materialized)} (${getViewSuggestionScope(view, meta)})`,
range,
sortText: '05' + meta.objectName,
})),
];
const routineSuggestions = sharedRoutinesData
.filter((routine) => {
const meta = buildRoutineSuggestionMeta(routine);
@@ -5424,6 +5518,38 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
};
});
const completionViews = [
...sharedViewsData.map((view) => ({ view, materialized: false })),
...sharedMaterializedViewsData.map((view) => ({ view, materialized: true })),
].filter(({ view }) => (
!expectsTableName
|| !currentDatabase
|| isCurrentCompletionDatabase(view.dbName || '')
));
const viewSuggestions = completionViews
.map(({ view, materialized }) => ({ view, materialized, meta: buildViewSuggestionMeta(view) }))
.filter(({ view, meta }) => (
includesWordPrefix(meta.dbQualifiedLabel)
|| includesWordPrefix(meta.displayName)
|| includesWordPrefix(meta.objectName)
|| includesWordPrefix(view.viewName || '')
))
.map(({ view, materialized, meta }) => {
const isCurrentDb = isCurrentCompletionDatabase(view.dbName || '');
const label = isCurrentDb ? meta.displayName : meta.dbQualifiedLabel;
return {
label,
kind: monaco.languages.CompletionItemKind.Class,
insertText: meta.insertText,
detail: `${getViewTypeLabel(materialized)} (${getViewSuggestionScope(view, meta)})`,
range,
sortText: (isCurrentDb ? sortGroups.tableCurrent : sortGroups.tableOther)
+ '1'
+ getPrefixMatchRank(label, meta.displayName, meta.objectName, view.viewName || '')
+ label,
};
});
const routineSuggestions = sharedRoutinesData
.map((routine) => ({ routine, meta: buildRoutineSuggestionMeta(routine) }))
.filter(({ routine, meta }) => {
@@ -5492,6 +5618,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
...keywordSuggestions,
...funcSuggestions,
...tableSuggestions,
...viewSuggestions,
...dbSuggestions,
...routineSuggestions,
...relevantColumns,
@@ -5499,6 +5626,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
: [
...relevantColumns, // FROM 表的列最优先
...tableSuggestions, // 表次之
...viewSuggestions, // 视图和表同属可查询对象
...dbSuggestions, // 数据库
...routineSuggestions, // 存储过程/函数
...funcSuggestions, // 内置函数
@@ -6643,6 +6771,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
.length || sourceStatements.length;
const forceReadOnlyResult = connCaps.forceReadOnlyQueryResult;
const oceanBaseOracleConnection = isOceanBaseOracleConnection(config);
const defaultOracleSchema = isOracleLikeDialect(normalizedDbType)
? resolveOracleLikeDefaultSchemaName(config)
: '';
@@ -6694,8 +6823,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
}
};
const executedSourceStatements: string[] = [];
const allowOracleRowIDByStatement: boolean[] = [];
for (const statement of sourceStatements) {
let executableStatement = statement;
let allowOracleRowID = !oceanBaseOracleConnection;
if (isOracleLikeDialect(normalizedDbType)) {
const leadingTable = matchLeadingSelectTableReference(statement);
if (leadingTable) {
@@ -6706,6 +6837,12 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
let exactQualifiedTable: string | undefined;
for (const oracleLookupDbName of oracleLookupDbCandidates) {
const oracleTables = oracleLookupDbName ? await getOracleTablesForDb(oracleLookupDbName) : [];
if (
oceanBaseOracleConnection
&& isOracleBaseTableReference(statement, oracleLookupDbName, oracleTables)
) {
allowOracleRowID = true;
}
exactQualifiedTable = resolveOracleExactCaseTableReference(statement, oracleLookupDbName, oracleTables, {
qualifyUnqualified: Boolean(
leadingSegments.length === 1
@@ -6723,6 +6860,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
}
}
executedSourceStatements.push(executableStatement);
allowOracleRowIDByStatement.push(allowOracleRowID);
}
const statementPlans: QueryStatementPlan[] = [];
for (let index = 0; index < sourceStatements.length; index += 1) {
@@ -6735,6 +6873,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
currentDb,
config,
forceReadOnly: forceReadOnlyResult,
allowOracleRowID: allowOracleRowIDByStatement[index],
}));
} catch (planError) {
// 行定位计划失败绝不能阻断查询执行,兜底裸计划保证结果页始终呈现。

View File

@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
import {
collectQueryEditorReferencedDatabaseNames,
isOracleBaseTableReference,
resolveOracleLikeDefaultSchemaName,
resolveOracleLikeExecutionSchemaName,
resolveOracleLikeLookupSchemaCandidates,
@@ -58,6 +59,17 @@ describe('QueryEditorHelpers Oracle-like execution schema', () => {
expect(resolveOracleLikeExecutionSchemaName(config, 'APP_OWNER')).toBe('APP_OWNER');
expect(resolveOracleLikeLookupSchemaCandidates(config, 'APP_OWNER')).toEqual(['APP_OWNER']);
});
it('recognizes base tables but not synonyms when deciding whether ROWID is safe', () => {
const baseTables = [
{ dbName: 'A', tableName: 'A.PERSON' },
{ dbName: 'B', tableName: 'B.ORDERS' },
];
expect(isOracleBaseTableReference('SELECT * FROM A.person', 'A', baseTables)).toBe(true);
expect(isOracleBaseTableReference('SELECT * FROM person', 'B', baseTables)).toBe(false);
expect(isOracleBaseTableReference('SELECT * FROM person_view', 'B', baseTables)).toBe(false);
});
});
describe('QueryEditorHelpers qualified navigation (MySQL db.table + PG schema.table)', () => {

View File

@@ -1240,6 +1240,33 @@ export const rewriteLeadingSelectTableReference = (sql: string, replacement: str
return `${match.prefix}${replacement}${match.suffix}`;
};
export const isOracleBaseTableReference = (
statement: string,
currentDb: string,
tables: CompletionTableMeta[],
): boolean => {
const leadingTable = matchLeadingSelectTableReference(statement);
if (!leadingTable) return false;
const segments = splitQueryIdentifierPathSegments(leadingTable.tableText);
if (segments.length === 0 || segments.length > 2) return false;
const explicitSchemaName = segments.length === 2 ? String(segments[0]?.value || '').trim() : '';
const objectName = String(segments[segments.length - 1]?.value || '').trim();
const targetSchemaName = explicitSchemaName || String(currentDb || '').trim();
if (!objectName || !targetSchemaName) return false;
const normalizedSchemaName = targetSchemaName.toLowerCase();
return tables.some((table) => {
if (String(table.dbName || '').trim().toLowerCase() !== normalizedSchemaName) return false;
const parsed = splitSidebarQualifiedName(String(table.tableName || ''));
const tableObjectName = String(parsed.objectName || table.tableName || '').trim();
const tableSchemaName = String(parsed.schemaName || table.dbName || '').trim();
if (tableObjectName.toLowerCase() !== objectName.toLowerCase()) return false;
return !explicitSchemaName || tableSchemaName.toLowerCase() === normalizedSchemaName;
});
};
export const resolveOracleExactCaseTableReference = (
statement: string,
currentDb: string,
@@ -2330,6 +2357,7 @@ export const resolveQueryLocatorPlan = async ({
currentDb,
config,
forceReadOnly,
allowOracleRowID = true,
}: {
statement: string;
originalStatement?: string;
@@ -2337,6 +2365,7 @@ export const resolveQueryLocatorPlan = async ({
currentDb: string;
config: any;
forceReadOnly: boolean;
allowOracleRowID?: boolean;
}): Promise<QueryStatementPlan> => {
const plan: QueryStatementPlan = {
originalSql: originalStatement || statement,
@@ -2452,7 +2481,7 @@ export const resolveQueryLocatorPlan = async ({
const uniqueKeyGroup = uniqueKeyGroups.find((group) => group.length > 0);
if (uniqueKeyGroup) {
plan.editLocator = buildColumnLocator('unique-key', uniqueKeyGroup);
} else if (isOracleLikeDialect(dbType)) {
} else if (allowOracleRowID && isOracleLikeDialect(dbType)) {
needsOracleRowIDExpression = true;
plan.editLocator = {
strategy: 'oracle-rowid',