Skip to main content

cqlite_cli/commands/
admin.rs

1use crate::cli_types::AdminCommands;
2use anyhow::Result;
3#[cfg(feature = "state_machine")]
4use chrono;
5use cqlite_core::Database;
6
7#[cfg(feature = "state_machine")]
8pub async fn handle_admin_command(database: &Database, command: AdminCommands) -> Result<()> {
9    match command {
10        AdminCommands::Info => show_database_info(database).await,
11        AdminCommands::Compact { force: _ } => compact_database(database).await,
12        AdminCommands::Backup {
13            destination,
14            compression: _,
15        } => backup_database(database, &destination).await,
16        AdminCommands::Restore { backup, force: _ } => restore_database(database, &backup).await,
17    }
18}
19
20#[cfg(not(feature = "state_machine"))]
21pub async fn handle_admin_command(_database: &Database, _command: AdminCommands) -> Result<()> {
22    Err(anyhow::anyhow!(
23        "Admin commands requiring query execution are not available in M1.\n\
24         Build with --features state_machine or use SSTableReader directly.\n\
25         See CLAUDE.md for M1 API examples."
26    ))
27}
28
29#[cfg(feature = "state_machine")]
30async fn show_database_info(database: &Database) -> Result<()> {
31    println!("Database Information:");
32
33    // Get database statistics
34    match database.stats().await {
35        Ok(stats) => {
36            println!("Storage Engine Stats:");
37            println!(
38                "  - SSTable count: {}",
39                stats.storage_stats.sstables.sstable_count
40            );
41            println!(
42                "  - Total entries: {}",
43                stats.storage_stats.sstables.total_entries
44            );
45
46            println!("Memory Stats:");
47            println!(
48                "  - Total memory used: {} bytes",
49                stats.memory_stats.total_memory_used
50            );
51            println!(
52                "  - Block cache hits: {}",
53                stats.memory_stats.block_cache_hits
54            );
55
56            #[cfg(feature = "state_machine")]
57            {
58                println!("Query Engine Stats:");
59                println!("  - Total queries: {}", stats.query_stats.total_queries);
60                println!(
61                    "  - Average execution time: {} μs",
62                    stats.query_stats.avg_execution_time_us
63                );
64            }
65        }
66        Err(e) => {
67            println!("Failed to get database statistics: {}", e);
68        }
69    }
70
71    Ok(())
72}
73
74#[cfg(all(feature = "state_machine", feature = "experimental"))]
75async fn compact_database(database: &Database) -> Result<()> {
76    println!("Starting database compaction...");
77
78    match database.compact().await {
79        Ok(_) => {
80            println!("Database compaction successfully");
81        }
82        Err(e) => {
83            println!("Database compaction failed: {}", e);
84            return Err(anyhow::anyhow!("Compaction failed: {}", e));
85        }
86    }
87
88    Ok(())
89}
90
91#[cfg(all(feature = "state_machine", not(feature = "experimental")))]
92async fn compact_database(_database: &Database) -> Result<()> {
93    Err(anyhow::anyhow!(
94        "Database compaction requires the 'experimental' feature.\n\
95         Build with --features experimental to enable write operations."
96    ))
97}
98
99#[cfg(feature = "state_machine")]
100async fn backup_database(database: &Database, output: &std::path::Path) -> Result<()> {
101    use indicatif::{ProgressBar, ProgressStyle};
102    use serde_json;
103    use std::fs::File;
104    use std::io::{BufWriter, Write};
105
106    println!("Backing up database to {}", output.display());
107
108    // Create output directory if it doesn't exist
109    if let Some(parent) = output.parent() {
110        std::fs::create_dir_all(parent)
111            .map_err(|e| anyhow::anyhow!("Failed to create backup directory: {}", e))?;
112    }
113
114    // Create backup file
115    let backup_file =
116        File::create(output).map_err(|e| anyhow::anyhow!("Failed to create backup file: {}", e))?;
117    let mut writer = BufWriter::new(backup_file);
118
119    // Get database statistics for progress indication
120    let stats = match database.stats().await {
121        Ok(stats) => stats,
122        Err(e) => {
123            println!("Warning: Could not get database statistics: {}", e);
124            // Continue with backup anyway
125            return create_basic_backup(database, &mut writer, output).await;
126        }
127    };
128
129    // Create progress bar
130    let total_entries = stats.storage_stats.sstables.total_entries;
131    let pb = ProgressBar::new(total_entries);
132    pb.set_style(
133        ProgressStyle::default_bar()
134            .template(
135                "Backing up [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} entries ({eta})",
136            )
137            .unwrap()
138            .progress_chars("=>-"),
139    );
140
141    // Create backup metadata
142    let backup_metadata = serde_json::json!({
143        "version": "1.0",
144        "timestamp": chrono::Utc::now().to_rfc3339(),
145        "cqlite_version": env!("CARGO_PKG_VERSION"),
146        "total_entries": total_entries,
147        "sstable_entries": stats.storage_stats.sstables.total_entries,
148        "sstable_count": stats.storage_stats.sstables.sstable_count
149    });
150
151    // Write backup header
152    writeln!(writer, "# CQLite Database Backup")?;
153    writeln!(writer, "# {}", backup_metadata.to_string())?;
154    writeln!(writer)?;
155
156    let mut processed_entries = 0;
157
158    // Export all data by querying system tables and user data
159    // Start with schema information
160    match database
161        .execute(
162            "SELECT keyspace_name, table_name FROM system.tables WHERE keyspace_name != 'system'",
163        )
164        .await
165    {
166        Ok(tables_result) => {
167            for table_row in &tables_result.rows {
168                if let (Some(keyspace), Some(table)) =
169                    (table_row.get("keyspace_name"), table_row.get("table_name"))
170                {
171                    // Export table schema first
172                    let create_table_query = format!("DESCRIBE TABLE {}.{}", keyspace, table);
173                    match database.execute(&create_table_query).await {
174                        Ok(schema_result) => {
175                            writeln!(writer, "-- Schema for {}.{}", keyspace, table)?;
176                            for schema_row in &schema_result.rows {
177                                if let Some(ddl) = schema_row.get("create_statement") {
178                                    writeln!(writer, "{}", ddl)?;
179                                }
180                            }
181                            writeln!(writer)?;
182                        }
183                        Err(_) => {
184                            // If DESCRIBE doesn't work, create a basic CREATE TABLE statement
185                            writeln!(
186                                writer,
187                                "-- Table: {}.{} (schema extraction failed)",
188                                keyspace, table
189                            )?;
190                            writeln!(
191                                writer,
192                                "-- CREATE TABLE {}.{} (...); -- Please recreate manually",
193                                keyspace, table
194                            )?;
195                            writeln!(writer)?;
196                        }
197                    }
198
199                    // Export table data
200                    let select_query = format!("SELECT * FROM {}.{}", keyspace, table);
201                    match database.execute(&select_query).await {
202                        Ok(data_result) => {
203                            writeln!(writer, "-- Data for {}.{}", keyspace, table)?;
204                            for data_row in &data_result.rows {
205                                // Convert row to INSERT statement
206                                let columns: Vec<String> = data_row.column_names();
207                                let values: Vec<String> = columns
208                                    .iter()
209                                    .map(|col| {
210                                        data_row
211                                            .get(col)
212                                            .map(|v| format!("'{}'", v)) // Simple quote escaping
213                                            .unwrap_or_else(|| "NULL".to_string())
214                                    })
215                                    .collect();
216
217                                let insert_stmt = format!(
218                                    "INSERT INTO {}.{} ({}) VALUES ({});",
219                                    keyspace,
220                                    table,
221                                    columns.join(", "),
222                                    values.join(", ")
223                                );
224                                writeln!(writer, "{}", insert_stmt)?;
225
226                                processed_entries += 1;
227                                pb.set_position(processed_entries);
228                            }
229                            writeln!(writer)?;
230                        }
231                        Err(e) => {
232                            writeln!(
233                                writer,
234                                "-- Error exporting data for {}.{}: {}",
235                                keyspace, table, e
236                            )?;
237                        }
238                    }
239                }
240            }
241        }
242        Err(_) => {
243            println!("Warning: Could not access system tables, creating basic backup");
244            return create_basic_backup(database, &mut writer, output).await;
245        }
246    }
247
248    pb.finish_with_message("Backup completed");
249    writer.flush()?;
250
251    let file_size = std::fs::metadata(output)?.len();
252    println!("✓ Database backup completed successfully");
253    println!("  Backup file: {}", output.display());
254    println!("  Entries exported: {}", processed_entries);
255    println!("  File size: {:.2} MB", file_size as f64 / 1_048_576.0);
256
257    Ok(())
258}
259
260#[cfg(feature = "state_machine")]
261async fn create_basic_backup(
262    _database: &Database,
263    writer: &mut std::io::BufWriter<std::fs::File>,
264    output: &std::path::Path,
265) -> Result<()> {
266    use std::io::Write;
267
268    // Basic backup without system table access
269    writeln!(writer, "# CQLite Database Backup (Basic Mode)")?;
270    writeln!(writer, "# Timestamp: {}", chrono::Utc::now().to_rfc3339())?;
271    writeln!(writer, "# Version: {}", env!("CARGO_PKG_VERSION"))?;
272    writeln!(writer)?;
273    writeln!(
274        writer,
275        "-- Note: This is a basic backup. Full schema and data export requires system table access."
276    )?;
277    writeln!(
278        writer,
279        "-- Please use manual CQL queries to restore your data."
280    )?;
281
282    writer.flush()?;
283
284    let file_size = std::fs::metadata(output)?.len();
285    println!("✓ Basic database backup completed");
286    println!("  Backup file: {}", output.display());
287    println!("  File size: {} bytes", file_size);
288    println!(
289        "  Note: This backup contains minimal information. Consider upgrading core library for full backup support."
290    );
291
292    Ok(())
293}
294
295#[cfg(feature = "state_machine")]
296async fn restore_database(database: &Database, input: &std::path::Path) -> Result<()> {
297    use indicatif::{ProgressBar, ProgressStyle};
298    use std::fs::File;
299    use std::io::{BufRead, BufReader};
300
301    println!("Restoring database from {}", input.display());
302
303    // Check if backup file exists
304    if !input.exists() {
305        return Err(anyhow::anyhow!(
306            "Backup file not found: {}",
307            input.display()
308        ));
309    }
310
311    // Open backup file
312    let backup_file =
313        File::open(input).map_err(|e| anyhow::anyhow!("Failed to open backup file: {}", e))?;
314    let reader = BufReader::new(backup_file);
315
316    // Count total lines for progress
317    let total_lines = reader.lines().count() as u64;
318
319    // Reopen file for actual processing
320    let backup_file = File::open(input)?;
321    let reader = BufReader::new(backup_file);
322
323    // Create progress bar
324    let pb = ProgressBar::new(total_lines);
325    pb.set_style(
326        ProgressStyle::default_bar()
327            .template(
328                "Restoring [{elapsed_precise}] [{bar:40.green/blue}] {pos}/{len} lines ({eta})",
329            )
330            .unwrap()
331            .progress_chars("=>-"),
332    );
333
334    let mut executed_statements = 0;
335    let mut errors = 0;
336    let mut line_number = 0;
337    let mut current_statement = String::new();
338
339    // Parse backup metadata if present
340    let mut backup_version: Option<String> = None;
341    let mut backup_timestamp: Option<String> = None;
342
343    for line_result in reader.lines() {
344        line_number += 1;
345        pb.set_position(line_number);
346
347        let line = line_result
348            .map_err(|e| anyhow::anyhow!("Error reading line {}: {}", line_number, e))?;
349
350        let trimmed_line = line.trim();
351
352        // Skip empty lines and comments, but extract metadata
353        if trimmed_line.is_empty() {
354            continue;
355        }
356
357        if trimmed_line.starts_with('#') {
358            // Extract metadata from comments
359            if trimmed_line.contains("\"version\":") {
360                // Try to parse JSON metadata
361                if let Some(start) = trimmed_line.find('{') {
362                    let json_str = &trimmed_line[start..];
363                    if let Ok(metadata) = serde_json::from_str::<serde_json::Value>(json_str) {
364                        backup_version = metadata["version"].as_str().map(|s| s.to_string());
365                        backup_timestamp = metadata["timestamp"].as_str().map(|s| s.to_string());
366                    }
367                }
368            }
369            continue;
370        }
371
372        if trimmed_line.starts_with("--") {
373            continue;
374        }
375
376        // Handle multi-line statements
377        current_statement.push_str(&line);
378        current_statement.push(' ');
379
380        // Check if statement is complete (ends with semicolon)
381        if trimmed_line.ends_with(';') {
382            let statement = current_statement.trim().trim_end_matches(';');
383
384            if !statement.is_empty() {
385                // Execute the statement
386                match database.execute(statement).await {
387                    Ok(_result) => {
388                        executed_statements += 1;
389                        if line_number % 100 == 0 {
390                            pb.set_message(format!("Executed {} statements", executed_statements));
391                        }
392
393                        // Log successful operations for important statements
394                        if statement.to_uppercase().starts_with("CREATE") {
395                            println!(
396                                "✓ Executed: {}",
397                                statement.chars().take(50).collect::<String>() + "..."
398                            );
399                        }
400                    }
401                    Err(e) => {
402                        errors += 1;
403                        eprintln!(
404                            "❌ Error executing statement at line {}: {}",
405                            line_number, e
406                        );
407                        eprintln!(
408                            "   Statement: {}",
409                            statement.chars().take(100).collect::<String>() + "..."
410                        );
411
412                        // Continue with restore unless it's a critical error
413                        if statement.to_uppercase().contains("CREATE KEYSPACE")
414                            || statement.to_uppercase().contains("CREATE TABLE")
415                        {
416                            println!(
417                                "   ⚠️  Schema error - continuing but data integrity may be affected"
418                            );
419                        }
420                    }
421                }
422            }
423
424            current_statement.clear();
425        }
426    }
427
428    // Handle any remaining incomplete statement
429    if !current_statement.trim().is_empty() {
430        let statement = current_statement.trim();
431        println!(
432            "⚠️  Warning: Incomplete statement at end of file: {}",
433            statement.chars().take(50).collect::<String>() + "..."
434        );
435    }
436
437    pb.finish_with_message("Restore completed");
438
439    // Show restore summary
440    println!("\n📊 Restore Summary:");
441    if let Some(version) = backup_version {
442        println!("  Backup version: {}", version);
443    }
444    if let Some(timestamp) = backup_timestamp {
445        println!("  Backup created: {}", timestamp);
446    }
447    println!("  Total lines processed: {}", line_number);
448    println!("  Statements executed: {}", executed_statements);
449
450    if errors > 0 {
451        println!("  ❌ Errors encountered: {}", errors);
452        println!("  ⚠️  Database restore completed with errors. Please verify data integrity.");
453
454        // Return error if too many failures
455        if errors > executed_statements / 2 {
456            return Err(anyhow::anyhow!(
457                "Restore failed: too many errors ({} errors out of {} statements)",
458                errors,
459                executed_statements
460            ));
461        }
462    } else {
463        println!("  ✅ Database restore completed successfully!");
464    }
465
466    Ok(())
467}