Skip to content

Commit f76355b

Browse files
committed
v1.0.0: Functions in WHERE, performance optimization
- LOWER(), UPPER(), TRIM(), SPLIT(), REPLACE() now work in WHERE conditions - 4x faster file traversal (use DirEntry::file_type instead of stat) - Skip file open when query only needs metadata (no binary check overhead) - Race condition fix: skip vanished files gracefully
1 parent f717c99 commit f76355b

4 files changed

Lines changed: 97 additions & 3 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "fquery"
3-
version = "0.1.0"
3+
version = "1.0.0"
44
edition = "2024"
55

66
[profile.release]

src/engine.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,27 @@ fn matches_condition(
621621
let len = val_str.chars().count();
622622
Ok(cmp_op_eval(op, &len, val))
623623
}
624+
Condition::ExprEquals(expr, val, negated) => {
625+
let v = resolve_column_in_where(expr, line, lineno, file_info) == *val;
626+
Ok(if *negated { !v } else { v })
627+
}
628+
Condition::ExprContains(expr, val, negated) => {
629+
let v = resolve_column_in_where(expr, line, lineno, file_info).contains(val.as_str());
630+
Ok(if *negated { !v } else { v })
631+
}
632+
Condition::ExprMatches(expr, pat, negated) => {
633+
let re = compiled_regexes.get(pat)
634+
.ok_or_else(|| FQueryError::Parse(format!("regex not compiled: {pat}")))?;
635+
let v = re.is_match(&resolve_column_in_where(expr, line, lineno, file_info));
636+
Ok(if *negated { !v } else { v })
637+
}
638+
Condition::ExprLike(expr, pat, negated) => {
639+
let re_pat = like_to_regex(pat);
640+
let re = compiled_regexes.get(&re_pat)
641+
.ok_or_else(|| FQueryError::Parse(format!("like regex not compiled: {pat}")))?;
642+
let v = re.is_match(&resolve_column_in_where(expr, line, lineno, file_info));
643+
Ok(if *negated { !v } else { v })
644+
}
624645
Condition::FileSizeBetween(low, high) => {
625646
Ok(file_info.size >= *low && file_info.size <= *high)
626647
}
@@ -692,12 +713,17 @@ fn collect_patterns(clause: &WhereClause, patterns: &mut Vec<String>) {
692713
patterns.push(p.clone());
693714
}
694715
}
695-
Condition::LineLike(p, _) | Condition::FileFieldLike(_, p, _) => {
716+
Condition::LineLike(p, _) | Condition::FileFieldLike(_, p, _) | Condition::ExprLike(_, p, _) => {
696717
let re_pat = like_to_regex(p);
697718
if !patterns.contains(&re_pat) {
698719
patterns.push(re_pat);
699720
}
700721
}
722+
Condition::ExprMatches(_, p, _) => {
723+
if !patterns.contains(p) {
724+
patterns.push(p.clone());
725+
}
726+
}
701727
_ => {}
702728
},
703729
WhereClause::And(parts) | WhereClause::Or(parts) => {
@@ -734,6 +760,10 @@ fn is_file_condition(c: &Condition) -> bool {
734760
| Condition::ModifiedCmp(..)
735761
| Condition::CreatedCmp(..) => true,
736762
Condition::LenCmp(col, _, _) => !is_line_level_column(&col.col),
763+
Condition::ExprEquals(col, _, _)
764+
| Condition::ExprContains(col, _, _)
765+
| Condition::ExprMatches(col, _, _)
766+
| Condition::ExprLike(col, _, _) => !is_line_level_column(&col.col),
737767
_ => false,
738768
}
739769
}

src/parser.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ pub enum Condition {
129129
FileFieldMatches(FileField, String, bool),
130130
/// FILENAME|FILEPATH|FILEDIR|FILEEXT LIKE "pattern"
131131
FileFieldLike(FileField, String, bool),
132+
// --- Generic column expression conditions (for functions in WHERE) ---
133+
/// LOWER(col) = "value", TRIM(col) CONTAINS "x", etc.
134+
ExprEquals(ColumnExpr, String, bool),
135+
ExprContains(ColumnExpr, String, bool),
136+
ExprMatches(ColumnExpr, String, bool),
137+
ExprLike(ColumnExpr, String, bool),
132138
// --- LEN() function ---
133139
/// LEN(col) <op> N
134140
LenCmp(ColumnExpr, CmpOp, usize),
@@ -893,7 +899,65 @@ impl Parser {
893899
Ok(WhereClause::Single(cond))
894900
}
895901

902+
fn parse_expr_condition(&mut self, expr: ColumnExpr) -> Result<Condition> {
903+
let op = self.advance()?.clone();
904+
match &op {
905+
Token::Eq => {
906+
let val = self.expect_string()?;
907+
Ok(Condition::ExprEquals(expr, val, false))
908+
}
909+
Token::Neq => {
910+
let val = self.expect_string()?;
911+
Ok(Condition::ExprEquals(expr, val, true))
912+
}
913+
Token::Keyword(k) if k.eq_ignore_ascii_case("CONTAINS") => {
914+
let val = self.expect_string()?;
915+
Ok(Condition::ExprContains(expr, val, false))
916+
}
917+
Token::Keyword(k) if k.eq_ignore_ascii_case("MATCHES") => {
918+
let val = self.expect_string()?;
919+
Ok(Condition::ExprMatches(expr, val, false))
920+
}
921+
Token::Keyword(k) if k.eq_ignore_ascii_case("LIKE") => {
922+
let val = self.expect_string()?;
923+
Ok(Condition::ExprLike(expr, val, false))
924+
}
925+
Token::Keyword(k) if k.eq_ignore_ascii_case("NOT") => {
926+
let next = self.advance()?.clone();
927+
match &next {
928+
Token::Keyword(kk) if kk.eq_ignore_ascii_case("CONTAINS") => {
929+
let val = self.expect_string()?;
930+
Ok(Condition::ExprContains(expr, val, true))
931+
}
932+
Token::Keyword(kk) if kk.eq_ignore_ascii_case("MATCHES") => {
933+
let val = self.expect_string()?;
934+
Ok(Condition::ExprMatches(expr, val, true))
935+
}
936+
Token::Keyword(kk) if kk.eq_ignore_ascii_case("LIKE") => {
937+
let val = self.expect_string()?;
938+
Ok(Condition::ExprLike(expr, val, true))
939+
}
940+
other => Err(FQueryError::Parse(format!(
941+
"expected CONTAINS, MATCHES, or LIKE after NOT, got {other:?}"
942+
))),
943+
}
944+
}
945+
other => Err(FQueryError::Parse(format!(
946+
"expected operator after expression, got {other:?}"
947+
))),
948+
}
949+
}
950+
896951
fn parse_condition(&mut self) -> Result<Condition> {
952+
// Check for function-wrapped columns: LOWER(col), UPPER(col), TRIM(col), etc.
953+
if let Some(Token::Keyword(k)) = self.peek() {
954+
let upper = k.to_ascii_uppercase();
955+
if matches!(upper.as_str(), "LOWER" | "UPPER" | "TRIM" | "TRIMSTART" | "TRIMEND" | "SPLIT" | "REPLACE") {
956+
let expr = self.parse_single_column()?;
957+
return self.parse_expr_condition(expr);
958+
}
959+
}
960+
897961
let field = self.advance()?.clone();
898962
match &field {
899963
Token::Keyword(k) if k.eq_ignore_ascii_case("LINE") => {

0 commit comments

Comments
 (0)