From 9071362fbf98aebef9eb1226b632543450e5f557 Mon Sep 17 00:00:00 2001 From: Joel Parker Henderson Date: Mon, 24 Aug 2026 08:16:42 +0100 Subject: [PATCH] feat: helpers for IN lists and the 2100-parameter limit SQL Server has no array parameter, so an IN list must name one placeholder per value. Binding a comma-separated string to IN (@P1) matches nothing rather than failing, so every caller ends up writing the same format loop (#157). Adds four small, additive items to Query: Query::placeholders(first, count) -> String builds "@P1, @P2, @P3" Query::bind_iter(iter) binds each item in order Query::param_count() -> usize how many are bound Query::MAX_PARAMETERS: usize = 2100 the server's limit MAX_PARAMETERS matters most for exactly these runtime-sized statements: an IN list or a multi-row INSERT reaches the limit by data volume, on a batch that may be larger than any that was tested, and the server reports it only after the whole batch has been sent. No public API changes; nothing is renamed or removed. Eight unit tests and four doc tests, none of which need a server. --- src/query.rs | 185 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) diff --git a/src/query.rs b/src/query.rs index 86e949996..48592050b 100644 --- a/src/query.rs +++ b/src/query.rs @@ -35,6 +35,119 @@ impl<'a> Query<'a> { self.params.push(param.into_sql()); } + /// Bind every item of an iterator, in order. + /// + /// Equivalent to calling [`bind`] once per item. Pairs with + /// [`placeholders`] to build an `IN` list, where the number of + /// parameters is only known at runtime. + /// + /// # Example + /// + /// ``` + /// # use tiberius::Query; + /// let ids = vec![1i32, 2, 3]; + /// + /// let sql = format!( + /// "SELECT name FROM users WHERE id IN ({})", + /// Query::placeholders(1, ids.len()), + /// ); + /// + /// let mut query = Query::new(sql); + /// query.bind_iter(ids); + /// + /// assert_eq!(query.param_count(), 3); + /// ``` + /// + /// [`bind`]: #method.bind + /// [`placeholders`]: #method.placeholders + pub fn bind_iter(&mut self, params: impl IntoIterator + 'a>) { + for param in params { + self.bind(param); + } + } + + /// How many parameters have been bound so far. + /// + /// Useful for checking against [`MAX_PARAMETERS`] before executing a + /// statement whose parameter count is decided at runtime. + /// + /// [`MAX_PARAMETERS`]: #associatedconstant.MAX_PARAMETERS + pub fn param_count(&self) -> usize { + self.params.len() + } + + /// The largest number of parameters SQL Server accepts in one statement. + /// + /// A statement carrying more is rejected by the server with + /// "The incoming request has too many parameters. The server supports a + /// maximum of 2100 parameters." — which arrives only after the whole + /// batch has been sent. + /// + /// This matters most for an `IN` list or a multi-row `INSERT`, where the + /// count comes from the length of a collection rather than from the SQL + /// text: the limit is reached by data volume, at run time, on a batch + /// that may be larger than any that was tested. Split such a batch into + /// chunks of at most `MAX_PARAMETERS / parameters_per_row` items. + /// + /// # Example + /// + /// ``` + /// # use tiberius::Query; + /// // A three-column INSERT: three parameters per row. + /// let rows_per_statement = Query::MAX_PARAMETERS / 3; + /// assert_eq!(rows_per_statement, 700); + /// ``` + pub const MAX_PARAMETERS: usize = 2100; + + /// Build a `@P1, @P2, …` placeholder list for `count` parameters, + /// numbered from `first`. + /// + /// SQL Server has no array parameter, so an `IN` list must name one + /// placeholder per value, and `IN (@P1)` bound to a comma-separated + /// string matches nothing rather than failing. Generating the list is + /// the only way to write such a query, and this does it without a + /// format loop at every call site. + /// + /// `first` is 1-based, matching the `@P1` numbering + /// [`Query::new`] documents. + /// + /// # Example + /// + /// ``` + /// # use tiberius::Query; + /// assert_eq!(Query::placeholders(1, 3), "@P1, @P2, @P3"); + /// + /// // Continuing after parameters that are already bound. + /// assert_eq!(Query::placeholders(4, 2), "@P4, @P5"); + /// ``` + /// + /// A count of zero yields an empty string. `IN ()` is a syntax error, so + /// a caller with nothing to match on should skip the query rather than + /// build one: + /// + /// ``` + /// # use tiberius::Query; + /// let ids: Vec = Vec::new(); + /// assert!(Query::placeholders(1, ids.len()).is_empty()); + /// ``` + /// + /// [`Query::new`]: #method.new + pub fn placeholders(first: usize, count: usize) -> String { + use std::fmt::Write; + + let mut out = String::with_capacity(count * 6); + + for index in 0..count { + if index > 0 { + out.push_str(", "); + } + // Writing into a String cannot fail. + let _ = write!(out, "@P{}", first + index); + } + + out + } + /// Executes SQL statements in the SQL Server, returning the number rows /// affected. Useful for `INSERT`, `UPDATE` and `DELETE` statements. See /// [`Client#execute`] for a simpler API if the parameters are statically @@ -136,3 +249,75 @@ impl<'a> Query<'a> { Ok(result) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn placeholders_are_numbered_from_one() { + assert_eq!(Query::placeholders(1, 1), "@P1"); + assert_eq!(Query::placeholders(1, 3), "@P1, @P2, @P3"); + } + + #[test] + fn placeholders_can_continue_from_an_offset() { + // For a query that already binds parameters before the list. + assert_eq!(Query::placeholders(4, 2), "@P4, @P5"); + assert_eq!(Query::placeholders(10, 1), "@P10"); + } + + #[test] + fn no_placeholders_is_an_empty_string() { + // `IN ()` is a syntax error, so a caller with nothing to match on + // must skip the query rather than build one. + assert_eq!(Query::placeholders(1, 0), ""); + assert_eq!(Query::placeholders(7, 0), ""); + } + + #[test] + fn placeholders_have_no_trailing_separator() { + let list = Query::placeholders(1, 5); + assert!(!list.ends_with(", ")); + assert_eq!(list.matches(',').count(), 4); + } + + #[test] + fn binding_an_iterator_counts_every_item() { + let mut query = Query::new("SELECT 1"); + assert_eq!(query.param_count(), 0); + + query.bind_iter(vec![1i32, 2, 3]); + assert_eq!(query.param_count(), 3); + + query.bind(4i32); + assert_eq!(query.param_count(), 4); + } + + #[test] + fn binding_an_empty_iterator_binds_nothing() { + let mut query = Query::new("SELECT 1"); + query.bind_iter(Vec::::new()); + assert_eq!(query.param_count(), 0); + } + + #[test] + fn a_generated_list_matches_the_number_of_bound_parameters() { + // The invariant that makes this pair usable: one placeholder per + // bound value, or the server rejects the statement. + let ids = vec![10i32, 20, 30, 40]; + let list = Query::placeholders(1, ids.len()); + + let mut query = Query::new(format!("SELECT * FROM t WHERE id IN ({list})")); + query.bind_iter(ids); + + assert_eq!(list.matches("@P").count(), query.param_count()); + } + + #[test] + fn the_parameter_limit_is_the_documented_tds_maximum() { + assert_eq!(Query::MAX_PARAMETERS, 2100); + // The chunking arithmetic the docs describe. + assert_eq!(Query::MAX_PARAMETERS / 3, 700); + } +}