2019-10-07 13:42:44 +00:00
/*
* meli - sqlite3 . rs
*
* Copyright 2019 Manos Pitsidianakis
*
* This file is part of meli .
*
* meli is free software : you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* meli is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with meli . If not , see < http ://www.gnu.org/licenses/>.
* /
2020-02-04 13:52:12 +00:00
/*! Use an sqlite3 database for fast searching.
* /
2020-07-16 20:58:46 +00:00
use crate ::melib ::ResultIntoMeliError ;
2020-05-29 17:19:34 +00:00
use melib ::search ::{
2020-07-25 15:06:42 +00:00
escape_double_quote ,
2020-05-29 17:19:34 +00:00
Query ::{ self , * } ,
} ;
2019-10-12 15:03:58 +00:00
use melib ::{
2020-07-16 20:58:46 +00:00
backends ::{ MailBackend , ResultFuture } ,
2019-11-05 14:42:55 +00:00
email ::{ Envelope , EnvelopeHash } ,
2019-11-03 11:18:43 +00:00
log ,
2020-07-16 20:58:46 +00:00
sqlite3 ::{
self as melib_sqlite3 ,
rusqlite ::{ self , params } ,
2020-08-25 11:02:30 +00:00
DatabaseDescription ,
2020-07-16 20:58:46 +00:00
} ,
2019-10-12 15:03:58 +00:00
thread ::{ SortField , SortOrder } ,
2020-01-08 15:04:44 +00:00
MeliError , Result , ERROR ,
2019-10-12 15:03:58 +00:00
} ;
2020-05-30 12:35:51 +00:00
2020-05-29 17:19:34 +00:00
use smallvec ::SmallVec ;
2019-10-07 13:42:44 +00:00
use std ::convert ::TryInto ;
2020-05-29 17:19:34 +00:00
use std ::path ::PathBuf ;
2019-11-02 10:18:41 +00:00
use std ::sync ::{ Arc , RwLock } ;
2019-10-07 13:42:44 +00:00
2020-08-25 11:02:30 +00:00
const DB : DatabaseDescription = DatabaseDescription {
name : " index.db " ,
init_script :Some ( " CREATE TABLE IF NOT EXISTS envelopes (
2019-10-07 13:42:44 +00:00
id INTEGER PRIMARY KEY ,
2019-11-11 16:01:01 +00:00
account_id INTEGER REFERENCES accounts ON UPDATE CASCADE ,
2019-11-12 11:09:43 +00:00
hash BLOB NOT NULL UNIQUE ,
2019-10-07 13:42:44 +00:00
date TEXT NOT NULL ,
2019-10-12 15:03:58 +00:00
_from TEXT NOT NULL ,
_to TEXT NOT NULL ,
cc TEXT NOT NULL ,
bcc TEXT NOT NULL ,
subject TEXT NOT NULL ,
message_id TEXT NOT NULL ,
in_reply_to TEXT NOT NULL ,
_references TEXT NOT NULL ,
flags INTEGER NOT NULL ,
has_attachments BOOLEAN NOT NULL ,
body_text TEXT NOT NULL ,
timestamp BLOB NOT NULL
2019-11-11 16:01:01 +00:00
) ;
2019-10-12 15:03:58 +00:00
CREATE TABLE IF NOT EXISTS folders (
id INTEGER PRIMARY KEY ,
2019-11-11 16:01:01 +00:00
account_id INTEGER NOT NULL REFERENCES accounts ON UPDATE CASCADE ,
2019-10-12 15:03:58 +00:00
hash BLOB NOT NULL ,
date TEXT NOT NULL ,
2019-11-11 16:01:01 +00:00
name TEXT NOT NULL
2019-10-07 13:42:44 +00:00
) ;
2019-11-11 16:01:01 +00:00
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY ,
2019-11-12 20:20:20 +00:00
name TEXT NOT NULL UNIQUE
2019-11-11 16:01:01 +00:00
) ;
CREATE TABLE IF NOT EXISTS folder_and_envelope (
folder_id INTEGER NOT NULL ,
envelope_id INTEGER NOT NULL ,
PRIMARY KEY ( folder_id , envelope_id ) ,
FOREIGN KEY ( folder_id ) REFERENCES folders ( id ) ON UPDATE CASCADE ,
FOREIGN KEY ( envelope_id ) REFERENCES envelopes ( id ) ON UPDATE CASCADE
) ;
CREATE INDEX IF NOT EXISTS folder_env_idx ON folder_and_envelope ( folder_id ) ;
CREATE INDEX IF NOT EXISTS env_folder_idx ON folder_and_envelope ( envelope_id ) ;
CREATE UNIQUE INDEX IF NOT EXISTS acc_idx ON accounts ( name ) ;
2019-10-07 13:42:44 +00:00
CREATE INDEX IF NOT EXISTS envelope_timestamp_index ON envelopes ( timestamp ) ;
CREATE INDEX IF NOT EXISTS envelope__from_index ON envelopes ( _from ) ;
CREATE INDEX IF NOT EXISTS envelope__to_index ON envelopes ( _to ) ;
CREATE INDEX IF NOT EXISTS envelope_cc_index ON envelopes ( cc ) ;
CREATE INDEX IF NOT EXISTS envelope_bcc_index ON envelopes ( bcc ) ;
CREATE INDEX IF NOT EXISTS envelope_message_id_index ON envelopes ( message_id ) ;
CREATE VIRTUAL TABLE IF NOT EXISTS fts USING fts5 ( subject , body_text , content = envelopes , content_rowid = id ) ;
- - Triggers to keep the FTS index up to date .
CREATE TRIGGER IF NOT EXISTS envelopes_ai AFTER INSERT ON envelopes BEGIN
INSERT INTO fts ( rowid , subject , body_text ) VALUES ( new . id , new . subject , new . body_text ) ;
END ;
CREATE TRIGGER IF NOT EXISTS envelopes_ad AFTER DELETE ON envelopes BEGIN
INSERT INTO fts ( fts , rowid , subject , body_text ) VALUES ( ' delete ' , old . id , old . subject , old . body_text ) ;
END ;
CREATE TRIGGER IF NOT EXISTS envelopes_au AFTER UPDATE ON envelopes BEGIN
INSERT INTO fts ( fts , rowid , subject , body_text ) VALUES ( ' delete ' , old . id , old . subject , old . body_text ) ;
INSERT INTO fts ( rowid , subject , body_text ) VALUES ( new . id , new . subject , new . body_text ) ;
2020-08-25 11:02:30 +00:00
END ; " ),
version : 1 ,
} ;
2019-10-07 13:42:44 +00:00
2020-05-30 12:35:51 +00:00
pub fn db_path ( ) -> Result < PathBuf > {
2020-08-25 11:02:30 +00:00
melib_sqlite3 ::db_path ( DB . name )
2019-10-07 13:42:44 +00:00
}
2020-05-30 12:35:51 +00:00
//#[inline(always)]
//fn fts5_bareword(w: &str) -> Cow<str> {
// if w == "AND" || w == "OR" || w == "NOT" {
// Cow::from(w)
// } else {
// if !w.is_ascii() {
// Cow::from(format!("\"{}\"", escape_double_quote(w)))
// } else {
// for &b in w.as_bytes() {
// if !(b > 0x2f && b < 0x3a)
// || !(b > 0x40 && b < 0x5b)
// || !(b > 0x60 && b < 0x7b)
// || b != 0x60
// || b != 26
// {
// return Cow::from(format!("\"{}\"", escape_double_quote(w)));
// }
// }
// Cow::from(w)
// }
// }
//}
//
//
2020-08-25 21:00:38 +00:00
pub async fn insert (
envelope : Envelope ,
backend : Arc < RwLock < Box < dyn MailBackend > > > ,
acc_name : String ,
2020-05-29 17:19:34 +00:00
) -> Result < ( ) > {
2020-08-25 11:02:30 +00:00
let db_path = db_path ( ) ? ;
2020-05-30 12:35:51 +00:00
if ! db_path . exists ( ) {
return Err ( MeliError ::new (
" Database hasn't been initialised. Run `reindex` command " ,
) ) ;
}
let conn = melib_sqlite3 ::open_db ( db_path ) ? ;
2020-08-25 21:00:38 +00:00
let op = backend
. read ( )
. unwrap ( )
. operation ( envelope . hash ( ) ) ?
. as_bytes ( ) ? ;
let body = match op . await . map ( | bytes | envelope . body_bytes ( & bytes ) ) {
2019-11-05 14:42:55 +00:00
Ok ( body ) = > body . text ( ) ,
Err ( err ) = > {
debug! (
" {} " ,
format! (
" Failed to open envelope {}: {} " ,
envelope . message_id_display ( ) ,
err . to_string ( )
)
) ;
log (
format! (
" Failed to open envelope {}: {} " ,
envelope . message_id_display ( ) ,
err . to_string ( )
) ,
ERROR ,
) ;
return Err ( err ) ;
}
} ;
2019-11-12 20:20:20 +00:00
2020-05-29 17:19:34 +00:00
if let Err ( err ) = conn . execute (
" INSERT OR IGNORE INTO accounts (name) VALUES (?1) " ,
params! [ acc_name , ] ,
) {
2019-11-12 20:20:20 +00:00
debug! (
" Failed to insert envelope {}: {} " ,
envelope . message_id_display ( ) ,
err . to_string ( )
) ;
log (
format! (
" Failed to insert envelope {}: {} " ,
envelope . message_id_display ( ) ,
err . to_string ( )
) ,
ERROR ,
) ;
return Err ( MeliError ::new ( err . to_string ( ) ) ) ;
}
let account_id : i32 = {
2020-05-29 17:19:34 +00:00
let mut stmt = conn
. prepare ( " SELECT id FROM accounts WHERE name = ? " )
. unwrap ( ) ;
let x = stmt
. query_map ( params! [ acc_name ] , | row | row . get ( 0 ) )
. unwrap ( )
. next ( )
. unwrap ( )
. unwrap ( ) ;
2019-11-12 20:20:20 +00:00
x
} ;
2019-11-05 14:42:55 +00:00
if let Err ( err ) = conn . execute (
2019-11-11 16:01:01 +00:00
" INSERT OR REPLACE INTO envelopes (account_id, hash, date, _from, _to, cc, bcc, subject, message_id, in_reply_to, _references, flags, has_attachments, body_text, timestamp)
VALUES ( ? 1 , ? 2 , ? 3 , ? 4 , ? 5 , ? 6 , ? 7 , ? 8 , ? 9 , ? 10 , ? 11 , ? 12 , ? 13 , ? 14 , ? 15 ) " ,
2019-11-12 20:20:20 +00:00
params! [ account_id , envelope . hash ( ) . to_be_bytes ( ) . to_vec ( ) , envelope . date_as_str ( ) , envelope . field_from_to_string ( ) , envelope . field_to_to_string ( ) , envelope . field_cc_to_string ( ) , envelope . field_bcc_to_string ( ) , envelope . subject ( ) . into_owned ( ) . trim_end_matches ( '\u{0}' ) , envelope . message_id_display ( ) . to_string ( ) , envelope . in_reply_to_display ( ) . map ( | f | f . to_string ( ) ) . unwrap_or ( String ::new ( ) ) , envelope . field_references_to_string ( ) , i64 ::from ( envelope . flags ( ) . bits ( ) ) , if envelope . has_attachments ( ) { 1 } else { 0 } , body , envelope . date ( ) . to_be_bytes ( ) . to_vec ( ) ] ,
2019-11-05 14:42:55 +00:00
)
. map_err ( | e | MeliError ::new ( e . to_string ( ) ) ) {
debug! (
" Failed to insert envelope {}: {} " ,
envelope . message_id_display ( ) ,
err . to_string ( )
) ;
log (
format! (
" Failed to insert envelope {}: {} " ,
envelope . message_id_display ( ) ,
err . to_string ( )
) ,
ERROR ,
) ;
}
Ok ( ( ) )
}
2019-11-12 11:09:43 +00:00
pub fn remove ( env_hash : EnvelopeHash ) -> Result < ( ) > {
2020-08-25 11:02:30 +00:00
let db_path = db_path ( ) ? ;
2020-05-30 12:35:51 +00:00
if ! db_path . exists ( ) {
return Err ( MeliError ::new (
" Database hasn't been initialised. Run `reindex` command " ,
) ) ;
}
let conn = melib_sqlite3 ::open_db ( db_path ) ? ;
2020-05-29 17:19:34 +00:00
if let Err ( err ) = conn
. execute (
" DELETE FROM envelopes WHERE hash = ? " ,
params! [ env_hash . to_be_bytes ( ) . to_vec ( ) , ] ,
)
. map_err ( | e | MeliError ::new ( e . to_string ( ) ) )
{
debug! (
" Failed to remove envelope {}: {} " ,
env_hash ,
err . to_string ( )
) ;
log (
format! (
2019-11-12 11:09:43 +00:00
" Failed to remove envelope {}: {} " ,
env_hash ,
err . to_string ( )
2020-05-29 17:19:34 +00:00
) ,
ERROR ,
) ;
return Err ( err ) ;
2019-11-12 11:09:43 +00:00
}
Ok ( ( ) )
}
2020-07-16 20:58:46 +00:00
pub fn index ( context : & mut crate ::state ::Context , account_index : usize ) -> ResultFuture < ( ) > {
let account = & context . accounts [ account_index ] ;
let ( acc_name , acc_mutex , backend_mutex ) : ( String , Arc < RwLock < _ > > , Arc < _ > ) = (
account . name ( ) . to_string ( ) ,
account . collection . envelopes . clone ( ) ,
account . backend . clone ( ) ,
) ;
2020-08-25 11:02:30 +00:00
let conn = melib_sqlite3 ::open_or_create_db ( & DB , None ) ? ;
2020-05-29 17:19:34 +00:00
let env_hashes = acc_mutex
. read ( )
. unwrap ( )
. keys ( )
. cloned ( )
. collect ::< Vec < _ > > ( ) ;
2019-11-03 11:18:43 +00:00
/* Sleep, index and repeat in order not to block the main process */
2020-07-16 20:58:46 +00:00
Ok ( Box ::pin ( async move {
conn . execute (
" INSERT OR REPLACE INTO accounts (name) VALUES (?1) " ,
params! [ acc_name . as_str ( ) , ] ,
)
. chain_err_summary ( | | " Failed to update index: " ) ? ;
let account_id : i32 = {
let mut stmt = conn
. prepare ( " SELECT id FROM accounts WHERE name = ? " )
. unwrap ( ) ;
let x = stmt
. query_map ( params! [ acc_name . as_str ( ) ] , | row | row . get ( 0 ) )
. unwrap ( )
. next ( )
. unwrap ( )
2019-11-03 11:18:43 +00:00
. unwrap ( ) ;
2020-07-16 20:58:46 +00:00
x
} ;
let mut ctr = 0 ;
debug! (
" {} " ,
format! (
" Rebuilding {} index. {}/{} " ,
acc_name ,
ctr ,
env_hashes . len ( )
)
) ;
for chunk in env_hashes . chunks ( 200 ) {
ctr + = chunk . len ( ) ;
for env_hash in chunk {
2020-08-25 21:00:38 +00:00
let mut op = backend_mutex . read ( ) . unwrap ( ) . operation ( * env_hash ) ? ;
let bytes = op
. as_bytes ( ) ?
. await
. chain_err_summary ( | | format! ( " Failed to open envelope {} " , env_hash ) ) ? ;
let envelopes_lck = acc_mutex . read ( ) . unwrap ( ) ;
2020-07-16 20:58:46 +00:00
if let Some ( e ) = envelopes_lck . get ( & env_hash ) {
2020-08-25 21:00:38 +00:00
let body = e . body_bytes ( & bytes ) . text ( ) . replace ( '\0' , " " ) ;
2020-07-16 20:58:46 +00:00
conn . execute ( " INSERT OR REPLACE INTO envelopes (account_id, hash, date, _from, _to, cc, bcc, subject, message_id, in_reply_to, _references, flags, has_attachments, body_text, timestamp)
2019-11-11 16:01:01 +00:00
VALUES ( ? 1 , ? 2 , ? 3 , ? 4 , ? 5 , ? 6 , ? 7 , ? 8 , ? 9 , ? 10 , ? 11 , ? 12 , ? 13 , ? 14 , ? 15 ) " ,
params! [ account_id , e . hash ( ) . to_be_bytes ( ) . to_vec ( ) , e . date_as_str ( ) , e . field_from_to_string ( ) , e . field_to_to_string ( ) , e . field_cc_to_string ( ) , e . field_bcc_to_string ( ) , e . subject ( ) . into_owned ( ) . trim_end_matches ( '\u{0}' ) , e . message_id_display ( ) . to_string ( ) , e . in_reply_to_display ( ) . map ( | f | f . to_string ( ) ) . unwrap_or ( String ::new ( ) ) , e . field_references_to_string ( ) , i64 ::from ( e . flags ( ) . bits ( ) ) , if e . has_attachments ( ) { 1 } else { 0 } , body , e . date ( ) . to_be_bytes ( ) . to_vec ( ) ] ,
2020-07-16 20:58:46 +00:00
) . chain_err_summary ( | | format! ( " Failed to insert envelope {} " , e . message_id_display ( ) ) ) ? ;
2019-11-03 11:18:43 +00:00
}
2020-07-16 20:58:46 +00:00
}
let sleep_dur = std ::time ::Duration ::from_millis ( 20 ) ;
std ::thread ::sleep ( sleep_dur ) ;
2019-10-07 13:42:44 +00:00
}
2020-07-16 20:58:46 +00:00
Ok ( ( ) )
} ) )
2019-10-07 13:42:44 +00:00
}
pub fn search (
2020-07-25 15:06:42 +00:00
query : & Query ,
2019-10-12 15:03:58 +00:00
( sort_field , sort_order ) : ( SortField , SortOrder ) ,
2020-07-16 20:58:46 +00:00
) -> ResultFuture < SmallVec < [ EnvelopeHash ; 512 ] > > {
2020-08-25 11:02:30 +00:00
let db_path = db_path ( ) ? ;
2020-05-30 12:35:51 +00:00
if ! db_path . exists ( ) {
return Err ( MeliError ::new (
" Database hasn't been initialised. Run `reindex` command " ,
) ) ;
}
let conn = melib_sqlite3 ::open_db ( db_path ) ? ;
2019-10-12 15:03:58 +00:00
let sort_field = match debug! ( sort_field ) {
SortField ::Subject = > " subject " ,
SortField ::Date = > " timestamp " ,
} ;
let sort_order = match debug! ( sort_order ) {
SortOrder ::Asc = > " ASC " ,
SortOrder ::Desc = > " DESC " ,
} ;
2019-11-07 17:14:38 +00:00
let mut stmt = conn
. prepare (
debug! ( format! (
" SELECT hash FROM envelopes WHERE {} ORDER BY {} {}; " ,
2020-07-25 15:06:42 +00:00
query_to_sql ( & query ) ,
2019-11-07 17:14:38 +00:00
sort_field ,
sort_order
) )
. as_str ( ) ,
)
. map_err ( | e | MeliError ::new ( e . to_string ( ) ) ) ? ;
2019-10-07 13:42:44 +00:00
let results = stmt
2019-11-07 17:14:38 +00:00
. query_map ( rusqlite ::NO_PARAMS , | row | Ok ( row . get ( 0 ) ? ) )
2019-10-07 13:42:44 +00:00
. map_err ( | e | MeliError ::new ( e . to_string ( ) ) ) ?
. map ( | r : std ::result ::Result < Vec < u8 > , rusqlite ::Error > | {
Ok ( u64 ::from_be_bytes (
r . map_err ( | e | MeliError ::new ( e . to_string ( ) ) ) ?
. as_slice ( )
. try_into ( )
. map_err ( | e : std ::array ::TryFromSliceError | MeliError ::new ( e . to_string ( ) ) ) ? ,
) )
} )
2020-01-08 15:04:44 +00:00
. collect ::< Result < SmallVec < [ EnvelopeHash ; 512 ] > > > ( ) ;
2020-07-16 20:58:46 +00:00
Ok ( Box ::pin ( async { results } ) )
2019-10-07 13:42:44 +00:00
}
2019-11-07 20:32:42 +00:00
/// Translates a `Query` to an Sqlite3 expression in a `String`.
2019-11-07 17:14:38 +00:00
pub fn query_to_sql ( q : & Query ) -> String {
fn rec ( q : & Query , s : & mut String ) {
match q {
Subject ( t ) = > {
2019-11-07 20:32:42 +00:00
s . push_str ( " subject LIKE \" % " ) ;
2019-11-07 17:14:38 +00:00
s . extend ( escape_double_quote ( t ) . chars ( ) ) ;
2019-11-07 20:32:42 +00:00
s . push_str ( " % \" " ) ;
2019-11-07 17:14:38 +00:00
}
From ( t ) = > {
2019-11-07 20:32:42 +00:00
s . push_str ( " _from LIKE \" % " ) ;
2019-11-07 17:14:38 +00:00
s . extend ( escape_double_quote ( t ) . chars ( ) ) ;
2019-11-07 20:32:42 +00:00
s . push_str ( " % \" " ) ;
2019-11-07 17:14:38 +00:00
}
2019-11-11 16:01:01 +00:00
To ( t ) = > {
s . push_str ( " _to LIKE \" % " ) ;
s . extend ( escape_double_quote ( t ) . chars ( ) ) ;
s . push_str ( " % \" " ) ;
}
Cc ( t ) = > {
s . push_str ( " cc LIKE \" % " ) ;
s . extend ( escape_double_quote ( t ) . chars ( ) ) ;
s . push_str ( " % \" " ) ;
}
Bcc ( t ) = > {
s . push_str ( " bcc LIKE \" % " ) ;
s . extend ( escape_double_quote ( t ) . chars ( ) ) ;
s . push_str ( " % \" " ) ;
}
2019-11-07 17:14:38 +00:00
AllText ( t ) = > {
2019-11-07 20:32:42 +00:00
s . push_str ( " body_text LIKE \" % " ) ;
2019-11-07 17:14:38 +00:00
s . extend ( escape_double_quote ( t ) . chars ( ) ) ;
2019-11-07 20:32:42 +00:00
s . push_str ( " % \" " ) ;
2019-11-07 17:14:38 +00:00
}
And ( q1 , q2 ) = > {
2019-11-07 20:32:42 +00:00
s . push_str ( " ( " ) ;
2019-11-07 17:14:38 +00:00
rec ( q1 , s ) ;
2019-11-07 20:32:42 +00:00
s . push_str ( " ) AND ( " ) ;
2019-11-07 17:14:38 +00:00
rec ( q2 , s ) ;
s . push_str ( " ) " ) ;
}
Or ( q1 , q2 ) = > {
2019-11-07 20:32:42 +00:00
s . push_str ( " ( " ) ;
2019-11-07 17:14:38 +00:00
rec ( q1 , s ) ;
2019-11-07 20:32:42 +00:00
s . push_str ( " ) OR ( " ) ;
2019-11-07 17:14:38 +00:00
rec ( q2 , s ) ;
s . push_str ( " ) " ) ;
}
Not ( q ) = > {
2019-11-07 20:32:42 +00:00
s . push_str ( " NOT ( " ) ;
2019-11-07 17:14:38 +00:00
rec ( q , s ) ;
2019-11-07 20:32:42 +00:00
s . push_str ( " ) " ) ;
2019-11-07 17:14:38 +00:00
}
2019-11-11 20:43:08 +00:00
Flags ( v ) = > {
let total = v . len ( ) ;
if total > 1 {
s . push_str ( " ( " ) ;
}
for ( i , f ) in v . iter ( ) . enumerate ( ) {
match f . as_str ( ) {
" draft " = > {
s . push_str ( " (flags & 8 > 0) " ) ;
}
" deleted " | " trashed " = > {
s . push_str ( " (flags & 6 > 0) " ) ;
}
" flagged " = > {
s . push_str ( " (flags & 16 > 0) " ) ;
}
" recent " = > {
s . push_str ( " (flags & 4 == 0) " ) ;
}
" seen " | " read " = > {
s . push_str ( " (flags & 4 > 0) " ) ;
}
" unseen " | " unread " = > {
s . push_str ( " (flags & 4 == 0) " ) ;
}
" answered " | " replied " = > {
s . push_str ( " (flags & 2 > 0) " ) ;
}
" unanswered " = > {
s . push_str ( " (flags & 2 == 0) " ) ;
}
_ = > {
continue ;
}
}
if total > 1 & & i ! = total - 1 {
s . push_str ( " AND " ) ;
}
}
if total > 1 {
s . push_str ( " ) " ) ;
}
}
2019-11-11 20:59:37 +00:00
HasAttachment = > {
s . push_str ( " has_attachments == 1 " ) ;
}
2019-11-07 17:14:38 +00:00
_ = > { }
}
}
let mut ret = String ::new ( ) ;
rec ( q , & mut ret ) ;
ret
}
#[ test ]
fn test_query_to_sql ( ) {
2020-07-25 15:06:42 +00:00
use melib ::parsec ::Parser ;
use melib ::search ::query ;
2019-11-07 17:14:38 +00:00
assert_eq! (
2019-11-07 20:32:42 +00:00
" (subject LIKE \" %test% \" ) AND (body_text LIKE \" %i% \" ) " ,
2019-11-07 17:14:38 +00:00
& query_to_sql ( & query ( ) . parse_complete ( " subject: test and i " ) . unwrap ( ) . 1 )
) ;
assert_eq! (
2019-11-07 20:32:42 +00:00
" (subject LIKE \" %github% \" ) OR ((_from LIKE \" %epilys% \" ) AND ((subject LIKE \" %lib% \" ) OR (subject LIKE \" %meli% \" ) ) ) " ,
2019-11-07 17:14:38 +00:00
& query_to_sql (
& query ( )
. parse_complete (
" subject: github or (from: epilys and (subject:lib or subject: meli)) "
)
. unwrap ( )
. 1
)
) ;
}