fix(oracle): skip rowid for view data queries

This commit is contained in:
t8y2 2026-07-04 17:07:43 +08:00
parent 7e7c4944e9
commit 223ecbec20
16 changed files with 78 additions and 31 deletions

View File

@ -1341,6 +1341,7 @@ async function handleQuickOpenSelect(item: any) {
database: item.database,
schema: item.schema,
tableName: item.objectName || item.tableName,
tableType: item.type === "view" ? "VIEW" : item.type === "materialized_view" ? "MATERIALIZED_VIEW" : "TABLE",
});
} else if (item.type === "procedure" || item.type === "function" || item.type === "sequence" || item.type === "package" || item.type === "package-body") {
// Open the object source in a source tab
@ -1857,6 +1858,7 @@ onUnmounted(() => {
database: activeTab.database,
schema: target.schema,
tableName: target.tableName,
tableType: target.tableType,
})
"
@object-schema-change="(schema) => activeTab && queryStore.updateSchema(activeTab.id, schema)"

View File

@ -2461,7 +2461,7 @@ const persistedColumnOrderKeys = ref<string[]>([]);
const displayableColumnIndexes = computed(() =>
props.result.columns
.map((column, index) => ({ column, index }))
.filter(({ column }) => !isHiddenGridColumn(props.databaseType, column, props.tableMeta?.primaryKeys ?? []))
.filter(({ column }) => !isHiddenGridColumn(props.databaseType, column, props.tableMeta?.primaryKeys ?? [], props.tableMeta?.tableType))
.map(({ index }) => index),
);
const goToColumnItems = computed(() =>
@ -5004,7 +5004,7 @@ async function applyOrderBySearch() {
orderBy: orderByClause,
limit: pageSize.value,
whereInput: currentWhereInput(),
includeRowId: usesSyntheticRowIdKey(resolvedDatabaseType.value, tableMeta.primaryKeys),
includeRowId: usesSyntheticRowIdKey(resolvedDatabaseType.value, tableMeta.primaryKeys, tableMeta.tableType),
});
await props.onExecuteSql(sql);
} catch (e: any) {
@ -5035,7 +5035,7 @@ async function applyWhereFilter() {
orderBy: orderByInput.value.trim() || (sortCol.value ? `${queryColumnRef(sortCol.value)} ${sortDir.value.toUpperCase()}` : undefined),
limit: pageSize.value,
whereInput,
includeRowId: usesSyntheticRowIdKey(resolvedDatabaseType.value, tableMeta.primaryKeys),
includeRowId: usesSyntheticRowIdKey(resolvedDatabaseType.value, tableMeta.primaryKeys, tableMeta.tableType),
});
await props.onExecuteSql(sql);
} catch (e: any) {

View File

@ -51,6 +51,7 @@ const emit = defineEmits<{
database: string;
schema?: string;
tableName: string;
tableType?: string;
columnName?: string;
},
];
@ -60,6 +61,7 @@ const emit = defineEmits<{
database: string;
schema?: string;
tableName: string;
tableType?: string;
whereInput?: string;
},
];

View File

@ -136,7 +136,7 @@ const emit = defineEmits<{
viewTableData: [tableName: string];
viewTableDdl: [tableName: string];
editTableStructure: [tableName: string];
openObjectTable: [target: { tableName: string; schema?: string }];
openObjectTable: [target: { tableName: string; schema?: string; tableType?: string }];
objectSchemaChange: [schema: string | undefined];
structureEditorSaved: [commentChanged: boolean];
structureEditorClose: [];

View File

@ -30,6 +30,7 @@ const emit = defineEmits<{
database: string;
schema?: string;
tableName: string;
tableType?: string;
columnName?: string;
},
];
@ -268,6 +269,7 @@ function openItemTarget(item: FieldLineageItem) {
database: props.prefillDatabase,
schema: item.schema || props.prefillSchema,
tableName: item.table,
tableType: item.kind === "viewReference" ? "VIEW" : "TABLE",
columnName: item.column || props.prefillColumn,
});
}

View File

