1use crate::argument_parser::{CollectionType, SyncResources};
2use crate::data_stores::replicate_datastore::Datastore;
3use crate::data_stores::sync_requests::{
4 self, AllSync, AllSyncResult, ClassDataSync, SelectSync, SyncAction, SyncOptions,
5 TermSyncResult,
6};
7use crate::errors::{Error, SyncError};
8use log::{trace, warn};
9use rusqlite::{Connection, Transaction, params_from_iter};
10use serde_json::Value;
11use std::collections::{HashMap, HashSet};
12use std::path::Path;
13use std::result::Result;
14use std::{env, fs};
15
16const DEFAULT_MAX_RECORDS: u16 = 10_000;
17
18pub struct Sqlite {
19 conn: Connection,
20}
21
22impl Sqlite {
23 #[cfg(not(test))]
24 pub fn new() -> Result<Sqlite, Error> {
25 let db_path = env::var("SQLITE_DB_PATH")?;
26 let file_path = Path::new(&db_path);
27 Ok(Sqlite {
28 conn: Sqlite::get_db_connection(file_path)?,
29 })
30 }
31
32 #[cfg(test)]
33 pub fn new() -> Result<Sqlite, Error> {
34 let db_path = env::var("SQLITE_DB_PATH")?;
35 let file_path = Path::new(&db_path);
36 Ok(Sqlite {
37 conn: Sqlite::get_db_connection(file_path)?,
38 })
39 }
40
41 fn get_db_connection(file_path: &Path) -> Result<Connection, Error> {
42 if !file_path.exists() {
43 if let Some(parent_dir) = file_path.parent() {
44 fs::create_dir_all(parent_dir)?;
45 }
46 fs::File::create(file_path)?;
47 let up_migration_classy =
49 fs::read_to_string("src/data_stores/sqlite/migrations/001.up.sql").unwrap();
50 let up_migration_sync =
51 fs::read_to_string("src/data_stores/sqlite/migrations/002.up.sql").unwrap();
52 let conn = Connection::open(file_path)?;
53 conn.execute_batch(&up_migration_classy)?;
54 conn.execute_batch(&up_migration_sync)?;
55 Ok(conn)
56 } else {
57 Ok(Connection::open(file_path)?)
59 }
60 }
61 fn execute_sync(conn: &Transaction, sync: ClassDataSync) -> Result<(), Error> {
64 sync.verify_columns()?;
65 let result = match sync.sync_action {
66 SyncAction::Update => {
67 if sync.relevant_fields.is_none()
68 || sync.relevant_fields.as_ref().unwrap().is_empty()
69 {
70 warn!("Update sync with no changes: `{:?}`", sync);
71 return Ok(());
72 }
73 let mut arg_counter: usize = 0;
74 let mut param_args: Vec<rusqlite::types::Value> = vec![];
75 let mut set_values = vec![];
76 for (col, val) in sync
77 .relevant_fields
78 .as_ref()
79 .unwrap_or(&HashMap::new())
80 .iter()
81 {
82 match convert_to_sql_value(val) {
83 Ok(v) => param_args.push(v),
84 Err(e) => return Err(e),
85 }
86 arg_counter += 1;
87 set_values.push(format!("{col} = ?{arg_counter}"))
88 }
89 let set_values = set_values.join(", ");
90 let mut where_values = vec![];
91 for (col, val) in sync.pk_fields.iter() {
92 match convert_to_sql_value(val) {
93 Ok(v) => param_args.push(v),
94 Err(e) => return Err(e),
95 }
96 arg_counter += 1;
97 where_values.push(format!("{col} = ?{arg_counter}"))
98 }
99
100 let where_values = where_values.join(" AND ");
101 let sql_string = format!(
102 "UPDATE {} SET {} WHERE {};",
103 sync.table_name, set_values, where_values
104 );
105 trace!("update: {}", &sql_string);
106 let maybe_statement = conn.prepare_cached(&sql_string);
107
108 maybe_statement.map(|mut s| s.execute(params_from_iter(param_args)))
109 }
110 SyncAction::Delete => {
111 let mut arg_counter: usize = 0;
112 let mut param_args: Vec<rusqlite::types::Value> = vec![];
113 let mut where_values = vec![];
114 for (col, val) in sync.pk_fields.iter() {
115 match convert_to_sql_value(val) {
116 Ok(v) => param_args.push(v),
117 Err(e) => return Err(e),
118 }
119 arg_counter += 1;
120 where_values.push(format!("{col} = ?{arg_counter}"))
121 }
122 let where_values = where_values.join(" AND ");
123
124 let sql_string = format!("DELETE FROM {} WHERE {};", sync.table_name, where_values);
125 trace!("delete: {}", &sql_string);
126 let maybe_statement = conn.prepare_cached(&sql_string);
127 maybe_statement.map(|mut s| s.execute(params_from_iter(param_args)))
128 }
129 SyncAction::Insert => {
130 let mut arg_counter: usize = 0;
131 let mut param_args: Vec<rusqlite::types::Value> = vec![];
132 let mut columns = vec![];
133 let mut values = vec![];
134 for (col, val) in sync.pk_fields.iter() {
135 match convert_to_sql_value(val) {
136 Ok(v) => param_args.push(v),
137 Err(e) => return Err(e),
138 }
139 arg_counter += 1;
140 columns.push(col.to_string());
141 values.push(format!("?{arg_counter}"))
142 }
143 for (col, val) in sync
144 .relevant_fields
145 .as_ref()
146 .unwrap_or(&HashMap::new())
147 .iter()
148 {
149 match convert_to_sql_value(val) {
150 Ok(v) => param_args.push(v),
151 Err(e) => return Err(e),
152 }
153 arg_counter += 1;
154 columns.push(col.to_string());
155 values.push(format!("?{arg_counter}"))
156 }
157 let columns = columns.join(", ");
158 let values = values.join(", ");
159
160 let sql_string = format!(
161 "INSERT INTO {} ({}) VALUES ({});",
162 sync.table_name, columns, values
163 );
164 trace!("insert: {} {:?}", &sql_string, param_args);
165 let maybe_statement = conn.prepare_cached(&sql_string);
166
167 maybe_statement.map(|mut s| s.execute(params_from_iter(param_args)))
168 }
169 };
170 match result {
171 Ok(statement) => match statement {
172 Ok(num) => {
173 if num != 1 {
174 warn!("Query affected {} rows expected 1", num)
175 }
176 Ok(())
177 }
178 Err(err) => Err(SyncError::new(format!("Error executing query {err:?}"))),
179 },
180 Err(err) => Err(SyncError::new(
181 format!("Error preparing statement {err:?}",),
182 )),
183 }
184 }
185
186 fn is_all_sync(&mut self) -> Result<bool, Error> {
187 Ok(self.conn.query_row(
188 r#"
189 SELECT EXISTS (
190 SELECT 1 FROM _previous_all_collections
191 );
192 "#,
193 (),
194 |row| row.get(0),
195 )?)
196 }
197
198 fn is_select_sync(&mut self) -> Result<bool, Error> {
199 Ok(self.conn.query_row(
200 r#"
201 SELECT (
202 EXISTS (SELECT 1 FROM _school_strategies)
203 );
204 "#,
205 (),
206 |row| row.get(0),
207 )?)
208 }
209 fn get_all_request_options(&mut self) -> Result<AllSync, Error> {
210 if self.is_select_sync()? {
211 return Err(SyncError::new(
212 "Cannot sync all because term sync and or school sync was ran before",
213 ));
214 }
215 let last_sync: u64 = self.conn.query_row(
216 r#"
217 SELECT COALESCE(MAX(synced_at), 0)
218 FROM _previous_all_collections;
219 "#,
220 (),
221 |row| row.get(0),
222 )?;
223 Ok(AllSync {
224 last_sync,
225 max_records_count: Some(DEFAULT_MAX_RECORDS),
226 })
227 }
228
229 fn get_select_request_options(&mut self) -> Result<SelectSync, Error> {
230 if self.is_all_sync()? {
231 return Err(SyncError::new(
232 "Cannot sync select because sync all has been run previously",
233 ));
234 }
235 let mut all_school_query = self.conn.prepare(
236 r#"
237 SELECT s.school_id, COALESCE(MAX(p.synced_at), 0) AS sequence
238 FROM _school_strategies s
239 LEFT JOIN _previous_school_collections p ON s.school_id = p.school_id
240 WHERE s.term_collection_id IS NULL
241 GROUP BY s.school_id
242 ;
243 "#,
244 )?;
245 let school_to_last_sequence = all_school_query
246 .query_map((), |r| {
247 let res: (String, u64) = (r.get(0)?, r.get(1)?);
248 Ok(res)
249 })?
250 .collect::<Result<HashMap<_, _>, _>>()?;
251
252 let mut term_school_query = self.conn.prepare(
253 r#"
254 SELECT s.school_id, s.term_collection_id, COALESCE(MAX(p.synced_at), 0) AS sequence
255 FROM _school_strategies s
256 LEFT JOIN _previous_term_collections p
257 ON s.school_id = p.school_id AND s.term_collection_id = p.term_collection_id
258 WHERE s.term_collection_id IS NOT NULL
259 GROUP BY s.school_id, s.term_collection_id
260 ;
261 "#,
262 )?;
263
264 let term_to_last_sequence = term_school_query
265 .query_map((), |r| {
266 let res: ((String, String), u64) = ((r.get(0)?, r.get(1)?), r.get(2)?);
267 Ok(res)
268 })?
269 .collect::<Result<HashMap<_, _>, _>>()?;
270
271 let mut term_sync = SelectSync::new();
272 for ((school_id, term_collection_id), sequence) in term_to_last_sequence {
273 if school_to_last_sequence.contains_key(&school_id) {
274 term_sync.add_exclusion(school_id, term_collection_id, sequence)?;
279 } else {
280 term_sync.add_term_sync(school_id, term_collection_id, sequence)?;
281 }
282 }
283
284 for (school_id, sequence) in school_to_last_sequence {
285 term_sync.add_school_sync(school_id, sequence)?;
286 }
287 Ok(term_sync)
288 }
289}
290
291impl Datastore for Sqlite {
292 fn execute_all_request_sync(&mut self, all_sync_response: AllSyncResult) -> Result<(), Error> {
293 let tx = self.conn.transaction()?;
294 tx.execute(
295 r#" INSERT INTO _previous_all_collections (synced_at)
296 VALUES ($1);
297 "#,
298 (all_sync_response.new_latest_sync,),
299 )?;
300 for sync in all_sync_response.sync_data.into_iter() {
301 Sqlite::execute_sync(&tx, sync)?
302 }
303 tx.commit()?;
304 Ok(())
305 }
306
307 fn execute_select_request_sync(
308 &mut self,
309 select_sync_request: SelectSync,
310 select_sync_response: TermSyncResult,
311 ) -> Result<(), Error> {
312 let _ = select_sync_request;
313 let tx = self.conn.transaction()?;
314 for (school_id, entry) in &select_sync_response.new_sync_term_sequences {
315 match entry {
316 sync_requests::SchoolEntry::TermToSequence(term_sequence) => {
317 for (term, sequence) in term_sequence {
318 tx.execute(
319 r#"
320 INSERT INTO _previous_term_collections (synced_at, school_id, term_collection_id)
321 VALUES ($1, $2, $3);
322 "#,
323 (sequence, school_id, term),
324 )?;
325 }
326 }
327 sync_requests::SchoolEntry::Sequence(sequence) => {
328 tx.execute(
329 r#"
330 INSERT INTO _previous_school_collections (synced_at, school_id)
331 VALUES ($1, $2);
332 "#,
333 (sequence, school_id),
334 )?;
335 }
336 }
337 }
338 for sync in select_sync_response.sync_data.into_iter() {
339 Sqlite::execute_sync(&tx, sync)?
340 }
341 tx.commit()?;
342 Ok(())
343 }
344
345 fn generate_sync_options(&mut self) -> Result<SyncOptions, Error> {
346 match (self.is_select_sync()?, self.is_all_sync()?) {
347 (true, true) => Err(SyncError::new(
348 "Dirty db state cannot be both select and all sync",
349 )),
350 (true, false) => Ok(SyncOptions::Select(self.get_select_request_options()?)),
351 (false, true) => Ok(SyncOptions::All(self.get_all_request_options()?)),
352 (false, false) => Err(SyncError::new(
353 "Sync stratgey not set! Set the resources to sync.",
354 )),
355 }
356 }
357
358 fn set_request_sync_resources(&mut self, resources: SyncResources) -> Result<(), Error> {
359 match resources {
360 SyncResources::Everything => {
361 if self.is_select_sync()? {
362 return Err(SyncError::new(
363 "Cannot set sync all because select syncs have already been done",
364 ));
365 }
366 if self.is_all_sync()? {
368 return Ok(());
369 }
370 self.conn.execute(
371 r#"
372 INSERT INTO _previous_all_collections (synced_at)
373 VALUES (0);
374 "#,
375 (),
376 )?;
377 }
378 SyncResources::Select(select_sync_options) => {
379 if self.is_all_sync()? {
380 return Err(SyncError::new(
381 "Cannot set sync select because sync all has already been done",
382 ));
383 }
384 let mut get_full_schools = self.conn.prepare(
385 r#"
386 SELECT school_id, term_collection_id
387 FROM _school_strategies
388 "#,
389 )?;
390 let mut full_school_collections: HashSet<(String, Option<String>)> = HashSet::new();
391 let full_school_collections_rows =
392 get_full_schools.query_map((), |r| Ok((r.get(0)?, r.get(1)?)))?;
393 for f in full_school_collections_rows {
394 full_school_collections.insert(f?);
395 }
396
397 for (school_id, collection_type) in select_sync_options.get_collections() {
398 match collection_type {
399 CollectionType::AllSchoolData => {
400 if !full_school_collections.contains(&(school_id.clone(), None)) {
401 self.conn.execute(
402 r#"
403 INSERT INTO _school_strategies
404 (school_id, term_collection_id)
405 VALUES (?, NULL)
406 "#,
407 [school_id],
408 )?;
409 }
410 }
411 CollectionType::SelectTermData(terms) => {
412 if full_school_collections.contains(&(school_id.clone(), None)) {
413 return Err(SyncError::new(format!(
414 "Cannot do select term sync for school `{school_id}` because the whole school as been synced"
415 )));
416 }
417 for term in terms {
418 if !full_school_collections
419 .contains(&(school_id.clone(), Some(term.clone())))
420 {
421 self.conn.execute(
422 r#"
423 INSERT INTO _school_strategies
424 (school_id, term_collection_id)
425 VALUES (?, ?)
426 "#,
427 [school_id, term],
428 )?;
429 }
430 }
431 }
432 }
433 }
434 }
435 }
436 Ok(())
437 }
438
439 fn unset_request_sync_resources(&mut self, resources: SyncResources) -> Result<(), Error> {
440 let _ = resources;
441 todo!()
442 }
443}
444
445fn convert_to_sql_value(v: &Value) -> Result<rusqlite::types::Value, Error> {
446 match v {
447 Value::String(s) => Ok(rusqlite::types::Value::Text(s.to_string())),
448 Value::Null => Ok(rusqlite::types::Value::Null),
449 Value::Bool(b) => Ok(rusqlite::types::Value::Integer(*b as i64)),
450 Value::Number(n) => {
451 if let Some(n) = n.as_i64() {
452 Ok(rusqlite::types::Value::Integer(n))
453 } else if let Some(n) = n.as_f64() {
454 Ok(rusqlite::types::Value::Real(n))
455 } else {
456 Ok(rusqlite::types::Value::Null)
457 }
458 }
459 _ => Err(SyncError::new(format!("Unsupported type {v:?}"))),
460 }
461}
462
463#[cfg(test)]
464mod sync_tests {
465 use super::*;
466 use dotenv::dotenv;
467 use log::info;
468 use serde_json::from_str;
469 use std::{fs, path::PathBuf};
470
471 #[test]
474 fn full_sync() {
475 dotenv().ok();
476 env_logger::init();
477 let mut conn;
478 if let Ok(path_to_sqlite_db) = env::var("TEST_SQLITE_DB_PATH") {
479 let file_path = Path::new(&path_to_sqlite_db);
480 if file_path.exists() {
481 fs::remove_file(file_path).unwrap()
482 }
483 conn = Sqlite::get_db_connection(file_path).unwrap();
484 } else {
485 conn = Connection::open_in_memory().unwrap();
486 }
487
488 let mut stored_syncs = Vec::new();
489 let directory_of_test_syncs = "test-syncs/maristfall2024";
490 for entry in fs::read_dir(directory_of_test_syncs).unwrap() {
491 let entry = entry.unwrap();
492 let path = entry.path();
493 if path.is_file() {
494 if let Some(extension) = path.extension() {
495 if extension != "json" {
496 continue;
497 }
498 if let Some(file_name) = path.file_name() {
499 if let Some(file_name_str) = file_name.to_str() {
500 stored_syncs.push(file_name_str.to_string());
501 }
502 }
503 }
504 }
505 }
506 stored_syncs.sort();
507
508 let mut base_path = PathBuf::new();
509 base_path.push(directory_of_test_syncs);
510 for test_sync in &stored_syncs {
511 let mut full_path = base_path.clone();
512 full_path.push(test_sync);
513 info!("Starting sync: {}", test_sync);
514 let tx = conn.transaction().unwrap();
515 let updates_text = fs::read_to_string(&full_path).unwrap();
516 let response: AllSyncResult = from_str(&updates_text).unwrap();
517 for update in response.sync_data {
518 Sqlite::execute_sync(&tx, update).unwrap()
519 }
520 tx.commit().unwrap();
521 info!("Finished sync: {}", test_sync);
522 }
523 }
524}