mirror of
https://github.com/isar/rusqlite.git
synced 2024-11-25 02:21:37 +08:00
Merge pull request #1346 from gwenn/captured_identifiers
Captured identifiers in SQL strings
This commit is contained in:
commit
476a02a595
@ -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(())
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user