things3_core/database/
mappers.rs1use crate::{
10 database::{safe_timestamp_convert, things_date_to_naive_date},
11 error::Result as ThingsResult,
12 models::{Project, Task, TaskStatus, TaskType, ThingsId},
13};
14use chrono::{DateTime, Utc};
15use sqlx::sqlite::SqliteRow;
16use sqlx::Row;
17
18fn id_from_row(s: String) -> ThingsId {
22 ThingsId::from_trusted(s)
23}
24
25fn optional_id_from_row(opt: Option<String>) -> Option<ThingsId> {
27 opt.map(ThingsId::from_trusted)
28}
29
30pub fn map_task_row(row: &SqliteRow) -> ThingsResult<Task> {
39 let uuid = id_from_row(row.get("uuid"));
40
41 let title: String = row.get("title");
42
43 let status_i32: i32 = row.get("status");
44 let status = match status_i32 {
45 2 => TaskStatus::Canceled,
46 3 => TaskStatus::Completed,
47 _ => TaskStatus::Incomplete, };
49
50 let type_i32: i32 = row.get("type");
51 let task_type = match type_i32 {
52 1 => TaskType::Project,
53 2 => TaskType::Heading,
54 _ => TaskType::Todo,
55 };
56
57 let notes: Option<String> = row.get("notes");
58
59 let start_date = row
60 .get::<Option<i64>, _>("startDate")
61 .and_then(things_date_to_naive_date);
62
63 let deadline = row
64 .get::<Option<i64>, _>("deadline")
65 .and_then(things_date_to_naive_date);
66
67 let creation_ts: f64 = row.get("creationDate");
68 let created = {
69 let ts = safe_timestamp_convert(creation_ts);
70 DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
71 };
72
73 let modification_ts: f64 = row.get("userModificationDate");
74 let modified = {
75 let ts = safe_timestamp_convert(modification_ts);
76 DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
77 };
78
79 let stop_date = row.get::<Option<f64>, _>("stopDate").and_then(|ts| {
80 let ts_i64 = safe_timestamp_convert(ts);
81 DateTime::from_timestamp(ts_i64, 0)
82 });
83
84 let project_uuid = optional_id_from_row(row.get::<Option<String>, _>("project"));
85 let area_uuid = optional_id_from_row(row.get::<Option<String>, _>("area"));
86 let parent_uuid = optional_id_from_row(row.get::<Option<String>, _>("heading"));
87
88 let tags = row
90 .get::<Option<String>, _>("tags_csv")
91 .map(|s| {
92 s.split('\x1f')
93 .filter(|t| !t.is_empty())
94 .map(str::to_owned)
95 .collect()
96 })
97 .unwrap_or_default();
98
99 Ok(Task {
100 uuid,
101 title,
102 status,
103 task_type,
104 notes,
105 start_date,
106 deadline,
107 created,
108 modified,
109 stop_date,
110 project_uuid,
111 area_uuid,
112 parent_uuid,
113 tags,
114 children: Vec::new(),
115 })
116}
117
118pub fn map_project_row(row: &SqliteRow) -> Project {
120 Project {
121 uuid: id_from_row(row.get("uuid")),
122 title: row.get("title"),
123 status: match row.get::<i32, _>("status") {
124 1 => TaskStatus::Completed,
125 2 => TaskStatus::Canceled,
126 3 => TaskStatus::Trashed,
127 _ => TaskStatus::Incomplete,
128 },
129 area_uuid: optional_id_from_row(row.get::<Option<String>, _>("area")),
130 notes: row.get("notes"),
131 deadline: row
132 .get::<Option<i64>, _>("deadline")
133 .and_then(|ts| DateTime::from_timestamp(ts, 0))
134 .map(|dt| dt.date_naive()),
135 start_date: row
136 .get::<Option<i64>, _>("startDate")
137 .and_then(|ts| DateTime::from_timestamp(ts, 0))
138 .map(|dt| dt.date_naive()),
139 tags: Vec::new(),
140 tasks: Vec::new(),
141 created: {
142 let ts = safe_timestamp_convert(row.get::<f64, _>("creationDate"));
143 DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
144 },
145 modified: {
146 let ts = safe_timestamp_convert(row.get::<f64, _>("userModificationDate"));
147 DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
148 },
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn id_from_row_preserves_native_things_id() {
158 let id = id_from_row("R4t2G8Q63aGZq4epMHNeCr".to_string());
159 assert_eq!(id.as_str(), "R4t2G8Q63aGZq4epMHNeCr");
160 }
161
162 #[test]
163 fn id_from_row_preserves_hyphenated_uuid() {
164 let id = id_from_row("550e8400-e29b-41d4-a716-446655440000".to_string());
165 assert_eq!(id.as_str(), "550e8400-e29b-41d4-a716-446655440000");
166 }
167
168 #[test]
169 fn optional_id_from_row_passes_through_none() {
170 assert!(optional_id_from_row(None).is_none());
171 }
172
173 #[test]
174 fn optional_id_from_row_wraps_some() {
175 let opt = optional_id_from_row(Some("ABC123XYZ456789012345".to_string()));
176 assert_eq!(opt.unwrap().as_str(), "ABC123XYZ456789012345");
177 }
178}