@ -96,7 +96,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
openTable: [target: { tableName: string; schema?: string }];
openTable: [target: { tableName: string; schema?: string; tableType?: string }];
schemaChange: [schema: string | undefined];
}>();
@ -661,7 +661,7 @@ async function saveFileContent(content: string, defaultFileName: string, filterN
}
function openViewData(row: ObjectBrowserRow) {
emit("openTable", { tableName: row.name, schema: row.schema });
emit("openTable", { tableName: row.name, schema: row.schema, tableType: row.type });
}
function openStructureEditor(row: ObjectBrowserRow) {

View File

@ -29,6 +29,7 @@ const emit = defineEmits<{
database: string;
schema?: string;
tableName: string;
tableType?: string;
whereInput?: string;
},
];
@ -45,6 +46,7 @@ type SearchResultItem = {
id: string;
schema?: string;
tableName: string;
tableType?: string;
matchedColumns: string[];
preview: string;
whereInput: string;
@ -223,6 +225,7 @@ async function searchTable(task: SearchTableTask, databaseType: DatabaseType, cu
id: `${tableLabel}:${rowIndex}:${results.value.length}`,
schema: task.schema,
tableName: task.table.name,
tableType: task.table.table_type,
matchedColumns,
preview: rowPreview(result.columns, row, matchedColumns),
whereInput,
@ -257,6 +260,7 @@ function openResult(item: SearchResultItem) {
database: props.prefillDatabase,
schema: item.schema,
tableName: item.tableName,
tableType: item.tableType,
whereInput: item.whereInput,
});
}

View File

@ -1283,7 +1283,7 @@ async function openData() {
const columns = cachedTableMeta?.columns ?? [];
const primaryKeys = cachedTableMeta?.primaryKeys ?? [];
const includeRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys);
const includeRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys, tableType);
const sql = await buildTableSelectSql({
databaseType: effectiveDbType,
schema: tableSchema,

View File

@ -33,7 +33,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
const effectiveDbType = effectiveDatabaseTypeForConnection(config);
const tableMeta = tableMetaForDataTab(tab);
const primaryKeys = tab.tableMeta ? tab.tableMeta.primaryKeys : (tableMeta?.primaryKeys ?? []);
const useRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys);
const useRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys, tableMeta?.tableType);
return buildTableSelectSql({
databaseType: effectiveDbType,
schema: tableMeta?.schema,

View File

@ -14,6 +14,7 @@ export type NavigationTarget = {
database: string;
schema?: string;
tableName: string;
tableType?: string;
columnName?: string;
whereInput?: string;
};
@ -55,13 +56,15 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
if (!config) throw new Error("Connection config not found");
const effectiveDbType = effectiveDatabaseTypeForConnection(config);
const querySchema = metadataSchemaForConnection(config, target.database, target.schema);
const targetTableType = target.tableType ?? "TABLE";
if (config.db_type === "neo4j") {
const columns = await api.getColumns(target.connectionId, target.database, querySchema, target.tableName);
const primaryKeys = editableRowIdentifierColumns(effectiveDbType, columns);
const primaryKeys = editableRowIdentifierColumns(effectiveDbType, columns, undefined, targetTableType);
const sql = await buildTableSelectSql({
databaseType: effectiveDbType,
schema: target.schema,
tableName: target.tableName,
tableType: targetTableType,
columns: columns.map((column) => column.name),
primaryKeys,
whereInput: target.whereInput,
@ -71,7 +74,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
queryStore.setTableMeta(tabId, {
schema: target.schema,
tableName: target.tableName,
tableType: "TABLE",
tableType: targetTableType,
columns,
primaryKeys,
});
@ -82,6 +85,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
databaseType: effectiveDbType,
schema: target.schema,
tableName: target.tableName,
tableType: targetTableType,
whereInput: target.whereInput,
limit: pageLimit,
});
@ -89,7 +93,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
queryStore.setTableMeta(tabId, {
schema: target.schema,
tableName: target.tableName,
tableType: "TABLE",
tableType: targetTableType,
columns: [],
primaryKeys: [],
});
@ -106,6 +110,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
databaseType: effectiveDbType,
schema: target.schema,
tableName: target.tableName,
tableType: targetTableType,
whereInput: target.whereInput,
limit: 0,
});
@ -115,12 +120,12 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
try {
const columns = await api.getColumns(target.connectionId, target.database, querySchema, target.tableName);
const indexes = await api.listIndexes(target.connectionId, target.database, querySchema, target.tableName).catch(() => []);
const primaryKeys = editableRowIdentifierColumns(effectiveDbType, columns, indexes);
const useRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys);
const primaryKeys = editableRowIdentifierColumns(effectiveDbType, columns, indexes, targetTableType);
const useRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys, targetTableType);
queryStore.setTableMeta(tabId, {
schema: target.schema,
tableName: target.tableName,
tableType: "TABLE",
tableType: targetTableType,
columns,
primaryKeys,
});
@ -129,6 +134,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
databaseType: effectiveDbType,
schema: target.schema,
tableName: target.tableName,
tableType: targetTableType,
whereInput: target.whereInput,
primaryKeys,
columns: columns.map((column) => column.name),

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { DBX_ROWID_COLUMN, canEditExistingTableRows, canUseKeylessRowPredicate, editablePrimaryKeys, editableRowIdentifierColumns, isClickHouseExistingRowReadonlyColumn, isTableDataEditable, supportsDataGridTransaction } from "@/lib/table/tableEditing";
import { DBX_ROWID_COLUMN, canEditExistingTableRows, canUseKeylessRowPredicate, editablePrimaryKeys, editableRowIdentifierColumns, isClickHouseExistingRowReadonlyColumn, isTableDataEditable, supportsDataGridTransaction, usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
import type { ColumnInfo, IndexInfo } from "@/types/database";
function column(name: string, isPrimaryKey = false): ColumnInfo {
@ -33,6 +33,11 @@ describe("tableEditing", () => {
expect(isTableDataEditable("oracle", [DBX_ROWID_COLUMN], "VIEW")).toBe(false);
});
it("does not include Oracle ROWID for view data tabs", () => {
expect(usesSyntheticRowIdKey("oracle", [DBX_ROWID_COLUMN], "VIEW")).toBe(false);
expect(usesSyntheticRowIdKey("oracle", [DBX_ROWID_COLUMN], "MATERIALIZED_VIEW")).toBe(false);
});
it("allows keyless row predicates only for databases that support them", () => {
expect(canUseKeylessRowPredicate("postgres", [])).toBe(true);
expect(canUseKeylessRowPredicate("mysql", [])).toBe(true);

View File

@ -72,13 +72,14 @@ export function hiveTablePropertiesIndicateTransactional(result: { rows: readonl
});
}
export function usesSyntheticRowIdKey(databaseType: DatabaseType | undefined, primaryKeys: string[]): boolean {
export function usesSyntheticRowIdKey(databaseType: DatabaseType | undefined, primaryKeys: string[], tableType?: string): boolean {
if (isViewTableType(tableType)) return false;
return primaryKeys.length === 1 && ((databaseType === "oracle" && primaryKeys[0].toUpperCase() === DBX_ROWID_COLUMN) || (databaseType === "neo4j" && primaryKeys[0] === DBX_NEO4J_ELEMENT_ID_COLUMN));
}
export function isHiddenGridColumn(databaseType: DatabaseType | undefined, column: string, primaryKeys: string[]): boolean {
export function isHiddenGridColumn(databaseType: DatabaseType | undefined, column: string, primaryKeys: string[], tableType?: string): boolean {
if (databaseType === "neo4j" && column === DBX_NEO4J_ELEMENT_ID_COLUMN) return true;
return usesSyntheticRowIdKey(databaseType, primaryKeys) && column.toUpperCase() === DBX_ROWID_COLUMN;
return usesSyntheticRowIdKey(databaseType, primaryKeys, tableType) && column.toUpperCase() === DBX_ROWID_COLUMN;
}
export function isTdengineExistingRowReadonlyColumn(databaseType: DatabaseType | undefined, column: string, columns: ColumnInfo[]): boolean {

View File

@ -1687,10 +1687,12 @@ export const useQueryStore = defineStore("query", () => {
elapsed: elapsed?.(),
});
const indexes = await api.listIndexes(tab.connectionId, tab.database, metadataSchema, metadataTableName).catch(() => []);
const primaryKeys = editableRowIdentifierColumns(dbType as DatabaseType, columns, indexes);
const tableType = tab.tableMeta?.tableType;
const primaryKeys = editableRowIdentifierColumns(dbType as DatabaseType, columns, indexes, tableType);
const tableMeta = {
schema: metadataSchema || undefined,
tableName: metadataTableName,
tableType,
columns,
primaryKeys,
};

View File

@ -29,8 +29,13 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String
};
let order_by = options.order_by.as_deref().filter(|order| !order.trim().is_empty()).or(default_order_by.as_deref());
let order = order_by.map(|order_by| format!(" ORDER BY {order_by}")).unwrap_or_default();
// Oracle join views can raise ORA-01445 when ROWID is selected; keep the
// synthetic ROWID fallback scoped to base-table reads.
let include_oracle_row_id = options.include_row_id
&& database_type == Some(DatabaseType::Oracle)
&& !is_view_table_type(options.table_type.as_deref());
let select_columns = if options.include_row_id && database_type == Some(DatabaseType::Oracle) {
let select_columns = if include_oracle_row_id {
format!("ROWIDTOCHAR(t.ROWID) AS \"{DBX_ROWID_COLUMN}\", t.*")
} else {
build_select_columns(
@ -40,7 +45,7 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String
)
};
let rownum_select_columns = quoted_table_columns_or_star(database_type, &options.columns);
let page_select_columns = if options.include_row_id && database_type == Some(DatabaseType::Oracle) {
let page_select_columns = if include_oracle_row_id {
if options.columns.is_empty() {
"*".to_string()
} else {
@ -49,11 +54,8 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String
} else {
rownum_select_columns.clone()
};
let table_alias = if options.include_row_id && database_type.is_some_and(uses_fetch_first) {
format!("{table} t")
} else {
table
};
let table_alias =
if include_oracle_row_id && database_type.is_some_and(uses_fetch_first) { format!("{table} t") } else { table };
match table_pagination_strategy(database_type) {
TablePaginationStrategy::IrisTop => {
@ -88,11 +90,8 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String
)
}
TablePaginationStrategy::Rownum => {
let rownum_inner_select_columns = if options.include_row_id && database_type == Some(DatabaseType::Oracle) {
&select_columns
} else {
&rownum_select_columns
};
let rownum_inner_select_columns =
if include_oracle_row_id { &select_columns } else { &rownum_select_columns };
build_rownum_table_select_sql(
&table_alias,
&where_clause,
@ -136,6 +135,10 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String
}
}
fn is_view_table_type(table_type: Option<&str>) -> bool {
table_type.is_some_and(|value| value.to_ascii_uppercase().contains("VIEW"))
}
pub fn build_table_select_sql(options: TableSelectSqlOptions<'_>) -> String {
let database_type = options.database_type;
let table = qualified_table_name(database_type, options.schema, options.table_name);

View File

@ -699,6 +699,23 @@ fn builds_oracle_and_neo4j_table_data_queries() {
}),
"SELECT \"__DBX_ROWID\", \"ID\", \"NAME\" FROM (SELECT ROWIDTOCHAR(t.ROWID) AS \"__DBX_ROWID\", t.* FROM \"DBXTEST\".\"DBX_LOAD_TABLE_006\" t) WHERE ROWNUM <= 100"
);
assert_eq!(
build_table_data_select_sql(TableDataSelectSqlOptions {
database_type: Some(DatabaseType::Oracle),
schema: Some("DBXTEST".to_string()),
table_name: "DBX_JOIN_VIEW".to_string(),
table_type: Some("VIEW".to_string()),
primary_keys: vec![DBX_ROWID_COLUMN.to_string()],
columns: vec!["ID".to_string(), "NAME".to_string()],
fallback_order_columns: Vec::new(),
order_by: None,
limit: Some(100),
offset: None,
where_input: None,
include_row_id: true,
}),
"SELECT \"ID\", \"NAME\" FROM (SELECT \"ID\", \"NAME\" FROM \"DBXTEST\".\"DBX_JOIN_VIEW\") WHERE ROWNUM <= 100"
);
assert_eq!(
build_table_data_select_sql(TableDataSelectSqlOptions {
database_type: Some(DatabaseType::Neo4j),

View File

@ -134,6 +134,8 @@ test("keeps TDengine existing row identity and tag columns read-only", () => {
test("detects the synthetic Oracle ROWID key case", () => {
assert.equal(usesSyntheticRowIdKey("oracle", [DBX_ROWID_COLUMN]), true);
assert.equal(usesSyntheticRowIdKey("oracle", [DBX_ROWID_COLUMN.toLowerCase()]), true);
assert.equal(usesSyntheticRowIdKey("oracle", [DBX_ROWID_COLUMN], "VIEW"), false);
assert.equal(usesSyntheticRowIdKey("oracle", [DBX_ROWID_COLUMN], "MATERIALIZED_VIEW"), false);
assert.equal(usesSyntheticRowIdKey("postgres", [DBX_ROWID_COLUMN]), false);
assert.equal(usesSyntheticRowIdKey("oracle", ["ID"]), false);
assert.equal(usesSyntheticRowIdKey("neo4j", [DBX_NEO4J_ELEMENT_ID_COLUMN]), true);
@ -141,6 +143,7 @@ test("detects the synthetic Oracle ROWID key case", () => {
test("hides only the synthetic Oracle ROWID grid column", () => {
assert.equal(isHiddenGridColumn("oracle", DBX_ROWID_COLUMN, [DBX_ROWID_COLUMN]), true);
assert.equal(isHiddenGridColumn("oracle", DBX_ROWID_COLUMN, [DBX_ROWID_COLUMN], "VIEW"), false);
assert.equal(isHiddenGridColumn("oracle", "ROWID", [DBX_ROWID_COLUMN]), false);
assert.equal(isHiddenGridColumn("mysql", DBX_ROWID_COLUMN, [DBX_ROWID_COLUMN]), false);
assert.equal(isHiddenGridColumn("neo4j", DBX_NEO4J_ELEMENT_ID_COLUMN, [DBX_NEO4J_ELEMENT_ID_COLUMN]), true);