fix(grid): resolve oracle jdbc edits with folded table names
Closes #8368
This commit is contained in:
3 files changed
+114
-5
No files matched your search
@@ -612,6 +612,81 @@ describe("queryStore hidden primary key editing", () => {
|
|||||||
expect(tab.queryEditabilityReason).toBeUndefined();
|
expect(tab.queryEditabilityReason).toBeUndefined();
|
||||||
}, 10_000);
|
}, 10_000);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
connection: { id: "oracle-1", name: "Oracle", db_type: "oracle", database: "XE", query_timeout_secs: 30 },
|
||||||
|
label: "native Oracle",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
connection: { id: "oracle-jdbc-1", name: "Oracle JDBC", db_type: "jdbc", connection_string: "jdbc:oracle:thin:@//localhost:1521/XE", database: "XE", query_timeout_secs: 30 },
|
||||||
|
label: "Oracle-inferred JDBC",
|
||||||
|
},
|
||||||
|
])("edits an unqualified lowercase query through the folded current-schema table for $label", async ({ connection }) => {
|
||||||
|
getConnectionConfig.mockReturnValue(connection);
|
||||||
|
getColumns.mockResolvedValue([
|
||||||
|
{ name: "ID", data_type: "NUMBER", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
|
||||||
|
{ name: "NAME", data_type: "VARCHAR2(100)", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
|
||||||
|
]);
|
||||||
|
listIndexes.mockResolvedValue([]);
|
||||||
|
analyzeEditableQueryEditability.mockResolvedValue({
|
||||||
|
editable: true,
|
||||||
|
analysis: {
|
||||||
|
schema: undefined,
|
||||||
|
tableName: "imp_t",
|
||||||
|
selectStar: true,
|
||||||
|
columns: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
executeMulti.mockResolvedValue([{ columns: ["ID", "NAME"], rows: [[1, "Alice"]], affected_rows: 0, execution_time_ms: 1 }]);
|
||||||
|
|
||||||
|
const { useQueryStore } = await import("@/stores/queryStore");
|
||||||
|
const store = useQueryStore();
|
||||||
|
const tabId = store.createTab(connection.id, "XE", "Query");
|
||||||
|
store.setAutoCommit(tabId, true);
|
||||||
|
|
||||||
|
await store.executeTabSql(tabId, "select * from imp_t");
|
||||||
|
|
||||||
|
// The service name must not leak into the metadata schema, and the stored
|
||||||
|
// table spelling has to fold to uppercase so the generated UPDATE quotes
|
||||||
|
// the object Oracle actually resolved for the unqualified SELECT.
|
||||||
|
expect(getColumns).toHaveBeenCalledWith(connection.id, "XE", "", "IMP_T", undefined);
|
||||||
|
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||||
|
await vi.waitFor(() => expect(tab.tableMeta?.tableName).toBe("IMP_T"));
|
||||||
|
expect(tab.tableMeta?.schema).toBeUndefined();
|
||||||
|
expect(tab.queryEditabilityReason).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps an unqualified lowercase MySQL JDBC query on its unfolded table name", async () => {
|
||||||
|
getConnectionConfig.mockReturnValue({ id: "jdbc-mysql-1", name: "MySQL JDBC", db_type: "jdbc", connection_string: "jdbc:mysql://localhost:3306/app", database: "app", query_timeout_secs: 30 });
|
||||||
|
getColumns.mockResolvedValue([
|
||||||
|
{ name: "id", data_type: "int", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
|
||||||
|
{ name: "name", data_type: "varchar(64)", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
|
||||||
|
]);
|
||||||
|
analyzeEditableQueryEditability.mockResolvedValue({
|
||||||
|
editable: true,
|
||||||
|
analysis: {
|
||||||
|
schema: undefined,
|
||||||
|
tableName: "users",
|
||||||
|
selectStar: true,
|
||||||
|
columns: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
executeMulti.mockResolvedValue([{ columns: ["id", "name"], rows: [[1, "Alice"]], affected_rows: 0, execution_time_ms: 1 }]);
|
||||||
|
|
||||||
|
const { useQueryStore } = await import("@/stores/queryStore");
|
||||||
|
const store = useQueryStore();
|
||||||
|
const tabId = store.createTab("jdbc-mysql-1", "app", "Query");
|
||||||
|
store.setAutoCommit(tabId, true);
|
||||||
|
|
||||||
|
await store.executeTabSql(tabId, "select * from users");
|
||||||
|
|
||||||
|
expect(getColumns).toHaveBeenCalledWith("jdbc-mysql-1", "app", "", "users", undefined);
|
||||||
|
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||||
|
await vi.waitFor(() => expect(tab.tableMeta?.tableName).toBe("users"));
|
||||||
|
expect(tab.tableMeta?.schema).toBeUndefined();
|
||||||
|
expect(tab.queryEditabilityReason).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it("uses the configured current schema to keep an unqualified Oracle base-table query editable", async () => {
|
it("uses the configured current schema to keep an unqualified Oracle base-table query editable", async () => {
|
||||||
getConnectionConfig.mockReturnValue({ id: "oracle-1", name: "Oracle", db_type: "oracle", database: "ORCL", default_schema: "APP", query_timeout_secs: 30 });
|
getConnectionConfig.mockReturnValue({ id: "oracle-1", name: "Oracle", db_type: "oracle", database: "ORCL", default_schema: "APP", query_timeout_secs: 30 });
|
||||||
getColumns.mockResolvedValue([
|
getColumns.mockResolvedValue([
|
||||||
|
|||||||
@@ -4684,6 +4684,12 @@ export const useQueryStore = defineStore("query", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveEditableSourceMetadataTarget(tab: QueryTab, analysis: EditableQueryInfo, source: EditableQuerySource, conn: ConnectionConfig | undefined, dbType: string, executionDatabase: string): EditableSourceMetadataTarget {
|
function resolveEditableSourceMetadataTarget(tab: QueryTab, analysis: EditableQueryInfo, source: EditableQuerySource, conn: ConnectionConfig | undefined, dbType: string, executionDatabase: string): EditableSourceMetadataTarget {
|
||||||
|
// Oracle-family metadata rules (schema-less resolution + uppercase folding)
|
||||||
|
// must follow the connection's effective database type. Callers built from
|
||||||
|
// the raw connection db_type pass "jdbc" for a JDBC Oracle connection,
|
||||||
|
// which would keep the service-name schema fallback and the query's
|
||||||
|
// lowercase table spelling, so the write targets a non-existent table.
|
||||||
|
const metadataDbType = effectiveDatabaseTypeForConnection(conn) || dbType;
|
||||||
// Metadata must resolve in the same namespace as the query execution. An
|
// Metadata must resolve in the same namespace as the query execution. An
|
||||||
// empty query-tab database still executes in the connection's default DB,
|
// empty query-tab database still executes in the connection's default DB,
|
||||||
// while database-tree dialects and SQL Server 3-part names may override it
|
// while database-tree dialects and SQL Server 3-part names may override it
|
||||||
@@ -4707,10 +4713,10 @@ export const useQueryStore = defineStore("query", () => {
|
|||||||
// connection's search_path. Keep the metadata request unqualified when no
|
// connection's search_path. Keep the metadata request unqualified when no
|
||||||
// schema was selected instead of assuming public (or the database name).
|
// schema was selected instead of assuming public (or the database name).
|
||||||
const useCurrentPostgresSchema = (dbType === "postgres" || dbType === "kwdb") && !source.schema && !tab.schema;
|
const useCurrentPostgresSchema = (dbType === "postgres" || dbType === "kwdb") && !source.schema && !tab.schema;
|
||||||
const resolvedSchema = (dbType === "sqlserver" && !source.schema) || (ORACLE_LIKE_METADATA_TYPES.has(dbType) && !schema) || resolveAgentSearchPathSchema || useCurrentPostgresSchema ? "" : metadataSchemaForConnection(conn, metadataDatabase, schema || undefined);
|
const resolvedSchema = (dbType === "sqlserver" && !source.schema) || (ORACLE_LIKE_METADATA_TYPES.has(metadataDbType) && !schema) || resolveAgentSearchPathSchema || useCurrentPostgresSchema ? "" : metadataSchemaForConnection(conn, metadataDatabase, schema || undefined);
|
||||||
const metadataSchema = normalizeUppercaseFoldedMetadataIdentifier(dbType, resolvedSchema || undefined, source.schema ? source.schemaQuoted : false) || "";
|
const metadataSchema = normalizeUppercaseFoldedMetadataIdentifier(metadataDbType, resolvedSchema || undefined, source.schema ? source.schemaQuoted : false) || "";
|
||||||
const metadataTableName = normalizeUppercaseFoldedMetadataIdentifier(dbType, source.tableName, source.tableNameQuoted)!;
|
const metadataTableName = normalizeUppercaseFoldedMetadataIdentifier(metadataDbType, source.tableName, source.tableNameQuoted)!;
|
||||||
const metadataCatalog = normalizeUppercaseFoldedMetadataIdentifier(dbType, source.catalog, source.catalogQuoted);
|
const metadataCatalog = normalizeUppercaseFoldedMetadataIdentifier(metadataDbType, source.catalog, source.catalogQuoted);
|
||||||
const metadataSource: EditableQuerySource = {
|
const metadataSource: EditableQuerySource = {
|
||||||
...source,
|
...source,
|
||||||
catalog: metadataCatalog,
|
catalog: metadataCatalog,
|
||||||
@@ -4723,7 +4729,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||||||
const knownTableType = tab.tableMeta?.tableName.toLowerCase() === metadataTableName.toLowerCase() && normalizeOptionalSchema(tab.tableMeta.schema) === normalizeOptionalSchema(metadataSchema) ? tab.tableMeta.tableType : undefined;
|
const knownTableType = tab.tableMeta?.tableName.toLowerCase() === metadataTableName.toLowerCase() && normalizeOptionalSchema(tab.tableMeta.schema) === normalizeOptionalSchema(metadataSchema) ? tab.tableMeta.tableType : undefined;
|
||||||
return {
|
return {
|
||||||
source: metadataSource,
|
source: metadataSource,
|
||||||
analysis: normalizeUppercaseFoldedQueryAnalysis(dbType, cloneAnalysisForSource(analysis, metadataSource), metadataSchema || undefined, metadataTableName),
|
analysis: normalizeUppercaseFoldedQueryAnalysis(metadataDbType, cloneAnalysisForSource(analysis, metadataSource), metadataSchema || undefined, metadataTableName),
|
||||||
writeSchema,
|
writeSchema,
|
||||||
request: {
|
request: {
|
||||||
connectionId: tab.connectionId!,
|
connectionId: tab.connectionId!,
|
||||||
|
|||||||
@@ -6713,6 +6713,34 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prepares_oracle_update_without_schema_when_the_frontend_resolved_the_current_schema() {
|
||||||
|
// A JDBC Oracle edit resolves the login user's schema on the frontend and
|
||||||
|
// sends the folded table name with an empty schema. The generated UPDATE
|
||||||
|
// must stay unqualified instead of reintroducing the service name.
|
||||||
|
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
|
||||||
|
database_type: Some(DatabaseType::Oracle),
|
||||||
|
identifier_quote: None,
|
||||||
|
table_meta: DataGridTableMeta {
|
||||||
|
catalog: None,
|
||||||
|
database: None,
|
||||||
|
schema: None,
|
||||||
|
table_name: "IMP_T".to_string(),
|
||||||
|
primary_keys: vec!["ID".to_string()],
|
||||||
|
columns: Some(vec![column("ID", "NUMBER", false, None), column("NAME", "VARCHAR2(100)", true, None)]),
|
||||||
|
},
|
||||||
|
columns: vec!["ID".to_string(), "NAME".to_string()],
|
||||||
|
source_columns: None,
|
||||||
|
rows: vec![vec![json!(7), json!("old")]],
|
||||||
|
dirty_rows: vec![(0, vec![(1, json!("new"))])],
|
||||||
|
deleted_rows: vec![],
|
||||||
|
new_rows: vec![],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(result.validation_error, None);
|
||||||
|
assert_eq!(result.statements, vec!["UPDATE \"IMP_T\" SET \"NAME\" = 'new' WHERE \"ID\" = 7;"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn oracle_raw_literals_require_valid_even_length_hex() {
|
fn oracle_raw_literals_require_valid_even_length_hex() {
|
||||||
let raw = column("ID", "RAW(16)", false, None);
|
let raw = column("ID", "RAW(16)", false, None);
|
||||||
|
|||||||
Reference in new issue
Block a user