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