Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,20 @@ while let Some(row) = rows.try_next().await? {
}
```

Databases with native named parameter support can bind using `bind_named()`. Pass the exact
parameter token used in the SQL statement, including its marker. SQLite supports `:name`, `@name`,
and `$name`; MSSQL uses `@name`.

```rust
let row = query_scalar::<_, i64>("SELECT :value + :value")
.bind_named(":value", 150_i64)
.fetch_one(&mut conn)
.await?;
```

Named and positional parameters cannot be mixed in one query. Databases without native named
parameter support do not provide `bind_named()`.

To assist with mapping the row into a domain type, there are two idioms that may be used:

```rust
Expand Down
13 changes: 13 additions & 0 deletions sqlx-core/src/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ pub trait Arguments<'q>: Send + Sized + Default {
}
}

/// Arguments bound by their database-native parameter names.
///
/// The name must include the parameter marker used in the SQL statement, e.g. `:id` for SQLite
/// or `@id` for MSSQL. This trait is implemented only by databases whose native parameter
/// protocol supports named parameters. Named and positional parameters must not be mixed in one
/// query.
pub trait NamedArguments<'q>: Arguments<'q> {
/// Add a value for the named parameter.
fn add_named<T>(&mut self, name: &'q str, value: T)
where
T: 'q + Send + Encode<'q, Self::Database> + Type<Self::Database>;
}

pub trait IntoArguments<'q, DB: HasArguments<'q>>: Sized + Send {
fn into_arguments(self) -> <DB as HasArguments<'q>>::Arguments;
}
Expand Down
4 changes: 2 additions & 2 deletions sqlx-core/src/logger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ impl<'q> QueryLogger<'q> {

let sql = if summary != self.sql {
summary.push_str(" …");
format!("\n\n{}\n", &self.sql)
format!("\n\n{}\n", self.sql)
} else {
String::new()
};
Expand Down Expand Up @@ -125,7 +125,7 @@ impl<'q, O: Debug + Hash + Eq, R: Debug, P: Debug> QueryPlanLogger<'q, O, R, P>

let sql = if summary != self.sql {
summary.push_str(" …");
format!("\n\n{}\n", &self.sql)
format!("\n\n{}\n", self.sql)
} else {
String::new()
};
Expand Down
82 changes: 55 additions & 27 deletions sqlx-core/src/mssql/arguments.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::arguments::Arguments;
use crate::arguments::{Arguments, NamedArguments};
use crate::encode::Encode;
use crate::mssql::database::Mssql;
use crate::mssql::io::MssqlBufMutExt;
Expand All @@ -14,14 +14,12 @@ pub struct MssqlArguments {
name: String,
pub(crate) data: Vec<u8>,
pub(crate) declarations: String,
positional: bool,
named: bool,
}

impl MssqlArguments {
pub(crate) fn add_named<'q, T: Encode<'q, Mssql> + Type<Mssql>>(
&mut self,
name: &str,
value: T,
) {
fn add_rpc_named<'q, T: Encode<'q, Mssql> + Type<Mssql>>(&mut self, name: &str, value: T) {
let ty = value.produces().unwrap_or_else(T::type_info);

let mut ty_name = String::new();
Expand All @@ -35,7 +33,7 @@ impl MssqlArguments {
}

pub(crate) fn add_unnamed<'q, T: Encode<'q, Mssql> + Type<Mssql>>(&mut self, value: T) {
self.add_named("", value);
self.add_rpc_named("", value);
}

pub(crate) fn declare<'q, T: Encode<'q, Mssql> + Type<Mssql>>(
Expand Down Expand Up @@ -64,7 +62,7 @@ impl MssqlArguments {
where
T: Encode<'q, Mssql> + Type<Mssql>,
{
let ty = value.produces().unwrap_or_else(T::type_info);
self.positional = true;

// produce an ordinal parameter name
// @p1, @p2, ... @pN
Expand All @@ -75,31 +73,33 @@ impl MssqlArguments {
self.ordinal += 1;
self.name.push_str(itoa::Buffer::new().format(self.ordinal));

let MssqlArguments {
ref name,
ref mut declarations,
ref mut data,
..
} = self;
let name = std::mem::take(&mut self.name);
self.add_query_named(&name, value);
self.name = name;
}

// add this to our variable declaration list
// @p1 int, @p2 nvarchar(10), ...
fn add_query_named<'q, T>(&mut self, name: &str, value: T)
where
T: Encode<'q, Mssql> + Type<Mssql>,
{
let ty = value.produces().unwrap_or_else(T::type_info);

if !declarations.is_empty() {
declarations.push(',');
if !self.declarations.is_empty() {
self.declarations.push(',');
}

declarations.push_str(name);
declarations.push(' ');
ty.0.fmt(declarations);
self.declarations.push_str(name);
self.declarations.push(' ');
ty.0.fmt(&mut self.declarations);

// write out the parameter

data.put_b_varchar(name); // [ParamName]
data.push(0); // [StatusFlags]
self.data.put_b_varchar(name); // [ParamName]
self.data.push(0); // [StatusFlags]
ty.0.put(&mut self.data); // [TYPE_INFO]
ty.0.put_value(&mut self.data, value); // [ParamLenData]
}

ty.0.put(data); // [TYPE_INFO]
ty.0.put_value(data, value); // [ParamLenData]
pub(crate) fn has_mixed_binding(&self) -> bool {
self.positional && self.named
}
}

Expand All @@ -126,6 +126,16 @@ impl<'q> Arguments<'q> for MssqlArguments {
}
}

impl<'q> NamedArguments<'q> for MssqlArguments {
fn add_named<T>(&mut self, name: &'q str, value: T)
where
T: 'q + Send + Encode<'q, Self::Database> + Type<Self::Database>,
{
self.named = true;
self.add_query_named(name, value);
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -167,4 +177,22 @@ mod tests {

assert_eq!(sql, "SELECT * FROM table WHERE id=@p1 AND name=@p2");
}

#[test]
fn test_named_query_parameter() {
let mut args = MssqlArguments::default();
args.add_query_named("@id", 42_i32);

assert_eq!(args.declarations, "@id int");
assert!(!args.data.is_empty());
}

#[test]
fn test_mixed_query_parameters_are_detected() {
let mut args = MssqlArguments::default();
args.add(42_i32);
args.add_named("@id", 42_i32);

assert!(args.has_mixed_binding());
}
}
8 changes: 8 additions & 0 deletions sqlx-core/src/mssql/connection/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ use std::sync::Arc;

impl MssqlConnection {
async fn run(&mut self, query: &str, arguments: Option<MssqlArguments>) -> Result<(), Error> {
if let Some(arguments) = arguments.as_ref() {
if arguments.has_mixed_binding() {
return Err(err_protocol!(
"cannot mix named and positional MSSQL parameters"
));
}
}

self.stream.wait_until_ready().await?;
self.stream.pending_done_count += 1;

Expand Down
2 changes: 1 addition & 1 deletion sqlx-core/src/mssql/connection/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ pub(crate) fn write_packets<'en, T: Encode<'en>>(
);
}

packet_header.truncate(0);
packet_header.clear();
PacketHeader {
r#type: ty,
status: if is_last {
Expand Down
2 changes: 1 addition & 1 deletion sqlx-core/src/postgres/connection/sasl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ pub(crate) async fn authenticate(
let client_final_message_wo_proof = format!(
"{channel_binding},r={nonce}",
channel_binding = channel_binding,
nonce = &cont.nonce
nonce = cont.nonce
);

// AuthMessage := client-first-message-bare + "," + server-first-message + "," + client-final-message-without-proof
Expand Down
17 changes: 16 additions & 1 deletion sqlx-core/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use either::Either;
use futures_core::stream::BoxStream;
use futures_util::{future, StreamExt, TryFutureExt, TryStreamExt};

use crate::arguments::{Arguments, IntoArguments};
use crate::arguments::{Arguments, IntoArguments, NamedArguments};
use crate::database::{Database, HasArguments, HasStatement, HasStatementCache};
use crate::encode::Encode;
use crate::error::Error;
Expand Down Expand Up @@ -82,6 +82,21 @@ impl<'q, DB: Database> Query<'q, DB, <DB as HasArguments<'q>>::Arguments> {
.add(value);
self
}

/// Bind a value for use with a database-native named SQL parameter.
///
/// `name` must include the parameter marker used in the query, such as `:id` for SQLite or
/// `@id` for MSSQL. Named and positional parameters must not be mixed in one query.
pub fn bind_named<T>(mut self, name: &'q str, value: T) -> Self
where
<DB as HasArguments<'q>>::Arguments: NamedArguments<'q, Database = DB>,
T: 'q + Send + Encode<'q, DB> + Type<DB>,
{
self.arguments
.get_or_insert_with(Default::default)
.add_named(name, value);
self
}
}

impl<'q, DB, A> Query<'q, DB, A>
Expand Down
12 changes: 11 additions & 1 deletion sqlx-core/src/query_as.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use either::Either;
use futures_core::stream::BoxStream;
use futures_util::{StreamExt, TryStreamExt};

use crate::arguments::IntoArguments;
use crate::arguments::{IntoArguments, NamedArguments};
use crate::database::{Database, HasArguments, HasStatement, HasStatementCache};
use crate::encode::Encode;
use crate::error::Error;
Expand Down Expand Up @@ -55,6 +55,16 @@ impl<'q, DB: Database, O> QueryAs<'q, DB, O, <DB as HasArguments<'q>>::Arguments
self.inner = self.inner.bind(value);
self
}

/// Bind a value for use with a database-native named SQL parameter.
pub fn bind_named<T>(mut self, name: &'q str, value: T) -> Self
where
<DB as HasArguments<'q>>::Arguments: NamedArguments<'q, Database = DB>,
T: 'q + Send + Encode<'q, DB> + Type<DB>,
{
self.inner = self.inner.bind_named(name, value);
self
}
}

impl<'q, DB, O, A> QueryAs<'q, DB, O, A>
Expand Down
52 changes: 51 additions & 1 deletion sqlx-core/src/query_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::fmt::Display;
use std::fmt::Write;
use std::marker::PhantomData;

use crate::arguments::Arguments;
use crate::arguments::{Arguments, NamedArguments};
use crate::database::{Database, HasArguments};
use crate::encode::Encode;
use crate::from_row::FromRow;
Expand Down Expand Up @@ -135,6 +135,26 @@ where
self
}

/// Push an exact database-native named parameter token and bind a value to it.
///
/// For example, use `:id` for SQLite or `@id` for MSSQL. The token is appended verbatim to the
/// SQL query. Named and positional parameters must not be mixed in one query.
pub fn push_bind_named<T>(&mut self, name: &'args str, value: T) -> &mut Self
where
<DB as HasArguments<'args>>::Arguments: NamedArguments<'args, Database = DB>,
T: 'args + Encode<'args, DB> + Send + Type<DB>,
{
self.sanity_check();

self.arguments
.as_mut()
.expect("BUG: Arguments taken already")
.add_named(name, value);
self.query.push_str(name);

self
}

/// Start a list separated by `separator`.
///
/// The returned type exposes identical [`.push()`][Separated::push] and
Expand Down Expand Up @@ -527,6 +547,23 @@ where
self
}

/// Push the separator if applicable, then append an exact database-native named parameter
/// token and bind a value to it.
pub fn push_bind_named<T>(&mut self, name: &'args str, value: T) -> &mut Self
where
<DB as HasArguments<'args>>::Arguments: NamedArguments<'args, Database = DB>,
T: 'args + Encode<'args, DB> + Send + Type<DB>,
{
if self.push_separator {
self.query_builder.push(&self.separator);
}

self.query_builder.push_bind_named(name, value);
self.push_separator = true;

self
}

/// Push a bind argument placeholder (`?` or `$N` for Postgres) and bind a value to it
/// without a separator.
///
Expand All @@ -544,6 +581,9 @@ where
mod test {
use crate::postgres::Postgres;

#[cfg(feature = "sqlite")]
use crate::sqlite::Sqlite;

use super::*;

#[test]
Expand Down Expand Up @@ -588,6 +628,16 @@ mod test {
);
}

#[cfg(feature = "sqlite")]
#[test]
fn test_push_bind_named() {
let mut qb: QueryBuilder<'_, Sqlite> = QueryBuilder::new("SELECT * FROM users WHERE id = ");

qb.push_bind_named(":id", 42_i32);

assert_eq!(qb.sql(), "SELECT * FROM users WHERE id = :id");
}

#[test]
fn test_build() {
let mut qb: QueryBuilder<'_, Postgres> = QueryBuilder::new("SELECT * FROM users");
Expand Down
12 changes: 11 additions & 1 deletion sqlx-core/src/query_scalar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use either::Either;
use futures_core::stream::BoxStream;
use futures_util::{StreamExt, TryFutureExt, TryStreamExt};

use crate::arguments::IntoArguments;
use crate::arguments::{IntoArguments, NamedArguments};
use crate::database::{Database, HasArguments, HasStatement, HasStatementCache};
use crate::encode::Encode;
use crate::error::Error;
Expand Down Expand Up @@ -52,6 +52,16 @@ impl<'q, DB: Database, O> QueryScalar<'q, DB, O, <DB as HasArguments<'q>>::Argum
self.inner = self.inner.bind(value);
self
}

/// Bind a value for use with a database-native named SQL parameter.
pub fn bind_named<T>(mut self, name: &'q str, value: T) -> Self
where
<DB as HasArguments<'q>>::Arguments: NamedArguments<'q, Database = DB>,
T: 'q + Send + Encode<'q, DB> + Type<DB>,
{
self.inner = self.inner.bind_named(name, value);
self
}
}

impl<'q, DB, O, A> QueryScalar<'q, DB, O, A>
Expand Down
Loading
Loading