Skip to main content

spg_engine/
dump.rs

1//! r1054 (7.38 S3.1, design D27) — the engine dumps itself to SQL.
2//!
3//! The contract is SELF-consistency, not pg_dump emission fidelity
4//! (that campaign is registered separately): `dump → restore into a
5//! fresh engine → dump` must be a FIXED POINT, and the restored data
6//! must checksum-match the original. Everything here leans on surfaces
7//! the engine already answers for — `pg_indexes.indexdef` for index
8//! DDL, its own SELECT for data (visibility included), the constfold
9//! literal renderer for values — so the dump cannot drift from what
10//! the engine itself believes.
11
12use crate::{Engine, EngineError, QueryResult};
13use alloc::format;
14use alloc::string::String;
15use alloc::vec::Vec;
16
17impl Engine {
18    /// Serialize every user table (schema, constraints, data), index
19    /// and view to SQL the engine itself re-executes.
20    ///
21    /// # Errors
22    /// Storage or introspection failures; a value the literal renderer
23    /// cannot express round-trip-safely.
24    pub fn dump_sql(&mut self) -> Result<String, EngineError> {
25        let mut out = String::from("-- spg dump (self-consistent form)\n");
26        let mut tables = self.active_catalog().table_names();
27        tables.retain(|t| !t.starts_with("__spg_"));
28        tables.sort();
29
30        // ── schema ──────────────────────────────────────────────────
31        for name in &tables {
32            let Some(t) = self.active_catalog().get(name) else {
33                continue;
34            };
35            let schema = t.schema().clone();
36            let mut lines: Vec<String> = Vec::new();
37            for c in &schema.columns {
38                let mut line = format!("  {} {}", quote_ident(&c.name), ddl_type(c.ty));
39                if let Some(e) = &c.user_enum_type {
40                    line = format!("  {} {}", quote_ident(&c.name), quote_ident(e));
41                }
42                if !c.nullable {
43                    line.push_str(" NOT NULL");
44                }
45                if let Some(d) = &c.default_text {
46                    line.push_str(&format!(" DEFAULT {d}"));
47                }
48                lines.push(line);
49            }
50            for uc in &schema.uniqueness_constraints {
51                let cols: Vec<String> = uc
52                    .columns
53                    .iter()
54                    .filter_map(|&p| schema.columns.get(p))
55                    .map(|c| quote_ident(&c.name))
56                    .collect();
57                let kind = if uc.is_primary_key {
58                    "PRIMARY KEY"
59                } else if uc.nulls_not_distinct {
60                    "UNIQUE NULLS NOT DISTINCT"
61                } else {
62                    "UNIQUE"
63                };
64                lines.push(format!("  {kind} ({})", cols.join(", ")));
65            }
66            out.push_str(&format!(
67                "CREATE TABLE {} (\n{}\n);\n",
68                quote_ident(name),
69                lines.join(",\n")
70            ));
71        }
72
73        // ── data — through the engine's own SELECT, so visibility and
74        // rendering are the engine's, not this module's ─────────────
75        for name in &tables {
76            let rows = match self.execute(&format!("SELECT * FROM {}", quote_ident(name)))? {
77                QueryResult::Rows { rows, .. } => rows,
78                _ => continue,
79            };
80            for chunk in rows.chunks(100) {
81                let tuples: Vec<String> = chunk
82                    .iter()
83                    .map(|r| {
84                        let vals: Vec<String> = r
85                            .values
86                            .iter()
87                            .map(|v| format!("{}", crate::clock::value_to_literal(v.clone())))
88                            .collect();
89                        format!("({})", vals.join(", "))
90                    })
91                    .collect();
92                out.push_str(&format!(
93                    "INSERT INTO {} VALUES {};\n",
94                    quote_ident(name),
95                    tuples.join(", ")
96                ));
97            }
98        }
99
100        // ── secondary indexes, via the engine's own pg_indexes ──────
101        if let QueryResult::Rows { rows, .. } = self.execute(
102            "SELECT indexdef FROM pg_indexes WHERE schemaname = 'public' ORDER BY indexname",
103        )? {
104            for r in rows {
105                let def = crate::eval::value_to_text(&r.values[0]);
106                // Constraint-backing indexes are recreated by the
107                // table's own PRIMARY KEY / UNIQUE clauses.
108                if def.contains("_pkey") || def.contains("_key\"") || def.contains("_key ") {
109                    continue;
110                }
111                out.push_str(&format!("{def};\n"));
112            }
113        }
114
115        // ── views, from their stored deterministic bodies ───────────
116        let mut views: Vec<(String, Vec<String>, String)> = Vec::new();
117        for (name, v) in self.active_catalog().views_all() {
118            if name.starts_with("__spg_") {
119                continue;
120            }
121            views.push((v.name.clone(), v.columns.clone(), v.body.clone()));
122        }
123        views.sort();
124        for (name, columns, body) in views {
125            let cols = if columns.is_empty() {
126                String::new()
127            } else {
128                format!(
129                    " ({})",
130                    columns
131                        .iter()
132                        .map(|c| quote_ident(c))
133                        .collect::<Vec<_>>()
134                        .join(", ")
135                )
136            };
137            out.push_str(&format!(
138                "CREATE VIEW {}{cols} AS {body};\n",
139                quote_ident(&name)
140            ));
141        }
142        Ok(out)
143    }
144}
145
146/// A column type as re-parseable DDL. `pg_data_type_text` is the
147/// canonical name (information_schema's own renderer); the length /
148/// precision parameters it reports separately are re-attached here,
149/// because a dump that silently widens `varchar(9)` to `varchar`
150/// changes what the restored table accepts. The bare `DataType`
151/// Display was tried first and printed `NUMERIC(0)` for an
152/// unconstrained NUMERIC — not SQL.
153fn ddl_type(ty: spg_storage::DataType) -> String {
154    use spg_storage::DataType as T;
155    match ty {
156        T::Varchar(n) if n > 0 => format!("varchar({n})"),
157        T::Char(n) if n > 0 => format!("char({n})"),
158        T::Numeric { precision, scale } if precision > 0 => {
159            format!("numeric({precision},{scale})")
160        }
161        // information_schema reports every array as the single word
162        // ARRAY (element in udt_name) — right for that catalog, not
163        // SQL. Spell the element.
164        T::TextArray => "text[]".into(),
165        T::IntArray => "integer[]".into(),
166        T::BigIntArray => "bigint[]".into(),
167        T::SmallIntArray => "smallint[]".into(),
168        T::FloatArray => "double precision[]".into(),
169        T::BoolArray => "boolean[]".into(),
170        T::NumericArray => "numeric[]".into(),
171        T::DateArray => "date[]".into(),
172        T::TimestampArray => "timestamp without time zone[]".into(),
173        T::TimestamptzArray => "timestamp with time zone[]".into(),
174        T::UuidArray => "uuid[]".into(),
175        T::JsonArray => "json[]".into(),
176        T::JsonbArray => "jsonb[]".into(),
177        T::BytesArray => "bytea[]".into(),
178        T::VarcharArray => "varchar[]".into(),
179        T::CharArray => "char[]".into(),
180        T::IntervalArray => "interval[]".into(),
181        T::OidArray => "oid[]".into(),
182        T::MoneyArray => "money[]".into(),
183        other => crate::system_catalog::pg_data_type_text(other),
184    }
185}
186
187/// Double-quote when the ident isn't a lowercase bare word.
188fn quote_ident(s: &str) -> String {
189    if !s.is_empty()
190        && s.chars()
191            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
192        && !s.starts_with(|c: char| c.is_ascii_digit())
193    {
194        s.into()
195    } else {
196        format!("\"{}\"", s.replace('"', "\"\""))
197    }
198}