absurder-sql 0.1.23

AbsurderSQL - SQLite + IndexedDB that's absurdly better than absurd-sql
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
//! Import functionality for SQLite databases
//!
//! This module handles importing SQLite .db files into the block-based storage system.

use super::block_storage::BLOCK_SIZE;
use super::export::validate_sqlite_file;
use crate::types::DatabaseError;

/// Clear all storage data for a specific database
///
/// Removes all blocks, metadata, commit markers, and allocation maps for the specified
/// database from global storage. This is a destructive operation used primarily before
/// importing a new database.
///
/// # Arguments
/// * `db_name` - Name of the database to clear
///
/// # Returns
/// * `Ok(())` - Storage cleared successfully
/// * `Err(DatabaseError)` - If clearing fails
///
/// # Safety
/// This operation clears data from global storage but does not affect:
/// - Open database connections (they may still reference cleared data)
/// - IndexedDB persistence (for WASM, requires separate clearing)
///
/// # Example
/// ```rust,no_run
/// use absurder_sql::storage::import::clear_database_storage;
///
/// # async fn example() -> Result<(), absurder_sql::types::DatabaseError> {
/// // Clear all data for "mydb"
/// clear_database_storage("mydb").await?;
/// # Ok(())
/// # }
/// ```
pub async fn clear_database_storage(db_name: &str) -> Result<(), DatabaseError> {
    use super::vfs_sync::{
        with_global_allocation_map, with_global_commit_marker, with_global_storage,
    };

    log::info!("Clearing storage for database: {}", db_name);

    // CRITICAL: Remove from STORAGE_REGISTRY so a fresh BlockStorage is created on next open
    // This prevents stale state (local cache, dirty_blocks) from being reused
    #[cfg(target_arch = "wasm32")]
    {
        crate::vfs::indexeddb_vfs::remove_storage_from_registry(db_name);
        log::debug!("Removed {} from STORAGE_REGISTRY", db_name);
    }

    // CRITICAL: Force close connection pool entry to reset SQLite internal state
    // Without this, SQLite may use cached pages from the old database
    #[cfg(target_arch = "wasm32")]
    {
        let pool_key = db_name.trim_end_matches(".db");
        crate::connection_pool::force_close_connection(pool_key);
        log::debug!("Force closed connection pool for {}", pool_key);
    }

    // Clear GLOBAL_STORAGE blocks
    with_global_storage(|gs| {
        let mut storage = gs.borrow_mut();
        if let Some(blocks) = storage.get_mut(db_name) {
            let count = blocks.len();
            blocks.clear();
            log::debug!(
                "Cleared {} blocks from GLOBAL_STORAGE for {}",
                count,
                db_name
            );
        }
        // Remove the database entry entirely
        storage.remove(db_name);
    });

    // Clear metadata - platform specific
    #[cfg(target_arch = "wasm32")]
    {
        use super::vfs_sync::with_global_metadata;
        with_global_metadata(|gm| {
            let mut metadata = gm.borrow_mut();
            if let Some(meta) = metadata.get_mut(db_name) {
                let count = meta.len();
                meta.clear();
                log::debug!("Cleared {} metadata entries for {} (WASM)", count, db_name);
            }
            metadata.remove(db_name);
        });
    }

    #[cfg(all(
        not(target_arch = "wasm32"),
        any(test, debug_assertions),
        not(feature = "fs_persist")
    ))]
    {
        use super::block_storage::GLOBAL_METADATA_TEST;
        GLOBAL_METADATA_TEST.with(|gm| {
            #[cfg(target_arch = "wasm32")]
            let metadata = gm;
            #[cfg(not(target_arch = "wasm32"))]
            let mut metadata = gm.lock();
            if let Some(meta) = metadata.get_mut(db_name) {
                let count = meta.len();
                meta.clear();
                log::debug!(
                    "Cleared {} metadata entries from GLOBAL_METADATA_TEST for {} (native test)",
                    count,
                    db_name
                );
            }
            metadata.remove(db_name);
        });
    }

    // Reset GLOBAL_COMMIT_MARKER
    with_global_commit_marker(|gcm| {
        let mut markers = gcm.borrow_mut();
        if markers.contains_key(db_name) {
            markers.insert(db_name.to_string(), 0);
            log::debug!("Reset commit marker for {}", db_name);
        }
        markers.remove(db_name);
    });

    // Clear GLOBAL_ALLOCATION_MAP
    with_global_allocation_map(|gam| {
        let mut alloc = gam.borrow_mut();
        if let Some(ids) = alloc.get_mut(db_name) {
            let count = ids.len();
            ids.clear();
            log::debug!("Cleared {} allocation IDs for {}", count, db_name);
        }
        alloc.remove(db_name);
    });

    // For WASM, also clear IndexedDB (if needed)
    #[cfg(target_arch = "wasm32")]
    {
        // Note: IndexedDB clearing would be done via JavaScript
        // The VFS layer will handle actual persistence clearing
        log::debug!(
            "WASM: In-memory storage cleared for {}. IndexedDB clearing requires VFS interaction.",
            db_name
        );
    }

    log::info!("Storage cleared successfully for: {}", db_name);
    Ok(())
}

