Skip to main content

idb/cli/
export.rs

1//! CLI implementation for the `inno export` subcommand.
2//!
3//! Extracts user records from clustered index leaf pages and outputs them
4//! as CSV, JSON, or raw hex. Uses SDI metadata for typed field decoding
5//! when available.
6
7use std::io::Write;
8
9use crate::cli::wprintln;
10use crate::innodb::export::{csv_escape, decode_page_records, extract_column_layout};
11use crate::innodb::field_decode::{ColumnStorageInfo, FieldValue};
12use crate::innodb::index::IndexHeader;
13use crate::innodb::page::FilHeader;
14use crate::innodb::page_types::PageType;
15use crate::innodb::record::walk_compact_records;
16use crate::IdbError;
17
18/// Output format for exported records.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum ExportFormat {
21    Csv,
22    Json,
23    Hex,
24}
25
26impl ExportFormat {
27    fn from_str(s: &str) -> Result<Self, IdbError> {
28        match s.to_lowercase().as_str() {
29            "csv" => Ok(ExportFormat::Csv),
30            "json" => Ok(ExportFormat::Json),
31            "hex" => Ok(ExportFormat::Hex),
32            _ => Err(IdbError::Argument(format!(
33                "Unknown format '{}'. Use csv, json, or hex.",
34                s
35            ))),
36        }
37    }
38}
39
40/// Options for the `inno export` subcommand.
41pub struct ExportOptions {
42    /// Path to the InnoDB tablespace file (.ibd).
43    pub file: String,
44    /// Export records from a specific page only.
45    pub page: Option<u64>,
46    /// Output format: csv, json, or hex.
47    pub format: String,
48    /// Include only delete-marked records.
49    pub where_delete_mark: bool,
50    /// Include system columns (DB_TRX_ID, DB_ROLL_PTR) in output.
51    pub system_columns: bool,
52    /// Show additional details.
53    pub verbose: bool,
54    /// Override the auto-detected page size.
55    pub page_size: Option<u32>,
56    /// Path to MySQL keyring file for decrypting encrypted tablespaces.
57    pub keyring: Option<String>,
58    /// Use memory-mapped I/O for file access.
59    pub mmap: bool,
60}
61
62/// Export records from a tablespace.
63pub fn execute(opts: &ExportOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
64    let format = ExportFormat::from_str(&opts.format)?;
65
66    let mut ts = crate::cli::open_tablespace(&opts.file, opts.page_size, opts.mmap)?;
67
68    if let Some(ref keyring_path) = opts.keyring {
69        crate::cli::setup_decryption(&mut ts, keyring_path)?;
70    }
71
72    let page_size = ts.page_size();
73
74    // Try SDI extraction for typed decoding
75    let column_layout = extract_column_layout(&mut ts);
76    let (columns, clustered_index_id) = match column_layout {
77        Some((cols, idx_id)) => (Some(cols), Some(idx_id)),
78        None => {
79            if format != ExportFormat::Hex {
80                eprintln!("Warning: No SDI metadata found. Falling back to hex output.");
81            }
82            (None, None)
83        }
84    };
85
86    let use_hex = columns.is_none() || format == ExportFormat::Hex;
87
88    // Determine which index_id to export (clustered/PRIMARY)
89    // If we don't have SDI, we'll export all leaf INDEX pages
90    let target_index_id = clustered_index_id;
91
92    // Collect pages to process
93    let mut pages_data: Vec<(u64, Vec<u8>)> = Vec::new();
94    ts.for_each_page(|page_num, data| {
95        if let Some(specific_page) = opts.page {
96            if page_num != specific_page {
97                return Ok(());
98            }
99        }
100        let fil = match FilHeader::parse(data) {
101            Some(h) => h,
102            None => return Ok(()),
103        };
104        if fil.page_type != PageType::Index {
105            return Ok(());
106        }
107        let idx = match IndexHeader::parse(data) {
108            Some(h) => h,
109            None => return Ok(()),
110        };
111        // Only leaf pages
112        if !idx.is_leaf() {
113            return Ok(());
114        }
115        // Filter by clustered index if known
116        if let Some(target_id) = target_index_id {
117            if idx.index_id != target_id {
118                return Ok(());
119            }
120        }
121        pages_data.push((page_num, data.to_vec()));
122        Ok(())
123    })?;
124
125    if use_hex {
126        output_hex(writer, &pages_data, opts)?;
127    } else {
128        let cols = columns.as_ref().unwrap();
129        match format {
130            ExportFormat::Csv => output_csv(writer, &pages_data, cols, opts, page_size)?,
131            ExportFormat::Json => output_json(writer, &pages_data, cols, opts, page_size)?,
132            ExportFormat::Hex => unreachable!(),
133        }
134    }
135
136    Ok(())
137}
138
139/// Output records as CSV.
140fn output_csv(
141    writer: &mut dyn Write,
142    pages: &[(u64, Vec<u8>)],
143    columns: &[ColumnStorageInfo],
144    opts: &ExportOptions,
145    page_size: u32,
146) -> Result<(), IdbError> {
147    // Header row
148    let headers: Vec<&str> = columns
149        .iter()
150        .filter(|c| opts.system_columns || !c.is_system_column)
151        .map(|c| c.name.as_str())
152        .collect();
153    wprintln!(writer, "{}", headers.join(","))?;
154
155    for (_, page_data) in pages {
156        let rows = decode_page_records(
157            page_data,
158            columns,
159            opts.where_delete_mark,
160            opts.system_columns,
161            page_size,
162        );
163        for row in &rows {
164            let values: Vec<String> = row.iter().map(|(_, v)| csv_escape(v)).collect();
165            wprintln!(writer, "{}", values.join(","))?;
166        }
167    }
168
169    Ok(())
170}
171
172/// Output records as JSON (array of objects).
173fn output_json(
174    writer: &mut dyn Write,
175    pages: &[(u64, Vec<u8>)],
176    columns: &[ColumnStorageInfo],
177    opts: &ExportOptions,
178    page_size: u32,
179) -> Result<(), IdbError> {
180    let mut all_rows: Vec<serde_json::Map<String, serde_json::Value>> = Vec::new();
181
182    for (_, page_data) in pages {
183        let rows = decode_page_records(
184            page_data,
185            columns,
186            opts.where_delete_mark,
187            opts.system_columns,
188            page_size,
189        );
190        for row in rows {
191            let mut obj = serde_json::Map::new();
192            for (name, val) in row {
193                let json_val = match val {
194                    FieldValue::Null => serde_json::Value::Null,
195                    FieldValue::Int(n) => serde_json::Value::Number(n.into()),
196                    FieldValue::Uint(n) => serde_json::Value::Number(n.into()),
197                    FieldValue::Float(f) => serde_json::Number::from_f64(f as f64)
198                        .map(serde_json::Value::Number)
199                        .unwrap_or(serde_json::Value::Null),
200                    FieldValue::Double(d) => serde_json::Number::from_f64(d)
201                        .map(serde_json::Value::Number)
202                        .unwrap_or(serde_json::Value::Null),
203                    FieldValue::Str(s) => serde_json::Value::String(s),
204                    FieldValue::Hex(h) => serde_json::Value::String(h),
205                };
206                obj.insert(name, json_val);
207            }
208            all_rows.push(obj);
209        }
210    }
211
212    let json_output =
213        serde_json::to_string_pretty(&all_rows).map_err(|e| IdbError::Parse(e.to_string()))?;
214    wprintln!(writer, "{}", json_output)?;
215
216    Ok(())
217}
218
219/// Output records as hex (page/offset/heap_no/delete_mark/data).
220fn output_hex(
221    writer: &mut dyn Write,
222    pages: &[(u64, Vec<u8>)],
223    opts: &ExportOptions,
224) -> Result<(), IdbError> {
225    wprintln!(
226        writer,
227        "{:<8} {:<8} {:<8} {:<6} {}",
228        "PAGE",
229        "OFFSET",
230        "HEAP_NO",
231        "DEL",
232        "DATA (hex)"
233    )?;
234
235    for (page_num, page_data) in pages {
236        let records = walk_compact_records(page_data);
237        for rec in &records {
238            let delete_mark = rec.header.delete_mark();
239            if opts.where_delete_mark && !delete_mark {
240                continue;
241            }
242            if !opts.where_delete_mark && delete_mark {
243                continue;
244            }
245
246            let heap_no = rec.header.heap_no();
247            // Show up to 64 bytes of record data
248            let data_end = (rec.offset + 64).min(page_data.len());
249            let data_hex: String = page_data[rec.offset..data_end]
250                .iter()
251                .map(|b| format!("{:02x}", b))
252                .collect();
253
254            wprintln!(
255                writer,
256                "{:<8} {:<8} {:<8} {:<6} {}",
257                page_num,
258                rec.offset,
259                heap_no,
260                if delete_mark { "Y" } else { "N" },
261                data_hex
262            )?;
263        }
264    }
265
266    Ok(())
267}