mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-02 02:34:42 +08:00
fix(xugu): enable safe row editing without primary keys
This commit is contained in:
@@ -112,7 +112,7 @@ import {
|
||||
isClickHouseExistingRowReadonlyColumn,
|
||||
isHiddenGridColumn,
|
||||
isTdengineExistingRowReadonlyColumn,
|
||||
usesSyntheticRowIdKey,
|
||||
shouldIncludeSyntheticRowId,
|
||||
} from "@/lib/table/tableEditing";
|
||||
import { buildDataGridColumnDistinctValuesSql, buildDataGridConditionalUpdateSql, buildDataGridContextFilterCondition, buildDataGridCountSql, buildHiveTablePropertiesSql, type DataGridContextFilterMode } from "@/lib/dataGrid/dataGridSql";
|
||||
import {
|
||||
@@ -4349,7 +4349,7 @@ async function refreshSavedRows(request: { dirtyRows: ReadonlyMap<number, Readon
|
||||
...tableDataLargeValuePreviewOptions(resolvedDatabaseType.value, tableMeta.columns, tableMeta.primaryKeys, pageSize.value),
|
||||
whereInput: identityConditions.join(" OR "),
|
||||
limit: planResult.plan.sourceIndexes.length + 1,
|
||||
includeRowId: usesSyntheticRowIdKey(resolvedDatabaseType.value, tableMeta.primaryKeys, tableMeta.tableType),
|
||||
includeRowId: shouldIncludeSyntheticRowId(resolvedDatabaseType.value, tableMeta.primaryKeys, tableMeta.tableType),
|
||||
});
|
||||
const refreshed = await api.executeQuery(connectionId, props.executionDatabase ?? props.database ?? "", sql, tableMeta.schema ?? props.schema, undefined, {
|
||||
maxRows: planResult.plan.sourceIndexes.length + 1,
|
||||
@@ -5542,7 +5542,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),
|
||||
includeRowId: shouldIncludeSyntheticRowId(resolvedDatabaseType.value, tableMeta.primaryKeys, tableMeta.tableType),
|
||||
});
|
||||
if (!visibleLargeValuePreviewActive || generation !== visibleLargeValuePreviewRequestedGeneration || props.result !== sourceResult) return;
|
||||
const connection = connectionStore.getConfig(props.connectionId);
|
||||
@@ -5709,7 +5709,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),
|
||||
includeRowId: shouldIncludeSyntheticRowId(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(), {
|
||||
@@ -7120,7 +7120,7 @@ async function applyOrderBySearch() {
|
||||
injectDefaultTimeSeriesWhere: true,
|
||||
limit: pageSize.value,
|
||||
whereInput: currentWhereInput(),
|
||||
includeRowId: usesSyntheticRowIdKey(resolvedDatabaseType.value, tableMeta.primaryKeys, tableMeta.tableType),
|
||||
includeRowId: shouldIncludeSyntheticRowId(resolvedDatabaseType.value, tableMeta.primaryKeys, tableMeta.tableType),
|
||||
});
|
||||
markConditionInputsApplied();
|
||||
await props.onExecuteSql(sql);
|
||||
@@ -7159,7 +7159,7 @@ async function applyWhereFilter() {
|
||||
limit: pageSize.value,
|
||||
injectDefaultTimeSeriesWhere: true,
|
||||
whereInput,
|
||||
includeRowId: usesSyntheticRowIdKey(resolvedDatabaseType.value, tableMeta.primaryKeys, tableMeta.tableType),
|
||||
includeRowId: shouldIncludeSyntheticRowId(resolvedDatabaseType.value, tableMeta.primaryKeys, tableMeta.tableType),
|
||||
});
|
||||
markConditionInputsApplied();
|
||||
await props.onExecuteSql(sql);
|
||||
|
||||
+2
-2
@@ -18,12 +18,12 @@ describe("DataGrid large-value reload SQL", () => {
|
||||
// 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)");
|
||||
expect(fetchChunkSource).toContain("includeRowId: shouldIncludeSyntheticRowId(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)");
|
||||
expect(hydrateSource).toContain("includeRowId: shouldIncludeSyntheticRowId(resolvedDatabaseType.value, tableMeta.primaryKeys, tableMeta.tableType)");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,7 +118,10 @@ vi.mock("@/lib/backend/debugLog", () => ({ appendDebugLog: vi.fn(), isDebugLoggi
|
||||
// dataTabOpenPolicy 使用真实实现,覆盖设置开关对应的复用范围
|
||||
vi.mock("@/lib/sidebar/treeNodeContext", () => ({ hasTreeNodeDatabaseContext: () => true }));
|
||||
vi.mock("@/lib/table/tableSelectSql", () => ({ buildTableSelectSql: mocks.buildTableSelectSql }));
|
||||
vi.mock("@/lib/table/tableEditing", () => ({ usesSyntheticRowIdKey: () => false }));
|
||||
vi.mock("@/lib/table/tableEditing", () => ({
|
||||
usesSyntheticRowIdKey: () => false,
|
||||
shouldIncludeSyntheticRowId: () => false,
|
||||
}));
|
||||
vi.mock("@/lib/table/tableOpenPageLimit", () => ({ tableOpenPageLimit: () => 100 }));
|
||||
vi.mock("@/lib/tabs/dataTabActivation", () => ({ canActivateExistingDataTableTab: () => false }));
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { buildTableSelectSql, quoteTableDataIdentifier } from "@/lib/table/table
|
||||
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
|
||||
import { tableDataLargeValuePreviewOptions } from "@/lib/dataGrid/dataGridLargeValues";
|
||||
import { elasticsearchCursorPageJumpRequestCount } from "@/lib/dataGrid/dataGridPagination";
|
||||
import { usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
|
||||
import { shouldIncludeSyntheticRowId } from "@/lib/table/tableEditing";
|
||||
import { tableMetaForDataTab } from "@/lib/table/tableDataTabMeta";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import type { QueryTab } from "@/types/database";
|
||||
@@ -87,7 +87,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, tableMeta?.tableType);
|
||||
const useRowId = shouldIncludeSyntheticRowId(effectiveDbType, primaryKeys, tableMeta?.tableType);
|
||||
// 列投影只信任真实元数据列:tableMetaForDataTab 的 fallback 列来自查询
|
||||
// 结果(可能是失败结果的 ["Error"]),进入 SQL 会生成非法投影;
|
||||
// 真实列缺失时省略 columns 让 builder 生成 SELECT *
|
||||
|
||||
@@ -5,7 +5,7 @@ import { canApplyDataTabMetadata, canReuseActiveMongoTab, type DataTabReuseMode
|
||||
import { isNoSnapshotErrorResult, isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
|
||||
import { buildTableSelectSql } from "@/lib/table/tableSelectSql";
|
||||
import { tableDataLargeValuePreviewOptions } from "@/lib/dataGrid/dataGridLargeValues";
|
||||
import { editableRowIdentifierColumns, usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
|
||||
import { editableRowIdentifierColumns, shouldIncludeSyntheticRowId } from "@/lib/table/tableEditing";
|
||||
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
|
||||
import { uuid } from "@/lib/common/utils";
|
||||
import { beginDataTabNavigation, endDataTabNavigation, isCurrentDataTabNavigation } from "@/lib/tabs/dataTabNavigationGeneration";
|
||||
@@ -272,7 +272,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
|
||||
// 异步窗口内 tab 可能已被复用为其他目标:旧请求的元数据不得落地、
|
||||
// 不得解除新目标的 pending
|
||||
if (!isCurrentTarget() || !isCurrentGeneration()) return;
|
||||
const useRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys, targetTableType);
|
||||
const useRowId = shouldIncludeSyntheticRowId(effectiveDbType, primaryKeys, targetTableType);
|
||||
queryStore.setTableMeta(tabId, {
|
||||
schema: tableSchema,
|
||||
catalog: target.catalog,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { canApplyDataTabMetadata, dataTabMetadataNeedsRefresh, findExistingDataT
|
||||
import type { SidebarDataOpenRequest } from "@/lib/sidebar/sidebarDataOpenCoordinator";
|
||||
import { hasTreeNodeDatabaseContext } from "@/lib/sidebar/treeNodeContext";
|
||||
import { buildTableSelectSql } from "@/lib/table/tableSelectSql";
|
||||
import { usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
|
||||
import { shouldIncludeSyntheticRowId } from "@/lib/table/tableEditing";
|
||||
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
|
||||
import { tableDataLargeValuePreviewOptions } from "@/lib/dataGrid/dataGridLargeValues";
|
||||
import { canActivateExistingDataTableTab } from "@/lib/tabs/dataTabActivation";
|
||||
@@ -339,7 +339,7 @@ export function useSidebarDataOpenRuntime() {
|
||||
const loadedTableMeta = cachedTableMeta ?? queryStore.tabs.find((item) => item.id === tabId)?.tableMeta;
|
||||
const columns = loadedTableMeta?.columns ?? [];
|
||||
const primaryKeys = loadedTableMeta?.primaryKeys ?? [];
|
||||
const includeRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys, tableType);
|
||||
const includeRowId = shouldIncludeSyntheticRowId(effectiveDbType, primaryKeys, tableType);
|
||||
const sql = await buildTableSelectSql({
|
||||
databaseType: effectiveDbType,
|
||||
driverProfile: config?.driver_profile,
|
||||
|
||||
@@ -64,6 +64,21 @@ describe("editable query hidden primary keys", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("supports Xugu's unqualified ROWID expression for keyless base tables", () => {
|
||||
expect(
|
||||
buildQueryWithHiddenPrimaryKeys({
|
||||
sql: "SELECT * FROM APP.USERS t WHERE t.ACTIVE = 1",
|
||||
databaseType: "xugu",
|
||||
primaryKeys: ["__DBX_ROWID"],
|
||||
existingResultNames: ["ID", "NAME"],
|
||||
sourceExpressions: { __DBX_ROWID: "ROWID" },
|
||||
}),
|
||||
).toEqual({
|
||||
sql: 'SELECT *, ROWID AS "__DBX_PK_0" FROM APP.USERS t WHERE t.ACTIVE = 1',
|
||||
projections: [{ sourceName: "__DBX_ROWID", alias: "__DBX_PK_0" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves an Oracle FOR UPDATE clause when appending a hidden row key", () => {
|
||||
expect(
|
||||
buildQueryWithHiddenPrimaryKeys({
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
DBX_NEO4J_ELEMENT_ID_COLUMN,
|
||||
DBX_ROWID_COLUMN,
|
||||
DBX_TDENGINE_TBNAME_COLUMN,
|
||||
canInsertTableRows,
|
||||
canDeleteExistingTdengineRows,
|
||||
canEditExistingTableRows,
|
||||
canUseKeylessRowPredicate,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
isTdengineExistingRowReadonlyColumn,
|
||||
isTableDataEditable,
|
||||
supportsDataGridTransaction,
|
||||
shouldIncludeSyntheticRowId,
|
||||
usesSyntheticRowIdKey,
|
||||
} from "@/lib/table/tableEditing";
|
||||
import type { ColumnInfo, IndexInfo } from "@/types/database";
|
||||
@@ -46,6 +48,29 @@ describe("tableEditing", () => {
|
||||
expect(editablePrimaryKeys("oceanbase-oracle", [column("ID", true), column("NAME")], "TABLE")).toEqual(["ID"]);
|
||||
});
|
||||
|
||||
it("uses Xugu ROWID for ordinary, partitioned, and temporary tables but not views", () => {
|
||||
const columns = [column("ID"), column("VALUE")];
|
||||
expect(editablePrimaryKeys("xugu", columns, "TABLE")).toEqual([DBX_ROWID_COLUMN]);
|
||||
expect(editablePrimaryKeys("xugu", columns, "PARTITIONED TABLE")).toEqual([DBX_ROWID_COLUMN]);
|
||||
expect(editablePrimaryKeys("xugu", columns, "TEMPORARY TABLE")).toEqual([DBX_ROWID_COLUMN]);
|
||||
expect(editablePrimaryKeys("xugu", columns, "VIEW")).toEqual([]);
|
||||
expect(usesSyntheticRowIdKey("xugu", [DBX_ROWID_COLUMN], "TABLE")).toBe(true);
|
||||
expect(usesSyntheticRowIdKey("xugu", [DBX_ROWID_COLUMN], "PARTITIONED TABLE")).toBe(true);
|
||||
expect(usesSyntheticRowIdKey("xugu", [DBX_ROWID_COLUMN], "TEMPORARY TABLE")).toBe(true);
|
||||
expect(usesSyntheticRowIdKey("xugu", [DBX_ROWID_COLUMN], "VIEW")).toBe(false);
|
||||
expect(isTableDataEditable("xugu", [DBX_ROWID_COLUMN], "TABLE")).toBe(true);
|
||||
expect(isTableDataEditable("xugu", [DBX_ROWID_COLUMN], "VIEW")).toBe(false);
|
||||
expect(canInsertTableRows("xugu")).toBe(true);
|
||||
});
|
||||
|
||||
it("includes Xugu ROWID while cold table metadata has no declared primary keys", () => {
|
||||
expect(shouldIncludeSyntheticRowId("xugu", [], "TABLE")).toBe(true);
|
||||
expect(shouldIncludeSyntheticRowId("xugu", [], "PARTITIONED TABLE")).toBe(true);
|
||||
expect(shouldIncludeSyntheticRowId("xugu", [], "TEMPORARY TABLE")).toBe(true);
|
||||
expect(shouldIncludeSyntheticRowId("xugu", [], "VIEW")).toBe(false);
|
||||
expect(shouldIncludeSyntheticRowId("oracle", [], "TABLE")).toBe(false);
|
||||
});
|
||||
|
||||
it("treats view data tabs as readonly", () => {
|
||||
expect(isTableDataEditable("oracle", [DBX_ROWID_COLUMN], "VIEW")).toBe(false);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
import { isSchemaAware, usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport";
|
||||
|
||||
export type SyntheticEditKey = "oracle-rowid" | "neo4j-element-id";
|
||||
export type SyntheticEditKey = "oracle-rowid" | "xugu-rowid" | "neo4j-element-id";
|
||||
|
||||
export interface TableDataCapability {
|
||||
insert: boolean;
|
||||
@@ -158,6 +158,20 @@ const DATABASE_CAPABILITY_OVERRIDES: Partial<Record<DatabaseType, Partial<Databa
|
||||
oracle: {
|
||||
syntheticKey: "oracle-rowid",
|
||||
},
|
||||
xugu: {
|
||||
// Xugu exposes a stable ROWID pseudo-column for base, partitioned and
|
||||
// temporary tables. Keep this capability scoped to Xugu instead of
|
||||
// broadening Oracle-compatible behavior for other drivers.
|
||||
syntheticKey: "xugu-rowid",
|
||||
tableData: {
|
||||
insert: true,
|
||||
updateRequiresPrimaryKey: false,
|
||||
deleteRequiresPrimaryKey: false,
|
||||
keylessRowPredicate: true,
|
||||
requiresTransactionalTableForExistingRows: false,
|
||||
transaction: true,
|
||||
},
|
||||
},
|
||||
"oceanbase-oracle": {
|
||||
syntheticKey: "oracle-rowid",
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ export function editablePrimaryKeys(databaseType: DatabaseType | undefined, colu
|
||||
if (isViewTableType(tableType)) return primaryKeys;
|
||||
if (databaseType === "tdengine" && primaryKeys.length > 0 && isTdengineStableTableType(tableType)) return [DBX_TDENGINE_TBNAME_COLUMN, ...primaryKeys];
|
||||
const syntheticKey = getDatabaseCapability(databaseType).syntheticKey;
|
||||
if (syntheticKey === "oracle-rowid" && primaryKeys.length === 0) return [DBX_ROWID_COLUMN];
|
||||
if ((syntheticKey === "oracle-rowid" || syntheticKey === "xugu-rowid") && primaryKeys.length === 0) return [DBX_ROWID_COLUMN];
|
||||
if (syntheticKey === "neo4j-element-id" && primaryKeys.length === 0) return [DBX_NEO4J_ELEMENT_ID_COLUMN];
|
||||
return primaryKeys;
|
||||
}
|
||||
@@ -93,12 +93,23 @@ export function hiveTablePropertiesIndicateTransactional(result: { rows: readonl
|
||||
export function usesSyntheticRowIdKey(databaseType: DatabaseType | undefined, primaryKeys: string[], tableType?: string): boolean {
|
||||
if (isViewTableType(tableType)) return false;
|
||||
const syntheticKey = getDatabaseCapability(databaseType).syntheticKey;
|
||||
return primaryKeys.length === 1 && ((syntheticKey === "oracle-rowid" && primaryKeys[0].toUpperCase() === DBX_ROWID_COLUMN) || (syntheticKey === "neo4j-element-id" && primaryKeys[0] === DBX_NEO4J_ELEMENT_ID_COLUMN));
|
||||
return primaryKeys.length === 1 && (((syntheticKey === "oracle-rowid" || syntheticKey === "xugu-rowid") && primaryKeys[0].toUpperCase() === DBX_ROWID_COLUMN) || (syntheticKey === "neo4j-element-id" && primaryKeys[0] === DBX_NEO4J_ELEMENT_ID_COLUMN));
|
||||
}
|
||||
|
||||
/**
|
||||
* Table-data tabs may start before metadata has populated declared primary keys.
|
||||
* Xugu base/partitioned/temp tables can still be addressed safely with ROWID,
|
||||
* so request the hidden projection during that cold-cache window as well.
|
||||
*/
|
||||
export function shouldIncludeSyntheticRowId(databaseType: DatabaseType | undefined, primaryKeys: string[], tableType?: string): boolean {
|
||||
if (isViewTableType(tableType)) return false;
|
||||
if (usesSyntheticRowIdKey(databaseType, primaryKeys, tableType)) return true;
|
||||
return databaseType === "xugu" && primaryKeys.length === 0;
|
||||
}
|
||||
|
||||
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, tableType) && column.toUpperCase() === DBX_ROWID_COLUMN;
|
||||
return shouldIncludeSyntheticRowId(databaseType, primaryKeys, tableType) && column.toUpperCase() === DBX_ROWID_COLUMN;
|
||||
}
|
||||
|
||||
export function isTdengineExistingRowReadonlyColumn(databaseType: DatabaseType | undefined, column: string, columns: ColumnInfo[]): boolean {
|
||||
|
||||
@@ -36,7 +36,7 @@ import { nextRedisCommandDb } from "@/lib/redis/redisCommandSession";
|
||||
import { isRedisMutatingCommand } from "@/lib/redis/redisCommandTable";
|
||||
import { usesAgentCursorForQuery } from "@/lib/database/databaseDriverManifest";
|
||||
import { defaultAutoCommitForDbType, supportsClearableQuerySchema, supportsTransaction } from "@/lib/database/databaseFeatureSupport";
|
||||
import { canInsertTableRows, canUseKeylessRowPredicate, DBX_ROWID_COLUMN, editablePrimaryKeys, usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
|
||||
import { canInsertTableRows, canUseKeylessRowPredicate, DBX_ROWID_COLUMN, editablePrimaryKeys, shouldIncludeSyntheticRowId, usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
|
||||
import { TABLE_DATA_EXPORT_PAGE_SIZE } from "@/lib/table/tableDataExport";
|
||||
import { tableMetaForDataTab } from "@/lib/table/tableDataTabMeta";
|
||||
import { isDataTabMetadataLifecycleStale } from "@/lib/sidebar/dataTabOpenPolicy";
|
||||
@@ -105,7 +105,7 @@ const groupedDisplayMetadataLimiter = new MetadataTaskLimiter(GROUPED_DISPLAY_ME
|
||||
console.debug("[DBX][metadata-load:grouped-display-limiter]", event);
|
||||
});
|
||||
const UPPERCASE_FOLDED_METADATA_TYPES = new Set<string>([...ORACLE_LIKE_METADATA_TYPES, "saphana"]);
|
||||
const HIDDEN_QUERY_KEY_DATABASE_TYPES = new Set<DatabaseType>(["mysql", "postgres", "sqlserver", "oracle"]);
|
||||
const HIDDEN_QUERY_KEY_DATABASE_TYPES = new Set<DatabaseType>(["mysql", "postgres", "sqlserver", "oracle", "xugu"]);
|
||||
const QUERY_RESULT_EXPORT_UNSUPPORTED_ERROR = "Streaming export is unsupported for this query. Simplify it or use a supported driver.";
|
||||
const BACKGROUND_CLIENT_SESSION_SUFFIXES = ["count", "explain", "export"] as const;
|
||||
const CANCEL_QUERY_TIMEOUT_MS = 10_000;
|
||||
@@ -3902,7 +3902,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
primaryKeys,
|
||||
...tableDataLargeValuePreviewOptions(effectiveDbType, tableMeta.columns, primaryKeys, limit),
|
||||
includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName,
|
||||
includeRowId: usesSyntheticRowIdKey(effectiveDbType, primaryKeys, tableMeta.tableType),
|
||||
includeRowId: shouldIncludeSyntheticRowId(effectiveDbType, primaryKeys, tableMeta.tableType),
|
||||
whereInput: tab.whereInput,
|
||||
injectDefaultTimeSeriesWhere: true,
|
||||
orderBy,
|
||||
@@ -4813,7 +4813,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
return matches.length === 1 && matches[0]?.type === "table";
|
||||
}
|
||||
|
||||
async function resolveOracleRowIdSafety(tab: QueryTab, loaded: LoadedEditableSource): Promise<boolean> {
|
||||
async function resolveOracleRowIdSafety(tab: QueryTab, loaded: LoadedEditableSource, databaseType: DatabaseType): Promise<boolean> {
|
||||
if (oracleRowIdIsSafeForQuery(tab, loaded)) return true;
|
||||
if (loaded.tableMeta.tableType?.trim()) return false;
|
||||
|
||||
@@ -4830,7 +4830,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
database: loaded.tableMeta.database ?? tab.database,
|
||||
schema: loaded.tableMeta.schema,
|
||||
tableName: loaded.tableMeta.tableName,
|
||||
databaseType: "oracle",
|
||||
databaseType,
|
||||
driverProfile: connection?.driver_profile || connection?.db_type,
|
||||
catalog: loaded.tableMeta.catalog,
|
||||
},
|
||||
@@ -4858,7 +4858,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
databaseType,
|
||||
primaryKeys: missingPrimaryKeys,
|
||||
existingResultNames: metadataAnalysis.selectStar ? loaded.tableMeta.columns.map((column) => column.name) : metadataAnalysis.columns.map((column) => column.resultName),
|
||||
sourceExpressions: databaseType === "oracle" && missingPrimaryKeys.includes(DBX_ROWID_COLUMN) ? { [DBX_ROWID_COLUMN]: "ROWIDTOCHAR(ROWID)" } : undefined,
|
||||
sourceExpressions: missingPrimaryKeys.includes(DBX_ROWID_COLUMN) && (databaseType === "oracle" || databaseType === "xugu") ? { [DBX_ROWID_COLUMN]: databaseType === "oracle" ? "ROWIDTOCHAR(ROWID)" : "ROWID" } : undefined,
|
||||
});
|
||||
if (!rewritten) return unchanged;
|
||||
queryExecutionLog("info", "hidden-primary-keys", {
|
||||
@@ -4885,13 +4885,14 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const hasDirectSourceProjection = analysis.columns.some((column) => Boolean(column.sourceName) && (!column.sourceKey || column.sourceKey === source.key));
|
||||
if (!wholeSourceProjected && !hasDirectSourceProjection) return unchanged;
|
||||
// Whole-source projections already include declared primary keys. Only
|
||||
// Oracle needs preflight metadata here to add ROWID for a keyless table.
|
||||
if (databaseType !== "oracle" && wholeSourceProjected) return unchanged;
|
||||
// Oracle and Xugu need preflight metadata here to add their synthetic
|
||||
// row key for a keyless base table.
|
||||
if (databaseType !== "oracle" && databaseType !== "xugu" && wholeSourceProjected) return unchanged;
|
||||
|
||||
const target = resolveEditableSourceMetadataTarget(tab, analysis, source, conn, databaseType, executionDatabase);
|
||||
const cached = getCachedTableMetadata(target.request);
|
||||
let loaded = cached ? loadedEditableSourceFromMetadata(target, cached.metadata) : undefined;
|
||||
if (!cached && databaseType === "oracle") {
|
||||
if (!cached && (databaseType === "oracle" || databaseType === "xugu")) {
|
||||
// Oracle column discovery can be slow. A star projection over a table
|
||||
// with a declared primary key already returns the complete row identity,
|
||||
// so SQL can start while the full metadata needed for editing loads.
|
||||
@@ -4922,12 +4923,12 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (loaded.tableMeta.tableType?.toUpperCase().includes("VIEW")) return unchanged;
|
||||
const columnPrimaryKeys = loaded.tableMeta.columns.filter((column) => column.is_primary_key).map((column) => column.name);
|
||||
const primaryKeys = databaseType === "oracle" ? loaded.tableMeta.primaryKeys : editablePrimaryKeys(databaseType, loaded.tableMeta.columns, loaded.tableMeta.tableType);
|
||||
const syntheticOracleRowId = databaseType === "oracle" && usesSyntheticRowIdKey(databaseType, primaryKeys, loaded.tableMeta.tableType);
|
||||
// Oracle base tables without a natural identifier use the same ROWID
|
||||
// identity as table-data tabs. Confirm the object is a base table because
|
||||
// selecting ROWID from a view can fail with ORA-01445.
|
||||
if (syntheticOracleRowId && !(await resolveOracleRowIdSafety(tab, loaded))) return unchanged;
|
||||
const declaredPrimaryKeys = databaseType === "oracle" && !syntheticOracleRowId ? primaryKeys : columnPrimaryKeys;
|
||||
const syntheticRowId = (databaseType === "oracle" || databaseType === "xugu") && usesSyntheticRowIdKey(databaseType, primaryKeys, loaded.tableMeta.tableType);
|
||||
// Base tables without a natural identifier use the same ROWID identity
|
||||
// as table-data tabs (Oracle and Xugu). Confirm the object is a base
|
||||
// table because selecting ROWID from a view can fail with ORA-01445.
|
||||
if (syntheticRowId && !(await resolveOracleRowIdSafety(tab, loaded, databaseType))) return unchanged;
|
||||
const declaredPrimaryKeys = databaseType === "oracle" && !syntheticRowId ? primaryKeys : columnPrimaryKeys;
|
||||
return buildHiddenPrimaryKeyPreparation(tab, sql, databaseType, loaded, primaryKeys, declaredPrimaryKeys, traceId, elapsed);
|
||||
} catch (error) {
|
||||
// Metadata enrichment is optional. Query execution must retain its prior
|
||||
|
||||
@@ -24,7 +24,7 @@ use data_grid_tdengine_sql::{
|
||||
use crate::models::connection::DatabaseType;
|
||||
use crate::sql_dialect::{
|
||||
firebird_rows_clause, quote_table_identifier, table_pagination_strategy, uses_oracle_row_id,
|
||||
uses_single_row_insert_statements, TablePaginationStrategy,
|
||||
uses_single_row_insert_statements, uses_synthetic_row_id, uses_xugu_row_id, TablePaginationStrategy,
|
||||
};
|
||||
use crate::transfer::{format_ch_array_sql_literal, format_pg_array_sql_literal};
|
||||
|
||||
@@ -378,7 +378,7 @@ pub fn build_data_grid_copy_update_statements(options: DataGridCopyUpdateStateme
|
||||
.enumerate()
|
||||
.filter_map(|(index, column)| Some((column.as_deref()?, index)))
|
||||
.filter(|(column, _)| !primary_key_set.contains(&normalize_column_name(column)))
|
||||
.filter(|(column, _)| !is_oracle_row_id(options.database_type, Some(column)))
|
||||
.filter(|(column, _)| !is_synthetic_row_id(options.database_type, Some(column)))
|
||||
.map(|(column, index)| (column, index, column_info_for(column_info, column)))
|
||||
.collect();
|
||||
let primary_key_info =
|
||||
@@ -1122,7 +1122,7 @@ fn validate_data_grid_save(options: &DataGridSaveStatementOptions) -> Option<Str
|
||||
&& column.column_default.is_none()
|
||||
&& !is_auto_generated_column(column)
|
||||
&& !is_non_identity_generated_column(Some(column))
|
||||
&& !is_oracle_row_id(options.database_type, Some(&column.name))
|
||||
&& !is_synthetic_row_id(options.database_type, Some(&column.name))
|
||||
})
|
||||
.map(|column| normalize_column_name(&column.name))
|
||||
.collect();
|
||||
@@ -2848,7 +2848,7 @@ fn build_row_where(
|
||||
.enumerate()
|
||||
.filter_map(|(index, column)| {
|
||||
let column = column.as_deref()?;
|
||||
if is_oracle_row_id(database_type, Some(column)) {
|
||||
if is_synthetic_row_id(database_type, Some(column)) {
|
||||
return None;
|
||||
}
|
||||
Some(build_column_predicate(
|
||||
@@ -2876,7 +2876,7 @@ fn build_save_row_where(
|
||||
.enumerate()
|
||||
.filter_map(|(index, column)| {
|
||||
let column = column.as_deref()?;
|
||||
if is_oracle_row_id(database_type, Some(column)) {
|
||||
if is_synthetic_row_id(database_type, Some(column)) {
|
||||
return None;
|
||||
}
|
||||
Some(build_save_column_predicate(
|
||||
@@ -3057,8 +3057,8 @@ fn oracle_character_lob_constructor(data_type: &str) -> Option<&'static str> {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_oracle_row_id(database_type: Option<DatabaseType>, name: Option<&str>) -> bool {
|
||||
uses_oracle_row_id(database_type) && name.is_some_and(|name| name.eq_ignore_ascii_case(DBX_ROWID_COLUMN))
|
||||
fn is_synthetic_row_id(database_type: Option<DatabaseType>, name: Option<&str>) -> bool {
|
||||
uses_synthetic_row_id(database_type) && name.is_some_and(|name| name.eq_ignore_ascii_case(DBX_ROWID_COLUMN))
|
||||
}
|
||||
|
||||
pub(crate) fn is_neo4j_element_id(database_type: Option<DatabaseType>, name: Option<&str>) -> bool {
|
||||
@@ -3085,7 +3085,7 @@ pub(crate) fn is_grid_insert_omitted_column(
|
||||
name: Option<&str>,
|
||||
include_computed_columns: bool,
|
||||
) -> bool {
|
||||
is_oracle_row_id(database_type, name)
|
||||
is_synthetic_row_id(database_type, name)
|
||||
|| is_postgres_tsvector_column(database_type, column_info)
|
||||
|| (!include_computed_columns && is_non_identity_generated_column(column_info))
|
||||
}
|
||||
@@ -3096,7 +3096,7 @@ fn is_grid_update_omitted_column(
|
||||
name: Option<&str>,
|
||||
primary_key_set: &[String],
|
||||
) -> bool {
|
||||
is_oracle_row_id(database_type, name)
|
||||
is_synthetic_row_id(database_type, name)
|
||||
|| is_clickhouse_key_column(database_type, column_info, name, primary_key_set)
|
||||
|| is_non_identity_generated_column(column_info)
|
||||
}
|
||||
@@ -3150,7 +3150,7 @@ fn is_null_write_to_not_null_column(
|
||||
let Some(column) = column else {
|
||||
return false;
|
||||
};
|
||||
if is_oracle_row_id(database_type, Some(column)) || is_neo4j_element_id(database_type, Some(column)) {
|
||||
if is_synthetic_row_id(database_type, Some(column)) || is_neo4j_element_id(database_type, Some(column)) {
|
||||
return false;
|
||||
}
|
||||
value.is_null() && not_null_columns.iter().any(|not_null| not_null == &normalize_column_name(column))
|
||||
@@ -3231,7 +3231,10 @@ fn clickhouse_no_mutable_columns_error() -> String {
|
||||
}
|
||||
|
||||
fn predicate_ident(database_type: Option<DatabaseType>, name: &str, identifier_quote: Option<&str>) -> String {
|
||||
if is_oracle_row_id(database_type, Some(name)) {
|
||||
if is_synthetic_row_id(database_type, Some(name)) {
|
||||
if uses_xugu_row_id(database_type) {
|
||||
return "ROWID".to_string();
|
||||
}
|
||||
"ROWIDTOCHAR(ROWID)".to_string()
|
||||
} else {
|
||||
data_grid_identifier(database_type, name, identifier_quote)
|
||||
@@ -8243,6 +8246,42 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepares_xugu_rowid_updates_deletes_and_inserts_without_writing_synthetic_key() {
|
||||
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
|
||||
database_type: Some(DatabaseType::Xugu),
|
||||
identifier_quote: None,
|
||||
table_meta: DataGridTableMeta {
|
||||
catalog: None,
|
||||
database: None,
|
||||
schema: Some("APP".to_string()),
|
||||
table_name: "ROWID_TABLE".to_string(),
|
||||
primary_keys: vec![DBX_ROWID_COLUMN.to_string()],
|
||||
columns: Some(vec![
|
||||
column(DBX_ROWID_COLUMN, "ROWID", false, None),
|
||||
column("ID", "INTEGER", false, None),
|
||||
column("VALUE", "VARCHAR(40)", true, None),
|
||||
]),
|
||||
},
|
||||
columns: vec![DBX_ROWID_COLUMN.to_string(), "ID".to_string(), "VALUE".to_string()],
|
||||
source_columns: None,
|
||||
rows: vec![vec![json!("AA-1"), json!(1), json!("old")], vec![json!("AA-2"), json!(2), json!("remove")]],
|
||||
dirty_rows: vec![(0, vec![(2, json!("new"))])],
|
||||
deleted_rows: vec![1],
|
||||
new_rows: vec![vec![Value::Null, json!(3), json!("inserted")]],
|
||||
});
|
||||
|
||||
assert_eq!(result.validation_error, None);
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec![
|
||||
"UPDATE \"APP\".\"ROWID_TABLE\" SET \"VALUE\" = 'new' WHERE ROWID = 'AA-1';",
|
||||
"DELETE FROM \"APP\".\"ROWID_TABLE\" WHERE ROWID = 'AA-2';",
|
||||
"INSERT INTO \"APP\".\"ROWID_TABLE\" (\"ID\", \"VALUE\") VALUES (3, 'inserted');",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_data_grid_save_skips_sqlite_autoincrement_pk_validation() {
|
||||
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
|
||||
|
||||
@@ -1252,9 +1252,9 @@ fn export_sql_statement_bytes(database_type: Option<DatabaseType>, text: &str) -
|
||||
}
|
||||
|
||||
pub(crate) fn is_internal_export_column(database_type: Option<DatabaseType>, column: &str) -> bool {
|
||||
// Oracle-compatible ROWID is injected only to identify editable rows. It
|
||||
// is not a physical table column and must never propagate into exports.
|
||||
crate::sql_dialect::uses_oracle_row_id(database_type)
|
||||
// Synthetic ROWID is injected only to identify editable rows. It is not a
|
||||
// physical table column and must never propagate into exports.
|
||||
crate::sql_dialect::uses_synthetic_row_id(database_type)
|
||||
&& column.eq_ignore_ascii_case(crate::sql_dialect::DBX_ROWID_COLUMN)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ mod descriptor_snapshots;
|
||||
|
||||
pub use capabilities::{
|
||||
firebird_rows_clause, is_schema_aware, pagination_strategy, table_pagination_strategy, uses_fetch_first,
|
||||
uses_oracle_row_id, uses_single_row_insert_statements, PaginationContext, TablePaginationStrategy,
|
||||
uses_oracle_row_id, uses_single_row_insert_statements, uses_synthetic_row_id, uses_xugu_row_id, PaginationContext,
|
||||
TablePaginationStrategy,
|
||||
};
|
||||
pub use ddl_profile::{
|
||||
profile_for, AutoIncSyntax, DdlDialectProfile, IndexTypePlacement, QuoteStyle, RenameColumnSyntax, TriggerTemplate,
|
||||
|
||||
@@ -80,6 +80,18 @@ pub fn uses_oracle_row_id(database_type: Option<DatabaseType>) -> bool {
|
||||
matches!(database_type, Some(DatabaseType::Oracle | DatabaseType::OceanbaseOracle))
|
||||
}
|
||||
|
||||
/// Xugu exposes an unqualified ROWID pseudo-column for base, partitioned and
|
||||
/// temporary tables. It is intentionally separate from Oracle's ROWIDTOCHAR
|
||||
/// representation because qualified ROWID and ROWIDTOCHAR are not supported
|
||||
/// by Xugu.
|
||||
pub fn uses_xugu_row_id(database_type: Option<DatabaseType>) -> bool {
|
||||
database_type == Some(DatabaseType::Xugu)
|
||||
}
|
||||
|
||||
pub fn uses_synthetic_row_id(database_type: Option<DatabaseType>) -> bool {
|
||||
uses_oracle_row_id(database_type) || uses_xugu_row_id(database_type)
|
||||
}
|
||||
|
||||
/// Oracle 系方言不支持 `INSERT ... VALUES (...), (...)` 多行语法,
|
||||
/// 复制为 INSERT 与导出 INSERT 都需按行生成单条语句。
|
||||
pub fn uses_single_row_insert_statements(database_type: DatabaseType) -> bool {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::models::connection::DatabaseType;
|
||||
|
||||
use super::capabilities::{
|
||||
firebird_rows_clause, table_pagination_strategy, uses_oracle_row_id, TablePaginationStrategy,
|
||||
firebird_rows_clause, table_pagination_strategy, uses_oracle_row_id, uses_xugu_row_id, TablePaginationStrategy,
|
||||
};
|
||||
use super::identifiers::{
|
||||
normalize_where_input, qualified_table_name, qualified_table_name_with_catalog, quote_gaussdb_jdbc_identifier,
|
||||
@@ -272,6 +272,8 @@ pub fn build_table_data_select_sql_with_database(
|
||||
let include_oracle_row_id = options.include_row_id
|
||||
&& uses_oracle_row_id(database_type)
|
||||
&& !is_view_table_type(options.table_type.as_deref());
|
||||
let include_xugu_row_id =
|
||||
options.include_row_id && uses_xugu_row_id(database_type) && !is_view_table_type(options.table_type.as_deref());
|
||||
let offset = options.offset.unwrap_or(0);
|
||||
let oracle_view_first_page =
|
||||
database_type == Some(DatabaseType::Oracle) && is_view_table_type(options.table_type.as_deref()) && offset == 0;
|
||||
@@ -280,6 +282,15 @@ pub fn build_table_data_select_sql_with_database(
|
||||
format!("ROWIDTOCHAR(t.ROWID) AS \"{DBX_ROWID_COLUMN}\", t.*")
|
||||
} else if let Some(preview_columns) = build_large_value_preview_columns(&options) {
|
||||
preview_columns
|
||||
} else if include_xugu_row_id {
|
||||
if options.columns.is_empty() {
|
||||
format!("ROWID AS \"{DBX_ROWID_COLUMN}\", *")
|
||||
} else {
|
||||
format!(
|
||||
"ROWID AS \"{DBX_ROWID_COLUMN}\", {}",
|
||||
quoted_table_columns_or_star(database_type, &options.columns)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
build_select_columns(
|
||||
database_type,
|
||||
|
||||
@@ -1326,6 +1326,60 @@ fn builds_oracle_and_neo4j_table_data_queries() {
|
||||
}),
|
||||
"SELECT \"__DBX_ROWID\", \"ID\", \"SMC_RESPONSE\" FROM (SELECT ROWIDTOCHAR(t.ROWID) AS \"__DBX_ROWID\", t.* FROM \"APP\".\"DATA_REPORT_SUB_TASK\" t) WHERE ROWNUM <= 100"
|
||||
);
|
||||
assert_eq!(
|
||||
build_table_data_select_sql(TableDataSelectSqlOptions {
|
||||
database_type: Some(DatabaseType::Xugu),
|
||||
schema: Some("DBXTEST".to_string()),
|
||||
table_name: "DBX_LOAD_TABLE_006".to_string(),
|
||||
table_type: Some("PARTITIONED TABLE".to_string()),
|
||||
primary_keys: vec![DBX_ROWID_COLUMN.to_string()],
|
||||
columns: Vec::new(),
|
||||
fallback_order_columns: Vec::new(),
|
||||
order_by: None,
|
||||
limit: Some(100),
|
||||
offset: None,
|
||||
where_input: None,
|
||||
include_row_id: true,
|
||||
..Default::default()
|
||||
}),
|
||||
"SELECT ROWID AS \"__DBX_ROWID\", * FROM \"DBXTEST\".\"DBX_LOAD_TABLE_006\" LIMIT 100;"
|
||||
);
|
||||
assert_eq!(
|
||||
build_table_data_select_sql(TableDataSelectSqlOptions {
|
||||
database_type: Some(DatabaseType::Xugu),
|
||||
schema: Some("DBXTEST".to_string()),
|
||||
table_name: "DBX_LOAD_TABLE_006".to_string(),
|
||||
table_type: Some("TEMPORARY TABLE".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(25),
|
||||
offset: Some(10),
|
||||
where_input: None,
|
||||
include_row_id: true,
|
||||
..Default::default()
|
||||
}),
|
||||
"SELECT ROWID AS \"__DBX_ROWID\", \"ID\", \"NAME\" FROM \"DBXTEST\".\"DBX_LOAD_TABLE_006\" LIMIT 25 OFFSET 10;"
|
||||
);
|
||||
assert_eq!(
|
||||
build_table_data_select_sql(TableDataSelectSqlOptions {
|
||||
database_type: Some(DatabaseType::Xugu),
|
||||
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,
|
||||
..Default::default()
|
||||
}),
|
||||
"SELECT * FROM \"DBXTEST\".\"DBX_JOIN_VIEW\" LIMIT 100;"
|
||||
);
|
||||
assert_eq!(
|
||||
build_table_data_select_sql(TableDataSelectSqlOptions {
|
||||
database_type: Some(DatabaseType::Oracle),
|
||||
|
||||
Reference in New Issue
Block a user