feat(query): show database server messages in query results

This commit is contained in:
Eddy Lei authored and GitHub committed 2026-08-07 15:18:32 +08:00
1 parent 4c7ddd266b
commit a5e46fd659
45 files changed
+1401 -51

No files matched your search

+81 -5
View File
@@ -1,7 +1,7 @@
use crate::query::MAX_ROWS;
use crate::sql::starts_with_executable_sql_keyword;
use crate::types::{
ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, LinkedServerInfo, ObjectStatistics, QueryResult,
ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, LinkedServerInfo, ObjectStatistics, QueryMessage, QueryResult,
SpatialColumnBuilder, TableInfo, TriggerInfo,
};
use futures::{FutureExt, TryStreamExt};
@@ -486,15 +486,41 @@ where
(output, messages)
}
/// Map captured tiberius INFO-token texts to generic query messages. Tiberius
/// exposes only the message text in its tracing event, so severity is always
/// `INFO` and no code/detail/hint is available.
fn sqlserver_query_messages(messages: &[String]) -> Vec<QueryMessage> {
messages
.iter()
.map(|message| QueryMessage {
severity: "INFO".to_string(),
message: message.clone(),
code: None,
detail: None,
hint: None,
})
.collect()
}
fn query_result_with_server_messages(result: QueryResult, messages: Vec<String>) -> QueryResult {
query_result_with_server_messages_metadata(result, messages).result
}
fn query_result_with_server_messages_metadata(mut result: QueryResult, messages: Vec<String>) -> SqlServerBatchResult {
if messages.is_empty() || !result.columns.is_empty() || !result.rows.is_empty() {
if messages.is_empty() {
return SqlServerBatchResult { result, server_message: false };
}
if !result.columns.is_empty() || !result.rows.is_empty() {
// Tabular results keep their shape and additionally carry the
// messages; only the empty-result synthesis below leaves
// `messages` empty so consumers do not render the same text twice.
result.messages = sqlserver_query_messages(&messages);
return SqlServerBatchResult { result, server_message: false };
}
// Empty results synthesize the legacy single-"Message"-column grid; the
// text lives only there, so `messages` stays empty to avoid consumers
// rendering the same text twice.
result.columns = vec![SQLSERVER_MESSAGE_COLUMN.to_string()];
result.column_types = vec!["nvarchar".to_string()];
result.rows = messages.into_iter().map(|message| vec![serde_json::Value::String(message)]).collect();
@@ -520,6 +546,7 @@ fn server_messages_query_result(messages: Vec<String>, start: Instant) -> Option
session_id: None,
has_more: false,
elasticsearch_raw_body: None,
messages: Vec::new(),
},
messages,
))
@@ -585,6 +612,7 @@ async fn collect_first_result_limited(
session_id: None,
has_more: false,
elasticsearch_raw_body: None,
messages: Vec::new(),
})
}
@@ -1399,6 +1427,7 @@ fn push_sqlserver_result_set(results: &mut Vec<QueryResult>, result: Option<SqlS
session_id: None,
has_more: false,
elasticsearch_raw_body: None,
messages: Vec::new(),
});
}
}
@@ -1456,6 +1485,7 @@ fn push_sqlserver_ordered_events(
session_id: None,
has_more: false,
elasticsearch_raw_body: None,
messages: Vec::new(),
},
server_message: false,
});
@@ -2785,6 +2815,7 @@ pub async fn execute_query_with_max_rows(
session_id: None,
has_more: false,
elasticsearch_raw_body: None,
messages: Vec::new(),
},
messages,
))
@@ -2829,6 +2860,7 @@ pub(crate) async fn execute_batch_with_max_rows_metadata(
session_id: None,
has_more: false,
elasticsearch_raw_body: None,
messages: Vec::new(),
},
messages,
)]);
@@ -2913,6 +2945,7 @@ pub(crate) async fn execute_simple_batch_with_max_rows_metadata(
session_id: None,
has_more: false,
elasticsearch_raw_body: None,
messages: Vec::new(),
},
server_message: false,
});
@@ -3182,12 +3215,12 @@ mod tests {
is_sqlserver_legacy_duplicate_probe_error, is_sqlserver_spatial_column, is_sqlserver_variant_column,
push_sqlserver_ordered_events, query_result_with_server_messages, query_result_with_server_messages_metadata,
requires_simple_query_batch, restore_sqlserver_legacy_probe_output_names,
restore_sqlserver_spatial_column_types, sqlserver_batch_can_use_execute, sqlserver_bulk_token_row,
sqlserver_cell_to_json, sqlserver_columns_sql, sqlserver_completion_assistant_sql,
restore_sqlserver_spatial_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_legacy_wildcard_metadata_query, sqlserver_list_objects_sql,
sqlserver_list_schemas_sql, sqlserver_list_tables_sql, sqlserver_probe_explicit_alias,
sqlserver_list_schemas_sql, sqlserver_list_tables_sql, sqlserver_probe_explicit_alias, sqlserver_query_messages,
sqlserver_schema_name_predicate, sqlserver_spatial_marker, sqlserver_supports_session_database_switch,
sqlserver_table_comment_sql, sqlserver_triggers_sql, sqlserver_visible_object_predicate,
strip_dbx_sqlserver_row_number_column, SqlServerDescribedColumn, SqlServerProbeOutputNameOverride,
@@ -3237,10 +3270,15 @@ mod tests {
session_id: None,
has_more: false,
elasticsearch_raw_body: None,
messages: Vec::new(),
};
let result = query_result_with_server_messages(empty, vec!["DBCC execution completed".to_string()]);
assert_eq!(result.columns, vec!["Message"]);
assert_eq!(result.column_types, vec!["nvarchar"]);
assert_eq!(result.rows, vec![vec![serde_json::json!("DBCC execution completed")]]);
// The synthesized grid already carries the text; `messages` stays
// empty so consumers do not render the same text twice.
assert!(result.messages.is_empty());
let message = query_result_with_server_messages_metadata(
QueryResult {
@@ -3256,6 +3294,7 @@ mod tests {
session_id: None,
has_more: false,
elasticsearch_raw_body: None,
messages: Vec::new(),
},
vec!["PRINT output".to_string()],
);
@@ -3274,11 +3313,16 @@ mod tests {
session_id: None,
has_more: false,
elasticsearch_raw_body: None,
messages: Vec::new(),
};
let result = query_result_with_server_messages_metadata(select, vec!["informational".to_string()]);
assert!(!result.server_message);
assert_eq!(result.result.columns, vec!["id"]);
assert_eq!(result.result.rows, vec![vec![serde_json::json!(1)]]);
// Non-empty results keep their shape but still carry the server messages.
assert_eq!(result.result.messages.len(), 1);
assert_eq!(result.result.messages[0].severity, "INFO");
assert_eq!(result.result.messages[0].message, "informational");
}
#[test]
@@ -3421,6 +3465,36 @@ mod tests {
assert_eq!(results[1].result.affected_rows, 2);
}
#[test]
fn sqlserver_query_messages_map_info_severity() {
let messages = sqlserver_query_messages(&["first".to_string(), "second".to_string()]);
assert_eq!(messages.len(), 2);
for (message, expected) in messages.iter().zip(["first", "second"]) {
assert_eq!(message.severity, "INFO");
assert_eq!(message.message, expected);
assert!(message.code.is_none());
assert!(message.detail.is_none());
assert!(message.hint.is_none());
}
assert!(sqlserver_query_messages(&[]).is_empty());
}
#[test]
fn sqlserver_server_messages_query_result_synthesizes_grid() {
assert!(server_messages_query_result(vec![], Instant::now()).is_none());
let result = server_messages_query_result(vec!["print output".to_string()], Instant::now())
.expect("non-empty messages yield a result");
assert!(result.server_message);
assert_eq!(result.result.columns, vec!["Message"]);
assert_eq!(result.result.column_types, vec!["nvarchar"]);
assert_eq!(result.result.rows, vec![vec![serde_json::json!("print output")]]);
// The synthesized grid already carries the text; `messages` stays
// empty so consumers do not render the same text twice.
assert!(result.result.messages.is_empty());
}
#[test]
fn sqlserver_endpoint_splits_named_instance_hosts() {
assert_eq!(
@@ -3939,6 +4013,7 @@ mod tests {
session_id: None,
has_more: false,
elasticsearch_raw_body: None,
messages: Vec::new(),
})
.unwrap();
@@ -4281,6 +4356,7 @@ mod tests {
session_id: None,
has_more: false,
elasticsearch_raw_body: None,
messages: Vec::new(),
};
strip_dbx_sqlserver_row_number_column(&mut result, sql);