use std::borrow::Cow;
use enumflags2::BitFlags;
use futures_util::io::{AsyncRead, AsyncWrite};
use crate::{
tds::{
codec::{RpcParam, RpcStatus::ByRefValue, RpcValue, TypeInfoTvp},
stream::{CommandStream, TokenStream},
},
Client, ColumnData, IntoSql,
};
#[doc(inline)]
pub use tiberius_macros::TableValueRow;
pub trait TableValueRow<'a> {
fn bind_fields(&self, data_row: &mut SqlTableDataRow<'a>);
fn get_db_type() -> &'static str;
}
pub trait TableValue<'a> {
fn into_sql(self) -> SqlTableData<'a>;
}
impl<'a, R, C> TableValue<'a> for C
where
R: TableValueRow<'a> + 'a,
C: IntoIterator<Item = R>,
{
fn into_sql(self) -> SqlTableData<'a> {
let mut data = Vec::new();
for row in self {
let mut data_row = SqlTableDataRow::new();
row.bind_fields(&mut data_row);
data.push(data_row);
}
SqlTableData {
rows: data,
db_type: R::get_db_type(),
}
}
}
#[derive(Debug)]
pub struct Command<'a> {
name: Cow<'a, str>,
params: Vec<CommandParam<'a>>,
}
#[derive(Debug)]
struct CommandParam<'a> {
name: Cow<'a, str>,
out: bool,
data: CommandParamData<'a>,
}
#[derive(Debug)]
enum CommandParamData<'a> {
Scalar(ColumnData<'a>),
Table(SqlTableData<'a>),
}
#[derive(Debug)]
pub struct SqlTableData<'a> {
rows: Vec<SqlTableDataRow<'a>>,
db_type: &'a str,
}
#[derive(Debug)]
pub struct SqlTableDataRow<'a> {
col_data: Vec<ColumnData<'a>>,
}
impl<'a> SqlTableDataRow<'a> {
fn new() -> SqlTableDataRow<'a> {
SqlTableDataRow {
col_data: Vec::new(),
}
}
pub fn add_field(&mut self, data: impl IntoSql<'a> + 'a) {
self.col_data.push(data.into_sql());
}
#[doc(hidden)]
pub fn columns(&self) -> &[ColumnData<'a>] {
&self.col_data
}
}
impl<'a> SqlTableData<'a> {
#[doc(hidden)]
pub fn rows(&self) -> &[SqlTableDataRow<'a>] {
&self.rows
}
#[doc(hidden)]
pub fn db_type(&self) -> &str {
self.db_type
}
}
impl<'a> Command<'a> {
pub fn new(proc_name: impl Into<Cow<'a, str>>) -> Self {
Self {
name: proc_name.into(),
params: Vec::new(),
}
}
pub fn bind_param(&mut self, name: impl Into<Cow<'a, str>>, data: impl IntoSql<'a> + 'a) {
self.params.push(CommandParam {
name: name.into(),
out: false,
data: CommandParamData::Scalar(data.into_sql()),
});
}
pub fn bind_out_param(&mut self, name: impl Into<Cow<'a, str>>, data: impl IntoSql<'a> + 'a) {
self.params.push(CommandParam {
name: name.into(),
out: true,
data: CommandParamData::Scalar(data.into_sql()),
});
}
pub fn bind_table(&mut self, name: impl Into<Cow<'a, str>>, data: impl TableValue<'a> + 'a) {
self.params.push(CommandParam {
name: name.into(),
out: false,
data: CommandParamData::Table(data.into_sql()),
});
}
pub fn bind_table_with_dbtype(
&mut self,
name: impl Into<Cow<'a, str>>,
db_type: &'a str,
data: impl TableValue<'a> + 'a,
) {
self.params.push(CommandParam {
name: name.into(),
out: false,
data: CommandParamData::Table(SqlTableData {
db_type,
..data.into_sql()
}),
});
}
pub async fn exec<'b, S>(self, client: &'b mut Client<S>) -> crate::Result<CommandStream<'b>>
where
S: AsyncRead + AsyncWrite + Unpin + Send,
{
let rpc_params = Command::build_rpc_params(self.params, client).await?;
client.connection.flush_stream().await?;
client.rpc_run_command(self.name, rpc_params).await?;
let ts = TokenStream::new(&mut client.connection);
let result = CommandStream::new(ts.try_unfold());
Ok(result)
}
async fn build_rpc_params<'b, S>(
cmd_params: Vec<CommandParam<'a>>,
client: &'b mut Client<S>,
) -> crate::Result<Vec<RpcParam<'a>>>
where
S: AsyncRead + AsyncWrite + Unpin + Send,
{
let mut rpc_params = Vec::new();
for p in cmd_params {
let rpc_val = match p.data {
CommandParamData::Scalar(col) => RpcValue::Scalar(col),
CommandParamData::Table(t) => {
validate_db_type_identifier(t.db_type)?;
let type_info_tvp = TypeInfoTvp::new(
t.db_type,
t.rows.into_iter().map(|r| r.col_data).collect(),
);
let cols_metadata = client
.query_run_for_metadata(format!(
"DECLARE @P AS {};SELECT TOP 0 * FROM @P",
t.db_type
))
.await?;
RpcValue::Table(if let Some(cm) = cols_metadata {
type_info_tvp.with_metadata(cm)
} else {
type_info_tvp
})
}
};
let rpc_param = RpcParam {
name: p.name,
flags: if p.out {
BitFlags::from_flag(ByRefValue)
} else {
BitFlags::empty()
},
value: rpc_val,
};
rpc_params.push(rpc_param);
}
Ok(rpc_params)
}
}
fn validate_db_type_identifier(db_type: &str) -> crate::Result<()> {
crate::client::validate_sql_identifier("TVP db_type", db_type)
}
#[cfg(test)]
mod tests {
use super::*;
struct TestRow;
impl<'a> TableValueRow<'a> for TestRow {
fn bind_fields(&self, row: &mut SqlTableDataRow<'a>) {
row.add_field(1i32);
}
fn get_db_type() -> &'static str {
"default.Type"
}
}
#[test]
fn bind_table_with_dbtype_uses_the_explicit_db_type() {
let mut cmd = Command::new("proc");
cmd.bind_table_with_dbtype("@tvp", "explicit.Type", vec![TestRow]);
assert_eq!(cmd.params.len(), 1);
assert_eq!(cmd.params[0].name, "@tvp");
match &cmd.params[0].data {
CommandParamData::Table(t) => assert_eq!(t.db_type, "explicit.Type"),
other => panic!("expected a table parameter, got {other:?}"),
}
}
#[test]
fn bind_table_uses_the_rows_db_type() {
let mut cmd = Command::new("proc");
cmd.bind_table("@tvp", vec![TestRow]);
match &cmd.params[0].data {
CommandParamData::Table(t) => assert_eq!(t.db_type, "default.Type"),
other => panic!("expected a table parameter, got {other:?}"),
}
}
#[test]
fn validate_db_type_accepts_normal_type_names() {
for db_type in [
"int",
"dbo.MyType",
"[my type]",
"[dbo].[my type]",
"[weird]]type]",
"decimal(18,4)",
"decimal(10,2)",
"varchar(max)",
] {
assert!(
validate_db_type_identifier(db_type).is_ok(),
"expected {db_type:?} to be accepted",
);
}
}
#[test]
fn validate_db_type_rejects_statement_breaking_characters() {
for db_type in [
"int; DROP TABLE x",
"int; DROP TABLE x--",
"int' OR '1'='1",
"int\" ",
"foo-- bar",
] {
assert!(
matches!(
validate_db_type_identifier(db_type),
Err(crate::Error::BulkInput(_))
),
"expected {db_type:?} to be rejected",
);
}
}
#[test]
fn validate_db_type_rejects_control_characters() {
assert!(matches!(
validate_db_type_identifier("int\0"),
Err(crate::Error::BulkInput(_))
));
assert!(matches!(
validate_db_type_identifier("dbo.\nMyType"),
Err(crate::Error::BulkInput(_))
));
}
#[test]
fn validate_db_type_rejects_unbalanced_closing_bracket() {
assert!(matches!(
validate_db_type_identifier("MyType]"),
Err(crate::Error::BulkInput(_))
));
assert!(matches!(
validate_db_type_identifier("a]b"),
Err(crate::Error::BulkInput(_))
));
}
}