fix(doris): qualify external catalog tables
This commit is contained in:
parent
c9c2fddfe2
commit
a9ca66a829
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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()],
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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()],
|
||||
|
|
|
|||
|
|
@ -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()],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue