Skip to content

Commit 5455787

Browse files
authored
Merge pull request #46 from microsoft/dev/saurabh/sync-2026-05-18
Sync ADO main to GitHub main
2 parents 63de19d + c3aed00 commit 5455787

28 files changed

Lines changed: 1407 additions & 100 deletions

.pipeline/OneBranch/NonOfficialPythonWheelsPublish.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
# Support: https://aka.ms/onebranchsup #
99
#################################################################################
1010

11+
# CI trigger only on development. Main-branch coverage comes from the
12+
# Official pipeline (on merge) and the nightly schedule below; no need to
13+
# also fire NonOfficial on every merge to main.
1114
trigger:
12-
- main
1315
- development
1416

1517
pr:

.pipeline/OneBranch/OfficialPythonWheelsBuild.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@
1010
# Support: https://aka.ms/onebranchsup #
1111
#################################################################################
1212

13-
# No CI or PR triggers — official builds are manual only
14-
trigger: none
13+
# Official build runs on every merge to main; no PR validation (PRs are
14+
# validated by the separate validation pipeline). Can also be queued manually.
15+
trigger:
16+
batch: true
17+
branches:
18+
include:
19+
- main
1520
pr: none
1621

1722
variables:

.pipeline/validation-pipeline.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
trigger:
2-
- main
1+
# CI trigger only on development. Main-branch builds are produced by the
2+
# Official pipeline (OneBranch/OfficialPythonWheelsBuild.yml) on merge.
3+
trigger:
34
- development
45

56
parameters:

docs/github-migration-plan.md

