diff --git a/Cargo.toml b/Cargo.toml index ffe4eb4..0696f63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,6 +124,7 @@ fallible-iterator = "0.3" fallible-streaming-iterator = "0.1" uuid = { version = "1.0", optional = true } smallvec = "1.6.1" +rusqlite-macros = { path = "rusqlite-macros", version = "0.1.0", optional = true } [dev-dependencies] doc-comment = "0.3" @@ -159,7 +160,7 @@ name = "exec" harness = false [package.metadata.docs.rs] -features = ["modern-full"] +features = ["modern-full", "rusqlite-macros"] all-features = false no-default-features = true default-target = "x86_64-unknown-linux-gnu" diff --git a/rusqlite-macros/Cargo.toml b/rusqlite-macros/Cargo.toml new file mode 100644 index 0000000..2fad35b --- /dev/null +++ b/rusqlite-macros/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "rusqlite-macros" +version = "0.1.0" +authors = ["The rusqlite developers"] +edition = "2021" +description = "Private implementation detail of rusqlite crate" +repository = "https://github.com/rusqlite/rusqlite" +license = "MIT" +categories = ["database"] + +[lib] +proc-macro = true + +[dependencies] +sqlite3-parser = { version = "0.11", default-features = false, features = ["YYNOERRORRECOVERY"] } +fallible-iterator = "0.3" diff --git a/rusqlite-macros/src/lib.rs b/rusqlite-macros/src/lib.rs new file mode 100644 index 0000000..8dda22c --- /dev/null +++ b/rusqlite-macros/src/lib.rs @@ -0,0 +1,115 @@ +//! Private implementation details of `rusqlite`. + +use proc_macro::{Delimiter, Group, Literal, Span, TokenStream, TokenTree}; + +use fallible_iterator::FallibleIterator; +use sqlite3_parser::ast::{ParameterInfo, ToTokens}; +use sqlite3_parser::lexer::sql::Parser; + +// https://internals.rust-lang.org/t/custom-error-diagnostics-with-procedural-macros-on-almost-stable-rust/8113 + +#[doc(hidden)] +#[proc_macro] +pub fn __bind(input: TokenStream) -> TokenStream { + try_bind(input).unwrap_or_else(|msg| parse_ts(&format!("compile_error!({:?})", msg))) +} + +type Result = std::result::Result; + +fn try_bind(input: TokenStream) -> Result { + let (stmt, literal) = { + let mut iter = input.clone().into_iter(); + let stmt = iter.next().unwrap(); + let literal = iter.next().unwrap(); + assert!(iter.next().is_none()); + (stmt, literal) + }; + + let literal = match into_literal(&literal) { + Some(it) => it, + None => return Err("expected a plain string literal".to_string()), + }; + let sql = literal.to_string(); + if !sql.starts_with('"') { + return Err("expected a plain string literal".to_string()); + } + let sql = strip_matches(&sql, "\""); + + let mut parser = Parser::new(sql.as_bytes()); + let ast = match parser.next() { + Ok(None) => return Err("Invalid input".to_owned()), + Err(err) => { + return Err(err.to_string()); + } + Ok(Some(ast)) => ast, + }; + let mut info = ParameterInfo::default(); + if let Err(err) = ast.to_tokens(&mut info) { + return Err(err.to_string()); + } + if info.count == 0 { + return Ok(input); + } + if info.count as usize != info.names.len() { + return Err("Mixing named and numbered parameters is not supported.".to_string()); + } + + let call_site = literal.span(); + let mut res = TokenStream::new(); + for (i, name) in info.names.iter().enumerate() { + res.extend(Some(stmt.clone())); + res.extend(respan( + parse_ts(&format!( + ".raw_bind_parameter({}, &{})?;", + i + 1, + &name[1..] + )), + call_site, + )); + } + + Ok(res) +} + +fn into_literal(ts: &TokenTree) -> Option { + match ts { + TokenTree::Literal(l) => Some(l.clone()), + TokenTree::Group(g) => match g.delimiter() { + Delimiter::None => match g.stream().into_iter().collect::>().as_slice() { + [TokenTree::Literal(l)] => Some(l.clone()), + _ => None, + }, + Delimiter::Parenthesis | Delimiter::Brace | Delimiter::Bracket => None, + }, + _ => None, + } +} + +fn strip_matches<'a>(s: &'a str, pattern: &str) -> &'a str { + s.strip_prefix(pattern) + .unwrap_or(s) + .strip_suffix(pattern) + .unwrap_or(s) +} + +fn respan(ts: TokenStream, span: Span) -> TokenStream { + let mut res = TokenStream::new(); + for tt in ts { + let tt = match tt { + TokenTree::Ident(mut ident) => { + ident.set_span(ident.span().resolved_at(span).located_at(span)); + TokenTree::Ident(ident) + } + TokenTree::Group(group) => { + TokenTree::Group(Group::new(group.delimiter(), respan(group.stream(), span))) + } + _ => tt, + }; + res.extend(Some(tt)) + } + res +} + +fn parse_ts(s: &str) -> TokenStream { + s.parse().unwrap() +} diff --git a/rusqlite-macros/tests/test.rs b/rusqlite-macros/tests/test.rs new file mode 100644 index 0000000..785ca9b --- /dev/null +++ b/rusqlite-macros/tests/test.rs @@ -0,0 +1,36 @@ +use rusqlite_macros::__bind; + +type Result = std::result::Result<(), String>; + +struct Stmt; + +impl Stmt { + pub fn raw_bind_parameter(&mut self, one_based_col_index: usize, param: &str) -> Result { + let (..) = (one_based_col_index, param); + Ok(()) + } +} + +#[test] +fn test_literal() -> Result { + let first_name = "El"; + let last_name = "Barto"; + let mut stmt = Stmt; + __bind!(stmt "SELECT $first_name, $last_name"); + Ok(()) +} + +/* FIXME +#[test] +fn test_raw_string() { + let stmt = (); + __bind!(stmt r#"SELECT 1"#); +} + +#[test] +fn test_const() { + const SQL: &str = "SELECT 1"; + let stmt = (); + __bind!(stmt SQL); +} +*/ diff --git a/src/lib.rs b/src/lib.rs index 6424308..1a20074 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,6 +88,9 @@ pub use crate::transaction::TransactionState; pub use crate::transaction::{DropBehavior, Savepoint, Transaction, TransactionBehavior}; pub use crate::types::ToSql; pub use crate::version::*; +#[cfg(feature = "rusqlite-macros")] +#[doc(hidden)] +pub use rusqlite_macros::__bind; mod error; @@ -218,6 +221,51 @@ macro_rules! named_params { }; } +/// Captured identifiers in SQL +/// +/// * only SQLite `$x` / `@x` / `:x` syntax works (Rust `&x` syntax does not +/// work). +/// * `$x.y` expression does not work. +/// +/// # Example +/// +/// ```rust, no_run +/// # use rusqlite::{prepare_and_bind, Connection, Result, Statement}; +/// +/// fn misc(db: &Connection) -> Result { +/// let name = "Lisa"; +/// let age = 8; +/// let smart = true; +/// Ok(prepare_and_bind!(db, "SELECT $name, @age, :smart;")) +/// } +/// ``` +#[cfg(feature = "rusqlite-macros")] +#[cfg_attr(docsrs, doc(cfg(feature = "rusqlite-macros")))] +#[macro_export] +macro_rules! prepare_and_bind { + ($conn:expr, $sql:literal) => {{ + let mut stmt = $conn.prepare($sql)?; + $crate::__bind!(stmt $sql); + stmt + }}; +} + +/// Captured identifiers in SQL +/// +/// * only SQLite `$x` / `@x` / `:x` syntax works (Rust `&x` syntax does not +/// work). +/// * `$x.y` expression does not work. +#[cfg(feature = "rusqlite-macros")] +#[cfg_attr(docsrs, doc(cfg(feature = "rusqlite-macros")))] +#[macro_export] +macro_rules! prepare_cached_and_bind { + ($conn:expr, $sql:literal) => {{ + let mut stmt = $conn.prepare_cached($sql)?; + $crate::__bind!(stmt $sql); + stmt + }}; +} + /// A typedef of the result returned by many methods. pub type Result = result::Result; @@ -2117,9 +2165,25 @@ mod test { } #[test] - pub fn db_readonly() -> Result<()> { + fn db_readonly() -> Result<()> { let db = Connection::open_in_memory()?; assert!(!db.is_readonly(MAIN_DB)?); Ok(()) } + + #[test] + #[cfg(feature = "rusqlite-macros")] + fn prepare_and_bind() -> Result<()> { + let db = Connection::open_in_memory()?; + let name = "Lisa"; + let age = 8; + let mut stmt = prepare_and_bind!(db, "SELECT $name, $age;"); + let (v1, v2) = stmt + .raw_query() + .next() + .and_then(|o| o.ok_or(Error::QueryReturnedNoRows)) + .and_then(|r| Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?)))?; + assert_eq!((v1.as_str(), v2), (name, age)); + Ok(()) + } }