diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index b6cf52dfef..4ca0b30e29 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -1117,7 +1117,7 @@ async function newQuery() { targetDatabase: target.database, databaseType: effectiveDatabaseTypeForConnection(conn), }); - const tabId = queryStore.createTab(conn.id, target.database, undefined, "query", target.schema, initialSql); + const tabId = queryStore.createTab(conn.id, target.database, undefined, "query", target.schema, initialSql, target.catalog); try { await connectionStore.ensureConnected(target.connectionId); if (target.shouldRefreshDefaultDatabase) { diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 52733d94cd..6eeb350730 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -280,6 +280,7 @@ interface DataGridProps { sortMode?: DataGridSortMode; tableMeta?: { catalog?: string; + database?: string; schema?: string; tableName: string; tableType?: string; @@ -1420,6 +1421,7 @@ async function loadServerFilterValues(columnIndex: number, searchValue: string) const sql = await buildDataGridColumnDistinctValuesSql({ databaseType: resolvedDatabaseType.value, catalog: tableMeta.catalog, + database: tableMeta.database, schema: tableMeta.schema, tableName: tableMeta.tableName, columnName, @@ -3961,6 +3963,7 @@ async function buildCurrentCountTarget(): Promise<{ sql: string; schema?: string databaseType: props.databaseType, identifierQuote: connectionStore.connectionIdentifierQuote?.(props.connectionId), catalog: props.tableMeta.catalog, + database: props.tableMeta.database, schema: props.tableMeta.schema, tableName: props.tableMeta.tableName, whereInput: currentWhereInput(), @@ -5622,6 +5625,7 @@ async function applyOrderBySearch() { databaseType: resolvedDatabaseType.value, identifierQuote: connectionStore.connectionIdentifierQuote?.(props.connectionId), catalog: tableMeta.catalog, + database: tableMeta.database, schema: tableMeta.schema, tableName: tableMeta.tableName, tableType: tableMeta.tableType, @@ -5655,6 +5659,7 @@ async function applyWhereFilter() { databaseType: resolvedDatabaseType.value, identifierQuote: connectionStore.connectionIdentifierQuote?.(props.connectionId), catalog: tableMeta.catalog, + database: tableMeta.database, schema: tableMeta.schema, tableName: tableMeta.tableName, tableType: tableMeta.tableType, diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index c7f18ecbe4..b348a823cd 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -852,7 +852,7 @@ async function fetchTableDdl() { tableDdlLoading.value = true; try { const schema = row.schema || selectedSchema.value || props.database; - const ddl = await api.getTableDdl(props.connection.id, props.database || "", schema, row.name, tableDdlObjectType(row.type)); + const ddl = await api.getTableDdl(props.connection.id, props.database || "", schema, row.name, tableDdlObjectType(row.type), props.catalog); if (sidePanelGuard.isStale(epoch)) return; tableDdlContent.value = ddl; } catch (e: any) { @@ -870,7 +870,7 @@ async function fetchTableColumns() { tableColumnsLoading.value = true; try { const schema = row.schema || selectedSchema.value || props.database; - const columns = await api.getColumns(props.connection.id, props.database || "", schema, row.name); + const columns = await api.getColumns(props.connection.id, props.database || "", schema, row.name, props.catalog); if (sidePanelGuard.isStale(epoch)) return; tableColumns.value = columns; } catch { @@ -888,7 +888,7 @@ async function fetchTableIndexes() { tableIndexesLoading.value = true; try { const schema = row.schema || selectedSchema.value || props.database; - const indexes = await api.listIndexes(props.connection.id, props.database || "", schema, row.name); + const indexes = await api.listIndexes(props.connection.id, props.database || "", schema, row.name, props.catalog); if (sidePanelGuard.isStale(epoch)) return; tableIndexes.value = indexes; } catch { @@ -906,7 +906,7 @@ async function fetchTableForeignKeys() { tableForeignKeysLoading.value = true; try { const schema = row.schema || selectedSchema.value || props.database; - const fks = await api.listForeignKeys(props.connection.id, props.database || "", schema, row.name); + const fks = await api.listForeignKeys(props.connection.id, props.database || "", schema, row.name, props.catalog); if (sidePanelGuard.isStale(epoch)) return; tableForeignKeys.value = fks; } catch { @@ -924,7 +924,7 @@ async function fetchTableTriggers() { tableTriggersLoading.value = true; try { const schema = row.schema || selectedSchema.value || props.database; - const triggers = await api.listTriggers(props.connection.id, props.database || "", schema, row.name); + const triggers = await api.listTriggers(props.connection.id, props.database || "", schema, row.name, props.catalog); if (sidePanelGuard.isStale(epoch)) return; tableTriggers.value = triggers; } catch { @@ -987,7 +987,7 @@ const canOpenTableStructureEditor = computed(() => sidePanelRow.value?.type === function openTableStructureEditor() { const row = sidePanelRow.value; if (!row || row.type !== "TABLE" || !canOpenTableStructureEditor.value) return; - queryStore.openTableStructure(props.connection.id, props.database, row.schema || selectedSchema.value, row.name, tableInfoTab.value); + queryStore.openTableStructure(props.connection.id, props.database, row.schema || selectedSchema.value, row.name, tableInfoTab.value, undefined, props.catalog); } async function openSource(row: ObjectBrowserRow) { @@ -1274,7 +1274,7 @@ function openViewData(row: ObjectBrowserRow) { function openStructureEditor(row: ObjectBrowserRow) { if (row.type !== "TABLE") return; - queryStore.openTableStructure(props.connection.id, props.database, row.schema || selectedSchema.value, row.name); + queryStore.openTableStructure(props.connection.id, props.database, row.schema || selectedSchema.value, row.name, undefined, undefined, props.catalog); } function droppedTableObjectTypeForRow(row: ObjectBrowserRow): "TABLE" | "VIEW" | "MATERIALIZED_VIEW" | null { @@ -1377,7 +1377,7 @@ async function fetchSortedTableRowsForDrop(): Promise { const rows = [...selectedTableRows.value]; if (rows.length <= 1) return rows; - const fkResults = await Promise.all(rows.map((row) => api.listForeignKeys(props.connection.id, props.database, row.schema || selectedSchema.value || "", row.name).catch(() => [] as ForeignKeyInfo[]))); + const fkResults = await Promise.all(rows.map((row) => api.listForeignKeys(props.connection.id, props.database, row.schema || selectedSchema.value || "", row.name, props.catalog).catch(() => [] as ForeignKeyInfo[]))); const tablesWithFk: TableWithFk[] = rows.map((row, i) => ({ name: row.name, @@ -1567,7 +1567,7 @@ async function confirmBatchEmptyTables() { async function exportStructure(row: ObjectBrowserRow) { try { const schema = row.schema || selectedSchema.value || props.database; - const ddl = await api.getTableDdl(props.connection.id, props.database, schema, row.name, tableDdlObjectType(row.type)); + const ddl = await api.getTableDdl(props.connection.id, props.database, schema, row.name, tableDdlObjectType(row.type), props.catalog); await saveFileContent(buildSingleDdlExportFileContent(ddl), `${row.name}.sql`, "SQL", "sql"); } catch (e: any) { console.error("Export structure failed:", e); @@ -1582,8 +1582,8 @@ function tableDdlObjectType(type: ObjectBrowserRow["type"]): ObjectSourceKind | async function exportDataLegacy(row: ObjectBrowserRow, format: "json" | "sql") { try { const schema = row.schema || selectedSchema.value; - const tableColumns = format === "sql" ? await api.getColumns(props.connection.id, props.database, schema || props.database, row.name) : undefined; - const queryColumns = props.connection.db_type === "neo4j" ? (tableColumns ?? (await api.getColumns(props.connection.id, props.database, schema || props.database, row.name))).map((column) => column.name) : undefined; + const tableColumns = format === "sql" ? await api.getColumns(props.connection.id, props.database, schema || props.database, row.name, props.catalog) : undefined; + const queryColumns = props.connection.db_type === "neo4j" ? (tableColumns ?? (await api.getColumns(props.connection.id, props.database, schema || props.database, row.name, props.catalog))).map((column) => column.name) : undefined; const result = await fetchTableDataForExport({ databaseType: effectiveDatabaseType.value, identifierQuote: connectionStore.connectionIdentifierQuote?.(props.connection.id), @@ -1669,7 +1669,7 @@ async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx") { let task: ExportTask | null = null; try { - const queryColumns = props.connection.db_type === "neo4j" ? (await api.getColumns(props.connection.id, props.database, schema || props.database, row.name)).map((column) => column.name) : undefined; + const queryColumns = props.connection.db_type === "neo4j" ? (await api.getColumns(props.connection.id, props.database, schema || props.database, row.name, props.catalog)).map((column) => column.name) : undefined; task = addExportTask(row.name, format, filePath); const currentTask = task; @@ -1832,7 +1832,7 @@ async function confirmPasteTable() { if (!executed) return; } if (copyData) { - const sourceColumns = await api.getColumns(props.connection.id, props.database, schema || "", entry.sourceName); + const sourceColumns = await api.getColumns(props.connection.id, props.database, schema || "", entry.sourceName, props.catalog); const dataCopyColumnOptions = tableDataCopyColumnOptions(effectiveDatabaseType.value, sourceColumns); if (dataCopyColumnOptions.columns.length === 0) { throw new Error("No writable columns available for table data copy."); diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 063330bc3b..eaa18dc10f 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -1316,6 +1316,7 @@ async function openData() { tabId, cachedTableMeta ?? { catalog: node.catalog, + database: node.database, schema: tableSchema, tableName: node.label, tableType, @@ -1425,6 +1426,7 @@ async function openData() { databaseType: effectiveDbType, identifierQuote: connectionStore.connectionIdentifierQuote?.(node.connectionId), schema: tableSchema, + database: node.database, tableName: node.label, tableType, catalog: node.catalog, diff --git a/apps/desktop/src/composables/useDataGridActions.ts b/apps/desktop/src/composables/useDataGridActions.ts index fd97ba53c2..d4fd3ea29a 100644 --- a/apps/desktop/src/composables/useDataGridActions.ts +++ b/apps/desktop/src/composables/useDataGridActions.ts @@ -52,6 +52,7 @@ export function useDataGridActions(activeTab: ComputedRef) return buildTableSelectSql({ databaseType: effectiveDbType, identifierQuote: connectionStore.connectionIdentifierQuote?.(tab.connectionId), + database: tableMeta?.database, schema: tableMeta?.schema, tableName: tableMeta?.tableName ?? "", tableType: tableMeta?.tableType, diff --git a/apps/desktop/src/composables/useNavigationTargets.ts b/apps/desktop/src/composables/useNavigationTargets.ts index d3f270b95e..d48f3134be 100644 --- a/apps/desktop/src/composables/useNavigationTargets.ts +++ b/apps/desktop/src/composables/useNavigationTargets.ts @@ -67,6 +67,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab identifierQuote, schema: target.schema, catalog: target.catalog, + database: target.database, tableName: target.tableName, tableType: targetTableType, columns: columns.map((column) => column.name), @@ -77,6 +78,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab queryStore.updateSql(tabId, sql); queryStore.setTableMeta(tabId, { catalog: target.catalog, + database: target.database, schema: target.schema, tableName: target.tableName, tableType: targetTableType, @@ -91,6 +93,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab identifierQuote, schema: target.schema, catalog: target.catalog, + database: target.database, tableName: target.tableName, tableType: targetTableType, whereInput: target.whereInput, @@ -100,6 +103,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab queryStore.setTableMeta(tabId, { schema: target.schema, catalog: target.catalog, + database: target.database, tableName: target.tableName, tableType: targetTableType, columns: [], @@ -119,6 +123,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab identifierQuote, schema: target.schema, catalog: target.catalog, + database: target.database, tableName: target.tableName, tableType: targetTableType, whereInput: target.whereInput, @@ -135,6 +140,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab queryStore.setTableMeta(tabId, { schema: target.schema, catalog: target.catalog, + database: target.database, tableName: target.tableName, tableType: targetTableType, columns, @@ -146,6 +152,7 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab identifierQuote, schema: target.schema, catalog: target.catalog, + database: target.database, tableName: target.tableName, tableType: targetTableType, whereInput: target.whereInput, diff --git a/apps/desktop/src/lib/dataGrid/dataGridSql.ts b/apps/desktop/src/lib/dataGrid/dataGridSql.ts index 6d7782a24e..7ec7fb94fe 100644 --- a/apps/desktop/src/lib/dataGrid/dataGridSql.ts +++ b/apps/desktop/src/lib/dataGrid/dataGridSql.ts @@ -5,6 +5,7 @@ export type GridCellValue = string | number | boolean | null | unknown[] | { [ke export interface DataGridTableMeta { catalog?: string; + database?: string; schema?: string; tableName: string; primaryKeys: string[]; @@ -81,6 +82,7 @@ export interface DataGridColumnValuesFilterConditionOptions { export interface DataGridColumnDistinctValuesSqlOptions { databaseType?: DatabaseType; catalog?: string; + database?: string; schema?: string; tableName: string; columnName: string; @@ -95,6 +97,7 @@ export interface DataGridCountSqlOptions { databaseType?: DatabaseType; identifierQuote?: string; catalog?: string; + database?: string; schema?: string; tableName: string; whereInput?: string; diff --git a/apps/desktop/src/lib/database/jdbcDialect.ts b/apps/desktop/src/lib/database/jdbcDialect.ts index 486c479701..7ac1392ab0 100644 --- a/apps/desktop/src/lib/database/jdbcDialect.ts +++ b/apps/desktop/src/lib/database/jdbcDialect.ts @@ -34,6 +34,16 @@ export function effectiveDatabaseTypeForConnection(connection?: JdbcDialectConne if (!connection) return undefined; if (connection.db_type === "gbase" && isGbase8sProfile(connection.driver_profile)) return "informix"; if (connection.db_type === "gbase") return "mysql"; + // MySQL-protocol connections to Doris/StarRocks (db_type=mysql with a + // starrocks/doris driver_profile) must use the Doris/StarRocks SQL dialect so + // that multi-catalog 3-part names (`catalog.database.table`) are emitted. + // mysql and starrocks share the backtick-quoting + LIMIT dialect, so this + // only widens the catalog-aware SQL generation path without other side effects. + if (connection.db_type === "mysql") { + const profile = connection.driver_profile?.toLowerCase(); + if (profile === "starrocks") return "starrocks"; + if (profile === "doris" || profile === "selectdb") return "doris"; + } if (connection.db_type !== "jdbc") return connection.db_type; return inferJdbcDialect(connection) ?? "jdbc"; } diff --git a/apps/desktop/src/lib/metadata/tableMetadataCache.ts b/apps/desktop/src/lib/metadata/tableMetadataCache.ts index f7c7654a5c..20cbc04728 100644 --- a/apps/desktop/src/lib/metadata/tableMetadataCache.ts +++ b/apps/desktop/src/lib/metadata/tableMetadataCache.ts @@ -13,6 +13,7 @@ export interface TableMetadata { tableName: string; tableType?: string; catalog?: string; + database?: string; columns: ColumnInfo[]; indexes: IndexInfo[]; primaryKeys: string[]; @@ -72,6 +73,7 @@ export function tableMetadataToDataTabMeta(metadata: TableMetadata, schema = met tableName: metadata.tableName, tableType: metadata.tableType, catalog: metadata.catalog, + database: metadata.database, columns: metadata.columns, primaryKeys: metadata.primaryKeys, }; @@ -104,6 +106,7 @@ export async function loadTableMetadata(request: TableMetadataRequest): Promise< tableName: request.tableName, tableType: request.tableType, catalog: request.catalog, + database: request.database, columns, indexes, primaryKeys, diff --git a/apps/desktop/src/lib/sql/newQueryContext.ts b/apps/desktop/src/lib/sql/newQueryContext.ts index 449f3cd1fd..777e5e458b 100644 --- a/apps/desktop/src/lib/sql/newQueryContext.ts +++ b/apps/desktop/src/lib/sql/newQueryContext.ts @@ -6,14 +6,15 @@ export interface NewQueryTarget { connectionId: string; database: string; schema?: string; + catalog?: string; shouldRefreshDefaultDatabase: boolean; } export type NewQueryContextSource = "tab" | "sidebar"; interface ResolveNewQueryTargetInput { - activeTab?: Pick; - selectedTreeNode?: Pick | null; + activeTab?: Pick; + selectedTreeNode?: Pick | null; activeConnectionId?: string | null; connections: Pick[]; preferredSource?: NewQueryContextSource; @@ -48,7 +49,7 @@ export function resolveNewQueryTarget(input: ResolveNewQueryTargetInput): NewQue : null; } -function targetFromContext(context: Pick | undefined, connections: Pick[]): NewQueryTarget | null { +function targetFromContext(context: Pick | undefined, connections: Pick[]): NewQueryTarget | null { if (!context?.connectionId) return null; const connection = connections.find((item) => item.id === context.connectionId); if (!connection) return null; @@ -57,6 +58,7 @@ function targetFromContext(context: Pick): string { - const { databaseType, schema, tableName, catalog } = options; +export function qualifiedTableName(options: Pick): string { + const { databaseType, schema, tableName, catalog, database } = options; // Doris / StarRocks multi-catalog: address external-catalog tables with the // 3-part `catalog.database.table` form, which the engines accept directly. if (catalog && catalog !== "internal" && (databaseType === "doris" || databaseType === "starrocks")) { const quotedCatalog = quoteTableIdentifier(databaseType, catalog); const quotedTable = quoteTableIdentifier(databaseType, tableName); - const trimmedSchema = schema?.trim(); - if (trimmedSchema) { - return `${quotedCatalog}.${quoteTableIdentifier(databaseType, trimmedSchema)}.${quotedTable}`; + // Doris/StarRocks have no separate schema concept; the database under the + // external catalog is the middle segment. Prefer schema when a caller + // passes it that way, otherwise fall back to database. + const middle = schema?.trim() || database?.trim(); + if (middle) { + return `${quotedCatalog}.${quoteTableIdentifier(databaseType, middle)}.${quotedTable}`; } return `${quotedCatalog}.${quotedTable}`; } diff --git a/apps/desktop/src/stores/__tests__/queryStore.tableDataRefresh.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.tableDataRefresh.spec.ts index 816780db46..6762b126c1 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.tableDataRefresh.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.tableDataRefresh.spec.ts @@ -71,6 +71,7 @@ describe("queryStore table data refresh", () => { const publicTabId = store.createTab("pg-1", "app", "users", "data", "public"); store.setTableMeta(publicTabId, { + database: "analytics", schema: "public", tableName: "users", tableType: "TABLE", @@ -106,6 +107,7 @@ describe("queryStore table data refresh", () => { expect(mocks.buildTableSelectSql).toHaveBeenCalledWith({ databaseType: "postgres", identifierQuote: undefined, + database: "analytics", schema: "public", tableName: "users", tableType: "TABLE", diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 24fb1db00c..3b633e7f4f 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -870,7 +870,7 @@ export const useQueryStore = defineStore("query", () => { return tabs.value.find((tab) => tab.connectionId === connectionId && tab.database === database && tab.title === title && tab.mode === mode && (tab.schema || "") === (schema || "")); } - function createTab(connectionId: string, database: string, title?: string, mode: QueryTab["mode"] = "query", schema?: string, initialSql?: string) { + function createTab(connectionId: string, database: string, title?: string, mode: QueryTab["mode"] = "query", schema?: string, initialSql?: string, catalog?: string) { if (title) { const existing = findTabByIdentity(connectionId, database, title, mode, schema); if (existing) { @@ -887,6 +887,7 @@ export const useQueryStore = defineStore("query", () => { connectionId, database, schema, + catalog, sql: initialSql ?? "", isExecuting: false, isCancelling: false, @@ -1194,7 +1195,7 @@ export const useQueryStore = defineStore("query", () => { tab.structureInitialTabRequestId = (tab.structureInitialTabRequestId ?? 0) + 1; } - function openTableStructure(connectionId: string, database: string, schema?: string, tableName?: string, initialTab?: TableInfoTab, initialTarget?: TableStructureEditorTarget) { + function openTableStructure(connectionId: string, database: string, schema?: string, tableName?: string, initialTab?: TableInfoTab, initialTarget?: TableStructureEditorTarget, catalog?: string) { const resolvedTableName = tableName || ""; if (resolvedTableName) { const existing = tabs.value.find((tab) => tab.mode === "structure" && tab.connectionId === connectionId && tab.database === database && (tab.structureTableName || "") === resolvedTableName); @@ -1213,6 +1214,7 @@ export const useQueryStore = defineStore("query", () => { connectionId, database, schema, + catalog, sql: "", isExecuting: false, isCancelling: false, @@ -1704,6 +1706,7 @@ export const useQueryStore = defineStore("query", () => { const sql = await buildTableSelectSql({ databaseType: effectiveDbType, identifierQuote, + database: tableMeta.database, schema: tableMeta.schema, tableName: tableMeta.tableName, tableType: tableMeta.tableType, @@ -2601,7 +2604,7 @@ export const useQueryStore = defineStore("query", () => { mongoCommands = splitMongoCommandRanges(sql); } const effectiveDbType = effectiveDatabaseTypeForConnection(conn); - const executionDatabase = dataTabExecutionDatabase(conn, tab.database, tab.mode === "data" ? tab.tableMeta?.catalog : undefined); + const executionDatabase = dataTabExecutionDatabase(conn, tab.database, tab.mode === "data" ? tab.tableMeta?.catalog : tab.catalog); const useAgentCursor = usesAgentCursorForQuery(conn?.db_type); const queryTimeoutSecs = queryTimeoutSecsForConnection(conn); const settingsStore = useSettingsStore(); @@ -3044,6 +3047,7 @@ export const useQueryStore = defineStore("query", () => { databaseType: effectiveDbType, identifierQuote: useConnectionStore().connectionIdentifierQuote?.(current.connectionId), catalog: tableMeta.catalog, + database: tableMeta.database, schema: tableMeta.schema, tableName: tableMeta.tableName, whereInput: current.whereInput?.trim() || undefined, @@ -3068,7 +3072,7 @@ export const useQueryStore = defineStore("query", () => { countQueryTotalRowsInBackground({ tabId: id, connectionId: current.connectionId, - database: current.database, + database: executionDatabase, schema: current.schema, countSql, countSqlTarget: dataCountTarget @@ -3672,6 +3676,7 @@ export const useQueryStore = defineStore("query", () => { const sql = await api.buildTableSelectSql({ databaseType: effectiveDbType, identifierQuote, + database: tableMeta.database, schema: tableMeta.schema, tableName: tableMeta.tableName, tableType: tableMeta.tableType, diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index f400aac4b3..876f754f90 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -724,6 +724,9 @@ export interface QueryTab { connectionId: string; database: string; schema?: string; + /** Doris / StarRocks multi-catalog: the external catalog this tab's + * database belongs to (undefined for internal/default catalog). */ + catalog?: string; sql: string; savedSqlId?: string; externalSqlPath?: string; @@ -805,6 +808,7 @@ export interface QueryTab { tableName: string; tableType?: string; catalog?: string; + database?: string; columns: ColumnInfo[]; primaryKeys: string[]; }; diff --git a/crates/dbx-core/src/data_grid_sql.rs b/crates/dbx-core/src/data_grid_sql.rs index 14eeec6fa1..8c20b9b40e 100644 --- a/crates/dbx-core/src/data_grid_sql.rs +++ b/crates/dbx-core/src/data_grid_sql.rs @@ -32,6 +32,10 @@ const DATA_GRID_COLUMN_DISTINCT_VALUES_MAX_LIMIT: usize = 1000; pub struct DataGridTableMeta { #[serde(default, skip_serializing_if = "Option::is_none")] pub catalog: Option, + /// Doris / StarRocks multi-catalog: the database under the external + /// catalog, used as the middle segment of the 3-part qualified name. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub database: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, pub table_name: String, @@ -196,6 +200,8 @@ pub struct DataGridColumnDistinctValuesSqlOptions { #[serde(default, skip_serializing_if = "Option::is_none")] pub catalog: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub database: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, pub table_name: String, pub column_name: String, @@ -220,6 +226,11 @@ pub struct DataGridCountSqlOptions { pub identifier_quote: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub catalog: Option, + /// Doris / StarRocks multi-catalog: the database under the external + /// catalog, used as the middle segment of the 3-part qualified name when + /// `schema` is absent. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub database: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub schema: Option, pub table_name: String, @@ -301,6 +312,7 @@ pub fn build_data_grid_copy_update_statements(options: DataGridCopyUpdateStateme options.database_type, options.table_meta.catalog.as_deref(), options.table_meta.schema.as_deref(), + options.table_meta.database.as_deref(), &options.table_meta.table_name, ); let mut statements = Vec::new(); @@ -383,6 +395,7 @@ pub fn build_data_grid_copy_insert_statement(options: DataGridCopyInsertStatemen options.database_type, meta.catalog.as_deref(), meta.schema.as_deref(), + meta.database.as_deref(), &meta.table_name, ) }, @@ -676,6 +689,7 @@ pub fn build_data_grid_column_distinct_values_sql(options: DataGridColumnDistinc options.database_type, options.catalog.as_deref(), options.schema.as_deref(), + options.database.as_deref(), &options.table_name, ); let column = column_filter_ref(options.database_type, &options.column_name); @@ -735,6 +749,7 @@ pub fn build_data_grid_count_sql(options: DataGridCountSqlOptions) -> String { options.database_type, options.catalog.as_deref(), options.schema.as_deref(), + options.database.as_deref(), &options.table_name, ) }; @@ -953,6 +968,7 @@ fn build_data_grid_save_statements(options: &DataGridSaveStatementOptions) -> Ve options.database_type, options.table_meta.catalog.as_deref(), options.table_meta.schema.as_deref(), + options.table_meta.database.as_deref(), &options.table_meta.table_name, ); let mut statements = Vec::new(); @@ -2325,6 +2341,7 @@ mod tests { database_type: Some(DatabaseType::Postgres), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("public".to_string()), table_name: "users".to_string(), primary_keys: vec!["id".to_string()], @@ -2346,6 +2363,7 @@ mod tests { database_type: Some(DatabaseType::Mysql), table_meta: Some(DataGridTableMeta { catalog: None, + database: None, schema: None, table_name: "users".to_string(), primary_keys: vec!["id".to_string()], @@ -2370,6 +2388,7 @@ mod tests { database_type: Some(DatabaseType::Mysql), table_meta: Some(DataGridTableMeta { catalog: None, + database: None, schema: None, table_name: "users".to_string(), primary_keys: vec!["id".to_string()], @@ -2395,6 +2414,7 @@ mod tests { database_type: Some(DatabaseType::Mysql), table_meta: Some(DataGridTableMeta { catalog: None, + database: None, schema: None, table_name: "users".to_string(), primary_keys: vec!["id".to_string()], @@ -2421,6 +2441,7 @@ mod tests { database_type: Some(DatabaseType::Oracle), table_meta: Some(DataGridTableMeta { catalog: None, + database: None, schema: Some("APP".to_string()), table_name: "USERS".to_string(), primary_keys: vec!["ID".to_string()], @@ -2444,6 +2465,7 @@ mod tests { fn mysql_copy_statements_preserve_blob_hex_literals() { let table_meta = DataGridTableMeta { catalog: None, + database: None, schema: None, table_name: "reports".to_string(), primary_keys: vec!["id".to_string()], @@ -2509,6 +2531,7 @@ mod tests { database_type: Some(DatabaseType::Postgres), table_meta: Some(DataGridTableMeta { catalog: None, + database: None, schema: Some("public".to_string()), table_name: "articles".to_string(), primary_keys: vec!["id".to_string()], @@ -2946,6 +2969,7 @@ mod tests { build_data_grid_column_distinct_values_sql(DataGridColumnDistinctValuesSqlOptions { database_type: Some(DatabaseType::Postgres), catalog: None, + database: None, schema: Some("public".to_string()), table_name: "users".to_string(), column_name: "status".to_string(), @@ -2961,6 +2985,7 @@ mod tests { build_data_grid_column_distinct_values_sql(DataGridColumnDistinctValuesSqlOptions { database_type: Some(DatabaseType::SqlServer), catalog: None, + database: None, schema: None, table_name: "users".to_string(), column_name: "status".to_string(), @@ -2976,6 +3001,7 @@ mod tests { build_data_grid_column_distinct_values_sql(DataGridColumnDistinctValuesSqlOptions { database_type: Some(DatabaseType::SqlServer), catalog: None, + database: None, schema: None, table_name: "users".to_string(), column_name: "id".to_string(), @@ -2991,6 +3017,7 @@ mod tests { build_data_grid_column_distinct_values_sql(DataGridColumnDistinctValuesSqlOptions { database_type: Some(DatabaseType::Oracle), catalog: None, + database: None, schema: Some("APP".to_string()), table_name: "EVENTS".to_string(), column_name: "KIND".to_string(), @@ -3006,6 +3033,7 @@ mod tests { build_data_grid_column_distinct_values_sql(DataGridColumnDistinctValuesSqlOptions { database_type: Some(DatabaseType::Firebird), catalog: None, + database: None, schema: None, table_name: "USERS".to_string(), column_name: "STATUS".to_string(), @@ -3022,6 +3050,7 @@ mod tests { build_data_grid_column_distinct_values_sql(DataGridColumnDistinctValuesSqlOptions { database_type: Some(DatabaseType::Doris), catalog: Some("iceberg_catalog".to_string()), + database: None, schema: Some("sales".to_string()), table_name: "orders".to_string(), column_name: "status".to_string(), @@ -3037,6 +3066,7 @@ mod tests { build_data_grid_column_distinct_values_sql(DataGridColumnDistinctValuesSqlOptions { database_type: Some(DatabaseType::StarRocks), catalog: Some("hive_catalog".to_string()), + database: None, schema: None, table_name: "orders".to_string(), column_name: "status".to_string(), @@ -3053,6 +3083,7 @@ mod tests { build_data_grid_column_distinct_values_sql(DataGridColumnDistinctValuesSqlOptions { database_type: Some(DatabaseType::Doris), catalog: Some("internal".to_string()), + database: None, schema: None, table_name: "orders".to_string(), column_name: "status".to_string(), @@ -3073,6 +3104,7 @@ mod tests { database_type: Some(DatabaseType::Postgres), identifier_quote: None, catalog: None, + database: None, schema: Some("public".to_string()), table_name: "users".to_string(), where_input: Some("WHERE active = true;".to_string()), @@ -3084,17 +3116,32 @@ mod tests { database_type: Some(DatabaseType::Doris), identifier_quote: None, catalog: Some("iceberg_catalog".to_string()), + database: None, schema: Some("sales".to_string()), table_name: "orders".to_string(), where_input: Some("WHERE active = true;".to_string()), }), "SELECT COUNT(*) AS cnt FROM `iceberg_catalog`.`sales`.`orders` WHERE (active = true)" ); + // catalog + database (schema absent) → 3-part `catalog.database.table` + assert_eq!( + build_data_grid_count_sql(DataGridCountSqlOptions { + database_type: Some(DatabaseType::Doris), + identifier_quote: None, + catalog: Some("iceberg_catalog".to_string()), + database: Some("sales".to_string()), + schema: None, + table_name: "orders".to_string(), + where_input: Some("WHERE active = true;".to_string()), + }), + "SELECT COUNT(*) AS cnt FROM `iceberg_catalog`.`sales`.`orders` WHERE (active = true)" + ); assert_eq!( build_data_grid_count_sql(DataGridCountSqlOptions { database_type: Some(DatabaseType::StarRocks), identifier_quote: None, catalog: Some("hive_catalog".to_string()), + database: None, schema: None, table_name: "orders".to_string(), where_input: None, @@ -3106,6 +3153,7 @@ mod tests { database_type: Some(DatabaseType::Kingbase), identifier_quote: Some("`".to_string()), catalog: None, + database: None, schema: Some("cqbq_ls".to_string()), table_name: "ANALYZE".to_string(), where_input: None, @@ -3272,6 +3320,7 @@ mod tests { database_type: Some(DatabaseType::SqlServer), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("dbo".to_string()), table_name: "users".to_string(), primary_keys: vec!["Id".to_string()], @@ -3295,6 +3344,7 @@ mod tests { database_type: Some(DatabaseType::Oracle), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("APP".to_string()), table_name: "EVENTS".to_string(), primary_keys: vec!["ID".to_string()], @@ -3341,6 +3391,7 @@ mod tests { database_type: Some(DatabaseType::SqlServer), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("dbo".to_string()), table_name: "flags".to_string(), primary_keys: vec![], @@ -3372,6 +3423,7 @@ mod tests { database_type: Some(DatabaseType::Mysql), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: None, table_name: "employees".to_string(), primary_keys: vec!["id".to_string()], @@ -3399,6 +3451,7 @@ mod tests { database_type: Some(DatabaseType::Mysql), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: None, table_name: "employees".to_string(), primary_keys: vec!["id".to_string()], @@ -3426,6 +3479,7 @@ mod tests { database_type: Some(DatabaseType::Mysql), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: None, table_name: "employees".to_string(), primary_keys: vec!["id".to_string()], @@ -3453,6 +3507,7 @@ mod tests { database_type: Some(DatabaseType::SqlServer), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("game".to_string()), table_name: "player states".to_string(), primary_keys: vec!["role id".to_string()], @@ -3482,6 +3537,7 @@ mod tests { database_type: Some(DatabaseType::Tdengine), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("dbx_tdengine_demo".to_string()), table_name: "meters".to_string(), primary_keys: vec![DBX_TDENGINE_TBNAME_COLUMN.to_string(), "ts".to_string()], @@ -3512,6 +3568,7 @@ mod tests { database_type: Some(DatabaseType::Tdengine), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("dbx_tdengine_demo".to_string()), table_name: "meters".to_string(), primary_keys: vec![DBX_TDENGINE_TBNAME_COLUMN.to_string(), "ts".to_string(), "seq".to_string()], @@ -3551,6 +3608,7 @@ mod tests { database_type: Some(DatabaseType::Tdengine), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("dbx_tdengine_demo".to_string()), table_name: "codex_grid_accept_20260710".to_string(), primary_keys: vec!["ts".to_string()], @@ -3581,6 +3639,7 @@ mod tests { database_type: Some(DatabaseType::Tdengine), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("dbx_tdengine_demo".to_string()), table_name: "codex_grid_update_verify_20260710".to_string(), primary_keys: vec!["ts".to_string()], @@ -3615,6 +3674,7 @@ mod tests { database_type: Some(DatabaseType::Tdengine), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("dbx_tdengine_demo".to_string()), table_name: "device_a".to_string(), primary_keys: vec!["ts".to_string(), "seq".to_string()], @@ -3652,6 +3712,7 @@ mod tests { database_type: Some(DatabaseType::Tdengine), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("dbx_tdengine_demo".to_string()), table_name: "issue_3121_devices".to_string(), primary_keys: vec![DBX_TDENGINE_TBNAME_COLUMN.to_string(), "ts".to_string()], @@ -3696,6 +3757,7 @@ mod tests { database_type: Some(DatabaseType::Tdengine), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("dbx_tdengine_demo".to_string()), table_name: "meters".to_string(), primary_keys: vec![DBX_TDENGINE_TBNAME_COLUMN.to_string(), "ts".to_string(), "seq".to_string()], @@ -3738,6 +3800,7 @@ mod tests { database_type: Some(DatabaseType::Tdengine), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("dbx_tdengine_demo".to_string()), table_name: "issue_3121_devices".to_string(), primary_keys: vec![DBX_TDENGINE_TBNAME_COLUMN.to_string(), "ts".to_string()], @@ -3765,6 +3828,7 @@ mod tests { database_type: Some(DatabaseType::Tdengine), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("dbx_tdengine_demo".to_string()), table_name: "meters".to_string(), primary_keys: vec![DBX_TDENGINE_TBNAME_COLUMN.to_string(), "ts".to_string()], @@ -3791,6 +3855,7 @@ mod tests { database_type: Some(DatabaseType::Tdengine), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("dbx_tdengine_demo".to_string()), table_name: "device_a".to_string(), primary_keys: vec!["ts".to_string(), "seq".to_string()], @@ -3822,6 +3887,7 @@ mod tests { database_type: Some(DatabaseType::Tdengine), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("dbx_tdengine_demo".to_string()), table_name: "device_a".to_string(), primary_keys: vec!["ts".to_string(), "seq".to_string()], @@ -3850,6 +3916,7 @@ mod tests { database_type: Some(DatabaseType::Databend), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("default".to_string()), table_name: "people".to_string(), primary_keys: vec!["id".to_string()], @@ -3881,6 +3948,7 @@ mod tests { database_type: Some(DatabaseType::ClickHouse), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("default".to_string()), table_name: "people".to_string(), primary_keys: vec!["id".to_string()], @@ -3914,6 +3982,7 @@ mod tests { database_type: Some(DatabaseType::ClickHouse), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("default".to_string()), table_name: "people".to_string(), primary_keys: vec!["id".to_string()], @@ -3945,6 +4014,7 @@ mod tests { database_type: Some(DatabaseType::ClickHouse), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("default".to_string()), table_name: "events".to_string(), primary_keys: vec!["id".to_string()], @@ -3972,6 +4042,7 @@ mod tests { database_type: Some(DatabaseType::ClickHouse), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("default".to_string()), table_name: "events".to_string(), primary_keys: vec!["id".to_string()], @@ -4005,6 +4076,7 @@ mod tests { database_type: Some(DatabaseType::ClickHouse), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("default".to_string()), table_name: "people".to_string(), primary_keys: vec!["id".to_string()], @@ -4022,6 +4094,7 @@ mod tests { fn doris_external_catalog_save_and_copy_statements_use_catalog_scope() { let table_meta = DataGridTableMeta { catalog: Some("iceberg_catalog".to_string()), + database: None, schema: Some("sales".to_string()), table_name: "orders".to_string(), primary_keys: vec!["id".to_string()], @@ -4082,6 +4155,7 @@ mod tests { database_type: Some(DatabaseType::Databend), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("default".to_string()), table_name: "people".to_string(), primary_keys: vec![], @@ -4110,6 +4184,7 @@ mod tests { database_type: Some(DatabaseType::Oscar), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("SYSDBA".to_string()), table_name: "PEOPLE".to_string(), primary_keys: vec![], @@ -4138,6 +4213,7 @@ mod tests { database_type: Some(DatabaseType::Postgres), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("public".to_string()), table_name: "ihli_data".to_string(), primary_keys: vec!["iso3".to_string(), "year".to_string()], @@ -4171,6 +4247,7 @@ mod tests { database_type: Some(DatabaseType::Mysql), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: None, table_name: "policies".to_string(), primary_keys: vec!["id".to_string()], @@ -4222,6 +4299,7 @@ mod tests { database_type: Some(DatabaseType::Mysql), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: None, table_name: "school".to_string(), primary_keys: vec!["id".to_string()], @@ -4247,6 +4325,7 @@ mod tests { database_type: Some(DatabaseType::Mysql), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: None, table_name: "parts".to_string(), primary_keys: vec![], @@ -4278,6 +4357,7 @@ mod tests { database_type: Some(DatabaseType::ManticoreSearch), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: None, table_name: "rt_products".to_string(), primary_keys: vec![], @@ -4308,6 +4388,7 @@ mod tests { database_type: Some(DatabaseType::Postgres), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: None, table_name: "education_data".to_string(), primary_keys: vec!["country_code".to_string(), "year".to_string()], @@ -4345,6 +4426,7 @@ mod tests { database_type: Some(DatabaseType::Sqlite), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: None, table_name: "OnlineLogs".to_string(), primary_keys: vec!["OnlineLogId".to_string()], @@ -4371,6 +4453,7 @@ mod tests { database_type: Some(DatabaseType::Sqlite), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: None, table_name: "OnlineLogs".to_string(), primary_keys: vec!["OnlineLogId".to_string()], @@ -4400,6 +4483,7 @@ mod tests { database_type: Some(DatabaseType::Mysql), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("app".to_string()), table_name: "users".to_string(), primary_keys: vec!["id".to_string()], @@ -4426,6 +4510,7 @@ mod tests { database_type: Some(DatabaseType::Sqlite), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: None, table_name: "OnlineLogs".to_string(), primary_keys: vec!["OnlineLogId".to_string()], diff --git a/crates/dbx-core/src/sql_dialect/identifiers.rs b/crates/dbx-core/src/sql_dialect/identifiers.rs index e4fef2e49b..ad80e74b1d 100644 --- a/crates/dbx-core/src/sql_dialect/identifiers.rs +++ b/crates/dbx-core/src/sql_dialect/identifiers.rs @@ -72,25 +72,41 @@ pub fn qualified_table_name(database_type: Option, schema: Option< } /// Like `qualified_table_name`, but prefixes a Doris/StarRocks external -/// catalog (`..`) when `catalog` is present, non-empty, -/// and not the engine's `internal` catalog. The `internal` guard is defensive — -/// built-in-catalog tables never carry a catalog in the first place (the -/// sidebar routes them through the standard path), so this only ever prefixes -/// genuine external catalogs. Other engines ignore `catalog` (they have no -/// 3-part catalog naming). +/// catalog (`..
`) when `catalog` is present, +/// non-empty, and not the engine's `internal` catalog. The middle segment is +/// the database — Doris/StarRocks have no separate schema concept, so +/// `schema` is only used when a caller passes it that way; otherwise `database` +/// fills the middle slot. When neither is set the name degrades to the 2-part +/// `.
` form. The `internal` guard is defensive — built-in +/// catalog tables never carry a catalog in the first place (the sidebar routes +/// them through the standard path), so this only ever prefixes genuine +/// external catalogs. Other engines ignore `catalog` (they have no 3-part +/// catalog naming). pub fn qualified_table_name_with_catalog( database_type: Option, catalog: Option<&str>, schema: Option<&str>, + database: Option<&str>, table_name: &str, ) -> String { - let table = qualified_table_name(database_type, schema, table_name); let catalog = catalog.map(str::trim).filter(|catalog| !catalog.is_empty() && *catalog != "internal"); match (catalog, database_type) { (Some(catalog), Some(DatabaseType::Doris | DatabaseType::StarRocks)) => { + let middle = schema + .map(str::trim) + .filter(|schema| !schema.is_empty()) + .or_else(|| database.map(str::trim).filter(|database| !database.is_empty())); + let table = match middle { + Some(middle) => format!( + "{}.{}", + quote_table_identifier(database_type, middle), + quote_table_identifier(database_type, table_name) + ), + None => quote_table_identifier(database_type, table_name), + }; format!("{}.{}", quote_table_identifier(database_type, catalog), table) } - _ => table, + _ => qualified_table_name(database_type, schema, table_name), } } diff --git a/crates/dbx-core/src/sql_dialect/table_select.rs b/crates/dbx-core/src/sql_dialect/table_select.rs index 394272dfc3..d238f7aa27 100644 --- a/crates/dbx-core/src/sql_dialect/table_select.rs +++ b/crates/dbx-core/src/sql_dialect/table_select.rs @@ -33,6 +33,7 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String database_type, options.catalog.as_deref(), options.schema.as_deref(), + options.database.as_deref(), &options.table_name, ) }; @@ -458,13 +459,19 @@ pub(super) fn build_questdb_table_select_sql( mod tests { use super::*; - fn opts(database_type: DatabaseType, catalog: Option<&str>, table: &str) -> TableDataSelectSqlOptions { + fn opts( + database_type: DatabaseType, + catalog: Option<&str>, + database: Option<&str>, + table: &str, + ) -> TableDataSelectSqlOptions { TableDataSelectSqlOptions { database_type: Some(database_type), identifier_quote: None, schema: None, table_name: table.to_string(), catalog: catalog.map(|c| c.to_string()), + database: database.map(|d| d.to_string()), table_type: None, primary_keys: Vec::new(), columns: Vec::new(), @@ -479,32 +486,42 @@ mod tests { #[test] fn doris_external_catalog_prefixes_from_clause() { - let sql = build_table_data_select_sql(opts(DatabaseType::Doris, Some("iceberg_catalog"), "orders")); - assert!(sql.contains("FROM `iceberg_catalog`.`orders`"), "sql was: {sql}"); + let sql = + build_table_data_select_sql(opts(DatabaseType::Doris, Some("iceberg_catalog"), Some("sales"), "orders")); + assert!(sql.contains("FROM `iceberg_catalog`.`sales`.`orders`"), "sql was: {sql}"); } #[test] fn starrocks_external_catalog_prefixes_from_clause() { - let sql = build_table_data_select_sql(opts(DatabaseType::StarRocks, Some("hive_catalog"), "orders")); - assert!(sql.contains("FROM `hive_catalog`.`orders`"), "sql was: {sql}"); + let sql = + build_table_data_select_sql(opts(DatabaseType::StarRocks, Some("hive_catalog"), Some("sales"), "orders")); + assert!(sql.contains("FROM `hive_catalog`.`sales`.`orders`"), "sql was: {sql}"); + } + + #[test] + fn doris_external_catalog_without_database_degrades_to_two_part() { + // When neither schema nor database is provided the name degrades to the + // 2-part `catalog.table` form. + let sql = build_table_data_select_sql(opts(DatabaseType::Doris, Some("iceberg_catalog"), None, "orders")); + assert!(sql.contains("FROM `iceberg_catalog`.`orders`"), "sql was: {sql}"); } #[test] fn doris_internal_catalog_is_not_prefixed() { - let sql = build_table_data_select_sql(opts(DatabaseType::Doris, Some("internal"), "orders")); + let sql = build_table_data_select_sql(opts(DatabaseType::Doris, Some("internal"), None, "orders")); assert!(!sql.contains("internal"), "sql was: {sql}"); assert!(sql.contains("FROM `orders`"), "sql was: {sql}"); } #[test] fn doris_empty_catalog_is_not_prefixed() { - let sql = build_table_data_select_sql(opts(DatabaseType::Doris, Some(" "), "orders")); + let sql = build_table_data_select_sql(opts(DatabaseType::Doris, Some(" "), None, "orders")); assert!(sql.contains("FROM `orders`"), "sql was: {sql}"); } #[test] fn doris_no_catalog_is_not_prefixed() { - let sql = build_table_data_select_sql(opts(DatabaseType::Doris, None, "orders")); + let sql = build_table_data_select_sql(opts(DatabaseType::Doris, None, None, "orders")); assert!(sql.contains("FROM `orders`"), "sql was: {sql}"); } @@ -512,7 +529,8 @@ mod tests { fn external_catalog_is_ignored_for_non_doris_engines() { // Postgres does not support the 3-part catalog naming; the catalog // must be ignored to avoid emitting an invalid qualified name. - let sql = build_table_data_select_sql(opts(DatabaseType::Postgres, Some("iceberg_catalog"), "orders")); + let sql = + build_table_data_select_sql(opts(DatabaseType::Postgres, Some("iceberg_catalog"), Some("sales"), "orders")); assert!(!sql.contains("iceberg_catalog"), "sql was: {sql}"); assert!(sql.contains("orders"), "sql was: {sql}"); } diff --git a/crates/dbx-core/src/sql_dialect/types.rs b/crates/dbx-core/src/sql_dialect/types.rs index 21aca4629d..5e6fd1b37c 100644 --- a/crates/dbx-core/src/sql_dialect/types.rs +++ b/crates/dbx-core/src/sql_dialect/types.rs @@ -28,9 +28,14 @@ pub struct TableDataSelectSqlOptions { pub table_name: String, /// Doris / StarRocks multi-catalog: when set to a non-`internal` catalog, /// the FROM clause is prefixed with the catalog - /// (`..
`). + /// (`..
`). #[serde(default, skip_serializing_if = "Option::is_none")] pub catalog: Option, + /// Doris / StarRocks multi-catalog: the database under the external + /// catalog, used as the middle segment of the 3-part qualified name when + /// `schema` is absent (Doris/StarRocks have no separate schema concept). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub database: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub table_type: Option, #[serde(default)] diff --git a/crates/dbx-core/tests/live_postgres_tsvector.rs b/crates/dbx-core/tests/live_postgres_tsvector.rs index d85994e63b..d3b3809d27 100644 --- a/crates/dbx-core/tests/live_postgres_tsvector.rs +++ b/crates/dbx-core/tests/live_postgres_tsvector.rs @@ -62,6 +62,7 @@ async fn postgres_tsvector_generated_columns_are_readable_and_omitted_from_inser let table_meta = DataGridTableMeta { catalog: None, + database: None, schema: Some(schema.clone()), table_name: "articles".to_string(), primary_keys: vec!["id".to_string()], diff --git a/crates/dbx-core/tests/mysql_cross_database_editing.rs b/crates/dbx-core/tests/mysql_cross_database_editing.rs index 611f04db5e..05ca62a97d 100644 --- a/crates/dbx-core/tests/mysql_cross_database_editing.rs +++ b/crates/dbx-core/tests/mysql_cross_database_editing.rs @@ -56,6 +56,7 @@ fn mysql_cross_database_query_flow_preserves_target_database() { database_type: Some(DatabaseType::Mysql), table_meta: DataGridTableMeta { catalog: None, + database: None, schema: Some("db_9".to_string()), table_name: "users".to_string(), primary_keys: vec!["id".to_string()], diff --git a/packages/app-tests/newQueryContext.test.ts b/packages/app-tests/newQueryContext.test.ts index e3d5b744f7..17445cf46d 100644 --- a/packages/app-tests/newQueryContext.test.ts +++ b/packages/app-tests/newQueryContext.test.ts @@ -48,6 +48,7 @@ test("new query target prefers the active data tab context", () => { connectionId: "conn-data", database: "analytics", schema: undefined, + catalog: undefined, shouldRefreshDefaultDatabase: false, }); }); @@ -73,6 +74,7 @@ test("new query target uses the selected sidebar node when there is no active ta connectionId: "conn-tree", database: "reporting", schema: "public", + catalog: undefined, shouldRefreshDefaultDatabase: false, }); }); @@ -96,6 +98,7 @@ test("new query target prefers the selected sidebar node after sidebar focus", ( connectionId: "conn-tree", database: "reporting", schema: undefined, + catalog: undefined, shouldRefreshDefaultDatabase: false, }); }); @@ -112,6 +115,7 @@ test("new query target refreshes default database for connection-only sidebar no connectionId: "conn-tree", database: "saved_default", schema: undefined, + catalog: undefined, shouldRefreshDefaultDatabase: true, }); });