use std::num::NonZeroU32;
use sqlx::SqlitePool;
use crate::{Kind, short_id::UidAndShortId};
#[derive(Debug, Clone)]
pub struct ShortIds {
pool: SqlitePool,
}
impl ShortIds {
pub fn new(pool: SqlitePool) -> Self {
Self { pool }
}
pub async fn get_by_short_id(
&self,
short_id: NonZeroU32,
) -> Result<Option<UidAndShortId>, sqlx::Error> {
let row: Option<(String, String)> =
sqlx::query_as("SELECT uid, kind FROM short_ids WHERE short_id = ?;")
.bind(i64::from(short_id.get()))
.fetch_optional(&self.pool)
.await?;
match row {
Some((uid, kind)) => Ok(if let Some(kind) = Kind::parse_stable(&kind) {
Some(UidAndShortId {
uid,
short_id,
kind,
})
} else {
tracing::warn!(kind, "unknown short_id kind");
None
}),
None => Ok(None),
}
}
pub async fn get_or_assign_short_id(
&self,
uid: &str,
kind: Kind,
) -> Result<NonZeroU32, sqlx::Error> {
const SQL: &str = "\
INSERT INTO short_ids (uid, kind) VALUES (?, ?)
ON CONFLICT(uid) DO NOTHING
RETURNING short_id;
";
if let Some((short_id,)) = sqlx::query_as::<_, (NonZeroU32,)>(SQL)
.bind(uid)
.bind(kind.to_str_stable())
.fetch_optional(&self.pool)
.await?
{
return Ok(short_id);
}
let (short_id,): (NonZeroU32,) =
sqlx::query_as("SELECT short_id FROM short_ids WHERE uid = ?")
.bind(uid)
.fetch_one(&self.pool)
.await?;
Ok(short_id)
}
pub async fn truncate(&self) -> Result<(), sqlx::Error> {
sqlx::query("DELETE FROM short_ids;")
.execute(&self.pool)
.await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use super::*;
use crate::{
Event as EventTrait, EventStatus, Id, LooseDateTime, Priority, Todo as TodoTrait,
TodoStatus,
};
use aimcal_ical::{
Completed, Description, DtEnd, DtStamp, DtStart, Due, PercentComplete,
Priority as IcalPriority, Summary, Uid, VEvent, VTodo,
};
use aimcal_ical::{
EventStatus as IcalEventStatus, EventStatusValue, TodoStatus as IcalTodoStatus,
TodoStatusValue,
};
use crate::short_id::ShortIds;
async fn setup_test_db() -> crate::db::Db {
crate::db::Db::open(None)
.await
.expect("Failed to create test database")
}
fn test_event(uid: &str, summary: &str) -> VEvent<String> {
let now = jiff::Zoned::now();
VEvent {
uid: Uid::new(uid.to_string()),
dt_stamp: DtStamp::new(now.datetime()),
dt_start: DtStart::new(LooseDateTime::Local(now.clone())),
dt_end: Some(DtEnd::new(LooseDateTime::Local(now.clone()))),
duration: None,
summary: Some(Summary::new(summary.to_string())),
description: Some(Description::new("Test description".to_string())),
status: Some(IcalEventStatus::new(EventStatusValue::Confirmed)),
location: None,
geo: None,
url: None,
organizer: None,
attendees: Vec::new(),
last_modified: None,
transparency: None,
sequence: None,
priority: None,
classification: None,
resources: None,
categories: None,
rrule: None,
rdates: Vec::new(),
ex_dates: Vec::new(),
x_properties: Vec::new(),
retained_properties: Vec::new(),
alarms: Vec::new(),
}
}
fn test_todo(uid: &str, summary: &str) -> VTodo<String> {
let now = jiff::Zoned::now();
let utc_now = now.with_time_zone(jiff::tz::TimeZone::UTC);
VTodo {
uid: Uid::new(uid.to_string()),
dt_stamp: DtStamp::new(now.datetime()),
dt_start: None,
due: Some(Due::new(LooseDateTime::Local(now.clone()))),
completed: Some(Completed::new(utc_now.datetime())),
duration: None,
summary: Some(Summary::new(summary.to_string())),
description: Some(Description::new("Test todo description".to_string())),
location: None,
geo: None,
url: None,
organizer: None,
attendees: Vec::new(),
last_modified: None,
status: Some(IcalTodoStatus::new(TodoStatusValue::NeedsAction)),
sequence: None,
priority: Some(IcalPriority::new(5)),
percent_complete: Some(PercentComplete::new(0)),
classification: None,
resources: None,
categories: None,
rrule: None,
rdates: Vec::new(),
ex_dates: Vec::new(),
x_properties: Vec::new(),
retained_properties: Vec::new(),
alarms: Vec::new(),
}
}
#[tokio::test]
async fn short_ids_get_or_assign_short_id_assigns_new_id() {
let db = setup_test_db().await;
let uid = "test-uid-1";
let short_id = db
.short_ids
.get_or_assign_short_id(uid, Kind::Todo)
.await
.expect("Failed to assign short ID");
assert_eq!(short_id.get(), 1);
}
#[tokio::test]
async fn short_ids_get_or_assign_short_id_returns_existing_id() {
let db = setup_test_db().await;
let uid = "test-uid-1";
let first_id = db
.short_ids
.get_or_assign_short_id(uid, Kind::Todo)
.await
.expect("Failed to assign short ID");
let second_id = db
.short_ids
.get_or_assign_short_id(uid, Kind::Todo)
.await
.expect("Failed to get short ID");
assert_eq!(first_id, second_id);
assert_eq!(first_id.get(), 1);
}
#[tokio::test]
async fn short_ids_get_by_short_id_returns_correct_data() {
let db = setup_test_db().await;
let uid = "test-uid-1";
let short_id = db
.short_ids
.get_or_assign_short_id(uid, Kind::Event)
.await
.expect("Failed to assign short ID");
let result = db
.short_ids
.get_by_short_id(short_id)
.await
.expect("Failed to get by short ID");
assert!(result.is_some());
let data = result.unwrap();
assert_eq!(data.uid, uid);
assert_eq!(data.short_id, short_id);
assert_eq!(data.kind, Kind::Event);
}
#[tokio::test]
async fn short_ids_get_by_short_id_returns_none_for_missing_id() {
let db = setup_test_db().await;
let result = db
.short_ids
.get_by_short_id(NonZeroU32::new(999).unwrap())
.await
.expect("Failed to get by short ID");
assert!(result.is_none());
}
#[tokio::test]
async fn short_ids_get_or_assign_short_id_increments_for_new_uids() {
let db = setup_test_db().await;
let id1 = db
.short_ids
.get_or_assign_short_id("uid-1", Kind::Todo)
.await
.expect("Failed to assign short ID");
let id2 = db
.short_ids
.get_or_assign_short_id("uid-2", Kind::Todo)
.await
.expect("Failed to assign short ID");
let id3 = db
.short_ids
.get_or_assign_short_id("uid-3", Kind::Event)
.await
.expect("Failed to assign short ID");
assert_eq!(id1.get(), 1);
assert_eq!(id2.get(), 2);
assert_eq!(id3.get(), 3);
}
#[tokio::test]
async fn short_ids_handles_same_uid_with_same_kind() {
let db = setup_test_db().await;
let uid = "test-uid-1";
let id1 = db
.short_ids
.get_or_assign_short_id(uid, Kind::Todo)
.await
.expect("Failed to assign short ID");
let id2 = db
.short_ids
.get_or_assign_short_id(uid, Kind::Todo)
.await
.expect("Failed to get short ID");
assert_eq!(id1, id2);
assert_eq!(id1.get(), 1);
}
#[tokio::test]
async fn short_ids_truncate_removes_all_entries() {
let db = setup_test_db().await;
for i in 1..=5 {
let uid = format!("uid-{i}");
db.short_ids
.get_or_assign_short_id(&uid, Kind::Todo)
.await
.expect("Failed to assign short ID");
}
db.short_ids
.truncate()
.await
.expect("Failed to truncate short_ids");
let result = db
.short_ids
.get_by_short_id(NonZeroU32::new(1).unwrap())
.await
.expect("Failed to get by short ID");
assert!(result.is_none());
}
#[tokio::test]
async fn short_ids_truncate_resets_id_generation() {
let db = setup_test_db().await;
let id1 = db
.short_ids
.get_or_assign_short_id("uid-1", Kind::Todo)
.await
.expect("Failed to assign short ID");
assert_eq!(id1.get(), 1);
db.short_ids
.truncate()
.await
.expect("Failed to truncate short_ids");
let id2 = db
.short_ids
.get_or_assign_short_id("uid-2", Kind::Todo)
.await
.expect("Failed to assign short ID");
assert_eq!(id2.get(), 1);
}
#[tokio::test]
async fn short_ids_assign_sequential_ids_starting_from_one() {
let db = setup_test_db().await;
let id1 = db
.short_ids
.get_or_assign_short_id("uid-1", Kind::Event)
.await
.expect("Failed to assign short ID");
let id2 = db
.short_ids
.get_or_assign_short_id("uid-2", Kind::Event)
.await
.expect("Failed to assign short ID");
let id3 = db
.short_ids
.get_or_assign_short_id("uid-3", Kind::Event)
.await
.expect("Failed to assign short ID");
assert_eq!(id1.get(), 1);
assert_eq!(id2.get(), 2);
assert_eq!(id3.get(), 3);
}
#[tokio::test]
async fn short_ids_increment_across_different_kinds() {
let db = setup_test_db().await;
let id1 = db
.short_ids
.get_or_assign_short_id("event-1", Kind::Event)
.await
.expect("Failed to assign short ID");
let id2 = db
.short_ids
.get_or_assign_short_id("todo-1", Kind::Todo)
.await
.expect("Failed to assign short ID");
let id3 = db
.short_ids
.get_or_assign_short_id("event-2", Kind::Event)
.await
.expect("Failed to assign short ID");
assert_eq!(id1.get(), 1);
assert_eq!(id2.get(), 2);
assert_eq!(id3.get(), 3);
}
#[tokio::test]
async fn short_ids_flush_removes_all_mappings() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db.clone());
db.short_ids
.get_or_assign_short_id("uid-1", Kind::Event)
.await
.expect("Failed to assign short ID");
db.short_ids
.get_or_assign_short_id("uid-2", Kind::Todo)
.await
.expect("Failed to assign short ID");
short_ids.flush().await.expect("Failed to flush short IDs");
let result = db
.short_ids
.get_by_short_id(1.try_into().unwrap())
.await
.expect("Failed to check short ID");
assert!(result.is_none());
}
#[tokio::test]
async fn short_ids_restart_from_one_after_flush() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db.clone());
let id1 = db
.short_ids
.get_or_assign_short_id("uid-1", Kind::Event)
.await
.expect("Failed to assign short ID");
assert_eq!(id1.get(), 1);
let id2 = db
.short_ids
.get_or_assign_short_id("uid-2", Kind::Event)
.await
.expect("Failed to assign short ID");
assert_eq!(id2.get(), 2);
short_ids.flush().await.expect("Failed to flush short IDs");
let id3 = db
.short_ids
.get_or_assign_short_id("uid-3", Kind::Event)
.await
.expect("Failed to assign short ID");
assert_eq!(id3.get(), 1);
}
#[tokio::test]
async fn short_ids_preserve_existing_mapping_on_reassign() {
let db = setup_test_db().await;
let uid = "persistent-uid";
let id1 = db
.short_ids
.get_or_assign_short_id(uid, Kind::Event)
.await
.expect("Failed to assign short ID");
let id2 = db
.short_ids
.get_or_assign_short_id(uid, Kind::Event)
.await
.expect("Failed to get short ID");
assert_eq!(id1, id2);
assert_eq!(id1.get(), 1);
}
#[tokio::test]
async fn short_ids_continue_sequentially_after_flush_with_new_uids() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db.clone());
db.short_ids
.get_or_assign_short_id("uid-1", Kind::Event)
.await
.expect("Failed to assign short ID");
db.short_ids
.get_or_assign_short_id("uid-2", Kind::Event)
.await
.expect("Failed to assign short ID");
db.short_ids
.get_or_assign_short_id("uid-3", Kind::Event)
.await
.expect("Failed to assign short ID");
short_ids.flush().await.expect("Failed to flush short IDs");
let id1 = db
.short_ids
.get_or_assign_short_id("new-uid-1", Kind::Event)
.await
.expect("Failed to assign short ID");
let id2 = db
.short_ids
.get_or_assign_short_id("new-uid-2", Kind::Event)
.await
.expect("Failed to assign short ID");
assert_eq!(id1.get(), 1);
assert_eq!(id2.get(), 2);
}
#[tokio::test]
async fn short_ids_get_returns_uid_and_short_id_for_short_id() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db.clone());
let uid = "test-uid-event-1";
let assigned_short_id = db
.short_ids
.get_or_assign_short_id(uid, Kind::Event)
.await
.expect("Failed to assign short ID");
let id = Id::ShortIdOrUid(assigned_short_id.get().to_string());
let result = short_ids
.get(&id)
.await
.expect("Failed to get short ID mapping");
assert!(result.is_some());
let data = result.unwrap();
assert_eq!(data.uid, uid);
assert_eq!(data.short_id, assigned_short_id);
assert_eq!(data.kind, Kind::Event);
}
#[tokio::test]
async fn short_ids_get_returns_none_for_uid_variant() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let uid = "test-uid-123";
let id = Id::Uid(uid.to_string());
let result = short_ids
.get(&id)
.await
.expect("Failed to get short ID mapping");
assert!(result.is_none());
}
#[tokio::test]
async fn short_ids_get_returns_none_for_non_existent_short_id() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let id = Id::ShortIdOrUid("999".to_string());
let result = short_ids
.get(&id)
.await
.expect("Failed to get short ID mapping");
assert!(result.is_none());
}
#[tokio::test]
async fn short_ids_get_uid_resolves_short_id_to_uid() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db.clone());
let uid = "test-uid-todo-1";
let assigned_short_id = db
.short_ids
.get_or_assign_short_id(uid, Kind::Todo)
.await
.expect("Failed to assign short ID");
let id = Id::ShortIdOrUid(assigned_short_id.get().to_string());
let resolved_uid = short_ids.get_uid(&id).await.expect("Failed to resolve UID");
assert_eq!(resolved_uid, uid);
}
#[tokio::test]
async fn short_ids_get_uid_returns_uid_string_for_uid_variant() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let uid = "direct-uid-string";
let id = Id::Uid(uid.to_string());
let resolved_uid = short_ids.get_uid(&id).await.expect("Failed to resolve UID");
assert_eq!(resolved_uid, uid);
}
#[tokio::test]
async fn short_ids_get_uid_returns_string_for_short_id_or_uid_lookalike() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let uid_string = "abc-123-def-456";
let id = Id::ShortIdOrUid(uid_string.to_string());
let resolved_uid = short_ids.get_uid(&id).await.expect("Failed to resolve UID");
assert_eq!(resolved_uid, uid_string);
}
#[tokio::test]
async fn short_ids_get_returns_none_for_invalid_short_id_zero() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let id = Id::ShortIdOrUid("0".to_string());
let result = short_ids
.get(&id)
.await
.expect("Failed to get short ID mapping");
assert!(result.is_none());
}
#[tokio::test]
async fn event_with_short_id_delegates_short_id() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let event = test_event("event-1", "Test Event");
let wrapped = short_ids
.event(event)
.await
.expect("Failed to wrap event with short ID");
assert_eq!(wrapped.short_id(), Some(wrapped.short_id));
assert!(wrapped.short_id.get() > 0);
}
#[tokio::test]
async fn event_with_short_id_delegates_uid() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let uid = "event-delegates-uid";
let event = test_event(uid, "Test Event");
let wrapped = short_ids
.event(event)
.await
.expect("Failed to wrap event with short ID");
assert_eq!(wrapped.uid(), Cow::Borrowed(uid));
}
#[tokio::test]
async fn event_with_short_id_delegates_summary() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let summary = "Event Summary Test";
let event = test_event("event-1", summary);
let wrapped = short_ids
.event(event)
.await
.expect("Failed to wrap event with short ID");
assert_eq!(wrapped.summary(), Cow::Borrowed(summary));
}
#[tokio::test]
async fn event_with_short_id_delegates_description() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let event = test_event("event-1", "Test");
let wrapped = short_ids
.event(event)
.await
.expect("Failed to wrap event with short ID");
assert_eq!(
wrapped.description(),
Some(Cow::Borrowed("Test description"))
);
}
#[tokio::test]
async fn event_with_short_id_delegates_status() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let event = test_event("event-1", "Test");
let wrapped = short_ids
.event(event)
.await
.expect("Failed to wrap event with short ID");
assert_eq!(wrapped.status(), Some(EventStatus::Confirmed));
}
#[tokio::test]
async fn event_with_short_id_delegates_start_end() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let event = test_event("event-1", "Test");
let wrapped = short_ids
.event(event)
.await
.expect("Failed to wrap event with short ID");
assert!(wrapped.start().is_some());
assert!(wrapped.end().is_some());
}
#[tokio::test]
async fn todo_with_short_id_delegates_short_id() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let todo = test_todo("todo-1", "Test Todo");
let wrapped = short_ids
.todo(todo)
.await
.expect("Failed to wrap todo with short ID");
assert_eq!(wrapped.short_id(), Some(wrapped.short_id));
assert!(wrapped.short_id.get() > 0);
}
#[tokio::test]
async fn todo_with_short_id_delegates_uid() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let uid = "todo-delegates-uid";
let todo = test_todo(uid, "Test Todo");
let wrapped = short_ids
.todo(todo)
.await
.expect("Failed to wrap todo with short ID");
assert_eq!(wrapped.uid(), Cow::Borrowed(uid));
}
#[tokio::test]
async fn todo_with_short_id_delegates_summary() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let summary = "Todo Summary Test";
let todo = test_todo("todo-1", summary);
let wrapped = short_ids
.todo(todo)
.await
.expect("Failed to wrap todo with short ID");
assert_eq!(wrapped.summary(), Cow::Borrowed(summary));
}
#[tokio::test]
async fn todo_with_short_id_delegates_description() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let todo = test_todo("todo-1", "Test");
let wrapped = short_ids
.todo(todo)
.await
.expect("Failed to wrap todo with short ID");
assert_eq!(
wrapped.description(),
Some(Cow::Borrowed("Test todo description"))
);
}
#[tokio::test]
async fn todo_with_short_id_delegates_status() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let todo = test_todo("todo-1", "Test");
let wrapped = short_ids
.todo(todo)
.await
.expect("Failed to wrap todo with short ID");
assert_eq!(wrapped.status(), TodoStatus::NeedsAction);
}
#[tokio::test]
async fn todo_with_short_id_delegates_priority() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let todo = test_todo("todo-1", "Test");
let wrapped = short_ids
.todo(todo)
.await
.expect("Failed to wrap todo with short ID");
assert_eq!(wrapped.priority(), Priority::P5);
}
#[tokio::test]
async fn todo_with_short_id_delegates_percent_complete() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let todo = test_todo("todo-1", "Test");
let wrapped = short_ids
.todo(todo)
.await
.expect("Failed to wrap todo with short ID");
assert_eq!(wrapped.percent_complete(), Some(0));
}
#[tokio::test]
async fn todo_with_short_id_delegates_due_and_completed() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let todo = test_todo("todo-1", "Test");
let wrapped = short_ids
.todo(todo)
.await
.expect("Failed to wrap todo with short ID");
assert!(wrapped.due().is_some());
assert!(wrapped.completed().is_some());
}
#[tokio::test]
async fn short_ids_events_wraps_multiple_events() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let events = vec![
test_event("event-1", "Event 1"),
test_event("event-2", "Event 2"),
test_event("event-3", "Event 3"),
];
let wrapped = short_ids
.events(events)
.await
.expect("Failed to wrap events with short IDs");
assert_eq!(wrapped.len(), 3);
let first = wrapped.first().expect("should have first element");
let second = wrapped.get(1).expect("should have second element");
let third = wrapped.get(2).expect("should have third element");
assert!(first.short_id.get() > 0);
assert!(second.short_id.get() > first.short_id.get());
assert!(third.short_id.get() > second.short_id.get());
}
#[tokio::test]
async fn short_ids_todos_wraps_multiple_todos() {
let db = setup_test_db().await;
let short_ids = ShortIds::new(db);
let todos = vec![
test_todo("todo-1", "Todo 1"),
test_todo("todo-2", "Todo 2"),
test_todo("todo-3", "Todo 3"),
];
let wrapped = short_ids
.todos(todos)
.await
.expect("Failed to wrap todos with short IDs");
assert_eq!(wrapped.len(), 3);
let first = wrapped.first().expect("should have first element");
let second = wrapped.get(1).expect("should have second element");
let third = wrapped.get(2).expect("should have third element");
assert!(first.short_id.get() > 0);
assert!(second.short_id.get() > first.short_id.get());
assert!(third.short_id.get() > second.short_id.get());
}
}