fix(sqlserver): show PRINT messages
This commit is contained in:
7 files changed
+638
-87
No files matched your search
@@ -374,34 +374,59 @@ fn column_types_from_metadata(metadata: &tiberius::ResultMetadata) -> Vec<String
|
||||
|
||||
const SQLSERVER_MESSAGE_COLUMN: &str = "Message";
|
||||
// Tiberius 0.12.3 emits both user-visible INFO tokens and internal ENVCHANGE
|
||||
// tokens at the same tracing target/level. Keep only the TokenInfo callsite;
|
||||
// update this guard together with the pinned driver when Tiberius is upgraded.
|
||||
// tokens at the same tracing target/level. DONE tokens are also consumed before
|
||||
// QueryStream yields its next public item. Keep these callsite guards in sync
|
||||
// with the pinned driver when Tiberius is upgraded.
|
||||
const TIBERIUS_INFO_TOKEN_EVENT_LINE: u32 = 194;
|
||||
const TIBERIUS_DONE_TOKEN_EVENT_LINE: u32 = 153;
|
||||
const TIBERIUS_DONE_PROC_TOKEN_EVENT_LINE: u32 = 159;
|
||||
const TIBERIUS_DONE_IN_PROC_TOKEN_EVENT_LINE: u32 = 165;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct SqlServerMessageLayer {
|
||||
messages: StdArc<StdMutex<Vec<String>>>,
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SqlServerBatchResult {
|
||||
pub result: QueryResult,
|
||||
pub server_message: bool,
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for SqlServerMessageLayer
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum SqlServerTdsEvent {
|
||||
Message(String),
|
||||
Done(Option<u64>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct SqlServerTdsEventLayer {
|
||||
events: StdArc<StdMutex<Vec<SqlServerTdsEvent>>>,
|
||||
}
|
||||
|
||||
impl SqlServerTdsEventLayer {
|
||||
fn take_events(&self) -> Vec<SqlServerTdsEvent> {
|
||||
self.events.lock().map(|mut events| std::mem::take(&mut *events)).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for SqlServerTdsEventLayer
|
||||
where
|
||||
S: Subscriber,
|
||||
{
|
||||
fn on_event(&self, event: &Event<'_>, _ctx: LayerContext<'_, S>) {
|
||||
let metadata = event.metadata();
|
||||
if metadata.level() != &Level::INFO
|
||||
|| metadata.target() != "tiberius::tds::stream::token"
|
||||
|| metadata.line() != Some(TIBERIUS_INFO_TOKEN_EVENT_LINE)
|
||||
{
|
||||
if metadata.target() != "tiberius::tds::stream::token" {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut visitor = SqlServerMessageVisitor::default();
|
||||
event.record(&mut visitor);
|
||||
if let Some(message) = visitor.message.filter(|message| !message.trim().is_empty()) {
|
||||
if let Ok(mut messages) = self.messages.lock() {
|
||||
messages.push(message);
|
||||
}
|
||||
let Some(message) = visitor.message.filter(|message| !message.trim().is_empty()) else {
|
||||
return;
|
||||
};
|
||||
let captured = match (metadata.level(), metadata.line()) {
|
||||
(&Level::INFO, Some(TIBERIUS_INFO_TOKEN_EVENT_LINE)) => Some(SqlServerTdsEvent::Message(message)),
|
||||
(&Level::TRACE, Some(line)) => sqlserver_done_trace_event(line, &message),
|
||||
_ => None,
|
||||
};
|
||||
if let (Some(captured), Ok(mut events)) = (captured, self.events.lock()) {
|
||||
events.push(captured);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -419,36 +444,69 @@ impl tracing::field::Visit for SqlServerMessageVisitor {
|
||||
}
|
||||
}
|
||||
|
||||
fn sqlserver_done_trace_event(line: u32, message: &str) -> Option<SqlServerTdsEvent> {
|
||||
if !matches!(
|
||||
line,
|
||||
TIBERIUS_DONE_TOKEN_EVENT_LINE | TIBERIUS_DONE_PROC_TOKEN_EVENT_LINE | TIBERIUS_DONE_IN_PROC_TOKEN_EVENT_LINE
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
if line == TIBERIUS_DONE_PROC_TOKEN_EVENT_LINE && message.contains("BitFlags<DoneStatus>(0b0)") {
|
||||
return None;
|
||||
}
|
||||
let rows = if !message.contains("Count") {
|
||||
None
|
||||
} else if message.ends_with("(1 row left)") {
|
||||
Some(1)
|
||||
} else if let Some(prefix) = message.strip_suffix(" rows left)") {
|
||||
Some(prefix.rsplit_once('(').and_then(|(_, rows)| rows.parse::<u64>().ok()).unwrap_or(0))
|
||||
} else {
|
||||
Some(0)
|
||||
};
|
||||
Some(SqlServerTdsEvent::Done(rows))
|
||||
}
|
||||
|
||||
async fn capture_sqlserver_messages<F, T>(future: F) -> (T, Vec<String>)
|
||||
where
|
||||
F: Future<Output = T>,
|
||||
{
|
||||
let layer = SqlServerMessageLayer::default();
|
||||
let messages = layer.messages.clone();
|
||||
let layer = SqlServerTdsEventLayer::default();
|
||||
let captured = layer.clone();
|
||||
// Tiberius consumes TDS INFO tokens internally and exposes them only as
|
||||
// tracing events. Scope collection to this future to isolate connections.
|
||||
let output = future.with_subscriber(tracing_subscriber::registry().with(layer)).await;
|
||||
let messages = messages.lock().map(|messages| messages.clone()).unwrap_or_default();
|
||||
let messages = captured
|
||||
.take_events()
|
||||
.into_iter()
|
||||
.filter_map(|event| match event {
|
||||
SqlServerTdsEvent::Message(message) => Some(message),
|
||||
SqlServerTdsEvent::Done(_) => None,
|
||||
})
|
||||
.collect();
|
||||
(output, messages)
|
||||
}
|
||||
|
||||
fn query_result_with_server_messages(mut result: QueryResult, messages: Vec<String>) -> QueryResult {
|
||||
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() {
|
||||
return result;
|
||||
return SqlServerBatchResult { result, server_message: false };
|
||||
}
|
||||
|
||||
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();
|
||||
result
|
||||
SqlServerBatchResult { result, server_message: true }
|
||||
}
|
||||
|
||||
fn server_messages_query_result(messages: Vec<String>, start: Instant) -> Option<QueryResult> {
|
||||
fn server_messages_query_result(messages: Vec<String>, start: Instant) -> Option<SqlServerBatchResult> {
|
||||
if messages.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(query_result_with_server_messages(
|
||||
Some(query_result_with_server_messages_metadata(
|
||||
QueryResult {
|
||||
columns: vec![],
|
||||
column_types: vec![],
|
||||
@@ -1229,21 +1287,91 @@ fn push_sqlserver_result_set(results: &mut Vec<QueryResult>, result: Option<SqlS
|
||||
}
|
||||
}
|
||||
|
||||
async fn collect_result_sets_limited(
|
||||
fn push_sqlserver_ordered_result_set(
|
||||
results: &mut Vec<SqlServerBatchResult>,
|
||||
result: Option<SqlServerResultSet>,
|
||||
start: Instant,
|
||||
) {
|
||||
let mut query_results = Vec::with_capacity(1);
|
||||
push_sqlserver_result_set(&mut query_results, result, start);
|
||||
results.extend(query_results.into_iter().map(|result| SqlServerBatchResult { result, server_message: false }));
|
||||
}
|
||||
|
||||
fn push_sqlserver_ordered_events(
|
||||
results: &mut Vec<SqlServerBatchResult>,
|
||||
current: &mut Option<SqlServerResultSet>,
|
||||
events: Vec<SqlServerTdsEvent>,
|
||||
start: Instant,
|
||||
) {
|
||||
if events.is_empty() {
|
||||
return;
|
||||
}
|
||||
// The first DONE after an active result set closes that SELECT. Its row
|
||||
// count is not a standalone DML result.
|
||||
let mut result_set_done_pending = current.is_some();
|
||||
push_sqlserver_ordered_result_set(results, current.take(), start);
|
||||
|
||||
let mut messages = Vec::new();
|
||||
for event in events {
|
||||
match event {
|
||||
SqlServerTdsEvent::Message(message) => messages.push(message),
|
||||
SqlServerTdsEvent::Done(affected_rows) => {
|
||||
if result_set_done_pending {
|
||||
result_set_done_pending = false;
|
||||
continue;
|
||||
}
|
||||
let Some(affected_rows) = affected_rows else {
|
||||
continue;
|
||||
};
|
||||
if let Some(message_result) = server_messages_query_result(std::mem::take(&mut messages), start) {
|
||||
results.push(message_result);
|
||||
}
|
||||
results.push(SqlServerBatchResult {
|
||||
result: QueryResult {
|
||||
columns: vec![],
|
||||
column_types: vec![],
|
||||
column_sortables: vec![],
|
||||
spatial_columns: vec![],
|
||||
spatial_values: vec![],
|
||||
rows: vec![],
|
||||
affected_rows,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
truncated: false,
|
||||
session_id: None,
|
||||
has_more: false,
|
||||
elasticsearch_raw_body: None,
|
||||
},
|
||||
server_message: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(message_result) = server_messages_query_result(messages, start) {
|
||||
results.push(message_result);
|
||||
}
|
||||
}
|
||||
|
||||
async fn collect_ordered_result_sets_limited(
|
||||
mut stream: QueryStream<'_>,
|
||||
start: Instant,
|
||||
max_rows: Option<usize>,
|
||||
result_offset: usize,
|
||||
) -> Result<Vec<QueryResult>, String> {
|
||||
event_layer: SqlServerTdsEventLayer,
|
||||
) -> Result<Vec<SqlServerBatchResult>, String> {
|
||||
let row_limit = query_result_row_limit(max_rows);
|
||||
let mut results = Vec::new();
|
||||
let mut current: Option<SqlServerResultSet> = None;
|
||||
let mut saw_result_set = false;
|
||||
|
||||
while let Some(item) = stream.try_next().await.map_err(|e| e.to_string())? {
|
||||
loop {
|
||||
let item = stream.try_next().await.map_err(|e| e.to_string())?;
|
||||
push_sqlserver_ordered_events(&mut results, &mut current, event_layer.take_events(), start);
|
||||
let Some(item) = item else {
|
||||
break;
|
||||
};
|
||||
match item {
|
||||
QueryItem::Metadata(metadata) => {
|
||||
push_sqlserver_result_set(&mut results, current.take(), start);
|
||||
push_sqlserver_ordered_result_set(&mut results, current.take(), start);
|
||||
let remaining_offset = if saw_result_set { 0 } else { result_offset };
|
||||
saw_result_set = true;
|
||||
current = Some(SqlServerResultSet {
|
||||
@@ -1276,7 +1404,7 @@ async fn collect_result_sets_limited(
|
||||
}
|
||||
}
|
||||
|
||||
push_sqlserver_result_set(&mut results, current, start);
|
||||
push_sqlserver_ordered_result_set(&mut results, current, start);
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
@@ -2556,12 +2684,22 @@ pub async fn execute_batch_with_max_rows(
|
||||
sql: &str,
|
||||
max_rows: Option<usize>,
|
||||
) -> Result<Vec<QueryResult>, String> {
|
||||
execute_batch_with_max_rows_metadata(client, sql, max_rows)
|
||||
.await
|
||||
.map(|results| results.into_iter().map(|result| result.result).collect())
|
||||
}
|
||||
|
||||
pub(crate) async fn execute_batch_with_max_rows_metadata(
|
||||
client: &mut SqlServerClient,
|
||||
sql: &str,
|
||||
max_rows: Option<usize>,
|
||||
) -> Result<Vec<SqlServerBatchResult>, String> {
|
||||
let start = Instant::now();
|
||||
let result_offset = crate::query_result_sql::sqlserver_result_offset(sql);
|
||||
if sqlserver_batch_can_use_execute(sql) {
|
||||
let (result, messages) = capture_sqlserver_messages(sqlserver_driver_result(client.execute(sql, &[]))).await;
|
||||
let result = result?;
|
||||
return Ok(vec![query_result_with_server_messages(
|
||||
return Ok(vec![query_result_with_server_messages_metadata(
|
||||
QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
@@ -2596,8 +2734,8 @@ pub async fn execute_batch_with_max_rows(
|
||||
})
|
||||
.await;
|
||||
return result.map(|result| {
|
||||
let mut result = query_result_with_server_messages(result, messages);
|
||||
strip_dbx_sqlserver_row_number_column(&mut result, sql);
|
||||
let mut result = query_result_with_server_messages_metadata(result, messages);
|
||||
strip_dbx_sqlserver_row_number_column(&mut result.result, sql);
|
||||
vec![result]
|
||||
});
|
||||
}
|
||||
@@ -2605,7 +2743,7 @@ pub async fn execute_batch_with_max_rows(
|
||||
Ok(None) | Err(_) => {}
|
||||
}
|
||||
}
|
||||
execute_simple_batch_with_max_rows(client, sql, max_rows).await
|
||||
execute_simple_batch_with_max_rows_metadata(client, sql, max_rows).await
|
||||
}
|
||||
|
||||
/// Execute a SQL Server batch directly through TDS simple-query mode.
|
||||
@@ -2618,34 +2756,49 @@ pub async fn execute_simple_batch_with_max_rows(
|
||||
sql: &str,
|
||||
max_rows: Option<usize>,
|
||||
) -> Result<Vec<QueryResult>, String> {
|
||||
execute_simple_batch_with_max_rows_metadata(client, sql, max_rows)
|
||||
.await
|
||||
.map(|results| results.into_iter().map(|result| result.result).collect())
|
||||
}
|
||||
|
||||
pub(crate) async fn execute_simple_batch_with_max_rows_metadata(
|
||||
client: &mut SqlServerClient,
|
||||
sql: &str,
|
||||
max_rows: Option<usize>,
|
||||
) -> Result<Vec<SqlServerBatchResult>, String> {
|
||||
let start = Instant::now();
|
||||
let result_offset = crate::query_result_sql::sqlserver_result_offset(sql);
|
||||
let (results, messages) = capture_sqlserver_messages(async {
|
||||
let layer = SqlServerTdsEventLayer::default();
|
||||
let captured = layer.clone();
|
||||
let results = async {
|
||||
let stream = sqlserver_driver_result(client.simple_query(sql)).await?;
|
||||
sqlserver_driver_result(collect_result_sets_limited(stream, start, max_rows, result_offset)).await
|
||||
})
|
||||
sqlserver_driver_result(collect_ordered_result_sets_limited(stream, start, max_rows, result_offset, captured))
|
||||
.await
|
||||
}
|
||||
.with_subscriber(tracing_subscriber::registry().with(layer))
|
||||
.await;
|
||||
let mut results = results?;
|
||||
for result in &mut results {
|
||||
strip_dbx_sqlserver_row_number_column(result, sql);
|
||||
strip_dbx_sqlserver_row_number_column(&mut result.result, sql);
|
||||
}
|
||||
|
||||
if let Some(message_result) = server_messages_query_result(messages, start) {
|
||||
results.push(message_result);
|
||||
} else if results.is_empty() {
|
||||
results.push(QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
column_sortables: vec![],
|
||||
spatial_columns: vec![],
|
||||
spatial_values: vec![],
|
||||
rows: vec![],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
truncated: false,
|
||||
session_id: None,
|
||||
has_more: false,
|
||||
elasticsearch_raw_body: None,
|
||||
if results.is_empty() {
|
||||
results.push(SqlServerBatchResult {
|
||||
result: QueryResult {
|
||||
columns: vec![],
|
||||
column_types: Vec::new(),
|
||||
column_sortables: vec![],
|
||||
spatial_columns: vec![],
|
||||
spatial_values: vec![],
|
||||
rows: vec![],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: start.elapsed().as_millis(),
|
||||
truncated: false,
|
||||
session_id: None,
|
||||
has_more: false,
|
||||
elasticsearch_raw_body: None,
|
||||
},
|
||||
server_message: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2701,6 +2854,7 @@ fn is_dbx_sqlserver_row_number_page_sql(sql: &str) -> bool {
|
||||
fn sqlserver_batch_can_use_execute(sql: &str) -> bool {
|
||||
let contains_transaction_control = contains_transaction_control(sql);
|
||||
!requires_simple_query_batch(sql)
|
||||
&& !sqlserver_batch_may_emit_info_messages(sql)
|
||||
// Tiberius executes this path through an RPC. SQL Server rejects an RPC
|
||||
// that changes @@TRANCOUNT with error 266, while a regular batch is allowed
|
||||
// to leave an explicit transaction open for a later COMMIT or ROLLBACK.
|
||||
@@ -2709,6 +2863,10 @@ fn sqlserver_batch_can_use_execute(sql: &str) -> bool {
|
||||
&& !sqlserver_dml_output_returns_rows(sql)
|
||||
}
|
||||
|
||||
fn sqlserver_batch_may_emit_info_messages(sql: &str) -> bool {
|
||||
top_level_sqlserver_tokens(sql).iter().any(|token| matches!(token.text.as_str(), "PRINT" | "RAISERROR" | "DBCC"))
|
||||
}
|
||||
|
||||
fn sqlserver_balanced_transaction_batch_can_use_execute(sql: &str) -> bool {
|
||||
let tokens = top_level_sqlserver_tokens(sql);
|
||||
if tokens.iter().any(|token| {
|
||||
@@ -2905,18 +3063,19 @@ mod tests {
|
||||
use super::{
|
||||
build_sqlserver_unsafe_type_query, capture_sqlserver_messages, completion_context_from_query_result,
|
||||
decode_sqlserver_spatial_values, format_sqlserver_numeric, is_blocking_sqlserver_unsafe_probe_error,
|
||||
is_sqlserver_spatial_column, is_sqlserver_variant_column, query_result_with_server_messages,
|
||||
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,
|
||||
sqlserver_dml_output_returns_rows, sqlserver_filter_definition_error, sqlserver_hidden_schema_names,
|
||||
sqlserver_indexes_sql, sqlserver_legacy_indexes_sql, sqlserver_legacy_probe, sqlserver_legacy_probe_with_nonce,
|
||||
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, 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_list_objects_sql, sqlserver_list_schemas_sql, sqlserver_list_tables_sql,
|
||||
sqlserver_probe_explicit_alias, 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, SqlServerResultSet, SqlServerSpatialColumn, SQLSERVER_COMPLETION_CONTEXT_SQL,
|
||||
SQLSERVER_RESULT_TYPE_PROBE_SQL,
|
||||
SqlServerProbeOutputNameOverride, SqlServerResultSet, SqlServerSpatialColumn, SqlServerTdsEvent,
|
||||
SQLSERVER_COMPLETION_CONTEXT_SQL, SQLSERVER_RESULT_TYPE_PROBE_SQL,
|
||||
};
|
||||
use crate::types::{
|
||||
CompletionAssistantMatchMode, CompletionAssistantObjectKind, CompletionAssistantRequest, QueryResult,
|
||||
@@ -2966,6 +3125,25 @@ mod tests {
|
||||
assert_eq!(result.columns, vec!["Message"]);
|
||||
assert_eq!(result.rows, vec![vec![serde_json::json!("DBCC execution completed")]]);
|
||||
|
||||
let message = query_result_with_server_messages_metadata(
|
||||
QueryResult {
|
||||
columns: vec![],
|
||||
column_types: vec![],
|
||||
column_sortables: vec![],
|
||||
spatial_columns: vec![],
|
||||
spatial_values: vec![],
|
||||
rows: vec![],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
truncated: false,
|
||||
session_id: None,
|
||||
has_more: false,
|
||||
elasticsearch_raw_body: None,
|
||||
},
|
||||
vec!["PRINT output".to_string()],
|
||||
);
|
||||
assert!(message.server_message);
|
||||
|
||||
let select = QueryResult {
|
||||
columns: vec!["id".to_string()],
|
||||
column_types: vec!["int".to_string()],
|
||||
@@ -2980,9 +3158,150 @@ mod tests {
|
||||
has_more: false,
|
||||
elasticsearch_raw_body: None,
|
||||
};
|
||||
let result = query_result_with_server_messages(select, vec!["informational".to_string()]);
|
||||
assert_eq!(result.columns, vec!["id"]);
|
||||
assert_eq!(result.rows, vec![vec![serde_json::json!(1)]]);
|
||||
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)]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_done_trace_events_keep_statement_boundaries_and_counts() {
|
||||
assert_eq!(
|
||||
sqlserver_done_trace_event(
|
||||
super::TIBERIUS_DONE_TOKEN_EVENT_LINE,
|
||||
"Done with status BitFlags<DoneStatus>(0b10001, More | Count) (2 rows left)",
|
||||
),
|
||||
Some(SqlServerTdsEvent::Done(Some(2)))
|
||||
);
|
||||
assert_eq!(
|
||||
sqlserver_done_trace_event(
|
||||
super::TIBERIUS_DONE_IN_PROC_TOKEN_EVENT_LINE,
|
||||
"Done with status BitFlags<DoneStatus>(0b10000, Count)",
|
||||
),
|
||||
Some(SqlServerTdsEvent::Done(Some(0)))
|
||||
);
|
||||
assert_eq!(
|
||||
sqlserver_done_trace_event(
|
||||
super::TIBERIUS_DONE_PROC_TOKEN_EVENT_LINE,
|
||||
"Done with status BitFlags<DoneStatus>(0b0)",
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
sqlserver_done_trace_event(
|
||||
super::TIBERIUS_DONE_TOKEN_EVENT_LINE,
|
||||
"Done with status BitFlags<DoneStatus>(0b1, More)",
|
||||
),
|
||||
Some(SqlServerTdsEvent::Done(None))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_ordered_events_preserve_message_result_and_count_order() {
|
||||
let start = Instant::now();
|
||||
let mut results = Vec::new();
|
||||
let mut current = None;
|
||||
|
||||
push_sqlserver_ordered_events(
|
||||
&mut results,
|
||||
&mut current,
|
||||
vec![SqlServerTdsEvent::Message("before".to_string())],
|
||||
start,
|
||||
);
|
||||
current = Some(SqlServerResultSet {
|
||||
columns: vec!["first".to_string()],
|
||||
column_types: vec!["int".to_string()],
|
||||
rows: vec![vec![serde_json::json!(1)]],
|
||||
truncated: false,
|
||||
remaining_offset: 0,
|
||||
});
|
||||
push_sqlserver_ordered_events(
|
||||
&mut results,
|
||||
&mut current,
|
||||
vec![SqlServerTdsEvent::Done(Some(1)), SqlServerTdsEvent::Message("between".to_string())],
|
||||
start,
|
||||
);
|
||||
current = Some(SqlServerResultSet {
|
||||
columns: vec!["second".to_string()],
|
||||
column_types: vec!["int".to_string()],
|
||||
rows: vec![vec![serde_json::json!(2)]],
|
||||
truncated: false,
|
||||
remaining_offset: 0,
|
||||
});
|
||||
push_sqlserver_ordered_events(
|
||||
&mut results,
|
||||
&mut current,
|
||||
vec![
|
||||
SqlServerTdsEvent::Done(Some(1)),
|
||||
SqlServerTdsEvent::Message("after one".to_string()),
|
||||
SqlServerTdsEvent::Message("after two".to_string()),
|
||||
SqlServerTdsEvent::Done(Some(3)),
|
||||
],
|
||||
start,
|
||||
);
|
||||
|
||||
assert_eq!(results.len(), 6);
|
||||
assert!(results[0].server_message);
|
||||
assert_eq!(results[0].result.rows, vec![vec![serde_json::json!("before")]]);
|
||||
assert_eq!(results[1].result.columns, vec!["first"]);
|
||||
assert!(results[2].server_message);
|
||||
assert_eq!(results[2].result.rows, vec![vec![serde_json::json!("between")]]);
|
||||
assert_eq!(results[3].result.columns, vec!["second"]);
|
||||
assert!(results[4].server_message);
|
||||
assert_eq!(
|
||||
results[4].result.rows,
|
||||
vec![vec![serde_json::json!("after one")], vec![serde_json::json!("after two")]]
|
||||
);
|
||||
assert_eq!(results[5].result.affected_rows, 3);
|
||||
assert!(!results[5].server_message);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_ordered_events_drop_select_counts_and_keep_following_dml_counts() {
|
||||
let start = Instant::now();
|
||||
let mut results = Vec::new();
|
||||
let mut current = Some(SqlServerResultSet {
|
||||
columns: vec!["selected".to_string()],
|
||||
column_types: vec!["int".to_string()],
|
||||
rows: vec![vec![serde_json::json!(1)]],
|
||||
truncated: false,
|
||||
remaining_offset: 0,
|
||||
});
|
||||
|
||||
push_sqlserver_ordered_events(
|
||||
&mut results,
|
||||
&mut current,
|
||||
vec![SqlServerTdsEvent::Done(Some(1)), SqlServerTdsEvent::Done(Some(2))],
|
||||
start,
|
||||
);
|
||||
|
||||
assert_eq!(results.len(), 2);
|
||||
assert_eq!(results[0].result.columns, vec!["selected"]);
|
||||
assert_eq!(results[1].result.affected_rows, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_ordered_events_use_no_count_done_to_close_select_result() {
|
||||
let start = Instant::now();
|
||||
let mut results = Vec::new();
|
||||
let mut current = Some(SqlServerResultSet {
|
||||
columns: vec!["selected".to_string()],
|
||||
column_types: vec!["int".to_string()],
|
||||
rows: vec![vec![serde_json::json!(1)]],
|
||||
truncated: false,
|
||||
remaining_offset: 0,
|
||||
});
|
||||
|
||||
push_sqlserver_ordered_events(
|
||||
&mut results,
|
||||
&mut current,
|
||||
vec![SqlServerTdsEvent::Done(None), SqlServerTdsEvent::Done(Some(2))],
|
||||
start,
|
||||
);
|
||||
|
||||
assert_eq!(results.len(), 2);
|
||||
assert_eq!(results[0].result.columns, vec!["selected"]);
|
||||
assert_eq!(results[1].result.affected_rows, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3137,6 +3456,11 @@ mod tests {
|
||||
assert!(sqlserver_batch_can_use_execute(
|
||||
"MERGE dbo.t AS t USING dbo.s AS s ON t.id = s.id WHEN MATCHED THEN UPDATE SET name = s.name;"
|
||||
));
|
||||
assert!(!sqlserver_batch_can_use_execute(
|
||||
"PRINT N'before'; UPDATE dbo.users SET active = 0 WHERE id = 1; PRINT N'after';"
|
||||
));
|
||||
assert!(!sqlserver_batch_can_use_execute("DBCC CHECKIDENT ('dbo.users', NORESEED);"));
|
||||
assert!(!sqlserver_batch_can_use_execute("RAISERROR(N'progress', 0, 1) WITH NOWAIT;"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -144,6 +144,7 @@ fn query_error_with_omitted_sql_context(error: &str, _sql: &str) -> String {
|
||||
/// `execution_error` is emitted for synthesized per-statement errors so clients
|
||||
/// can distinguish them from a successful result column named `Error`.
|
||||
/// `statement_index` is emitted only after a concrete statement starts running.
|
||||
/// `server_message` is emitted only for SQL Server TDS informational messages.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ExecuteMultiResult {
|
||||
#[serde(flatten)]
|
||||
@@ -154,6 +155,8 @@ pub struct ExecuteMultiResult {
|
||||
pub statement_index: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<crate::backend_error::BackendError>,
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub server_message: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -193,12 +196,12 @@ fn report_execute_multi_progress(
|
||||
impl ExecuteMultiResult {
|
||||
fn execution_error(result: db::QueryResult) -> Self {
|
||||
let error = error_from_query_result(&result);
|
||||
Self { result, execution_error: true, statement_index: None, error }
|
||||
Self { result, execution_error: true, statement_index: None, error, server_message: false }
|
||||
}
|
||||
|
||||
fn execution_error_with_index(result: db::QueryResult, statement_index: usize) -> Self {
|
||||
let error = error_from_query_result(&result);
|
||||
Self { result, execution_error: true, statement_index: Some(statement_index), error }
|
||||
Self { result, execution_error: true, statement_index: Some(statement_index), error, server_message: false }
|
||||
}
|
||||
|
||||
fn execution_error_with_backend(
|
||||
@@ -206,11 +209,17 @@ impl ExecuteMultiResult {
|
||||
statement_index: Option<usize>,
|
||||
error: crate::backend_error::BackendError,
|
||||
) -> Self {
|
||||
Self { result, execution_error: true, statement_index, error: Some(error) }
|
||||
Self { result, execution_error: true, statement_index, error: Some(error), server_message: false }
|
||||
}
|
||||
|
||||
fn success_with_index(result: db::QueryResult, statement_index: usize) -> Self {
|
||||
Self { result, execution_error: false, statement_index: Some(statement_index), error: None }
|
||||
Self {
|
||||
result,
|
||||
execution_error: false,
|
||||
statement_index: Some(statement_index),
|
||||
error: None,
|
||||
server_message: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn without_error_detail(mut self) -> Self {
|
||||
@@ -225,7 +234,19 @@ impl ExecuteMultiResult {
|
||||
|
||||
impl From<db::QueryResult> for ExecuteMultiResult {
|
||||
fn from(result: db::QueryResult) -> Self {
|
||||
Self { result, execution_error: false, statement_index: None, error: None }
|
||||
Self { result, execution_error: false, statement_index: None, error: None, server_message: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<db::sqlserver::SqlServerBatchResult> for ExecuteMultiResult {
|
||||
fn from(result: db::sqlserver::SqlServerBatchResult) -> Self {
|
||||
Self {
|
||||
result: result.result,
|
||||
execution_error: false,
|
||||
statement_index: None,
|
||||
error: None,
|
||||
server_message: result.server_message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2431,7 +2452,7 @@ fn empty_query_result(execution_time_ms: u128) -> db::QueryResult {
|
||||
}
|
||||
}
|
||||
|
||||
fn sqlserver_batch_results(results: Vec<db::QueryResult>) -> Vec<ExecuteMultiResult> {
|
||||
fn sqlserver_batch_results(results: Vec<db::sqlserver::SqlServerBatchResult>) -> Vec<ExecuteMultiResult> {
|
||||
results.into_iter().map(ExecuteMultiResult::from).collect()
|
||||
}
|
||||
|
||||
@@ -2494,9 +2515,9 @@ async fn execute_multi_sqlserver(
|
||||
|
||||
let execution = async {
|
||||
if execution_mode == QueryExecutionMode::Simple {
|
||||
db::sqlserver::execute_simple_batch_with_max_rows(&mut client_guard, batch, max_rows).await
|
||||
db::sqlserver::execute_simple_batch_with_max_rows_metadata(&mut client_guard, batch, max_rows).await
|
||||
} else {
|
||||
db::sqlserver::execute_batch_with_max_rows(&mut client_guard, batch, max_rows).await
|
||||
db::sqlserver::execute_batch_with_max_rows_metadata(&mut client_guard, batch, max_rows).await
|
||||
}
|
||||
};
|
||||
let result = wait_for_result_opt(cancel_token.clone(), query_timeout, execution).await;
|
||||
@@ -4954,6 +4975,7 @@ for line in sys.stdin:
|
||||
let success = serde_json::to_value(ExecuteMultiResult::from(empty_query_result(0))).unwrap();
|
||||
assert!(success.get("execution_error").is_none());
|
||||
assert!(success.get("statement_index").is_none());
|
||||
assert!(success.get("server_message").is_none());
|
||||
|
||||
let mut error_column = empty_query_result(0);
|
||||
error_column.columns = vec!["Error".to_string()];
|
||||
@@ -4987,9 +5009,17 @@ for line in sys.stdin:
|
||||
fn sqlserver_batch_results_do_not_claim_statement_indexes() {
|
||||
assert_eq!(split_sql_batches("SELECT 1; SELECT 2;").len(), 1);
|
||||
|
||||
let results = sqlserver_batch_results(vec![empty_query_result(1), empty_query_result(2)]);
|
||||
let results = sqlserver_batch_results(vec![
|
||||
db::sqlserver::SqlServerBatchResult { result: empty_query_result(1), server_message: false },
|
||||
db::sqlserver::SqlServerBatchResult { result: empty_query_result(2), server_message: true },
|
||||
]);
|
||||
|
||||
assert_eq!(results.iter().map(|result| result.statement_index).collect::<Vec<_>>(), vec![None, None]);
|
||||
assert!(!results[0].server_message);
|
||||
assert!(results[1].server_message);
|
||||
|
||||
let serialized = serde_json::to_value(&results[1]).unwrap();
|
||||
assert_eq!(serialized.get("server_message"), Some(&serde_json::Value::Bool(true)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user