Lines changed: 415 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# GitHub issue [microsoft/mssql-rs#38](https://github.com/microsoft/mssql-rs/issues/38): sp_prepare fails when statement uses a non-int named parameter
2+
3+
## Summary
4+
5+
`TdsClient::execute_sp_prepare` was forwarding the user's named parameter
6+
values as RPC parameters on the `sp_prepare` call. `sp_prepare`'s signature
7+
is fixed (`@handle int OUTPUT, @params ntext, @stmt ntext, @options int`)
8+
and accepts no caller-supplied values; those belong on `sp_execute`,
9+
`sp_prepexec`, or `sp_executesql`. When the extra named arguments did not
10+
coincidentally fit the types `sp_prepare` expects, the server returned
11+
12+
Msg 214: Procedure expects parameter '@options' of type 'int'.
13+
14+
`drain_stream()` collected the ERROR token but the call site dropped the
15+
returned `Vec<SqlErrorInfo>`. The post-drain shape check then saw
16+
`ColumnValues::Null` for the `@handle` output and raised the generic
17+
`ProtocolError("Expected an integer value")` reported in the issue.
18+
19+
## Root cause
20+
21+
In `mssql-tds/src/connection/tds_client.rs`, `execute_sp_prepare` built the
22+
RPC as:
23+
24+
```rust
25+
let rpc = SqlRpc::new(
26+
RpcType::ProcId(RpcProcs::Prepare),
27+
positional_parameters,
28+
Some(named_params), // <-- bug: user values smuggled onto sp_prepare
29+
&database_collation,
30+
&self.execution_context,
31+
);
32+
```
33+
34+
`SqlRpc::write_named_parameters` (`mssql-tds/src/message/rpc.rs`) iterates
35+
the named slice and serializes every entry on the wire. Because the wire
36+
contract for `sp_prepare` does not include user values, the server tried to
37+
bind those as `@options` and rejected anything that wasn't an int.
38+
39+
The same call site also discarded `drain_stream`'s returned errors:
40+
41+
```rust
42+
self.drain_stream().await?; // returned Vec<SqlErrorInfo> dropped
43+
// ... shape check then raised a generic ProtocolError
44+
```
45+
46+
This collapsed the real diagnostic into an unrelated message and is the
47+
reason the issue was originally misread as a parameter-declaration bug.
48+
49+
## Fix
50+
51+
`mssql-tds/src/connection/tds_client.rs::execute_sp_prepare`:
52+
53+
- Pass `None` (not `Some(named_params)`) as the RPC named-parameter list.
54+
`named_params` is still consumed locally to build the `@params` text
55+
string serialized as the second positional argument; only the wire-level
56+
RPC named arguments are dropped.
57+
- Capture the `Vec<SqlErrorInfo>` returned by `drain_stream()` and, when
58+
non-empty, return `Error::SqlServerError { errors }` so callers see the
59+
actual server diagnostic.
60+
61+
The other RPC paths (`execute_sp_executesql`, `execute_sp_prepexec`) are
62+
correct as-is: those stored procedures' contracts include the user's
63+
parameter values after the fixed positional arguments, and the server
64+
expects them on the wire.
65+
66+
## Tests
67+
68+
Added in `mssql-tds/tests/test_rpc_results.rs` (the file already contains
69+
the other `sp_prepare` / `sp_prepexec` integration tests):
70+
71+
- `test_sp_prepare_with_named_nvarchar_param_succeeds`: regression test
72+
for [microsoft/mssql-rs#38](https://github.com/microsoft/mssql-rs/issues/38);
73+
prepares a statement that references
74+
`@db_name nvarchar(6)` and asserts a positive handle is returned, then
75+
unprepares it.
76+
- `test_sp_prepare_surfaces_server_error_on_invalid_sql`: verifies the
77+
diagnostics fix; preparing an undeclared-variable statement returns
78+
`Error::SqlServerError` carrying the populated server message instead
79+
of a generic `ProtocolError`.
80+
81+
Both are integration tests; like the rest of the file they require the
82+
DB environment variables (`DB_HOST`, `DB_USERNAME`, `SQL_PASSWORD`).
83+
84+
## Why the existing tests didn't catch this
85+
86+
- The integration tests in `mssql-tds/tests/test_rpc_results.rs` for
87+
`sp_prepare` (`test_sp_prepare_and_unprepare_multi_param`) only use
88+
`Int` user parameters. SQL Server tolerates extra named values on
89+
`sp_prepare` when their types happen to match what the optional
90+
`@options` parameter accepts (int), so all-int tests pass against a
91+
live server even though the code is wrong.
92+
- No existing test exercises `sp_prepare` with `NVarchar`, `NChar`,
93+
`Decimal`, `DateTime2`, `Uuid`, `Vector`, or any other non-int user
94+
parameter type.
95+
- No negative test asserts what happens when `sp_prepare` fails. With
96+
the drained errors discarded, even a CI run that happened to surface
97+
the bug would have reported the misleading
98+
`ProtocolError("Expected an integer value")`.
99+
- No mock-server test exercises the wire-level RPC structure.
100+
`mssql-mock-tds` exists but is not used to assert that `sp_prepare` is
101+
sent without user named parameters or that ERROR tokens propagate as
102+
`SqlServerError`.
103+
- The integration tests panic when `DB_HOST`/`DB_USERNAME`/`SQL_PASSWORD`
104+
are not set (`mssql-tds/tests/common/mod.rs`). They cannot run as
105+
unit-only checks, so the suite often runs in environments where the
106+
prepare paths simply do not execute.
107+
108+
## Test-coverage follow-ups
109+
110+
These are scoped to the `sp_prepare` regression and would have caught
111+
this specific class of bug. They are not part of this fix.
112+
113+
- Add per-type integration tests for `sp_prepare`, `sp_prepexec`, and
114+
`sp_executesql` that cover at least: `Char`, `NChar`, `Varchar`,
115+
`NVarchar`, `VarcharMax`, `NVarcharMax`, `Decimal` with explicit
116+
precision and scale, `DateTime2`, `DateTimeOffset`, `Time`, `Uuid`,
117+
`VarBinary`, `Vector`. This would have caught the
118+
[microsoft/mssql-rs#38](https://github.com/microsoft/mssql-rs/issues/38)
119+
regression on the first non-int type added.
120+
- Add a mock-TDS test that injects an ERROR token into the response
121+
stream for an `sp_prepare` RPC and asserts the call surfaces it as
122+
`SqlServerError`. This covers the diagnostics path without a live
123+
server.
124+
- Make the integration helpers in `mssql-tds/tests/common/mod.rs` skip
125+
cleanly (instead of panicking) when the database environment variables
126+
are absent, so CI without a SQL Server can still run the unit-style
127+
tests.

mssql-js/src/tracing_init.rs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,7 @@ fn get_trace_log_path() -> Option<PathBuf> {
4747

4848
// Security check: warn about insecure paths but allow them
4949
if is_insecure_path(&path) {
50-
eprintln!(
51-
"[mssql-js] WARNING: Insecure log directory detected: '{}'",
52-
path.display()
53-
);
50+
eprintln!("[mssql-js] WARNING: Insecure log directory detected: {path:?}",);
5451
eprintln!("[mssql-js] WARNING: Logs may contain sensitive data (queries, data etc).");
5552
eprintln!(
5653
"[mssql-js] WARNING: Logging to /tmp, /var/tmp, or system temp directories is not recommended."
@@ -68,11 +65,7 @@ fn get_trace_log_path() -> Option<PathBuf> {
6865
if !path.exists()
6966
&& let Err(e) = create_dir_all(&path)
7067
{
71-
eprintln!(
72-
"[mssql-js] ERROR: Could not create log directory '{}': {}",
73-
path.display(),
74-
e
75-
);
68+
eprintln!("[mssql-js] ERROR: Could not create log directory '{path:?}': {e}",);
7669
return None;
7770
}
7871
return Some(path.join(TRACE_LOG_FILENAME));
@@ -145,8 +138,7 @@ pub fn init_tracing() {
145138
},
146139
Err(e) => {
147140
eprintln!(
148-
"[mssql-js] ERROR: Could not create trace log file '{}': {}. File logging will be disabled.",
149-
log_path.display(), e
141+
"[mssql-js] ERROR: Could not create trace log file {log_path:?}: {e}. File logging will be disabled."
150142
);
151143
}
152144
}

mssql-py-core/src/bulkcopy.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1707,6 +1707,10 @@ impl PythonRowAdapter {
17071707
let vector = SqlVector::try_from_f32(values)?;
17081708
Ok(ColumnValues::Vector(vector))
17091709
}
1710+
VectorBaseType::Float16 => Err(Error::UsageError(format!(
1711+
"Float16 VECTOR column '{}' is not supported in Python bulk copy yet",
1712+
target_meta.column_name
1713+
))),
17101714
}
17111715
}
17121716

@@ -1791,6 +1795,10 @@ impl PythonRowAdapter {
17911795
let vector = SqlVector::try_from_f32(floats)?;
17921796
Ok(ColumnValues::Vector(vector))
17931797
}
1798+
VectorBaseType::Float16 => Err(Error::UsageError(format!(
1799+
"Float16 VECTOR column '{}' is not supported in Python bulk copy JSON coercion",
1800+
target_meta.column_name
1801+
))),
17941802
}
17951803
}
17961804

mssql-py-core/src/connection.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
use pyo3::exceptions::PyRuntimeError;
55
use pyo3::prelude::*;
66
use pyo3::types::PyDict;
7-
use std::sync::Arc;
7+
use std::{path::PathBuf, sync::Arc};
88
use tokio::runtime::Runtime;
99
use tokio::sync::Mutex;
1010

@@ -254,7 +254,7 @@ impl PyCoreConnection {
254254
// ServerCertificate - path to the server certificate file for validation
255255
let server_certificate = dict
256256
.get_item("server_certificate")?
257-
.and_then(|v| v.extract::<String>().ok());
257+
.and_then(|v| v.extract::<PathBuf>().ok());
258258

259259
let encryption_options = EncryptionOptions {
260260
mode: encryption_mode,

mssql-py-core/src/cursor.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,9 @@ impl PyCoreCursor {
550550
return list.into_any();
551551
}
552552
}
553+
VectorBaseType::Float16 => {
554+
// Let it fall through to the string conversion below
555+
}
553556
}
554557
// Fallback to string if conversion fails
555558
format!("{:?}", v)

mssql-py-core/src/tracing_init.rs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,7 @@ fn get_trace_log_path() -> Option<PathBuf> {
8989
if !dir.exists()
9090
&& let Err(e) = create_dir_all(&dir)
9191
{
92-
eprintln!(
93-
"[mssql-py-core] ERROR: Could not create log directory '{}': {}",
94-
dir.display(),
95-
e
96-
);
92+
eprintln!("[mssql-py-core] ERROR: Could not create log directory {dir:?}: {e}");
9793
return None;
9894
}
9995

@@ -143,8 +139,7 @@ pub(crate) fn init_tracing() {
143139
.event_format(LogFormatter);
144140

145141
eprintln!(
146-
"[mssql-py-core] Created log file → {}",
147-
log_path.display()
142+
"[mssql-py-core] Created log file → {log_path:?}"
148143
);
149144

150145
// Store guard to keep async writer alive
@@ -162,9 +157,7 @@ pub(crate) fn init_tracing() {
162157
}
163158
Err(e) => {
164159
eprintln!(
165-
"[mssql-py-core] ERROR: Could not create trace log file '{}': {}. Tracing will be disabled.",
166-
log_path.display(),
167-
e
160+
"[mssql-py-core] ERROR: Could not create trace log file {log_path:?}: {e}. Tracing will be disabled."
168161
);
169162
}
170163
}

0 commit comments

Comments
 (0)