fix(phoenix): preserve schema in all table references
This commit is contained in:
1 parent
e139f63953
commit
f2e13922a2
18 files changed
+200
-9
No files matched your search
@@ -1686,6 +1686,7 @@ async function newQuery() {
|
||||
targetConnectionId: target.connectionId,
|
||||
targetDatabase: target.database,
|
||||
databaseType: effectiveDatabaseTypeForConnection(conn),
|
||||
driverProfile: conn.driver_profile,
|
||||
identifierQuote: connectionStore.connectionIdentifierQuote?.(target.connectionId),
|
||||
});
|
||||
const tabId = queryStore.createTab(conn.id, target.database, undefined, "query", target.schema, initialSql, target.catalog);
|
||||
|
||||
@@ -1719,7 +1719,7 @@ async function loadTemplateContext(allowView = false) {
|
||||
}
|
||||
|
||||
const identifierQuote = connectionStore.connectionIdentifierQuote(node.connectionId);
|
||||
return { node, dbType, identifierQuote, tableSchema, columns, tableType };
|
||||
return { node, dbType, driverProfile: config?.driver_profile, identifierQuote, tableSchema, columns, tableType };
|
||||
}
|
||||
|
||||
function openSqlTemplateTab(connectionId: string, database: string, schema: string | undefined, catalog: string | undefined, sql: string, title?: string) {
|
||||
@@ -1733,6 +1733,7 @@ async function newSelectTemplate() {
|
||||
if (!context) return;
|
||||
const sql = buildTableSelectTemplate({
|
||||
databaseType: context.dbType,
|
||||
driverProfile: context.driverProfile,
|
||||
identifierQuote: context.identifierQuote,
|
||||
catalog: context.node.catalog,
|
||||
database: context.node.database,
|
||||
@@ -1752,6 +1753,7 @@ async function newInsertTemplate() {
|
||||
if (!context) return;
|
||||
const sql = buildTableInsertTemplate({
|
||||
databaseType: context.dbType,
|
||||
driverProfile: context.driverProfile,
|
||||
identifierQuote: context.identifierQuote,
|
||||
catalog: context.node.catalog,
|
||||
database: context.node.database,
|
||||
@@ -1772,6 +1774,7 @@ async function newUpdateTemplate() {
|
||||
if (!context) return;
|
||||
const sql = buildTableUpdateTemplate({
|
||||
databaseType: context.dbType,
|
||||
driverProfile: context.driverProfile,
|
||||
identifierQuote: context.identifierQuote,
|
||||
catalog: context.node.catalog,
|
||||
database: context.node.database,
|
||||
@@ -1791,6 +1794,7 @@ async function newDeleteTemplate() {
|
||||
if (!context) return;
|
||||
const sql = buildTableDeleteTemplate({
|
||||
databaseType: context.dbType,
|
||||
driverProfile: context.driverProfile,
|
||||
identifierQuote: context.identifierQuote,
|
||||
catalog: context.node.catalog,
|
||||
database: context.node.database,
|
||||
|
||||
@@ -208,6 +208,10 @@ function currentDatabaseType(): DatabaseType | undefined {
|
||||
return activeNode.value.connectionId ? effectiveDatabaseTypeForConnection(connectionStore.getConfig(activeNode.value.connectionId)) : undefined;
|
||||
}
|
||||
|
||||
function currentDriverProfile(): string | undefined {
|
||||
return activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId)?.driver_profile : undefined;
|
||||
}
|
||||
|
||||
function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
|
||||
switch (node.type) {
|
||||
case "connection":
|
||||
@@ -1116,6 +1120,7 @@ function tableReferenceDragPayload(): QueryEditorTableReferencePayload | null {
|
||||
database: activeNode.value.database,
|
||||
referenceType: "database",
|
||||
databaseType: currentDatabaseType(),
|
||||
driverProfile: currentDriverProfile(),
|
||||
});
|
||||
}
|
||||
if (activeNode.value.type === "column") {
|
||||
@@ -1128,6 +1133,7 @@ function tableReferenceDragPayload(): QueryEditorTableReferencePayload | null {
|
||||
tableName: activeNode.value.tableName,
|
||||
columnName,
|
||||
databaseType: currentDatabaseType(),
|
||||
driverProfile: currentDriverProfile(),
|
||||
});
|
||||
}
|
||||
const payload = createTableReferencePayload({
|
||||
@@ -1136,6 +1142,7 @@ function tableReferenceDragPayload(): QueryEditorTableReferencePayload | null {
|
||||
schema: activeNode.value.schema,
|
||||
tableName: activeNode.value.label,
|
||||
databaseType: currentDatabaseType(),
|
||||
driverProfile: currentDriverProfile(),
|
||||
});
|
||||
return payload;
|
||||
}
|
||||
|
||||
@@ -359,6 +359,31 @@ describe("useSidebarDataOpenRuntime", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves JDBC table schema through the table-data request", async () => {
|
||||
mocks.databaseType = "jdbc";
|
||||
mocks.loadTableMetadata.mockImplementation(async (request: { database: string; schema?: string; tableName: string; tableType?: string }) => ({
|
||||
metadata: {
|
||||
schema: request.schema,
|
||||
tableName: request.tableName,
|
||||
tableType: request.tableType,
|
||||
database: request.database,
|
||||
columns: [{ name: "id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null }],
|
||||
indexes: [],
|
||||
primaryKeys: ["id"],
|
||||
cachedAt: Date.now(),
|
||||
},
|
||||
cacheStatus: "miss",
|
||||
ageMs: 0,
|
||||
}));
|
||||
|
||||
await useSidebarDataOpenRuntime().openData(tableNode);
|
||||
|
||||
await vi.waitFor(() => expect(mocks.tabs[0]?.tableMeta?.primaryKeys).toEqual(["id"]));
|
||||
expect(mocks.loadTableMetadata).toHaveBeenCalledWith(expect.objectContaining({ database: "app", schema: "public" }));
|
||||
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(expect.objectContaining({ database: "app", schema: "public", tableName: "users" }));
|
||||
expect(mocks.tabs[0]?.tableMeta).toMatchObject({ database: "app", schema: "public", tableName: "users" });
|
||||
});
|
||||
|
||||
it("keeps MySQL data-tab identity unqualified after metadata loads", async () => {
|
||||
mocks.databaseType = "mysql";
|
||||
mocks.loadTableMetadata.mockImplementation(async (request: { database: string; schema?: string; tableName: string; tableType?: string }) => ({
|
||||
|
||||
@@ -73,6 +73,17 @@ describe("jdbc dialect inference", () => {
|
||||
).toBe("sqlserver");
|
||||
});
|
||||
|
||||
it("keeps Phoenix as generic JDBC while preserving its schema tree", () => {
|
||||
const connection = { db_type: "jdbc" as const, driver_profile: "phoenix" };
|
||||
|
||||
expect(inferJdbcDialect(connection)).toBe("jdbc");
|
||||
expect(effectiveDatabaseTypeForConnection(connection)).toBe("jdbc");
|
||||
expect(connectionUsesDatabaseObjectTreeMode(connection)).toBe(false);
|
||||
expect(connectionObjectTreeQuerySchema(connection, "default", "DEMO")).toBe("DEMO");
|
||||
expect(connectionObjectTreeNodeSchema(connection, "default", "DEMO")).toBe("DEMO");
|
||||
expect(connectionShouldLoadIdentifierQuote(connection)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["jdbc:oracle:thin:@//localhost:1521/XE", "oracle"],
|
||||
["jdbc:dm://localhost:5236/DAMENG", "dameng"],
|
||||
|
||||
@@ -14,6 +14,20 @@ describe("query editor table reference drop", () => {
|
||||
expect(tableReferenceInsertText(payload!)).toBe("`app-db`");
|
||||
});
|
||||
|
||||
it("preserves the Phoenix schema in dragged table references", () => {
|
||||
const payload = createTableReferencePayload({
|
||||
connectionId: "conn-1",
|
||||
database: "default",
|
||||
schema: "APP",
|
||||
tableName: "USERS",
|
||||
databaseType: "jdbc",
|
||||
driverProfile: "phoenix",
|
||||
})!;
|
||||
|
||||
expect(parseTableReferencePayload(JSON.stringify(payload))).toEqual(payload);
|
||||
expect(tableReferenceInsertText(payload)).toBe("APP.USERS");
|
||||
});
|
||||
|
||||
it("round-trips database reference payloads", () => {
|
||||
const payload = createTableReferencePayload({
|
||||
connectionId: "conn-1",
|
||||
|
||||
@@ -204,6 +204,10 @@ describe("buildSelectAllSql", () => {
|
||||
expect(buildSelectAllSql("postgres", { schema: "public", tableName: "users" })).toBe('SELECT * FROM "public"."users"');
|
||||
});
|
||||
|
||||
it("preserves the Phoenix schema for new-query prefill", () => {
|
||||
expect(buildSelectAllSql("jdbc", { schema: "APP", tableName: "USERS" }, '"', "phoenix")).toBe('SELECT * FROM "APP"."USERS"');
|
||||
});
|
||||
|
||||
it("bracket-quotes a SQL Server table", () => {
|
||||
expect(buildSelectAllSql("sqlserver", { schema: "dbo", tableName: "users" })).toBe("SELECT * FROM [dbo].[users]");
|
||||
expect(buildSelectAllSql("sqlserver", { tableName: "users" })).toBe("SELECT * FROM [users]");
|
||||
@@ -258,6 +262,20 @@ describe("resolveNewQueryInitialSql", () => {
|
||||
).toBe('SELECT * FROM "public"."users"');
|
||||
});
|
||||
|
||||
it("passes the Phoenix driver profile into the initial SQL builder", () => {
|
||||
expect(
|
||||
resolveNewQueryInitialSql({
|
||||
activeTab: dataTab({ schema: "APP", tableMeta: { schema: "APP", tableName: "USERS", columns: [], primaryKeys: [] } }),
|
||||
prefillEnabled: true,
|
||||
targetConnectionId: "conn-1",
|
||||
targetDatabase: "app_db",
|
||||
databaseType: "jdbc",
|
||||
driverProfile: "phoenix",
|
||||
identifierQuote: '"',
|
||||
}),
|
||||
).toBe('SELECT * FROM "APP"."USERS"');
|
||||
});
|
||||
|
||||
it("leaves new queries empty when the setting is disabled", () => {
|
||||
expect(
|
||||
resolveNewQueryInitialSql({
|
||||
|
||||
@@ -34,6 +34,20 @@ describe("qualifiedTableName — SQLite attached databases", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("qualifiedTableName — schema-aware JDBC profiles", () => {
|
||||
it("qualifies Phoenix tables without forcing identifier quotes", () => {
|
||||
expect(qualifiedTableName({ databaseType: "jdbc", driverProfile: "phoenix", schema: "DEMO", tableName: "STUDENT" })).toBe("DEMO.STUDENT");
|
||||
});
|
||||
|
||||
it("uses the driver-reported quote for Phoenix special identifiers", () => {
|
||||
expect(qualifiedTableName({ databaseType: "jdbc", driverProfile: "phoenix", identifierQuote: '"', schema: "MY_SCHEMA", tableName: "ORDER" })).toBe('"MY_SCHEMA"."ORDER"');
|
||||
});
|
||||
|
||||
it("keeps an unscoped JDBC table unqualified", () => {
|
||||
expect(qualifiedTableName({ databaseType: "jdbc", driverProfile: "phoenix", tableName: "STUDENT" })).toBe("STUDENT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("qualifiedTableName — GBase 8s", () => {
|
||||
it("omits the metadata owner for GBase 8s table data", () => {
|
||||
expect(qualifiedTableName({ databaseType: "informix", driverProfile: "gbase8s", identifierQuote: "", schema: "gbasedbt", tableName: "connection_smoke" })).toBe("connection_smoke");
|
||||
|
||||
@@ -21,6 +21,9 @@ const CONNECTION_ROOT_SCHEMA_TYPES = new Set<DatabaseType>(["oracle", "dameng",
|
||||
const JDBC_DIALECT_MATCHERS: Array<{ type: DatabaseType; patterns: RegExp[] }> = [
|
||||
{ type: "databend", patterns: [/jdbc:databend:/i, /com\.databend\.jdbc\.DatabendDriver/i, /databend-jdbc/i] },
|
||||
{ type: "starrocks", patterns: [/starrocks/i] },
|
||||
// Phoenix remains a generic JDBC connection, but exposes queryable schemas.
|
||||
// Returning `jdbc` keeps its generic SQL behavior while enabling the schema tree.
|
||||
{ type: "jdbc", patterns: [/phoenix/i] },
|
||||
{ type: "doris", patterns: [/doris/i] },
|
||||
{ type: "goldendb", patterns: [/jdbc:goldendb:/i, /goldendb/i] },
|
||||
{ type: "mysql", patterns: [/kyuubi/i] },
|
||||
@@ -54,6 +57,10 @@ export function inferJdbcDialect(connection?: JdbcDialectConnection): DatabaseTy
|
||||
return JDBC_DIALECT_MATCHERS.find((matcher) => matcher.patterns.some((pattern) => pattern.test(haystack)))?.type;
|
||||
}
|
||||
|
||||
export function jdbcDriverProfileUsesSchemaQualification(driverProfile?: string): boolean {
|
||||
return inferJdbcDialect({ db_type: "jdbc", driver_profile: driverProfile }) === "jdbc";
|
||||
}
|
||||
|
||||
export function effectiveDatabaseTypeForConnection(connection?: JdbcDialectConnection): DatabaseType | undefined {
|
||||
if (!connection) return undefined;
|
||||
if (connection.db_type === "gbase" && isGbase8sProfile(connection.driver_profile)) return "informix";
|
||||
@@ -90,7 +97,8 @@ export function connectionShouldLoadIdentifierQuote(connection: JdbcDialectConne
|
||||
if (gaussdbIdentifierQuoteStyle(connection) !== "auto") return false;
|
||||
if (connection.db_type === "gaussdb") return true;
|
||||
if (connection.db_type !== "jdbc") return false;
|
||||
return ["gaussdb", "opengauss", "postgres"].includes(inferJdbcDialect(connection) ?? "");
|
||||
const dialect = inferJdbcDialect(connection);
|
||||
return dialect === "jdbc" || ["gaussdb", "opengauss", "postgres"].includes(dialect ?? "");
|
||||
}
|
||||
|
||||
export function supportsGaussdbIdentifierQuoteStyle(connection: JdbcDialectConnection | undefined): boolean {
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface QueryEditorTableReferencePayload {
|
||||
columnName?: string;
|
||||
referenceType?: "database" | "table" | "column";
|
||||
databaseType?: DatabaseType;
|
||||
driverProfile?: string;
|
||||
}
|
||||
|
||||
export interface QueryEditorTableReferenceDropDetail {
|
||||
@@ -23,7 +24,7 @@ export interface QueryEditorTableReferenceDropDetail {
|
||||
|
||||
let activeTableReferencePayload: QueryEditorTableReferencePayload | null = null;
|
||||
|
||||
export function createTableReferencePayload(options: { connectionId?: string; database?: string; schema?: string; tableName?: string; columnName?: string; referenceType?: "database" | "table" | "column"; databaseType?: DatabaseType }): QueryEditorTableReferencePayload | null {
|
||||
export function createTableReferencePayload(options: { connectionId?: string; database?: string; schema?: string; tableName?: string; columnName?: string; referenceType?: "database" | "table" | "column"; databaseType?: DatabaseType; driverProfile?: string }): QueryEditorTableReferencePayload | null {
|
||||
if (!options.connectionId || options.database == null) return null;
|
||||
const referenceType = options.referenceType ?? (options.columnName ? "column" : "table");
|
||||
if (referenceType !== "database" && !options.tableName) return null;
|
||||
@@ -43,6 +44,7 @@ export function createTableReferencePayload(options: { connectionId?: string; da
|
||||
}
|
||||
if (options.schema) payload.schema = options.schema;
|
||||
if (options.databaseType) payload.databaseType = options.databaseType;
|
||||
if (options.driverProfile) payload.driverProfile = options.driverProfile;
|
||||
return payload;
|
||||
}
|
||||
|
||||
@@ -65,6 +67,7 @@ export function parseTableReferencePayload(value: string | undefined | null): Qu
|
||||
referenceType: "database",
|
||||
};
|
||||
if (parsed.databaseType) payload.databaseType = parsed.databaseType;
|
||||
if (typeof parsed.driverProfile === "string" && parsed.driverProfile) payload.driverProfile = parsed.driverProfile;
|
||||
return payload;
|
||||
}
|
||||
if (typeof parsed.tableName !== "string" || !parsed.tableName) return null;
|
||||
@@ -83,6 +86,7 @@ export function parseTableReferencePayload(value: string | undefined | null): Qu
|
||||
}
|
||||
if (typeof parsed.schema === "string" && parsed.schema) payload.schema = parsed.schema;
|
||||
if (parsed.databaseType) payload.databaseType = parsed.databaseType;
|
||||
if (typeof parsed.driverProfile === "string" && parsed.driverProfile) payload.driverProfile = parsed.driverProfile;
|
||||
return payload;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -126,6 +130,7 @@ export function tableReferenceInsertText(payload: QueryEditorTableReferencePaylo
|
||||
const tableName = payload.tableName || payload.database;
|
||||
return qualifiedTableName({
|
||||
databaseType,
|
||||
driverProfile: payload.driverProfile,
|
||||
schema: payload.schema,
|
||||
tableName,
|
||||
});
|
||||
|
||||
@@ -90,6 +90,7 @@ export interface ResolveNewQueryInitialSqlInput extends ResolveNewQueryTableInpu
|
||||
targetConnectionId: string;
|
||||
targetDatabase: string;
|
||||
databaseType?: DatabaseType;
|
||||
driverProfile?: string;
|
||||
identifierQuote?: string;
|
||||
}
|
||||
|
||||
@@ -146,9 +147,9 @@ export function resolveNewQueryTable(input: ResolveNewQueryTableInput): NewQuery
|
||||
* the same per-dialect identifier quoting and schema/catalog qualification used
|
||||
* by the table-data view.
|
||||
*/
|
||||
export function buildSelectAllSql(databaseType: DatabaseType | undefined, table: Pick<NewQueryTable, "schema" | "catalog" | "tableName"> & Partial<Pick<NewQueryTable, "database">>, identifierQuote?: string): string {
|
||||
export function buildSelectAllSql(databaseType: DatabaseType | undefined, table: Pick<NewQueryTable, "schema" | "catalog" | "tableName"> & Partial<Pick<NewQueryTable, "database">>, identifierQuote?: string, driverProfile?: string): string {
|
||||
if (databaseType === "victoriametrics") return metricRangeQuery(table.tableName);
|
||||
const ref = qualifiedTableName({ databaseType, identifierQuote, database: table.database, schema: table.schema, catalog: table.catalog, tableName: table.tableName });
|
||||
const ref = qualifiedTableName({ databaseType, driverProfile, identifierQuote, database: table.database, schema: table.schema, catalog: table.catalog, tableName: table.tableName });
|
||||
return `SELECT * FROM ${ref}`;
|
||||
}
|
||||
|
||||
@@ -163,5 +164,5 @@ export function resolveNewQueryInitialSql(input: ResolveNewQueryInitialSqlInput)
|
||||
const table = resolveNewQueryTable(input);
|
||||
if (!table || table.connectionId !== input.targetConnectionId || table.database !== input.targetDatabase) return undefined;
|
||||
|
||||
return buildSelectAllSql(input.databaseType, table, input.identifierQuote);
|
||||
return buildSelectAllSql(input.databaseType, table, input.identifierQuote, input.driverProfile);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { DatabaseType } from "@/types/database.ts";
|
||||
import { isSchemaAware, usesDatabaseObjectTreeMode } from "@/lib/database/databaseCapabilities.ts";
|
||||
import { jdbcDriverProfileUsesSchemaQualification } from "@/lib/database/jdbcDialect";
|
||||
import * as api from "@/lib/backend/api.ts";
|
||||
import { parseSqlServerLinkedSchema, sqlServerLinkedTableName } from "@/lib/database/sqlServerLinkedServers.ts";
|
||||
import { isExplicitlyQuotedSqlIdentifier, quoteGaussDbJdbcIdentifier } from "@/lib/sql/sqlIdentifier.ts";
|
||||
@@ -57,6 +58,10 @@ export function quoteTableIdentifier(databaseType: DatabaseType | undefined, nam
|
||||
}
|
||||
|
||||
export function quoteTableDataIdentifier(databaseType: DatabaseType | undefined, name: string, identifierQuote?: string): string {
|
||||
if (databaseType === "jdbc" && identifierQuote != null) {
|
||||
if (!identifierQuote) return name;
|
||||
return `${identifierQuote}${name.replaceAll(identifierQuote, identifierQuote + identifierQuote)}${identifierQuote}`;
|
||||
}
|
||||
if ((databaseType === "gaussdb" || databaseType === "opengauss" || databaseType === "postgres") && identifierQuote != null) return quoteGaussDbJdbcIdentifier(name, identifierQuote);
|
||||
if ((databaseType === "kingbase" || databaseType === "informix" || databaseType === "spanner") && identifierQuote != null) {
|
||||
if (!identifierQuote) return name;
|
||||
@@ -103,6 +108,11 @@ export function qualifiedTableName(options: Pick<BuildTableSelectSqlOptions, "da
|
||||
}
|
||||
return quotedTable;
|
||||
}
|
||||
if (databaseType === "jdbc" && jdbcDriverProfileUsesSchemaQualification(driverProfile)) {
|
||||
const quotedTable = quoteTableDataIdentifier(databaseType, tableName, identifierQuote);
|
||||
const trimmedSchema = schema?.trim();
|
||||
return trimmedSchema ? `${quoteTableDataIdentifier(databaseType, trimmedSchema, identifierQuote)}.${quotedTable}` : quotedTable;
|
||||
}
|
||||
// Cloud Spanner mirrors `uses_connection_identifier_quote` on the backend, which
|
||||
// counts Spanner unconditionally: the branch must not be gated on a reported
|
||||
// quote, otherwise a connection whose quote has not loaded yet would fall through
|
||||
|
||||
@@ -3,6 +3,7 @@ import { metricRangeQuery, qualifiedTableName, quoteTableDataIdentifier } from "
|
||||
|
||||
export interface TableSqlTemplateOptions {
|
||||
databaseType?: DatabaseType;
|
||||
driverProfile?: string;
|
||||
identifierQuote?: string;
|
||||
schema?: string;
|
||||
catalog?: string;
|
||||
@@ -100,6 +101,7 @@ export function buildTableDeleteTemplate(options: TableSqlTemplateOptions): stri
|
||||
function templateTableName(options: TableSqlTemplateOptions): string {
|
||||
return qualifiedTableName({
|
||||
databaseType: options.databaseType,
|
||||
driverProfile: options.driverProfile,
|
||||
identifierQuote: options.identifierQuote,
|
||||
catalog: options.catalog,
|
||||
database: options.database,
|
||||
|
||||
Reference in new issue
Block a user