1use std::io::Write;
2
3use byteorder::{BigEndian, ByteOrder};
4use colored::Colorize;
5use serde::Serialize;
6
7use crate::cli::wprintln;
8use crate::innodb::constants::*;
9use crate::innodb::page::FilHeader;
10use crate::IdbError;
11
12pub struct InfoOptions {
14 pub ibdata: bool,
16 pub lsn_check: bool,
18 pub datadir: Option<String>,
20 pub database: Option<String>,
22 pub table: Option<String>,
24 pub host: Option<String>,
26 pub port: Option<u16>,
28 pub user: Option<String>,
30 pub password: Option<String>,
32 pub defaults_file: Option<String>,
34 pub json: bool,
36 pub page_size: Option<u32>,
38}
39
40#[derive(Serialize)]
41struct IbdataInfoJson {
42 ibdata_file: String,
43 page_checksum: u32,
44 page_number: u32,
45 page_type: u16,
46 lsn: u64,
47 flush_lsn: u64,
48 space_id: u32,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 redo_checkpoint_1_lsn: Option<u64>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 redo_checkpoint_2_lsn: Option<u64>,
53}
54
55#[derive(Serialize)]
56struct LsnCheckJson {
57 ibdata_lsn: u64,
58 redo_checkpoint_lsn: u64,
59 in_sync: bool,
60}
61
62pub fn execute(opts: &InfoOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
85 if opts.ibdata || opts.lsn_check {
86 let datadir = opts.datadir.as_deref().unwrap_or("/var/lib/mysql");
87 let datadir_path = std::path::Path::new(datadir);
88
89 if !datadir_path.is_dir() {
90 return Err(IdbError::Argument(format!(
91 "Data directory does not exist: {}",
92 datadir
93 )));
94 }
95
96 if opts.ibdata {
97 return execute_ibdata(opts, datadir_path, writer);
98 }
99 if opts.lsn_check {
100 return execute_lsn_check(opts, datadir_path, writer);
101 }
102 }
103
104 #[cfg(feature = "mysql")]
105 {
106 if opts.database.is_some() || opts.table.is_some() {
107 return execute_table_info(opts, writer);
108 }
109 }
110
111 #[cfg(not(feature = "mysql"))]
112 {
113 if opts.database.is_some() || opts.table.is_some() {
114 return Err(IdbError::Argument(
115 "MySQL support not compiled. Rebuild with: cargo build --features mysql"
116 .to_string(),
117 ));
118 }
119 }
120
121 wprintln!(writer, "Usage:")?;
123 wprintln!(
124 writer,
125 " idb info --ibdata -d <datadir> Read ibdata1 page 0 header"
126 )?;
127 wprintln!(
128 writer,
129 " idb info --lsn-check -d <datadir> Compare ibdata1 and redo log LSNs"
130 )?;
131 wprintln!(writer, " idb info -D <database> -t <table> Show table/index info (requires --features mysql)")?;
132 Ok(())
133}
134
135fn execute_ibdata(
136 opts: &InfoOptions,
137 datadir: &std::path::Path,
138 writer: &mut dyn Write,
139) -> Result<(), IdbError> {
140 let ibdata_path = datadir.join("ibdata1");
141 if !ibdata_path.exists() {
142 return Err(IdbError::Io(format!(
143 "ibdata1 not found in {}",
144 datadir.display()
145 )));
146 }
147
148 let page0 = read_file_bytes(&ibdata_path, 0, SIZE_PAGE_DEFAULT as usize)?;
150 let header = FilHeader::parse(&page0)
151 .ok_or_else(|| IdbError::Parse("Cannot parse ibdata1 page 0 FIL header".to_string()))?;
152
153 let (cp1_lsn, cp2_lsn) = read_redo_checkpoint_lsns(datadir);
155
156 if opts.json {
157 let info = IbdataInfoJson {
158 ibdata_file: ibdata_path.display().to_string(),
159 page_checksum: header.checksum,
160 page_number: header.page_number,
161 page_type: header.page_type.as_u16(),
162 lsn: header.lsn,
163 flush_lsn: header.flush_lsn,
164 space_id: header.space_id,
165 redo_checkpoint_1_lsn: cp1_lsn,
166 redo_checkpoint_2_lsn: cp2_lsn,
167 };
168 let json = serde_json::to_string_pretty(&info)
169 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
170 wprintln!(writer, "{}", json)?;
171 return Ok(());
172 }
173
174 wprintln!(writer, "{}", "ibdata1 Page 0 Header".bold())?;
175 wprintln!(writer, " File: {}", ibdata_path.display())?;
176 wprintln!(writer, " Checksum: {}", header.checksum)?;
177 wprintln!(writer, " Page No: {}", header.page_number)?;
178 wprintln!(
179 writer,
180 " Page Type: {} ({})",
181 header.page_type.as_u16(),
182 header.page_type.name()
183 )?;
184 wprintln!(writer, " LSN: {}", header.lsn)?;
185 wprintln!(writer, " Flush LSN: {}", header.flush_lsn)?;
186 wprintln!(writer, " Space ID: {}", header.space_id)?;
187 wprintln!(writer)?;
188
189 if let Some(lsn) = cp1_lsn {
190 wprintln!(writer, "Redo Log Checkpoint 1 LSN: {}", lsn)?;
191 }
192 if let Some(lsn) = cp2_lsn {
193 wprintln!(writer, "Redo Log Checkpoint 2 LSN: {}", lsn)?;
194 }
195
196 Ok(())
197}
198
199fn execute_lsn_check(
200 opts: &InfoOptions,
201 datadir: &std::path::Path,
202 writer: &mut dyn Write,
203) -> Result<(), IdbError> {
204 let ibdata_path = datadir.join("ibdata1");
205 if !ibdata_path.exists() {
206 return Err(IdbError::Io(format!(
207 "ibdata1 not found in {}",
208 datadir.display()
209 )));
210 }
211
212 let page0 = read_file_bytes(&ibdata_path, 0, SIZE_PAGE_DEFAULT as usize)?;
214 let ibdata_lsn = BigEndian::read_u64(&page0[FIL_PAGE_LSN..]);
215
216 let (cp1_lsn, _cp2_lsn) = read_redo_checkpoint_lsns(datadir);
218
219 let redo_lsn = cp1_lsn.unwrap_or(0);
220 let in_sync = ibdata_lsn == redo_lsn;
221
222 if opts.json {
223 let check = LsnCheckJson {
224 ibdata_lsn,
225 redo_checkpoint_lsn: redo_lsn,
226 in_sync,
227 };
228 let json = serde_json::to_string_pretty(&check)
229 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
230 wprintln!(writer, "{}", json)?;
231 return Ok(());
232 }
233
234 wprintln!(writer, "{}", "LSN Sync Check".bold())?;
235 wprintln!(writer, " ibdata1 LSN: {}", ibdata_lsn)?;
236 wprintln!(writer, " Redo checkpoint LSN: {}", redo_lsn)?;
237
238 if in_sync {
239 wprintln!(writer, " Status: {}", "IN SYNC".green())?;
240 } else {
241 wprintln!(writer, " Status: {}", "OUT OF SYNC".red())?;
242 wprintln!(
243 writer,
244 " Difference: {} bytes",
245 ibdata_lsn.abs_diff(redo_lsn)
246 )?;
247 }
248
249 Ok(())
250}
251
252fn read_redo_checkpoint_lsns(datadir: &std::path::Path) -> (Option<u64>, Option<u64>) {
257 const CP1_OFFSET: u64 = 512 + 8;
260 const CP2_OFFSET: u64 = 1536 + 8;
261
262 let redo_dir = datadir.join("#innodb_redo");
264 if redo_dir.is_dir() {
265 if let Ok(entries) = std::fs::read_dir(&redo_dir) {
267 let mut redo_files: Vec<_> = entries
268 .filter_map(|e| e.ok())
269 .filter(|e| e.file_name().to_string_lossy().starts_with("#ib_redo"))
270 .collect();
271 redo_files.sort_by_key(|e| e.file_name());
272 if let Some(first) = redo_files.first() {
273 let path = first.path();
274 let cp1 = read_u64_at(&path, CP1_OFFSET);
275 let cp2 = read_u64_at(&path, CP2_OFFSET);
276 return (cp1, cp2);
277 }
278 }
279 }
280
281 let logfile0 = datadir.join("ib_logfile0");
283 if logfile0.exists() {
284 let cp1 = read_u64_at(&logfile0, CP1_OFFSET);
285 let cp2 = read_u64_at(&logfile0, CP2_OFFSET);
286 return (cp1, cp2);
287 }
288
289 (None, None)
290}
291
292fn read_file_bytes(
293 path: &std::path::Path,
294 offset: u64,
295 length: usize,
296) -> Result<Vec<u8>, IdbError> {
297 use std::io::{Read, Seek, SeekFrom};
298
299 let mut file = std::fs::File::open(path)
300 .map_err(|e| IdbError::Io(format!("Cannot open {}: {}", path.display(), e)))?;
301
302 file.seek(SeekFrom::Start(offset))
303 .map_err(|e| IdbError::Io(format!("Cannot seek in {}: {}", path.display(), e)))?;
304
305 let mut buf = vec![0u8; length];
306 file.read_exact(&mut buf)
307 .map_err(|e| IdbError::Io(format!("Cannot read from {}: {}", path.display(), e)))?;
308
309 Ok(buf)
310}
311
312fn read_u64_at(path: &std::path::Path, offset: u64) -> Option<u64> {
313 let bytes = read_file_bytes(path, offset, 8).ok()?;
314 Some(BigEndian::read_u64(&bytes))
315}
316
317#[cfg(feature = "mysql")]
319fn execute_table_info(opts: &InfoOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
320 use mysql_async::prelude::*;
321
322 let database = opts
323 .database
324 .as_deref()
325 .ok_or_else(|| IdbError::Argument("Database name required (-D <database>)".to_string()))?;
326 let table = opts
327 .table
328 .as_deref()
329 .ok_or_else(|| IdbError::Argument("Table name required (-t <table>)".to_string()))?;
330
331 let mut config = crate::util::mysql::MysqlConfig::default();
333
334 if let Some(ref df) = opts.defaults_file {
336 if let Some(parsed) = crate::util::mysql::parse_defaults_file(std::path::Path::new(df)) {
337 config = parsed;
338 }
339 } else if let Some(df) = crate::util::mysql::find_defaults_file() {
340 if let Some(parsed) = crate::util::mysql::parse_defaults_file(&df) {
341 config = parsed;
342 }
343 }
344
345 if let Some(ref h) = opts.host {
347 config.host = h.clone();
348 }
349 if let Some(p) = opts.port {
350 config.port = p;
351 }
352 if let Some(ref u) = opts.user {
353 config.user = u.clone();
354 }
355 if opts.password.is_some() {
356 config.password = opts.password.clone();
357 }
358 config.database = Some(database.to_string());
359
360 let rt = tokio::runtime::Builder::new_current_thread()
361 .enable_all()
362 .build()
363 .map_err(|e| IdbError::Io(format!("Cannot create async runtime: {}", e)))?;
364
365 rt.block_on(async {
366 let pool = mysql_async::Pool::new(config.to_opts());
367 let mut conn = pool
368 .get_conn()
369 .await
370 .map_err(|e| IdbError::Io(format!("MySQL connection failed: {}", e)))?;
371
372 let table_query = format!(
374 "SELECT SPACE, TABLE_ID FROM information_schema.innodb_tables WHERE NAME = '{}/{}'",
375 database, table
376 );
377 let table_rows: Vec<(u64, u64)> = conn
378 .query(&table_query)
379 .await
380 .unwrap_or_default();
381
382 if table_rows.is_empty() {
383 let sys_query = format!(
385 "SELECT SPACE, TABLE_ID FROM information_schema.innodb_sys_tables WHERE NAME = '{}/{}'",
386 database, table
387 );
388 let sys_rows: Vec<(u64, u64)> = conn
389 .query(&sys_query)
390 .await
391 .unwrap_or_default();
392
393 if sys_rows.is_empty() {
394 wprintln!(writer, "Table {}.{} not found in InnoDB system tables.", database, table)?;
395 pool.disconnect().await.ok();
396 return Ok(());
397 }
398
399 print_table_info(writer, database, table, &sys_rows)?;
400 } else {
401 print_table_info(writer, database, table, &table_rows)?;
402 }
403
404 let idx_query = format!(
406 "SELECT NAME, INDEX_ID, PAGE_NO FROM information_schema.innodb_indexes \
407 WHERE TABLE_ID = (SELECT TABLE_ID FROM information_schema.innodb_tables WHERE NAME = '{}/{}')",
408 database, table
409 );
410 let idx_rows: Vec<(String, u64, u64)> = conn
411 .query(&idx_query)
412 .await
413 .unwrap_or_default();
414
415 if !idx_rows.is_empty() {
416 wprintln!(writer)?;
417 wprintln!(writer, "{}", "Indexes:".bold())?;
418 for (name, index_id, root_page) in &idx_rows {
419 wprintln!(writer, " {} (index_id={}, root_page={})", name, index_id, root_page)?;
420 }
421 }
422
423 let status_rows: Vec<(String, String, String)> = conn
425 .query("SHOW ENGINE INNODB STATUS")
426 .await
427 .unwrap_or_default();
428
429 if let Some((_type, _name, status)) = status_rows.first() {
430 wprintln!(writer)?;
431 wprintln!(writer, "{}", "InnoDB Status:".bold())?;
432 for line in status.lines() {
433 if line.starts_with("Log sequence number") || line.starts_with("Log flushed up to") {
434 wprintln!(writer, " {}", line.trim())?;
435 }
436 if line.starts_with("Trx id counter") {
437 wprintln!(writer, " {}", line.trim())?;
438 }
439 }
440 }
441
442 pool.disconnect().await.ok();
443 Ok(())
444 })
445}
446
447#[cfg(feature = "mysql")]
448fn print_table_info(
449 writer: &mut dyn Write,
450 database: &str,
451 table: &str,
452 rows: &[(u64, u64)],
453) -> Result<(), IdbError> {
454 wprintln!(
455 writer,
456 "{}",
457 format!("Table: {}.{}", database, table).bold()
458 )?;
459 for (space_id, table_id) in rows {
460 wprintln!(writer, " Space ID: {}", space_id)?;
461 wprintln!(writer, " Table ID: {}", table_id)?;
462 }
463 Ok(())
464}