fix(query_row_optional): do not treat rows with NULL as missing rows

Instead of treating NULL type error
as absence of the row,
handle NULL values with SQL.
Previously we sometimes
accidentally treated a single column
being NULL as the lack of the whole row.
This commit is contained in:
link2xt
2024-10-02 02:11:40 +00:00
parent 5711f2fe3a
commit 8a88479d8f
6 changed files with 60 additions and 22 deletions

View File

@@ -640,7 +640,7 @@ pub struct MessageInfo {
error: Option<String>, error: Option<String>,
rfc724_mid: String, rfc724_mid: String,
server_urls: Vec<String>, server_urls: Vec<String>,
hop_info: Option<String>, hop_info: String,
} }
impl MessageInfo { impl MessageInfo {

View File

@@ -1042,7 +1042,13 @@ impl ChatId {
pub(crate) async fn get_timestamp(self, context: &Context) -> Result<Option<i64>> { pub(crate) async fn get_timestamp(self, context: &Context) -> Result<Option<i64>> {
let timestamp = context let timestamp = context
.sql .sql
.query_get_value("SELECT MAX(timestamp) FROM msgs WHERE chat_id=?", (self,)) .query_get_value(
"SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=?
HAVING COUNT(*) > 0",
(self,),
)
.await?; .await?;
Ok(timestamp) Ok(timestamp)
} }
@@ -1251,7 +1257,7 @@ impl ChatId {
) -> Result<Option<(String, String, String)>> { ) -> Result<Option<(String, String, String)>> {
self.parent_query( self.parent_query(
context, context,
"rfc724_mid, mime_in_reply_to, mime_references", "rfc724_mid, mime_in_reply_to, IFNULL(mime_references, '')",
state_out_min, state_out_min,
|row: &rusqlite::Row| { |row: &rusqlite::Row| {
let rfc724_mid: String = row.get(0)?; let rfc724_mid: String = row.get(0)?;
@@ -1405,7 +1411,10 @@ impl ChatId {
context context
.sql .sql
.query_get_value( .query_get_value(
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND state!=?", "SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=? AND state!=?
HAVING COUNT(*) > 0",
(self, MessageState::OutDraft), (self, MessageState::OutDraft),
) )
.await? .await?
@@ -1421,7 +1430,10 @@ impl ChatId {
context context
.sql .sql
.query_get_value( .query_get_value(
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND hidden=0 AND state>?", "SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=? AND hidden=0 AND state>?
HAVING COUNT(*) > 0",
(self, MessageState::InFresh), (self, MessageState::InFresh),
) )
.await? .await?
@@ -4345,7 +4357,10 @@ pub async fn add_device_msg_with_importance(
if let Some(last_msg_time) = context if let Some(last_msg_time) = context
.sql .sql
.query_get_value( .query_get_value(
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=?", "SELECT MAX(timestamp)
FROM msgs
WHERE chat_id=?
HAVING COUNT(*) > 0",
(chat_id,), (chat_id,),
) )
.await? .await?

View File

@@ -69,7 +69,7 @@ use std::num::ParseIntError;
use std::str::FromStr; use std::str::FromStr;
use std::time::{Duration, UNIX_EPOCH}; use std::time::{Duration, UNIX_EPOCH};
use anyhow::{ensure, Result}; use anyhow::{ensure, Context as _, Result};
use async_channel::Receiver; use async_channel::Receiver;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::time::timeout; use tokio::time::timeout;
@@ -176,9 +176,13 @@ impl ChatId {
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer> { pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer> {
let timer = context let timer = context
.sql .sql
.query_get_value("SELECT ephemeral_timer FROM chats WHERE id=?;", (self,)) .query_get_value(
.await?; "SELECT IFNULL(ephemeral_timer, 0) FROM chats WHERE id=?",
Ok(timer.unwrap_or_default()) (self,),
)
.await?
.with_context(|| format!("Chat {self} not found"))?;
Ok(timer)
} }
/// Set ephemeral timer value without sending a message. /// Set ephemeral timer value without sending a message.
@@ -509,7 +513,8 @@ async fn next_delete_device_after_timestamp(context: &Context) -> Result<Option<
FROM msgs FROM msgs
WHERE chat_id > ? WHERE chat_id > ?
AND chat_id != ? AND chat_id != ?
AND chat_id != ?; AND chat_id != ?
HAVING count(*) > 0
"#, "#,
(DC_CHAT_ID_TRASH, self_chat_id, device_chat_id), (DC_CHAT_ID_TRASH, self_chat_id, device_chat_id),
) )
@@ -533,7 +538,8 @@ async fn next_expiration_timestamp(context: &Context) -> Option<i64> {
SELECT min(ephemeral_timestamp) SELECT min(ephemeral_timestamp)
FROM msgs FROM msgs
WHERE ephemeral_timestamp != 0 WHERE ephemeral_timestamp != 0
AND chat_id != ?; AND chat_id != ?
HAVING count(*) > 0
"#, "#,
(DC_CHAT_ID_TRASH,), // Trash contains already deleted messages, skip them (DC_CHAT_ID_TRASH,), // Trash contains already deleted messages, skip them
) )
@@ -1410,4 +1416,14 @@ mod tests {
Ok(()) Ok(())
} }
/// Tests that `.get_ephemeral_timer()` returns an error for invalid chat ID.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_ephemeral_timer_wrong_chat_id() -> Result<()> {
let context = TestContext::new().await;
let chat_id = ChatId::new(12345);
assert!(chat_id.get_ephemeral_timer(&context).await.is_err());
Ok(())
}
} }

View File

@@ -219,11 +219,13 @@ impl MsgId {
} }
/// Returns information about hops of a message, used for message info /// Returns information about hops of a message, used for message info
pub async fn hop_info(self, context: &Context) -> Result<Option<String>> { pub async fn hop_info(self, context: &Context) -> Result<String> {
context let hop_info = context
.sql .sql
.query_get_value("SELECT hop_info FROM msgs WHERE id=?", (self,)) .query_get_value("SELECT IFNULL(hop_info, '') FROM msgs WHERE id=?", (self,))
.await .await?
.with_context(|| format!("Message {self} not found"))?;
Ok(hop_info)
} }
/// Returns detailed message information in a multi-line text form. /// Returns detailed message information in a multi-line text form.
@@ -366,7 +368,11 @@ impl MsgId {
let hop_info = self.hop_info(context).await?; let hop_info = self.hop_info(context).await?;
ret += "\n\n"; ret += "\n\n";
ret += &hop_info.unwrap_or_else(|| "No Hop Info".to_owned()); if hop_info.is_empty() {
ret += "No Hop Info";
} else {
ret += &hop_info;
}
Ok(ret) Ok(ret)
} }
@@ -1998,7 +2004,9 @@ pub(crate) async fn rfc724_mid_exists_ex(
.query_row_optional( .query_row_optional(
&("SELECT id, timestamp_sent, MIN(".to_string() &("SELECT id, timestamp_sent, MIN(".to_string()
+ expr + expr
+ ") FROM msgs WHERE rfc724_mid=? ORDER BY timestamp_sent DESC"), + ") FROM msgs WHERE rfc724_mid=?
HAVING COUNT(*) > 0 -- Prevent MIN(expr) from returning NULL when there are no rows.
ORDER BY timestamp_sent DESC"),
(rfc724_mid,), (rfc724_mid,),
|row| { |row| {
let msg_id: MsgId = row.get(0)?; let msg_id: MsgId = row.get(0)?;

View File

@@ -201,7 +201,8 @@ impl MimeFactory {
let (in_reply_to, references) = context let (in_reply_to, references) = context
.sql .sql
.query_row( .query_row(
"SELECT mime_in_reply_to, mime_references FROM msgs WHERE id=?", "SELECT mime_in_reply_to, IFNULL(mime_references, '')
FROM msgs WHERE id=?",
(msg.id,), (msg.id,),
|row| { |row| {
let in_reply_to: String = row.get(0)?; let in_reply_to: String = row.get(0)?;

View File

@@ -558,15 +558,13 @@ impl Sql {
self.call(move |conn| match conn.query_row(sql.as_ref(), params, f) { self.call(move |conn| match conn.query_row(sql.as_ref(), params, f) {
Ok(res) => Ok(Some(res)), Ok(res) => Ok(Some(res)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(rusqlite::Error::InvalidColumnType(_, _, rusqlite::types::Type::Null)) => Ok(None),
Err(err) => Err(err.into()), Err(err) => Err(err.into()),
}) })
.await .await
} }
/// Executes a query which is expected to return one row and one /// Executes a query which is expected to return one row and one
/// column. If the query does not return a value or returns SQL /// column. If the query does not return any rows, returns `Ok(None)`.
/// `NULL`, returns `Ok(None)`.
pub async fn query_get_value<T>( pub async fn query_get_value<T>(
&self, &self,
query: &str, query: &str,