Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 36 additions & 24 deletions crates/persistence/src/backends/sqlite/search/chain_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -740,30 +740,13 @@ impl ChainQueryBuilder {

/// Builds a date comparison condition.
fn build_date_condition(column: &str, value: &SearchValue, param_num: usize) -> (String, SqlParam) {
use crate::types::SearchPrefix;

let (op, val) = match value.prefix {
SearchPrefix::Eq => ("=", &value.value),
SearchPrefix::Ne => ("!=", &value.value),
SearchPrefix::Gt => (">", &value.value),
SearchPrefix::Lt => ("<", &value.value),
SearchPrefix::Ge => (">=", &value.value),
SearchPrefix::Le => ("<=", &value.value),
SearchPrefix::Sa => (">", &value.value),
SearchPrefix::Eb => ("<", &value.value),
SearchPrefix::Ap => {
// Approximately equal: within a day for dates
return (
format!("DATE({}) = DATE(?{})", column, param_num),
SqlParam::String(value.value.clone()),
);
}
};

(
format!("{} {} ?{}", column, op, param_num),
SqlParam::String(val.clone()),
)
let (sql, bound) = super::parameter_handlers::date::date_condition(
column,
value.prefix,
&value.value,
param_num,
);
(sql, SqlParam::String(bound))
}

/// Builds a number comparison condition.
Expand Down Expand Up @@ -976,3 +959,32 @@ mod tests {
assert!(!outer.is_terminal());
}
}

#[cfg(test)]
mod date_condition_tests {
use super::*;
use crate::types::SearchPrefix;

/// #456: chained date terminals use the precision-aware normalized
/// comparison, not the raw text `=` this used to emit.
#[test]
fn chained_dates_are_precision_aware() {
let value = SearchValue::new(SearchPrefix::Eq, "1995-10-02");
let (sql, param) = build_date_condition("t2.value_date", &value, 7);
assert_eq!(
sql,
"(datetime(t2.value_date) >= datetime(?7) AND datetime(t2.value_date) < datetime(?7, '+1 day'))"
);
match param {
SqlParam::String(s) => assert_eq!(s, "1995-10-02T00:00:00"),
_ => panic!("expected string param"),
}
}

#[test]
fn chained_full_precision_is_equality() {
let value = SearchValue::new(SearchPrefix::Eq, "2016-01-23T13:07:42-04:00");
let (sql, _) = build_date_condition("t2.value_date", &value, 3);
assert_eq!(sql, "datetime(t2.value_date) = datetime(?3)");
}
}
68 changes: 68 additions & 0 deletions crates/persistence/src/backends/sqlite/search/filter_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,30 @@ impl FilterSqlGenerator {
// Infer the likely column based on parameter name patterns
let column = self.infer_column(param);

// Dates need precision-aware, normalized comparison (#456); the
// generic text operators below mis-order mixed-precision values.
if column == "value_date" {
use crate::types::SearchPrefix;
let prefix = match op {
FilterOp::Eq => Some(SearchPrefix::Eq),
FilterOp::Ne => Some(SearchPrefix::Ne),
FilterOp::Gt => Some(SearchPrefix::Gt),
FilterOp::Sa => Some(SearchPrefix::Sa),
FilterOp::Lt => Some(SearchPrefix::Lt),
FilterOp::Eb => Some(SearchPrefix::Eb),
FilterOp::Ge => Some(SearchPrefix::Ge),
FilterOp::Le => Some(SearchPrefix::Le),
FilterOp::Ap => Some(SearchPrefix::Ap),
_ => None,
};
if let Some(prefix) = prefix {
let (sql, bound) = super::parameter_handlers::date::date_condition(
column, prefix, value, param_num,
);
return (column, sql, bound);
}
}

match op {
FilterOp::Eq => (
column,
Expand Down Expand Up @@ -773,3 +797,47 @@ mod tests {
assert_eq!(sql.params.len(), 2);
}
}

#[cfg(test)]
mod date_filter_tests {
use super::*;

/// #456: `_filter` date comparisons use the precision-aware normalized
/// path, not raw text operators.
#[test]
fn filter_dates_use_normalized_precision_ranges() {
let expr = FilterParser::parse("birthdate eq 1995-10-02").unwrap();
let frag = FilterSqlGenerator::new(1).generate(&expr);
assert!(
frag.sql.contains("datetime(value_date) >= datetime(?2)"),
"{}",
frag.sql
);
assert!(frag.sql.contains("'+1 day'"), "{}", frag.sql);
}

#[test]
fn filter_date_bounds_honor_the_named_day() {
let ge = FilterSqlGenerator::new(1)
.generate(&FilterParser::parse("birthdate ge 1995-10-02").unwrap());
assert!(
ge.sql.contains("datetime(value_date) >= datetime(?2)"),
"{}",
ge.sql
);
let le = FilterSqlGenerator::new(1)
.generate(&FilterParser::parse("birthdate le 1995-10-02").unwrap());
assert!(le.sql.contains("'+1 day'"), "{}", le.sql);
let sa = FilterSqlGenerator::new(1)
.generate(&FilterParser::parse("birthdate sa 1995-10-02").unwrap());
assert!(sa.sql.contains("'+1 day'"), "{}", sa.sql);
}

/// Non-date columns keep the plain text operators.
#[test]
fn filter_strings_keep_text_operators() {
let frag =
FilterSqlGenerator::new(1).generate(&FilterParser::parse("name eq Smith").unwrap());
assert!(frag.sql.contains("value_string = ?2"), "{}", frag.sql);
}
}
Loading
Loading