experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(mobile): replace Once::call_once with retryable Mutex for DB init

Once::call_once permanently marks as done even if the closure returns
early (e.g., get_db_path() returns None before profiles.json is ready).
This prevented dedup and all migrations from ever running in that
process.

Now uses Mutex<bool> that only flips to true on successful completion.
Failed init retries on next call. Also fixes clear_db_cache() to
actually reset the flag.

+437 -452
+437 -452
backend/tauri-mobile/src-tauri/src/lib.rs
··· 1063 1063 Some(new_db_path) 1064 1064 } 1065 1065 1066 - use std::sync::Once; 1067 - 1068 - static DB_INIT: Once = Once::new(); 1066 + static DB_INITIALIZED: std::sync::Mutex<bool> = std::sync::Mutex::new(false); 1069 1067 static DEVICE_ID: OnceLock<String> = OnceLock::new(); 1070 1068 1071 1069 /// Schema version for tracking compatibility between Rust main app and Swift Share Extension. ··· 1074 1072 const SCHEMA_VERSION: &str = "2"; 1075 1073 1076 1074 fn ensure_database_initialized() -> Result<(), String> { 1077 - let mut init_result: Result<(), String> = Ok(()); 1075 + let mut initialized = DB_INITIALIZED.lock() 1076 + .map_err(|e| format!("Failed to lock DB_INITIALIZED: {}", e))?; 1077 + if *initialized { 1078 + return Ok(()); 1079 + } 1078 1080 1079 - DB_INIT.call_once(|| { 1080 - let db_path = match get_db_path() { 1081 - Some(p) => p, 1082 - None => { 1083 - init_result = Err("Failed to get database path".to_string()); 1084 - return; 1085 - } 1086 - }; 1081 + let db_path = get_db_path() 1082 + .ok_or_else(|| "Failed to get database path".to_string())?; 1087 1083 1088 - println!("[Rust] Initializing database at: {:?}", db_path); 1084 + println!("[Rust] Initializing database at: {:?}", db_path); 1089 1085 1090 - let conn = match Connection::open(&db_path) { 1091 - Ok(c) => c, 1092 - Err(e) => { 1093 - init_result = Err(format!("Failed to open database: {}", e)); 1094 - return; 1095 - } 1096 - }; 1086 + let conn = Connection::open(&db_path) 1087 + .map_err(|e| format!("Failed to open database: {}", e))?; 1097 1088 1098 - // Enable WAL mode for concurrent access from main app and share extension 1099 - if let Err(e) = conn.execute_batch("PRAGMA journal_mode=WAL;") { 1100 - init_result = Err(format!("Failed to set WAL mode: {}", e)); 1101 - return; 1102 - } 1089 + // Enable WAL mode for concurrent access from main app and share extension 1090 + if let Err(e) = conn.execute_batch("PRAGMA journal_mode=WAL;") { 1091 + return Err(format!("Failed to set WAL mode: {}", e)); 1092 + } 1103 1093 1104 - // Check if we need to migrate from old schema (urls table) to new schema (items table) 1105 - let has_urls_table: bool = conn 1106 - .query_row( 1107 - "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='urls'", 1108 - [], 1109 - |row| row.get::<_, i64>(0), 1110 - ) 1111 - .unwrap_or(0) > 0; 1094 + // Check if we need to migrate from old schema (urls table) to new schema (items table) 1095 + let has_urls_table: bool = conn 1096 + .query_row( 1097 + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='urls'", 1098 + [], 1099 + |row| row.get::<_, i64>(0), 1100 + ) 1101 + .unwrap_or(0) > 0; 1112 1102 1113 - let has_items_table: bool = conn 1114 - .query_row( 1115 - "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='items'", 1116 - [], 1117 - |row| row.get::<_, i64>(0), 1118 - ) 1119 - .unwrap_or(0) > 0; 1103 + let has_items_table: bool = conn 1104 + .query_row( 1105 + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='items'", 1106 + [], 1107 + |row| row.get::<_, i64>(0), 1108 + ) 1109 + .unwrap_or(0) > 0; 1120 1110 1121 - if has_urls_table && !has_items_table { 1122 - // Migration needed: urls -> items 1123 - println!("[Rust] Migrating database from urls to items schema..."); 1124 - if let Err(e) = conn.execute_batch( 1125 - " 1111 + if has_urls_table && !has_items_table { 1112 + // Migration needed: urls -> items 1113 + println!("[Rust] Migrating database from urls to items schema..."); 1114 + if let Err(e) = conn.execute_batch( 1115 + " 1126 1116 -- Create new items table 1127 1117 CREATE TABLE items ( 1128 1118 id TEXT PRIMARY KEY, ··· 1166 1156 CREATE INDEX IF NOT EXISTS idx_items_deleted ON items(deleted_at); 1167 1157 CREATE INDEX IF NOT EXISTS idx_items_sync_id ON items(sync_id); 1168 1158 ", 1169 - ) { 1170 - init_result = Err(format!("Failed to migrate database: {}", e)); 1171 - return; 1172 - } 1173 - println!("[Rust] Database migration completed successfully"); 1174 - } else if !has_items_table { 1175 - // Fresh install: create new schema 1176 - if let Err(e) = conn.execute_batch( 1177 - " 1159 + ) { 1160 + return Err(format!("Failed to migrate database: {}", e)); 1161 + } 1162 + println!("[Rust] Database migration completed successfully"); 1163 + } else if !has_items_table { 1164 + // Fresh install: create new schema 1165 + if let Err(e) = conn.execute_batch( 1166 + " 1178 1167 CREATE TABLE IF NOT EXISTS items ( 1179 1168 id TEXT PRIMARY KEY, 1180 1169 type TEXT NOT NULL DEFAULT 'url', ··· 1247 1236 value TEXT NOT NULL 1248 1237 ); 1249 1238 ", 1250 - ) { 1251 - init_result = Err(format!("Failed to create tables: {}", e)); 1252 - return; 1253 - } 1239 + ) { 1240 + return Err(format!("Failed to create tables: {}", e)); 1254 1241 } 1242 + } 1255 1243 1256 - // Add metadata column if it doesn't exist (for existing installs) 1257 - let has_metadata_column: bool = conn 1258 - .query_row( 1259 - "SELECT COUNT(*) FROM pragma_table_info('items') WHERE name='metadata'", 1260 - [], 1261 - |row| row.get::<_, i64>(0), 1262 - ) 1263 - .unwrap_or(0) > 0; 1244 + // Add metadata column if it doesn't exist (for existing installs) 1245 + let has_metadata_column: bool = conn 1246 + .query_row( 1247 + "SELECT COUNT(*) FROM pragma_table_info('items') WHERE name='metadata'", 1248 + [], 1249 + |row| row.get::<_, i64>(0), 1250 + ) 1251 + .unwrap_or(0) > 0; 1264 1252 1265 - if !has_metadata_column { 1266 - println!("[Rust] Adding metadata column to items table..."); 1267 - if let Err(e) = conn.execute("ALTER TABLE items ADD COLUMN metadata TEXT", []) { 1268 - println!("[Rust] Warning: Failed to add metadata column: {}", e); 1269 - // Not fatal - column may already exist 1270 - } 1253 + if !has_metadata_column { 1254 + println!("[Rust] Adding metadata column to items table..."); 1255 + if let Err(e) = conn.execute("ALTER TABLE items ADD COLUMN metadata TEXT", []) { 1256 + println!("[Rust] Warning: Failed to add metadata column: {}", e); 1257 + // Not fatal - column may already exist 1271 1258 } 1259 + } 1272 1260 1273 - // Add sync columns if they don't exist (for existing installs) 1274 - let has_sync_id: bool = conn 1275 - .query_row( 1276 - "SELECT COUNT(*) FROM pragma_table_info('items') WHERE name='sync_id'", 1277 - [], 1278 - |row| row.get::<_, i64>(0), 1279 - ) 1280 - .unwrap_or(0) > 0; 1261 + // Add sync columns if they don't exist (for existing installs) 1262 + let has_sync_id: bool = conn 1263 + .query_row( 1264 + "SELECT COUNT(*) FROM pragma_table_info('items') WHERE name='sync_id'", 1265 + [], 1266 + |row| row.get::<_, i64>(0), 1267 + ) 1268 + .unwrap_or(0) > 0; 1281 1269 1282 - if !has_sync_id { 1283 - println!("[Rust] Adding sync columns to items table..."); 1284 - let _ = conn.execute("ALTER TABLE items ADD COLUMN sync_id TEXT DEFAULT ''", []); 1285 - let _ = conn.execute("ALTER TABLE items ADD COLUMN sync_source TEXT DEFAULT ''", []); 1286 - let _ = conn.execute("ALTER TABLE items ADD COLUMN synced_at TEXT", []); 1287 - let _ = conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_items_sync_id ON items(sync_id)"); 1288 - } 1270 + if !has_sync_id { 1271 + println!("[Rust] Adding sync columns to items table..."); 1272 + let _ = conn.execute("ALTER TABLE items ADD COLUMN sync_id TEXT DEFAULT ''", []); 1273 + let _ = conn.execute("ALTER TABLE items ADD COLUMN sync_source TEXT DEFAULT ''", []); 1274 + let _ = conn.execute("ALTER TABLE items ADD COLUMN synced_at TEXT", []); 1275 + let _ = conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_items_sync_id ON items(sync_id)"); 1276 + } 1289 1277 1290 - // Migrate 'page' type items to 'url' (for existing installs with old type name) 1291 - let page_count: i64 = conn 1292 - .query_row( 1293 - "SELECT COUNT(*) FROM items WHERE type = 'page'", 1294 - [], 1295 - |row| row.get(0), 1296 - ) 1297 - .unwrap_or(0); 1278 + // Migrate 'page' type items to 'url' (for existing installs with old type name) 1279 + let page_count: i64 = conn 1280 + .query_row( 1281 + "SELECT COUNT(*) FROM items WHERE type = 'page'", 1282 + [], 1283 + |row| row.get(0), 1284 + ) 1285 + .unwrap_or(0); 1298 1286 1299 - if page_count > 0 { 1300 - println!("[Rust] Migrating {} 'page' items to 'url' type...", page_count); 1301 - if let Err(e) = conn.execute("UPDATE items SET type = 'url' WHERE type = 'page'", []) { 1302 - println!("[Rust] Warning: Failed to migrate page items: {}", e); 1303 - } else { 1304 - println!("[Rust] Page to URL migration complete"); 1305 - } 1287 + if page_count > 0 { 1288 + println!("[Rust] Migrating {} 'page' items to 'url' type...", page_count); 1289 + if let Err(e) = conn.execute("UPDATE items SET type = 'url' WHERE type = 'page'", []) { 1290 + println!("[Rust] Warning: Failed to migrate page items: {}", e); 1291 + } else { 1292 + println!("[Rust] Page to URL migration complete"); 1306 1293 } 1294 + } 1307 1295 1308 - // Migrate tags table columns from snake_case to camelCase 1309 - let has_last_used_snake: bool = conn 1310 - .query_row( 1311 - "SELECT COUNT(*) FROM pragma_table_info('tags') WHERE name='last_used'", 1312 - [], 1313 - |row| row.get::<_, i64>(0), 1314 - ) 1315 - .unwrap_or(0) > 0; 1296 + // Migrate tags table columns from snake_case to camelCase 1297 + let has_last_used_snake: bool = conn 1298 + .query_row( 1299 + "SELECT COUNT(*) FROM pragma_table_info('tags') WHERE name='last_used'", 1300 + [], 1301 + |row| row.get::<_, i64>(0), 1302 + ) 1303 + .unwrap_or(0) > 0; 1316 1304 1317 - if has_last_used_snake { 1318 - ios_log("Migrating tags table columns from snake_case to camelCase..."); 1319 - // SQLite 3.25+ supports RENAME COLUMN 1320 - let renames = [ 1321 - ("last_used", "lastUsed"), 1322 - ("frecency_score", "frecencyScore"), 1323 - ("created_at", "createdAt"), 1324 - ("updated_at", "updatedAt"), 1325 - ]; 1326 - for (old_name, new_name) in renames { 1327 - let sql = format!("ALTER TABLE tags RENAME COLUMN {} TO {}", old_name, new_name); 1328 - if let Err(e) = conn.execute(&sql, []) { 1329 - ios_log(&format!("Warning: Failed to rename column {} to {}: {}", old_name, new_name, e)); 1330 - } else { 1331 - ios_log(&format!("Renamed tags.{} to tags.{}", old_name, new_name)); 1332 - } 1305 + if has_last_used_snake { 1306 + ios_log("Migrating tags table columns from snake_case to camelCase..."); 1307 + // SQLite 3.25+ supports RENAME COLUMN 1308 + let renames = [ 1309 + ("last_used", "lastUsed"), 1310 + ("frecency_score", "frecencyScore"), 1311 + ("created_at", "createdAt"), 1312 + ("updated_at", "updatedAt"), 1313 + ]; 1314 + for (old_name, new_name) in renames { 1315 + let sql = format!("ALTER TABLE tags RENAME COLUMN {} TO {}", old_name, new_name); 1316 + if let Err(e) = conn.execute(&sql, []) { 1317 + ios_log(&format!("Warning: Failed to rename column {} to {}: {}", old_name, new_name, e)); 1318 + } else { 1319 + ios_log(&format!("Renamed tags.{} to tags.{}", old_name, new_name)); 1333 1320 } 1334 - // Recreate index with new column name 1335 - let _ = conn.execute("DROP INDEX IF EXISTS idx_tags_frecency", []); 1336 - let _ = conn.execute("CREATE INDEX IF NOT EXISTS idx_tags_frecency ON tags(frecencyScore DESC)", []); 1337 - ios_log("Tags table migration complete"); 1338 1321 } 1322 + // Recreate index with new column name 1323 + let _ = conn.execute("DROP INDEX IF EXISTS idx_tags_frecency", []); 1324 + let _ = conn.execute("CREATE INDEX IF NOT EXISTS idx_tags_frecency ON tags(frecencyScore DESC)", []); 1325 + ios_log("Tags table migration complete"); 1326 + } 1339 1327 1340 - // Ensure blobs table exists (for existing installs) 1341 - let has_blobs_table: bool = conn 1342 - .query_row( 1343 - "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='blobs'", 1344 - [], 1345 - |row| row.get::<_, i64>(0), 1346 - ) 1347 - .unwrap_or(0) > 0; 1328 + // Ensure blobs table exists (for existing installs) 1329 + let has_blobs_table: bool = conn 1330 + .query_row( 1331 + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='blobs'", 1332 + [], 1333 + |row| row.get::<_, i64>(0), 1334 + ) 1335 + .unwrap_or(0) > 0; 1348 1336 1349 - if !has_blobs_table { 1350 - println!("[Rust] Creating blobs table for image support..."); 1351 - if let Err(e) = conn.execute_batch( 1352 - " 1337 + if !has_blobs_table { 1338 + println!("[Rust] Creating blobs table for image support..."); 1339 + if let Err(e) = conn.execute_batch( 1340 + " 1353 1341 CREATE TABLE IF NOT EXISTS blobs ( 1354 1342 id TEXT PRIMARY KEY, 1355 1343 item_id TEXT NOT NULL, ··· 1364 1352 ); 1365 1353 CREATE INDEX IF NOT EXISTS idx_blobs_item ON blobs(item_id); 1366 1354 ", 1367 - ) { 1368 - println!("[Rust] Warning: Failed to create blobs table: {}", e); 1369 - } 1355 + ) { 1356 + println!("[Rust] Warning: Failed to create blobs table: {}", e); 1370 1357 } 1358 + } 1371 1359 1372 - // Ensure settings table exists (for migration case) 1373 - if let Err(e) = conn.execute_batch( 1374 - " 1360 + // Ensure settings table exists (for migration case) 1361 + if let Err(e) = conn.execute_batch( 1362 + " 1375 1363 CREATE TABLE IF NOT EXISTS settings ( 1376 1364 key TEXT PRIMARY KEY, 1377 1365 value TEXT NOT NULL 1378 1366 ); 1379 1367 ", 1380 - ) { 1381 - init_result = Err(format!("Failed to ensure settings table: {}", e)); 1382 - return; 1383 - } 1368 + ) { 1369 + return Err(format!("Failed to ensure settings table: {}", e)); 1370 + } 1384 1371 1385 - // Ensure item_visits table exists (for existing installs) 1386 - let has_item_visits_table: bool = conn 1387 - .query_row( 1388 - "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='item_visits'", 1389 - [], 1390 - |row| row.get::<_, i64>(0), 1391 - ) 1392 - .unwrap_or(0) > 0; 1372 + // Ensure item_visits table exists (for existing installs) 1373 + let has_item_visits_table: bool = conn 1374 + .query_row( 1375 + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='item_visits'", 1376 + [], 1377 + |row| row.get::<_, i64>(0), 1378 + ) 1379 + .unwrap_or(0) > 0; 1393 1380 1394 - if !has_item_visits_table { 1395 - println!("[Rust] Creating item_visits table for visit tracking..."); 1396 - if let Err(e) = conn.execute_batch( 1397 - " 1381 + if !has_item_visits_table { 1382 + println!("[Rust] Creating item_visits table for visit tracking..."); 1383 + if let Err(e) = conn.execute_batch( 1384 + " 1398 1385 CREATE TABLE IF NOT EXISTS item_visits ( 1399 1386 id TEXT PRIMARY KEY, 1400 1387 item_id TEXT NOT NULL, ··· 1408 1395 CREATE INDEX IF NOT EXISTS idx_item_visits_item ON item_visits(item_id); 1409 1396 CREATE INDEX IF NOT EXISTS idx_item_visits_timestamp ON item_visits(timestamp); 1410 1397 ", 1411 - ) { 1412 - println!("[Rust] Warning: Failed to create item_visits table: {}", e); 1413 - } 1398 + ) { 1399 + println!("[Rust] Warning: Failed to create item_visits table: {}", e); 1414 1400 } 1401 + } 1415 1402 1416 - // Migrate tags.id from INTEGER to TEXT (for existing installs with old schema) 1417 - // Check if tags table exists and has INTEGER id column 1418 - let tags_has_integer_id: bool = conn 1419 - .query_row( 1420 - "SELECT type FROM pragma_table_info('tags') WHERE name='id'", 1421 - [], 1422 - |row| row.get::<_, String>(0), 1423 - ) 1424 - .map(|t| t.to_uppercase() == "INTEGER") 1425 - .unwrap_or(false); 1403 + // Migrate tags.id from INTEGER to TEXT (for existing installs with old schema) 1404 + // Check if tags table exists and has INTEGER id column 1405 + let tags_has_integer_id: bool = conn 1406 + .query_row( 1407 + "SELECT type FROM pragma_table_info('tags') WHERE name='id'", 1408 + [], 1409 + |row| row.get::<_, String>(0), 1410 + ) 1411 + .map(|t| t.to_uppercase() == "INTEGER") 1412 + .unwrap_or(false); 1426 1413 1427 - if tags_has_integer_id { 1428 - ios_log("Migrating tags.id from INTEGER to TEXT..."); 1414 + if tags_has_integer_id { 1415 + ios_log("Migrating tags.id from INTEGER to TEXT..."); 1429 1416 1430 - // Use savepoint for atomic rollback on error 1431 - if let Err(e) = conn.execute("SAVEPOINT before_tag_migration", []) { 1432 - ios_log(&format!("Failed to create savepoint: {}", e)); 1433 - } else { 1434 - let migration_result: Result<(), String> = (|| { 1435 - // Step 1: Create temp mapping table 1436 - conn.execute_batch( 1437 - "CREATE TEMP TABLE tag_id_mapping ( 1438 - old_id INTEGER PRIMARY KEY, 1439 - new_id TEXT NOT NULL 1440 - );" 1441 - ).map_err(|e| format!("Failed to create mapping table: {}", e))?; 1417 + // Use savepoint for atomic rollback on error 1418 + if let Err(e) = conn.execute("SAVEPOINT before_tag_migration", []) { 1419 + ios_log(&format!("Failed to create savepoint: {}", e)); 1420 + } else { 1421 + let migration_result: Result<(), String> = (|| { 1422 + // Step 1: Create temp mapping table 1423 + conn.execute_batch( 1424 + "CREATE TEMP TABLE tag_id_mapping ( 1425 + old_id INTEGER PRIMARY KEY, 1426 + new_id TEXT NOT NULL 1427 + );" 1428 + ).map_err(|e| format!("Failed to create mapping table: {}", e))?; 1442 1429 1443 - // Step 2: Populate mapping with generated TEXT IDs 1444 - let mut stmt = conn.prepare("SELECT id FROM tags") 1445 - .map_err(|e| format!("Failed to prepare tag select: {}", e))?; 1446 - let old_ids: Vec<i64> = stmt 1447 - .query_map([], |row| row.get::<_, i64>(0)) 1448 - .map_err(|e| format!("Failed to query tags: {}", e))? 1449 - .filter_map(|r| r.ok()) 1450 - .collect(); 1451 - drop(stmt); 1430 + // Step 2: Populate mapping with generated TEXT IDs 1431 + let mut stmt = conn.prepare("SELECT id FROM tags") 1432 + .map_err(|e| format!("Failed to prepare tag select: {}", e))?; 1433 + let old_ids: Vec<i64> = stmt 1434 + .query_map([], |row| row.get::<_, i64>(0)) 1435 + .map_err(|e| format!("Failed to query tags: {}", e))? 1436 + .filter_map(|r| r.ok()) 1437 + .collect(); 1438 + drop(stmt); 1452 1439 1453 - for old_id in old_ids { 1454 - let new_id = generate_tag_id(); 1455 - conn.execute( 1456 - "INSERT INTO tag_id_mapping (old_id, new_id) VALUES (?, ?)", 1457 - params![old_id, new_id], 1458 - ).map_err(|e| format!("Failed to insert mapping: {}", e))?; 1459 - } 1440 + for old_id in old_ids { 1441 + let new_id = generate_tag_id(); 1442 + conn.execute( 1443 + "INSERT INTO tag_id_mapping (old_id, new_id) VALUES (?, ?)", 1444 + params![old_id, new_id], 1445 + ).map_err(|e| format!("Failed to insert mapping: {}", e))?; 1446 + } 1460 1447 1461 - // Step 3: Create new tables with TEXT schema 1462 - conn.execute_batch( 1463 - "CREATE TABLE tags_new ( 1464 - id TEXT PRIMARY KEY, 1465 - name TEXT NOT NULL UNIQUE, 1466 - frequency INTEGER NOT NULL DEFAULT 0, 1467 - lastUsed TEXT NOT NULL, 1468 - frecencyScore REAL NOT NULL DEFAULT 0.0, 1469 - createdAt TEXT NOT NULL, 1470 - updatedAt TEXT NOT NULL 1471 - ); 1448 + // Step 3: Create new tables with TEXT schema 1449 + conn.execute_batch( 1450 + "CREATE TABLE tags_new ( 1451 + id TEXT PRIMARY KEY, 1452 + name TEXT NOT NULL UNIQUE, 1453 + frequency INTEGER NOT NULL DEFAULT 0, 1454 + lastUsed TEXT NOT NULL, 1455 + frecencyScore REAL NOT NULL DEFAULT 0.0, 1456 + createdAt TEXT NOT NULL, 1457 + updatedAt TEXT NOT NULL 1458 + ); 1472 1459 1473 - CREATE TABLE item_tags_new ( 1474 - item_id TEXT NOT NULL, 1475 - tag_id TEXT NOT NULL, 1476 - created_at TEXT NOT NULL, 1477 - PRIMARY KEY (item_id, tag_id), 1478 - FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE, 1479 - FOREIGN KEY (tag_id) REFERENCES tags_new(id) ON DELETE CASCADE 1480 - );" 1481 - ).map_err(|e| format!("Failed to create new tables: {}", e))?; 1460 + CREATE TABLE item_tags_new ( 1461 + item_id TEXT NOT NULL, 1462 + tag_id TEXT NOT NULL, 1463 + created_at TEXT NOT NULL, 1464 + PRIMARY KEY (item_id, tag_id), 1465 + FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE, 1466 + FOREIGN KEY (tag_id) REFERENCES tags_new(id) ON DELETE CASCADE 1467 + );" 1468 + ).map_err(|e| format!("Failed to create new tables: {}", e))?; 1482 1469 1483 - // Step 4: Copy data using mapping 1484 - conn.execute_batch( 1485 - "INSERT INTO tags_new (id, name, frequency, lastUsed, frecencyScore, createdAt, updatedAt) 1486 - SELECT m.new_id, t.name, t.frequency, t.lastUsed, t.frecencyScore, t.createdAt, t.updatedAt 1487 - FROM tags t 1488 - JOIN tag_id_mapping m ON t.id = m.old_id; 1470 + // Step 4: Copy data using mapping 1471 + conn.execute_batch( 1472 + "INSERT INTO tags_new (id, name, frequency, lastUsed, frecencyScore, createdAt, updatedAt) 1473 + SELECT m.new_id, t.name, t.frequency, t.lastUsed, t.frecencyScore, t.createdAt, t.updatedAt 1474 + FROM tags t 1475 + JOIN tag_id_mapping m ON t.id = m.old_id; 1489 1476 1490 - INSERT INTO item_tags_new (item_id, tag_id, created_at) 1491 - SELECT it.item_id, m.new_id, it.created_at 1492 - FROM item_tags it 1493 - JOIN tag_id_mapping m ON it.tag_id = m.old_id;" 1494 - ).map_err(|e| format!("Failed to copy data: {}", e))?; 1477 + INSERT INTO item_tags_new (item_id, tag_id, created_at) 1478 + SELECT it.item_id, m.new_id, it.created_at 1479 + FROM item_tags it 1480 + JOIN tag_id_mapping m ON it.tag_id = m.old_id;" 1481 + ).map_err(|e| format!("Failed to copy data: {}", e))?; 1495 1482 1496 - // Step 5: Swap tables 1497 - conn.execute_batch( 1498 - "DROP TABLE item_tags; 1499 - DROP TABLE tags; 1500 - ALTER TABLE tags_new RENAME TO tags; 1501 - ALTER TABLE item_tags_new RENAME TO item_tags; 1483 + // Step 5: Swap tables 1484 + conn.execute_batch( 1485 + "DROP TABLE item_tags; 1486 + DROP TABLE tags; 1487 + ALTER TABLE tags_new RENAME TO tags; 1488 + ALTER TABLE item_tags_new RENAME TO item_tags; 1502 1489 1503 - CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name); 1504 - CREATE INDEX IF NOT EXISTS idx_tags_frecency ON tags(frecencyScore DESC);" 1505 - ).map_err(|e| format!("Failed to swap tables: {}", e))?; 1490 + CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name); 1491 + CREATE INDEX IF NOT EXISTS idx_tags_frecency ON tags(frecencyScore DESC);" 1492 + ).map_err(|e| format!("Failed to swap tables: {}", e))?; 1506 1493 1507 - // Cleanup temp table 1508 - let _ = conn.execute("DROP TABLE IF EXISTS tag_id_mapping", []); 1494 + // Cleanup temp table 1495 + let _ = conn.execute("DROP TABLE IF EXISTS tag_id_mapping", []); 1509 1496 1510 - Ok(()) 1511 - })(); 1497 + Ok(()) 1498 + })(); 1512 1499 1513 - match migration_result { 1514 - Ok(()) => { 1515 - let _ = conn.execute("RELEASE SAVEPOINT before_tag_migration", []); 1516 - ios_log("Tags INTEGER→TEXT migration completed successfully"); 1517 - } 1518 - Err(e) => { 1519 - ios_log(&format!("Tags migration failed, rolling back: {}", e)); 1520 - let _ = conn.execute("ROLLBACK TO SAVEPOINT before_tag_migration", []); 1521 - let _ = conn.execute("RELEASE SAVEPOINT before_tag_migration", []); 1522 - } 1500 + match migration_result { 1501 + Ok(()) => { 1502 + let _ = conn.execute("RELEASE SAVEPOINT before_tag_migration", []); 1503 + ios_log("Tags INTEGER→TEXT migration completed successfully"); 1504 + } 1505 + Err(e) => { 1506 + ios_log(&format!("Tags migration failed, rolling back: {}", e)); 1507 + let _ = conn.execute("ROLLBACK TO SAVEPOINT before_tag_migration", []); 1508 + let _ = conn.execute("RELEASE SAVEPOINT before_tag_migration", []); 1523 1509 } 1524 1510 } 1525 1511 } 1512 + } 1526 1513 1527 - // Ensure tags table exists (for fresh install or if migration skipped) 1528 - let has_tags_table: bool = conn 1529 - .query_row( 1530 - "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='tags'", 1531 - [], 1532 - |row| row.get::<_, i64>(0), 1533 - ) 1534 - .unwrap_or(0) > 0; 1514 + // Ensure tags table exists (for fresh install or if migration skipped) 1515 + let has_tags_table: bool = conn 1516 + .query_row( 1517 + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='tags'", 1518 + [], 1519 + |row| row.get::<_, i64>(0), 1520 + ) 1521 + .unwrap_or(0) > 0; 1535 1522 1536 - if !has_tags_table { 1537 - if let Err(e) = conn.execute_batch( 1538 - " 1523 + if !has_tags_table { 1524 + if let Err(e) = conn.execute_batch( 1525 + " 1539 1526 CREATE TABLE IF NOT EXISTS tags ( 1540 1527 id TEXT PRIMARY KEY, 1541 1528 name TEXT NOT NULL UNIQUE, ··· 1558 1545 CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name); 1559 1546 CREATE INDEX IF NOT EXISTS idx_tags_frecency ON tags(frecencyScore DESC); 1560 1547 ", 1561 - ) { 1562 - init_result = Err(format!("Failed to create tags tables: {}", e)); 1563 - return; 1564 - } 1548 + ) { 1549 + return Err(format!("Failed to create tags tables: {}", e)); 1565 1550 } 1551 + } 1566 1552 1567 - // One-time dedup migration: remove duplicate items 1568 - let dedup_done: bool = conn 1569 - .query_row( 1570 - "SELECT COUNT(*) FROM settings WHERE key = 'dedup_cleanup_v1'", 1571 - [], 1572 - |row| row.get::<_, i64>(0), 1573 - ) 1574 - .unwrap_or(0) > 0; 1553 + // One-time dedup migration: remove duplicate items 1554 + let dedup_done: bool = conn 1555 + .query_row( 1556 + "SELECT COUNT(*) FROM settings WHERE key = 'dedup_cleanup_v1'", 1557 + [], 1558 + |row| row.get::<_, i64>(0), 1559 + ) 1560 + .unwrap_or(0) > 0; 1575 1561 1576 - if !dedup_done { 1577 - println!("[Rust] Running one-time dedup cleanup..."); 1578 - let mut total_removed: i64 = 0; 1562 + if !dedup_done { 1563 + println!("[Rust] Running one-time dedup cleanup..."); 1564 + let mut total_removed: i64 = 0; 1579 1565 1580 - // --- Deduplicate url items by (type, url) --- 1581 - { 1582 - let mut dup_stmt = conn 1566 + // --- Deduplicate url items by (type, url) --- 1567 + { 1568 + let mut dup_stmt = conn 1569 + .prepare( 1570 + "SELECT type, url, COUNT(*) as cnt FROM items \ 1571 + WHERE deleted_at IS NULL AND type = 'url' AND url IS NOT NULL AND url != '' \ 1572 + GROUP BY type, url HAVING cnt > 1", 1573 + ) 1574 + .unwrap(); 1575 + let dup_groups: Vec<(String, String)> = dup_stmt 1576 + .query_map([], |row| { 1577 + Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) 1578 + }) 1579 + .unwrap() 1580 + .filter_map(|r| r.ok()) 1581 + .collect(); 1582 + 1583 + for (item_type, url_val) in &dup_groups { 1584 + let mut item_stmt = conn 1583 1585 .prepare( 1584 - "SELECT type, url, COUNT(*) as cnt FROM items \ 1585 - WHERE deleted_at IS NULL AND type = 'url' AND url IS NOT NULL AND url != '' \ 1586 - GROUP BY type, url HAVING cnt > 1", 1586 + "SELECT id, sync_id, updated_at FROM items \ 1587 + WHERE type = ?1 AND url = ?2 AND deleted_at IS NULL \ 1588 + ORDER BY \ 1589 + CASE WHEN sync_id IS NOT NULL AND sync_id != '' THEN 0 ELSE 1 END, \ 1590 + updated_at DESC", 1587 1591 ) 1588 1592 .unwrap(); 1589 - let dup_groups: Vec<(String, String)> = dup_stmt 1590 - .query_map([], |row| { 1591 - Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) 1592 - }) 1593 + let items: Vec<String> = item_stmt 1594 + .query_map(params![item_type, url_val], |row| row.get::<_, String>(0)) 1593 1595 .unwrap() 1594 1596 .filter_map(|r| r.ok()) 1595 1597 .collect(); 1596 1598 1597 - for (item_type, url_val) in &dup_groups { 1598 - let mut item_stmt = conn 1599 - .prepare( 1600 - "SELECT id, sync_id, updated_at FROM items \ 1601 - WHERE type = ?1 AND url = ?2 AND deleted_at IS NULL \ 1602 - ORDER BY \ 1603 - CASE WHEN sync_id IS NOT NULL AND sync_id != '' THEN 0 ELSE 1 END, \ 1604 - updated_at DESC", 1605 - ) 1606 - .unwrap(); 1607 - let items: Vec<String> = item_stmt 1608 - .query_map(params![item_type, url_val], |row| row.get::<_, String>(0)) 1609 - .unwrap() 1610 - .filter_map(|r| r.ok()) 1611 - .collect(); 1612 - 1613 - for id in items.iter().skip(1) { 1614 - let _ = conn.execute("DELETE FROM item_tags WHERE item_id = ?1", params![id]); 1615 - let _ = conn.execute("DELETE FROM items WHERE id = ?1", params![id]); 1616 - total_removed += 1; 1617 - } 1599 + for id in items.iter().skip(1) { 1600 + let _ = conn.execute("DELETE FROM item_tags WHERE item_id = ?1", params![id]); 1601 + let _ = conn.execute("DELETE FROM items WHERE id = ?1", params![id]); 1602 + total_removed += 1; 1618 1603 } 1619 1604 } 1605 + } 1620 1606 1621 - // --- Deduplicate text items by (type, content) --- 1622 - { 1623 - let mut dup_stmt = conn 1607 + // --- Deduplicate text items by (type, content) --- 1608 + { 1609 + let mut dup_stmt = conn 1610 + .prepare( 1611 + "SELECT type, content, COUNT(*) as cnt FROM items \ 1612 + WHERE deleted_at IS NULL AND type = 'text' AND content IS NOT NULL AND content != '' \ 1613 + GROUP BY type, content HAVING cnt > 1", 1614 + ) 1615 + .unwrap(); 1616 + let dup_groups: Vec<(String, String)> = dup_stmt 1617 + .query_map([], |row| { 1618 + Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) 1619 + }) 1620 + .unwrap() 1621 + .filter_map(|r| r.ok()) 1622 + .collect(); 1623 + 1624 + for (item_type, content_val) in &dup_groups { 1625 + let mut item_stmt = conn 1624 1626 .prepare( 1625 - "SELECT type, content, COUNT(*) as cnt FROM items \ 1626 - WHERE deleted_at IS NULL AND type = 'text' AND content IS NOT NULL AND content != '' \ 1627 - GROUP BY type, content HAVING cnt > 1", 1627 + "SELECT id, sync_id, updated_at FROM items \ 1628 + WHERE type = ?1 AND content = ?2 AND deleted_at IS NULL \ 1629 + ORDER BY \ 1630 + CASE WHEN sync_id IS NOT NULL AND sync_id != '' THEN 0 ELSE 1 END, \ 1631 + updated_at DESC", 1628 1632 ) 1629 1633 .unwrap(); 1630 - let dup_groups: Vec<(String, String)> = dup_stmt 1631 - .query_map([], |row| { 1632 - Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) 1633 - }) 1634 + let items: Vec<String> = item_stmt 1635 + .query_map(params![item_type, content_val], |row| row.get::<_, String>(0)) 1634 1636 .unwrap() 1635 1637 .filter_map(|r| r.ok()) 1636 1638 .collect(); 1637 1639 1638 - for (item_type, content_val) in &dup_groups { 1639 - let mut item_stmt = conn 1640 - .prepare( 1641 - "SELECT id, sync_id, updated_at FROM items \ 1642 - WHERE type = ?1 AND content = ?2 AND deleted_at IS NULL \ 1643 - ORDER BY \ 1644 - CASE WHEN sync_id IS NOT NULL AND sync_id != '' THEN 0 ELSE 1 END, \ 1645 - updated_at DESC", 1646 - ) 1647 - .unwrap(); 1648 - let items: Vec<String> = item_stmt 1649 - .query_map(params![item_type, content_val], |row| row.get::<_, String>(0)) 1650 - .unwrap() 1651 - .filter_map(|r| r.ok()) 1652 - .collect(); 1653 - 1654 - for id in items.iter().skip(1) { 1655 - let _ = conn.execute("DELETE FROM item_tags WHERE item_id = ?1", params![id]); 1656 - let _ = conn.execute("DELETE FROM items WHERE id = ?1", params![id]); 1657 - total_removed += 1; 1658 - } 1640 + for id in items.iter().skip(1) { 1641 + let _ = conn.execute("DELETE FROM item_tags WHERE item_id = ?1", params![id]); 1642 + let _ = conn.execute("DELETE FROM items WHERE id = ?1", params![id]); 1643 + total_removed += 1; 1659 1644 } 1660 1645 } 1646 + } 1661 1647 1662 - // --- Deduplicate tagsets by sorted tag names --- 1663 - { 1664 - let mut ts_stmt = conn 1648 + // --- Deduplicate tagsets by sorted tag names --- 1649 + { 1650 + let mut ts_stmt = conn 1651 + .prepare( 1652 + "SELECT id, sync_id, updated_at FROM items \ 1653 + WHERE type = 'tagset' AND deleted_at IS NULL", 1654 + ) 1655 + .unwrap(); 1656 + let tagsets: Vec<(String, String, String)> = ts_stmt 1657 + .query_map([], |row| { 1658 + Ok(( 1659 + row.get::<_, String>(0)?, 1660 + row.get::<_, String>(1).unwrap_or_default(), 1661 + row.get::<_, String>(2)?, 1662 + )) 1663 + }) 1664 + .unwrap() 1665 + .filter_map(|r| r.ok()) 1666 + .collect(); 1667 + 1668 + let mut groups: std::collections::HashMap<String, Vec<(String, String, String)>> = 1669 + std::collections::HashMap::new(); 1670 + 1671 + for (id, sync_id, updated_at) in &tagsets { 1672 + let mut tag_stmt = conn 1665 1673 .prepare( 1666 - "SELECT id, sync_id, updated_at FROM items \ 1667 - WHERE type = 'tagset' AND deleted_at IS NULL", 1674 + "SELECT t.name FROM tags t \ 1675 + JOIN item_tags it ON t.id = it.tag_id \ 1676 + WHERE it.item_id = ?1 \ 1677 + ORDER BY t.name", 1668 1678 ) 1669 1679 .unwrap(); 1670 - let tagsets: Vec<(String, String, String)> = ts_stmt 1671 - .query_map([], |row| { 1672 - Ok(( 1673 - row.get::<_, String>(0)?, 1674 - row.get::<_, String>(1).unwrap_or_default(), 1675 - row.get::<_, String>(2)?, 1676 - )) 1677 - }) 1680 + let tag_names: Vec<String> = tag_stmt 1681 + .query_map(params![id], |row| row.get::<_, String>(0)) 1678 1682 .unwrap() 1679 1683 .filter_map(|r| r.ok()) 1680 1684 .collect(); 1681 - 1682 - let mut groups: std::collections::HashMap<String, Vec<(String, String, String)>> = 1683 - std::collections::HashMap::new(); 1685 + let key = tag_names.join("\0"); 1686 + groups 1687 + .entry(key) 1688 + .or_default() 1689 + .push((id.clone(), sync_id.clone(), updated_at.clone())); 1690 + } 1684 1691 1685 - for (id, sync_id, updated_at) in &tagsets { 1686 - let mut tag_stmt = conn 1687 - .prepare( 1688 - "SELECT t.name FROM tags t \ 1689 - JOIN item_tags it ON t.id = it.tag_id \ 1690 - WHERE it.item_id = ?1 \ 1691 - ORDER BY t.name", 1692 - ) 1693 - .unwrap(); 1694 - let tag_names: Vec<String> = tag_stmt 1695 - .query_map(params![id], |row| row.get::<_, String>(0)) 1696 - .unwrap() 1697 - .filter_map(|r| r.ok()) 1698 - .collect(); 1699 - let key = tag_names.join("\0"); 1700 - groups 1701 - .entry(key) 1702 - .or_default() 1703 - .push((id.clone(), sync_id.clone(), updated_at.clone())); 1692 + for items in groups.values_mut() { 1693 + if items.len() <= 1 { 1694 + continue; 1704 1695 } 1705 1696 1706 - for items in groups.values_mut() { 1707 - if items.len() <= 1 { 1708 - continue; 1697 + // Sort: prefer sync_id, then newest updated_at 1698 + items.sort_by(|a, b| { 1699 + let a_has_sync = if !a.1.is_empty() { 0 } else { 1 }; 1700 + let b_has_sync = if !b.1.is_empty() { 0 } else { 1 }; 1701 + if a_has_sync != b_has_sync { 1702 + return a_has_sync.cmp(&b_has_sync); 1709 1703 } 1710 - 1711 - // Sort: prefer sync_id, then newest updated_at 1712 - items.sort_by(|a, b| { 1713 - let a_has_sync = if !a.1.is_empty() { 0 } else { 1 }; 1714 - let b_has_sync = if !b.1.is_empty() { 0 } else { 1 }; 1715 - if a_has_sync != b_has_sync { 1716 - return a_has_sync.cmp(&b_has_sync); 1717 - } 1718 - b.2.cmp(&a.2) 1719 - }); 1704 + b.2.cmp(&a.2) 1705 + }); 1720 1706 1721 - for item in items.iter().skip(1) { 1722 - let _ = conn.execute("DELETE FROM item_tags WHERE item_id = ?1", params![item.0]); 1723 - let _ = conn.execute("DELETE FROM items WHERE id = ?1", params![item.0]); 1724 - total_removed += 1; 1725 - } 1707 + for item in items.iter().skip(1) { 1708 + let _ = conn.execute("DELETE FROM item_tags WHERE item_id = ?1", params![item.0]); 1709 + let _ = conn.execute("DELETE FROM items WHERE id = ?1", params![item.0]); 1710 + total_removed += 1; 1726 1711 } 1727 1712 } 1728 - 1729 - // Set flag 1730 - let _ = conn.execute( 1731 - "INSERT OR REPLACE INTO settings (key, value) VALUES ('dedup_cleanup_v1', '1')", 1732 - [], 1733 - ); 1734 - 1735 - if total_removed > 0 { 1736 - println!("[Rust] Dedup cleanup: removed {} duplicate items", total_removed); 1737 - } else { 1738 - println!("[Rust] Dedup cleanup: no duplicates found"); 1739 - } 1740 1713 } 1741 1714 1742 - // Write schema version to settings table for compatibility tracking. 1743 - // This allows detecting mismatches between Rust main app and Swift Share Extension. 1715 + // Set flag 1744 1716 let _ = conn.execute( 1745 - "INSERT OR REPLACE INTO settings (key, value) VALUES ('schema_version', ?1)", 1746 - params![SCHEMA_VERSION], 1717 + "INSERT OR REPLACE INTO settings (key, value) VALUES ('dedup_cleanup_v1', '1')", 1718 + [], 1747 1719 ); 1748 1720 1749 - // Check for schema version mismatch (Swift Share Extension may have written a different version) 1750 - if let Ok(existing_version) = conn.query_row::<String, _, _>( 1751 - "SELECT value FROM settings WHERE key = 'schema_version_swift'", 1752 - [], 1753 - |row| row.get(0), 1754 - ) { 1755 - if existing_version != SCHEMA_VERSION { 1756 - println!( 1757 - "[Rust] WARNING: Schema version mismatch! Rust: {}, Swift: {}", 1758 - SCHEMA_VERSION, existing_version 1759 - ); 1760 - } 1721 + if total_removed > 0 { 1722 + println!("[Rust] Dedup cleanup: removed {} duplicate items", total_removed); 1723 + } else { 1724 + println!("[Rust] Dedup cleanup: no duplicates found"); 1761 1725 } 1726 + } 1762 1727 1763 - // Initialize device ID (generate and store if missing) 1764 - let device_id: String = conn 1765 - .query_row("SELECT value FROM settings WHERE key = 'device_id'", [], |row| row.get(0)) 1766 - .unwrap_or_default(); 1728 + // Write schema version to settings table for compatibility tracking. 1729 + // This allows detecting mismatches between Rust main app and Swift Share Extension. 1730 + let _ = conn.execute( 1731 + "INSERT OR REPLACE INTO settings (key, value) VALUES ('schema_version', ?1)", 1732 + params![SCHEMA_VERSION], 1733 + ); 1767 1734 1768 - if device_id.is_empty() { 1769 - let new_id = uuid::Uuid::new_v4().to_string(); 1770 - let _ = conn.execute( 1771 - "INSERT OR REPLACE INTO settings (key, value) VALUES ('device_id', ?1)", 1772 - params![&new_id], 1735 + // Check for schema version mismatch (Swift Share Extension may have written a different version) 1736 + if let Ok(existing_version) = conn.query_row::<String, _, _>( 1737 + "SELECT value FROM settings WHERE key = 'schema_version_swift'", 1738 + [], 1739 + |row| row.get(0), 1740 + ) { 1741 + if existing_version != SCHEMA_VERSION { 1742 + println!( 1743 + "[Rust] WARNING: Schema version mismatch! Rust: {}, Swift: {}", 1744 + SCHEMA_VERSION, existing_version 1773 1745 ); 1774 - println!("[Rust] Generated new device ID: {}", new_id); 1775 - let _ = DEVICE_ID.set(new_id); 1776 - } else { 1777 - println!("[Rust] Loaded existing device ID: {}", device_id); 1778 - let _ = DEVICE_ID.set(device_id); 1779 1746 } 1747 + } 1780 1748 1781 - println!("[Rust] Database initialized successfully (schema version {})", SCHEMA_VERSION); 1782 - }); 1749 + // Initialize device ID (generate and store if missing) 1750 + let device_id: String = conn 1751 + .query_row("SELECT value FROM settings WHERE key = 'device_id'", [], |row| row.get(0)) 1752 + .unwrap_or_default(); 1753 + 1754 + if device_id.is_empty() { 1755 + let new_id = uuid::Uuid::new_v4().to_string(); 1756 + let _ = conn.execute( 1757 + "INSERT OR REPLACE INTO settings (key, value) VALUES ('device_id', ?1)", 1758 + params![&new_id], 1759 + ); 1760 + println!("[Rust] Generated new device ID: {}", new_id); 1761 + let _ = DEVICE_ID.set(new_id); 1762 + } else { 1763 + println!("[Rust] Loaded existing device ID: {}", device_id); 1764 + let _ = DEVICE_ID.set(device_id); 1765 + } 1766 + 1767 + println!("[Rust] Database initialized successfully (schema version {})", SCHEMA_VERSION); 1783 1768 1784 - init_result 1769 + *initialized = true; 1770 + Ok(()) 1785 1771 } 1786 1772 1787 1773 /// Get the device ID (raw UUID, no prefix). Cached in OnceLock after first DB read. ··· 4215 4201 4216 4202 /// Clear database cache to force re-initialization on profile switch 4217 4203 fn clear_db_cache() { 4218 - // The DB_INIT Once guard can't be reset, but we can work around this 4219 - // by tracking the current profile in a separate variable 4220 - // For now, profile switch will require app restart for full isolation 4221 - // This matches desktop behavior where profile switch restarts the app 4222 - println!("[Rust] Note: Full profile switch requires app restart for complete database isolation"); 4204 + if let Ok(mut initialized) = DB_INITIALIZED.lock() { 4205 + *initialized = false; 4206 + } 4207 + println!("[Rust] Database cache cleared, will re-initialize on next access"); 4223 4208 } 4224 4209 4225 4210 #[tauri::command]