From c2de49c155e43d34a7b092794b2ff103d635e5c9 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 21 Jul 2026 19:46:02 +0800 Subject: [PATCH] fix(sqlserver): resolve default metadata schema --- crates/dbx-core/src/db/sqlserver.rs | 49 ++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/crates/dbx-core/src/db/sqlserver.rs b/crates/dbx-core/src/db/sqlserver.rs index fdcd97961..56b6adcb5 100644 --- a/crates/dbx-core/src/db/sqlserver.rs +++ b/crates/dbx-core/src/db/sqlserver.rs @@ -1378,15 +1378,14 @@ fn sqlserver_list_tables_sql( } }) .unwrap_or_default(); - let schema_escaped = schema.replace('\'', "''"); let base_columns = "o.name, CASE WHEN o.type = 'V' THEN 'VIEW' ELSE 'BASE TABLE' END, ep.value AS TABLE_COMMENT"; let base_from = "FROM sys.objects o \ JOIN sys.schemas s ON s.schema_id = o.schema_id \ OUTER APPLY (SELECT CAST(ep.value AS NVARCHAR(MAX)) AS value FROM sys.extended_properties ep \ WHERE ep.major_id = o.object_id AND ep.minor_id = 0 AND ep.name = N'MS_Description') ep"; let object_visibility = sqlserver_visible_object_predicate(); - let base_where = - format!("WHERE s.name = '{schema_escaped}' AND o.type IN ('U','V') AND {object_visibility} {filter_clause}"); + let schema_filter = sqlserver_schema_name_predicate(schema, "s.name"); + let base_where = format!("WHERE {schema_filter} AND o.type IN ('U','V') AND {object_visibility} {filter_clause}"); let order_by = "ORDER BY o.name"; // Use SELECT TOP for broad SQL Server version compatibility. @@ -1410,6 +1409,18 @@ fn sqlserver_list_tables_sql( } } +fn sqlserver_schema_name_predicate(schema: &str, schema_name_expression: &str) -> String { + if schema.trim().is_empty() { + // SQL Server resolves unqualified objects through the user's default schema. + // A configured default can be missing, so match DBeaver and fall back to dbo. + return format!( + "{schema_name_expression} = COALESCE((SELECT default_schema.name FROM sys.schemas default_schema WHERE default_schema.name = SCHEMA_NAME()), N'dbo')" + ); + } + + format!("{schema_name_expression} = N'{}'", schema.replace('\'', "''")) +} + fn escape_like_literal(value: &str) -> String { value.replace('\\', "\\\\").replace('\'', "''").replace('%', "\\%").replace('_', "\\_").replace('[', "\\[") } @@ -1624,8 +1635,8 @@ fn sqlserver_column_metadata_from_row(row: &Row) -> SqlServerColumnMetadata { } fn sqlserver_columns_sql(schema: &str, table: &str) -> String { - let s = schema.replace('\'', "''"); let t = table.replace('\'', "''"); + let schema_filter = sqlserver_schema_name_predicate(schema, "s.name"); // COLUMNPROPERTY keeps hidden/generated flags separate and returns NULL on // SQL Server versions that do not expose a newer property. format!( @@ -1665,7 +1676,7 @@ fn sqlserver_columns_sql(schema: &str, table: &str) -> String { WHERE i.is_primary_key = 1 \ ) pk ON pk.object_id = c.object_id AND pk.column_id = c.column_id \ OUTER APPLY (SELECT CAST(ep.value AS NVARCHAR(MAX)) AS value FROM sys.extended_properties ep WHERE ep.major_id = c.object_id AND ep.minor_id = c.column_id AND ep.name = N'MS_Description') ep \ - WHERE s.name = '{s}' AND o.name = '{t}' AND o.type IN ('U','V') \ + WHERE {schema_filter} AND o.name = '{t}' AND o.type IN ('U','V') \ ORDER BY c.column_id" ) } @@ -2126,8 +2137,8 @@ mod tests { requires_simple_query_batch, sqlserver_batch_can_use_execute, sqlserver_cell_to_json, sqlserver_columns_sql, sqlserver_completion_assistant_sql, sqlserver_dml_output_returns_rows, sqlserver_hidden_schema_names, sqlserver_indexes_sql, sqlserver_list_objects_sql, sqlserver_list_schemas_sql, sqlserver_list_tables_sql, - sqlserver_table_comment_sql, sqlserver_visible_object_predicate, strip_dbx_sqlserver_row_number_column, - SqlServerDescribedColumn, SqlServerResultSet, + sqlserver_schema_name_predicate, sqlserver_table_comment_sql, sqlserver_visible_object_predicate, + strip_dbx_sqlserver_row_number_column, SqlServerDescribedColumn, SqlServerResultSet, }; use crate::types::{ CompletionAssistantMatchMode, CompletionAssistantObjectKind, CompletionAssistantRequest, QueryResult, @@ -2498,12 +2509,34 @@ mod tests { let columns_sql = sqlserver_columns_sql("d'bo", "t'able"); let indexes_sql = sqlserver_indexes_sql("d'bo", "t'able"); - assert!(columns_sql.contains("s.name = 'd''bo'")); + assert!(columns_sql.contains("s.name = N'd''bo'")); assert!(columns_sql.contains("o.name = 't''able'")); assert!(columns_sql.contains("sys.identity_columns")); assert!(indexes_sql.contains("OBJECT_ID('d''bo.t''able')")); } + #[test] + fn sqlserver_metadata_resolves_blank_schema_to_default_with_dbo_fallback() { + let predicate = sqlserver_schema_name_predicate(" ", "s.name"); + + assert_eq!( + predicate, + "s.name = COALESCE((SELECT default_schema.name FROM sys.schemas default_schema WHERE default_schema.name = SCHEMA_NAME()), N'dbo')" + ); + assert!(sqlserver_list_tables_sql("", None, None, None).contains(&predicate)); + assert!(sqlserver_columns_sql("\t", "orders").contains(&predicate)); + } + + #[test] + fn sqlserver_metadata_preserves_explicit_schema_matching() { + let predicate = sqlserver_schema_name_predicate("Sales'Ops", "s.name"); + + assert_eq!(predicate, "s.name = N'Sales''Ops'"); + assert!(sqlserver_list_tables_sql("Sales'Ops", None, None, None).contains(&predicate)); + assert!(sqlserver_columns_sql("Sales'Ops", "orders").contains(&predicate)); + assert!(!predicate.contains("SCHEMA_NAME()")); + } + #[test] fn sqlserver_list_objects_sql_includes_timestamps() { let sql = sqlserver_list_objects_sql("dbo");