vibesql_storage/persistence/
save.rs

1// ============================================================================
2// SQL Dump Generation (Save Operations)
3// ============================================================================
4//
5// Generates SQL statements that recreate the database state including:
6// - Schemas
7// - Tables with column definitions
8// - Indexes
9// - Data (INSERT statements)
10// - Roles and privileges
11//
12// IMPORTANT: This module uses atomic writes to prevent database corruption.
13// All writes go to a temporary file first, then are atomically renamed to the
14// target path. This ensures that even if the process crashes mid-write, the
15// original database file remains intact.
16
17use std::{
18    fs::{self, File},
19    io::{BufWriter, Write},
20    path::Path,
21};
22
23use crate::{Database, StorageError};
24
25impl Database {
26    /// Save database state as SQL dump (human-readable, portable)
27    ///
28    /// Generates SQL statements that recreate the database state including:
29    /// - Schemas
30    /// - Tables with column definitions
31    /// - Indexes
32    /// - Data (INSERT statements)
33    /// - Roles and privileges
34    ///
35    /// # Atomicity
36    ///
37    /// This function uses atomic writes to prevent corruption:
38    /// 1. Writes to a temporary file in the same directory
39    /// 2. Flushes and syncs the buffer to ensure all data is on disk
40    /// 3. Atomically renames the temp file to the target path
41    ///
42    /// This ensures the database file is never in a partial/corrupt state,
43    /// even if the process crashes or is interrupted mid-write.
44    ///
45    /// # Example
46    /// ```no_run
47    /// # use vibesql_storage::Database;
48    /// let db = Database::new();
49    /// db.save_sql_dump("database.sql").unwrap();
50    /// ```
51    pub fn save_sql_dump<P: AsRef<Path>>(&self, path: P) -> Result<(), StorageError> {
52        let path_ref = path.as_ref();
53
54        // Create temp file in the same directory to ensure atomic rename works
55        // (rename across filesystems would fail)
56        let temp_path = {
57            let parent = path_ref.parent().unwrap_or(Path::new("."));
58            let file_name = path_ref
59                .file_name()
60                .map(|s| s.to_string_lossy().to_string());
61            let temp_name = format!(
62                ".{}.tmp.{}",
63                file_name.unwrap_or_else(|| "database".to_string()),
64                std::process::id()
65            );
66            parent.join(temp_name)
67        };
68
69        // Write to temp file - clean up on error
70        let result = self.write_sql_dump_to_file(&temp_path);
71        if let Err(e) = &result {
72            // Clean up temp file on error
73            let _ = fs::remove_file(&temp_path);
74            return Err(e.clone());
75        }
76
77        // Atomically rename temp file to target path
78        fs::rename(&temp_path, path_ref).map_err(|e| {
79            // Clean up temp file on rename failure
80            let _ = fs::remove_file(&temp_path);
81            StorageError::NotImplemented(format!("Failed to rename temp file to target: {}", e))
82        })?;
83
84        Ok(())
85    }
86
87    /// Internal helper to write the SQL dump to a file
88    fn write_sql_dump_to_file(&self, path: &Path) -> Result<(), StorageError> {
89        let file = File::create(path).map_err(|e| {
90            StorageError::NotImplemented(format!("Failed to create temp file: {}", e))
91        })?;
92
93        let mut writer = BufWriter::new(file);
94
95        // Header
96        writeln!(writer, "-- VibeSQL Database Dump")
97            .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
98        writeln!(writer, "-- Generated: {}", chrono::Utc::now())
99            .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
100        writeln!(writer, "--")
101            .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
102        writeln!(writer)
103            .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
104
105        // Export schemas (except built-in schemas which always exist)
106        writeln!(writer, "-- Schemas")
107            .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
108        for schema_name in &self.catalog.list_schemas() {
109            // Skip built-in schemas - they are recreated automatically on load
110            // Skip default schema and all temp schemas (temp_1, temp_2, etc.)
111            if schema_name != vibesql_catalog::DEFAULT_SCHEMA
112                && !vibesql_catalog::Catalog::is_temp_schema(schema_name)
113            {
114                writeln!(writer, "CREATE SCHEMA {};", schema_name)
115                    .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
116            }
117        }
118        writeln!(writer)
119            .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
120
121        // Export roles
122        writeln!(writer, "-- Roles")
123            .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
124        for role_name in &self.catalog.list_roles() {
125            writeln!(writer, "CREATE ROLE {};", role_name)
126                .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
127        }
128        writeln!(writer)
129            .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
130
131        // Export tables and data
132        writeln!(writer, "-- Tables and Data")
133            .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
134
135        // Iterate through all schemas except temp schemas
136        // We must NOT use get_table() which follows shadowing rules - temp tables would
137        // incorrectly override main schema tables in the dump.
138        for schema_name in &self.catalog.list_schemas() {
139            // Skip all temp schemas (temp_1, temp_2, etc.) - they are session-scoped
140            if vibesql_catalog::Catalog::is_temp_schema(schema_name) {
141                continue;
142            }
143
144            // Get tables directly from this schema
145            let schema_tables = if let Some(schema) = self.catalog.get_schema(schema_name) {
146                schema.list_tables()
147            } else {
148                continue;
149            };
150
151            for table_name in &schema_tables {
152                // Use fully qualified name to bypass temp table shadowing
153                let qualified_name = format!("{}.{}", schema_name, table_name);
154                let Some(table) = self.tables.get(&qualified_name) else {
155                    continue;
156                };
157
158                // For default schema, use unqualified name in output for cleaner SQL
159                let output_name = if schema_name == vibesql_catalog::DEFAULT_SCHEMA {
160                    table_name.clone()
161                } else {
162                    qualified_name.clone()
163                };
164                // CREATE TABLE statement
165                let schema = &table.schema;
166                write!(writer, "CREATE TABLE {} (", &output_name)
167                    .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
168
169                for (i, col) in schema.columns.iter().enumerate() {
170                    if i > 0 {
171                        write!(writer, ", ").map_err(|e| {
172                            StorageError::NotImplemented(format!("Write error: {}", e))
173                        })?;
174                    }
175                    // Format column type, preserving INT vs INTEGER distinction for rowid alias behavior
176                    let type_str = format_column_type(&col.data_type, col.is_exact_integer_type);
177                    write!(writer, "{} {}", col.name, type_str)
178                        .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
179
180                    // Handle generated columns (AS expression syntax)
181                    if let Some(ref generated_expr) = col.generated_expr {
182                        use vibesql_ast::pretty_print::ToSql;
183                        write!(writer, " AS ({})", generated_expr.to_sql()).map_err(|e| {
184                            StorageError::NotImplemented(format!("Write error: {}", e))
185                        })?;
186                    } else {
187                        // Only non-generated columns can have DEFAULT, COLLATE, NOT NULL
188                        // Add DEFAULT clause if present
189                        if let Some(ref default_expr) = col.default_value {
190                            use vibesql_ast::pretty_print::ToSql;
191                            write!(writer, " DEFAULT {}", default_expr.to_sql()).map_err(|e| {
192                                StorageError::NotImplemented(format!("Write error: {}", e))
193                            })?;
194                        }
195                        // Add COLLATE clause if present
196                        if let Some(ref collation) = col.collation {
197                            write!(writer, " COLLATE {}", collation).map_err(|e| {
198                                StorageError::NotImplemented(format!("Write error: {}", e))
199                            })?;
200                        }
201                        if !col.nullable {
202                            write!(writer, " NOT NULL").map_err(|e| {
203                                StorageError::NotImplemented(format!("Write error: {}", e))
204                            })?;
205                        }
206                    }
207                }
208
209                // Add PRIMARY KEY constraint if present
210                if let Some(pk_cols) = &schema.primary_key {
211                    write!(writer, ", PRIMARY KEY ({})", pk_cols.join(", "))
212                        .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
213                }
214
215                // Add UNIQUE constraints
216                for unique_cols in &schema.unique_constraints {
217                    write!(writer, ", UNIQUE ({})", unique_cols.join(", "))
218                        .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
219                }
220
221                // Add CHECK constraints
222                // We always output CONSTRAINT <name> CHECK (<expr>) to preserve user-provided names.
223                // For unnamed constraints, the name equals the expression text, which will be
224                // re-derived when the table is reloaded.
225                for (constraint_name, check_expr) in &schema.check_constraints {
226                    use vibesql_ast::pretty_print::ToSql;
227                    let expr_text = check_expr.to_sql();
228                    // Only output CONSTRAINT keyword if the name is different from expression
229                    // (i.e., it's a user-provided name)
230                    if constraint_name != &expr_text {
231                        write!(writer, ", CONSTRAINT {} CHECK ({})", constraint_name, expr_text)
232                            .map_err(|e| {
233                                StorageError::NotImplemented(format!("Write error: {}", e))
234                            })?;
235                    } else {
236                        write!(writer, ", CHECK ({})", expr_text)
237                            .map_err(|e| {
238                                StorageError::NotImplemented(format!("Write error: {}", e))
239                            })?;
240                    }
241                }
242
243                // Close the column definitions
244                write!(writer, ")")
245                    .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
246
247                // Add WITHOUT ROWID clause for SQLite compatibility (Issue #4803)
248                if schema.without_rowid {
249                    write!(writer, " WITHOUT ROWID")
250                        .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
251                }
252
253                writeln!(writer, ";")
254                    .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
255
256                // INSERT statements for data (only live/non-deleted rows)
257                // Using scan_live() to skip rows marked as deleted in the deletion bitmap.
258                // Note: We must skip generated columns - they cannot be inserted directly.
259                if table.row_count() > 0 {
260                    writeln!(writer)
261                        .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
262
263                    // Find indices of non-generated columns
264                    let non_generated_indices: Vec<usize> = schema
265                        .columns
266                        .iter()
267                        .enumerate()
268                        .filter(|(_, col)| col.generated_expr.is_none())
269                        .map(|(i, _)| i)
270                        .collect();
271
272                    // Build column list for INSERT if we have generated columns
273                    let has_generated_columns = non_generated_indices.len() < schema.columns.len();
274                    let column_list: String = if has_generated_columns {
275                        let col_names: Vec<&str> = non_generated_indices
276                            .iter()
277                            .map(|&i| schema.columns[i].name.as_str())
278                            .collect();
279                        format!(" ({})", col_names.join(", "))
280                    } else {
281                        String::new()
282                    };
283
284                    for (_idx, row) in table.scan_live() {
285                        write!(writer, "INSERT INTO {}{} VALUES (", &output_name, column_list)
286                            .map_err(|e| {
287                                StorageError::NotImplemented(format!("Write error: {}", e))
288                            })?;
289
290                        // Only include values for non-generated columns
291                        let mut first = true;
292                        for &col_idx in &non_generated_indices {
293                            if !first {
294                                write!(writer, ", ").map_err(|e| {
295                                    StorageError::NotImplemented(format!("Write error: {}", e))
296                                })?;
297                            }
298                            first = false;
299                            write!(writer, "{}", sql_value_to_literal(&row.values[col_idx]))
300                                .map_err(|e| {
301                                    StorageError::NotImplemented(format!("Write error: {}", e))
302                                })?;
303                        }
304                        writeln!(writer, ");").map_err(|e| {
305                            StorageError::NotImplemented(format!("Write error: {}", e))
306                        })?;
307                    }
308                }
309
310                writeln!(writer)
311                    .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
312            }
313        }
314
315        // Export indexes (skip auto-generated indexes which are recreated by constraints)
316        writeln!(writer, "-- Indexes")
317            .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
318        for index_name in self.list_indexes() {
319            // Skip auto-generated indexes - these are automatically created by constraints:
320            // - "pk_<table_name>" indexes are created by PRIMARY KEY constraints
321            // - "sqlite_autoindex_<table>_<n>" indexes are created by PRIMARY KEY/UNIQUE constraints
322            //   (follows SQLite naming convention for implicit indexes)
323            let lower_name = index_name.to_lowercase();
324            if lower_name.starts_with("pk_") || lower_name.starts_with("sqlite_autoindex_") {
325                continue;
326            }
327            let metadata = self.get_index(&index_name).unwrap();
328            write!(writer, "CREATE")
329                .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
330            if metadata.unique {
331                write!(writer, " UNIQUE")
332                    .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
333            }
334            write!(writer, " INDEX {} ON {} (", index_name, metadata.table_name)
335                .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
336
337            for (i, col) in metadata.columns.iter().enumerate() {
338                if i > 0 {
339                    write!(writer, ", ")
340                        .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
341                }
342                // Handle both column references and expression indexes
343                use vibesql_ast::IndexColumn;
344                match col {
345                    IndexColumn::Column { column_name, .. } => {
346                        write!(writer, "{}", column_name)
347                            .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
348                    }
349                    IndexColumn::Expression { expr, .. } => {
350                        use vibesql_ast::pretty_print::ToSql;
351                        write!(writer, "{}", expr.to_sql())
352                            .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
353                    }
354                }
355                write!(writer, " {:?}", col.direction())
356                    .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
357            }
358
359            writeln!(writer, ");")
360                .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
361        }
362        writeln!(writer)
363            .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
364
365        // Export views
366        writeln!(writer, "-- Views")
367            .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
368        for view_name in self.catalog.list_views() {
369            if let Some(view_def) = self.catalog.get_view(&view_name) {
370                // Use stored SQL definition if available, otherwise create a minimal definition
371                let sql = view_def.sql_definition.as_ref().map_or_else(
372                    || {
373                        // Fallback: create a representation from the stored query
374                        // This is a minimal representation and may not be fully accurate
375                        format!("CREATE VIEW {} AS {:?}", view_def.name, view_def.query)
376                    },
377                    |s| s.clone(),
378                );
379                // Strip trailing semicolons before adding one
380                let sql = sql.trim_end_matches(';').trim();
381                writeln!(writer, "{};", sql)
382                    .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
383            }
384        }
385        writeln!(writer)
386            .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
387
388        writeln!(writer, "-- End of dump")
389            .map_err(|e| StorageError::NotImplemented(format!("Write error: {}", e)))?;
390
391        // Flush the buffer and sync to disk to ensure data durability
392        // This is critical for atomic writes - we need all data on disk before rename
393        writer
394            .flush()
395            .map_err(|e| StorageError::NotImplemented(format!("Failed to flush buffer: {}", e)))?;
396
397        // Get the underlying file to sync it to disk
398        let file = writer
399            .into_inner()
400            .map_err(|e| StorageError::NotImplemented(format!("Failed to get file: {}", e)))?;
401        file.sync_all()
402            .map_err(|e| StorageError::NotImplemented(format!("Failed to sync file: {}", e)))?;
403
404        Ok(())
405    }
406}
407
408/// Format a column type, preserving the INT vs INTEGER distinction for rowid alias behavior.
409/// In SQLite, only `INTEGER PRIMARY KEY` is a rowid alias, not `INT PRIMARY KEY`.
410fn format_column_type(data_type: &vibesql_types::DataType, is_exact_integer_type: bool) -> String {
411    use vibesql_types::DataType;
412
413    match data_type {
414        DataType::Integer => {
415            if is_exact_integer_type {
416                "INTEGER".to_string()
417            } else {
418                "INT".to_string()
419            }
420        }
421        _ => format_data_type(data_type),
422    }
423}
424
425pub(super) fn format_data_type(data_type: &vibesql_types::DataType) -> String {
426    use vibesql_types::DataType;
427
428    match data_type {
429        DataType::Integer => "INTEGER".to_string(),
430        DataType::Smallint => "SMALLINT".to_string(),
431        DataType::Bigint => "BIGINT".to_string(),
432        DataType::Unsigned => "BIGINT UNSIGNED".to_string(),
433        DataType::Float { precision } => format!("FLOAT({})", precision),
434        DataType::Real => "REAL".to_string(),
435        DataType::DoublePrecision => "DOUBLE PRECISION".to_string(),
436        DataType::Varchar { max_length } => {
437            if let Some(len) = max_length {
438                format!("VARCHAR({})", len)
439            } else {
440                "VARCHAR".to_string()
441            }
442        }
443        DataType::Character { length } => format!("CHAR({})", length),
444        DataType::Boolean => "BOOLEAN".to_string(),
445        DataType::Date => "DATE".to_string(),
446        DataType::Time { .. } => "TIME".to_string(),
447        DataType::Timestamp { with_timezone } => {
448            if *with_timezone {
449                "TIMESTAMP WITH TIME ZONE".to_string()
450            } else {
451                "TIMESTAMP".to_string()
452            }
453        }
454        DataType::Interval { start_field, end_field: _ } => {
455            // Simplified interval representation for now
456            format!("INTERVAL {:?}", start_field)
457        }
458        DataType::Numeric { precision, scale } => {
459            format!("NUMERIC({}, {})", precision, scale)
460        }
461        DataType::Decimal { precision, scale } => {
462            format!("DECIMAL({}, {})", precision, scale)
463        }
464        DataType::CharacterLargeObject => "CLOB".to_string(),
465        DataType::Name => "VARCHAR(128)".to_string(),
466        DataType::BinaryLargeObject => "BLOB".to_string(),
467        DataType::Bit { length } => {
468            if let Some(len) = length {
469                format!("BIT({})", len)
470            } else {
471                "BIT".to_string()
472            }
473        }
474        DataType::Vector { dimensions } => format!("VECTOR({})", dimensions),
475        DataType::UserDefined { type_name } => type_name.clone(),
476        DataType::Null => "NULL".to_string(),
477    }
478}
479
480/// Convert a SqlValue to its SQL literal representation
481pub(super) fn sql_value_to_literal(value: &vibesql_types::SqlValue) -> String {
482    use vibesql_types::SqlValue;
483
484    match value {
485        SqlValue::Null => "NULL".to_string(),
486        SqlValue::Integer(n) => n.to_string(),
487        SqlValue::Smallint(n) => n.to_string(),
488        SqlValue::Bigint(n) => n.to_string(),
489        SqlValue::Unsigned(n) => n.to_string(),
490        SqlValue::Numeric(f) => {
491            // Handle special float values that would be parsed as identifiers
492            if f.is_nan() {
493                "'NaN'".to_string()
494            } else if f.is_infinite() {
495                if f.is_sign_positive() {
496                    "'Infinity'".to_string()
497                } else {
498                    "'-Infinity'".to_string()
499                }
500            } else {
501                format_f64_for_sql(*f)
502            }
503        }
504        SqlValue::Float(f) => {
505            if f.is_nan() {
506                "'NaN'".to_string()
507            } else if f.is_infinite() {
508                if f.is_sign_positive() {
509                    "'Infinity'".to_string()
510                } else {
511                    "'-Infinity'".to_string()
512                }
513            } else {
514                format_f32_for_sql(*f)
515            }
516        }
517        SqlValue::Real(f) => {
518            // Real is now f64 (SQLite REAL is 8-byte IEEE float)
519            if f.is_nan() {
520                "'NaN'".to_string()
521            } else if f.is_infinite() {
522                if f.is_sign_positive() {
523                    "'Infinity'".to_string()
524                } else {
525                    "'-Infinity'".to_string()
526                }
527            } else {
528                format_f64_for_sql(*f)
529            }
530        }
531        SqlValue::Double(f) => {
532            if f.is_nan() {
533                "'NaN'".to_string()
534            } else if f.is_infinite() {
535                if f.is_sign_positive() {
536                    "'Infinity'".to_string()
537                } else {
538                    "'-Infinity'".to_string()
539                }
540            } else {
541                format_f64_for_sql(*f)
542            }
543        }
544        SqlValue::Character(s) | SqlValue::Varchar(s) => format!("'{}'", s.replace('\'', "''")),
545        SqlValue::Boolean(b) => if *b { "TRUE" } else { "FALSE" }.to_string(),
546        SqlValue::Date(d) => format!("DATE '{}'", d),
547        SqlValue::Time(t) => format!("TIME '{}'", t),
548        SqlValue::Timestamp(ts) => format!("TIMESTAMP '{}'", ts),
549        SqlValue::Interval(i) => format!("INTERVAL '{}'", i),
550        SqlValue::Vector(v) => {
551            // Format vector as space-separated values: [v1, v2, ...]
552            let formatted: Vec<String> = v.iter().map(|f| f.to_string()).collect();
553            format!("'{}'", formatted.join(","))
554        }
555        SqlValue::Blob(b) => {
556            // Format blob as hex literal
557            let hex: String = b.iter().map(|byte| format!("{:02X}", byte)).collect();
558            format!("x'{}'", hex)
559        }
560    }
561}
562
563/// Format f64 for SQL literal with proper type preservation.
564/// Ensures whole numbers like 5200000.0 include the ".0" suffix
565/// so they're parsed as REAL, not INTEGER, when the dump is reloaded.
566fn format_f64_for_sql(n: f64) -> String {
567    // Use ryu for shortest round-trip representation
568    let mut buffer = ryu::Buffer::new();
569    let s = buffer.format(n);
570    s.to_string()
571}
572
573/// Format f32 for SQL literal with proper type preservation.
574/// Ensures whole numbers like 5200000.0 include the ".0" suffix
575/// so they're parsed as REAL, not INTEGER, when the dump is reloaded.
576fn format_f32_for_sql(n: f32) -> String {
577    // Use ryu for shortest round-trip representation at f32 precision
578    let mut buffer = ryu::Buffer::new();
579    let s = buffer.format(n);
580    s.to_string()
581}