mirror of
				https://github.com/isar/rusqlite.git
				synced 2025-10-31 13:58:55 +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" | fallible-streaming-iterator = "0.1" | ||||||
| uuid = { version = "1.0", optional = true } | uuid = { version = "1.0", optional = true } | ||||||
| smallvec = "1.6.1" | smallvec = "1.6.1" | ||||||
|  | rusqlite-macros = { path = "rusqlite-macros", version = "0.1.0", optional = true } | ||||||
|  |  | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
| doc-comment = "0.3" | doc-comment = "0.3" | ||||||
| @@ -159,7 +160,7 @@ name = "exec" | |||||||
| harness = false | harness = false | ||||||
|  |  | ||||||
| [package.metadata.docs.rs] | [package.metadata.docs.rs] | ||||||
| features = ["modern-full"] | features = ["modern-full", "rusqlite-macros"] | ||||||
| all-features = false | all-features = false | ||||||
| no-default-features = true | no-default-features = true | ||||||
| default-target = "x86_64-unknown-linux-gnu" | 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::transaction::{DropBehavior, Savepoint, Transaction, TransactionBehavior}; | ||||||
| pub use crate::types::ToSql; | pub use crate::types::ToSql; | ||||||
| pub use crate::version::*; | pub use crate::version::*; | ||||||
|  | #[cfg(feature = "rusqlite-macros")] | ||||||
|  | #[doc(hidden)] | ||||||
|  | pub use rusqlite_macros::__bind; | ||||||
|  |  | ||||||
| mod error; | 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. | /// A typedef of the result returned by many methods. | ||||||
| pub type Result<T, E = Error> = result::Result<T, E>; | pub type Result<T, E = Error> = result::Result<T, E>; | ||||||
|  |  | ||||||
| @@ -2117,9 +2165,25 @@ mod test { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     #[test] |     #[test] | ||||||
|     pub fn db_readonly() -> Result<()> { |     fn db_readonly() -> Result<()> { | ||||||
|         let db = Connection::open_in_memory()?; |         let db = Connection::open_in_memory()?; | ||||||
|         assert!(!db.is_readonly(MAIN_DB)?); |         assert!(!db.is_readonly(MAIN_DB)?); | ||||||
|         Ok(()) |         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