fix(sqlserver): preserve database-qualified editing targets
This commit is contained in:
parent
9d7f87feb8
commit
b89e0079a5
|
|
@ -2783,8 +2783,10 @@ export const useQueryStore = defineStore("query", () => {
|
|||
function resolveEditableSourceMetadataTarget(tab: QueryTab, analysis: EditableQueryInfo, source: EditableQuerySource, conn: ConnectionConfig | undefined, dbType: string, executionDatabase: string): EditableSourceMetadataTarget {
|
||||
// Metadata must resolve in the same namespace as the query execution. An
|
||||
// empty query-tab database still executes in the connection's default DB,
|
||||
// while database-tree dialects may override it with a qualified source.
|
||||
const metadataDatabase = (connectionUsesDatabaseObjectTreeMode(conn) ? source.schema : undefined) || executionDatabase || conn?.database || tab.database;
|
||||
// while database-tree dialects and SQL Server 3-part names may override it
|
||||
// with a qualified source.
|
||||
const qualifiedSourceDatabase = dbType === "sqlserver" ? source.catalog : connectionUsesDatabaseObjectTreeMode(conn) ? source.schema : undefined;
|
||||
const metadataDatabase = qualifiedSourceDatabase || executionDatabase || conn?.database || tab.database;
|
||||
let schema = source.schema || tab.schema;
|
||||
if (!schema) {
|
||||
if (dbType === "postgres" || dbType === "kwdb") schema = "public";
|
||||
|
|
|
|||
|
|
@ -4183,6 +4183,35 @@ mod tests {
|
|||
assert_eq!(result.statements, vec!["UPDATE [dbo].[users] SET [UserId] = 144847503924137986 WHERE [Id] = 1;"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepares_sqlserver_cross_database_update() {
|
||||
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
|
||||
database_type: Some(DatabaseType::SqlServer),
|
||||
identifier_quote: None,
|
||||
table_meta: DataGridTableMeta {
|
||||
catalog: Some("BarDB".to_string()),
|
||||
database: Some("BarDB".to_string()),
|
||||
schema: Some("dbo".to_string()),
|
||||
table_name: "TUser".to_string(),
|
||||
primary_keys: vec!["ID".to_string()],
|
||||
columns: Some(vec![column("ID", "int", false, None), column("UserId", "bigint", false, None)]),
|
||||
},
|
||||
columns: vec!["ID".to_string(), "UserId".to_string()],
|
||||
source_columns: None,
|
||||
rows: vec![vec![json!(1), json!(10279)]],
|
||||
dirty_rows: vec![(0, vec![(1, json!(10280))])],
|
||||
deleted_rows: vec![],
|
||||
new_rows: vec![],
|
||||
});
|
||||
|
||||
assert_eq!(result.validation_error, None);
|
||||
assert_eq!(result.statements, vec!["UPDATE [BarDB].[dbo].[TUser] SET [UserId] = 10280 WHERE [ID] = 1;"]);
|
||||
assert_eq!(
|
||||
result.rollback_statements,
|
||||
vec!["UPDATE [BarDB].[dbo].[TUser] SET [UserId] = 10279 WHERE [ID] = 1 AND [UserId] = 10280;"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepares_kingbase_update_when_source_primary_key_case_differs() {
|
||||
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
|
||||
|
|
|
|||
|
|
@ -71,17 +71,12 @@ pub fn qualified_table_name(database_type: Option<DatabaseType>, schema: Option<
|
|||
quote_table_identifier(database_type, table_name)
|
||||
}
|
||||
|
||||
/// Like `qualified_table_name`, but prefixes a Doris/StarRocks external
|
||||
/// 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).
|
||||
/// Like `qualified_table_name`, but also supports 3-part names for SQL Server
|
||||
/// (`<database>.<schema>.<table>`) and Doris/StarRocks external catalogs
|
||||
/// (`<catalog>.<database>.<table>`). SQL parsing stores the first segment of a
|
||||
/// 3-part source in `catalog`; for SQL Server that segment is its database.
|
||||
/// Doris/StarRocks use `schema` as the middle segment when present, otherwise
|
||||
/// `database`, and ignore their built-in `internal` catalog.
|
||||
pub fn qualified_table_name_with_catalog(
|
||||
database_type: Option<DatabaseType>,
|
||||
catalog: Option<&str>,
|
||||
|
|
@ -89,9 +84,13 @@ pub fn qualified_table_name_with_catalog(
|
|||
database: Option<&str>,
|
||||
table_name: &str,
|
||||
) -> String {
|
||||
let catalog = catalog.map(str::trim).filter(|catalog| !catalog.is_empty() && *catalog != "internal");
|
||||
let catalog = catalog.map(str::trim).filter(|catalog| !catalog.is_empty());
|
||||
match (catalog, database_type) {
|
||||
(Some(catalog), Some(DatabaseType::Doris | DatabaseType::StarRocks)) => {
|
||||
(Some(database), Some(DatabaseType::SqlServer)) => {
|
||||
let table = qualified_table_name(database_type, schema, table_name);
|
||||
format!("{}.{}", quote_table_identifier(database_type, database), table)
|
||||
}
|
||||
(Some(catalog), Some(DatabaseType::Doris | DatabaseType::StarRocks)) if catalog != "internal" => {
|
||||
let middle = schema
|
||||
.map(str::trim)
|
||||
.filter(|schema| !schema.is_empty())
|
||||
|
|
|
|||
|
|
@ -2034,6 +2034,93 @@ test("uses dbo as SQL Server metadata schema when query omits schema", async ()
|
|||
}
|
||||
});
|
||||
|
||||
test("uses the qualified SQL Server database as the editable result target", async () => {
|
||||
const restoreStorage = installMemoryStorage();
|
||||
setActivePinia(createPinia());
|
||||
const connectionStore = useConnectionStore();
|
||||
const store = useQueryStore();
|
||||
const originalFetch = globalThis.fetch;
|
||||
const sql = "select * from BarDB.dbo.TUser AS ts where UserId = 10279";
|
||||
const columnRequests: Array<{ database: string | null; schema: string | null; table: string | null; catalog: string | null }> = [];
|
||||
|
||||
connectionStore.addEphemeralConnection(sqlServerConn("sqlserver-cross-database"));
|
||||
|
||||
globalThis.fetch = withConnectionHealthMock(async (input, init) => {
|
||||
const url = String(input);
|
||||
if (url === "/api/query/execute-multi") {
|
||||
return new Response(
|
||||
JSON.stringify([
|
||||
{
|
||||
columns: ["ID", "UserId"],
|
||||
rows: [[1, 10279]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
},
|
||||
]),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
if (url === "/api/query/analyze-editability") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
editable: true,
|
||||
analysis: {
|
||||
catalog: "BarDB",
|
||||
catalogQuoted: false,
|
||||
schema: "dbo",
|
||||
schemaQuoted: false,
|
||||
tableName: "TUser",
|
||||
tableNameQuoted: false,
|
||||
tableAlias: "ts",
|
||||
selectStar: true,
|
||||
columns: [],
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
if (url.startsWith("/api/schema/columns?")) {
|
||||
const params = new URL(url, "http://localhost").searchParams;
|
||||
columnRequests.push({
|
||||
database: params.get("database"),
|
||||
schema: params.get("schema"),
|
||||
table: params.get("table"),
|
||||
catalog: params.get("catalog"),
|
||||
});
|
||||
return new Response(
|
||||
JSON.stringify([
|
||||
{ name: "ID", data_type: "int", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
|
||||
{ name: "UserId", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: false, extra: null },
|
||||
]),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
if (url === "/api/query/prepare-pagination-plan") {
|
||||
const body = JSON.parse(String(init?.body ?? "{}"));
|
||||
return new Response(JSON.stringify({ sqlToExecute: body.options.sql, useAgentResultSession: false }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return new Response("unexpected request", { status: 500 });
|
||||
});
|
||||
|
||||
try {
|
||||
const tabId = store.createTab("sqlserver-cross-database", "FooDB", "Query 1", "query");
|
||||
await store.executeTabSql(tabId, sql);
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId);
|
||||
await waitFor(() => columnRequests.length > 0 && tab?.tableMeta?.tableName === "TUser");
|
||||
assert.deepEqual(columnRequests, [{ database: "BarDB", schema: "dbo", table: "TUser", catalog: "BarDB" }]);
|
||||
assert.equal(tab?.tableMeta?.database, "BarDB");
|
||||
assert.equal(tab?.tableMeta?.catalog, "BarDB");
|
||||
assert.equal(tab?.tableMeta?.schema, "dbo");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
restoreStorage();
|
||||
}
|
||||
});
|
||||
|
||||
test("evicting cached tab results releases multi-result payloads and sessions", async () => {
|
||||
const restoreStorage = installMemoryStorage();
|
||||
setActivePinia(createPinia());
|
||||
|
|
|
|||
Loading…
Reference in New Issue