mirror of
				https://github.com/isar/rusqlite.git
				synced 2025-10-31 05:48:56 +08:00 
			
		
		
		
	Merge pull request #1346 from gwenn/captured_identifiers
Captured identifiers in SQL strings
This commit is contained in:
		| @@ -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" | ||||
|   | ||||
							
								
								
									
										16
									
								
								rusqlite-macros/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								rusqlite-macros/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -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" | ||||
							
								
								
									
										115
									
								
								rusqlite-macros/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								rusqlite-macros/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<T> = std::result::Result<T, String>; | ||||
|  | ||||
| fn try_bind(input: TokenStream) -> Result<TokenStream> { | ||||
|     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<Literal> { | ||||
|     match ts { | ||||
|         TokenTree::Literal(l) => Some(l.clone()), | ||||
|         TokenTree::Group(g) => match g.delimiter() { | ||||
|             Delimiter::None => match g.stream().into_iter().collect::<Vec<_>>().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() | ||||
| } | ||||
							
								
								
									
										36
									
								
								rusqlite-macros/tests/test.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								rusqlite-macros/tests/test.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
| } | ||||
| */ | ||||
							
								
								
									
										66
									
								
								src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										66
									
								
								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<Statement> { | ||||
| ///     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<T, E = Error> = result::Result<T, E>; | ||||
|  | ||||
| @@ -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(()) | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user