mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(sqlserver): 表结构支持查看约束(主键/唯一/外键/检查/默认值) (#9581)
SQL Server 连接的「编辑表结构」面板里只有 DDL/字段/索引/外键/触发器,
没有「约束」页签;用户无法查看某张表的约束。
list_constraints 在 SQL Server 驱动中完全缺失:驱动侧没有对应实现,
dbx-core 的 list_constraints_core 没有 SQL Server 分支,直接落到默认的
空数组;前端 tableMetadataCapabilities 也没有声明 sqlserver 支持,
因此即使是空页签也不会渲染。
- dbx-drivers 新增 list_constraints,用一条 UNION ALL 查询汇总
sys.key_constraints(PK/UNIQUE)、sys.foreign_keys(含引用库表列、
ON UPDATE/DELETE 动作、is_not_trusted)、sys.check_constraints、
sys.default_constraints;列名用 FOR XML PATH('') 聚合,兼容 SQL Server
2008(无 STRING_AGG)
- list_constraints_core 增加 SQL Server 分支与链接服务器早退
- tableMetadataCapabilities 为 sqlserver 打开 constraints,前端显示「约束」页签
- 新增单测覆盖 SQL 形态与列名拆包,并补充前端能力断言
Co-authored-by: zipg <4047349+zipg@users.noreply.github.com>
This commit is contained in:
@@ -19,6 +19,7 @@ describe("tableMetadataCapabilities", () => {
|
||||
expect(getTableMetadataCapabilities("kingbase").constraints).toBe(true);
|
||||
expect(getTableMetadataCapabilities("vastbase").constraints).toBe(true);
|
||||
expect(getTableMetadataCapabilities("opengauss").constraints).toBe(true);
|
||||
expect(getTableMetadataCapabilities("sqlserver").constraints).toBe(true);
|
||||
expect(getTableMetadataCapabilities("mysql").constraints).toBe(false);
|
||||
expect(getTableMetadataCapabilities(undefined).constraints).toBe(false);
|
||||
});
|
||||
|
||||
@@ -40,6 +40,11 @@ const capabilityByType: Partial<Record<DatabaseType, Partial<TableMetadataCapabi
|
||||
postgres: {
|
||||
constraints: true,
|
||||
},
|
||||
// SQL Server reports PK/UNIQUE/FOREIGN KEY/CHECK/DEFAULT constraints from the
|
||||
// sys.* catalog views through list_constraints.
|
||||
sqlserver: {
|
||||
constraints: true,
|
||||
},
|
||||
mongodb: {
|
||||
columns: false,
|
||||
foreignKeys: false,
|
||||
|
||||
@@ -7694,7 +7694,8 @@ pub async fn list_triggers_core(
|
||||
|
||||
/// Lists structured constraints for a relation. Exposed generically so the
|
||||
/// agent protocol and native drivers share one route; the built-in drivers
|
||||
/// that implement it today are PostgreSQL, OpenGauss, and the Xugu agent.
|
||||
/// that implement it today are PostgreSQL, OpenGauss, SQL Server, and the
|
||||
/// Xugu agent.
|
||||
pub async fn list_constraints_core(
|
||||
state: &AppState,
|
||||
connection_id: &str,
|
||||
@@ -7702,12 +7703,16 @@ pub async fn list_constraints_core(
|
||||
schema: &str,
|
||||
table: &str,
|
||||
) -> Result<Vec<db::ConstraintInfo>, String> {
|
||||
if crate::sql_dialect::parse_sqlserver_linked_schema_ref(schema).is_some() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
retry_metadata_connection(state, connection_id, Some(database), || async {
|
||||
let pool_key = state.get_or_create_metadata_pool_for_session(connection_id, Some(database), None).await?;
|
||||
let db_config = connection_config(state, connection_id).await;
|
||||
|
||||
{
|
||||
let pool_handle = state.pool_handle(&pool_key).await;
|
||||
try_sqlserver!(pool_handle, list_constraints, schema, table);
|
||||
if let Some(client) = extract_pool!(pool_handle.as_ref(), Agent) {
|
||||
let mut client = client.lock().await;
|
||||
return client
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::execution::MAX_ROWS;
|
||||
use crate::sql::starts_with_executable_sql_keyword;
|
||||
use crate::types::{
|
||||
ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, LinkedServerInfo, ObjectStatistics, QueryMessage, QueryResult,
|
||||
SpatialColumnBuilder, TableInfo, TriggerInfo,
|
||||
ColumnInfo, ConstraintInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, LinkedServerInfo, ObjectStatistics,
|
||||
QueryMessage, QueryResult, SpatialColumnBuilder, TableInfo, TriggerInfo,
|
||||
};
|
||||
use futures::{FutureExt, TryStreamExt};
|
||||
use sqlparser::ast::{Expr, Ident, ObjectNamePart, OrderByKind, SelectItem, SetExpr, Statement, TableFactor, Value};
|
||||
@@ -3107,6 +3107,118 @@ fn sqlserver_triggers_sql(schema: &str, table: &str) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn list_constraints(
|
||||
client: &mut SqlServerClient,
|
||||
schema: &str,
|
||||
table: &str,
|
||||
) -> Result<Vec<ConstraintInfo>, String> {
|
||||
let sql = sqlserver_constraints_sql(schema, table);
|
||||
let stream = client.query(&*sql, &[]).await.map_err(|e| e.to_string())?;
|
||||
let rows = stream.into_first_result().await.map_err(|e| e.to_string())?;
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
let columns = row.get::<&str, _>(3).unwrap_or("");
|
||||
let ref_columns = row.get::<&str, _>(6).unwrap_or("");
|
||||
ConstraintInfo {
|
||||
name: row.get::<&str, _>(0).unwrap_or("").to_string(),
|
||||
constraint_type: row.get::<&str, _>(1).unwrap_or("").to_string(),
|
||||
definition: row.get::<&str, _>(2).unwrap_or("").to_string(),
|
||||
columns: sqlserver_split_name_list(columns),
|
||||
ref_schema: row.get::<&str, _>(4).filter(|value| !value.is_empty()).map(str::to_string),
|
||||
ref_table: row.get::<&str, _>(5).filter(|value| !value.is_empty()).map(str::to_string),
|
||||
ref_columns: sqlserver_split_name_list(ref_columns),
|
||||
match_type: None,
|
||||
on_update: row.get::<&str, _>(7).filter(|value| !value.is_empty()).map(str::to_string),
|
||||
on_delete: row.get::<&str, _>(8).filter(|value| !value.is_empty()).map(str::to_string),
|
||||
deferrable: false,
|
||||
initially_deferred: false,
|
||||
enabled: row.get::<bool, _>(9).unwrap_or(true),
|
||||
valid: row.get::<bool, _>(10).unwrap_or(true),
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn sqlserver_split_name_list(value: &str) -> Vec<String> {
|
||||
value.split(',').filter(|name| !name.is_empty()).map(|name| name.to_string()).collect()
|
||||
}
|
||||
|
||||
/// One row per constraint (PK / UNIQUE / FOREIGN KEY / CHECK / DEFAULT) for a table.
|
||||
///
|
||||
/// `sys.key_constraints` and `sys.foreign_keys` are keyed by their backing index
|
||||
/// and constraint object respectively, so the participating columns are
|
||||
/// aggregated with the same `FOR XML PATH('')` trick the index listing uses.
|
||||
fn sqlserver_constraints_sql(schema: &str, table: &str) -> String {
|
||||
let object_id = sqlserver_object_id_expression(schema, table);
|
||||
let key_columns = "STUFF((SELECT ',' + c2.name \
|
||||
FROM sys.index_columns ic2 \
|
||||
JOIN sys.columns c2 ON c2.object_id = ic2.object_id AND c2.column_id = ic2.column_id \
|
||||
WHERE ic2.object_id = kc.parent_object_id AND ic2.index_id = kc.unique_index_id AND ic2.is_included_column = 0 \
|
||||
ORDER BY ic2.key_ordinal \
|
||||
FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)'), 1, 1, '')";
|
||||
let foreign_key_columns = "STUFF((SELECT ',' + pc2.name \
|
||||
FROM sys.foreign_key_columns fkc2 \
|
||||
JOIN sys.columns pc2 ON pc2.object_id = fkc2.parent_object_id AND pc2.column_id = fkc2.parent_column_id \
|
||||
WHERE fkc2.constraint_object_id = fk.object_id \
|
||||
ORDER BY fkc2.constraint_column_id \
|
||||
FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)'), 1, 1, '')";
|
||||
let foreign_key_ref_columns = "STUFF((SELECT ',' + rc2.name \
|
||||
FROM sys.foreign_key_columns fkc2 \
|
||||
JOIN sys.columns rc2 ON rc2.object_id = fkc2.referenced_object_id AND rc2.column_id = fkc2.referenced_column_id \
|
||||
WHERE fkc2.constraint_object_id = fk.object_id \
|
||||
ORDER BY fkc2.constraint_column_id \
|
||||
FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)'), 1, 1, '')";
|
||||
format!(
|
||||
"SELECT c.constraint_name, c.constraint_type, c.definition, c.constraint_columns, \
|
||||
c.ref_schema, c.ref_table, c.ref_columns, c.on_update, c.on_delete, c.is_enabled, c.is_valid \
|
||||
FROM ( \
|
||||
SELECT kc.name AS constraint_name, \
|
||||
CASE WHEN kc.type = 'PK' THEN N'PRIMARY KEY' ELSE N'UNIQUE' END AS constraint_type, \
|
||||
CAST(N'' AS NVARCHAR(MAX)) AS definition, \
|
||||
{key_columns} AS constraint_columns, \
|
||||
CAST(NULL AS NVARCHAR(128)) AS ref_schema, \
|
||||
CAST(NULL AS NVARCHAR(128)) AS ref_table, \
|
||||
CAST(N'' AS NVARCHAR(MAX)) AS ref_columns, \
|
||||
CAST(NULL AS NVARCHAR(60)) AS on_update, \
|
||||
CAST(NULL AS NVARCHAR(60)) AS on_delete, \
|
||||
CAST(CASE WHEN i.is_disabled = 1 THEN 0 ELSE 1 END AS bit) AS is_enabled, \
|
||||
CAST(1 AS bit) AS is_valid \
|
||||
FROM sys.key_constraints kc \
|
||||
JOIN sys.indexes i ON i.object_id = kc.parent_object_id AND i.index_id = kc.unique_index_id \
|
||||
WHERE kc.parent_object_id = {object_id} \
|
||||
UNION ALL \
|
||||
SELECT fk.name, N'FOREIGN KEY', CAST(N'' AS NVARCHAR(MAX)), {foreign_key_columns}, \
|
||||
SCHEMA_NAME(rt.schema_id), rt.name, {foreign_key_ref_columns}, \
|
||||
CASE fk.update_referential_action WHEN 1 THEN N'CASCADE' WHEN 2 THEN N'SET NULL' WHEN 3 THEN N'SET DEFAULT' ELSE N'NO ACTION' END, \
|
||||
CASE fk.delete_referential_action WHEN 1 THEN N'CASCADE' WHEN 2 THEN N'SET NULL' WHEN 3 THEN N'SET DEFAULT' ELSE N'NO ACTION' END, \
|
||||
CAST(CASE WHEN fk.is_disabled = 1 THEN 0 ELSE 1 END AS bit), \
|
||||
CAST(CASE WHEN fk.is_not_trusted = 1 THEN 0 ELSE 1 END AS bit) \
|
||||
FROM sys.foreign_keys fk \
|
||||
JOIN sys.tables rt ON rt.object_id = fk.referenced_object_id \
|
||||
WHERE fk.parent_object_id = {object_id} \
|
||||
UNION ALL \
|
||||
SELECT cc.name, N'CHECK', cc.definition, ISNULL(c.name, N''), \
|
||||
CAST(NULL AS NVARCHAR(128)), CAST(NULL AS NVARCHAR(128)), CAST(N'' AS NVARCHAR(MAX)), \
|
||||
CAST(NULL AS NVARCHAR(60)), CAST(NULL AS NVARCHAR(60)), \
|
||||
CAST(CASE WHEN cc.is_disabled = 1 THEN 0 ELSE 1 END AS bit), \
|
||||
CAST(CASE WHEN cc.is_not_trusted = 1 THEN 0 ELSE 1 END AS bit) \
|
||||
FROM sys.check_constraints cc \
|
||||
LEFT JOIN sys.columns c ON c.object_id = cc.parent_object_id AND c.column_id = cc.parent_column_id \
|
||||
WHERE cc.parent_object_id = {object_id} \
|
||||
UNION ALL \
|
||||
SELECT dc.name, N'DEFAULT', dc.definition, ISNULL(c.name, N''), \
|
||||
CAST(NULL AS NVARCHAR(128)), CAST(NULL AS NVARCHAR(128)), CAST(N'' AS NVARCHAR(MAX)), \
|
||||
CAST(NULL AS NVARCHAR(60)), CAST(NULL AS NVARCHAR(60)), \
|
||||
CAST(1 AS bit), CAST(1 AS bit) \
|
||||
FROM sys.default_constraints dc \
|
||||
LEFT JOIN sys.columns c ON c.object_id = dc.parent_object_id AND c.column_id = dc.parent_column_id \
|
||||
WHERE dc.parent_object_id = {object_id} \
|
||||
) c \
|
||||
ORDER BY c.constraint_name",
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn execute_query(client: &mut SqlServerClient, sql: &str) -> Result<QueryResult, String> {
|
||||
execute_query_with_max_rows(client, sql, None).await
|
||||
}
|
||||
@@ -3674,17 +3786,18 @@ mod tests {
|
||||
requires_simple_query_batch, restore_sqlserver_blank_column_names, restore_sqlserver_legacy_probe_output_names,
|
||||
restore_sqlserver_spatial_column_types, restore_sqlserver_unsafe_column_types, server_messages_query_result,
|
||||
sqlserver_batch_can_use_execute, sqlserver_bulk_token_row, sqlserver_cell_to_json, sqlserver_columns_sql,
|
||||
sqlserver_completion_assistant_sql, sqlserver_dml_output_returns_rows, sqlserver_done_trace_event,
|
||||
sqlserver_filter_definition_error, sqlserver_hidden_schema_names, sqlserver_indexes_sql,
|
||||
sqlserver_legacy_indexes_sql, sqlserver_legacy_probe, sqlserver_legacy_probe_with_nonce,
|
||||
sqlserver_completion_assistant_sql, sqlserver_constraints_sql, sqlserver_dml_output_returns_rows,
|
||||
sqlserver_done_trace_event, sqlserver_filter_definition_error, sqlserver_hidden_schema_names,
|
||||
sqlserver_indexes_sql, sqlserver_legacy_indexes_sql, sqlserver_legacy_probe, sqlserver_legacy_probe_with_nonce,
|
||||
sqlserver_legacy_wildcard_metadata_query, sqlserver_list_objects_sql, sqlserver_list_schemas_sql,
|
||||
sqlserver_list_tables_sql, sqlserver_probe_explicit_alias, sqlserver_query_messages,
|
||||
sqlserver_query_transport_for_engine_edition, sqlserver_query_transport_for_request,
|
||||
sqlserver_schema_name_predicate, sqlserver_spatial_marker, sqlserver_supports_session_database_switch,
|
||||
sqlserver_table_comment_sql, sqlserver_table_objects_sql, sqlserver_triggers_sql,
|
||||
sqlserver_visible_object_predicate, strip_dbx_sqlserver_row_number_column, SqlServerDescribedColumn,
|
||||
SqlServerProbeOutputNameOverride, SqlServerQueryTransport, SqlServerRestoredColumn, SqlServerResultSet,
|
||||
SqlServerSpatialColumn, SqlServerTdsEvent, SQLSERVER_COMPLETION_CONTEXT_SQL, SQLSERVER_RESULT_TYPE_PROBE_SQL,
|
||||
sqlserver_schema_name_predicate, sqlserver_spatial_marker, sqlserver_split_name_list,
|
||||
sqlserver_supports_session_database_switch, sqlserver_table_comment_sql, sqlserver_table_objects_sql,
|
||||
sqlserver_triggers_sql, sqlserver_visible_object_predicate, strip_dbx_sqlserver_row_number_column,
|
||||
SqlServerDescribedColumn, SqlServerProbeOutputNameOverride, SqlServerQueryTransport, SqlServerRestoredColumn,
|
||||
SqlServerResultSet, SqlServerSpatialColumn, SqlServerTdsEvent, SQLSERVER_COMPLETION_CONTEXT_SQL,
|
||||
SQLSERVER_RESULT_TYPE_PROBE_SQL,
|
||||
};
|
||||
use crate::types::{
|
||||
CompletionAssistantMatchMode, CompletionAssistantObjectKind, CompletionAssistantRequest, QueryResult,
|
||||
@@ -4523,6 +4636,32 @@ mod tests {
|
||||
assert!(!sql.contains("STRING_AGG"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_constraints_sql_unions_every_constraint_catalog() {
|
||||
let sql = sqlserver_constraints_sql("d'bo", "t'able");
|
||||
|
||||
assert!(sql.contains("sys.key_constraints"));
|
||||
assert!(sql.contains("sys.foreign_keys"));
|
||||
assert!(sql.contains("sys.check_constraints"));
|
||||
assert!(sql.contains("sys.default_constraints"));
|
||||
assert!(sql.contains("UNION ALL"));
|
||||
// Participating columns are aggregated with the same FOR XML trick the
|
||||
// index listing uses; SQL Server has no string_agg on 2008-era servers.
|
||||
assert!(sql.contains("FOR XML PATH(''), TYPE"));
|
||||
assert!(!sql.contains("STRING_AGG"));
|
||||
// The constraint order is stable so the tab does not reshuffle per refresh.
|
||||
assert!(sql.contains("ORDER BY c.constraint_name"));
|
||||
// Literals stay escaped through the shared object-id helper.
|
||||
assert!(sql.contains("OBJECT_ID(QUOTENAME(N'd''bo') + N'.' + QUOTENAME(N't''able'))"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_split_name_list_drops_trailing_empty_entries() {
|
||||
assert_eq!(sqlserver_split_name_list(""), Vec::<String>::new());
|
||||
assert_eq!(sqlserver_split_name_list("id"), vec!["id".to_string()]);
|
||||
assert_eq!(sqlserver_split_name_list("id,code"), vec!["id".to_string(), "code".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_metadata_sql_escapes_literals() {
|
||||
let columns_sql = sqlserver_columns_sql("d'bo", "t'able");
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
Reference in New Issue
Block a user