Skip to main content

idb/cli/
tsid.rs

1use std::collections::BTreeMap;
2use std::io::Write;
3use std::path::Path;
4
5use byteorder::{BigEndian, ByteOrder};
6use serde::Serialize;
7
8use crate::cli::wprintln;
9use crate::innodb::constants::FIL_PAGE_DATA;
10use crate::innodb::tablespace::Tablespace;
11use crate::util::fs::find_tablespace_files;
12use crate::IdbError;
13
14/// Options for the `inno tsid` subcommand.
15pub struct TsidOptions {
16    /// MySQL data directory path to scan.
17    pub datadir: String,
18    /// List all tablespace IDs found in the data directory.
19    pub list: bool,
20    /// Find the tablespace file with this specific space ID.
21    pub tablespace_id: Option<u32>,
22    /// Emit output as JSON.
23    pub json: bool,
24    /// Override the auto-detected page size.
25    pub page_size: Option<u32>,
26}
27
28#[derive(Serialize)]
29struct TsidResultJson {
30    datadir: String,
31    tablespaces: Vec<TsidEntryJson>,
32}
33
34#[derive(Serialize)]
35struct TsidEntryJson {
36    file: String,
37    space_id: u32,
38}
39
40/// List or look up tablespace IDs from files in a MySQL data directory.
41///
42/// Recursively discovers all `.ibd` (tablespace) and `.ibu` (undo tablespace)
43/// files under the data directory, opens page 0 of each, and reads the space ID
44/// from the FSP header at offset `FIL_PAGE_DATA` (byte 38). The space ID
45/// uniquely identifies each tablespace within a MySQL instance and appears in
46/// error logs, `INFORMATION_SCHEMA.INNODB_TABLESPACES`, and FIL headers of
47/// every page.
48///
49/// Two modes are available:
50///
51/// - **List mode** (`-l`): Prints every discovered file alongside its space ID,
52///   sorted by file path. Useful for building a map of the data directory.
53/// - **Lookup mode** (`-t <id>`): Filters results to only the file(s) with the
54///   given space ID. Useful for resolving a space ID from an error message back
55///   to a physical `.ibd` file on disk.
56///
57/// If neither `-l` nor `-t` is specified, both modes behave as list mode.
58pub fn execute(opts: &TsidOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
59    let datadir = Path::new(&opts.datadir);
60    if !datadir.is_dir() {
61        return Err(IdbError::Argument(format!(
62            "Data directory does not exist: {}",
63            opts.datadir
64        )));
65    }
66
67    let ibd_files = find_tablespace_files(datadir, &["ibd", "ibu"])?;
68
69    if ibd_files.is_empty() {
70        if opts.json {
71            let result = TsidResultJson {
72                datadir: opts.datadir.clone(),
73                tablespaces: Vec::new(),
74            };
75            let json = serde_json::to_string_pretty(&result)
76                .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
77            wprintln!(writer, "{}", json)?;
78        } else {
79            wprintln!(writer, "No .ibd/.ibu files found in {}", opts.datadir)?;
80        }
81        return Ok(());
82    }
83
84    // Collect tablespace IDs
85    let mut results: BTreeMap<String, u32> = BTreeMap::new();
86
87    for ibd_path in &ibd_files {
88        let mut ts = match match opts.page_size {
89            Some(ps) => Tablespace::open_with_page_size(ibd_path, ps),
90            None => Tablespace::open(ibd_path),
91        } {
92            Ok(t) => t,
93            Err(_) => continue,
94        };
95
96        let space_id = match ts.fsp_header() {
97            Some(fsp) => fsp.space_id,
98            None => {
99                // Try reading space_id directly from FSP header position
100                match ts.read_page(0) {
101                    Ok(page0) => {
102                        if page0.len() >= FIL_PAGE_DATA + 4 {
103                            BigEndian::read_u32(&page0[FIL_PAGE_DATA..])
104                        } else {
105                            continue;
106                        }
107                    }
108                    Err(_) => continue,
109                }
110            }
111        };
112
113        let display_path = ibd_path
114            .strip_prefix(datadir)
115            .unwrap_or(ibd_path)
116            .to_string_lossy()
117            .to_string();
118
119        // Filter by tablespace ID if specified
120        if let Some(target_id) = opts.tablespace_id {
121            if space_id != target_id {
122                continue;
123            }
124        }
125
126        results.insert(display_path, space_id);
127    }
128
129    if opts.json {
130        let tablespaces: Vec<TsidEntryJson> = results
131            .iter()
132            .map(|(path, &space_id)| TsidEntryJson {
133                file: path.clone(),
134                space_id,
135            })
136            .collect();
137
138        let result = TsidResultJson {
139            datadir: opts.datadir.clone(),
140            tablespaces,
141        };
142
143        let json = serde_json::to_string_pretty(&result)
144            .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
145        wprintln!(writer, "{}", json)?;
146    } else {
147        // Print results
148        for (path, space_id) in &results {
149            wprintln!(writer, "{} - Space ID: {}", path, space_id)?;
150        }
151
152        if results.is_empty() {
153            if let Some(target_id) = opts.tablespace_id {
154                wprintln!(writer, "Tablespace ID {} not found.", target_id)?;
155            }
156        }
157    }
158
159    Ok(())
160}
161