/// Import SQLite database from bytes into BlockStorage
///
/// Takes a complete SQLite .db file and imports it into the block-based storage system.
/// This is the inverse of `export_database_to_bytes()`.
///
/// # Arguments
/// * `db_name` - Name of the database to import into
/// * `data` - Complete SQLite database file as bytes
///
/// # Returns
/// * `Ok(())` - Import successful
/// * `Err(DatabaseError)` - If validation or import fails
///
/// # Process
/// 1. Validate SQLite file format
/// 2. Clear existing storage for the database
/// 3. Split data into BLOCK_SIZE (4096-byte) chunks
/// 4. Pad last block with zeros if needed
/// 5. Write all blocks to GLOBAL_STORAGE
/// 6. Update allocation map
///
/// # Example
/// ```rust,no_run
/// use absurder_sql::storage::import::import_database_from_bytes;
///
/// # async fn example() -> Result<(), absurder_sql::types::DatabaseError> {
/// let db_bytes = std::fs::read("mydb.db").unwrap();
/// import_database_from_bytes("mydb", db_bytes).await?;
/// # Ok(())
/// # }
/// ```
pub async fn import_database_from_bytes(db_name: &str, data: Vec<u8>) -> Result<(), DatabaseError> {
    use super::vfs_sync::{with_global_allocation_map, with_global_storage};
    use std::collections::{HashMap, HashSet};

    log::info!(
        "Starting database import for: {} ({} bytes)",
        db_name,
        data.len()
    );

    // Step 1: Validate SQLite file format
    validate_sqlite_file(&data)?;
    log::debug!("SQLite file validation passed");

    // Step 2: Clear existing storage from memory (this also does registry and connection pool cleanup)
    clear_database_storage(db_name).await?;
    log::debug!("Existing storage cleared from memory");

    // Step 3: Delete ALL old blocks from IndexedDB (without needing to know block IDs)
    // This is critical because close() clears GLOBAL_ALLOCATION_MAP, so we can't rely on it
    // to know which blocks exist. Instead, scan IndexedDB directly.
    #[cfg(target_arch = "wasm32")]
    {
        log::debug!(
            "Deleting all existing blocks from IndexedDB for: {}",
            db_name
        );
        super::wasm_indexeddb::delete_all_database_blocks_from_indexeddb(db_name).await?;
        log::debug!("All old blocks deleted from IndexedDB");
    }

    // Step 4: Split data into BLOCK_SIZE chunks
    let total_blocks = data.len().div_ceil(BLOCK_SIZE);
    log::debug!(
        "Splitting {} bytes into {} blocks of {} bytes",
        data.len(),
        total_blocks,
        BLOCK_SIZE
    );

    let mut blocks = HashMap::new();
    let mut allocated_ids = HashSet::new();

    for block_id in 0..total_blocks {
        let start = block_id * BLOCK_SIZE;
        let end = std::cmp::min(start + BLOCK_SIZE, data.len());

        let mut block_data = Vec::with_capacity(BLOCK_SIZE);
        block_data.extend_from_slice(&data[start..end]);

        // Step 4: Pad last block with zeros if needed
        if block_data.len() < BLOCK_SIZE {
            let padding = BLOCK_SIZE - block_data.len();
            block_data.resize(BLOCK_SIZE, 0);
            log::debug!(
                "Block {} padded with {} zero bytes ({} -> {} bytes)",
                block_id,
                padding,
                end - start,
                BLOCK_SIZE
            );
        }

        blocks.insert(block_id as u64, block_data);
        allocated_ids.insert(block_id as u64);
    }

    log::debug!("Created {} blocks for import", blocks.len());

    // Step 5: Write blocks to GLOBAL_STORAGE
    with_global_storage(|gs| {
        gs.borrow_mut().insert(db_name.to_string(), blocks.clone());
    });

    log::debug!("Blocks written to GLOBAL_STORAGE");

    // Step 6: Update allocation map
    with_global_allocation_map(|gam| {
        gam.borrow_mut()
            .insert(db_name.to_string(), allocated_ids.clone());
    });

    log::debug!("Allocation map updated");

    // Step 7: Set up metadata for imported blocks (for visibility tracking)
    // This ensures imported blocks are immediately visible when read

    // For WASM, set up metadata in global storage
    #[cfg(target_arch = "wasm32")]
    {
        use super::metadata::{BlockMetadataPersist, ChecksumAlgorithm, ChecksumManager};
        use super::vfs_sync::with_global_metadata;

        with_global_metadata(|gm| {
            let mut db_metadata = std::collections::HashMap::new();

            for block_id in allocated_ids.iter() {
                // Calculate checksum for each block using CRC32 (standard algorithm)
                let checksum = if let Some(block_data) = blocks.get(block_id) {
                    ChecksumManager::compute_checksum_with(block_data, ChecksumAlgorithm::CRC32)
                } else {
                    0
                };

                db_metadata.insert(
                    *block_id,
                    BlockMetadataPersist {
                        version: 1, // All imported blocks start at version 1
                        checksum,
                        last_modified_ms: 0,
                        algo: ChecksumAlgorithm::CRC32,
                    },
                );
            }

            gm.borrow_mut().insert(db_name.to_string(), db_metadata);
        });

        log::debug!(
            "Metadata created for {} blocks in global storage (WASM)",
            allocated_ids.len()
        );
    }

    // For native tests (without fs_persist), use GLOBAL_METADATA_TEST directly
    #[cfg(all(
        not(target_arch = "wasm32"),
        any(test, debug_assertions),
        not(feature = "fs_persist")
    ))]
    {
        use super::block_storage::GLOBAL_METADATA_TEST;
        use super::metadata::{BlockMetadataPersist, ChecksumAlgorithm, ChecksumManager};

        GLOBAL_METADATA_TEST.with(|gm| {
            #[cfg(target_arch = "wasm32")]
            let metadata = gm;
            #[cfg(not(target_arch = "wasm32"))]
            let mut metadata = gm.lock();
            let mut db_metadata = std::collections::HashMap::new();

            for block_id in allocated_ids.iter() {
                // Calculate checksum for each block using CRC32 (standard algorithm)
                let checksum = if let Some(block_data) = blocks.get(block_id) {
                    ChecksumManager::compute_checksum_with(block_data, ChecksumAlgorithm::CRC32)
                } else {
                    0
                };

                db_metadata.insert(
                    *block_id,
                    BlockMetadataPersist {
                        version: 1, // All imported blocks start at version 1
                        checksum,
                        last_modified_ms: 0,
                        algo: ChecksumAlgorithm::CRC32,
                    },
                );
            }

            metadata.insert(db_name.to_string(), db_metadata);
        });

        log::debug!(
            "Metadata created for {} blocks in GLOBAL_METADATA_TEST (native test)",
            allocated_ids.len()
        );
    }

    // For fs_persist (including tests), write metadata to filesystem
    #[cfg(all(not(target_arch = "wasm32"), feature = "fs_persist"))]
    {
        use super::metadata::{ChecksumAlgorithm, ChecksumManager};
        use std::path::PathBuf;

        let base_path =
            std::env::var("ABSURDERSQL_FS_BASE").unwrap_or_else(|_| "./test_storage".to_string());
        let mut meta_path = PathBuf::from(&base_path);
        meta_path.push(db_name);

        // Create directory if needed
        if let Err(e) = std::fs::create_dir_all(&meta_path) {
            log::warn!("Failed to create metadata directory during import: {}", e);
        }

        meta_path.push("metadata.json");

        // Build metadata structure
        let mut meta_entries = Vec::new();
        for block_id in allocated_ids.iter() {
            let checksum = if let Some(block_data) = blocks.get(block_id) {
                ChecksumManager::compute_checksum_with(block_data, ChecksumAlgorithm::CRC32)
            } else {
                0
            };

            meta_entries.push((
                *block_id,
                super::metadata::BlockMetadataPersist {
                    version: 1,
                    checksum,
                    last_modified_ms: 0,
                    algo: ChecksumAlgorithm::CRC32,
                },
            ));
        }

        let meta_json = serde_json::json!({
            "entries": meta_entries,
        });

        if let Err(e) = std::fs::write(
            &meta_path,
            serde_json::to_string_pretty(&meta_json).unwrap(),
        ) {
            log::warn!("Failed to write metadata during import: {}", e);
        } else {
            log::debug!(
                "Metadata written to filesystem for {} blocks",
                allocated_ids.len()
            );
        }

        // Write block data files to filesystem
        let mut blocks_dir = PathBuf::from(&base_path);
        blocks_dir.push(db_name);
        blocks_dir.push("blocks");

        if let Err(e) = std::fs::create_dir_all(&blocks_dir) {
            log::warn!("Failed to create blocks directory during import: {}", e);
        }

        for (block_id, block_data) in blocks.iter() {
            let mut block_path = blocks_dir.clone();
            block_path.push(format!("block_{}.bin", block_id));

            if let Err(e) = std::fs::write(&block_path, block_data) {
                log::warn!("Failed to write block {} during import: {}", block_id, e);
            }
        }

        log::debug!("Wrote {} block files to filesystem", blocks.len());

        // Write allocations.json
        let mut alloc_path = PathBuf::from(&base_path);
        alloc_path.push(db_name);
        alloc_path.push("allocations.json");

        let alloc_json = serde_json::json!({
            "allocated": allocated_ids.iter().copied().collect::<Vec<_>>(),
        });

        if let Err(e) = std::fs::write(
            &alloc_path,
            serde_json::to_string_pretty(&alloc_json).unwrap(),
        ) {
            log::warn!("Failed to write allocations during import: {}", e);
        } else {
            log::debug!("Allocations written to filesystem");
        }
    }

    // Step 8: Set commit marker to 1 to make all imported blocks visible
    use super::vfs_sync::with_global_commit_marker;
    with_global_commit_marker(|gcm| {
        gcm.borrow_mut().insert(db_name.to_string(), 1);
    });

    log::debug!("Commit marker set to 1 for immediate visibility");

    // Step 10: For WASM, sync imported data to IndexedDB immediately and WAIT for it
    #[cfg(target_arch = "wasm32")]
    {
        log::debug!("Syncing imported data to IndexedDB for {}", db_name);

        // Advance commit marker
        let next_commit = with_global_commit_marker(|cm| {
            let current = cm.borrow().get(db_name).copied().unwrap_or(0);
            let new_marker = current + 1;
            cm.borrow_mut().insert(db_name.to_string(), new_marker);
            new_marker
        });

        // Collect blocks and metadata to persist
        let (blocks_to_persist, metadata_to_persist) = {
            use super::vfs_sync::with_global_metadata;
            with_global_storage(|storage| {
                let blocks = if let Some(db_storage) = storage.borrow().get(db_name) {
                    db_storage
                        .iter()
                        .map(|(&id, data)| (id, data.clone()))
                        .collect::<Vec<_>>()
                } else {
                    Vec::new()
                };

                let metadata = with_global_metadata(|meta| {
                    if let Some(db_meta) = meta.borrow().get(db_name) {
                        db_meta
                            .iter()
                            .map(|(&id, metadata)| (id, metadata.checksum))
                            .collect::<Vec<_>>()
                    } else {
                        Vec::new()
                    }
                });

                (blocks, metadata)
            })
        };

        if !blocks_to_persist.is_empty() {
            log::debug!(
                "Persisting {} blocks to IndexedDB with commit marker {}",
                blocks_to_persist.len(),
                next_commit
            );

            // CRITICAL: AWAIT the persistence to complete BEFORE returning
            super::wasm_indexeddb::persist_to_indexeddb_event_based(
                db_name,
                blocks_to_persist,
                metadata_to_persist,
                next_commit,
                #[cfg(feature = "telemetry")]
                None,
                #[cfg(feature = "telemetry")]
                None,
            )
            .await
            .map_err(|e| {
                log::error!("Failed to persist imported data to IndexedDB: {}", e);
                DatabaseError::new(
                    "IMPORT_SYNC_FAILED",
                    &format!("Failed to persist imported data: {}", e),
                )
            })?;

            log::debug!("Import sync to IndexedDB complete for {}", db_name);
        } else {
            log::warn!("No blocks to persist to IndexedDB for {}", db_name);
        }
    }

    log::info!(
        "Database import complete: {} ({} blocks, {} bytes)",
        db_name,
        total_blocks,
        data.len()
    );

    // DEBUG: Log what blocks were actually imported
    #[cfg(target_arch = "wasm32")]
    {
        use super::vfs_sync::with_global_storage;
        with_global_storage(|storage_map| {
            if let Some(db_storage) = storage_map.borrow().get(db_name) {
                web_sys::console::log_1(
                    &format!(
                        "[IMPORT] GLOBAL_STORAGE now has {} blocks for {}",
                        db_storage.len(),
                        db_name
                    )
                    .into(),
                );
                for (block_id, data) in db_storage.iter().take(5) {
                    web_sys::console::log_1(
                        &format!(
                            "[IMPORT] Block {} has {} bytes, first 16: {:02x?}",
                            block_id,
                            data.len(),
                            &data[..16.min(data.len())]
                        )
                        .into(),
                    );
                }
            }
        });
    }

    Ok(())
}

/// Invalidate BlockStorage caches for a specific database
///
/// This function removes the BlockStorage from the registry, forcing a fresh
/// instance to be created on next open. This ensures no stale cached data
/// is read after an import operation.
///
/// # Arguments
/// * `db_name` - Name of the database whose caches should be invalidated
///
/// # Example
/// ```rust
/// use absurder_sql::storage::import::invalidate_block_storage_caches;
///
/// // After importing a database, clear caches
/// invalidate_block_storage_caches("mydb");
/// ```
pub fn invalidate_block_storage_caches(db_name: &str) {
    #[cfg(target_arch = "wasm32")]
    {
        crate::vfs::indexeddb_vfs::remove_storage_from_registry(db_name);
        log::info!("Removed BlockStorage from registry for: {}", db_name);
    }

    #[cfg(not(target_arch = "wasm32"))]
    {
        log::info!(
            "Cache invalidation for native not yet implemented for: {}",
            db_name
        );
    }
}