Support accumulating TypedValue instances into a SQL query. (#339) r=nalexander
These expand into a collection of named variables that should be passed via bind parameters when the query is executed. Bind parameters are now only named.
This commit is contained in:
parent
b0120aa446
commit
76f51015d9
2 changed files with 117 additions and 6 deletions
|
@ -2,3 +2,9 @@
|
|||
name = "mentat_sql"
|
||||
version = "0.0.1"
|
||||
workspace = ".."
|
||||
|
||||
[dependencies]
|
||||
ordered-float = "0.4.0"
|
||||
|
||||
[dependencies.mentat_core]
|
||||
path = "../core"
|
||||
|
|
117
sql/src/lib.rs
117
sql/src/lib.rs
|
@ -8,18 +8,42 @@
|
|||
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations under the License.
|
||||
|
||||
extern crate ordered_float;
|
||||
extern crate mentat_core;
|
||||
|
||||
use std::error::Error;
|
||||
|
||||
use ordered_float::OrderedFloat;
|
||||
|
||||
use mentat_core::TypedValue;
|
||||
|
||||
pub type BuildQueryError = Box<Error + Send + Sync>;
|
||||
pub type BuildQueryResult = Result<(), BuildQueryError>;
|
||||
|
||||
pub enum BindParamError {
|
||||
InvalidParameterName(String),
|
||||
BindParamCouldBeGenerated(String),
|
||||
}
|
||||
|
||||
/// We want to accumulate values that will later be substituted into a SQL statement execution.
|
||||
/// This struct encapsulates the generated string and the _initial_ argument list.
|
||||
/// Additional user-supplied argument bindings, with their placeholders accumulated via
|
||||
/// `push_bind_param`, will be appended to this argument list.
|
||||
pub struct SQLQuery {
|
||||
pub sql: String,
|
||||
|
||||
/// These will eventually perhaps be rusqlite `ToSql` instances.
|
||||
pub args: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
/// Gratefully based on Diesel's QueryBuilder trait:
|
||||
/// https://github.com/diesel-rs/diesel/blob/4885f61b8205f7f3c2cfa03837ed6714831abe6b/diesel/src/query_builder/mod.rs#L56
|
||||
pub trait QueryBuilder {
|
||||
fn push_sql(&mut self, sql: &str);
|
||||
fn push_identifier(&mut self, identifier: &str) -> BuildQueryResult;
|
||||
fn push_bind_param(&mut self);
|
||||
fn finish(self) -> String;
|
||||
fn push_typed_value(&mut self, value: &TypedValue) -> BuildQueryResult;
|
||||
fn push_bind_param(&mut self, name: &str) -> Result<(), BindParamError>;
|
||||
fn finish(self) -> SQLQuery;
|
||||
}
|
||||
|
||||
pub trait QueryFragment {
|
||||
|
@ -47,14 +71,36 @@ impl QueryFragment for () {
|
|||
/// A QueryBuilder that implements SQLite's specific escaping rules.
|
||||
pub struct SQLiteQueryBuilder {
|
||||
pub sql: String,
|
||||
|
||||
arg_prefix: String,
|
||||
arg_counter: i64,
|
||||
args: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl SQLiteQueryBuilder {
|
||||
pub fn new() -> Self {
|
||||
SQLiteQueryBuilder::with_prefix("$v".to_string())
|
||||
}
|
||||
|
||||
pub fn with_prefix(prefix: String) -> Self {
|
||||
SQLiteQueryBuilder {
|
||||
sql: String::new(),
|
||||
arg_prefix: prefix,
|
||||
arg_counter: 0,
|
||||
args: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn push_static_arg(&mut self, val: String) {
|
||||
let arg = format!("{}{}", self.arg_prefix, self.arg_counter);
|
||||
self.arg_counter = self.arg_counter + 1;
|
||||
self.push_named_arg(arg.as_str());
|
||||
self.args.push((arg, val));
|
||||
}
|
||||
|
||||
fn push_named_arg(&mut self, arg: &str) {
|
||||
self.push_sql(arg);
|
||||
}
|
||||
}
|
||||
|
||||
impl QueryBuilder for SQLiteQueryBuilder {
|
||||
|
@ -69,11 +115,70 @@ impl QueryBuilder for SQLiteQueryBuilder {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn push_bind_param(&mut self) {
|
||||
self.push_sql("?");
|
||||
fn push_typed_value(&mut self, value: &TypedValue) -> BuildQueryResult {
|
||||
use TypedValue::*;
|
||||
match value {
|
||||
&Ref(entid) => self.push_sql(entid.to_string().as_str()),
|
||||
&Boolean(v) => self.push_sql(if v { "1" } else { "0" }),
|
||||
&Long(v) => self.push_sql(v.to_string().as_str()),
|
||||
&Double(OrderedFloat(v)) => self.push_sql(v.to_string().as_str()),
|
||||
&String(ref s) => self.push_static_arg(s.clone()),
|
||||
&Keyword(ref s) => self.push_static_arg(s.to_string()),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish(self) -> String {
|
||||
self.sql
|
||||
/// Our bind parameters will be interleaved with pushed `TypedValue` instances. That means we
|
||||
/// need to use named parameters, not positional parameters.
|
||||
/// The `name` argument to this method is expected to be alphanumeric. If not, this method
|
||||
/// returns an `InvalidParameterName` error result.
|
||||
/// Callers should make sure that the name doesn't overlap with generated parameter names. If
|
||||
/// it does, `BindParamCouldBeGenerated` is the error.
|
||||
fn push_bind_param(&mut self, name: &str) -> Result<(), BindParamError> {
|
||||
// Do some validation first.
|
||||
// This is not free, but it's probably worth it for now.
|
||||
if !name.chars().all(char::is_alphanumeric) {
|
||||
return Err(BindParamError::InvalidParameterName(name.to_string()));
|
||||
}
|
||||
|
||||
if name.starts_with(self.arg_prefix.as_str()) &&
|
||||
name.chars().skip(self.arg_prefix.len()).all(char::is_numeric) {
|
||||
return Err(BindParamError::BindParamCouldBeGenerated(name.to_string()));
|
||||
}
|
||||
|
||||
self.push_sql("$");
|
||||
self.push_sql(name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish(self) -> SQLQuery {
|
||||
SQLQuery {
|
||||
sql: self.sql,
|
||||
args: self.args,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sql() {
|
||||
let mut s = SQLiteQueryBuilder::new();
|
||||
s.push_sql("SELECT ");
|
||||
s.push_identifier("foo").unwrap();
|
||||
s.push_sql(" WHERE ");
|
||||
s.push_identifier("bar").unwrap();
|
||||
s.push_sql(" = ");
|
||||
s.push_static_arg("frobnicate".to_string());
|
||||
s.push_sql(" OR ");
|
||||
s.push_static_arg("swoogle".to_string());
|
||||
let q = s.finish();
|
||||
|
||||
assert_eq!(q.sql.as_str(), "SELECT `foo` WHERE `bar` = $v0 OR $v1");
|
||||
assert_eq!(q.args,
|
||||
vec![("$v0".to_string(), "frobnicate".to_string()),
|
||||
("$v1".to_string(), "swoogle".to_string())]);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue