use crate::compile::{BindValue, CompiledQuery};
pub fn renumber_sql_params(sql: &str, removed: &[usize]) -> String {
let mut result = String::with_capacity(sql.len());
let bytes = sql.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'?' {
let num_start = i + 1;
let mut j = num_start;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
if j > num_start {
let num_str = &sql[num_start..j];
if let Ok(n) = num_str.parse::<usize>() {
let offset = removed.iter().filter(|&&r| r < n).count();
result.push('?');
result.push_str(&(n - offset).to_string());
i = j;
continue;
}
}
}
result.push(bytes[i] as char);
i += 1;
}
result
}
pub fn strip_prop_fts_union_arm(sql: &str) -> String {
let union_marker =
" UNION\n SELECT fp.node_logical_id";
if let Some(start) = sql.find(union_marker) {
let end_marker = "\n ) u";
if let Some(rel_end) = sql[start..].find(end_marker) {
let end = start + rel_end;
return format!("{}{}", &sql[..start], &sql[end..]);
}
}
sql.to_owned()
}
impl CompiledQuery {
#[must_use]
pub fn adapt_fts_for_kind(
&self,
prop_table_exists: bool,
prop_table_name: &str,
) -> (String, Vec<BindValue>) {
let (new_sql, removed_bind_positions) = if prop_table_exists {
let s = self
.sql
.replace("fts_node_properties", prop_table_name)
.replace("\n AND fp.kind = ?4", "");
(renumber_sql_params(&s, &[4]), vec![3usize])
} else {
let s = strip_prop_fts_union_arm(&self.sql);
(renumber_sql_params(&s, &[3, 4]), vec![2usize, 3])
};
let new_binds: Vec<BindValue> = self
.binds
.iter()
.enumerate()
.filter(|(i, _)| !removed_bind_positions.contains(i))
.map(|(_, b)| b.clone())
.collect();
(new_sql, new_binds)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compile::ShapeHash;
use crate::plan::{DrivingTable, ExecutionHints};
fn mk_compiled(sql: &str, binds: Vec<BindValue>) -> CompiledQuery {
CompiledQuery {
sql: sql.to_owned(),
binds,
shape_hash: ShapeHash(0),
driving_table: DrivingTable::FtsNodes,
hints: ExecutionHints {
recursion_limit: 0,
hard_limit: 0,
},
}
}
#[test]
fn renumber_shifts_params_past_single_removal() {
let out = renumber_sql_params("SELECT ?1, ?2, ?4, ?5, ?6", &[4]);
assert_eq!(out, "SELECT ?1, ?2, ?4, ?4, ?5");
}
#[test]
fn renumber_shifts_params_past_two_removals() {
let out = renumber_sql_params("SELECT ?1, ?2, ?5, ?6, ?7", &[3, 4]);
assert_eq!(out, "SELECT ?1, ?2, ?3, ?4, ?5");
}
#[test]
fn strip_removes_union_arm_between_markers() {
let sql = "\
SELECT c.logical_id AS logical_id
FROM fts_node_chunks c
WHERE fts_node_chunks MATCH ?1
AND c.kind = ?2
UNION
SELECT fp.node_logical_id AS logical_id
FROM fts_node_properties fp
WHERE fts_node_properties MATCH ?3
AND fp.kind = ?4
) u";
let out = strip_prop_fts_union_arm(sql);
assert!(!out.contains("fts_node_properties"));
assert!(out.contains(") u"));
assert!(out.contains("fts_node_chunks MATCH ?1"));
}
#[test]
fn adapt_fts_for_kind_table_exists_rewrites_and_drops_kind_bind() {
let sql = "\
SELECT ... FROM fts_node_properties fp WHERE fts_node_properties MATCH ?3
AND fp.kind = ?4
AND extra = ?5";
let binds = vec![
BindValue::Text("chunk-text".to_owned()),
BindValue::Text("Goal".to_owned()),
BindValue::Text("prop-text".to_owned()),
BindValue::Text("Goal".to_owned()),
BindValue::Text("extra".to_owned()),
];
let compiled = mk_compiled(sql, binds);
let (new_sql, new_binds) = compiled.adapt_fts_for_kind(true, "fts_props_goal");
assert!(new_sql.contains("fts_props_goal"));
assert!(!new_sql.contains("fts_node_properties"));
assert!(!new_sql.contains("AND fp.kind = ?4"));
assert!(new_sql.contains("extra = ?4"));
assert_eq!(new_binds.len(), 4);
assert_eq!(new_binds[0], BindValue::Text("chunk-text".to_owned()));
assert_eq!(new_binds[1], BindValue::Text("Goal".to_owned()));
assert_eq!(new_binds[2], BindValue::Text("prop-text".to_owned()));
assert_eq!(new_binds[3], BindValue::Text("extra".to_owned()));
}
#[test]
fn adapt_fts_for_kind_table_missing_strips_union_and_drops_two_binds() {
let sql = "\
SELECT c.logical_id AS logical_id
FROM fts_node_chunks c
WHERE fts_node_chunks MATCH ?1
AND c.kind = ?2
UNION
SELECT fp.node_logical_id AS logical_id
FROM fts_node_properties fp
WHERE fts_node_properties MATCH ?3
AND fp.kind = ?4
) u
WHERE extra = ?5";
let binds = vec![
BindValue::Text("chunk-text".to_owned()),
BindValue::Text("Goal".to_owned()),
BindValue::Text("prop-text".to_owned()),
BindValue::Text("Goal".to_owned()),
BindValue::Text("extra".to_owned()),
];
let compiled = mk_compiled(sql, binds);
let (new_sql, new_binds) = compiled.adapt_fts_for_kind(false, "fts_props_goal");
assert!(!new_sql.contains("fts_node_properties"));
assert!(!new_sql.contains("UNION"));
assert!(new_sql.contains("extra = ?3"));
assert_eq!(new_binds.len(), 3);
assert_eq!(new_binds[0], BindValue::Text("chunk-text".to_owned()));
assert_eq!(new_binds[1], BindValue::Text("Goal".to_owned()));
assert_eq!(new_binds[2], BindValue::Text("extra".to_owned()));
}
}