diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index b1ee05cea6..ab4fdba2af 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -5104,6 +5104,7 @@ async function hydrateVisibleLargeValuePreviews(generation: number) { whereInput: predicates.map((predicate) => `(${predicate})`).join(" OR "), limit: requests.size, offset: 0, + includeRowId: usesSyntheticRowIdKey(resolvedDatabaseType.value, tableMeta.primaryKeys, tableMeta.tableType), }); if (!visibleLargeValuePreviewActive || generation !== visibleLargeValuePreviewRequestedGeneration || props.result !== sourceResult) return; const connection = connectionStore.getConfig(props.connectionId); @@ -5270,6 +5271,7 @@ async function fetchLargeValueRequestChunk(columnIndex: number, requests: LargeV whereInput: predicates.map((predicate) => `(${predicate})`).join(" OR "), limit: requests.length, offset: 0, + includeRowId: usesSyntheticRowIdKey(resolvedDatabaseType.value, tableMeta.primaryKeys, tableMeta.tableType), }); const connection = props.connectionId ? connectionStore.getConfig(props.connectionId) : undefined; const results = await api.executeMulti(props.connectionId!, props.executionDatabase ?? props.database ?? "", sql, undefined, uuid(), { diff --git a/apps/desktop/src/components/grid/__tests__/DataGridLargeValueReloadSql.spec.ts b/apps/desktop/src/components/grid/__tests__/DataGridLargeValueReloadSql.spec.ts new file mode 100644 index 0000000000..d4c6107f4b --- /dev/null +++ b/apps/desktop/src/components/grid/__tests__/DataGridLargeValueReloadSql.spec.ts @@ -0,0 +1,29 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const dataGridSource = readFileSync(new URL("../DataGrid.vue", import.meta.url), "utf8"); + +function functionSource(name: string, nextName: string): string { + const start = dataGridSource.indexOf(`function ${name}`); + const relativeEnd = dataGridSource.slice(start).search(new RegExp(`\\n(?:async\\s+)?function\\s+${nextName}\\b`)); + expect(start).toBeGreaterThanOrEqual(0); + expect(relativeEnd).toBeGreaterThan(0); + return dataGridSource.slice(start, start + relativeEnd); +} + +describe("DataGrid large-value reload SQL", () => { + it("projects the synthetic rowid through the rowid-wrapped select on full-value reloads", () => { + const fetchChunkSource = functionSource("fetchLargeValueRequestChunk", "resolveLargeValueCells"); + + // Keyless Oracle tables address rows via the hidden __DBX_ROWID alias; the + // reload must opt into the ROWIDTOCHAR inline view so the generated SQL + // never references __DBX_ROWID as a base-table column (ORA-00904). + expect(fetchChunkSource).toContain("includeRowId: usesSyntheticRowIdKey(resolvedDatabaseType.value, tableMeta.primaryKeys, tableMeta.tableType)"); + }); + + it("keeps the visible-preview hydration consistent with the synthetic key", () => { + const hydrateSource = functionSource("hydrateVisibleLargeValuePreviews", "runVisibleLargeValuePreviewHydration"); + + expect(hydrateSource).toContain("includeRowId: usesSyntheticRowIdKey(resolvedDatabaseType.value, tableMeta.primaryKeys, tableMeta.tableType)"); + }); +}); diff --git a/crates/dbx-core/src/sql_dialect/table_select.rs b/crates/dbx-core/src/sql_dialect/table_select.rs index f4338ccfcd..a8155ea155 100644 --- a/crates/dbx-core/src/sql_dialect/table_select.rs +++ b/crates/dbx-core/src/sql_dialect/table_select.rs @@ -276,7 +276,21 @@ pub fn build_table_data_select_sql_with_database( if options.columns.is_empty() { "*".to_string() } else { - format!("\"{DBX_ROWID_COLUMN}\", {rownum_select_columns}") + // Callers that address rows by the synthetic key may list it among + // the requested columns; the leading projection already supplies + // it from the inline view, so drop the duplicate. + let rest = options + .columns + .iter() + .filter(|column| !column.eq_ignore_ascii_case(DBX_ROWID_COLUMN)) + .map(|column| quote_table_identifier(database_type, column)) + .collect::>() + .join(", "); + if rest.is_empty() { + format!("\"{DBX_ROWID_COLUMN}\"") + } else { + format!("\"{DBX_ROWID_COLUMN}\", {rest}") + } } } else { rownum_select_columns.clone() diff --git a/crates/dbx-core/src/sql_dialect/tests.rs b/crates/dbx-core/src/sql_dialect/tests.rs index bab6111583..82c51b3b55 100644 --- a/crates/dbx-core/src/sql_dialect/tests.rs +++ b/crates/dbx-core/src/sql_dialect/tests.rs @@ -1333,6 +1333,31 @@ fn builds_oracle_and_neo4j_table_data_queries() { ); } +#[test] +fn builds_oracle_rowid_wrapped_large_value_reload_sql() { + // The data-grid large-value reload selects the synthetic rowid key plus the + // target column with a rowid equality filter; `__DBX_ROWID` exists only as + // the inline-view alias, never as a base-table column (ORA-00904). + assert_eq!( + build_table_data_select_sql(TableDataSelectSqlOptions { + database_type: Some(DatabaseType::Oracle), + schema: Some("APP".to_string()), + table_name: "T_TEST".to_string(), + table_type: Some("TABLE".to_string()), + primary_keys: vec![DBX_ROWID_COLUMN.to_string()], + columns: vec![DBX_ROWID_COLUMN.to_string(), "ELM_CONTENT".to_string()], + where_input: Some("ROWIDTOCHAR(ROWID) = 'AAAFd1AAFAAAABSAA/'".to_string()), + fallback_order_columns: Vec::new(), + order_by: None, + limit: Some(1), + offset: Some(0), + include_row_id: true, + ..Default::default() + }), + "SELECT \"__DBX_ROWID\", \"ELM_CONTENT\" FROM (SELECT ROWIDTOCHAR(t.ROWID) AS \"__DBX_ROWID\", t.* FROM \"APP\".\"T_TEST\" t WHERE (ROWIDTOCHAR(ROWID) = 'AAAFd1AAFAAAABSAA/')) WHERE ROWNUM <= 1" + ); +} + #[test] fn oracle_view_first_page_preserves_filter_and_sort_without_rownum() { assert_eq!(