flashpoint_archive/update/
mod.rs

1use std::rc::Rc;
2
3use rusqlite::types::{ToSqlOutput, Value};
4use rusqlite::{params, Connection, ToSql};
5use serde::{Deserialize, Serialize};
6use snafu::ResultExt;
7use uuid::Uuid;
8
9use crate::game::GameRedirect;
10use crate::{error, game, tag, tag_category};
11use crate::error::Result;
12use crate::game::search::mark_index_dirty;
13use crate::platform;
14
15#[derive(Debug, Clone)]
16pub struct SqlVec<T> (pub Vec<T>);
17
18impl ToSql for SqlVec<i64> {
19    fn to_sql(&self) -> std::result::Result<ToSqlOutput<'_>, rusqlite::Error> {
20        let v = Rc::new(self.0.iter().map(|v| Value::from(v.clone())).collect::<Vec<Value>>());
21        Ok(ToSqlOutput::Array(v))
22    }
23}
24
25impl ToSql for SqlVec<String> {
26    fn to_sql(&self) -> std::result::Result<ToSqlOutput<'_>, rusqlite::Error> {
27        let v = Rc::new(self.0.iter().map(|v| Value::from(v.clone())).collect::<Vec<Value>>());
28        Ok(ToSqlOutput::Array(v))
29    }
30}
31
32#[cfg_attr(feature = "napi", napi(object))]
33#[derive(Debug, Clone, Deserialize, Serialize)]
34pub struct RemoteDeletedGamesRes {
35    pub games: Vec<RemoteDeletedGame>,
36}
37
38#[cfg_attr(feature = "napi", napi(object))]
39#[derive(Debug, Clone, Deserialize, Serialize)]
40pub struct RemoteDeletedGame {
41    pub id: String,
42    pub date_modified: String,
43    pub reason: String,
44}
45
46#[cfg_attr(feature = "napi", napi(object))]
47#[derive(Debug, Clone, Deserialize, Serialize)]
48pub struct RemoteGamesRes {
49    pub games: Vec<RemoteGame>,
50    pub add_apps: Vec<RemoteAddApp>,
51    pub game_data: Vec<RemoteGameData>,
52    pub tag_relations: Vec<Vec<String>>,
53    pub platform_relations: Vec<Vec<String>>,
54}
55
56#[cfg_attr(feature = "napi", napi(object))]
57#[derive(Debug, Clone, Deserialize, Serialize)]
58pub struct RemoteGameData {
59    pub game_id: String,
60    pub title: String,
61    pub date_added: String,
62    pub sha_256: String,
63    pub crc_32: u32,
64    pub size: i64,
65    pub parameters: Option<String>,
66    pub application_path: String,
67    pub launch_command: String,
68}
69
70#[cfg_attr(feature = "napi", napi(object))]
71#[derive(Debug, Clone, Deserialize, Serialize)]
72pub struct RemoteAddApp {
73    pub name: String,
74    pub application_path: String,
75    pub launch_command: String,
76    pub wait_for_exit: bool,
77    pub auto_run_before: bool,
78    pub parent_game_id: String,
79}
80
81#[cfg_attr(feature = "napi", napi(object))]
82#[derive(Debug, Clone, Deserialize, Serialize)]
83pub struct RemoteGame {
84    pub id: String,
85    pub title: String,
86    pub alternate_titles: String,
87    pub series: String,
88    pub developer: String,
89    pub publisher: String,
90    pub date_added: String,
91    pub date_modified: String,
92    pub play_mode: String,
93    pub status: String,
94    pub notes: String,
95    pub source: String,
96    pub application_path: String,
97    pub launch_command: String,
98    pub release_date: String,
99    pub version: String,
100    pub original_description: String,
101    pub language: String,
102    pub library: String,
103    pub platform_name: String,
104    pub archive_state: i32,
105    pub logo_path: String,
106    pub screenshot_path: String,
107    pub ruffle_support: String,
108}
109
110#[cfg_attr(feature = "napi", napi(object))]
111#[derive(Debug, Clone, Deserialize, Serialize)]
112pub struct RemoteCategory {
113    pub id: i64,
114    pub name: String,
115    pub color: String,
116    pub description: String,
117}
118
119#[cfg_attr(feature = "napi", napi(object))]
120#[derive(Debug, Clone, Deserialize, Serialize)]
121pub struct RemoteTag {
122    pub id: i64,
123    pub name: String,
124    pub description: String,
125    pub category: String,
126    pub date_modified: String,
127    pub aliases: Vec<String>,
128    pub deleted: bool,
129}
130
131#[cfg_attr(feature = "napi", napi(object))]
132#[derive(Debug, Clone, Deserialize, Serialize)]
133pub struct RemotePlatform {
134    pub id: i64,
135    pub name: String,
136    pub description: String,
137    pub date_modified: String,
138    pub aliases: Vec<String>,
139    pub deleted: bool,
140}
141
142#[derive(Debug, Deserialize, Serialize)]
143pub struct Alias {
144    id: i64,
145    value: String,
146}
147
148pub fn apply_platforms(conn: &Connection, platforms: Vec<RemotePlatform>) -> Result<()> {
149    // Allow use of rarray() in SQL queries
150    rusqlite::vtab::array::load_module(conn).context(error::SqliteSnafu)?;
151    
152    // Create a list of Alias structs from the aliases
153    let changed_aliases: Vec<Alias> = platforms.iter()
154        .flat_map(|cur| cur.aliases.iter().map(move |alias| Alias { id: cur.id, value: alias.clone() }))
155        .collect();
156
157    let existing_platforms = platform::find(conn).context(error::SqliteSnafu)?;
158    let existing_ids: std::collections::HashSet<i64> = existing_platforms.iter().map(|p| p.id).collect();
159
160    // Delete old platform aliases
161    let changed_alias_names = SqlVec(changed_aliases.iter().map(|a| a.value.clone()).collect::<Vec<String>>());
162    conn.execute("DELETE FROM platform_alias WHERE name IN rarray(?)", params![changed_alias_names]).context(error::SqliteSnafu)?;
163
164    let mut update_platform_stmt = conn.prepare("UPDATE platform SET dateModified = ?, primaryAliasId = (SELECT id FROM platform_alias WHERE name = ?), description = ? WHERE id = ?").context(error::SqliteSnafu)?;
165    let mut insert_platform_stmt = conn.prepare("INSERT INTO platform (id, dateModified, primaryAliasId, description) VALUES (?, ?, (SELECT id FROM platform_alias WHERE name = ?), ?)").context(error::SqliteSnafu)?;
166    let mut delete_platform_alias_stmt = conn.prepare("DELETE FROM platform_alias WHERE platformId = ?").context(error::SqliteSnafu)?;
167    let mut delete_platform_stmt = conn.prepare("DELETE FROM platform WHERE id = ?").context(error::SqliteSnafu)?;
168
169    // Insert new ones
170    let mut insert_alias_stmt = conn.prepare("INSERT INTO platform_alias (platformId, name) VALUES (?, ?)").context(error::SqliteSnafu)?;
171    for alias in changed_aliases {
172        insert_alias_stmt.execute(params![alias.id, alias.value]).context(error::SqliteSnafu)?;
173    }
174
175    // Handle deleted platforms
176    let deleted_platform_ids = SqlVec(platforms.iter().filter(|p| existing_ids.contains(&p.id) && p.deleted).map(|p| p.id).collect::<Vec<i64>>());
177    // Remove from game platformsStr
178    conn.execute("UPDATE game
179    SET platformsStr = (
180        SELECT IFNULL(string_agg(pa.name, '; '), '')
181        FROM game_platforms_platform gpp
182        JOIN platform p ON gpp.platformId = p.id
183        JOIN platform_alias pa ON p.primaryAliasId = pa.id
184        WHERE gpp.gameId = game.id AND p.id NOT IN rarray(?)
185    ) WHERE game.id IN (
186        SELECT gameId FROM game_platforms_platform WHERE platformId IN rarray(?) 
187    )", params![deleted_platform_ids, deleted_platform_ids]).context(error::SqliteSnafu)?;
188    // Remove from game platformName
189    conn.execute("UPDATE game
190    SET platformName = 'BROKEN'
191    WHERE platformName IN (
192        SELECT name FROM platform_alias WHERE platformId IN rarray(?)   
193    )", params![deleted_platform_ids]).context(error::SqliteSnafu)?;
194    // Remove all data
195    conn.execute("DELETE FROM game_platforms_platform WHERE platformId IN rarray(?)", params![deleted_platform_ids]).context(error::SqliteSnafu)?;
196    conn.execute("DELETE FROM platform_alias WHERE platformId IN rarray(?)", params![deleted_platform_ids]).context(error::SqliteSnafu)?;
197    conn.execute("DELETE FROM platform WHERE id IN rarray(?)", params![deleted_platform_ids]).context(error::SqliteSnafu)?;
198
199    // Handle updated platforms
200    for platform in platforms.iter().filter(|p| existing_ids.contains(&p.id) && !p.deleted) {
201        update_platform_stmt.execute(params![platform.date_modified, platform.name, platform.description, platform.id]).context(error::SqliteSnafu)?;
202    }
203
204    // Handle new platforms
205    for platform in platforms.iter().filter(|p| !existing_ids.contains(&p.id) && !p.deleted) {
206        // Clean up any 'loose' rows
207        delete_platform_alias_stmt.execute(params![platform.id]).context(error::SqliteSnafu)?;
208        delete_platform_stmt.execute(params![platform.id]).context(error::SqliteSnafu)?;
209
210        // Insert new platform entry (above already added aliases)
211        for alias in &platform.aliases {
212            insert_alias_stmt.execute(params![platform.id, &alias]).context(error::SqliteSnafu)?;
213        }
214        insert_platform_stmt.execute(params![platform.id, platform.date_modified, platform.name, platform.description]).context(error::SqliteSnafu)?;
215    }
216
217    Ok(())
218}
219
220pub fn apply_categories(conn: &Connection, categories: Vec<RemoteCategory>) -> Result<()> {
221    let existing_categories = tag_category::find(conn).context(error::SqliteSnafu)?;
222    let existing_ids: std::collections::HashSet<i64> = existing_categories.iter().map(|p| p.id).collect();
223
224    let mut update_stmt = conn.prepare("UPDATE tag_category SET description = ?, color = ?, name = ? WHERE id = ?").context(error::SqliteSnafu)?;
225    let mut insert_stmt = conn.prepare("INSERT INTO tag_category (id, description, color, name) VALUES (?, ?, ?, ?)").context(error::SqliteSnafu)?;
226
227    // Handle updated platforms
228    for cat in categories.iter().filter(|p| existing_ids.contains(&p.id)) {
229        update_stmt.execute(params![cat.description, cat.color, cat.name, cat.id]).context(error::SqliteSnafu)?;
230    }
231
232    // Handle new platforms
233    for cat in categories.iter().filter(|p| !existing_ids.contains(&p.id)) {
234        insert_stmt.execute(params![cat.id, cat.description, cat.color, cat.name]).context(error::SqliteSnafu)?;
235    }
236
237    Ok(())
238}
239
240pub fn apply_tags(conn: &Connection, tags: Vec<RemoteTag>) -> Result<()> {
241    // Allow use of rarray() in SQL queries
242    rusqlite::vtab::array::load_module(conn).context(error::SqliteSnafu)?;
243    
244    // Create a list of Alias structs from the aliases
245    let changed_aliases: Vec<Alias> = tags.iter()
246        .flat_map(|cur| cur.aliases.iter().map(move |alias| Alias { id: cur.id, value: alias.clone() }))
247        .collect();
248
249    let changed_ids: Vec<i64> = tags.iter().map(|cur| cur.id).collect();
250
251    let existing_tags = tag::find(conn).context(error::SqliteSnafu)?;
252    let existing_ids: std::collections::HashSet<i64> = existing_tags.iter().map(|p| p.id).collect();
253
254    // Delete old tag aliases
255    let changed_alias_names = SqlVec(changed_aliases.iter().map(|a| a.value.clone()).collect::<Vec<String>>());
256    conn.execute("DELETE FROM tag_alias WHERE name IN rarray(?)", params![changed_alias_names]).context(error::SqliteSnafu)?;
257
258    // Clear aliases on all changed tags
259    let changed_ids_vec = SqlVec(changed_ids);
260    conn.execute("DELETE FROM tag_alias WHERE tagId IN rarray(?)", params![changed_ids_vec]).context(error::SqliteSnafu)?;
261
262    let mut update_tag_stmt = conn.prepare("UPDATE tag SET dateModified = ?, primaryAliasId = (SELECT id FROM tag_alias WHERE name = ?), description = ?, categoryId = (SELECT id FROM tag_category WHERE name = ?) WHERE id = ?").context(error::SqliteSnafu)?;
263    let mut insert_tag_stmt = conn.prepare("INSERT INTO tag (id, dateModified, primaryAliasId, description, categoryId) 
264        VALUES (?, ?, (SELECT id FROM tag_alias WHERE name = ?), ?, (SELECT id FROM tag_category WHERE name = ?))").context(error::SqliteSnafu)?;
265    let mut delete_tag_alias_stmt = conn.prepare("DELETE FROM tag_alias WHERE tagId = ?").context(error::SqliteSnafu)?;
266    let mut delete_tag_stmt = conn.prepare("DELETE FROM tag WHERE id = ?").context(error::SqliteSnafu)?;
267
268    // Insert new ones
269    let mut insert_alias_stmt = conn.prepare("INSERT INTO tag_alias (tagId, name) VALUES (?, ?)").context(error::SqliteSnafu)?;
270    for alias in changed_aliases {
271        insert_alias_stmt.execute(params![alias.id, alias.value]).context(error::SqliteSnafu)?;
272    }
273
274    // Handle deleted tags
275    let deleted_tag_ids = SqlVec(tags.iter().filter(|p| existing_ids.contains(&p.id) && p.deleted).map(|p| p.id).collect::<Vec<i64>>());
276    // Remove from game tagsStr
277    conn.execute("UPDATE game
278    SET tagsStr = (
279        SELECT IFNULL(string_agg(ta.name, '; '), '')
280        FROM game_tags_tag gtt
281        JOIN tag t ON gtt.tagId = t.id
282        JOIN tag_alias ta ON t.primaryAliasId = ta.id
283        WHERE gtt.gameId = game.id AND t.id NOT IN rarray(?)
284    ) WHERE game.id IN (
285        SELECT gameId FROM game_tags_tag WHERE tagId IN rarray(?) 
286    )", params![deleted_tag_ids, deleted_tag_ids]).context(error::SqliteSnafu)?;
287    // Remove all data
288    conn.execute("DELETE FROM game_tags_tag WHERE tagId IN rarray(?)", params![deleted_tag_ids]).context(error::SqliteSnafu)?;
289    conn.execute("DELETE FROM tag_alias WHERE tagId IN rarray(?)", params![deleted_tag_ids]).context(error::SqliteSnafu)?;
290    conn.execute("DELETE FROM tag WHERE id IN rarray(?)", params![deleted_tag_ids]).context(error::SqliteSnafu)?;
291
292    // Handle updated tags
293    for tag in tags.iter().filter(|p| existing_ids.contains(&p.id) && !p.deleted) {
294        update_tag_stmt.execute(params![tag.date_modified, tag.name, tag.description, tag.category, tag.id]).context(error::SqliteSnafu)?;
295    }
296
297    // Handle new tags
298    for tag in tags.iter().filter(|p| !existing_ids.contains(&p.id) && !p.deleted) {
299        // Clean up any 'loose' rows
300        delete_tag_alias_stmt.execute(params![tag.id]).context(error::SqliteSnafu)?;
301        delete_tag_stmt.execute(params![tag.id]).context(error::SqliteSnafu)?;
302
303        // Insert new tag entry (above already added aliases)
304        for alias in &tag.aliases {
305            insert_alias_stmt.execute(params![tag.id, &alias]).context(error::SqliteSnafu)?;
306        }
307        insert_tag_stmt.execute(params![tag.id, tag.date_modified, tag.name, tag.description, tag.category]).context(error::SqliteSnafu)?;
308    }
309
310    mark_index_dirty(conn).context(error::SqliteSnafu)?;
311
312    Ok(())
313}
314
315pub fn apply_games(conn: &Connection, games_res: &RemoteGamesRes) -> Result<()> {
316    // Allow use of rarray() in SQL queries
317    rusqlite::vtab::array::load_module(conn).context(error::SqliteSnafu)?;
318
319    let changed_ids = SqlVec(games_res.games.iter().map(|g| g.id.clone()).collect::<Vec<String>>());
320
321    println!("Reassigning relations");
322
323    // Clear game relations
324    conn.execute("DELETE FROM game_tags_tag WHERE gameId IN rarray(?)", params![changed_ids]).context(error::SqliteSnafu)?;
325    conn.execute("DELETE FROM game_platforms_platform WHERE gameId IN rarray(?)", params![changed_ids]).context(error::SqliteSnafu)?;
326    // Insert game relations
327    let mut insert_tag_relation_stmt = conn.prepare("INSERT INTO game_tags_tag (gameId, tagId) 
328    VALUES (?, ?)").context(error::SqliteSnafu)?;
329    let mut insert_platform_relation_stmt = conn.prepare("INSERT INTO game_platforms_platform (gameId, platformId) 
330    VALUES (?, ?)").context(error::SqliteSnafu)?;
331    for ta in &games_res.tag_relations {
332        insert_tag_relation_stmt.execute(params![ta[0], ta[1]]).context(error::SqliteSnafu)?;
333    }
334    for pa in &games_res.platform_relations {
335        insert_platform_relation_stmt.execute(params![pa[0], pa[1]]).context(error::SqliteSnafu)?;
336    }
337
338    println!("Reassigning add apps");
339
340    // Unassign all add apps
341    conn.execute("DELETE FROM additional_app WHERE parentGameId IN rarray(?)", params![changed_ids]).context(error::SqliteSnafu)?;
342    // Reassign all add apps
343    let mut insert_add_app_stmt = conn.prepare("INSERT INTO additional_app
344    (id, applicationPath, launchCommand, name, parentGameId, autoRunBefore, waitForExit)
345    VALUES
346    (?, ?, ?, ?, ?, ?, ?)").context(error::SqliteSnafu)?;
347    for aa in &games_res.add_apps {
348        insert_add_app_stmt.execute(params![Uuid::new_v4().to_string(), aa.application_path, aa.launch_command, aa.name, aa.parent_game_id,
349            aa.auto_run_before, aa.wait_for_exit])
350            .context(error::SqliteSnafu)?;
351    }
352
353    println!("Reassigning game data");
354
355    // Unassign all removed game data (if it isn't already downloaded)
356    conn.execute("DELETE FROM game_data WHERE gameId IN rarray(?) AND presentOnDisk == false", params![changed_ids]).context(error::SqliteSnafu)?;
357    // Assign all new game data
358    let mut insert_game_data_stmt = conn.prepare("INSERT INTO game_data
359    (gameId, title, dateAdded, sha256, crc32, presentOnDisk, path, size, parameters, applicationPath, launchCommand)
360    VALUES
361    (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
362    ON CONFLICT(gameId, dateAdded)
363    DO UPDATE SET parameters = ?, applicationPath = ?, launchCommand = ?").context(error::SqliteSnafu)?;
364    for gd in &games_res.game_data {
365        insert_game_data_stmt.execute(params![gd.game_id, gd.title, gd.date_added, gd.sha_256,
366            gd.crc_32, false, "", gd.size, gd.parameters, gd.application_path, gd.launch_command,
367            gd.parameters, gd.application_path, gd.launch_command])
368            .context(error::SqliteSnafu)?;
369    }
370
371    let existing_ids = game::find_all_ids(conn).context(error::SqliteSnafu)?;
372
373    println!("Updating games");
374
375    // Handle updated games
376    let mut update_game_stmt = conn.prepare("UPDATE game SET library = ?, title = ?, alternateTitles = ?, series = ?, developer = ?, publisher = ?,
377        platformName = ?, platformId = (SELECT platformId FROM platform_alias WHERE name = ?), platformsStr = ?, dateAdded = ?, dateModified = ?, 
378        playMode = ?, status = ?, notes = ?, source = ?, activeDataId = -1,
379        applicationPath = ?, launchCommand = ?, releaseDate = ?, version = ?,
380        originalDescription = ?, language = ?, archiveState = ?, logoPath = ?, screenshotPath = ?, ruffleSupport = ? WHERE id = ?").context(error::SqliteSnafu)?;
381
382    for g in games_res.games.iter().filter(|p| existing_ids.contains(&p.id)) {
383        update_game_stmt.execute(params![
384            g.library, g.title, g.alternate_titles, g.series, g.developer, g.publisher,
385            g.platform_name, g.platform_name, "", g.date_added, g.date_modified,
386            g.play_mode, g.status, g.notes, g.source,
387            g.application_path, g.launch_command, g.release_date, g.version,
388            g.original_description, g.language, g.archive_state, g.logo_path, g.screenshot_path, g.ruffle_support, g.id]).context(error::SqliteSnafu)?;
389    }
390
391    println!("Inserting games");
392
393    // Handle new games
394    let mut insert_game_stmt = conn.prepare("INSERT INTO game (id, library, title, alternateTitles, series, developer, publisher,
395        platformName, platformId, platformsStr, dateAdded, dateModified, broken, extreme, playMode, status,
396        notes, tagsStr, source, applicationPath, launchCommand, releaseDate, version,
397        originalDescription, language, activeDataId, activeDataOnDisk, playtime,
398        archiveState, orderTitle, logoPath, screenshotPath, ruffleSupport) VALUES (?, ?, ?, ?, ?, ?, ?,
399        ?, ?, (SELECT platformId FROM platform_alias WHERE name = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").context(error::SqliteSnafu)?;
400
401    for g in games_res.games.iter().filter(|p| !existing_ids.contains(&p.id)) {
402        insert_game_stmt.execute(params![
403            g.id, g.library, g.title, g.alternate_titles, g.series, g.developer, g.publisher,
404            g.platform_name, g.platform_name, "", g.date_added, g.date_modified, false, false, g.play_mode, g.status,
405            g.notes, "", g.source, g.application_path, g.launch_command, g.release_date, g.version,
406            g.original_description, g.language, -1, false, 0,
407            g.archive_state, "", g.logo_path, g.screenshot_path, g.ruffle_support,
408        ]).context(error::SqliteSnafu)?;
409    }
410
411    println!("Updating games - cleanup");
412
413    // Update platformStr and tagsStr for all changed games
414    conn.execute("UPDATE game
415    SET tagsStr = (
416        SELECT IFNULL(string_agg(ta.name, '; '), '')
417        FROM game_tags_tag gtt
418        JOIN tag t ON gtt.tagId = t.id
419        JOIN tag_alias ta ON t.primaryAliasId = ta.id
420        WHERE gtt.gameId = game.id
421    ) WHERE game.id IN rarray(?)", params![changed_ids]).context(error::SqliteSnafu)?;
422    conn.execute("UPDATE game
423    SET platformsStr = (
424        SELECT IFNULL(string_agg(pa.name, '; '), '')
425        FROM game_platforms_platform gpp
426        JOIN platform p ON gpp.platformId = p.id
427        JOIN platform_alias pa ON p.primaryAliasId = pa.id
428        WHERE gpp.gameId = game.id
429    ) WHERE game.id IN rarray(?)", params![changed_ids]).context(error::SqliteSnafu)?;
430
431    println!("Active game id cleanup");
432
433    // Update active game id info
434    conn.execute("UPDATE game
435    SET activeDataId = (SELECT game_data.id FROM game_data WHERE game.id = game_data.gameId ORDER BY game_data.dateAdded DESC LIMIT 1)
436    WHERE game.activeDataId = -1", ()).context(error::SqliteSnafu)?;
437
438    mark_index_dirty(conn).context(error::SqliteSnafu)?;
439
440    Ok(())
441}
442
443pub fn delete_games(conn: &Connection, games_res: &RemoteDeletedGamesRes) -> Result<()> {
444    // Allow use of rarray() in SQL queries
445    rusqlite::vtab::array::load_module(conn).context(error::SqliteSnafu)?;
446
447    let ids = SqlVec(games_res.games.iter().map(|g| g.id.clone()).collect::<Vec<String>>());
448
449    conn.execute("DELETE FROM game_tags_tag WHERE gameId IN rarray(?)", params![ids]).context(error::SqliteSnafu)?;
450    conn.execute("DELETE FROM game_platforms_platform WHERE gameId IN rarray(?)", params![ids]).context(error::SqliteSnafu)?;
451    conn.execute("DELETE FROM game_data WHERE gameId IN rarray(?)", params![ids]).context(error::SqliteSnafu)?;
452    conn.execute("DELETE FROM additional_app WHERE parentGameId IN rarray(?)", params![ids]).context(error::SqliteSnafu)?;
453    conn.execute("DELETE FROM game WHERE id IN rarray(?)", params![ids]).context(error::SqliteSnafu)?;
454
455    Ok(())
456}
457
458pub fn apply_redirects(conn: &Connection, redirects: Vec<GameRedirect>) -> Result<()> {
459    let mut stmt = conn.prepare("INSERT OR IGNORE INTO game_redirect (sourceId, id) VALUES (?, ?)").context(error::SqliteSnafu)?;
460    for r in redirects.iter() {
461        stmt.execute(params![r.source_id, r.dest_id]).context(error::SqliteSnafu)?;
462    }
463    conn.execute("DELETE FROM game_redirect WHERE sourceId IN (SELECT id FROM game)", ()).context(error::SqliteSnafu)?;
464    Ok(())
465}