fix(doris): qualify external catalog tables

This commit is contained in:
jischeng 2026-07-13 23:56:53 +08:00 committed by GitHub
parent c9c2fddfe2
commit a9ca66a829
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 222 additions and 44 deletions

View File

@ -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) {

View File

@ -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,

View File

@ -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<ObjectBrowserRow[]> {
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.");

View File

@ -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,

View File

@ -52,6 +52,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
return buildTableSelectSql({
databaseType: effectiveDbType,
identifierQuote: connectionStore.connectionIdentifierQuote?.(tab.connectionId),
database: tableMeta?.database,
schema: tableMeta?.schema,
tableName: tableMeta?.tableName ?? "",
tableType: tableMeta?.tableType,

View File

@ -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,

View File

@ -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;

View File

@ -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";
}

View File

@ -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,

View File

@ -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<QueryTab, "connectionId" | "database" | "schema">;
selectedTreeNode?: Pick<TreeNode, "connectionId" | "database" | "schema"> | null;
activeTab?: Pick<QueryTab, "connectionId" | "database" | "schema" | "catalog">;
selectedTreeNode?: Pick<TreeNode, "connectionId" | "database" | "schema" | "catalog"> | null;
activeConnectionId?: string | null;
connections: Pick<ConnectionConfig, "id" | "database">[];
preferredSource?: NewQueryContextSource;
@ -48,7 +49,7 @@ export function resolveNewQueryTarget(input: ResolveNewQueryTargetInput): NewQue
: null;
}
function targetFromContext(context: Pick<QueryTab | TreeNode, "connectionId" | "database" | "schema"> | undefined, connections: Pick<ConnectionConfig, "id" | "database">[]): NewQueryTarget | null {
function targetFromContext(context: Pick<QueryTab | TreeNode, "connectionId" | "database" | "schema" | "catalog"> | undefined, connections: Pick<ConnectionConfig, "id" | "database">[]): 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<QueryTab | TreeNode, "connectionId" | "
connectionId: context.connectionId,
database,
schema: "schema" in context ? (context as { schema?: string }).schema : undefined,
catalog: "catalog" in context ? (context as { catalog?: string }).catalog : undefined,
shouldRefreshDefaultDatabase: !context.database,
};
}

View File

@ -18,6 +18,7 @@ export interface BuildTableSelectSqlOptions {
whereInput?: string;
includeRowId?: boolean;
catalog?: string;
database?: string;
}
export function quoteTableIdentifier(databaseType: DatabaseType | undefined, name: string): string {
@ -45,16 +46,19 @@ function quoteCypherIdentifier(name: string): string {
return `\`${name.replace(/`/g, "``")}\``;
}
export function qualifiedTableName(options: Pick<BuildTableSelectSqlOptions, "databaseType" | "schema" | "tableName" | "catalog">): string {
const { databaseType, schema, tableName, catalog } = options;
export function qualifiedTableName(options: Pick<BuildTableSelectSqlOptions, "databaseType" | "schema" | "tableName" | "catalog" | "database">): 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}`;
}

View File

@ -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",

View File

@ -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,

View File

@ -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[];
};

View File

@ -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<String>,
/// 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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema: Option<String>,
pub table_name: String,
@ -196,6 +200,8 @@ pub struct DataGridColumnDistinctValuesSqlOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub catalog: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub database: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema: Option<String>,
pub table_name: String,
pub column_name: String,
@ -220,6 +226,11 @@ pub struct DataGridCountSqlOptions {
pub identifier_quote: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub catalog: Option<String>,
/// 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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema: Option<String>,
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()],

View File

@ -72,25 +72,41 @@ pub fn qualified_table_name(database_type: Option<DatabaseType>, schema: Option<
}
/// Like `qualified_table_name`, but prefixes a Doris/StarRocks external
/// catalog (`<catalog>.<schema>.<table>`) 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 (`<catalog>.<database>.<table>`) 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
/// `<catalog>.<table>` 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<DatabaseType>,
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),
}
}

View File

@ -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}");
}

View File

@ -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
/// (`<catalog>.<schema>.<table>`).
/// (`<catalog>.<database>.<table>`).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub catalog: Option<String>,
/// 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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub table_type: Option<String>,
#[serde(default)]

View File

@ -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()],

View File

@ -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()],

View File

@ -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,
});
});