@bobfrankston/mailx-store 0.1.76 → 0.1.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/db.js +43 -1
- package/package.json +1 -1
package/db.js
CHANGED
|
@@ -3704,8 +3704,26 @@ export class MailxDB {
|
|
|
3704
3704
|
const LIKE_CC = ["m.cc_json"];
|
|
3705
3705
|
const LIKE_SUBJ = ["m.subject"];
|
|
3706
3706
|
const LIKE_ANY = ["m.subject", "m.from_name", "m.from_address"];
|
|
3707
|
+
// `NOT <item>` — exclusion, usable ANYWHERE including as the leading
|
|
3708
|
+
// (or only) token. FTS5's own NOT is strictly binary (`a NOT b`), so
|
|
3709
|
+
// a leading NOT was an FTS5 syntax error that the dangling-operator
|
|
3710
|
+
// trim "fixed" by dropping the NOT — silently searching FOR the term
|
|
3711
|
+
// the user asked to exclude (Bob 2026-07-30). Instead of leaning on
|
|
3712
|
+
// FTS5's operator, every `NOT x` pair is lifted OUT of the MATCH and
|
|
3713
|
+
// applied as a SQL-side exclusion (`m.id NOT IN (SELECT rowid …
|
|
3714
|
+
// MATCH x)`), which also negates qualifier tokens (`NOT from:…`,
|
|
3715
|
+
// `NOT is:read`) uniformly. Uppercase only, same as AND/OR — a
|
|
3716
|
+
// lowercase "not" stays an ordinary search term.
|
|
3717
|
+
const notFrags = [];
|
|
3718
|
+
let negateNext = false;
|
|
3707
3719
|
for (let pi = 0; pi < parts.length; pi++) {
|
|
3708
3720
|
const part = parts[pi];
|
|
3721
|
+
if (part === "NOT") {
|
|
3722
|
+
negateNext = true;
|
|
3723
|
+
continue;
|
|
3724
|
+
}
|
|
3725
|
+
const wPre = extraWhere.length;
|
|
3726
|
+
const fPre = frags.length;
|
|
3709
3727
|
// A 1-2 char term next to a user-typed OR must be DROPPED, not
|
|
3710
3728
|
// LIKE'd: LIKE clauses AND against the MATCH, so "ab OR sprinkler"
|
|
3711
3729
|
// would intersect down to zero. Over-matching (just "sprinkler")
|
|
@@ -3826,12 +3844,14 @@ export class MailxDB {
|
|
|
3826
3844
|
extraWhere.push("LOWER(f.name) LIKE ?");
|
|
3827
3845
|
extraParams.push(`%${v.toLowerCase()}%`);
|
|
3828
3846
|
}
|
|
3829
|
-
else if (/^(AND|OR
|
|
3847
|
+
else if (/^(AND|OR)$/.test(part)) {
|
|
3830
3848
|
// FTS5 boolean operators — pass through verbatim (must be uppercase).
|
|
3831
3849
|
// Without this branch, `hoddie AND git` got wildcarded into
|
|
3832
3850
|
// `hoddie* AND* git*`, which FTS5 reads as three required terms
|
|
3833
3851
|
// (one of them being any word starting with "AND") — so a real
|
|
3834
3852
|
// match like "Peter Hoddie" + "github" returned zero hits.
|
|
3853
|
+
// (NOT is handled above as an exclusion prefix, not passed
|
|
3854
|
+
// through — FTS5's binary-only NOT can't stand at the front.)
|
|
3835
3855
|
frags.push({ s: part, op: true });
|
|
3836
3856
|
}
|
|
3837
3857
|
else if (isTri) {
|
|
@@ -3893,6 +3913,20 @@ export class MailxDB {
|
|
|
3893
3913
|
}
|
|
3894
3914
|
}
|
|
3895
3915
|
}
|
|
3916
|
+
// Divert whatever this token contributed into the exclusion set.
|
|
3917
|
+
// FTS frags move to notFrags (applied as a NOT IN subquery below);
|
|
3918
|
+
// SQL-side qualifier clauses get wrapped in NOT(...) in place. An
|
|
3919
|
+
// AND/OR right after NOT is nonsense input — the op frag stands
|
|
3920
|
+
// and the stray NOT is dropped.
|
|
3921
|
+
if (negateNext) {
|
|
3922
|
+
if (!/^(AND|OR)$/.test(part)) {
|
|
3923
|
+
for (let w = wPre; w < extraWhere.length; w++)
|
|
3924
|
+
extraWhere[w] = `NOT (${extraWhere[w]})`;
|
|
3925
|
+
while (frags.length > fPre)
|
|
3926
|
+
notFrags.push(frags.pop().s);
|
|
3927
|
+
}
|
|
3928
|
+
negateNext = false;
|
|
3929
|
+
}
|
|
3896
3930
|
}
|
|
3897
3931
|
// A term diverted to the LIKE fallback can strand a user-typed
|
|
3898
3932
|
// operator at the edge of the MATCH string ("ab OR sprinkler" → the
|
|
@@ -3902,6 +3936,14 @@ export class MailxDB {
|
|
|
3902
3936
|
frags.shift();
|
|
3903
3937
|
while (frags.length && frags[frags.length - 1].op)
|
|
3904
3938
|
frags.pop();
|
|
3939
|
+
// NOT'd terms exclude via a subquery on the same FTS index: a message
|
|
3940
|
+
// matching ANY excluded term is out. Works whether or not there are
|
|
3941
|
+
// positive FTS terms — a pure `NOT foo` query takes the qualifier-only
|
|
3942
|
+
// path below and returns everything except matches.
|
|
3943
|
+
if (notFrags.length > 0) {
|
|
3944
|
+
extraWhere.push("m.id NOT IN (SELECT rowid FROM messages_fts WHERE messages_fts MATCH ?)");
|
|
3945
|
+
extraParams.push(notFrags.join(" OR "));
|
|
3946
|
+
}
|
|
3905
3947
|
// Join fragments. Two adjacent value fragments need an explicit `AND`
|
|
3906
3948
|
// (implicit-AND after a `(...)` / `{...}:` group is an FTS5 syntax
|
|
3907
3949
|
// error); a user operator fragment glues itself, so no insert around it.
|