diff --git a/Cargo.lock b/Cargo.lock index a5608f4de..dd7c98365 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,17 +113,6 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" -[[package]] -name = "ahash" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f200cbb1e856866d9eade941cf3aa0c5d7dd36f74311c4273b494f4ef036957" -dependencies = [ - "getrandom 0.2.2", - "once_cell", - "version_check 0.9.3", -] - [[package]] name = "aho-corasick" version = "0.7.15" @@ -425,15 +414,6 @@ dependencies = [ "syn", ] -[[package]] -name = "atoi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616896e05fc0e2649463a93a15183c6a16bf03413a7af88ef1285ddedfa9cda5" -dependencies = [ - "num-traits", -] - [[package]] name = "atomic-waker" version = "1.0.0" @@ -537,18 +517,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -[[package]] -name = "bitvec" -version = "0.19.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "blake2b_simd" version = "0.5.11" @@ -648,12 +616,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" -[[package]] -name = "build_const" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" - [[package]] name = "bumpalo" version = "3.6.1" @@ -682,12 +644,6 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" -[[package]] -name = "bytes" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" - [[package]] name = "cache-padded" version = "1.1.1" @@ -882,15 +838,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" -[[package]] -name = "crc" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" -dependencies = [ - "build_const", -] - [[package]] name = "crc24" version = "0.1.6" @@ -1178,6 +1125,7 @@ dependencies = [ "charset", "chrono", "criterion", + "deltachat_derive", "dirs 3.0.1", "email", "encoded-words", @@ -1191,7 +1139,6 @@ dependencies = [ "kamadak-exif", "lettre_email", "libc", - "libsqlite3-sys", "log", "mailparse", "native-tls", @@ -1205,8 +1152,11 @@ dependencies = [ "pretty_env_logger", "proptest", "quick-xml", + "r2d2", + "r2d2_sqlite", "rand 0.7.3", "regex", + "rusqlite", "rust-hsluv", "rustyline", "sanitize-filename", @@ -1215,7 +1165,6 @@ dependencies = [ "sha-1", "sha2", "smallvec", - "sqlx", "stop-token", "strum", "strum_macros", @@ -1227,6 +1176,14 @@ dependencies = [ "uuid", ] +[[package]] +name = "deltachat_derive" +version = "2.0.0" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "deltachat_ffi" version = "1.53.0" @@ -1330,12 +1287,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" -[[package]] -name = "dotenv" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" - [[package]] name = "ed25519" version = "1.0.3" @@ -1513,6 +1464,18 @@ version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fast_chemail" version = "0.9.6" @@ -1586,12 +1549,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "funty" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" - [[package]] name = "futures" version = "0.3.13" @@ -1782,7 +1739,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" dependencies = [ - "ahash 0.4.7", + "ahash", ] [[package]] @@ -2132,9 +2089,9 @@ checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" [[package]] name = "libsqlite3-sys" -version = "0.22.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f6332d94daa84478d55a6aa9dbb3b305ed6500fb0cb9400cb9e1525d0e0e188" +checksum = "64d31059f22935e6c31830db5249ba2b7ecd54fd73a9909286f0a67aa55c2fbd" dependencies = [ "cc", "pkg-config", @@ -2186,12 +2143,6 @@ dependencies = [ "quoted_printable", ] -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - [[package]] name = "match_cfg" version = "0.1.0" @@ -2339,19 +2290,6 @@ dependencies = [ "version_check 0.9.3", ] -[[package]] -name = "nom" -version = "6.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" -dependencies = [ - "bitvec", - "funty", - "lexical-core", - "memchr", - "version_check 0.9.3", -] - [[package]] name = "num-bigint" version = "0.2.6" @@ -2863,10 +2801,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b080c5db639b292ac79cbd34be0cfc5d36694768d8341109634d90b86930e2" [[package]] -name = "radium" -version = "0.5.3" +name = "r2d2" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" +checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + +[[package]] +name = "r2d2_sqlite" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "227ab35ff4cbb01fa76da8f062590fe677b93c8d9e8415eb5fa981f2c1dba9d8" +dependencies = [ + "r2d2", + "rusqlite", +] [[package]] name = "rand" @@ -3109,6 +3062,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rusqlite" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38ee71cbab2c827ec0ac24e76f82eca723cee92c509a65f67dee393c25112" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "memchr", + "smallvec", +] + [[package]] name = "rust-argon2" version = "0.8.3" @@ -3212,6 +3180,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "scheduled-thread-pool" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7" +dependencies = [ + "parking_lot", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -3449,94 +3426,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" -[[package]] -name = "sqlformat" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d86e3c77ff882a828346ba401a7ef4b8e440df804491c6064fe8295765de71c" -dependencies = [ - "lazy_static", - "maplit", - "nom 6.1.2", - "regex", - "unicode_categories", -] - -[[package]] -name = "sqlx" -version = "0.5.2" -source = "git+https://github.com/deltachat/sqlx?branch=master#36ebc7a5f7d34efc539766a8a3693513b85bef29" -dependencies = [ - "sqlx-core", - "sqlx-macros", -] - -[[package]] -name = "sqlx-core" -version = "0.5.2" -source = "git+https://github.com/deltachat/sqlx?branch=master#36ebc7a5f7d34efc539766a8a3693513b85bef29" -dependencies = [ - "ahash 0.7.2", - "atoi", - "bitflags", - "byteorder", - "bytes", - "crc", - "crossbeam-channel", - "crossbeam-queue 0.3.1", - "crossbeam-utils 0.8.3", - "either", - "futures-channel", - "futures-core", - "futures-util", - "hashlink", - "hex", - "itoa", - "libc", - "libsqlite3-sys", - "log", - "memchr", - "once_cell", - "parking_lot", - "percent-encoding", - "sha2", - "smallvec", - "sqlformat", - "sqlx-rt", - "stringprep", - "thiserror", - "url", - "whoami", -] - -[[package]] -name = "sqlx-macros" -version = "0.5.2" -source = "git+https://github.com/deltachat/sqlx?branch=master#36ebc7a5f7d34efc539766a8a3693513b85bef29" -dependencies = [ - "dotenv", - "either", - "futures", - "heck", - "proc-macro2", - "quote", - "sha2", - "sqlx-core", - "sqlx-rt", - "syn", - "url", -] - -[[package]] -name = "sqlx-rt" -version = "0.5.2" -source = "git+https://github.com/deltachat/sqlx?branch=master#36ebc7a5f7d34efc539766a8a3693513b85bef29" -dependencies = [ - "async-native-tls", - "async-std", - "native-tls", -] - [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -3627,16 +3516,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "stringprep" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "strsim" version = "0.9.3" @@ -3709,12 +3588,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "tempfile" version = "3.2.0" @@ -3969,12 +3842,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - [[package]] name = "universal-hash" version = "0.4.0" @@ -4182,16 +4049,6 @@ dependencies = [ "cc", ] -[[package]] -name = "whoami" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e296f550993cba2c5c3eba5da0fb335562b2fa3d97b7a8ac9dc91f40a3abc70" -dependencies = [ - "wasm-bindgen", - "web-sys", -] - [[package]] name = "widestring" version = "0.4.3" @@ -4247,12 +4104,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "wyz" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" - [[package]] name = "x25519-dalek" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 142ad4b56..7fb9dcfa7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,8 @@ debug = 0 lto = true [dependencies] +deltachat_derive = { path = "./deltachat_derive" } + ansi_term = { version = "0.12.1", optional = true } anyhow = "1.0.28" async-imap = "0.4.0" @@ -50,8 +52,11 @@ percent-encoding = "2.0" pgp = { version = "0.7.0", default-features = false } pretty_env_logger = { version = "0.4.0", optional = true } quick-xml = "0.18.1" +r2d2 = "0.8.5" +r2d2_sqlite = "0.17.0" rand = "0.7.0" regex = "1.1.6" +rusqlite = { version = "0.24", features = ["bundled"] } rust-hsluv = "0.1.4" rustyline = { version = "4.1.0", optional = true } sanitize-filename = "0.3.0" @@ -60,9 +65,6 @@ serde = { version = "1.0", features = ["derive"] } sha-1 = "0.9.3" sha2 = "0.9.0" smallvec = "1.0.0" -sqlx = { git = "https://github.com/deltachat/sqlx", branch = "master", features = ["runtime-async-std-native-tls", "sqlite"] } -# keep in sync with sqlx -libsqlite3-sys = { version = "0.22.0", default-features = false, features = [ "pkg-config", "vcpkg", "bundled" ] } stop-token = { version = "0.1.1", features = ["unstable"] } strum = "0.20.0" strum_macros = "0.20.1" @@ -86,6 +88,7 @@ tempfile = "3.0" [workspace] members = [ "deltachat-ffi", + "deltachat_derive", ] [[example]] diff --git a/deltachat_derive/Cargo.toml b/deltachat_derive/Cargo.toml new file mode 100644 index 000000000..ee6abfae6 --- /dev/null +++ b/deltachat_derive/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "deltachat_derive" +version = "2.0.0" +authors = ["Delta Chat Developers (ML) "] +edition = "2018" +license = "MPL-2.0" + +[lib] +proc-macro = true + +[dependencies] +syn = "1.0.13" +quote = "1.0.2" diff --git a/deltachat_derive/src/lib.rs b/deltachat_derive/src/lib.rs new file mode 100644 index 000000000..e24c5cf39 --- /dev/null +++ b/deltachat_derive/src/lib.rs @@ -0,0 +1,47 @@ +#![recursion_limit = "128"] +extern crate proc_macro; + +use crate::proc_macro::TokenStream; +use quote::quote; + +// For now, assume (not check) that these macroses are applied to enum without +// data. If this assumption is violated, compiler error will point to +// generated code, which is not very user-friendly. + +#[proc_macro_derive(ToSql)] +pub fn to_sql_derive(input: TokenStream) -> TokenStream { + let ast: syn::DeriveInput = syn::parse(input).unwrap(); + let name = &ast.ident; + + let gen = quote! { + impl rusqlite::types::ToSql for #name { + fn to_sql(&self) -> rusqlite::Result { + let num = *self as i64; + let value = rusqlite::types::Value::Integer(num); + let output = rusqlite::types::ToSqlOutput::Owned(value); + std::result::Result::Ok(output) + } + } + }; + gen.into() +} + +#[proc_macro_derive(FromSql)] +pub fn from_sql_derive(input: TokenStream) -> TokenStream { + let ast: syn::DeriveInput = syn::parse(input).unwrap(); + let name = &ast.ident; + + let gen = quote! { + impl rusqlite::types::FromSql for #name { + fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult { + let inner = rusqlite::types::FromSql::column_result(col)?; + if let Some(value) = num_traits::FromPrimitive::from_i64(inner) { + Ok(value) + } else { + Err(rusqlite::types::FromSqlError::OutOfRange(inner)) + } + } + } + }; + gen.into() +} diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index 1f4fa5fb9..9d61f4d20 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -34,7 +34,7 @@ async fn reset_tables(context: &Context, bits: i32) { if 0 != bits & 1 { context .sql() - .execute(sqlx::query("DELETE FROM jobs;")) + .execute("DELETE FROM jobs;", paramsv![]) .await .unwrap(); println!("(1) Jobs reset."); @@ -42,7 +42,7 @@ async fn reset_tables(context: &Context, bits: i32) { if 0 != bits & 2 { context .sql() - .execute(sqlx::query("DELETE FROM acpeerstates;")) + .execute("DELETE FROM acpeerstates;", paramsv![]) .await .unwrap(); println!("(2) Peerstates reset."); @@ -50,7 +50,7 @@ async fn reset_tables(context: &Context, bits: i32) { if 0 != bits & 4 { context .sql() - .execute(sqlx::query("DELETE FROM keypairs;")) + .execute("DELETE FROM keypairs;", paramsv![]) .await .unwrap(); println!("(4) Private keypairs reset."); @@ -58,34 +58,35 @@ async fn reset_tables(context: &Context, bits: i32) { if 0 != bits & 8 { context .sql() - .execute(sqlx::query("DELETE FROM contacts WHERE id>9;")) + .execute("DELETE FROM contacts WHERE id>9;", paramsv![]) .await .unwrap(); context .sql() - .execute(sqlx::query("DELETE FROM chats WHERE id>9;")) + .execute("DELETE FROM chats WHERE id>9;", paramsv![]) .await .unwrap(); context .sql() - .execute(sqlx::query("DELETE FROM chats_contacts;")) + .execute("DELETE FROM chats_contacts;", paramsv![]) .await .unwrap(); context .sql() - .execute(sqlx::query("DELETE FROM msgs WHERE id>9;")) + .execute("DELETE FROM msgs WHERE id>9;", paramsv![]) .await .unwrap(); context .sql() - .execute(sqlx::query( + .execute( "DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';", - )) + paramsv![], + ) .await .unwrap(); context .sql() - .execute(sqlx::query("DELETE FROM leftgrps;")) + .execute("DELETE FROM leftgrps;", paramsv![]) .await .unwrap(); println!("(8) Rest but server config reset."); @@ -602,7 +603,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu ensure!(sel_chat.is_some(), "Failed to select chat"); let sel_chat = sel_chat.as_ref().unwrap(); - let msglist = chat::get_chat_msgs(&context, sel_chat.get_id(), 0x1, None).await?; + let msglist = + chat::get_chat_msgs(&context, sel_chat.get_id(), DC_GCM_ADDDAYMARKER, None).await?; let msglist: Vec = msglist .into_iter() .map(|x| match x { diff --git a/src/chat.rs b/src/chat.rs index 6cadfc74a..eb5c11336 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1,18 +1,16 @@ //! # Chat module -use std::convert::TryFrom; -use std::convert::TryInto; +use std::convert::{TryFrom, TryInto}; use std::str::FromStr; use std::time::{Duration, SystemTime}; use anyhow::Context as _; use anyhow::{bail, ensure, format_err, Error}; use async_std::path::{Path, PathBuf}; -use async_std::prelude::*; +use deltachat_derive::{FromSql, ToSql}; use itertools::Itertools; use num_traits::FromPrimitive; use serde::{Deserialize, Serialize}; -use sqlx::Row; use crate::aheader::EncryptPreference; use crate::blob::{BlobError, BlobObject}; @@ -71,10 +69,11 @@ pub enum ChatItem { Eq, FromPrimitive, ToPrimitive, + FromSql, + ToSql, IntoStaticStr, Serialize, Deserialize, - sqlx::Type, )] #[repr(u32)] pub enum ProtectionStatus { @@ -93,20 +92,8 @@ impl Default for ProtectionStatus { /// Some chat IDs are reserved to identify special chat types. This /// type can represent both the special as well as normal chats. #[derive( - Debug, - Copy, - Clone, - Default, - PartialEq, - Eq, - Serialize, - Deserialize, - Hash, - PartialOrd, - Ord, - sqlx::Type, + Debug, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord, )] -#[sqlx(transparent)] pub struct ChatId(u32); impl ChatId { @@ -178,13 +165,10 @@ impl ChatId { context .sql .execute( - sqlx::query( - "UPDATE contacts + "UPDATE contacts SET selfavatar_sent=? WHERE id IN(SELECT contact_id FROM chats_contacts WHERE chat_id=?);", - ) - .bind(timestamp) - .bind(self), + paramsv![timestamp, self], ) .await?; Ok(()) @@ -198,9 +182,8 @@ impl ChatId { context .sql .execute( - sqlx::query("UPDATE chats SET blocked=? WHERE id=?;") - .bind(new_blocked) - .bind(self), + "UPDATE chats SET blocked=? WHERE id=?;", + paramsv![new_blocked, self], ) .await .is_ok() @@ -248,9 +231,8 @@ impl ChatId { context .sql .execute( - sqlx::query("UPDATE chats SET protected=? WHERE id=?;") - .bind(protect) - .bind(self), + "UPDATE chats SET protected=? WHERE id=?;", + paramsv![protect, self], ) .await?; @@ -333,10 +315,8 @@ impl ChatId { context .sql .execute( - sqlx::query("UPDATE msgs SET state=? WHERE chat_id=? AND state=?;") - .bind(MessageState::InNoticed) - .bind(self) - .bind(MessageState::InFresh), + "UPDATE msgs SET state=? WHERE chat_id=? AND state=?;", + paramsv![MessageState::InNoticed, self, MessageState::InFresh], ) .await?; } @@ -344,9 +324,8 @@ impl ChatId { context .sql .execute( - sqlx::query("UPDATE chats SET archived=? WHERE id=?;") - .bind(visibility) - .bind(self), + "UPDATE chats SET archived=? WHERE id=?;", + paramsv![visibility, self], ) .await?; @@ -364,7 +343,8 @@ impl ChatId { context .sql .execute( - sqlx::query("UPDATE chats SET archived=0 WHERE id=? and archived=1").bind(self), + "UPDATE chats SET archived=0 WHERE id=? and archived=1", + paramsv![self], ) .await?; Ok(()) @@ -383,26 +363,27 @@ impl ChatId { context .sql .execute( - sqlx::query( - "DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?);", - ) - .bind(self), + "DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?);", + paramsv![self], ) .await?; context .sql - .execute(sqlx::query("DELETE FROM msgs WHERE chat_id=?;").bind(self)) + .execute("DELETE FROM msgs WHERE chat_id=?;", paramsv![self]) .await?; context .sql - .execute(sqlx::query("DELETE FROM chats_contacts WHERE chat_id=?;").bind(self)) + .execute( + "DELETE FROM chats_contacts WHERE chat_id=?;", + paramsv![self], + ) .await?; context .sql - .execute(sqlx::query("DELETE FROM chats WHERE id=?;").bind(self)) + .execute("DELETE FROM chats WHERE id=?;", paramsv![self]) .await?; context.emit_event(EventType::MsgsChanged { @@ -462,10 +443,9 @@ impl ChatId { async fn get_draft_msg_id(self, context: &Context) -> Result, Error> { context .sql - .query_get_value::<_, MsgId>( - sqlx::query("SELECT id FROM msgs WHERE chat_id=? AND state=?;") - .bind(self) - .bind(MessageState::OutDraft), + .query_get_value::( + "SELECT id FROM msgs WHERE chat_id=? AND state=?;", + paramsv![self, MessageState::OutDraft], ) .await .map_err(Into::into) @@ -523,28 +503,28 @@ impl ChatId { context .sql .execute( - sqlx::query( - "INSERT INTO msgs ( - chat_id, - from_id, - timestamp, - type, - state, - txt, - param, - hidden, - mime_in_reply_to) - VALUES (?,?,?,?,?,?,?,?,?);", - ) - .bind(self) - .bind(DC_CONTACT_ID_SELF as i32) - .bind(time()) - .bind(msg.viewtype) - .bind(MessageState::OutDraft) - .bind(msg.text.as_deref().unwrap_or("")) - .bind(msg.param.to_string()) - .bind(1i32) - .bind(msg.in_reply_to.as_deref().unwrap_or_default()), + "INSERT INTO msgs ( + chat_id, + from_id, + timestamp, + type, + state, + txt, + param, + hidden, + mime_in_reply_to) + VALUES (?,?,?, ?,?,?,?,?,?);", + paramsv![ + self, + DC_CONTACT_ID_SELF, + time(), + msg.viewtype, + MessageState::OutDraft, + msg.text.as_deref().unwrap_or(""), + msg.param.to_string(), + 1, + msg.in_reply_to.as_deref().unwrap_or_default(), + ], ) .await?; Ok(()) @@ -554,7 +534,7 @@ impl ChatId { pub async fn get_msg_cnt(self, context: &Context) -> Result { let count = context .sql - .count(sqlx::query("SELECT COUNT(*) FROM msgs WHERE chat_id=?;").bind(self)) + .count("SELECT COUNT(*) FROM msgs WHERE chat_id=?", paramsv![self]) .await?; Ok(count as usize) } @@ -573,14 +553,12 @@ impl ChatId { let count = context .sql .count( - sqlx::query( - "SELECT COUNT(*) + "SELECT COUNT(*) FROM msgs WHERE state=10 AND hidden=0 AND chat_id=?;", - ) - .bind(self), + paramsv![self], ) .await?; Ok(count as usize) @@ -589,7 +567,7 @@ impl ChatId { pub(crate) async fn get_param(self, context: &Context) -> Result { let res: Option = context .sql - .query_get_value(sqlx::query("SELECT param FROM chats WHERE id=?").bind(self)) + .query_get_value("SELECT param FROM chats WHERE id=?", paramsv![self]) .await?; Ok(res .map(|s| s.parse().unwrap_or_default()) @@ -606,26 +584,36 @@ impl ChatId { Ok(self.get_param(context).await?.exists(Param::Devicetalk)) } - async fn parent_query( + async fn parent_query( self, context: &Context, fields: &str, - ) -> sql::Result> { - let q = format!( + f: F, + ) -> anyhow::Result> + where + F: FnOnce(&rusqlite::Row) -> rusqlite::Result, + { + let sql = &context.sql; + let query = format!( "SELECT {} \ FROM msgs WHERE chat_id=? AND state NOT IN (?, ?, ?, ?) AND NOT hidden \ ORDER BY timestamp DESC, id DESC \ LIMIT 1;", fields ); - let query = sqlx::query(&q) - .bind(self) - .bind(MessageState::OutPreparing) - .bind(MessageState::OutDraft) - .bind(MessageState::OutPending) - .bind(MessageState::OutFailed); - - let row = context.sql.fetch_optional(query).await?; + let row = sql + .query_row_optional( + query, + paramsv![ + self, + MessageState::OutPreparing, + MessageState::OutDraft, + MessageState::OutPending, + MessageState::OutFailed + ], + f, + ) + .await?; Ok(row) } @@ -633,18 +621,20 @@ impl ChatId { self, context: &Context, ) -> sql::Result> { - if let Some(row) = self + if let Some((rfc724_mid, mime_in_reply_to, mime_references, error)) = self .parent_query( context, "rfc724_mid, mime_in_reply_to, mime_references, error", + |row: &rusqlite::Row| { + let rfc724_mid: String = row.get(0)?; + let mime_in_reply_to: String = row.get(1)?; + let mime_references: String = row.get(2)?; + let error: String = row.get(3)?; + Ok((rfc724_mid, mime_in_reply_to, mime_references, error)) + }, ) .await? { - let rfc724_mid: String = row.try_get(0)?; - let mime_in_reply_to: String = row.try_get(1)?; - let mime_references: String = row.try_get(2)?; - let error: String = row.try_get(3)?; - if !error.is_empty() { // Do not reply to error messages. // @@ -729,6 +719,31 @@ impl std::fmt::Display for ChatId { } } +/// Allow converting [ChatId] to an SQLite type. +/// +/// This allows you to directly store [ChatId] into the database as +/// well as query for a [ChatId]. +impl rusqlite::types::ToSql for ChatId { + fn to_sql(&self) -> rusqlite::Result { + let val = rusqlite::types::Value::Integer(self.0 as i64); + let out = rusqlite::types::ToSqlOutput::Owned(val); + Ok(out) + } +} + +/// Allow converting an SQLite integer directly into [ChatId]. +impl rusqlite::types::FromSql for ChatId { + fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult { + i64::column_result(value).and_then(|val| { + if 0 <= val && val <= std::u32::MAX as i64 { + Ok(ChatId::new(val as u32)) + } else { + Err(rusqlite::types::FromSqlError::OutOfRange(val)) + } + }) + } +} + /// An object representing a single chat in memory. /// Chat objects are created using eg. `Chat::load_from_db` /// and are not updated on database changes; @@ -750,32 +765,32 @@ pub struct Chat { impl Chat { /// Loads chat from the database by its ID. pub async fn load_from_db(context: &Context, chat_id: ChatId) -> Result { - let row = context + let mut chat = context .sql - .fetch_one( - sqlx::query( - "SELECT c.type, c.name, c.grpid, c.param, c.archived, + .query_row( + "SELECT c.type, c.name, c.grpid, c.param, c.archived, c.blocked, c.locations_send_until, c.muted_until, c.protected FROM chats c WHERE c.id=?;", - ) - .bind(chat_id), + paramsv![chat_id], + |row| { + let c = Chat { + id: chat_id, + typ: row.get(0)?, + name: row.get::<_, String>(1)?, + grpid: row.get::<_, String>(2)?, + param: row.get::<_, String>(3)?.parse().unwrap_or_default(), + visibility: row.get(4)?, + blocked: row.get::<_, Option<_>>(5)?.unwrap_or_default(), + is_sending_locations: row.get(6)?, + mute_duration: row.get(7)?, + protected: row.get(8)?, + }; + Ok(c) + }, ) .await?; - let mut chat = Chat { - id: chat_id, - typ: row.try_get(0)?, - name: row.try_get::(1)?, - grpid: row.try_get::(2)?, - param: row.try_get::(3)?.parse().unwrap_or_default(), - visibility: row.try_get(4)?, - blocked: row.try_get::, _>(5)?.unwrap_or_default(), - is_sending_locations: row.try_get(6)?, - mute_duration: row.try_get(7)?, - protected: row.try_get(8)?, - }; - if chat.id.is_deaddrop() { chat.name = stock_str::dead_drop(context).await; } else if chat.id.is_archived_link() { @@ -831,9 +846,8 @@ impl Chat { context .sql .execute( - sqlx::query("UPDATE chats SET param=? WHERE id=?") - .bind(self.param.to_string()) - .bind(self.id), + "UPDATE chats SET param=? WHERE id=?", + paramsv![self.param.to_string(), self.id], ) .await?; Ok(()) @@ -993,8 +1007,8 @@ impl Chat { if let Some(id) = context .sql .query_get_value( - sqlx::query("SELECT contact_id FROM chats_contacts WHERE chat_id=?;") - .bind(self.id), + "SELECT contact_id FROM chats_contacts WHERE chat_id=?;", + paramsv![self.id], ) .await? { @@ -1069,16 +1083,16 @@ impl Chat { if let Ok(row_id) = context .sql .insert( - sqlx::query( - "INSERT INTO locations \ + "INSERT INTO locations \ (timestamp,from_id,chat_id, latitude,longitude,independent)\ VALUES (?,?,?, ?,?,1);", - ) - .bind(timestamp) - .bind(DC_CONTACT_ID_SELF as i32) - .bind(self.id) - .bind(msg.param.get_float(Param::SetLatitude).unwrap_or_default()) - .bind(msg.param.get_float(Param::SetLongitude).unwrap_or_default()), + paramsv![ + timestamp, + DC_CONTACT_ID_SELF, + self.id, + msg.param.get_float(Param::SetLatitude).unwrap_or_default(), + msg.param.get_float(Param::SetLongitude).unwrap_or_default(), + ], ) .await { @@ -1116,8 +1130,7 @@ impl Chat { let msg_id = context .sql .insert( - sqlx::query( - "INSERT INTO msgs ( + "INSERT INTO msgs ( rfc724_mid, chat_id, from_id, @@ -1136,27 +1149,27 @@ impl Chat { location_id, ephemeral_timer, ephemeral_timestamp) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);", - ) - .bind(&new_rfc724_mid) - .bind(self.id) - .bind(DC_CONTACT_ID_SELF as i32) - .bind(to_id as i32) - .bind(timestamp) - .bind(msg.viewtype) - .bind(msg.state) - .bind(msg.text.as_ref().cloned().unwrap_or_default()) - .bind(&msg.subject) - .bind(msg.param.to_string()) - .bind(msg.hidden) - .bind(msg.in_reply_to.as_deref().unwrap_or_default()) - .bind(new_references) - .bind(new_mime_headers.is_some()) - .bind(new_mime_headers.unwrap_or_default()) - .bind(location_id as i32) - .bind(ephemeral_timer) - .bind(ephemeral_timestamp), + paramsv![ + new_rfc724_mid, + self.id, + DC_CONTACT_ID_SELF, + to_id as i32, + timestamp, + msg.viewtype, + msg.state, + msg.text.as_ref().cloned().unwrap_or_default(), + &msg.subject, + msg.param.to_string(), + msg.hidden, + msg.in_reply_to.as_deref().unwrap_or_default(), + new_references, + new_mime_headers.is_some(), + new_mime_headers.unwrap_or_default(), + location_id as i32, + ephemeral_timer, + ephemeral_timestamp + ], ) .await?; schedule_ephemeral_task(context).await; @@ -1172,53 +1185,30 @@ pub enum ChatVisibility { Pinned, } -impl ChatVisibility { - fn to_u32(self) -> u32 { - match self { +impl rusqlite::types::ToSql for ChatVisibility { + fn to_sql(&self) -> rusqlite::Result { + let visibility = match &self { ChatVisibility::Normal => 0, ChatVisibility::Archived => 1, ChatVisibility::Pinned => 2, - } - } - - fn from_u32(val: u32) -> Self { - match val { - 2 => ChatVisibility::Pinned, - 1 => ChatVisibility::Archived, - 0 => ChatVisibility::Normal, - // fallback to to Normal for unknown values, may happen eg. on imports created by a newer version. - _ => ChatVisibility::Normal, - } + }; + let val = rusqlite::types::Value::Integer(visibility); + let out = rusqlite::types::ToSqlOutput::Owned(val); + Ok(out) } } -impl sqlx::Type for ChatVisibility { - fn type_info() -> sqlx::sqlite::SqliteTypeInfo { - >::type_info() - } - - fn compatible(ty: &sqlx::sqlite::SqliteTypeInfo) -> bool { - >::compatible(ty) - } -} - -impl<'q> sqlx::Encode<'q, sqlx::Sqlite> for ChatVisibility { - fn encode_by_ref( - &self, - args: &mut Vec>, - ) -> sqlx::encode::IsNull { - args.push(sqlx::sqlite::SqliteArgumentValue::Int64( - self.to_u32() as i64 - )); - - sqlx::encode::IsNull::No - } -} - -impl<'r> sqlx::Decode<'r, sqlx::Sqlite> for ChatVisibility { - fn decode(value: sqlx::sqlite::SqliteValueRef<'r>) -> Result { - let value: u32 = sqlx::Decode::decode(value)?; - Ok(ChatVisibility::from_u32(value)) +impl rusqlite::types::FromSql for ChatVisibility { + fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult { + i64::column_result(value).map(|val| { + match val { + 2 => ChatVisibility::Pinned, + 1 => ChatVisibility::Archived, + 0 => ChatVisibility::Normal, + // fallback to to Normal for unknown values, may happen eg. on imports created by a newer version. + _ => ChatVisibility::Normal, + } + }) } } @@ -1419,10 +1409,8 @@ async fn update_special_chat_name( context .sql .execute( - sqlx::query("UPDATE chats SET name=? WHERE id=? AND name!=?;") - .bind(&name) - .bind(chat_id) - .bind(&name), + "UPDATE chats SET name=? WHERE id=? AND name!=?", + paramsv![name, chat_id, name], ) .await?; } @@ -1463,40 +1451,32 @@ pub(crate) async fn create_or_lookup_by_contact_id( context .sql - .transaction(move |conn| { - Box::pin(async move { - sqlx::query( - "INSERT INTO chats ( - type, - name, - param, - blocked, - created_timestamp - ) - VALUES(?, ?, ?, ?, ?)", - ) - .bind(Chattype::Single) - .bind(chat_name) - .bind(match contact_id { - DC_CONTACT_ID_SELF => "K=1".to_string(), // K = Param::Selftalk - DC_CONTACT_ID_DEVICE => "D=1".to_string(), // D = Param::Devicetalk - _ => "".to_string(), - }) - .bind(create_blocked) - .bind(time()) - .execute(&mut *conn) - .await?; + .transaction(move |transaction| { + transaction.execute( + "INSERT INTO chats + (type, name, param, blocked, created_timestamp) + VALUES(?, ?, ?, ?, ?)", + params![ + Chattype::Single, + chat_name, + match contact_id { + DC_CONTACT_ID_SELF => "K=1".to_string(), // K = Param::Selftalk + DC_CONTACT_ID_DEVICE => "D=1".to_string(), // D = Param::Devicetalk + _ => "".to_string(), + }, + create_blocked as u8, + time(), + ], + )?; - sqlx::query( - "INSERT INTO chats_contacts - (chat_id, contact_id) - VALUES((SELECT last_insert_rowid()), ?)", - ) - .bind(contact_id) - .execute(&mut *conn) - .await?; - Ok(()) - }) + transaction.execute( + "INSERT INTO chats_contacts + (chat_id, contact_id) + VALUES((SELECT last_insert_rowid()), ?)", + params![contact_id], + )?; + + Ok(()) }) .await?; @@ -1517,23 +1497,24 @@ pub(crate) async fn lookup_by_contact_id( let row = context .sql - .fetch_one( - sqlx::query( - "SELECT c.id, c.blocked + .query_row( + "SELECT c.id, c.blocked FROM chats c INNER JOIN chats_contacts j ON c.id=j.chat_id WHERE c.type=100 AND c.id>9 AND j.contact_id=?;", - ) - .bind(contact_id), + paramsv![contact_id as i32], + |row| { + Ok(( + row.get::<_, ChatId>(0)?, + row.get::<_, Option<_>>(1)?.unwrap_or_default(), + )) + }, ) .await?; - Ok(( - row.try_get::(0)?, - row.try_get::, _>(1)?.unwrap_or_default(), - )) + Ok(row) } pub async fn get_by_contact_id(context: &Context, contact_id: u32) -> Result { @@ -1667,9 +1648,8 @@ pub async fn is_contact_in_chat(context: &Context, chat_id: ChatId, contact_id: context .sql .exists( - sqlx::query("SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=?;") - .bind(chat_id) - .bind(contact_id), + "SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=?;", + paramsv![chat_id, contact_id as i32], ) .await .unwrap_or_default() @@ -1862,11 +1842,71 @@ pub async fn get_chat_msgs( } } - let query = if chat_id.is_deaddrop() { + let process_row = if (flags & DC_GCM_INFO_ONLY) != 0 { + |row: &rusqlite::Row| { + // is_info logic taken from Message.is_info() + let params = row.get::<_, String>("param")?; + let (from_id, to_id) = (row.get::<_, u32>("from_id")?, row.get::<_, u32>("to_id")?); + let is_info_msg: bool = from_id == DC_CONTACT_ID_INFO as u32 + || to_id == DC_CONTACT_ID_INFO as u32 + || match Params::from_str(¶ms) { + Ok(p) => { + let cmd = p.get_cmd(); + cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage + } + _ => false, + }; + + Ok(( + row.get::<_, MsgId>("id")?, + row.get::<_, i64>("timestamp")?, + !is_info_msg, + )) + } + } else { + |row: &rusqlite::Row| { + Ok(( + row.get::<_, MsgId>("id")?, + row.get::<_, i64>("timestamp")?, + false, + )) + } + }; + let process_rows = |rows: rusqlite::MappedRows<_>| { + let mut ret = Vec::new(); + let mut last_day = 0; + let cnv_to_local = dc_gm2local_offset(); + for row in rows { + let (curr_id, ts, exclude_message): (MsgId, i64, bool) = row?; + if let Some(marker_id) = marker1before { + if curr_id == marker_id { + ret.push(ChatItem::Marker1); + } + } + if (flags & DC_GCM_ADDDAYMARKER) != 0 { + let curr_local_timestamp = ts + cnv_to_local; + let curr_day = curr_local_timestamp / 86400; + if curr_day != last_day { + ret.push(ChatItem::DayMarker { + timestamp: curr_day, + }); + last_day = curr_day; + } + } + if !exclude_message { + ret.push(ChatItem::Message { msg_id: curr_id }); + } + } + Ok(ret) + }; + + let items = if chat_id.is_deaddrop() { let show_emails = ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?) .unwrap_or_default(); - sqlx::query( - "SELECT m.id AS id, m.timestamp AS timestamp + context + .sql + .query_map( + "SELECT m.id AS id, m.timestamp AS timestamp FROM msgs m LEFT JOIN chats ON m.chat_id=chats.id @@ -1879,14 +1919,19 @@ pub async fn get_chat_msgs( AND contacts.blocked=0 AND m.msgrmsg>=? ORDER BY m.timestamp,m.id;", - ) - .bind(if show_emails == ShowEmails::All { - 0i32 - } else { - 1i32 - }) + paramsv![if show_emails == ShowEmails::All { + 0i32 + } else { + 1i32 + }], + process_row, + process_rows, + ) + .await? } else if (flags & DC_GCM_INFO_ONLY) != 0 { - sqlx::query( + context + .sql + .query_map( // GLOB is used here instead of LIKE becase it is case-sensitive "SELECT m.id AS id, m.timestamp AS timestamp, m.param AS param, m.from_id AS from_id, m.to_id AS to_id FROM msgs m @@ -1897,69 +1942,28 @@ pub async fn get_chat_msgs( OR m.from_id == ? OR m.to_id == ? ) - ORDER BY m.timestamp, m.id;" - ).bind(chat_id) - .bind(DC_CONTACT_ID_INFO) - .bind(DC_CONTACT_ID_INFO) + ORDER BY m.timestamp, m.id;", + paramsv![chat_id, DC_CONTACT_ID_INFO, DC_CONTACT_ID_INFO], + process_row, + process_rows, + ) + .await? } else { - sqlx::query( - "SELECT m.id AS id, m.timestamp AS timestamp + context + .sql + .query_map( + "SELECT m.id AS id, m.timestamp AS timestamp FROM msgs m WHERE m.chat_id=? AND m.hidden=0 ORDER BY m.timestamp, m.id;", - ) - .bind(chat_id) + paramsv![chat_id], + process_row, + process_rows, + ) + .await? }; - - let mut rows = context.sql.fetch(query).await?; - - let mut ret = Vec::new(); - let mut last_day = 0; - let cnv_to_local = dc_gm2local_offset(); - - while let Some(row) = rows.next().await { - let row = row?; - if (flags & DC_GCM_INFO_ONLY) != 0 { - // is_info logic taken from Message.is_info() - let params = row.try_get::("param")?; - let from_id = row.try_get::("from_id")?; - let to_id = row.try_get::("to_id")?; - let is_info_msg: bool = from_id == DC_CONTACT_ID_INFO - || to_id == DC_CONTACT_ID_INFO - || match Params::from_str(¶ms) { - Ok(p) => { - let cmd = p.get_cmd(); - cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage - } - _ => false, - }; - - if !is_info_msg { - continue; - } - } - - let curr_id = row.try_get::("id")?; - let ts = row.try_get::("timestamp")?; - if let Some(marker_id) = marker1before { - if curr_id == marker_id { - ret.push(ChatItem::Marker1); - } - } - if (flags & DC_GCM_ADDDAYMARKER) != 0 { - let curr_local_timestamp = ts + cnv_to_local; - let curr_day = curr_local_timestamp / 86400; - if curr_day != last_day { - ret.push(ChatItem::DayMarker { - timestamp: curr_day, - }); - last_day = curr_day; - } - } - ret.push(ChatItem::Message { msg_id: curr_id }); - } - Ok(ret) + Ok(items) } pub(crate) async fn marknoticed_chat_if_older_than( @@ -1970,7 +1974,8 @@ pub(crate) async fn marknoticed_chat_if_older_than( if let Some(chat_timestamp) = context .sql .query_get_value( - sqlx::query("SELECT MAX(timestamp) FROM msgs WHERE chat_id=?").bind(chat_id), + "SELECT MAX(timestamp) FROM msgs WHERE chat_id=?", + paramsv![chat_id], ) .await? { @@ -1986,9 +1991,8 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<(), let exists = context .sql .exists( - sqlx::query("SELECT COUNT(*) FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;") - .bind(MessageState::InFresh) - .bind(chat_id), + "SELECT COUNT(*) FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;", + paramsv![MessageState::InFresh, chat_id], ) .await?; if !exists { @@ -1998,16 +2002,12 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<(), context .sql .execute( - sqlx::query( - "UPDATE msgs + "UPDATE msgs SET state=? WHERE state=? AND hidden=0 AND chat_id=?;", - ) - .bind(MessageState::InNoticed) - .bind(MessageState::InFresh) - .bind(chat_id), + paramsv![MessageState::InNoticed, MessageState::InFresh, chat_id], ) .await?; @@ -2026,33 +2026,38 @@ pub async fn get_chat_media( // TODO This query could/should be converted to `AND type IN (?, ?, ?)`. let list = context .sql - .fetch( - sqlx::query( - "SELECT id + .query_map( + "SELECT id FROM msgs WHERE chat_id=? AND (type=? OR type=? OR type=?) ORDER BY timestamp, id;", - ) - .bind(chat_id) - .bind(msg_type) - .bind(if msg_type2 != Viewtype::Unknown { - msg_type2 - } else { - msg_type - }) - .bind(if msg_type3 != Viewtype::Unknown { - msg_type3 - } else { - msg_type - }), + paramsv![ + chat_id, + msg_type, + if msg_type2 != Viewtype::Unknown { + msg_type2 + } else { + msg_type + }, + if msg_type3 != Viewtype::Unknown { + msg_type3 + } else { + msg_type + }, + ], + |row| row.get::<_, MsgId>(0), + |ids| { + let mut ret = Vec::new(); + for id in ids { + if let Ok(msg_id) = id { + ret.push(msg_id) + } + } + Ok(ret) + }, ) - .await? - .map(|row| row?.try_get(0)) - .filter_map(|row| row.ok()) - .collect() - .await; - + .await?; Ok(list) } @@ -2121,20 +2126,17 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result(0), + |ids| ids.collect::, _>>().map_err(Into::into), ) - .await? - .map(|row| row?.try_get(0)) - .collect::>() .await?; Ok(list) @@ -2154,15 +2156,10 @@ pub async fn create_group_chat( let row_id = context .sql .insert( - sqlx::query( - "INSERT INTO chats - (type, name, grpid, param, created_timestamp) - VALUES(?, ?, ?, \'U=1\', ?);", - ) - .bind(Chattype::Group) - .bind(chat_name) - .bind(&grpid) - .bind(time()), + "INSERT INTO chats + (type, name, grpid, param, created_timestamp) + VALUES(?, ?, ?, \'U=1\', ?);", + paramsv![Chattype::Group, chat_name, grpid, time(),], ) .await?; @@ -2196,9 +2193,8 @@ pub(crate) async fn add_to_chat_contacts_table( match context .sql .execute( - sqlx::query("INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)") - .bind(chat_id) - .bind(contact_id as i32), + "INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)", + paramsv![chat_id, contact_id as i32], ) .await { @@ -2223,9 +2219,8 @@ pub(crate) async fn remove_from_chat_contacts_table( match context .sql .execute( - sqlx::query("DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?") - .bind(chat_id) - .bind(contact_id as i32), + "DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?", + paramsv![chat_id, contact_id as i32], ) .await { @@ -2352,8 +2347,9 @@ pub(crate) async fn reset_gossiped_timestamp( pub async fn get_gossiped_timestamp(context: &Context, chat_id: ChatId) -> Result { let timestamp = context .sql - .query_get_value( - sqlx::query("SELECT gossiped_timestamp FROM chats WHERE id=?;").bind(chat_id), + .query_get_value::( + "SELECT gossiped_timestamp FROM chats WHERE id=?;", + paramsv![chat_id], ) .await?; Ok(timestamp.unwrap_or_default()) @@ -2373,9 +2369,8 @@ pub(crate) async fn set_gossiped_timestamp( context .sql .execute( - sqlx::query("UPDATE chats SET gossiped_timestamp=? WHERE id=?;") - .bind(timestamp) - .bind(chat_id), + "UPDATE chats SET gossiped_timestamp=? WHERE id=?;", + paramsv![timestamp, chat_id], ) .await?; @@ -2394,28 +2389,28 @@ pub(crate) async fn shall_attach_selfavatar( } let timestamp_some_days_ago = time() - DC_RESEND_USER_AVATAR_DAYS * 24 * 60 * 60; - let mut rows = context + let needs_attach = context .sql - .fetch( - sqlx::query( - "SELECT c.selfavatar_sent + .query_map( + "SELECT c.selfavatar_sent FROM chats_contacts cc LEFT JOIN contacts c ON c.id=cc.contact_id WHERE cc.chat_id=? AND cc.contact_id!=?;", - ) - .bind(chat_id) - .bind(DC_CONTACT_ID_SELF), + paramsv![chat_id, DC_CONTACT_ID_SELF], + |row| Ok(row.get::<_, i64>(0)), + |rows| { + let mut needs_attach = false; + for row in rows { + let row = row?; + let selfavatar_sent = row?; + if selfavatar_sent < timestamp_some_days_ago { + needs_attach = true; + } + } + Ok(needs_attach) + }, ) .await?; - - let mut needs_attach = false; - while let Some(row) = rows.next().await { - let row = row?; - let selfavatar_sent: i64 = row.try_get(0)?; - if selfavatar_sent < timestamp_some_days_ago { - needs_attach = true; - } - } Ok(needs_attach) } @@ -2426,50 +2421,35 @@ pub enum MuteDuration { Until(SystemTime), } -impl sqlx::Type for MuteDuration { - fn type_info() -> sqlx::sqlite::SqliteTypeInfo { - >::type_info() - } - - fn compatible(ty: &sqlx::sqlite::SqliteTypeInfo) -> bool { - >::compatible(ty) - } -} - -impl<'q> sqlx::Encode<'q, sqlx::Sqlite> for MuteDuration { - fn encode_by_ref( - &self, - args: &mut Vec>, - ) -> sqlx::encode::IsNull { +impl rusqlite::types::ToSql for MuteDuration { + fn to_sql(&self) -> rusqlite::Result { let duration: i64 = match &self { MuteDuration::NotMuted => 0, MuteDuration::Forever => -1, - MuteDuration::Until(when) => when - .duration_since(SystemTime::UNIX_EPOCH) - .ok() - .and_then(|d| d.as_secs().try_into().ok()) - .unwrap_or(0), + MuteDuration::Until(when) => { + let duration = when + .duration_since(SystemTime::UNIX_EPOCH) + .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?; + i64::try_from(duration.as_secs()) + .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))? + } }; - - args.push(sqlx::sqlite::SqliteArgumentValue::Int64(duration)); - - sqlx::encode::IsNull::No + let val = rusqlite::types::Value::Integer(duration); + let out = rusqlite::types::ToSqlOutput::Owned(val); + Ok(out) } } -impl<'r> sqlx::Decode<'r, sqlx::Sqlite> for MuteDuration { - fn decode(value: sqlx::sqlite::SqliteValueRef<'r>) -> Result { - let value: i64 = sqlx::Decode::decode(value)?; +impl rusqlite::types::FromSql for MuteDuration { + fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult { // Negative values other than -1 should not be in the // database. If found they'll be NotMuted. - match value { + match i64::column_result(value)? { 0 => Ok(MuteDuration::NotMuted), -1 => Ok(MuteDuration::Forever), n if n > 0 => match SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(n as u64)) { Some(t) => Ok(MuteDuration::Until(t)), - None => Err(Box::new(sqlx::error::Error::Decode(Box::new( - crate::error::OutOfRangeError, - )))), + None => Err(rusqlite::types::FromSqlError::OutOfRange(n)), }, _ => Ok(MuteDuration::NotMuted), } @@ -2485,9 +2465,8 @@ pub async fn set_muted( if context .sql .execute( - sqlx::query("UPDATE chats SET muted_until=? WHERE id=?;") - .bind(duration) - .bind(chat_id), + "UPDATE chats SET muted_until=? WHERE id=?;", + paramsv![duration, chat_id], ) .await .is_ok() @@ -2579,7 +2558,10 @@ async fn set_group_explicitly_left(context: &Context, grpid: impl AsRef) -> if !is_group_explicitly_left(context, grpid.as_ref()).await? { context .sql - .execute(sqlx::query("INSERT INTO leftgrps (grpid) VALUES(?);").bind(grpid.as_ref())) + .execute( + "INSERT INTO leftgrps (grpid) VALUES(?);", + paramsv![grpid.as_ref().to_string()], + ) .await?; } @@ -2592,7 +2574,10 @@ pub(crate) async fn is_group_explicitly_left( ) -> Result { let exists = context .sql - .exists(sqlx::query("SELECT COUNT(*) FROM leftgrps WHERE grpid=?;").bind(grpid.as_ref())) + .exists( + "SELECT COUNT(*) FROM leftgrps WHERE grpid=?;", + paramsv![grpid.as_ref()], + ) .await?; Ok(exists) } @@ -2625,9 +2610,8 @@ pub async fn set_chat_name( if context .sql .execute( - sqlx::query("UPDATE chats SET name=? WHERE id=?;") - .bind(new_name.to_string()) - .bind(chat_id), + "UPDATE chats SET name=? WHERE id=?;", + paramsv![new_name.to_string(), chat_id], ) .await .is_ok() @@ -2745,20 +2729,21 @@ pub async fn forward_msgs( if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await { ensure!(chat.can_send(), "cannot send to {}", chat_id); curr_timestamp = dc_create_smeared_timestamps(context, msg_ids.len()).await; - let q = format!( - "SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id", - msg_ids.iter().map(|_| "?").join(",") - ); - let mut query = sqlx::query(&q); - for v in msg_ids { - query = query.bind(v); - } + let ids = context + .sql + .query_map( + format!( + "SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id", + msg_ids.iter().map(|_| "?").join(",") + ), + msg_ids.iter().map(|v| v as &dyn crate::ToSql).collect(), + |row| row.get::<_, MsgId>(0), + |ids| ids.collect::, _>>().map_err(Into::into), + ) + .await?; - let mut rows = context.sql.fetch(query).await?; - - while let Some(row) = rows.next().await { - let row = row?; - let src_msg_id: MsgId = row.try_get(0)?; + for id in ids { + let src_msg_id: MsgId = id; let msg = Message::load_from_db(context, src_msg_id).await; if msg.is_err() { break; @@ -2826,7 +2811,10 @@ pub(crate) async fn get_chat_contact_cnt( ) -> Result { let count = context .sql - .count(sqlx::query("SELECT COUNT(*) FROM chats_contacts WHERE chat_id=?;").bind(chat_id)) + .count( + "SELECT COUNT(*) FROM chats_contacts WHERE chat_id=?;", + paramsv![chat_id], + ) .await?; Ok(count as usize) } @@ -2836,9 +2824,10 @@ pub(crate) async fn get_chat_cnt(context: &Context) -> Result { // no database, no chats - this is no error (needed eg. for information) let count = context .sql - .count(sqlx::query( + .count( "SELECT COUNT(*) FROM chats WHERE id>9 AND blocked=0;", - )) + paramsv![], + ) .await?; Ok(count as usize) } else { @@ -2851,23 +2840,22 @@ pub(crate) async fn get_chat_id_by_grpid( context: &Context, grpid: impl AsRef, ) -> Result<(ChatId, bool, Blocked), sql::Error> { - let (chat_id, b, p) = context + context .sql - .fetch_one( - sqlx::query("SELECT id, blocked, protected FROM chats WHERE grpid=?;") - .bind(grpid.as_ref()), + .query_row( + "SELECT id, blocked, protected FROM chats WHERE grpid=?;", + paramsv![grpid.as_ref()], + |row| { + let chat_id = row.get::<_, ChatId>(0)?; + + let b = row.get::<_, Option>(1)?.unwrap_or_default(); + let p = row + .get::<_, Option>(2)? + .unwrap_or_default(); + Ok((chat_id, p == ProtectionStatus::Protected, b)) + }, ) .await - .and_then(|row| { - Ok(( - row.try_get(0)?, - row.try_get::, _>(1)?.unwrap_or_default(), - row.try_get::, _>(2)? - .unwrap_or_default(), - )) - })?; - - Ok((chat_id, p == ProtectionStatus::Protected, b)) } /// Adds a message to device chat. @@ -2912,7 +2900,8 @@ pub async fn add_device_msg_with_importance( if let Some(last_msg_time) = context .sql .query_get_value( - sqlx::query("SELECT MAX(timestamp) FROM msgs WHERE chat_id=?").bind(chat_id), + "SELECT MAX(timestamp) FROM msgs WHERE chat_id=?", + paramsv![chat_id], ) .await? { @@ -2924,8 +2913,7 @@ pub async fn add_device_msg_with_importance( let row_id = context .sql .insert( - sqlx::query( - "INSERT INTO msgs ( + "INSERT INTO msgs ( chat_id, from_id, to_id, @@ -2937,21 +2925,19 @@ pub async fn add_device_msg_with_importance( param, rfc724_mid) VALUES (?,?,?,?,?,?,?,?,?,?,?);", - ) - .bind(chat_id) - .bind(DC_CONTACT_ID_DEVICE as i32) - .bind(DC_CONTACT_ID_SELF as i32) - .bind(timestamp_sort) - .bind(timestamp_sent) - .bind(timestamp_sent) - .bind( - // timestamp_sent equals timestamp_rcvd + paramsv![ + chat_id, + DC_CONTACT_ID_DEVICE, + DC_CONTACT_ID_SELF, + timestamp_sort, + timestamp_sent, + timestamp_sent, // timestamp_sent equals timestamp_rcvd msg.viewtype, - ) - .bind(MessageState::InFresh) - .bind(msg.text.as_ref().cloned().unwrap_or_default()) - .bind(msg.param.to_string()) - .bind(&rfc724_mid), + MessageState::InFresh, + msg.text.as_ref().cloned().unwrap_or_default(), + msg.param.to_string(), + rfc724_mid, + ], ) .await?; @@ -2961,7 +2947,10 @@ pub async fn add_device_msg_with_importance( if let Some(label) = label { context .sql - .execute(sqlx::query("INSERT INTO devmsglabels (label) VALUES (?);").bind(label)) + .execute( + "INSERT INTO devmsglabels (label) VALUES (?);", + paramsv![label.to_string()], + ) .await?; } @@ -2988,7 +2977,10 @@ pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result ensure!(!label.is_empty(), "empty label"); let exists = context .sql - .exists(sqlx::query("SELECT COUNT(label) FROM devmsglabels WHERE label=?").bind(label)) + .exists( + "SELECT COUNT(label) FROM devmsglabels WHERE label=?", + paramsv![label], + ) .await?; Ok(exists) @@ -3002,11 +2994,14 @@ pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Result<(), Error> { context .sql - .execute(sqlx::query("DELETE FROM msgs WHERE from_id=?;").bind(DC_CONTACT_ID_DEVICE as i32)) + .execute( + "DELETE FROM msgs WHERE from_id=?;", + paramsv![DC_CONTACT_ID_DEVICE], + ) .await?; context .sql - .execute(sqlx::query("DELETE FROM devmsglabels;")) + .execute("DELETE FROM devmsglabels;", paramsv![]) .await?; Ok(()) } @@ -3030,20 +3025,22 @@ pub(crate) async fn add_info_msg_with_cmd( let row_id = context.sql.insert( - sqlx::query("INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,rfc724_mid,ephemeral_timer, param) VALUES (?,?,?, ?,?,?, ?,?,?, ?);") - .bind(chat_id) - .bind(DC_CONTACT_ID_INFO as i32) - .bind(DC_CONTACT_ID_INFO as i32) - .bind(dc_create_smeared_timestamp(context).await) - .bind(Viewtype::Text) - .bind(MessageState::InNoticed) - .bind(text.as_ref().to_string()) - .bind(&rfc724_mid) - .bind(ephemeral_timer) - .bind(param.to_string()) + "INSERT INTO msgs (chat_id,from_id,to_id,timestamp,type,state,txt,rfc724_mid,ephemeral_timer, param) VALUES (?,?,?, ?,?,?, ?,?,?, ?);", + paramsv![ + chat_id, + DC_CONTACT_ID_INFO, + DC_CONTACT_ID_INFO, + dc_create_smeared_timestamp(context).await, + Viewtype::Text, + MessageState::InNoticed, + text.as_ref().to_string(), + rfc724_mid, + ephemeral_timer, + param.to_string(), + ] ).await?; - let msg_id = MsgId::new(u32::try_from(row_id)?); + let msg_id = MsgId::new(row_id.try_into()?); context.emit_event(EventType::MsgsChanged { chat_id, msg_id }); Ok(msg_id) } diff --git a/src/chatlist.rs b/src/chatlist.rs index 6cdc4d241..325bba45a 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -1,8 +1,6 @@ //! # Chat list module use anyhow::{bail, ensure, Result}; -use async_std::prelude::*; -use sqlx::Row; use crate::chat; use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility}; @@ -121,13 +119,17 @@ impl Chatlist { ChatId::new(0) }; - let process_row = |row: sqlx::Result| { - let row = row?; - let chat_id: ChatId = row.try_get(0)?; - let msg_id: MsgId = row.try_get(1).unwrap_or_default(); + let process_row = |row: &rusqlite::Row| { + let chat_id: ChatId = row.get(0)?; + let msg_id: MsgId = row.get(1).unwrap_or_default(); Ok((chat_id, msg_id)) }; + let process_rows = |rows: rusqlite::MappedRows<_>| { + rows.collect::, _>>() + .map_err(Into::into) + }; + // select with left join and minimum: // // - the inner select must use `hidden` and _not_ `m.hidden` @@ -143,10 +145,10 @@ impl Chatlist { // tg do the same) for the deaddrop, however, they should // really be hidden, however, _currently_ the deaddrop is not // shown at all permanent in the chatlist. - let mut ids: Vec<_> = if let Some(query_contact_id) = query_contact_id { + let mut ids = if let Some(query_contact_id) = query_contact_id { // show chats shared with a given contact - context.sql.fetch( - sqlx::query("SELECT c.id, m.id + context.sql.query_map( + "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m ON c.id=m.chat_id @@ -160,9 +162,11 @@ impl Chatlist { AND c.blocked=0 AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2) GROUP BY c.id - ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;" - ).bind(MessageState::OutDraft).bind(query_contact_id).bind(ChatVisibility::Pinned) - ).await?.map(process_row).collect::>().await? + ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", + paramsv![MessageState::OutDraft, query_contact_id as i32, ChatVisibility::Pinned], + process_row, + process_rows, + ).await? } else if flag_archived_only { // show archived chats // (this includes the archived device-chat; we could skip it, @@ -170,9 +174,8 @@ impl Chatlist { // and adapting the number requires larger refactorings and seems not to be worth the effort) context .sql - .fetch( - sqlx::query( - "SELECT c.id, m.id + .query_map( + "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m ON c.id=m.chat_id @@ -187,13 +190,11 @@ impl Chatlist { AND c.archived=1 GROUP BY c.id ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", - ) - .bind(MessageState::OutDraft), + paramsv![MessageState::OutDraft], + process_row, + process_rows, ) .await? - .map(process_row) - .collect::>() - .await? } else if let Some(query) = query { let query = query.trim().to_string(); ensure!(!query.is_empty(), "missing query"); @@ -207,9 +208,8 @@ impl Chatlist { let str_like_cmd = format!("%{}%", query); context .sql - .fetch( - sqlx::query( - "SELECT c.id, m.id + .query_map( + "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m ON c.id=m.chat_id @@ -224,15 +224,11 @@ impl Chatlist { AND c.name LIKE ?3 GROUP BY c.id ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", - ) - .bind(MessageState::OutDraft) - .bind(skip_id) - .bind(str_like_cmd), + paramsv![MessageState::OutDraft, skip_id, str_like_cmd], + process_row, + process_rows, ) .await? - .map(process_row) - .collect::>() - .await? } else { // show normal chatlist let sort_id_up = if flag_for_forwarding { @@ -243,8 +239,7 @@ impl Chatlist { } else { ChatId::new(0) }; - - let mut ids: Vec<_> = context.sql.fetch(sqlx::query( + let mut ids = context.sql.query_map( "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m @@ -259,15 +254,11 @@ impl Chatlist { AND c.blocked=0 AND NOT c.archived=?3 GROUP BY c.id - ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;" - ) - .bind(MessageState::OutDraft) - .bind(skip_id) - .bind(ChatVisibility::Archived) - .bind(sort_id_up) - .bind(ChatVisibility::Pinned) - ).await?.map(process_row).collect::>().await?; - + ORDER BY c.id=?4 DESC, c.archived=?5 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;", + paramsv![MessageState::OutDraft, skip_id, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned], + process_row, + process_rows, + ).await?; if !flag_no_specials { if let Some(last_deaddrop_fresh_msg_id) = get_last_deaddrop_fresh_msg(context).await? @@ -410,9 +401,10 @@ impl Chatlist { pub async fn dc_get_archived_cnt(context: &Context) -> Result { let count = context .sql - .count(sqlx::query( + .count( "SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;", - )) + paramsv![], + ) .await?; Ok(count) } @@ -422,16 +414,19 @@ async fn get_last_deaddrop_fresh_msg(context: &Context) -> Result> // sufficient as there are typically only few fresh messages. let id = context .sql - .query_get_value(sqlx::query(concat!( - "SELECT m.id", - " FROM msgs m", - " LEFT JOIN chats c", - " ON c.id=m.chat_id", - " WHERE m.state=10", - " AND m.hidden=0", - " AND c.blocked=2", - " ORDER BY m.timestamp DESC, m.id DESC;" - ))) + .query_get_value( + concat!( + "SELECT m.id", + " FROM msgs m", + " LEFT JOIN chats c", + " ON c.id=m.chat_id", + " WHERE m.state=10", + " AND m.hidden=0", + " AND c.blocked=2", + " ORDER BY m.timestamp DESC, m.id DESC;" + ), + paramsv![], + ) .await?; Ok(id) } diff --git a/src/config.rs b/src/config.rs index 69824110f..67bcf4ff7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -242,7 +242,7 @@ impl Context { match key { Config::Selfavatar => { self.sql - .execute(sqlx::query("UPDATE contacts SET selfavatar_sent=0;")) + .execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![]) .await?; self.sql .set_raw_config_bool("attach_selfavatar", true) diff --git a/src/constants.rs b/src/constants.rs index 27a0e691f..a1ec8260c 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,4 +1,5 @@ //! # Constants +use deltachat_derive::{FromSql, ToSql}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -15,9 +16,10 @@ pub static DC_VERSION_STR: Lazy = Lazy::new(|| env!("CARGO_PKG_VERSION") Eq, FromPrimitive, ToPrimitive, + FromSql, + ToSql, Serialize, Deserialize, - sqlx::Type, )] #[repr(i8)] pub enum Blocked { @@ -32,7 +34,9 @@ impl Default for Blocked { } } -#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] +#[derive( + Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql, +)] #[repr(u8)] pub enum ShowEmails { Off = 0, @@ -46,7 +50,9 @@ impl Default for ShowEmails { } } -#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] +#[derive( + Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql, +)] #[repr(u8)] pub enum MediaQuality { Balanced = 0, @@ -59,7 +65,9 @@ impl Default for MediaQuality { } } -#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] +#[derive( + Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql, +)] #[repr(u8)] pub enum KeyGenType { Default = 0, @@ -73,7 +81,9 @@ impl Default for KeyGenType { } } -#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] +#[derive( + Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql, +)] #[repr(i8)] pub enum VideochatType { Unknown = 0, @@ -133,10 +143,11 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9); Eq, FromPrimitive, ToPrimitive, + FromSql, + ToSql, IntoStaticStr, Serialize, Deserialize, - sqlx::Type, )] #[repr(u32)] pub enum Chattype { @@ -247,9 +258,10 @@ pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50; Eq, FromPrimitive, ToPrimitive, + FromSql, + ToSql, Serialize, Deserialize, - sqlx::Type, )] #[repr(u32)] pub enum Viewtype { diff --git a/src/contact.rs b/src/contact.rs index d27f231f9..0fc1a4d9e 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -1,13 +1,13 @@ //! Contacts module -use std::convert::TryFrom; + +use std::convert::{TryFrom, TryInto}; use anyhow::{bail, ensure, format_err, Result}; use async_std::path::PathBuf; -use async_std::prelude::*; +use deltachat_derive::{FromSql, ToSql}; use itertools::Itertools; use once_cell::sync::Lazy; use regex::Regex; -use sqlx::Row; use crate::aheader::EncryptPreference; use crate::chat::ChatId; @@ -79,7 +79,7 @@ pub struct Contact { /// Possible origins of a contact. #[derive( - Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, sqlx::Type, + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, FromPrimitive, ToPrimitive, FromSql, ToSql, )] #[repr(u32)] pub enum Origin { @@ -176,29 +176,35 @@ pub enum VerifiedStatus { impl Contact { pub async fn load_from_db(context: &Context, contact_id: u32) -> crate::sql::Result { - let row = context + let mut contact = context .sql - .fetch_one( - sqlx::query( - "SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status + .query_row( + "SELECT c.name, c.addr, c.origin, c.blocked, c.authname, c.param, c.status FROM contacts c WHERE c.id=?;", - ) - .bind(contact_id), + paramsv![contact_id as i32], + |row| { + let name: String = row.get(0)?; + let addr: String = row.get(1)?; + let origin: Origin = row.get(2)?; + let blocked: Option = row.get(3)?; + let authname: String = row.get(4)?; + let param: String = row.get(5)?; + let status: Option = row.get(6)?; + let contact = Self { + id: contact_id, + name, + authname, + addr, + blocked: blocked.unwrap_or_default(), + origin, + param: param.parse().unwrap_or_default(), + status: status.unwrap_or_default(), + }; + Ok(contact) + }, ) .await?; - - let mut contact = Contact { - id: contact_id, - name: row.try_get(0)?, - authname: row.try_get(4)?, - addr: row.try_get(1)?, - blocked: row.try_get::, _>(3)?.unwrap_or_default() != 0, - origin: row.try_get(2)?, - param: row.try_get::(5)?.parse().unwrap_or_default(), - status: row.try_get::, _>(6)?.unwrap_or_default(), - }; - if contact_id == DC_CONTACT_ID_SELF { contact.name = stock_str::self_msg(context).await; contact.addr = context @@ -213,7 +219,6 @@ impl Contact { contact.name = stock_str::device_messages(context).await; contact.addr = DC_CONTACT_ID_DEVICE_ADDR.to_string(); } - Ok(contact) } @@ -285,10 +290,8 @@ impl Contact { if context .sql .execute( - sqlx::query("UPDATE msgs SET state=? WHERE from_id=? AND state=?;") - .bind(MessageState::InNoticed) - .bind(id as i32) - .bind(MessageState::InFresh), + "UPDATE msgs SET state=? WHERE from_id=? AND state=?;", + paramsv![MessageState::InNoticed, id as i32, MessageState::InFresh], ) .await .is_ok() @@ -322,18 +325,16 @@ impl Contact { let id = context .sql .query_get_value( - sqlx::query( - "SELECT id FROM contacts \ - WHERE addr=?1 COLLATE NOCASE \ - AND id>?2 AND origin>=?3 AND blocked=0;", - ) - .bind(addr_normalized) - .bind(DC_CONTACT_ID_LAST_SPECIAL) - .bind(min_origin), + "SELECT id FROM contacts \ + WHERE addr=?1 COLLATE NOCASE \ + AND id>?2 AND origin>=?3 AND blocked=0;", + paramsv![ + addr_normalized, + DC_CONTACT_ID_LAST_SPECIAL as i32, + min_origin as u32, + ], ) - .await? - .unwrap_or_default(); - + .await?; Ok(id) } @@ -433,23 +434,21 @@ impl Contact { if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context .sql - .fetch_one( - sqlx::query( - "SELECT id, name, addr, origin, authname \ - FROM contacts WHERE addr=? COLLATE NOCASE;", - ) - .bind(addr.to_string()), + .query_row( + "SELECT id, name, addr, origin, authname \ + FROM contacts WHERE addr=? COLLATE NOCASE;", + paramsv![addr.to_string()], + |row| { + let row_id: isize = row.get(0)?; + let row_name: String = row.get(1)?; + let row_addr: String = row.get(2)?; + let row_origin: Origin = row.get(3)?; + let row_authname: String = row.get(4)?; + + Ok((row_id, row_name, row_addr, row_origin, row_authname)) + }, ) .await - .and_then(|row| { - let row_id = row.try_get(0)?; - let row_name: String = row.try_get(1)?; - let row_addr: String = row.try_get(2)?; - let row_origin: Origin = row.try_get(3)?; - let row_authname: String = row.try_get(4)?; - - Ok((row_id, row_name, row_addr, row_origin, row_authname)) - }) { let update_name = manual && name != row_name; let update_authname = !manual @@ -458,7 +457,8 @@ impl Contact { && (origin >= row_origin || origin == Origin::IncomingUnknownFrom || row_authname.is_empty()); - row_id = id; + + row_id = u32::try_from(id)?; if origin as i32 >= row_origin as i32 && addr != row_addr { update_addr = true; } @@ -469,36 +469,39 @@ impl Contact { row_name }; - let query = sqlx::query( - "UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;", - ) - .bind(&new_name) - .bind(if update_addr { - addr.to_string() - } else { - row_addr - }) - .bind(if origin > row_origin { - origin - } else { - row_origin - }) - .bind(if update_authname { - name.to_string() - } else { - row_authname - }) - .bind(row_id); - - context.sql.execute(query).await.ok(); + context + .sql + .execute( + "UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;", + paramsv![ + new_name, + if update_addr { + addr.to_string() + } else { + row_addr + }, + if origin > row_origin { + origin + } else { + row_origin + }, + if update_authname { + name.to_string() + } else { + row_authname + }, + row_id + ], + ) + .await + .ok(); if update_name { // Update the contact name also if it is used as a group name. // This is one of the few duplicated data, however, getting the chat list is easier this way. - let chat_id = context.sql.query_get_value::<_, u32>( - sqlx::query( - "SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)" - ).bind(Chattype::Single).bind(row_id) + let chat_id = context.sql.query_get_value::( + "SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)", + paramsv![Chattype::Single, isize::try_from(row_id)?] ).await?; if let Some(chat_id) = chat_id { let contact = Contact::get_by_id(context, row_id as u32).await?; @@ -506,10 +509,8 @@ impl Contact { match context .sql .execute( - sqlx::query("UPDATE chats SET name=?1 WHERE id=?2 AND name!=?3") - .bind(&chat_name) - .bind(chat_id) - .bind(&chat_name), + "UPDATE chats SET name=?1 WHERE id=?2 AND name!=?3", + paramsv![chat_name, chat_id, chat_name], ) .await { @@ -517,8 +518,9 @@ impl Contact { Ok(count) => { if count > 0 { // Chat name updated - context - .emit_event(EventType::ChatModified(ChatId::new(chat_id))); + context.emit_event(EventType::ChatModified(ChatId::new( + chat_id.try_into()?, + ))); } } } @@ -533,25 +535,25 @@ impl Contact { if let Ok(new_row_id) = context .sql .insert( - sqlx::query( - "INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);", - ) - .bind(if update_name { - name.to_string() - } else { - "".to_string() - }) - .bind(&addr) - .bind(origin) - .bind(if update_authname { - name.to_string() - } else { - "".to_string() - }), + "INSERT INTO contacts (name, addr, origin, authname) VALUES(?, ?, ?, ?);", + paramsv![ + if update_name { + name.to_string() + } else { + "".to_string() + }, + addr, + origin, + if update_authname { + name.to_string() + } else { + "".to_string() + } + ], ) .await { - row_id = new_row_id; + row_id = u32::try_from(new_row_id)?; sth_modified = Modifier::Created; info!(context, "added contact id={} addr={}", row_id, &addr); } else { @@ -559,7 +561,7 @@ impl Contact { } } - Ok((u32::try_from(row_id)?, sth_modified)) + Ok((row_id, sth_modified)) } /// Add a number of contacts. @@ -638,12 +640,10 @@ impl Contact { .map(|s| s.as_ref().to_string()) .unwrap_or_default() ); - - let mut rows = context + context .sql - .fetch( - sqlx::query( - "SELECT c.id FROM contacts c \ + .query_map( + "SELECT c.id FROM contacts c \ LEFT JOIN acpeerstates ps ON c.addr=ps.addr \ WHERE c.addr!=?1 \ AND c.id>?2 \ @@ -652,19 +652,23 @@ impl Contact { AND (iif(c.name='',c.authname,c.name) LIKE ?4 OR c.addr LIKE ?5) \ AND (1=?6 OR LENGTH(ps.verified_key_fingerprint)!=0) \ ORDER BY LOWER(iif(c.name='',c.authname,c.name)||c.addr),c.id;", - ) - .bind(&self_addr) - .bind(DC_CONTACT_ID_LAST_SPECIAL) - .bind(Origin::IncomingReplyTo) - .bind(&s3str_like_cmd) - .bind(&s3str_like_cmd) - .bind(if flag_verified_only { 0i32 } else { 1i32 }), + paramsv![ + self_addr, + DC_CONTACT_ID_LAST_SPECIAL as i32, + Origin::IncomingReplyTo, + s3str_like_cmd, + s3str_like_cmd, + if flag_verified_only { 0i32 } else { 1i32 }, + ], + |row| row.get::<_, i32>(0), + |ids| { + for id in ids { + ret.push(id? as u32); + } + Ok(()) + }, ) - .await? - .map(|row| row?.try_get(0)); - while let Some(id) = rows.next().await { - ret.push(id?); - } + .await?; let self_name = context .get_config(Config::Displayname) @@ -685,27 +689,29 @@ impl Contact { } else { add_self = true; - let mut rows = context + context .sql - .fetch( - sqlx::query( - "SELECT id FROM contacts + .query_map( + "SELECT id FROM contacts WHERE addr!=?1 AND id>?2 AND origin>=?3 AND blocked=0 ORDER BY LOWER(iif(name='',authname,name)||addr),id;", - ) - .bind(self_addr) - .bind(DC_CONTACT_ID_LAST_SPECIAL) - .bind(Origin::IncomingReplyTo), + paramsv![ + self_addr, + DC_CONTACT_ID_LAST_SPECIAL as i32, + Origin::IncomingReplyTo + ], + |row| row.get::<_, i32>(0), + |ids| { + for id in ids { + ret.push(id? as u32); + } + Ok(()) + }, ) - .await? - .map(|row| row?.try_get(0)); - - while let Some(id) = rows.next().await { - ret.push(id?); - } + .await?; } if flag_add_self && add_self { @@ -721,38 +727,38 @@ impl Contact { // from the users perspective, // there is not much difference in an email- and a mailinglist-address) async fn update_blocked_mailinglist_contacts(context: &Context) -> Result<()> { - let mut rows = context + let blocked_mailinglists = context .sql - .fetch( - sqlx::query("SELECT name, grpid FROM chats WHERE type=? AND blocked=?;") - .bind(Chattype::Mailinglist) - .bind(Blocked::Manually), + .query_map( + "SELECT name, grpid FROM chats WHERE type=? AND blocked=?;", + paramsv![Chattype::Mailinglist, Blocked::Manually], + |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)), + |rows| { + rows.collect::, _>>() + .map_err(Into::into) + }, ) .await?; - - while let Some(row) = rows.next().await { - let row = row?; - let name = row.try_get::(0)?; - let grpid = row.try_get::(1)?; - + for (name, grpid) in blocked_mailinglists { if !context .sql - .exists(sqlx::query("SELECT COUNT(id) FROM contacts WHERE addr=?;").bind(&grpid)) + .exists( + "SELECT COUNT(id) FROM contacts WHERE addr=?;", + paramsv![grpid], + ) .await? { context .sql - .execute(sqlx::query("INSERT INTO contacts (addr) VALUES (?);").bind(&grpid)) + .execute("INSERT INTO contacts (addr) VALUES (?);", paramsv![grpid]) .await?; } // always do an update in case the blocking is reset or name is changed context .sql .execute( - sqlx::query("UPDATE contacts SET name=?, origin=?, blocked=1 WHERE addr=?;") - .bind(name) - .bind(Origin::MailinglistAddress) - .bind(&grpid), + "UPDATE contacts SET name=?, origin=?, blocked=1 WHERE addr=?;", + paramsv![name, Origin::MailinglistAddress, grpid], ) .await?; } @@ -763,8 +769,8 @@ impl Contact { let count = context .sql .count( - sqlx::query("SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0") - .bind(DC_CONTACT_ID_LAST_SPECIAL), + "SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0", + paramsv![DC_CONTACT_ID_LAST_SPECIAL], ) .await?; Ok(count as usize) @@ -781,16 +787,16 @@ impl Contact { let list = context .sql - .fetch( - sqlx::query( + .query_map( "SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(iif(name='',authname,name)||addr),id;", - ).bind(DC_CONTACT_ID_LAST_SPECIAL) + paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32], + |row| row.get::<_, u32>(0), + |ids| { + ids.collect::, _>>() + .map_err(Into::into) + }, ) - .await? - .map(|row| row?.try_get::(0)) - .collect::>>() .await?; - Ok(list) } @@ -877,8 +883,8 @@ impl Contact { let count_contacts = context .sql .count( - sqlx::query("SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;") - .bind(contact_id), + "SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;", + paramsv![contact_id as i32], ) .await?; @@ -886,9 +892,8 @@ impl Contact { context .sql .count( - sqlx::query("SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;") - .bind(contact_id) - .bind(contact_id), + "SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;", + paramsv![contact_id as i32, contact_id as i32], ) .await? } else { @@ -898,7 +903,10 @@ impl Contact { if count_msgs == 0 { match context .sql - .execute(sqlx::query("DELETE FROM contacts WHERE id=?;").bind(contact_id as i32)) + .execute( + "DELETE FROM contacts WHERE id=?;", + paramsv![contact_id as i32], + ) .await { Ok(_) => { @@ -935,9 +943,8 @@ impl Contact { context .sql .execute( - sqlx::query("UPDATE contacts SET param=? WHERE id=?") - .bind(self.param.to_string()) - .bind(self.id as i32), + "UPDATE contacts SET param=? WHERE id=?", + paramsv![self.param.to_string(), self.id as i32], ) .await?; Ok(()) @@ -948,9 +955,8 @@ impl Contact { context .sql .execute( - sqlx::query("UPDATE contacts SET status=? WHERE id=?") - .bind(&self.status) - .bind(self.id as i32), + "UPDATE contacts SET status=? WHERE id=?", + paramsv![self.status, self.id as i32], ) .await?; Ok(()) @@ -1121,8 +1127,8 @@ impl Contact { let count = context .sql .count( - sqlx::query("SELECT COUNT(*) FROM contacts WHERE id>?;") - .bind(DC_CONTACT_ID_LAST_SPECIAL), + "SELECT COUNT(*) FROM contacts WHERE id>?;", + paramsv![DC_CONTACT_ID_LAST_SPECIAL as i32], ) .await?; Ok(count) @@ -1135,7 +1141,10 @@ impl Contact { context .sql - .exists(sqlx::query("SELECT COUNT(*) FROM contacts WHERE id=?;").bind(contact_id)) + .exists( + "SELECT COUNT(*) FROM contacts WHERE id=?;", + paramsv![contact_id as i32], + ) .await .unwrap_or_default() } @@ -1144,10 +1153,8 @@ impl Contact { context .sql .execute( - sqlx::query("UPDATE contacts SET origin=? WHERE id=? AND origin BTreeMap<&'static str, String> { let mut res = BTreeMap::new(); res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR)); - res.insert("sqlite_version", crate::sql::version().to_string()); + res.insert("sqlite_version", rusqlite::version().to_string()); res.insert("arch", (std::mem::size_of::() * 8).to_string()); res.insert("num_cpus", num_cpus::get().to_string()); res.insert("level", "awesome".into()); @@ -290,7 +288,7 @@ impl Context { .unwrap_or_default(); let journal_mode = self .sql - .query_get_value(sqlx::query("PRAGMA journal_mode;")) + .query_get_value("PRAGMA journal_mode;", paramsv![]) .await? .unwrap_or_else(|| "unknown".to_string()); let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?; @@ -299,12 +297,12 @@ impl Context { let prv_key_cnt = self .sql - .count(sqlx::query("SELECT COUNT(*) FROM keypairs;")) + .count("SELECT COUNT(*) FROM keypairs;", paramsv![]) .await?; let pub_key_cnt = self .sql - .count(sqlx::query("SELECT COUNT(*) FROM acpeerstates;")) + .count("SELECT COUNT(*) FROM acpeerstates;", paramsv![]) .await?; let fingerprint_str = match SignedPublicKey::load_self(self).await { Ok(key) => key.fingerprint().hex(), @@ -431,8 +429,8 @@ impl Context { pub async fn get_fresh_msgs(&self) -> Result> { let list = self .sql - .fetch( - sqlx::query(concat!( + .query_map( + concat!( "SELECT m.id", " FROM msgs m", " LEFT JOIN contacts ct", @@ -446,13 +444,17 @@ impl Context { " AND c.blocked=0", " AND NOT(c.muted_until=-1 OR c.muted_until>?)", " ORDER BY m.timestamp DESC,m.id DESC;" - )) - .bind(MessageState::InFresh) - .bind(time()), + ), + paramsv![MessageState::InFresh, time()], + |row| row.get::<_, MsgId>(0), + |rows| { + let mut list = Vec::new(); + for row in rows { + list.push(row?); + } + Ok(list) + }, ) - .await? - .map(|row| row?.try_get("id")) - .collect::>() .await?; Ok(list) } @@ -472,11 +474,24 @@ impl Context { } let str_like_in_text = format!("%{}%", real_query); + let do_query = |query, params| { + self.sql.query_map( + query, + params, + |row| row.get::<_, MsgId>("id"), + |rows| { + let mut ret = Vec::new(); + for id in rows { + ret.push(id?); + } + Ok(ret) + }, + ) + }; + let list = if let Some(chat_id) = chat_id { - self.sql - .fetch( - sqlx::query( - "SELECT m.id AS id, m.timestamp AS timestamp + do_query( + "SELECT m.id AS id, m.timestamp AS timestamp FROM msgs m LEFT JOIN contacts ct ON m.from_id=ct.id @@ -485,18 +500,9 @@ impl Context { AND ct.blocked=0 AND txt LIKE ? ORDER BY m.timestamp,m.id;", - ) - .bind(chat_id) - .bind(str_like_in_text), - ) - .await? - .map(|row| { - let row = row?; - let id = row.try_get::("id")?; - Ok(id) - }) - .collect::>>() - .await? + paramsv![chat_id, str_like_in_text], + ) + .await? } else { // For performance reasons results are sorted only by `id`, that is in the order of // message reception. @@ -508,10 +514,8 @@ impl Context { // of unwanted results that are discarded moments later, we added `LIMIT 1000`. // According to some tests, this limit speeds up eg. 2 character searches by factor 10. // The limit is documented and UI may add a hint when getting 1000 results. - self.sql - .fetch( - sqlx::query( - "SELECT m.id AS id, m.timestamp AS timestamp + do_query( + "SELECT m.id AS id, m.timestamp AS timestamp FROM msgs m LEFT JOIN contacts ct ON m.from_id=ct.id @@ -523,17 +527,9 @@ impl Context { AND ct.blocked=0 AND m.txt LIKE ? ORDER BY m.id DESC LIMIT 1000", - ) - .bind(str_like_in_text), - ) - .await? - .map(|row| { - let row = row?; - let id = row.try_get::("id")?; - Ok(id) - }) - .collect::>>() - .await? + paramsv![str_like_in_text], + ) + .await? }; Ok(list) @@ -747,9 +743,8 @@ mod tests { // we need to modify the database directly t.sql .execute( - sqlx::query("UPDATE chats SET muted_until=? WHERE id=?;") - .bind(time() - 3600) - .bind(bob.id), + "UPDATE chats SET muted_until=? WHERE id=?;", + paramsv![time() - 3600, bob.id], ) .await .unwrap(); @@ -766,7 +761,10 @@ mod tests { // to test get_fresh_msgs() with invalid mute_until (everything < -1), // that results in "muted forever" by definition. t.sql - .execute(sqlx::query("UPDATE chats SET muted_until=-2 WHERE id=?;").bind(bob.id)) + .execute( + "UPDATE chats SET muted_until=-2 WHERE id=?;", + paramsv![bob.id], + ) .await .unwrap(); let bob = Chat::load_from_db(&t, bob.id).await.unwrap(); diff --git a/src/dc_receive_imf.rs b/src/dc_receive_imf.rs index e8933f58c..662280a94 100644 --- a/src/dc_receive_imf.rs +++ b/src/dc_receive_imf.rs @@ -1,14 +1,12 @@ use std::convert::TryFrom; use anyhow::{bail, ensure, format_err, Result}; -use async_std::prelude::*; use itertools::join; use mailparse::SingleInfo; use num_traits::FromPrimitive; use once_cell::sync::Lazy; use regex::Regex; use sha2::{Digest, Sha256}; -use sqlx::Row; use crate::chat::{self, Chat, ChatId, ProtectionStatus}; use crate::config::Config; @@ -915,7 +913,8 @@ async fn add_parts( let subject = mime_parser.get_subject().unwrap_or_default(); - let server_folder = server_folder.as_ref(); + let mut parts = std::mem::replace(&mut mime_parser.parts, Vec::new()); + let server_folder = server_folder.as_ref().to_string(); let is_system_message = mime_parser.is_system_message; // if indicated by the parser, @@ -927,59 +926,30 @@ async fn add_parts( let mime_headers = if save_mime_headers || save_mime_modified { if mime_parser.was_encrypted() && !mime_parser.decoded_data.is_empty() { - String::from_utf8_lossy(&mime_parser.decoded_data) + String::from_utf8_lossy(&mime_parser.decoded_data).to_string() } else { - String::from_utf8_lossy(imf_raw) + String::from_utf8_lossy(imf_raw).to_string() } } else { "".into() }; - for part in &mut mime_parser.parts { - let mut txt_raw = "".to_string(); + let sent_timestamp = *sent_timestamp; + let is_hidden = *hidden; + let chat_id = *chat_id; - let is_location_kml = - location_kml_is && icnt == 1 && (part.msg == "-location-" || part.msg.is_empty()); + // TODO: can this clone be avoided? + let rfc724_mid = rfc724_mid.to_string(); - if is_mdn || is_location_kml { - *hidden = true; - if incoming { - // Set the state to InSeen so that precheck_imf() adds a markseen job after we moved the message - state = MessageState::InSeen; - } - } + let (new_parts, ids, is_hidden) = context + .sql + .with_conn(move |conn| { + let mut ids = Vec::with_capacity(parts.len()); + let mut is_hidden = is_hidden; - let mime_modified = save_mime_modified && !part.msg.is_empty(); - if mime_modified { - // Avoid setting mime_modified for more than one part. - save_mime_modified = false; - } - - if part.typ == Viewtype::Text { - let msg_raw = part.msg_raw.as_ref().cloned().unwrap_or_default(); - txt_raw = format!("{}\n\n{}", subject, msg_raw); - } - if is_system_message != SystemMessage::Unknown { - part.param.set_int(Param::Cmd, is_system_message as i32); - } - - let ephemeral_timestamp = if in_fresh { - 0 - } else { - match ephemeral_timer { - EphemeralTimer::Disabled => 0, - EphemeralTimer::Enabled { duration } => rcvd_timestamp + i64::from(duration), - } - }; - - // If you change which information is skipped if the message is trashed, - // also change `MsgId::trash()` and `delete_expired_messages()` - let trash = chat_id.is_trash(); - - let row_id = context - .sql - .insert( - sqlx::query( + for part in &mut parts { + let mut txt_raw = "".to_string(); + let mut stmt = conn.prepare_cached( r#" INSERT INTO msgs ( @@ -1001,53 +971,105 @@ INSERT INTO msgs ? ); "#, - ) - .bind(rfc724_mid) - .bind(server_folder) - .bind(server_uid as i32) - .bind(*chat_id) - .bind(if trash { 0 } else { from_id as i32 }) - .bind(if trash { 0 } else { to_id as i32 }) - .bind(sort_timestamp) - .bind(*sent_timestamp) - .bind(rcvd_timestamp) - .bind(part.typ) - .bind(state) - .bind(is_dc_message) - .bind(if trash { "" } else { &part.msg }) - .bind(if trash { "" } else { &subject }) - // txt_raw might contain invalid utf8 - .bind(if trash { "" } else { &txt_raw }) - .bind(if trash { - "".to_string() - } else { - part.param.to_string() - }) - .bind(part.bytes as i64) - .bind(*hidden) - .bind(if (save_mime_headers || mime_modified) && !trash { - mime_headers.to_string() - } else { - "".to_string() - }) - .bind(&mime_in_reply_to) - .bind(&mime_references) - .bind(&mime_modified) - .bind(part.error.take().unwrap_or_default()) - .bind(ephemeral_timer) - .bind(ephemeral_timestamp), - ) - .await?; - let msg_id = MsgId::new(u32::try_from(row_id)?); + )?; - created_db_entries.push((*chat_id, msg_id)); - *insert_msg_id = msg_id; + let is_location_kml = location_kml_is + && icnt == 1 + && (part.msg == "-location-" || part.msg.is_empty()); + + if is_mdn || is_location_kml { + is_hidden = true; + if incoming { + state = MessageState::InSeen; // Set the state to InSeen so that precheck_imf() adds a markseen job after we moved the message + } + } + + let mime_modified = save_mime_modified && !part.msg.is_empty(); + if mime_modified { + // Avoid setting mime_modified for more than one part. + save_mime_modified = false; + } + + if part.typ == Viewtype::Text { + let msg_raw = part.msg_raw.as_ref().cloned().unwrap_or_default(); + txt_raw = format!("{}\n\n{}", subject, msg_raw); + } + if is_system_message != SystemMessage::Unknown { + part.param.set_int(Param::Cmd, is_system_message as i32); + } + + let ephemeral_timestamp = if in_fresh { + 0 + } else { + match ephemeral_timer { + EphemeralTimer::Disabled => 0, + EphemeralTimer::Enabled { duration } => { + rcvd_timestamp + i64::from(duration) + } + } + }; + + // If you change which information is skipped if the message is trashed, + // also change `MsgId::trash()` and `delete_expired_messages()` + let trash = chat_id.is_trash(); + + stmt.execute(paramsv![ + rfc724_mid, + server_folder, + server_uid as i32, + chat_id, + if trash { 0 } else { from_id as i32 }, + if trash { 0 } else { to_id as i32 }, + sort_timestamp, + sent_timestamp, + rcvd_timestamp, + part.typ, + state, + is_dc_message, + if trash { "" } else { &part.msg }, + if trash { "" } else { &subject }, + // txt_raw might contain invalid utf8 + if trash { "" } else { &txt_raw }, + if trash { + "".to_string() + } else { + part.param.to_string() + }, + part.bytes as isize, + is_hidden, + if (save_mime_headers || mime_modified) && !trash { + mime_headers.to_string() + } else { + "".to_string() + }, + mime_in_reply_to, + mime_references, + mime_modified, + part.error.take().unwrap_or_default(), + ephemeral_timer, + ephemeral_timestamp + ])?; + let row_id = conn.last_insert_rowid(); + + drop(stmt); + ids.push(MsgId::new(u32::try_from(row_id)?)); + } + Ok((parts, ids, is_hidden)) + }) + .await?; + + if let Some(id) = ids.iter().last() { + *insert_msg_id = *id; } - if !*hidden { + if !is_hidden { chat_id.unarchive(context).await?; } + *hidden = is_hidden; + created_db_entries.extend(ids.iter().map(|id| (chat_id, *id))); + mime_parser.parts = new_parts; + info!( context, "Message has {} parts and is assigned to chat #{}.", icnt, chat_id, @@ -1055,7 +1077,7 @@ INSERT INTO msgs // new outgoing message from another device marks the chat as noticed. if !incoming && !*hidden && !chat_id.is_special() { - chat::marknoticed_chat_if_older_than(context, *chat_id, sort_timestamp).await?; + chat::marknoticed_chat_if_older_than(context, chat_id, sort_timestamp).await?; } // check event to send @@ -1085,7 +1107,7 @@ INSERT INTO msgs Ok(()) } if !is_mdn { - update_last_subject(context, *chat_id, mime_parser) + update_last_subject(context, chat_id, mime_parser) .await .ok_or_log_msg(context, "Could not update LastSubject of chat"); } @@ -1167,9 +1189,8 @@ async fn calc_sort_timestamp( let last_msg_time: Option = context .sql .query_get_value( - sqlx::query("SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND state>?") - .bind(chat_id) - .bind(MessageState::InFresh), + "SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND state>?", + paramsv![chat_id, MessageState::InFresh], ) .await?; @@ -1481,9 +1502,8 @@ async fn create_or_lookup_group( if context .sql .execute( - sqlx::query("UPDATE chats SET name=? WHERE id=?;") - .bind(grpname.to_string()) - .bind(chat_id), + "UPDATE chats SET name=? WHERE id=?;", + paramsv![grpname.to_string(), chat_id], ) .await .is_ok() @@ -1520,7 +1540,10 @@ async fn create_or_lookup_group( // start from scratch. context .sql - .execute(sqlx::query("DELETE FROM chats_contacts WHERE chat_id=?;").bind(chat_id)) + .execute( + "DELETE FROM chats_contacts WHERE chat_id=?;", + paramsv![chat_id], + ) .await .ok(); @@ -1764,14 +1787,15 @@ async fn create_multiuser_record( ) -> Result { let row_id = context.sql.insert( - sqlx::query( - "INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected) VALUES(?, ?, ?, ?, ?, ?);") - .bind(chattype) - .bind(grpname.as_ref()) - .bind(grpid.as_ref()) - .bind(create_blocked) - .bind(time()) - .bind(create_protected) + "INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected) VALUES(?, ?, ?, ?, ?, ?);", + paramsv![ + chattype, + grpname.as_ref(), + grpid.as_ref(), + create_blocked, + time(), + create_protected, + ], ).await?; let chat_id = ChatId::new(u32::try_from(row_id)?); @@ -1805,24 +1829,27 @@ async fn create_adhoc_grp_id(context: &Context, member_ids: &[u32]) -> Result(0)) - .collect::>>() - .await?; - addrs.sort(); - for addr in &addrs { - members += ","; - members += &addr.to_lowercase(); - } - } + let members = context + .sql + .query_map( + format!( + "SELECT addr FROM contacts WHERE id IN({}) AND id!=1", // 1=DC_CONTACT_ID_SELF + member_ids_str + ), + paramsv![], + |row| row.get::<_, String>(0), + |rows| { + let mut addrs = rows.collect::, _>>()?; + addrs.sort(); + let mut acc = member_cs.clone(); + for addr in &addrs { + acc += ","; + acc += &addr.to_lowercase(); + } + Ok(acc) + }, + ) + .await?; Ok(hex_hash(&members)) } @@ -1889,26 +1916,34 @@ async fn check_verified_properties( } let to_ids_str = join(to_ids.iter().map(|x| x.to_string()), ","); - let q = format!( - "SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c \ + let rows = context + .sql + .query_map( + format!( + "SELECT c.addr, LENGTH(ps.verified_key_fingerprint) FROM contacts c \ LEFT JOIN acpeerstates ps ON c.addr=ps.addr WHERE c.id IN({}) ", - to_ids_str - ); - - let mut rows = context.sql.fetch(sqlx::query(&q)).await?; - - while let Some(row) = rows.next().await { - let row = row?; - let to_addr: String = row.try_get(0)?; - let mut is_verified = row.try_get::(1)? != 0; + to_ids_str + ), + paramsv![], + |row| { + let to_addr: String = row.get(0)?; + let is_verified: i32 = row.get(1)?; + Ok((to_addr, is_verified != 0)) + }, + |rows| { + rows.collect::, _>>() + .map_err(Into::into) + }, + ) + .await?; + for (to_addr, mut is_verified) in rows.into_iter() { info!( context, "check_verified_properties: {:?} self={:?}", to_addr, context.is_self_addr(&to_addr).await ); - let peerstate = Peerstate::from_addr(context, &to_addr).await?; // mark gossiped keys (if any) as verified diff --git a/src/dc_tools.rs b/src/dc_tools.rs index 3385dd6a1..9176e8dbf 100644 --- a/src/dc_tools.rs +++ b/src/dc_tools.rs @@ -632,6 +632,14 @@ impl FromStr for EmailAddress { } } +impl rusqlite::types::ToSql for EmailAddress { + fn to_sql(&self) -> rusqlite::Result { + let val = rusqlite::types::Value::Text(self.to_string()); + let out = rusqlite::types::ToSqlOutput::Owned(val); + Ok(out) + } +} + /// Makes sure that a user input that is not supposed to contain newlines does not contain newlines. pub(crate) fn improve_single_line_input(input: impl AsRef) -> String { input diff --git a/src/ephemeral.rs b/src/ephemeral.rs index 80a799875..45b937421 100644 --- a/src/ephemeral.rs +++ b/src/ephemeral.rs @@ -64,7 +64,6 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use anyhow::{ensure, Context as _, Error}; use async_std::task; use serde::{Deserialize, Serialize}; -use sqlx::Row; use crate::constants::{ Viewtype, DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_CONTACT_ID_DEVICE, DC_CONTACT_ID_SELF, @@ -124,41 +123,28 @@ impl FromStr for Timer { } } -impl sqlx::Type for Timer { - fn type_info() -> sqlx::sqlite::SqliteTypeInfo { - >::type_info() - } - - fn compatible(ty: &sqlx::sqlite::SqliteTypeInfo) -> bool { - >::compatible(ty) +impl rusqlite::types::ToSql for Timer { + fn to_sql(&self) -> rusqlite::Result { + let val = rusqlite::types::Value::Integer(match self { + Self::Disabled => 0, + Self::Enabled { duration } => i64::from(*duration), + }); + let out = rusqlite::types::ToSqlOutput::Owned(val); + Ok(out) } } -impl<'q> sqlx::Encode<'q, sqlx::Sqlite> for Timer { - fn encode_by_ref( - &self, - args: &mut Vec>, - ) -> sqlx::encode::IsNull { - args.push(sqlx::sqlite::SqliteArgumentValue::Int64( - self.to_u32() as i64 - )); - - sqlx::encode::IsNull::No - } -} - -impl<'r> sqlx::Decode<'r, sqlx::Sqlite> for Timer { - fn decode(value: sqlx::sqlite::SqliteValueRef<'r>) -> Result { - let value: i64 = sqlx::Decode::decode(value)?; - if value == 0 { - Ok(Self::Disabled) - } else if let Ok(duration) = u32::try_from(value) { - Ok(Self::Enabled { duration }) - } else { - Err(Box::new(sqlx::Error::Decode(Box::new( - crate::error::OutOfRangeError, - )))) - } +impl rusqlite::types::FromSql for Timer { + fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult { + i64::column_result(value).and_then(|value| { + if value == 0 { + Ok(Self::Disabled) + } else if let Ok(duration) = u32::try_from(value) { + Ok(Self::Enabled { duration }) + } else { + Err(rusqlite::types::FromSqlError::OutOfRange(value)) + } + }) } } @@ -168,7 +154,8 @@ impl ChatId { let timer = context .sql .query_get_value( - sqlx::query("SELECT ephemeral_timer FROM chats WHERE id=?;").bind(self), + "SELECT ephemeral_timer FROM chats WHERE id=?;", + paramsv![self], ) .await?; Ok(timer.unwrap_or_default()) @@ -188,13 +175,10 @@ impl ChatId { context .sql .execute( - sqlx::query( - "UPDATE chats + "UPDATE chats SET ephemeral_timer=? WHERE id=?;", - ) - .bind(timer) - .bind(self), + paramsv![timer, self], ) .await?; @@ -233,45 +217,44 @@ pub(crate) async fn stock_ephemeral_timer_changed( from_id: u32, ) -> String { match timer { - Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id as u32).await, + Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id).await, Timer::Enabled { duration } => match duration { 0..=59 => { - stock_str::msg_ephemeral_timer_enabled(context, timer.to_string(), from_id as u32) - .await + stock_str::msg_ephemeral_timer_enabled(context, timer.to_string(), from_id).await } - 60 => stock_str::msg_ephemeral_timer_minute(context, from_id as u32).await, + 60 => stock_str::msg_ephemeral_timer_minute(context, from_id).await, 61..=3599 => { stock_str::msg_ephemeral_timer_minutes( context, format!("{}", (f64::from(duration) / 6.0).round() / 10.0), - from_id as u32, + from_id, ) .await } - 3600 => stock_str::msg_ephemeral_timer_hour(context, from_id as u32).await, + 3600 => stock_str::msg_ephemeral_timer_hour(context, from_id).await, 3601..=86399 => { stock_str::msg_ephemeral_timer_hours( context, format!("{}", (f64::from(duration) / 360.0).round() / 10.0), - from_id as u32, + from_id, ) .await } - 86400 => stock_str::msg_ephemeral_timer_day(context, from_id as u32).await, + 86400 => stock_str::msg_ephemeral_timer_day(context, from_id).await, 86401..=604_799 => { stock_str::msg_ephemeral_timer_days( context, format!("{}", (f64::from(duration) / 8640.0).round() / 10.0), - from_id as u32, + from_id, ) .await } - 604_800 => stock_str::msg_ephemeral_timer_week(context, from_id as u32).await, + 604_800 => stock_str::msg_ephemeral_timer_week(context, from_id).await, _ => { stock_str::msg_ephemeral_timer_weeks( context, format!("{}", (f64::from(duration) / 60480.0).round() / 10.0), - from_id as u32, + from_id, ) .await } @@ -284,15 +267,14 @@ impl MsgId { pub(crate) async fn ephemeral_timer(self, context: &Context) -> anyhow::Result { let res = match context .sql - .query_get_value::<_, i64>( - sqlx::query("SELECT ephemeral_timer FROM msgs WHERE id=?").bind(self), + .query_get_value( + "SELECT ephemeral_timer FROM msgs WHERE id=?", + paramsv![self], ) .await? { None | Some(0) => Timer::Disabled, - Some(duration) => Timer::Enabled { - duration: u32::try_from(duration)?, - }, + Some(duration) => Timer::Enabled { duration }, }; Ok(res) } @@ -305,14 +287,10 @@ impl MsgId { context .sql .execute( - sqlx::query( - "UPDATE msgs SET ephemeral_timestamp = ? \ + "UPDATE msgs SET ephemeral_timestamp = ? \ WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \ AND id = ?", - ) - .bind(ephemeral_timestamp) - .bind(ephemeral_timestamp) - .bind(self), + paramsv![ephemeral_timestamp, ephemeral_timestamp, self], ) .await?; schedule_ephemeral_task(context).await; @@ -333,10 +311,9 @@ pub(crate) async fn delete_expired_messages(context: &Context) -> Result ? \ AND chat_id != ? \ AND chat_id != ?", - ) - .bind(DC_CHAT_ID_TRASH) - .bind(threshold_timestamp) - .bind(DC_CHAT_ID_LAST_SPECIAL) - .bind(self_chat_id) - .bind(device_chat_id), + paramsv![ + DC_CHAT_ID_TRASH, + threshold_timestamp, + DC_CHAT_ID_LAST_SPECIAL, + self_chat_id, + device_chat_id + ], ) .await .context("deleted update failed")?; @@ -412,8 +386,7 @@ pub async fn schedule_ephemeral_task(context: &Context) { let ephemeral_timestamp: Option = match context .sql .query_get_value( - sqlx::query( - r#" + r#" SELECT ephemeral_timestamp FROM msgs WHERE ephemeral_timestamp != 0 @@ -421,8 +394,7 @@ pub async fn schedule_ephemeral_task(context: &Context) { ORDER BY ephemeral_timestamp ASC LIMIT 1; "#, - ) - .bind(DC_CHAT_ID_TRASH), // Trash contains already deleted messages, skip them + paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them ) .await { @@ -475,7 +447,7 @@ pub async fn schedule_ephemeral_task(context: &Context) { /// /// It looks up the trash chat too, to find messages that are already /// deleted locally, but not deleted on the server. -pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result> { +pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> anyhow::Result> { let now = time(); let threshold_timestamp = match context.get_config_delete_server_after().await? { @@ -483,11 +455,10 @@ pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result now - delete_server_after, }; - let row = context + context .sql - .fetch_optional( - sqlx::query( - "SELECT id FROM msgs \ + .query_row_optional( + "SELECT id FROM msgs \ WHERE ( \ timestamp < ? \ OR (ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?) \ @@ -495,19 +466,13 @@ pub(crate) async fn load_imap_deletion_msgid(context: &Context) -> sql::Result sql::Result<()> context .sql .execute( - sqlx::query( - "UPDATE msgs \ + "UPDATE msgs \ SET ephemeral_timestamp = ? + ephemeral_timer \ WHERE ephemeral_timer > 0 \ AND ephemeral_timestamp = 0 \ AND state NOT IN (?, ?, ?)", - ) - .bind(time()) - .bind(MessageState::InFresh) - .bind(MessageState::InNoticed) - .bind(MessageState::OutDraft), + paramsv![ + time(), + MessageState::InFresh, + MessageState::InNoticed, + MessageState::OutDraft + ], ) .await?; @@ -770,7 +735,10 @@ mod tests { // Check that the msg will be deleted on the server // First of all, set a server_uid so that DC thinks that it's actually possible to delete t.sql - .execute(sqlx::query("UPDATE msgs SET server_uid=1 WHERE id=?").bind(msg.sender_msg_id)) + .execute( + "UPDATE msgs SET server_uid=1 WHERE id=?", + paramsv![msg.sender_msg_id], + ) .await .unwrap(); let job = job::load_imap_deletion_job(&t).await.unwrap(); @@ -808,7 +776,7 @@ mod tests { assert!(msg.text.is_none_or_empty(), "{:?}", msg.text); let rawtxt: Option = t .sql - .query_get_value(sqlx::query("SELECT txt_raw FROM msgs WHERE id=?;").bind(msg_id)) + .query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", paramsv![msg_id]) .await .unwrap(); assert!(rawtxt.is_none_or_empty(), "{:?}", rawtxt); diff --git a/src/error.rs b/src/error.rs index 1c471bdce..13a7dd10b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,9 +1,5 @@ //! # Error handling -#[derive(Debug, thiserror::Error)] -#[error("Out of Range")] -pub struct OutOfRangeError; - #[macro_export] macro_rules! ensure_eq { ($left:expr, $right:expr) => ({ diff --git a/src/html.rs b/src/html.rs index 42f03c192..794af4dcc 100644 --- a/src/html.rs +++ b/src/html.rs @@ -426,7 +426,7 @@ test some special html-characters as < > and & but also " and &#x #[async_std::test] async fn test_get_html_empty() { let t = TestContext::new().await; - let msg_id = MsgId::new_unset(); + let msg_id = MsgId::new(100); assert!(msg_id.get_html(&t).await.unwrap().is_none()) } diff --git a/src/imap.rs b/src/imap.rs index 2ffc0e913..19e37e471 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -521,29 +521,21 @@ impl Imap { // Write collected UIDs to SQLite database. context .sql - .transaction(|conn| { - Box::pin(async move { - sqlx::query("UPDATE msgs SET server_uid=0 WHERE server_folder=?") - .bind(&folder) - .execute(&mut *conn) - .await?; - - for (uid, rfc724_mid) in &msg_ids { - // This may detect previously undetected moved - // messages, so we update server_folder too. - sqlx::query( - "UPDATE msgs \ + .transaction(move |transaction| { + transaction.execute( + "UPDATE msgs SET server_uid=0 WHERE server_folder=?", + params![folder], + )?; + for (uid, rfc724_mid) in &msg_ids { + // This may detect previously undetected moved + // messages, so we update server_folder too. + transaction.execute( + "UPDATE msgs \ SET server_folder=?,server_uid=? WHERE rfc724_mid=?", - ) - .bind(&folder) - .bind(uid) - .bind(rfc724_mid) - .execute(&mut *conn) - .await?; - } - - Ok(()) - }) + params![folder, uid, rfc724_mid], + )?; + } + Ok(()) }) .await?; Ok(()) @@ -1732,15 +1724,9 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32) context .sql .execute( - sqlx::query( - "INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?) + "INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?) ON CONFLICT(folder) DO UPDATE SET uid_next=? WHERE folder=?;", - ) - .bind(folder) - .bind(0i32) - .bind(uid_next as i64) - .bind(uid_next as i64) - .bind(folder), + paramsv![folder, 0u32, uid_next, uid_next, folder], ) .await?; Ok(()) @@ -1754,7 +1740,10 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32) async fn get_uid_next(context: &Context, folder: &str) -> Result { Ok(context .sql - .query_get_value(sqlx::query("SELECT uid_next FROM imap_sync WHERE folder=?;").bind(folder)) + .query_get_value( + "SELECT uid_next FROM imap_sync WHERE folder=?;", + paramsv![folder], + ) .await? .unwrap_or(0)) } @@ -1767,15 +1756,9 @@ pub(crate) async fn set_uidvalidity( context .sql .execute( - sqlx::query( - "INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?) + "INSERT INTO imap_sync (folder, uidvalidity, uid_next) VALUES (?,?,?) ON CONFLICT(folder) DO UPDATE SET uidvalidity=? WHERE folder=?;", - ) - .bind(folder) - .bind(uidvalidity as i32) - .bind(0i32) - .bind(uidvalidity as i32) - .bind(folder), + paramsv![folder, uidvalidity, 0u32, uidvalidity, folder], ) .await?; Ok(()) @@ -1785,7 +1768,8 @@ async fn get_uidvalidity(context: &Context, folder: &str) -> Result { Ok(context .sql .query_get_value( - sqlx::query("SELECT uidvalidity FROM imap_sync WHERE folder=?;").bind(folder), + "SELECT uidvalidity FROM imap_sync WHERE folder=?;", + paramsv![folder], ) .await? .unwrap_or(0)) diff --git a/src/imex.rs b/src/imex.rs index 1c323a5fc..cdf156c6d 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -10,7 +10,6 @@ use async_std::{ prelude::*, }; use rand::{thread_rng, Rng}; -use sqlx::Row; use crate::chat; use crate::chat::delete_and_reset_all_device_msgs; @@ -595,9 +594,8 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef let total_files_cnt = context .sql - .count(sqlx::query("SELECT COUNT(*) FROM backup_blobs;")) + .count("SELECT COUNT(*) FROM backup_blobs;", paramsv![]) .await?; - info!( context, "***IMPORT-in-progress: total_files_cnt={:?}", total_files_cnt, @@ -607,25 +605,33 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef // consuming too much memory. let file_ids = context .sql - .fetch(sqlx::query("SELECT id FROM backup_blobs ORDER BY id")) - .await? - .map(|row| row?.try_get(0)) - .collect::>>() + .query_map( + "SELECT id FROM backup_blobs ORDER BY id", + paramsv![], + |row| row.get(0), + |ids| { + ids.collect::, _>>() + .map_err(Into::into) + }, + ) .await?; let mut all_files_extracted = true; for (processed_files_cnt, file_id) in file_ids.into_iter().enumerate() { // Load a single blob into memory - let row = context + let (file_name, file_blob) = context .sql - .fetch_one( - sqlx::query("SELECT file_name, file_content FROM backup_blobs WHERE id = ?") - .bind(file_id), + .query_row( + "SELECT file_name, file_content FROM backup_blobs WHERE id = ?", + paramsv![file_id], + |row| { + let file_name: String = row.get(0)?; + let file_blob: Vec = row.get(1)?; + Ok((file_name, file_blob)) + }, ) .await?; - let file_name: String = row.try_get(0)?; - let file_blob: &[u8] = row.try_get(1)?; if context.shall_stop_ongoing().await { all_files_extracted = false; break; @@ -643,16 +649,16 @@ async fn import_backup_old(context: &Context, backup_to_import: impl AsRef } let path_filename = context.get_blobdir().join(file_name); - dc_write_file(context, &path_filename, file_blob).await?; + dc_write_file(context, &path_filename, &file_blob).await?; } if all_files_extracted { // only delete backup_blobs if all files were successfully extracted context .sql - .execute(sqlx::query("DROP TABLE backup_blobs;")) + .execute("DROP TABLE backup_blobs;", paramsv![]) .await?; - context.sql.execute(sqlx::query("VACUUM;")).await.ok(); + context.sql.execute("VACUUM;", paramsv![]).await.ok(); Ok(()) } else { bail!("received stop signal"); @@ -677,7 +683,7 @@ async fn export_backup(context: &Context, dir: impl AsRef) -> Result<()> { context .sql - .execute(sqlx::query("VACUUM;")) + .execute("VACUUM;", paramsv![]) .await .map_err(|e| warn!(context, "Vacuum failed, exporting anyway {}", e)); @@ -830,26 +836,29 @@ async fn import_self_keys(context: &Context, dir: impl AsRef) -> Result<() async fn export_self_keys(context: &Context, dir: impl AsRef) -> Result<()> { let mut export_errors = 0; - let mut keys = context + let keys = context .sql - .fetch(sqlx::query( + .query_map( "SELECT id, public_key, private_key, is_default FROM keypairs;", - )) - .await? - .map(|row| -> sqlx::Result<_> { - let row = row?; - let id = row.try_get(0)?; - let public_key_blob: &[u8] = row.try_get(1)?; - let public_key = SignedPublicKey::from_slice(public_key_blob); - let private_key_blob: &[u8] = row.try_get(2)?; - let private_key = SignedSecretKey::from_slice(private_key_blob); - let is_default: i32 = row.try_get(3)?; + paramsv![], + |row| { + let id = row.get(0)?; + let public_key_blob: Vec = row.get(1)?; + let public_key = SignedPublicKey::from_slice(&public_key_blob); + let private_key_blob: Vec = row.get(2)?; + let private_key = SignedSecretKey::from_slice(&private_key_blob); + let is_default: i32 = row.get(3)?; - Ok((id, public_key, private_key, is_default)) - }); + Ok((id, public_key, private_key, is_default)) + }, + |keys| { + keys.collect::, _>>() + .map_err(Into::into) + }, + ) + .await?; - while let Some(parts) = keys.next().await { - let (id, public_key, private_key, is_default) = parts?; + for (id, public_key, private_key, is_default) in keys { let id = Some(id).filter(|_| is_default != 0); if let Ok(key) = public_key { if export_key_to_asc_file(context, &dir, id, &key) diff --git a/src/job.rs b/src/job.rs index 7b8285275..49dd81903 100644 --- a/src/job.rs +++ b/src/job.rs @@ -7,11 +7,10 @@ use std::{fmt, time::Duration}; use anyhow::{bail, ensure, format_err, Context as _, Error, Result}; use async_smtp::smtp::response::{Category, Code, Detail}; -use async_std::prelude::*; use async_std::task::sleep; +use deltachat_derive::{FromSql, ToSql}; use itertools::Itertools; use rand::{thread_rng, Rng}; -use sqlx::Row; use crate::dc_tools::{dc_delete_file, dc_read_file, time}; use crate::ephemeral::load_imap_deletion_msgid; @@ -37,7 +36,9 @@ use crate::{scheduler::InterruptInfo, sql}; const JOB_RETRIES: u32 = 17; /// Thread IDs -#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)] +#[derive( + Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql, +)] #[repr(u32)] pub(crate) enum Thread { Unknown = 0, @@ -75,7 +76,17 @@ impl Default for Thread { } #[derive( - Debug, Display, Copy, Clone, PartialEq, Eq, PartialOrd, FromPrimitive, ToPrimitive, sqlx::Type, + Debug, + Display, + Copy, + Clone, + PartialEq, + Eq, + PartialOrd, + FromPrimitive, + ToPrimitive, + FromSql, + ToSql, )] #[repr(u32)] pub enum Action { @@ -173,7 +184,7 @@ impl Job { if self.job_id != 0 { context .sql - .execute(sqlx::query("DELETE FROM jobs WHERE id=?;").bind(self.job_id as i32)) + .execute("DELETE FROM jobs WHERE id=?;", paramsv![self.job_id as i32]) .await?; } @@ -192,24 +203,26 @@ impl Job { context .sql .execute( - sqlx::query( - "UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;", - ) - .bind(self.desired_timestamp) - .bind(self.tries as i64) - .bind(self.param.to_string()) - .bind(self.job_id as i32), + "UPDATE jobs SET desired_timestamp=?, tries=?, param=? WHERE id=?;", + paramsv![ + self.desired_timestamp, + self.tries as i64, + self.param.to_string(), + self.job_id as i32, + ], ) .await?; } else { context.sql.execute( - sqlx::query("INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);") - .bind(self.added_timestamp) - .bind(thread) - .bind(self.action) - .bind(self.foreign_id) - .bind(self.param.to_string()) - .bind(self.desired_timestamp) + "INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);", + paramsv![ + self.added_timestamp, + thread, + self.action, + self.foreign_id, + self.param.to_string(), + self.desired_timestamp + ] ).await?; } @@ -419,30 +432,37 @@ impl Job { contact_id: u32, ) -> sql::Result<(Vec, Vec)> { // Extract message IDs from job parameters - let mut rows = context + let res: Vec<(u32, MsgId)> = context .sql - .fetch( - sqlx::query("SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?") - .bind(contact_id) - .bind(self.job_id), + .query_map( + "SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?", + paramsv![contact_id, self.job_id], + |row| { + let job_id: u32 = row.get(0)?; + let params_str: String = row.get(1)?; + let params: Params = params_str.parse().unwrap_or_default(); + Ok((job_id, params)) + }, + |jobs| { + let res = jobs + .filter_map(|row| { + let (job_id, params) = row.ok()?; + let msg_id = params.get_msg_id()?; + Some((job_id, msg_id)) + }) + .collect(); + Ok(res) + }, ) .await?; // Load corresponding RFC724 message IDs let mut job_ids = Vec::new(); let mut rfc724_mids = Vec::new(); - - while let Some(row) = rows.next().await { - let row = row?; - let job_id: u32 = row.try_get(0)?; - let params_str: String = row.try_get(1)?; - let params: Params = params_str.parse().unwrap_or_default(); - if let Some(msg_id) = params.get_msg_id() { - if let Ok(Message { rfc724_mid, .. }) = Message::load_from_db(context, msg_id).await - { - job_ids.push(job_id); - rfc724_mids.push(rfc724_mid); - } + for (job_id, msg_id) in res { + if let Ok(Message { rfc724_mid, .. }) = Message::load_from_db(context, msg_id).await { + job_ids.push(job_id); + rfc724_mids.push(rfc724_mid); } } Ok((job_ids, rfc724_mids)) @@ -820,7 +840,7 @@ impl Job { pub async fn kill_action(context: &Context, action: Action) -> bool { context .sql - .execute(sqlx::query("DELETE FROM jobs WHERE action=?;").bind(action)) + .execute("DELETE FROM jobs WHERE action=?;", paramsv![action]) .await .is_ok() } @@ -831,18 +851,20 @@ async fn kill_ids(context: &Context, job_ids: &[u32]) -> sql::Result<()> { "DELETE FROM jobs WHERE id IN({})", job_ids.iter().map(|_| "?").join(",") ); - let mut query = sqlx::query(&q); - for id in job_ids { - query = query.bind(*id); - } - context.sql.execute(query).await?; + context + .sql + .execute(q, job_ids.iter().map(|i| i as &dyn crate::ToSql).collect()) + .await?; Ok(()) } pub async fn action_exists(context: &Context, action: Action) -> bool { context .sql - .exists(sqlx::query("SELECT COUNT(*) FROM jobs WHERE action=?;").bind(action)) + .exists( + "SELECT COUNT(*) FROM jobs WHERE action=?;", + paramsv![action], + ) .await .unwrap_or_default() } @@ -851,7 +873,7 @@ async fn set_delivered(context: &Context, msg_id: MsgId) -> Result<()> { message::update_msg_state(context, msg_id, MessageState::OutDelivered).await; let chat_id: ChatId = context .sql - .query_get_value(sqlx::query("SELECT chat_id FROM msgs WHERE id=?").bind(msg_id)) + .query_get_value("SELECT chat_id FROM msgs WHERE id=?", paramsv![msg_id]) .await? .unwrap_or_default(); context.emit_event(EventType::MsgDelivered { chat_id, msg_id }); @@ -1282,77 +1304,65 @@ pub(crate) async fn load_next( sleep(Duration::from_millis(500)).await; } + let query; + let params; let t = time(); + let m; let thread_i = thread as i64; - let get_query = || { - if let Some(msg_id) = info.msg_id { - sqlx::query( - r#" + if let Some(msg_id) = info.msg_id { + query = r#" SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries FROM jobs WHERE thread=? AND foreign_id=? ORDER BY action DESC, added_timestamp LIMIT 1; -"#, - ) - .bind(thread_i) - .bind(msg_id) - } else if !info.probe_network { - // processing for first-try and after backoff-timeouts: - // process jobs in the order they were added. - sqlx::query( - r#" +"#; + m = msg_id; + params = paramsv![thread_i, m]; + } else if !info.probe_network { + // processing for first-try and after backoff-timeouts: + // process jobs in the order they were added. + query = r#" SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries FROM jobs WHERE thread=? AND desired_timestamp<=? ORDER BY action DESC, added_timestamp LIMIT 1; -"#, - ) - .bind(thread_i) - .bind(t) - } else { - // processing after call to dc_maybe_network(): - // process _all_ pending jobs that failed before - // in the order of their backoff-times. - sqlx::query( - r#" +"#; + params = paramsv![thread_i, t]; + } else { + // processing after call to dc_maybe_network(): + // process _all_ pending jobs that failed before + // in the order of their backoff-times. + query = r#" SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries FROM jobs WHERE thread=? AND tries>0 ORDER BY desired_timestamp, action DESC LIMIT 1; -"#, - ) - .bind(thread_i) - } +"#; + params = paramsv![thread_i]; }; let job = loop { let job_res = context .sql - .fetch_optional(get_query()) - .await - .and_then(|row| { - if let Some(row) = row { - Ok(Some(Job { - job_id: row.try_get("id")?, - action: row.try_get("action")?, - foreign_id: row.try_get("foreign_id")?, - desired_timestamp: row.try_get("desired_timestamp")?, - added_timestamp: row.try_get("added_timestamp")?, - tries: row.try_get::("tries")? as u32, - param: row - .try_get::("param")? - .parse() - .unwrap_or_default(), - pending_error: None, - })) - } else { - Ok(None) - } - }); + .query_row_optional(query, params.clone(), |row| { + let job = Job { + job_id: row.get("id")?, + action: row.get("action")?, + foreign_id: row.get("foreign_id")?, + desired_timestamp: row.get("desired_timestamp")?, + added_timestamp: row.get("added_timestamp")?, + tries: row.get("tries")?, + param: row.get::<_, String>("param")?.parse().unwrap_or_default(), + pending_error: None, + }; + + Ok(job) + }) + .await; match job_res { Ok(job) => break job, @@ -1363,14 +1373,13 @@ LIMIT 1; // TODO: improve by only doing a single query match context .sql - .fetch_one(get_query()) + .query_row(query, params.clone(), |row| row.get::<_, i32>(0)) .await - .and_then(|row| row.try_get::(0).map_err(Into::into)) { Ok(id) => { if let Err(err) = context .sql - .execute(sqlx::query("DELETE FROM jobs WHERE id=?;").bind(id)) + .execute("DELETE FROM jobs WHERE id=?;", paramsv![id]) .await { warn!(context, "failed to delete job {}: {:?}", id, err); @@ -1421,17 +1430,17 @@ mod tests { context .sql .execute( - sqlx::query( - "INSERT INTO jobs + "INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?, ?, ?, ?, ?, ?);", - ) - .bind(now) - .bind(Thread::from(Action::MoveMsg)) - .bind(if valid { Action::MoveMsg as i32 } else { -1 }) - .bind(foreign_id) - .bind(Params::new().to_string()) - .bind(now), + paramsv![ + now, + Thread::from(Action::MoveMsg), + if valid { Action::MoveMsg as i32 } else { -1 }, + foreign_id, + Params::new().to_string(), + now + ], ) .await .unwrap(); @@ -1451,7 +1460,7 @@ mod tests { ) .await; // The housekeeping job should be loaded as we didn't run housekeeping in the last day: - assert!(jobs.unwrap().action == Action::Housekeeping); + assert_eq!(jobs.unwrap().action, Action::Housekeeping); insert_job(&t, 1, true).await; let jobs = load_next( diff --git a/src/key.rs b/src/key.rs index cfd6628ed..c4c4c50a9 100644 --- a/src/key.rs +++ b/src/key.rs @@ -9,7 +9,6 @@ use num_traits::FromPrimitive; use pgp::composed::Deserializable; use pgp::ser::Serialize; use pgp::types::{KeyTrait, SecretKeyTrait}; -use sqlx::Row; use thiserror::Error; use crate::config::Config; @@ -42,8 +41,6 @@ pub enum Error { InvalidConfiguredAddr(#[from] InvalidEmailError), #[error("no data provided")] Empty, - #[error("db: {}", _0)] - Sql(#[from] sqlx::Error), #[error("{0}")] Other(#[from] anyhow::Error), } @@ -123,17 +120,22 @@ impl DcKey for SignedPublicKey { async fn load_self(context: &Context) -> Result { match context .sql - .fetch_optional(sqlx::query( + .query_row_optional( r#" SELECT public_key FROM keypairs WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr") AND is_default=1; "#, - )) + paramsv![], + |row| { + let bytes: Vec = row.get(0)?; + Ok(bytes) + }, + ) .await? { - Some(row) => Self::from_slice(row.try_get(0)?), + Some(bytes) => Self::from_slice(&bytes), None => { let keypair = generate_keypair(context).await?; Ok(keypair.public) @@ -165,17 +167,22 @@ impl DcKey for SignedSecretKey { async fn load_self(context: &Context) -> Result { match context .sql - .fetch_optional(sqlx::query( + .query_row_optional( r#" SELECT private_key FROM keypairs WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr") AND is_default=1; "#, - )) + paramsv![], + |row| { + let bytes: Vec = row.get(0)?; + Ok(bytes) + }, + ) .await? { - Some(row) => Self::from_slice(row.try_get(0)?), + Some(bytes) => Self::from_slice(&bytes), None => { let keypair = generate_keypair(context).await?; Ok(keypair.secret) @@ -228,23 +235,26 @@ async fn generate_keypair(context: &Context) -> Result { // Check if the key appeared while we were waiting on the lock. match context .sql - .fetch_optional( - sqlx::query( - r#" + .query_row_optional( + r#" SELECT public_key, private_key FROM keypairs WHERE addr=?1 AND is_default=1; "#, - ) - .bind(addr.to_string()), + paramsv![addr], + |row| { + let pub_bytes: Vec = row.get(0)?; + let sec_bytes: Vec = row.get(1)?; + Ok((pub_bytes, sec_bytes)) + }, ) .await? { - Some(row) => Ok(KeyPair { + Some((pub_bytes, sec_bytes)) => Ok(KeyPair { addr, - public: SignedPublicKey::from_slice(row.try_get(0)?)?, - secret: SignedSecretKey::from_slice(row.try_get(1)?)?, + public: SignedPublicKey::from_slice(&pub_bytes)?, + secret: SignedSecretKey::from_slice(&sec_bytes)?, }), None => { let start = std::time::SystemTime::now(); @@ -319,16 +329,15 @@ pub async fn store_self_keypair( context .sql .execute( - sqlx::query("DELETE FROM keypairs WHERE public_key=? OR private_key=?;") - .bind(&public_key) - .bind(&secret_key), + "DELETE FROM keypairs WHERE public_key=? OR private_key=?;", + paramsv![public_key, secret_key], ) .await .map_err(|err| SaveKeyError::new("failed to remove old use of key", err))?; if default == KeyPairUse::Default { context .sql - .execute(sqlx::query("UPDATE keypairs SET is_default=0;")) + .execute("UPDATE keypairs SET is_default=0;", paramsv![]) .await .map_err(|err| SaveKeyError::new("failed to clear default", err))?; } @@ -343,15 +352,9 @@ pub async fn store_self_keypair( context .sql .execute( - sqlx::query( - "INSERT INTO keypairs (addr, is_default, public_key, private_key, created) + "INSERT INTO keypairs (addr, is_default, public_key, private_key, created) VALUES (?,?,?,?,?);", - ) - .bind(addr) - .bind(is_default) - .bind(&public_key) - .bind(&secret_key) - .bind(t), + paramsv![addr, is_default, public_key, secret_key, t], ) .await .map_err(|err| SaveKeyError::new("failed to insert keypair", err))?; @@ -625,7 +628,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD let nrows = || async { ctx.sql - .count(sqlx::query("SELECT COUNT(*) FROM keypairs;")) + .count("SELECT COUNT(*) FROM keypairs;", paramsv![]) .await .unwrap() }; diff --git a/src/lib.rs b/src/lib.rs index 244390c33..eb40102b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,11 @@ +#![forbid(unsafe_code)] #![deny( clippy::correctness, missing_debug_implementations, clippy::all, clippy::indexing_slicing, clippy::wildcard_imports, - clippy::needless_borrow, - unsafe_code + clippy::needless_borrow )] #![allow(clippy::match_bool, clippy::eval_order_dependence)] @@ -13,10 +13,16 @@ extern crate num_derive; #[macro_use] extern crate smallvec; +#[macro_use] +extern crate rusqlite; extern crate strum; #[macro_use] extern crate strum_macros; +pub trait ToSql: rusqlite::ToSql + Send + Sync {} + +impl ToSql for T {} + #[macro_use] pub mod log; #[macro_use] diff --git a/src/location.rs b/src/location.rs index caa941f5d..486034c26 100644 --- a/src/location.rs +++ b/src/location.rs @@ -2,10 +2,8 @@ use std::convert::TryFrom; use anyhow::{ensure, Error}; -use async_std::prelude::*; use bitflags::bitflags; use quick_xml::events::{BytesEnd, BytesStart, BytesText}; -use sqlx::Row; use crate::chat::{self, ChatId}; use crate::config::Config; @@ -201,15 +199,15 @@ pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: if context .sql .execute( - sqlx::query( - "UPDATE chats \ + "UPDATE chats \ SET locations_send_begin=?, \ locations_send_until=? \ WHERE id=?", - ) - .bind(if 0 != seconds { now } else { 0 }) - .bind(if 0 != seconds { now + seconds } else { 0 }) - .bind(chat_id), + paramsv![ + if 0 != seconds { now } else { 0 }, + if 0 != seconds { now + seconds } else { 0 }, + chat_id, + ], ) .await .is_ok() @@ -262,17 +260,16 @@ pub async fn is_sending_locations_to_chat(context: &Context, chat_id: Option context .sql .exists( - sqlx::query("SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;") - .bind(chat_id) - .bind(time()), + "SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;", + paramsv![chat_id, time()], ) .await .unwrap_or_default(), None => context .sql .exists( - sqlx::query("SELECT COUNT(id) FROM chats WHERE locations_send_until>?;") - .bind(time()), + "SELECT COUNT(id) FROM chats WHERE locations_send_until>?;", + paramsv![time()], ) .await .unwrap_or_default(), @@ -285,29 +282,28 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64 } let mut continue_streaming = false; - if let Ok(mut chats) = context + if let Ok(chats) = context .sql - .fetch(sqlx::query("SELECT id FROM chats WHERE locations_send_until>?;").bind(time())) + .query_map( + "SELECT id FROM chats WHERE locations_send_until>?;", + paramsv![time()], + |row| row.get::<_, i32>(0), + |chats| chats.collect::, _>>().map_err(Into::into), + ) .await - .map(|rows| rows.map(|row| row?.try_get::(0))) { - while let Some(chat_id) = chats.next().await { - let chat_id = match chat_id { - Ok(id) => id, - Err(_) => break, - }; + for chat_id in chats { if let Err(err) = context.sql.execute( - sqlx::query( "INSERT INTO locations \ - (latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);" - ) - .bind(latitude) - .bind(longitude) - .bind(accuracy) - .bind(time()) - .bind(chat_id) - .bind(DC_CONTACT_ID_SELF) - + (latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);", + paramsv![ + latitude, + longitude, + accuracy, + time(), + chat_id, + DC_CONTACT_ID_SELF, + ] ).await { warn!(context, "failed to store location {:?}", err); } else { @@ -342,50 +338,54 @@ pub async fn get_range( Some(contact_id) => (0, contact_id), None => (1, 0), // this contact_id is unused }; - let list = context .sql - .fetch( - sqlx::query( - "SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \ + .query_map( + "SELECT l.id, l.latitude, l.longitude, l.accuracy, l.timestamp, l.independent, \ COALESCE(m.id, 0) AS msg_id, l.from_id, l.chat_id, COALESCE(m.txt, '') AS txt \ FROM locations l LEFT JOIN msgs m ON l.id=m.location_id WHERE (? OR l.chat_id=?) \ AND (? OR l.from_id=?) \ AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \ ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;", - ) - .bind(disable_chat_id) - .bind(chat_id) - .bind(disable_contact_id) - .bind(contact_id as i64) - .bind(timestamp_from) - .bind(timestamp_to), + paramsv![ + disable_chat_id, + chat_id, + disable_contact_id, + contact_id as i32, + timestamp_from, + timestamp_to, + ], + |row| { + let msg_id = row.get(6)?; + let txt: String = row.get(9)?; + let marker = if msg_id != 0 && is_marker(&txt) { + Some(txt) + } else { + None + }; + let loc = Location { + location_id: row.get(0)?, + latitude: row.get(1)?, + longitude: row.get(2)?, + accuracy: row.get(3)?, + timestamp: row.get(4)?, + independent: row.get(5)?, + msg_id, + contact_id: row.get(7)?, + chat_id: row.get(8)?, + marker, + }; + Ok(loc) + }, + |locations| { + let mut ret = Vec::new(); + + for location in locations { + ret.push(location?); + } + Ok(ret) + }, ) - .await? - .map(|row| { - let row = row?; - let msg_id = row.try_get(6)?; - let txt: String = row.try_get(9)?; - let marker = if msg_id != 0 && is_marker(&txt) { - Some(txt) - } else { - None - }; - let loc = Location { - location_id: row.try_get(0)?, - latitude: row.try_get(1)?, - longitude: row.try_get(2)?, - accuracy: row.try_get(3)?, - timestamp: row.try_get(4)?, - independent: row.try_get(5)?, - msg_id, - contact_id: row.try_get(7)?, - chat_id: row.try_get(8)?, - marker, - }; - Ok(loc) - }) - .collect::>() .await?; Ok(list) } @@ -403,7 +403,7 @@ fn is_marker(txt: &str) -> bool { pub async fn delete_all(context: &Context) -> Result<(), Error> { context .sql - .execute(sqlx::query("DELETE FROM locations;")) + .execute("DELETE FROM locations;", paramsv![]) .await?; context.emit_event(EventType::LocationChanged(None)); Ok(()) @@ -417,65 +417,70 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32) .await? .unwrap_or_default(); - let (locations_send_begin, locations_send_until, locations_last_sent) = { - let row = context.sql.fetch_one( - sqlx::query( - "SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;" - ) - .bind(chat_id) - ).await?; + let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row( + "SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;", + paramsv![chat_id], |row| { + let send_begin: i64 = row.get(0)?; + let send_until: i64 = row.get(1)?; + let last_sent: i64 = row.get(2)?; - let send_begin: i64 = row.try_get(0)?; - let send_until: i64 = row.try_get(1)?; - let last_sent: i64 = row.try_get(2)?; - - (send_begin, send_until, last_sent) - }; + Ok((send_begin, send_until, last_sent)) + }) + .await?; let now = time(); let mut location_count = 0; let mut ret = String::new(); if locations_send_begin != 0 && now <= locations_send_until { ret += &format!( - "\n\n\n", + "\n\ + \n\n", self_addr, ); - let mut rows = context.sql.fetch( - sqlx::query( + context + .sql + .query_map( "SELECT id, latitude, longitude, accuracy, timestamp \ FROM locations WHERE from_id=? \ AND timestamp>=? \ - AND (timestamp>=? OR timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \ + AND (timestamp>=? OR \ + timestamp=(SELECT MAX(timestamp) FROM locations WHERE from_id=?)) \ AND independent=0 \ GROUP BY timestamp \ - ORDER BY timestamp;" + ORDER BY timestamp;", + paramsv![ + DC_CONTACT_ID_SELF, + locations_send_begin, + locations_last_sent, + DC_CONTACT_ID_SELF + ], + |row| { + let location_id: i32 = row.get(0)?; + let latitude: f64 = row.get(1)?; + let longitude: f64 = row.get(2)?; + let accuracy: f64 = row.get(3)?; + let timestamp = get_kml_timestamp(row.get(4)?); + + Ok((location_id, latitude, longitude, accuracy, timestamp)) + }, + |rows| { + for row in rows { + let (location_id, latitude, longitude, accuracy, timestamp) = row?; + ret += &format!( + "\ + {}\ + {},{}\ + \n", + timestamp, accuracy, longitude, latitude + ); + location_count += 1; + last_added_location_id = location_id as u32; + } + Ok(()) + }, ) - .bind(DC_CONTACT_ID_SELF) - .bind(locations_send_begin) - .bind(locations_last_sent) - .bind(DC_CONTACT_ID_SELF) - ).await?; - - while let Some(row) = rows.next().await { - let row = row?; - let location_id: u32 = row.try_get(0)?; - let latitude: f64 = row.try_get(1)?; - let longitude: f64 = row.try_get(2)?; - let accuracy: f64 = row.try_get(3)?; - let timestamp = get_kml_timestamp(row.try_get(4)?); - - ret += &format!( - "{}{},{}\n", - timestamp, - accuracy, - longitude, - latitude - ); - location_count += 1; - last_added_location_id = location_id; - } - + .await?; ret += "\n"; } @@ -516,9 +521,8 @@ pub async fn set_kml_sent_timestamp( context .sql .execute( - sqlx::query("UPDATE chats SET locations_last_sent=? WHERE id=?;") - .bind(timestamp) - .bind(chat_id), + "UPDATE chats SET locations_last_sent=? WHERE id=?;", + paramsv![timestamp, chat_id], ) .await?; Ok(()) @@ -532,9 +536,8 @@ pub async fn set_msg_location_id( context .sql .execute( - sqlx::query("UPDATE msgs SET location_id=? WHERE id=?;") - .bind(location_id) - .bind(msg_id), + "UPDATE msgs SET location_id=? WHERE id=?;", + paramsv![location_id, msg_id], ) .await?; @@ -553,7 +556,6 @@ pub async fn save( let mut newest_timestamp = 0; let mut newest_location_id = 0; - let stmt_test = "SELECT COUNT(*) FROM locations WHERE timestamp=? AND from_id=?"; let stmt_insert = "INSERT INTO locations\ (timestamp, from_id, chat_id, latitude, longitude, accuracy, independent) \ VALUES (?,?,?,?,?,?,?);"; @@ -566,30 +568,39 @@ pub async fn save( accuracy, .. } = location; - let exists = context + let (loc_id, ts) = context .sql - .exists(sqlx::query(stmt_test).bind(timestamp).bind(contact_id)) - .await?; - if independent || !exists { - let row_id = context - .sql - .insert( - sqlx::query(stmt_insert) - .bind(timestamp) - .bind(contact_id) - .bind(chat_id) - .bind(latitude) - .bind(longitude) - .bind(accuracy) - .bind(independent), - ) - .await?; + .with_conn(move |conn| { + let mut stmt_test = conn + .prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?; + let mut stmt_insert = conn.prepare_cached(stmt_insert)?; - if timestamp > newest_timestamp { - newest_timestamp = timestamp; - newest_location_id = row_id; - } - } + let exists = stmt_test.exists(paramsv![timestamp, contact_id as i32])?; + + if independent || !exists { + stmt_insert.execute(paramsv![ + timestamp, + contact_id as i32, + chat_id, + latitude, + longitude, + accuracy, + independent, + ])?; + + if timestamp > newest_timestamp { + // okay to drop, as we use cached prepared statements + drop(stmt_test); + drop(stmt_insert); + newest_timestamp = timestamp; + newest_location_id = conn.last_insert_rowid(); + } + } + Ok((newest_location_id, newest_timestamp)) + }) + .await?; + newest_timestamp = ts; + newest_location_id = loc_id; } Ok(u32::try_from(newest_location_id)?) @@ -605,21 +616,15 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j let rows = context .sql - .fetch( - sqlx::query( - "SELECT id, locations_send_begin, locations_last_sent \ + .query_map( + "SELECT id, locations_send_begin, locations_last_sent \ FROM chats \ WHERE locations_send_until>?;", - ) - .bind(now), - ) - .await - .map(|rows| { - rows.map(|row| -> sqlx::Result> { - let row = row?; - let chat_id: ChatId = row.try_get(0)?; - let locations_send_begin: i64 = row.try_get(1)?; - let locations_last_sent: i64 = row.try_get(2)?; + paramsv![now], + |row| { + let chat_id: ChatId = row.get(0)?; + let locations_send_begin: i64 = row.get(1)?; + let locations_last_sent: i64 = row.get(2)?; continue_streaming = true; // be a bit tolerant as the timer may not align exactly with time(NULL) @@ -628,55 +633,64 @@ pub(crate) async fn job_maybe_send_locations(context: &Context, _job: &Job) -> j } else { Ok(Some((chat_id, locations_send_begin, locations_last_sent))) } - }) - .filter_map(|v| v.transpose()) - }); + }, + |rows| { + rows.filter_map(|v| v.transpose()) + .collect::, _>>() + .map_err(Into::into) + }, + ) + .await; - let stmt = "SELECT COUNT(*) \ + if rows.is_ok() { + let msgs = context + .sql + .with_conn(move |conn| { + let rows = rows.unwrap(); + + let mut stmt_locations = conn.prepare_cached( + "SELECT id \ FROM locations \ WHERE from_id=? \ AND timestamp>=? \ AND timestamp>? \ AND independent=0 \ - ORDER BY timestamp;"; + ORDER BY timestamp;", + )?; - if let Ok(mut rows) = rows { - let mut msgs = Vec::new(); - while let Some(row) = rows.next().await { - let (chat_id, locations_send_begin, locations_last_sent) = match row { - Ok(row) => row, - Err(_) => break, - }; - let exists = context - .sql - .exists( - sqlx::query(stmt) - .bind(DC_CONTACT_ID_SELF) - .bind(locations_send_begin) - .bind(locations_last_sent), - ) - .await - .unwrap_or_default(); // TODO: better error handling + let mut msgs = Vec::new(); + for (chat_id, locations_send_begin, locations_last_sent) in &rows { + if !stmt_locations + .exists(paramsv![ + DC_CONTACT_ID_SELF, + *locations_send_begin, + *locations_last_sent, + ]) + .unwrap_or_default() + { + // if there is no new location, there's nothing to send. + // however, maybe we want to bypass this test eg. 15 minutes + } else { + // pending locations are attached automatically to every message, + // so also to this empty text message. + // DC_CMD_LOCATION is only needed to create a nicer subject. + // + // for optimisation and to avoid flooding the sending queue, + // we could sending these messages only if we're really online. + // the easiest way to determine this, is to check for an empty message queue. + // (might not be 100%, however, as positions are sent combined later + // and dc_set_location() is typically called periodically, this is ok) + let mut msg = Message::new(Viewtype::Text); + msg.hidden = true; + msg.param.set_cmd(SystemMessage::LocationOnly); + msgs.push((*chat_id, msg)); + } + } - if !exists { - // if there is no new location, there's nothing to send. - // however, maybe we want to bypass this test eg. 15 minutes - } else { - // pending locations are attached automatically to every message, - // so also to this empty text message. - // DC_CMD_LOCATION is only needed to create a nicer subject. - // - // for optimisation and to avoid flooding the sending queue, - // we could sending these messages only if we're really online. - // the easiest way to determine this, is to check for an empty message queue. - // (might not be 100%, however, as positions are sent combined later - // and dc_set_location() is typically called periodically, this is ok) - let mut msg = Message::new(Viewtype::Text); - msg.hidden = true; - msg.param.set_cmd(SystemMessage::LocationOnly); - msgs.push((chat_id, msg)); - } - } + Ok(msgs) + }) + .await + .unwrap_or_default(); // TODO: better error handling for (chat_id, mut msg) in msgs.into_iter() { // TODO: better error handling @@ -702,16 +716,16 @@ pub(crate) async fn job_maybe_send_locations_ended( let chat_id = ChatId::new(job.foreign_id); - let (send_begin, send_until) = job_try!(context - .sql - .fetch_one( - sqlx::query( + let (send_begin, send_until) = job_try!( + context + .sql + .query_row( "SELECT locations_send_begin, locations_send_until FROM chats WHERE id=?", + paramsv![chat_id], + |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)), ) - .bind(chat_id) - ) - .await - .and_then(|row| { Ok((row.try_get::(0)?, row.try_get::(1)?)) })); + .await + ); if !(send_begin != 0 && time() <= send_until) { // still streaming - @@ -723,12 +737,10 @@ pub(crate) async fn job_maybe_send_locations_ended( context .sql .execute( - sqlx::query( - "UPDATE chats \ + "UPDATE chats \ SET locations_send_begin=0, locations_send_until=0 \ - WHERE id=?" - ) - .bind(chat_id) + WHERE id=?", + paramsv![chat_id], ) .await ); diff --git a/src/lot.rs b/src/lot.rs index 4a132173c..ed746977f 100644 --- a/src/lot.rs +++ b/src/lot.rs @@ -1,3 +1,5 @@ +use deltachat_derive::{FromSql, ToSql}; + use crate::key::Fingerprint; /// An object containing a set of values. @@ -20,7 +22,9 @@ pub struct Lot { } #[repr(u8)] -#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] +#[derive( + Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql, +)] pub enum Meaning { None = 0, Text1Draft = 1, @@ -64,8 +68,10 @@ impl Lot { } } -#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] #[repr(u32)] +#[derive( + Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql, +)] pub enum LotState { // Default Undefined = 0, diff --git a/src/message.rs b/src/message.rs index 0d1c36371..1418e836c 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,11 +1,13 @@ //! # Messages and their identifiers +use std::collections::BTreeMap; +use std::convert::TryInto; + use anyhow::{ensure, Error}; use async_std::path::{Path, PathBuf}; -use async_std::prelude::*; +use deltachat_derive::{FromSql, ToSql}; use itertools::Itertools; use serde::{Deserialize, Serialize}; -use sqlx::Row; use crate::chat::{self, Chat, ChatId}; use crate::config::Config; @@ -29,7 +31,6 @@ use crate::mimeparser::{FailureReport, SystemMessage}; use crate::param::{Param, Params}; use crate::pgp::split_armored_data; use crate::stock_str; -use std::collections::BTreeMap; // In practice, the user additionally cuts the string themselves // pixel-accurate. @@ -41,20 +42,8 @@ const SUMMARY_CHARACTERS: usize = 160; /// This type can represent both the special as well as normal /// messages. #[derive( - Debug, - Copy, - Clone, - Default, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - Serialize, - Deserialize, - sqlx::Type, + Debug, Copy, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, )] -#[sqlx(transparent)] pub struct MsgId(u32); impl MsgId { @@ -92,7 +81,7 @@ impl MsgId { pub async fn get_state(self, context: &Context) -> crate::sql::Result { let result = context .sql - .query_get_value(sqlx::query("SELECT state FROM msgs WHERE id=?").bind(self)) + .query_get_value("SELECT state FROM msgs WHERE id=?", paramsv![self]) .await? .unwrap_or_default(); Ok(result) @@ -172,10 +161,9 @@ impl MsgId { context .sql .execute( - sqlx::query( - // If you change which information is removed here, also change delete_expired_messages() and - // which information dc_receive_imf::add_parts() still adds to the db if the chat_id is TRASH - r#" + // If you change which information is removed here, also change delete_expired_messages() and + // which information dc_receive_imf::add_parts() still adds to the db if the chat_id is TRASH + r#" UPDATE msgs SET chat_id=?, txt='', @@ -185,9 +173,7 @@ SET param='' WHERE id=?; "#, - ) - .bind(chat_id) - .bind(self), + paramsv![chat_id, self], ) .await?; @@ -200,11 +186,11 @@ WHERE id=?; // sure they are not left while the message is deleted. context .sql - .execute(sqlx::query("DELETE FROM msgs_mdns WHERE msg_id=?;").bind(self)) + .execute("DELETE FROM msgs_mdns WHERE msg_id=?;", paramsv![self]) .await?; context .sql - .execute(sqlx::query("DELETE FROM msgs WHERE id=?;").bind(self)) + .execute("DELETE FROM msgs WHERE id=?;", paramsv![self]) .await?; Ok(()) } @@ -218,12 +204,10 @@ WHERE id=?; context .sql .execute( - sqlx::query( - "UPDATE msgs \ + "UPDATE msgs \ SET server_folder='', server_uid=0 \ WHERE id=?", - ) - .bind(self), + paramsv![self], ) .await?; Ok(()) @@ -244,6 +228,41 @@ impl std::fmt::Display for MsgId { } } +/// Allow converting [MsgId] to an SQLite type. +/// +/// This allows you to directly store [MsgId] into the database. +/// +/// # Errors +/// +/// This **does** ensure that no special message IDs are written into +/// the database and the conversion will fail if this is not the case. +impl rusqlite::types::ToSql for MsgId { + fn to_sql(&self) -> rusqlite::Result { + if self.0 <= DC_MSG_ID_LAST_SPECIAL { + return Err(rusqlite::Error::ToSqlConversionFailure(Box::new( + InvalidMsgId, + ))); + } + let val = rusqlite::types::Value::Integer(self.0 as i64); + let out = rusqlite::types::ToSqlOutput::Owned(val); + Ok(out) + } +} + +/// Allow converting an SQLite integer directly into [MsgId]. +impl rusqlite::types::FromSql for MsgId { + fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult { + // Would be nice if we could use match here, but alas. + i64::column_result(value).and_then(|val| { + if 0 <= val && val <= std::u32::MAX as i64 { + Ok(MsgId::new(val as u32)) + } else { + Err(rusqlite::types::FromSqlError::OutOfRange(val)) + } + }) + } +} + /// Message ID was invalid. /// /// This usually occurs when trying to use a message ID of @@ -254,9 +273,18 @@ impl std::fmt::Display for MsgId { pub struct InvalidMsgId; #[derive( - Debug, Copy, Clone, PartialEq, FromPrimitive, ToPrimitive, Serialize, Deserialize, sqlx::Type, + Debug, + Copy, + Clone, + PartialEq, + FromPrimitive, + ToPrimitive, + FromSql, + ToSql, + Serialize, + Deserialize, )] -#[repr(i8)] +#[repr(u8)] pub(crate) enum MessengerMessage { No = 0, Yes = 1, @@ -320,10 +348,10 @@ impl Message { "Can not load special message ID {} from DB.", id ); - let row = context + let msg = context .sql - .fetch_one( - sqlx::query(concat!( + .query_row( + concat!( "SELECT", " m.id AS id,", " rfc724_mid AS rfc724mid,", @@ -351,63 +379,62 @@ impl Message { " c.blocked AS blocked", " FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id", " WHERE m.id=?;" - )) - .bind(id), + ), + paramsv![id], + |row| { + let text = match row.get_raw("txt") { + rusqlite::types::ValueRef::Text(buf) => { + match String::from_utf8(buf.to_vec()) { + Ok(t) => t, + Err(_) => { + warn!( + context, + concat!( + "dc_msg_load_from_db: could not get ", + "text column as non-lossy utf8 id {}" + ), + id + ); + String::from_utf8_lossy(buf).into_owned() + } + } + } + _ => String::new(), + }; + let msg = Message { + id: row.get("id")?, + rfc724_mid: row.get::<_, String>("rfc724mid")?, + in_reply_to: row.get::<_, Option>("mime_in_reply_to")?, + server_folder: row.get::<_, Option>("server_folder")?, + server_uid: row.get("server_uid")?, + chat_id: row.get("chat_id")?, + from_id: row.get("from_id")?, + to_id: row.get("to_id")?, + timestamp_sort: row.get("timestamp")?, + timestamp_sent: row.get("timestamp_sent")?, + timestamp_rcvd: row.get("timestamp_rcvd")?, + ephemeral_timer: row.get("ephemeral_timer")?, + ephemeral_timestamp: row.get("ephemeral_timestamp")?, + viewtype: row.get("type")?, + state: row.get("state")?, + error: Some(row.get::<_, String>("error")?) + .filter(|error| !error.is_empty()), + is_dc_message: row.get("msgrmsg")?, + mime_modified: row.get("mime_modified")?, + text: Some(text), + subject: row.get("subject")?, + param: row.get::<_, String>("param")?.parse().unwrap_or_default(), + hidden: row.get("hidden")?, + location_id: row.get("location")?, + chat_blocked: row + .get::<_, Option>("blocked")? + .unwrap_or_default(), + }; + Ok(msg) + }, ) .await?; - let text; - if let Ok(Some(buf)) = row.try_get::, _>("txt") { - if let Ok(t) = String::from_utf8(buf.to_vec()) { - text = t; - } else { - warn!( - context, - concat!( - "dc_msg_load_from_db: could not get ", - "text column as non-lossy utf8 id {}" - ), - id - ); - text = String::from_utf8_lossy(buf).into_owned(); - } - } else { - text = "".to_string(); - } - - let msg = Message { - id: row.try_get("id")?, - rfc724_mid: row.try_get("rfc724mid")?, - in_reply_to: row.try_get("mime_in_reply_to")?, - server_folder: row.try_get("server_folder")?, - server_uid: row.try_get("server_uid")?, - chat_id: row.try_get("chat_id")?, - from_id: row.try_get("from_id")?, - to_id: row.try_get("to_id")?, - timestamp_sort: row.try_get("timestamp")?, - timestamp_sent: row.try_get("timestamp_sent")?, - timestamp_rcvd: row.try_get("timestamp_rcvd")?, - ephemeral_timer: row.try_get("ephemeral_timer")?, - ephemeral_timestamp: row.try_get("ephemeral_timestamp")?, - viewtype: row.try_get("type")?, - state: row.try_get("state")?, - error: row - .try_get::, _>("error")? - .filter(|e| !e.is_empty()), - is_dc_message: row.try_get("msgrmsg")?, - mime_modified: row.try_get("mime_modified")?, - subject: row.try_get("subject")?, - param: row - .try_get::("param")? - .parse() - .unwrap_or_default(), - hidden: row.try_get("hidden")?, - location_id: row.try_get("location")?, - chat_blocked: row - .try_get::, _>("blocked")? - .unwrap_or_default(), - text: Some(text), - }; Ok(msg) } @@ -901,9 +928,8 @@ impl Message { context .sql .execute( - sqlx::query("UPDATE msgs SET param=? WHERE id=?;") - .bind(self.param.to_string()) - .bind(self.id), + "UPDATE msgs SET param=? WHERE id=?;", + paramsv![self.param.to_string(), self.id], ) .await .ok_or_log(context); @@ -913,9 +939,8 @@ impl Message { context .sql .execute( - sqlx::query("UPDATE msgs SET subject=? WHERE id=?;") - .bind(&self.subject) - .bind(&self.id), + "UPDATE msgs SET subject=? WHERE id=?;", + paramsv![self.subject, self.id], ) .await .ok_or_log(context); @@ -953,9 +978,10 @@ pub enum ContactRequestDecision { Eq, FromPrimitive, ToPrimitive, + ToSql, + FromSql, Serialize, Deserialize, - sqlx::Type, )] #[repr(u32)] pub enum MessageState { @@ -1198,7 +1224,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result = context .sql - .query_get_value(sqlx::query("SELECT txt_raw FROM msgs WHERE id=?;").bind(msg_id)) + .query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", paramsv![msg_id]) .await?; let mut ret = String::new(); @@ -1247,29 +1273,25 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result, _>>().map_err(Into::into), ) .await - .map(|rows| { - rows.map(|row| -> sqlx::Result<_> { - let row = row?; - let contact_id = row.try_get(0)?; - let ts = row.try_get(1)?; - Ok((contact_id, ts)) - }) - }) { - while let Some(row) = rows.next().await { - let (contact_id, ts) = row?; - + for (contact_id, ts) in rows { let fts = dc_timestamp_to_str(ts); ret += &format!("Read: {}", fts); - let name = Contact::load_from_db(context, contact_id) + let name = Contact::load_from_db(context, contact_id.try_into()?) .await .map(|contact| contact.get_name_n_addr()) .unwrap_or_default(); @@ -1422,7 +1444,10 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result { let headers = context .sql - .query_get_value(sqlx::query("SELECT mime_headers FROM msgs WHERE id=?;").bind(msg_id)) + .query_get_value( + "SELECT mime_headers FROM msgs WHERE id=?;", + paramsv![msg_id], + ) .await? .unwrap_or_default(); Ok(headers) @@ -1463,7 +1488,8 @@ async fn delete_poi_location(context: &Context, location_id: u32) -> bool { context .sql .execute( - sqlx::query("DELETE FROM locations WHERE independent = 1 AND id=?;").bind(location_id), + "DELETE FROM locations WHERE independent = 1 AND id=?;", + paramsv![location_id as i32], ) .await .is_ok() @@ -1473,39 +1499,40 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec) -> bool { if msg_ids.is_empty() { return false; } - let stmt = concat!( - "SELECT", - " m.chat_id AS chat_id,", - " m.state AS state,", - " c.blocked AS blocked", - " FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id", - " WHERE m.id=? AND m.chat_id>9" - ); - let mut msgs = Vec::with_capacity(msg_ids.len()); - for id in msg_ids.into_iter() { - match context - .sql - .fetch_optional(sqlx::query(stmt).bind(id)) - .await - .and_then(|row| { - if let Some(row) = row { - Ok(Some(( - row.try_get::("chat_id")?, - row.try_get::("state")?, - row.try_get::, _>("blocked")? + + let msgs = context + .sql + .with_conn(move |conn| { + let mut stmt = conn.prepare_cached(concat!( + "SELECT", + " m.chat_id AS chat_id,", + " m.state AS state,", + " c.blocked AS blocked", + " FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id", + " WHERE m.id=? AND m.chat_id>9" + ))?; + + let mut msgs = Vec::with_capacity(msg_ids.len()); + for id in msg_ids.into_iter() { + let query_res = stmt.query_row(paramsv![id], |row| { + Ok(( + row.get::<_, ChatId>("chat_id")?, + row.get::<_, MessageState>("state")?, + row.get::<_, Option>("blocked")? .unwrap_or_default(), - ))) - } else { - Ok(None) + )) + }); + if let Err(rusqlite::Error::QueryReturnedNoRows) = query_res { + continue; } - }) { - Ok(Some((chat_id, state, blocked))) => msgs.push((id, chat_id, state, blocked)), - Ok(None) => {} - Err(err) => { - warn!(context, "failed to markseen msgs: {:?}", err); + let (chat_id, state, blocked) = query_res.map_err(Into::::into)?; + msgs.push((id, chat_id, state, blocked)); } - } - } + + Ok(msgs) + }) + .await + .unwrap_or_default(); let mut updated_chat_ids = BTreeMap::new(); @@ -1547,9 +1574,8 @@ pub async fn update_msg_state(context: &Context, msg_id: MsgId, state: MessageSt context .sql .execute( - sqlx::query("UPDATE msgs SET state=? WHERE id=?;") - .bind(state) - .bind(msg_id), + "UPDATE msgs SET state=? WHERE id=?;", + paramsv![state, msg_id], ) .await .is_ok() @@ -1639,7 +1665,7 @@ pub async fn exists(context: &Context, msg_id: MsgId) -> anyhow::Result { let chat_id: Option = context .sql - .query_get_value(sqlx::query("SELECT chat_id FROM msgs WHERE id=?;").bind(msg_id)) + .query_get_value("SELECT chat_id FROM msgs WHERE id=?;", paramsv![msg_id]) .await?; if let Some(chat_id) = chat_id { @@ -1665,10 +1691,8 @@ pub async fn set_msg_failed(context: &Context, msg_id: MsgId, error: Option("msg_id")?, + row.get::<_, ChatId>("chat_id")?, + row.get::<_, Chattype>("type")?, + row.get::<_, MessageState>("state")?, + )) + }, ) - .await - .and_then(|row| { - Ok(( - row.try_get::("msg_id")?, - row.try_get::("chat_id")?, - row.try_get::("type")?, - row.try_get::("state")?, - )) - }); - + .await; if let Err(ref err) = res { info!(context, "Failed to select MDN {:?}", err); } @@ -1733,19 +1756,16 @@ pub async fn handle_mdn( let mdn_already_in_table = context .sql .exists( - sqlx::query("SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=? AND contact_id=?;") - .bind(msg_id) - .bind(from_id), + "SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=? AND contact_id=?;", + paramsv![msg_id, from_id as i32,], ) .await .unwrap_or_default(); if !mdn_already_in_table { context.sql.execute( - sqlx::query("INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?);") - .bind(msg_id) - .bind(from_id) - .bind(timestamp_sent) + "INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?);", + paramsv![msg_id, from_id as i32, timestamp_sent], ) .await .unwrap_or_default(); // TODO: better error handling @@ -1760,7 +1780,8 @@ pub async fn handle_mdn( let ist_cnt = context .sql .count( - sqlx::query("SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?;").bind(msg_id), + "SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?;", + paramsv![msg_id], ) .await?; @@ -1804,28 +1825,32 @@ pub(crate) async fn handle_ndn( // The NDN might be for a message-id that had attachments and was sent from a non-Delta Chat client. // In this case we need to mark multiple "msgids" as failed that all refer to the same message-id. - let mut rows = context + let msgs: Vec<_> = context .sql - .fetch( - sqlx::query(concat!( + .query_map( + concat!( "SELECT", " m.id AS msg_id,", " c.id AS chat_id,", " c.type AS type", " FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id", " WHERE rfc724_mid=? AND from_id=1", - )) - .bind(&failed.rfc724_mid), + ), + paramsv![failed.rfc724_mid], + |row| { + Ok(( + row.get::<_, MsgId>("msg_id")?, + row.get::<_, ChatId>("chat_id")?, + row.get::<_, Chattype>("type")?, + )) + }, + |rows| Ok(rows.collect::>()), ) .await?; let mut first = true; - while let Some(row) = rows.next().await { - let row = row?; - let msg_id = row.try_get::("msg_id")?; - let chat_id = row.try_get::("chat_id")?; - let chat_type = row.try_get::("type")?; - + for msg in msgs.into_iter() { + let (msg_id, chat_id, chat_type) = msg?; set_msg_failed(context, msg_id, error.as_ref()).await; if first { // Add only one info msg for all failed messages @@ -1874,11 +1899,12 @@ async fn ndn_maybe_add_info_msg( pub async fn get_real_msg_cnt(context: &Context) -> usize { match context .sql - .count(sqlx::query( + .count( "SELECT COUNT(*) \ FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \ WHERE m.id>9 AND m.chat_id>9 AND c.blocked=0;", - )) + paramsv![], + ) .await { Ok(res) => res, @@ -1892,11 +1918,12 @@ pub async fn get_real_msg_cnt(context: &Context) -> usize { pub async fn get_deaddrop_msg_cnt(context: &Context) -> usize { match context .sql - .count(sqlx::query( + .count( "SELECT COUNT(*) \ FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id \ WHERE c.blocked=2;", - )) + paramsv![], + ) .await { Ok(res) => res, @@ -1922,35 +1949,31 @@ pub async fn estimate_deletion_cnt( context .sql .count( - sqlx::query( - "SELECT COUNT(*) + "SELECT COUNT(*) FROM msgs m WHERE m.id > ? AND timestamp < ? AND chat_id != ? AND server_uid != 0;", - ) - .bind(DC_MSG_ID_LAST_SPECIAL) - .bind(threshold_timestamp) - .bind(self_chat_id), + paramsv![DC_MSG_ID_LAST_SPECIAL, threshold_timestamp, self_chat_id], ) .await? } else { context .sql .count( - sqlx::query( - "SELECT COUNT(*) + "SELECT COUNT(*) FROM msgs m WHERE m.id > ? AND timestamp < ? AND chat_id != ? AND chat_id != ? AND hidden = 0;", - ) - .bind(DC_MSG_ID_LAST_SPECIAL) - .bind(threshold_timestamp) - .bind(self_chat_id) - .bind(DC_CHAT_ID_TRASH), + paramsv![ + DC_MSG_ID_LAST_SPECIAL, + threshold_timestamp, + self_chat_id, + DC_CHAT_ID_TRASH + ], ) .await? }; @@ -1966,8 +1989,8 @@ pub async fn rfc724_mid_cnt(context: &Context, rfc724_mid: &str) -> usize { match context .sql .count( - sqlx::query("SELECT COUNT(*) FROM msgs WHERE rfc724_mid=? AND NOT server_uid = 0") - .bind(rfc724_mid), + "SELECT COUNT(*) FROM msgs WHERE rfc724_mid=? AND NOT server_uid = 0", + paramsv![rfc724_mid], ) .await { @@ -1989,22 +2012,22 @@ pub(crate) async fn rfc724_mid_exists( return Ok(None); } - let row = context + let res = context .sql - .fetch_optional( - sqlx::query("SELECT server_folder, server_uid, id FROM msgs WHERE rfc724_mid=?") - .bind(rfc724_mid), + .query_row_optional( + "SELECT server_folder, server_uid, id FROM msgs WHERE rfc724_mid=?", + paramsv![rfc724_mid], + |row| { + let server_folder = row.get::<_, Option>(0)?.unwrap_or_default(); + let server_uid = row.get(1)?; + let msg_id: MsgId = row.get(2)?; + + Ok((server_folder, server_uid, msg_id)) + }, ) .await?; - if let Some(row) = row { - let server_folder = row.try_get::, _>(0)?.unwrap_or_default(); - let server_uid = row.try_get::(1)?; - let msg_id: MsgId = row.try_get(2)?; - Ok(Some((server_folder, server_uid, msg_id))) - } else { - Ok(None) - } + Ok(res) } pub async fn update_server_uid( @@ -2016,13 +2039,9 @@ pub async fn update_server_uid( match context .sql .execute( - sqlx::query( - "UPDATE msgs SET server_folder=?, server_uid=? \ + "UPDATE msgs SET server_folder=?, server_uid=? \ WHERE rfc724_mid=?", - ) - .bind(server_folder.as_ref()) - .bind(server_uid as i64) - .bind(rfc724_mid), + paramsv![server_folder.as_ref(), server_uid, rfc724_mid], ) .await { diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 5be72a7c7..82171e1c0 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1,10 +1,8 @@ use std::convert::TryInto; use anyhow::{bail, ensure, format_err, Result}; -use async_std::prelude::*; use chrono::TimeZone; use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder}; -use sqlx::Row; use crate::blob::BlobObject; use crate::chat::{self, Chat}; @@ -115,42 +113,51 @@ impl<'a> MimeFactory<'a> { if chat.is_self_talk() { recipients.push((from_displayname.to_string(), from_addr.to_string())); } else { - let mut rows = context + context .sql - .fetch( - sqlx::query( - "SELECT c.authname, c.addr \ + .query_map( + "SELECT c.authname, c.addr \ FROM chats_contacts cc \ LEFT JOIN contacts c ON cc.contact_id=c.id \ WHERE cc.chat_id=? AND cc.contact_id>9;", - ) - .bind(msg.chat_id), + paramsv![msg.chat_id], + |row| { + let authname: String = row.get(0)?; + let addr: String = row.get(1)?; + Ok((authname, addr)) + }, + |rows| { + for row in rows { + let (authname, addr) = row?; + if !recipients_contain_addr(&recipients, &addr) { + recipients.push((authname, addr)); + } + } + Ok(()) + }, ) .await?; - while let Some(row) = rows.next().await { - let row = row?; - let authname: String = row.try_get(0)?; - let addr: String = row.try_get(1)?; - if !recipients_contain_addr(&recipients, &addr) { - recipients.push((authname, addr)); - } - } if !msg.is_system_message() && context.get_config_bool(Config::MdnsEnabled).await? { req_mdn = true; } } - let row = context + let (in_reply_to, references) = context .sql - .fetch_one( - sqlx::query("SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?") - .bind(msg.id), + .query_row( + "SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?", + paramsv![msg.id], + |row| { + let in_reply_to: String = row.get(0)?; + let references: String = row.get(1)?; + + Ok(( + render_rfc724_mid_list(&in_reply_to), + render_rfc724_mid_list(&references), + )) + }, ) .await?; - let (in_reply_to, references) = ( - render_rfc724_mid_list(row.try_get(0)?), - render_rfc724_mid_list(row.try_get(1)?), - ); let default_str = stock_str::status_line(context).await; let factory = MimeFactory { diff --git a/src/mimeparser.rs b/src/mimeparser.rs index dd9829ec7..33edf152b 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -4,6 +4,7 @@ use std::pin::Pin; use anyhow::{bail, Result}; use charset::Charset; +use deltachat_derive::{FromSql, ToSql}; use lettre_email::mime::{self, Mime}; use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo}; use once_cell::sync::Lazy; @@ -102,7 +103,9 @@ pub(crate) enum MailinglistType { None, } -#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] +#[derive( + Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql, +)] #[repr(u32)] pub enum SystemMessage { Unknown = 0, @@ -1249,7 +1252,8 @@ impl MimeMessage { context .sql .query_get_value( - sqlx::query("SELECT timestamp FROM msgs WHERE rfc724_mid=?").bind(field), + "SELECT timestamp FROM msgs WHERE rfc724_mid=?", + paramsv![field], ) .await? } else { @@ -1920,9 +1924,8 @@ mod tests { .ctx .sql .execute( - sqlx::query("INSERT INTO msgs (rfc724_mid, timestamp) VALUES(?,?)") - .bind("Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org") - .bind(timestamp), + "INSERT INTO msgs (rfc724_mid, timestamp) VALUES(?,?)", + paramsv!["Gr.beZgAF2Nn0-.oyaJOpeuT70@example.org", timestamp], ) .await .expect("Failed to write to the database"); diff --git a/src/peerstate.rs b/src/peerstate.rs index 18a3841b0..0d73c87bb 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -3,10 +3,6 @@ use std::collections::HashSet; use std::fmt; -use anyhow::{bail, Result}; -use num_traits::FromPrimitive; -use sqlx::{query::Query, sqlite::Sqlite, Row}; - use crate::aheader::{Aheader, EncryptPreference}; use crate::chat; use crate::constants::Blocked; @@ -15,6 +11,8 @@ use crate::events::EventType; use crate::key::{DcKey, Fingerprint, SignedPublicKey}; use crate::sql::Sql; use crate::stock_str; +use anyhow::{bail, Result}; +use num_traits::FromPrimitive; #[derive(Debug)] pub enum PeerstateKeyType { @@ -140,15 +138,12 @@ impl Peerstate { } pub async fn from_addr(context: &Context, addr: &str) -> Result> { - let query = sqlx::query( - "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ + let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \ verified_key, verified_key_fingerprint \ FROM acpeerstates \ - WHERE addr=? COLLATE NOCASE;", - ) - .bind(addr); - Self::from_stmt(context, query).await + WHERE addr=? COLLATE NOCASE;"; + Self::from_stmt(context, query, paramsv![addr]).await } pub async fn from_fingerprint( @@ -156,77 +151,71 @@ impl Peerstate { _sql: &Sql, fingerprint: &Fingerprint, ) -> Result> { - let fp = fingerprint.hex(); - let query = sqlx::query( - "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ + let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \ verified_key, verified_key_fingerprint \ FROM acpeerstates \ WHERE public_key_fingerprint=? COLLATE NOCASE \ OR gossip_key_fingerprint=? COLLATE NOCASE \ - ORDER BY public_key_fingerprint=? DESC;", - ) - .bind(&fp) - .bind(&fp) - .bind(&fp); - - Self::from_stmt(context, query).await + ORDER BY public_key_fingerprint=? DESC;"; + let fp = fingerprint.hex(); + Self::from_stmt(context, query, paramsv![fp, fp, fp]).await } - async fn from_stmt<'q, E>( + async fn from_stmt( context: &Context, - query: Query<'q, Sqlite, E>, - ) -> Result> - where - E: 'q + sqlx::IntoArguments<'q, sqlx::Sqlite>, - { - if let Some(row) = context.sql.fetch_optional(query).await? { - // all the above queries start with this: SELECT - // addr, last_seen, last_seen_autocrypt, prefer_encrypted, - // public_key, gossip_timestamp, gossip_key, public_key_fingerprint, - // gossip_key_fingerprint, verified_key, verified_key_fingerprint + query: &str, + params: Vec<&dyn crate::ToSql>, + ) -> Result> { + let peerstate = context + .sql + .query_row_optional(query, params, |row| { + // all the above queries start with this: SELECT + // addr, last_seen, last_seen_autocrypt, prefer_encrypted, + // public_key, gossip_timestamp, gossip_key, public_key_fingerprint, + // gossip_key_fingerprint, verified_key, verified_key_fingerprint - let peerstate = Peerstate { - addr: row.try_get(0)?, - last_seen: row.try_get(1)?, - last_seen_autocrypt: row.try_get(2)?, - prefer_encrypt: EncryptPreference::from_i32(row.try_get(3)?).unwrap_or_default(), - public_key: row - .try_get::<&[u8], _>(4) - .ok() - .and_then(|blob| SignedPublicKey::from_slice(blob).ok()), - public_key_fingerprint: row - .try_get::, _>(7)? - .map(|s| s.parse::()) - .transpose() - .unwrap_or_default(), - gossip_key: row - .try_get::<&[u8], _>(6) - .ok() - .and_then(|blob| SignedPublicKey::from_slice(blob).ok()), - gossip_key_fingerprint: row - .try_get::, _>(8)? - .map(|s| s.parse::()) - .transpose() - .unwrap_or_default(), - gossip_timestamp: row.try_get(5)?, - verified_key: row - .try_get::<&[u8], _>(9) - .ok() - .and_then(|blob| SignedPublicKey::from_slice(blob).ok()), - verified_key_fingerprint: row - .try_get::, _>(10)? - .map(|s| s.parse::()) - .transpose() - .unwrap_or_default(), - to_save: None, - fingerprint_changed: false, - }; + let res = Peerstate { + addr: row.get(0)?, + last_seen: row.get(1)?, + last_seen_autocrypt: row.get(2)?, + prefer_encrypt: EncryptPreference::from_i32(row.get(3)?).unwrap_or_default(), + public_key: row + .get(4) + .ok() + .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()), + public_key_fingerprint: row + .get::<_, Option>(7)? + .map(|s| s.parse::()) + .transpose() + .unwrap_or_default(), + gossip_key: row + .get(6) + .ok() + .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()), + gossip_key_fingerprint: row + .get::<_, Option>(8)? + .map(|s| s.parse::()) + .transpose() + .unwrap_or_default(), + gossip_timestamp: row.get(5)?, + verified_key: row + .get(9) + .ok() + .and_then(|blob: Vec| SignedPublicKey::from_slice(&blob).ok()), + verified_key_fingerprint: row + .get::<_, Option>(10)? + .map(|s| s.parse::()) + .transpose() + .unwrap_or_default(), + to_save: None, + fingerprint_changed: false, + }; - Ok(Some(peerstate)) - } else { - Ok(None) - } + Ok(res) + }) + .await?; + Ok(peerstate) } pub fn recalc_fingerprint(&mut self) { @@ -275,9 +264,7 @@ impl Peerstate { if self.fingerprint_changed { if let Some(contact_id) = context .sql - .query_get_value( - sqlx::query("SELECT id FROM contacts WHERE addr=?;").bind(&self.addr), - ) + .query_get_value("SELECT id FROM contacts WHERE addr=?;", paramsv![self.addr]) .await? { let (contact_chat_id, _) = @@ -437,9 +424,8 @@ impl Peerstate { pub async fn save_to_db(&self, sql: &Sql, create: bool) -> crate::sql::Result<()> { if self.to_save == Some(ToSave::All) || create { sql.execute( - (if create { - sqlx::query( - "INSERT INTO acpeerstates ( \ + if create { + "INSERT INTO acpeerstates ( \ last_seen, \ last_seen_autocrypt, \ prefer_encrypted, \ @@ -451,11 +437,9 @@ impl Peerstate { verified_key, \ verified_key_fingerprint, \ addr \ - ) VALUES(?,?,?,?,?,?,?,?,?,?,?)", - ) + ) VALUES(?,?,?,?,?,?,?,?,?,?,?)" } else { - sqlx::query( - "UPDATE acpeerstates \ + "UPDATE acpeerstates \ SET last_seen=?, \ last_seen_autocrypt=?, \ prefer_encrypted=?, \ @@ -466,30 +450,33 @@ impl Peerstate { gossip_key_fingerprint=?, \ verified_key=?, \ verified_key_fingerprint=? \ - WHERE addr=?", - ) - }) - .bind(self.last_seen) - .bind(self.last_seen_autocrypt) - .bind(self.prefer_encrypt as i64) - .bind(self.public_key.as_ref().map(|k| k.to_bytes())) - .bind(self.gossip_timestamp) - .bind(self.gossip_key.as_ref().map(|k| k.to_bytes())) - .bind(self.public_key_fingerprint.as_ref().map(|fp| fp.hex())) - .bind(self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex())) - .bind(self.verified_key.as_ref().map(|k| k.to_bytes())) - .bind(self.verified_key_fingerprint.as_ref().map(|fp| fp.hex())) - .bind(&self.addr), + WHERE addr=?" + }, + paramsv![ + self.last_seen, + self.last_seen_autocrypt, + self.prefer_encrypt as i64, + self.public_key.as_ref().map(|k| k.to_bytes()), + self.gossip_timestamp, + self.gossip_key.as_ref().map(|k| k.to_bytes()), + self.public_key_fingerprint.as_ref().map(|fp| fp.hex()), + self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()), + self.verified_key.as_ref().map(|k| k.to_bytes()), + self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()), + self.addr, + ], ) .await?; } else if self.to_save == Some(ToSave::Timestamps) { sql.execute( - sqlx::query("UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \ - WHERE addr=?;").bind( - self.last_seen).bind( - self.last_seen_autocrypt).bind( - self.gossip_timestamp).bind( - &self.addr) + "UPDATE acpeerstates SET last_seen=?, last_seen_autocrypt=?, gossip_timestamp=? \ + WHERE addr=?;", + paramsv![ + self.last_seen, + self.last_seen_autocrypt, + self.gossip_timestamp, + self.addr + ], ) .await?; } @@ -506,6 +493,12 @@ impl Peerstate { } } +impl From for rusqlite::Error { + fn from(_source: crate::key::FingerprintError) -> Self { + Self::InvalidColumnType(0, "Invalid fingerprint".into(), rusqlite::types::Type::Text) + } +} + #[cfg(test)] mod tests { use super::*; @@ -638,7 +631,7 @@ mod tests { // can be loaded without errors. ctx.ctx .sql - .execute(sqlx::query("INSERT INTO acpeerstates (addr) VALUES(?)").bind(addr)) + .execute("INSERT INTO acpeerstates (addr) VALUES(?)", paramsv![addr]) .await .expect("Failed to write to the database"); diff --git a/src/sql.rs b/src/sql.rs index 822fded24..0e44d9153 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -1,19 +1,15 @@ //! # SQLite wrapper +use async_std::sync::RwLock; + use std::collections::HashSet; +use std::convert::TryFrom; use std::path::Path; -use std::pin::Pin; use std::time::Duration; use anyhow::Context as _; use async_std::prelude::*; -use async_std::sync::RwLock; -use sqlx::{ - pool::PoolOptions, - query::Query, - sqlite::{Sqlite, SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqliteSynchronous}, - Executor, IntoArguments, Row, -}; +use rusqlite::OpenFlags; use crate::chat::{add_device_msg, update_device_icon, update_saved_messages_icon}; use crate::config::Config; @@ -26,38 +22,35 @@ use crate::param::{Param, Params}; use crate::peerstate::Peerstate; use crate::stock_str; +#[macro_export] +macro_rules! paramsv { + () => { + Vec::new() + }; + ($($param:expr),+ $(,)?) => { + vec![$(&$param as &dyn $crate::ToSql),+] + }; +} + mod error; mod migrations; pub use self::error::*; /// A wrapper around the underlying Sqlite3 object. -/// -/// We maintain two different pools to sqlite, on for reading, one for writing. -/// This can go away once https://github.com/launchbadge/sqlx/issues/459 is implemented. #[derive(Debug)] pub struct Sql { - /// Writer pool, must only have 1 connection in it. - writer: RwLock>, - /// Reader pool, maintains multiple connections for reading data. - reader: RwLock>, + pool: RwLock>>, } impl Default for Sql { fn default() -> Self { Self { - writer: RwLock::new(None), - reader: RwLock::new(None), + pool: RwLock::new(None), } } } -impl Drop for Sql { - fn drop(&mut self) { - async_std::task::block_on(self.close()); - } -} - impl Sql { pub fn new() -> Sql { Self::default() @@ -65,76 +58,50 @@ impl Sql { /// Checks if there is currently a connection to the underlying Sqlite database. pub async fn is_open(&self) -> bool { - // in read only mode the writer does not exists - self.reader.read().await.is_some() + self.pool.read().await.is_some() } /// Closes all underlying Sqlite connections. pub async fn close(&self) { - if let Some(sql) = self.writer.write().await.take() { - sql.close().await; - } - if let Some(sql) = self.reader.write().await.take() { - sql.close().await; - } + let _ = self.pool.write().await.take(); + // drop closes the connection } - async fn new_writer_pool(dbfile: impl AsRef) -> sqlx::Result { - let config = SqliteConnectOptions::new() - .journal_mode(SqliteJournalMode::Wal) - .filename(dbfile.as_ref()) - .read_only(false) - .busy_timeout(Duration::from_secs(100)) - .create_if_missing(true) - .shared_cache(true) - .synchronous(SqliteSynchronous::Normal); + pub fn new_pool( + dbfile: &Path, + readonly: bool, + ) -> anyhow::Result> { + let mut open_flags = OpenFlags::SQLITE_OPEN_NO_MUTEX; + if readonly { + open_flags.insert(OpenFlags::SQLITE_OPEN_READ_ONLY); + } else { + open_flags.insert(OpenFlags::SQLITE_OPEN_READ_WRITE); + open_flags.insert(OpenFlags::SQLITE_OPEN_CREATE); + } - PoolOptions::::new() - .max_connections(1) - .after_connect(|conn| { - Box::pin(async move { - let q = r#" -PRAGMA secure_delete=on; -PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android -"#; + // this actually creates min_idle database handles just now. + // therefore, with_init() must not try to modify the database as otherwise + // we easily get busy-errors (eg. table-creation, journal_mode etc. should be done on only one handle) + let mgr = r2d2_sqlite::SqliteConnectionManager::file(dbfile) + .with_flags(open_flags) + .with_init(|c| { + c.execute_batch(&format!( + "PRAGMA secure_delete=on; + PRAGMA busy_timeout = {}; + PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android + ", + Duration::from_secs(10).as_millis() + ))?; + Ok(()) + }); - conn.execute_many(sqlx::query(q)) - .collect::, _>>() - .await?; - Ok(()) - }) - }) - .connect_with(config) - .await - } - - async fn new_reader_pool(dbfile: impl AsRef, readonly: bool) -> sqlx::Result { - let config = SqliteConnectOptions::new() - .journal_mode(SqliteJournalMode::Wal) - .filename(dbfile.as_ref()) - .read_only(readonly) - .shared_cache(true) - .busy_timeout(Duration::from_secs(100)) - .synchronous(SqliteSynchronous::Normal); - - PoolOptions::::new() - .max_connections(10) - .after_connect(|conn| { - Box::pin(async move { - let q = r#" -PRAGMA temp_store=memory; -- Avoid SQLITE_IOERR_GETTEMPPATH errors on Android -PRAGMA query_only=1; -- Protect against writes even in read-write mode -PRAGMA read_uncommitted=1; -- This helps avoid "table locked" errors in shared cache mode -"#; - - conn.execute_many(sqlx::query(q)) - .collect::, _>>() - .await?; - Ok(()) - }) - }) - .connect_with(config) - .await + let pool = r2d2::Pool::builder() + .min_idle(Some(2)) + .max_size(10) + .connection_timeout(Duration::from_secs(60)) + .build(mgr) + .map_err(Error::ConnectionPool)?; + Ok(pool) } /// Opens the provided database and runs any necessary migrations. @@ -154,15 +121,16 @@ PRAGMA read_uncommitted=1; -- This helps avoid "table locked" errors in shared c return Err(Error::SqlAlreadyOpen.into()); } - // Open write pool - if !readonly { - *self.writer.write().await = Some(Self::new_writer_pool(&dbfile).await?); - } - - // Open read pool - *self.reader.write().await = Some(Self::new_reader_pool(&dbfile, readonly).await?); + *self.pool.write().await = Some(Self::new_pool(dbfile.as_ref(), readonly)?); if !readonly { + // journal_mode is persisted, it is sufficient to change it only for one handle. + self.with_conn(move |conn| { + conn.pragma_update(None, "journal_mode", &"WAL".to_string())?; + Ok(()) + }) + .await?; + // (1) update low-level database structure. // this should be done before updates that use high-level objects that // rely themselves on the low-level structure. @@ -175,13 +143,19 @@ PRAGMA read_uncommitted=1; -- This helps avoid "table locked" errors in shared c if recalc_fingerprints { info!(context, "[migration] recalc fingerprints"); - let mut rows = self - .fetch(sqlx::query("SELECT addr FROM acpeerstates;")) + let addrs = self + .query_map( + "SELECT addr FROM acpeerstates;", + paramsv![], + |row| row.get::<_, String>(0), + |addrs| { + addrs + .collect::, _>>() + .map_err(Into::into) + }, + ) .await?; - - while let Some(row) = rows.next().await { - let row = row?; - let addr = row.try_get(0)?; + for addr in &addrs { if let Some(ref mut peerstate) = Peerstate::from_addr(context, addr).await? { peerstate.recalc_fingerprint(); peerstate.save_to_db(self, false).await?; @@ -214,158 +188,161 @@ PRAGMA read_uncommitted=1; -- This helps avoid "table locked" errors in shared c } /// Execute the given query, returning the number of affected rows. - pub async fn execute<'q, E>(&self, query: Query<'q, Sqlite, E>) -> Result - where - E: 'q + IntoArguments<'q, Sqlite>, - { - let lock = self.writer.read().await; - let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?; - - let rows = pool.execute(query).await?; - Ok(rows.rows_affected()) + pub async fn execute( + &self, + query: impl AsRef, + params: Vec<&dyn crate::ToSql>, + ) -> Result { + let conn = self.get_conn().await?; + let res = conn.execute(query.as_ref(), params)?; + Ok(res) } /// Executes the given query, returning the last inserted row ID. - pub async fn insert<'q, E>(&self, query: Query<'q, Sqlite, E>) -> Result - where - E: 'q + IntoArguments<'q, Sqlite>, - { - let lock = self.writer.read().await; - let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?; - - let rows = pool.execute(query).await?; - Ok(rows.last_insert_rowid()) - } - - /// Execute many queries. - pub async fn execute_many<'q, E>(&self, query: Query<'q, Sqlite, E>) -> Result<()> - where - E: 'q + IntoArguments<'q, Sqlite>, - { - let lock = self.writer.read().await; - let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?; - - pool.execute_many(query) - .collect::>>() - .await?; - Ok(()) - } - - /// Fetch the given query. - pub async fn fetch<'q, E>( + pub async fn insert( &self, - query: Query<'q, Sqlite, E>, - ) -> Result::Row>> + Send + 'q> - where - E: 'q + IntoArguments<'q, Sqlite>, - { - let lock = self.reader.read().await; - let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?; - - let rows = pool.fetch(query); - Ok(rows) + query: impl AsRef, + params: Vec<&dyn crate::ToSql>, + ) -> anyhow::Result { + let conn = self.get_conn().await?; + conn.execute(query.as_ref(), params)?; + Ok(usize::try_from(conn.last_insert_rowid())?) } - /// Fetch exactly one row, errors if no row is found. - pub async fn fetch_one<'q, E>( + /// Prepares and executes the statement and maps a function over the resulting rows. + /// Then executes the second function over the returned iterator and returns the + /// result of that function. + pub async fn query_map( &self, - query: Query<'q, Sqlite, E>, - ) -> Result<::Row> + sql: impl AsRef, + params: Vec<&dyn crate::ToSql>, + f: F, + mut g: G, + ) -> Result where - E: 'q + IntoArguments<'q, Sqlite>, + F: FnMut(&rusqlite::Row) -> rusqlite::Result, + G: FnMut(rusqlite::MappedRows) -> Result, { - let lock = self.reader.read().await; - let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?; + let sql = sql.as_ref(); - let row = pool.fetch_one(query).await?; - Ok(row) + let conn = self.get_conn().await?; + let mut stmt = conn.prepare(sql)?; + let res = stmt.query_map(¶ms, f)?; + g(res) } - /// Fetches at most one row. - pub async fn fetch_optional<'e, 'q, E>( + pub async fn get_conn( &self, - query: Query<'q, Sqlite, E>, - ) -> Result::Row>> + ) -> Result> { + let lock = self.pool.read().await; + let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?; + let conn = pool.get()?; + + Ok(conn) + } + + pub async fn with_conn(&self, g: G) -> anyhow::Result where - E: 'q + IntoArguments<'q, Sqlite>, + H: Send + 'static, + G: Send + + 'static + + FnOnce( + r2d2::PooledConnection, + ) -> anyhow::Result, { - let lock = self.reader.read().await; + let lock = self.pool.read().await; + let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?; + let conn = pool.get()?; + + g(conn) + } + + pub async fn with_conn_async(&self, mut g: G) -> Result + where + G: FnMut(r2d2::PooledConnection) -> Fut, + Fut: Future> + Send, + { + let lock = self.pool.read().await; let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?; - let row = pool.fetch_optional(query).await?; - Ok(row) + let conn = pool.get()?; + g(conn).await } /// Used for executing `SELECT COUNT` statements only. Returns the resulting count. - pub async fn count<'e, 'q, E>(&self, query: Query<'q, Sqlite, E>) -> Result - where - E: 'q + IntoArguments<'q, Sqlite>, - { - use std::convert::TryFrom; - - let row = self.fetch_one(query).await?; - let count: i64 = row.try_get(0)?; - - Ok(usize::try_from(count).map_err::(Into::into)?) + pub async fn count( + &self, + query: impl AsRef, + params: Vec<&dyn crate::ToSql>, + ) -> anyhow::Result { + let count: isize = self.query_row(query, params, |row| row.get(0)).await?; + Ok(usize::try_from(count)?) } /// Used for executing `SELECT COUNT` statements only. Returns `true`, if the count is at least /// one, `false` otherwise. - pub async fn exists<'e, 'q, E>(&self, query: Query<'q, Sqlite, E>) -> Result - where - E: 'q + IntoArguments<'q, Sqlite>, - { - let count = self.count(query).await?; + pub async fn exists(&self, sql: &str, params: Vec<&dyn crate::ToSql>) -> Result { + let count = self.count(sql, params).await?; Ok(count > 0) } + /// Execute a query which is expected to return one row. + pub async fn query_row( + &self, + query: impl AsRef, + params: Vec<&dyn crate::ToSql>, + f: F, + ) -> Result + where + F: FnOnce(&rusqlite::Row) -> rusqlite::Result, + { + let conn = self.get_conn().await?; + let res = conn.query_row(query.as_ref(), params, f)?; + Ok(res) + } + /// Execute the function inside a transaction. /// /// If the function returns an error, the transaction will be rolled back. If it does not return an /// error, the transaction will be committed. - pub async fn transaction(&self, callback: F) -> Result + pub async fn transaction(&self, callback: G) -> anyhow::Result where - F: for<'c> FnOnce( - &'c mut sqlx::Transaction<'_, Sqlite>, - ) -> Pin> + 'c + Send>> - + 'static - + Send - + Sync, - R: Send, + H: Send + 'static, + G: Send + 'static + FnOnce(&mut rusqlite::Transaction<'_>) -> anyhow::Result, { - let lock = self.writer.read().await; - let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?; + self.with_conn(move |mut conn| { + let conn2 = &mut conn; + let mut transaction = conn2.transaction()?; + let ret = callback(&mut transaction); - let mut transaction = pool.begin().await?; - let ret = callback(&mut transaction).await; - - match ret { - Ok(ret) => { - transaction.commit().await?; - - Ok(ret) + match ret { + Ok(ret) => { + transaction.commit()?; + Ok(ret) + } + Err(err) => { + transaction.rollback()?; + Err(err) + } } - Err(err) => { - transaction.rollback().await?; - - Err(err) - } - } + }) + .await } /// Query the database if the requested table already exists. - pub async fn table_exists(&self, name: impl AsRef) -> Result { - let q = format!("PRAGMA table_info(\"{}\")", name.as_ref()); + pub async fn table_exists(&self, name: impl AsRef) -> anyhow::Result { + let name = name.as_ref().to_string(); + self.with_conn(move |conn| { + let mut exists = false; + conn.pragma(None, "table_info", &name, |_row| { + // will only be executed if the info was found + exists = true; + Ok(()) + })?; - let lock = self.reader.read().await; - let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?; - - let mut rows = pool.fetch(sqlx::query(&q)); - if let Some(first_row) = rows.next().await { - Ok(first_row.is_ok()) - } else { - Ok(false) - } + Ok(exists) + }) + .await } /// Check if a column exists in a given table. @@ -373,43 +350,62 @@ PRAGMA read_uncommitted=1; -- This helps avoid "table locked" errors in shared c &self, table_name: impl AsRef, col_name: impl AsRef, - ) -> Result { - let q = format!("PRAGMA table_info(\"{}\")", table_name.as_ref()); - let lock = self.reader.read().await; - let pool = lock.as_ref().ok_or(Error::SqlNoConnection)?; - - let mut rows = pool.fetch(sqlx::query(&q)); - while let Some(row) = rows.next().await { - let row = row?; - + ) -> anyhow::Result { + let table_name = table_name.as_ref().to_string(); + let col_name = col_name.as_ref().to_string(); + self.with_conn(move |conn| { + let mut exists = false; // `PRAGMA table_info` returns one row per column, // each row containing 0=cid, 1=name, 2=type, 3=notnull, 4=dflt_value + conn.pragma(None, "table_info", &table_name, |row| { + let curr_name: String = row.get(1)?; + if col_name == curr_name { + exists = true; + } + Ok(()) + })?; - let curr_name: &str = row.try_get(1)?; - if col_name.as_ref() == curr_name { - return Ok(true); - } - } + Ok(exists) + }) + .await + } - Ok(false) + /// Execute a query which is expected to return zero or one row. + pub async fn query_row_optional( + &self, + sql: impl AsRef, + params: Vec<&dyn crate::ToSql>, + f: F, + ) -> anyhow::Result> + where + F: FnOnce(&rusqlite::Row) -> rusqlite::Result, + { + let res = match self.query_row(sql, params, f).await { + Ok(res) => Ok(Some(res)), + Err(Error::Sql(rusqlite::Error::QueryReturnedNoRows)) => Ok(None), + Err(Error::Sql(rusqlite::Error::InvalidColumnType( + _, + _, + rusqlite::types::Type::Null, + ))) => Ok(None), + Err(err) => Err(err), + }?; + Ok(res) } /// Executes a query which is expected to return one row and one /// column. If the query does not return a value or returns SQL /// `NULL`, returns `Ok(None)`. - pub async fn query_get_value<'e, 'q, E, T>( + pub async fn query_get_value( &self, - query: Query<'q, Sqlite, E>, - ) -> Result> + query: &str, + params: Vec<&dyn crate::ToSql>, + ) -> anyhow::Result> where - E: 'q + IntoArguments<'q, Sqlite>, - T: for<'r> sqlx::Decode<'r, Sqlite> + sqlx::Type, + T: rusqlite::types::FromSql, { - let res = self - .fetch_optional(query) - .await? - .map(|row| row.get::(0)); - Ok(res) + self.query_row_optional(query, params, |row| row.get::<_, T>(0)) + .await } /// Set private configuration options. @@ -424,26 +420,27 @@ PRAGMA read_uncommitted=1; -- This helps avoid "table locked" errors in shared c let key = key.as_ref(); if let Some(value) = value { let exists = self - .exists(sqlx::query("SELECT COUNT(*) FROM config WHERE keyname=?;").bind(key)) + .exists( + "SELECT COUNT(*) FROM config WHERE keyname=?;", + paramsv![key], + ) .await?; if exists { self.execute( - sqlx::query("UPDATE config SET value=? WHERE keyname=?;") - .bind(value) - .bind(key), + "UPDATE config SET value=? WHERE keyname=?;", + paramsv![(*value).to_string(), key.to_string()], ) .await?; } else { self.execute( - sqlx::query("INSERT INTO config (keyname, value) VALUES (?, ?);") - .bind(key) - .bind(value), + "INSERT INTO config (keyname, value) VALUES (?, ?);", + paramsv![key.to_string(), (*value).to_string()], ) .await?; } } else { - self.execute(sqlx::query("DELETE FROM config WHERE keyname=?;").bind(key)) + self.execute("DELETE FROM config WHERE keyname=?;", paramsv![key]) .await?; } @@ -457,7 +454,8 @@ PRAGMA read_uncommitted=1; -- This helps avoid "table locked" errors in shared c } let value = self .query_get_value( - sqlx::query("SELECT value FROM config WHERE keyname=?;").bind(key.as_ref()), + "SELECT value FROM config WHERE keyname=?;", + paramsv![key.as_ref().to_string()], ) .await .context(format!("failed to fetch raw config: {}", key.as_ref()))?; @@ -539,14 +537,21 @@ pub async fn housekeeping(context: &Context) -> Result<()> { ) .await?; - let mut rows = context + context .sql - .fetch(sqlx::query("SELECT value FROM config;")) - .await?; - while let Some(row) = rows.next().await { - let row: String = row?.try_get(0)?; - maybe_add_file(&mut files_in_use, row); - } + .query_map( + "SELECT value FROM config;", + paramsv![], + |row| row.get::<_, String>(0), + |rows| { + for row in rows { + maybe_add_file(&mut files_in_use, row?); + } + Ok(()) + }, + ) + .await + .context("housekeeping: failed to SELECT value FROM config")?; info!(context, "{} files in use.", files_in_use.len(),); /* go through directory and delete unused files */ @@ -665,14 +670,22 @@ async fn maybe_add_from_param( query: &str, param_id: Param, ) -> Result<()> { - let mut rows = sql.fetch(sqlx::query(query)).await?; - while let Some(row) = rows.next().await { - let row: String = row?.try_get(0)?; - let param: Params = row.parse().unwrap_or_default(); - if let Some(file) = param.get(param_id) { - maybe_add_file(files_in_use, file); - } - } + sql.query_map( + query, + paramsv![], + |row| row.get::<_, String>(0), + |rows| { + for row in rows { + let param: Params = row?.parse().unwrap_or_default(); + if let Some(file) = param.get(param_id) { + maybe_add_file(files_in_use, file); + } + } + Ok(()) + }, + ) + .await + .context(format!("housekeeping: failed to add_from_param {}", query))?; Ok(()) } @@ -681,25 +694,15 @@ async fn maybe_add_from_param( /// have a server UID. async fn prune_tombstones(sql: &Sql) -> Result<()> { sql.execute( - sqlx::query( - "DELETE FROM msgs \ + "DELETE FROM msgs \ WHERE (chat_id = ? OR hidden) \ AND server_uid = 0", - ) - .bind(DC_CHAT_ID_TRASH), + paramsv![DC_CHAT_ID_TRASH], ) .await?; Ok(()) } -/// Returns the SQLite version as a string; e.g., `"3.16.2"` for version 3.16.2. -pub fn version() -> &'static str { - #[allow(unsafe_code)] - let cstr = unsafe { std::ffi::CStr::from_ptr(libsqlite3_sys::sqlite3_libversion()) }; - cstr.to_str() - .expect("SQLite version string is not valid UTF8 ?!") -} - #[cfg(test)] mod test { use async_std::fs::File; @@ -789,49 +792,4 @@ mod test { let a = t.get_config(Config::Selfavatar).await.unwrap().unwrap(); assert_eq!(avatar_bytes, &async_std::fs::read(&a).await.unwrap()[..]); } - - /// Regression test. - /// - /// Previously the code checking for existence of `config` table - /// checked it with `PRAGMA table_info("config")` but did not - /// drain `SqlitePool.fetch` result, only using the first row - /// returned. As a result, prepared statement for `PRAGMA` was not - /// finalized early enough, leaving reader connection in a broken - /// state after reopening the database, when `config` table - /// existed and `PRAGMA` returned non-empty result. - /// - /// Statements were not finalized due to a bug in sqlx: - /// https://github.com/launchbadge/sqlx/issues/1147 - #[async_std::test] - async fn test_db_reopen() -> Result<()> { - use tempfile::tempdir; - - // The context is used only for logging. - let t = TestContext::new().await; - - // Create a separate empty database for testing. - let dir = tempdir()?; - let dbfile = dir.path().join("testdb.sqlite"); - let sql = Sql::new(); - - // Create database with all the tables. - sql.open(&t, &dbfile, false).await.unwrap(); - sql.close().await; - - // Reopen the database - sql.open(&t, &dbfile, false).await?; - sql.execute( - sqlx::query("INSERT INTO config (keyname, value) VALUES (?, ?);") - .bind("foo") - .bind("bar"), - ) - .await?; - - let value: Option = sql - .query_get_value(sqlx::query("SELECT value FROM config WHERE keyname=?;").bind("foo")) - .await?; - assert_eq!(value.unwrap(), "bar"); - - Ok(()) - } } diff --git a/src/sql/error.rs b/src/sql/error.rs index 5fa71a36f..f272bf6bc 100644 --- a/src/sql/error.rs +++ b/src/sql/error.rs @@ -1,7 +1,9 @@ #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("Sqlx: {0:?}")] - Sqlx(#[from] sqlx::Error), + #[error("Sqlite error: {0:?}")] + Sql(#[from] rusqlite::Error), + #[error("Sqlite Connection Pool Error: {0:?}")] + ConnectionPool(#[from] r2d2::Error), #[error("Sqlite: Connection closed")] SqlNoConnection, #[error("Sqlite: Already open")] diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 1a99220ae..c26a0bfac 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -1,5 +1,3 @@ -use async_std::prelude::*; - use super::{Result, Sql}; use crate::config::Config; use crate::constants::ShowEmails; @@ -19,22 +17,15 @@ pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool)> { if !sql.table_exists("config").await? { info!(context, "First time init: creating tables",); - sql.transaction(move |conn| { - Box::pin(async move { - sqlx::query(TABLES) - .execute_many(&mut *conn) - .await - .collect::, _>>() - .await?; + sql.transaction(move |transaction| { + transaction.execute_batch(TABLES)?; - // set raw config inside the transaction - sqlx::query("INSERT INTO config (keyname, value) VALUES (?, ?);") - .bind(VERSION_CFG) - .bind(format!("{}", dbversion_before_update)) - .execute(&mut *conn) - .await?; - Ok(()) - }) + // set raw config inside the transaction + transaction.execute( + "INSERT INTO config (keyname, value) VALUES (?, ?);", + paramsv![VERSION_CFG, format!("{}", dbversion_before_update)], + )?; + Ok(()) }) .await?; } else { @@ -417,9 +408,10 @@ ALTER TABLE msgs ADD COLUMN mime_modified INTEGER DEFAULT 0;"#, if dbversion < 73 { use Config::*; info!(context, "[migration] v73"); - sql.execute(sqlx::query( + sql.execute( r#" -CREATE TABLE imap_sync (folder TEXT PRIMARY KEY, uidvalidity INTEGER DEFAULT 0, uid_next INTEGER DEFAULT 0);"#), +CREATE TABLE imap_sync (folder TEXT PRIMARY KEY, uidvalidity INTEGER DEFAULT 0, uid_next INTEGER DEFAULT 0);"#, +paramsv![] ) .await?; for c in &[ @@ -479,24 +471,16 @@ impl Sql { } async fn execute_migration(&self, query: &'static str, version: i32) -> Result<()> { - let query = sqlx::query(query); - self.transaction(move |conn| { - Box::pin(async move { - query - .execute_many(&mut *conn) - .await - .collect::, _>>() - .await?; + self.transaction(move |transaction| { + transaction.execute_batch(query)?; - // set raw config inside the transaction - sqlx::query("UPDATE config SET value=? WHERE keyname=?;") - .bind(format!("{}", version)) - .bind(VERSION_CFG) - .execute(&mut *conn) - .await?; + // set raw config inside the transaction + transaction.execute( + "UPDATE config SET value=? WHERE keyname=?;", + paramsv![format!("{}", version), VERSION_CFG], + )?; - Ok(()) - }) + Ok(()) }) .await?; diff --git a/src/test_utils.rs b/src/test_utils.rs index b9d6c7537..aef9be4da 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -15,7 +15,6 @@ use async_std::{channel, pin::Pin}; use async_std::{future::Future, task}; use chat::ChatItem; use once_cell::sync::Lazy; -use sqlx::Row; use tempfile::{tempdir, TempDir}; use crate::chat::{self, Chat, ChatId}; @@ -228,25 +227,22 @@ impl TestContext { let row = self .ctx .sql - .fetch_one( - sqlx::query( - r#" + .query_row( + r#" SELECT id, foreign_id, param FROM jobs WHERE action=? ORDER BY desired_timestamp DESC; "#, - ) - .bind(Action::SendMsgToSmtp), + paramsv![Action::SendMsgToSmtp], + |row| { + let id: u32 = row.get(0)?; + let foreign_id: u32 = row.get(1)?; + let param: String = row.get(2)?; + Ok((id, foreign_id, param)) + }, ) - .await - .and_then(|row| { - let id: u32 = row.try_get(0)?; - let foreign_id: u32 = row.try_get(1)?; - let param: String = row.try_get(2)?; - Ok((id, foreign_id, param)) - }); - + .await; if let Ok(row) = row { break row; } @@ -266,7 +262,7 @@ impl TestContext { .to_abs_path(); self.ctx .sql - .execute(sqlx::query("DELETE FROM jobs WHERE id=?;").bind(rowid)) + .execute("DELETE FROM jobs WHERE id=?;", paramsv![rowid]) .await .expect("failed to remove job"); update_msg_state(&self.ctx, id, MessageState::OutDelivered).await; diff --git a/src/token.rs b/src/token.rs index 84e49fc5b..994da4880 100644 --- a/src/token.rs +++ b/src/token.rs @@ -4,12 +4,16 @@ //! //! Tokens are used in countermitm verification protocols. +use deltachat_derive::{FromSql, ToSql}; + use crate::chat::ChatId; use crate::context::Context; use crate::dc_tools::{dc_create_id, time}; /// Token namespace -#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, sqlx::Type)] +#[derive( + Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql, +)] #[repr(u32)] pub enum Namespace { Unknown = 0, @@ -32,25 +36,16 @@ pub async fn save(context: &Context, namespace: Namespace, foreign_id: Option context .sql .execute( - sqlx::query( - "INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);" - ) - .bind(namespace) - .bind(foreign_id) - .bind(&token) - .bind(time()), + "INSERT INTO tokens (namespc, foreign_id, token, timestamp) VALUES (?, ?, ?, ?);", + paramsv![namespace, foreign_id, token, time()], ) .await .ok(), None => context .sql .execute( - sqlx::query( - "INSERT INTO tokens (namespc, token, timestamp) VALUES (?, ?, ?);" - ) - .bind(namespace) - .bind(&token) - .bind(time()), + "INSERT INTO tokens (namespc, token, timestamp) VALUES (?, ?, ?);", + paramsv![namespace, token, time()], ) .await .ok(), @@ -68,10 +63,9 @@ pub async fn lookup( Some(chat_id) => { context .sql - .query_get_value( - sqlx::query("SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;") - .bind(namespace) - .bind(chat_id), + .query_get_value::( + "SELECT token FROM tokens WHERE namespc=? AND foreign_id=?;", + paramsv![namespace, chat_id], ) .await? } @@ -79,9 +73,9 @@ pub async fn lookup( None => { context .sql - .query_get_value( - sqlx::query("SELECT token FROM tokens WHERE namespc=? AND foreign_id=0;") - .bind(namespace), + .query_get_value::( + "SELECT token FROM tokens WHERE namespc=? AND foreign_id=0;", + paramsv![namespace], ) .await? } @@ -105,9 +99,8 @@ pub async fn exists(context: &Context, namespace: Namespace, token: &str) -> boo context .sql .exists( - sqlx::query("SELECT COUNT(*) FROM tokens WHERE namespc=? AND token=?;") - .bind(namespace) - .bind(token), + "SELECT COUNT(*) FROM tokens WHERE namespc=? AND token=?;", + paramsv![namespace, token], ) .await .unwrap_or_default()