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"
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user