Skip to main content

idb/cli/
schema.rs

1//! CLI implementation for the `inno schema` subcommand.
2//!
3//! Extracts table schema from SDI metadata in MySQL 8.0+ tablespaces and
4//! reconstructs human-readable `CREATE TABLE` DDL. For pre-8.0 tablespaces,
5//! provides a best-effort inference from INDEX page structure.
6
7use std::io::Write;
8
9use crate::cli::wprintln;
10use crate::innodb::schema::{self, InferredSchema, TableSchema};
11use crate::innodb::sdi;
12use crate::IdbError;
13
14/// Options for the `inno schema` subcommand.
15pub struct SchemaOptions {
16    /// Path to the InnoDB tablespace file (.ibd).
17    pub file: String,
18    /// Show additional structured details above the DDL.
19    pub verbose: bool,
20    /// Output in JSON format.
21    pub json: bool,
22    /// Override the auto-detected page size.
23    pub page_size: Option<u32>,
24    /// Path to MySQL keyring file for decrypting encrypted tablespaces.
25    pub keyring: Option<String>,
26    /// Use memory-mapped I/O for file access.
27    pub mmap: bool,
28}
29
30/// Extract schema and reconstruct DDL from tablespace metadata.
31///
32/// For MySQL 8.0+ tablespaces with SDI, extracts the data dictionary JSON,
33/// parses it into typed structs, and generates `CREATE TABLE` DDL. For
34/// pre-8.0 tablespaces without SDI, scans INDEX pages to infer basic
35/// index structure.
36pub fn execute(opts: &SchemaOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
37    let mut ts = crate::cli::open_tablespace(&opts.file, opts.page_size, opts.mmap)?;
38
39    if let Some(ref keyring_path) = opts.keyring {
40        crate::cli::setup_decryption(&mut ts, keyring_path)?;
41    }
42
43    // MariaDB does not use SDI
44    if ts.vendor_info().vendor == crate::innodb::vendor::InnoDbVendor::MariaDB {
45        let inferred = schema::infer_schema_from_pages(&mut ts)?;
46        if opts.json {
47            wprintln!(
48                writer,
49                "{}",
50                serde_json::to_string_pretty(&inferred)
51                    .map_err(|e| IdbError::Parse(e.to_string()))?
52            )?;
53        } else {
54            print_inferred_text(writer, &inferred)?;
55        }
56        return Ok(());
57    }
58
59    // Try SDI extraction
60    let sdi_pages = sdi::find_sdi_pages(&mut ts)?;
61
62    if sdi_pages.is_empty() {
63        // Pre-8.0 fallback
64        let inferred = schema::infer_schema_from_pages(&mut ts)?;
65        if opts.json {
66            wprintln!(
67                writer,
68                "{}",
69                serde_json::to_string_pretty(&inferred)
70                    .map_err(|e| IdbError::Parse(e.to_string()))?
71            )?;
72        } else {
73            print_inferred_text(writer, &inferred)?;
74        }
75        return Ok(());
76    }
77
78    // Extract SDI records
79    let records = sdi::extract_sdi_from_pages(&mut ts, &sdi_pages)?;
80
81    // Filter for Table records (sdi_type == 1)
82    let table_records: Vec<_> = records.iter().filter(|r| r.sdi_type == 1).collect();
83
84    if table_records.is_empty() {
85        wprintln!(writer, "No table SDI records found in {}.", opts.file)?;
86        return Ok(());
87    }
88
89    for rec in &table_records {
90        let table_schema = schema::extract_schema_from_sdi(&rec.data)?;
91
92        if opts.json {
93            wprintln!(
94                writer,
95                "{}",
96                serde_json::to_string_pretty(&table_schema)
97                    .map_err(|e| IdbError::Parse(e.to_string()))?
98            )?;
99        } else if opts.verbose {
100            print_verbose_text(writer, &table_schema)?;
101        } else {
102            print_default_text(writer, &table_schema)?;
103        }
104    }
105
106    Ok(())
107}
108
109/// Print default text output: comment header + DDL.
110fn print_default_text(writer: &mut dyn Write, schema: &TableSchema) -> Result<(), IdbError> {
111    // Header comment
112    if let Some(ref db) = schema.schema_name {
113        wprintln!(writer, "-- Table: `{}`.`{}`", db, schema.table_name)?;
114    } else {
115        wprintln!(writer, "-- Table: `{}`", schema.table_name)?;
116    }
117
118    if let Some(ref ver) = schema.mysql_version {
119        wprintln!(writer, "-- Source: SDI (MySQL {})", ver)?;
120    } else {
121        wprintln!(writer, "-- Source: SDI")?;
122    }
123
124    wprintln!(writer)?;
125    wprintln!(writer, "{}", schema.ddl)?;
126
127    Ok(())
128}
129
130/// Print verbose text output: structured breakdown + DDL.
131fn print_verbose_text(writer: &mut dyn Write, schema: &TableSchema) -> Result<(), IdbError> {
132    if let Some(ref db) = schema.schema_name {
133        wprintln!(writer, "Schema:  {}", db)?;
134    }
135    wprintln!(writer, "Table:   {}", schema.table_name)?;
136    wprintln!(writer, "Engine:  {}", schema.engine)?;
137    if let Some(ref fmt) = schema.row_format {
138        wprintln!(writer, "Format:  {}", fmt)?;
139    }
140    if let Some(ref ver) = schema.mysql_version {
141        wprintln!(writer, "Source:  SDI (MySQL {})", ver)?;
142    }
143    if let Some(ref coll) = schema.collation {
144        wprintln!(writer, "Collation: {}", coll)?;
145    }
146    if let Some(ref cs) = schema.charset {
147        wprintln!(writer, "Charset: {}", cs)?;
148    }
149    if let Some(ref comment) = schema.comment {
150        wprintln!(writer, "Comment: {}", comment)?;
151    }
152
153    // Columns
154    wprintln!(writer)?;
155    wprintln!(writer, "Columns ({}):", schema.columns.len())?;
156    for (i, col) in schema.columns.iter().enumerate() {
157        let mut parts = vec![format!(
158            "  {}. {:<16} {:<20}",
159            i + 1,
160            col.name,
161            col.column_type
162        )];
163        if !col.is_nullable {
164            parts.push("NOT NULL".to_string());
165        }
166        if col.is_auto_increment {
167            parts.push("AUTO_INCREMENT".to_string());
168        }
169        if let Some(ref expr) = col.generation_expression {
170            let kind = if col.is_virtual == Some(true) {
171                "VIRTUAL"
172            } else {
173                "STORED"
174            };
175            parts.push(format!("AS ({}) {}", expr, kind));
176        }
177        if col.is_invisible {
178            parts.push("INVISIBLE".to_string());
179        }
180        wprintln!(writer, "{}", parts.join("  "))?;
181    }
182
183    // Indexes
184    if !schema.indexes.is_empty() {
185        wprintln!(writer)?;
186        wprintln!(writer, "Indexes ({}):", schema.indexes.len())?;
187        for idx in &schema.indexes {
188            let cols: Vec<String> = idx
189                .columns
190                .iter()
191                .map(|c| {
192                    let mut s = c.name.clone();
193                    if let Some(len) = c.prefix_length {
194                        s.push_str(&format!("({})", len));
195                    }
196                    if let Some(ref ord) = c.order {
197                        s.push(' ');
198                        s.push_str(ord);
199                    }
200                    s
201                })
202                .collect();
203            if idx.index_type == "PRIMARY KEY" {
204                wprintln!(writer, "  {} ({})", idx.index_type, cols.join(", "))?;
205            } else {
206                wprintln!(
207                    writer,
208                    "  {} {} ({})",
209                    idx.index_type,
210                    idx.name,
211                    cols.join(", ")
212                )?;
213            }
214        }
215    }
216
217    // Foreign keys
218    if !schema.foreign_keys.is_empty() {
219        wprintln!(writer)?;
220        wprintln!(writer, "Foreign Keys ({}):", schema.foreign_keys.len())?;
221        for fk in &schema.foreign_keys {
222            wprintln!(
223                writer,
224                "  {} ({}) -> {} ({})",
225                fk.name,
226                fk.columns.join(", "),
227                fk.referenced_table,
228                fk.referenced_columns.join(", ")
229            )?;
230        }
231    }
232
233    // DDL
234    wprintln!(writer)?;
235    wprintln!(writer, "DDL:")?;
236    wprintln!(writer, "{}", schema.ddl)?;
237
238    Ok(())
239}
240
241/// Print inferred schema (pre-8.0 / no SDI).
242fn print_inferred_text(writer: &mut dyn Write, inferred: &InferredSchema) -> Result<(), IdbError> {
243    wprintln!(writer, "-- Source: {}", inferred.source)?;
244    wprintln!(
245        writer,
246        "-- Note: Column names and types cannot be determined without SDI."
247    )?;
248    wprintln!(writer)?;
249    wprintln!(writer, "Record format: {}", inferred.record_format)?;
250    wprintln!(writer, "Indexes detected: {}", inferred.indexes.len())?;
251    for idx in &inferred.indexes {
252        let levels = if idx.max_level > 0 {
253            format!(", {} non-leaf level(s)", idx.max_level)
254        } else {
255            String::new()
256        };
257        wprintln!(
258            writer,
259            "  Index ID {}: {} leaf page(s){}",
260            idx.index_id,
261            idx.leaf_pages,
262            levels
263        )?;
264    }
265
266    Ok(())
267}