Skip to main content

jetdb/
table.rs

1//! Table definition (TDEF page) parsing: columns, indexes, and data page lists.
2
3use std::collections::HashSet;
4
5use crate::encoding;
6use crate::file::{FileError, PageReader};
7use crate::format::{ColumnType, JetFormat, PageType, MAX_INDEX_COLUMNS};
8use crate::map;
9
10// ---------------------------------------------------------------------------
11// Public types
12// ---------------------------------------------------------------------------
13
14/// Sort order for an index column.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum IndexColumnOrder {
17    Ascending,
18    Descending,
19}
20
21/// A single column within an index definition.
22#[derive(Debug, Clone)]
23pub struct IndexColumn {
24    /// Column number (corresponds to `ColumnDef.col_num`).
25    pub col_num: u16,
26    /// Sort order.
27    pub order: IndexColumnOrder,
28}
29
30/// Foreign key reference information (for `index_type == 2`).
31#[derive(Debug, Clone)]
32pub struct ForeignKeyReference {
33    /// FK index type (0x00 or 0x01).
34    pub fk_index_type: u8,
35    /// FK index number.
36    pub fk_index_number: u32,
37    /// FK table page number.
38    pub fk_table_page: u32,
39    /// Update action flag.
40    pub update_action: u8,
41    /// Delete action flag.
42    pub delete_action: u8,
43}
44
45/// A single index definition parsed from a TDEF page.
46#[derive(Debug, Clone)]
47pub struct IndexDef {
48    /// Index name.
49    pub name: String,
50    /// Logical index number.
51    pub index_num: u16,
52    /// Index type: 0x01 = normal/PK, 0x02 = FK reference.
53    pub index_type: u8,
54    /// Columns in this index (empty for FK type=2).
55    pub columns: Vec<IndexColumn>,
56    /// Index flags (UNIQUE, IGNORE_NULLS, REQUIRED).
57    pub flags: u8,
58    /// B-tree root page number (0 for FK type=2 indexes).
59    pub first_data_page: u32,
60    /// Foreign key info (only for type=2 indexes).
61    pub foreign_key: Option<ForeignKeyReference>,
62}
63
64/// A single column definition parsed from a TDEF page.
65#[derive(Debug, Clone)]
66pub struct ColumnDef {
67    pub name: String,
68    pub col_type: ColumnType,
69    pub col_num: u16,
70    pub var_col_num: u16,
71    pub fixed_offset: u16,
72    pub col_size: u16,
73    pub flags: u8,
74    pub is_fixed: bool,
75    /// Scale for Numeric columns (number of decimal places).
76    pub scale: u8,
77    /// Precision for Numeric columns.
78    pub precision: u8,
79}
80
81/// A parsed table definition.
82#[derive(Debug, Clone)]
83pub struct TableDef {
84    pub name: String,
85    pub num_rows: u32,
86    pub num_cols: u16,
87    pub num_var_cols: u16,
88    pub columns: Vec<ColumnDef>,
89    pub indexes: Vec<IndexDef>,
90    pub data_pages: Vec<u32>,
91}
92
93/// Return `true` if the column has the REPLICATION flag set.
94pub fn is_replication_column(col: &ColumnDef) -> bool {
95    (col.flags & crate::format::column_flags::REPLICATION) != 0
96}
97
98// ---------------------------------------------------------------------------
99// Private types
100// ---------------------------------------------------------------------------
101
102/// Physical index entry: (columns, flags, first_data_page).
103type PhysicalIndexEntry = (Vec<IndexColumn>, u8, u32);
104
105/// Logical index entry parsed from TDEF section [6].
106struct LogicalIndex {
107    index_num: u16,
108    index_col_entry: u32,
109    fk_index_type: u8,
110    fk_index_number: u32,
111    fk_table_page: u32,
112    update_action: u8,
113    delete_action: u8,
114    index_type: u8,
115}
116
117// ---------------------------------------------------------------------------
118// read_table_def
119// ---------------------------------------------------------------------------
120
121/// Read and parse a table definition (TDEF) from the database.
122///
123/// `name` is the table name (stored in the returned `TableDef`).
124/// `tdef_page` is the first TDEF page number.
125pub fn read_table_def(
126    reader: &mut PageReader,
127    name: &str,
128    tdef_page: u32,
129) -> Result<TableDef, FileError> {
130    let is_jet3 = reader.header().version.is_jet3();
131
132    // 3a. Build TDEF buffer (multi-page support)
133    let tdef_buf = build_tdef_buffer(reader, tdef_page)?;
134
135    let format = reader.format();
136    let cursor = &mut TdefCursor::new(&tdef_buf, 0);
137
138    // 3b. Header fields (positional reads)
139    let num_rows = cursor.u32_le_at(format.tdef_row_count_pos)?;
140    let num_var_cols = cursor.u16_le_at(format.tdef_var_col_count_pos)?;
141    let num_cols = cursor.u16_le_at(format.tdef_column_count_pos)?;
142    let num_idxs = cursor.u32_le_at(format.tdef_index_count_pos)?;
143    let num_real_idxs = cursor.u32_le_at(format.tdef_real_index_count_pos)?;
144
145    // 3c. Data pages via owned-pages usage map
146    let pg_row = cursor.u32_le_at(format.tdef_owned_pages_pos)?;
147    let data_pages = if pg_row != 0 {
148        let map_data = reader.read_pg_row(pg_row)?;
149        map::collect_page_numbers(reader, &map_data)?
150    } else {
151        Vec::new()
152    };
153
154    // 3d. Column entries
155    let col_entry_start =
156        format.tdef_index_entries_pos + (num_real_idxs as usize) * format.tdef_index_entry_span;
157    cursor.set_position(col_entry_start);
158    let mut columns = parse_column_entries(
159        cursor,
160        format.tdef_column_entry_span,
161        num_cols as usize,
162        is_jet3,
163        format,
164    )?;
165
166    // 3e. Column names (cursor is already at correct position)
167    let col_names = read_names(cursor, num_cols as usize, is_jet3)?;
168    for (col, col_name) in columns.iter_mut().zip(col_names) {
169        col.name = col_name;
170    }
171
172    // 3f. Index column definitions
173    let mut idx_col_defs = parse_index_column_defs(cursor, num_real_idxs, format)?;
174
175    // 3g. Logical index definitions
176    let logical_indexes = parse_logical_indexes(cursor, num_idxs, format)?;
177
178    // Adjust idx_col_defs length based on actual non-FK count in section [6].
179    let non_fk_count = logical_indexes
180        .iter()
181        .filter(|li| li.index_type != crate::format::index_type::FOREIGN_KEY)
182        .count();
183    if non_fk_count != idx_col_defs.len() {
184        idx_col_defs.truncate(non_fk_count);
185    }
186
187    // 3h. Index names
188    let idx_names = read_names(cursor, num_idxs as usize, is_jet3)?;
189
190    // 3i. Build index defs
191    let indexes = build_index_defs(&logical_indexes, &idx_col_defs, idx_names);
192
193    // 3j. Sort columns by col_num
194    columns.sort_by_key(|c| c.col_num);
195
196    Ok(TableDef {
197        name: name.to_string(),
198        num_rows,
199        num_cols,
200        num_var_cols,
201        columns,
202        indexes,
203        data_pages,
204    })
205}
206
207// ---------------------------------------------------------------------------
208// Private helpers
209// ---------------------------------------------------------------------------
210
211/// Build a contiguous TDEF buffer by following the next-page chain.
212fn build_tdef_buffer(reader: &mut PageReader, tdef_page: u32) -> Result<Vec<u8>, FileError> {
213    let first_page = reader.read_page_copy(tdef_page)?;
214
215    // Validate page type
216    if first_page.is_empty() || first_page[0] != PageType::TableDefinition as u8 {
217        return Err(FileError::InvalidTableDef {
218            reason: "first page is not a TableDefinition page",
219        });
220    }
221
222    // Next-page pointer at offset 4 of the first page
223    let mut next = u32::from_le_bytes([first_page[4], first_page[5], first_page[6], first_page[7]]);
224    let mut buf = first_page;
225
226    // Follow continuation pages (skip their 8-byte header)
227    let mut visited = HashSet::new();
228    while next != 0 {
229        if !visited.insert(next) {
230            return Err(FileError::InvalidTableDef {
231                reason: "circular page reference in TDEF chain",
232            });
233        }
234        let cont_page = reader.read_page_copy(next)?;
235        if cont_page.len() > 8 {
236            buf.extend_from_slice(&cont_page[8..]);
237        }
238        next = u32::from_le_bytes([cont_page[4], cont_page[5], cont_page[6], cont_page[7]]);
239    }
240
241    Ok(buf)
242}
243
244// ---------------------------------------------------------------------------
245// TdefCursor — byte I/O abstraction for TDEF buffer parsing
246// ---------------------------------------------------------------------------
247
248struct TdefCursor<'a> {
249    buf: &'a [u8],
250    pos: usize,
251}
252
253impl<'a> TdefCursor<'a> {
254    fn new(buf: &'a [u8], pos: usize) -> Self {
255        Self { buf, pos }
256    }
257
258    fn position(&self) -> usize {
259        self.pos
260    }
261
262    fn set_position(&mut self, pos: usize) {
263        self.pos = pos;
264    }
265
266    // --- Sequential reads (advance cursor position) ---
267
268    fn read_u8(&mut self) -> Result<u8, FileError> {
269        if self.pos >= self.buf.len() {
270            return Err(FileError::InvalidTableDef {
271                reason: "unexpected end of TDEF buffer",
272            });
273        }
274        let v = self.buf[self.pos];
275        self.pos += 1;
276        Ok(v)
277    }
278
279    fn read_u16_le(&mut self) -> Result<u16, FileError> {
280        if self.pos + 2 > self.buf.len() {
281            return Err(FileError::InvalidTableDef {
282                reason: "unexpected end of TDEF buffer",
283            });
284        }
285        let v = u16::from_le_bytes([self.buf[self.pos], self.buf[self.pos + 1]]);
286        self.pos += 2;
287        Ok(v)
288    }
289
290    fn read_u32_le(&mut self) -> Result<u32, FileError> {
291        if self.pos + 4 > self.buf.len() {
292            return Err(FileError::InvalidTableDef {
293                reason: "unexpected end of TDEF buffer",
294            });
295        }
296        let v = u32::from_le_bytes([
297            self.buf[self.pos],
298            self.buf[self.pos + 1],
299            self.buf[self.pos + 2],
300            self.buf[self.pos + 3],
301        ]);
302        self.pos += 4;
303        Ok(v)
304    }
305
306    fn read_bytes(&mut self, n: usize) -> Result<&'a [u8], FileError> {
307        if self.pos + n > self.buf.len() {
308            return Err(FileError::InvalidTableDef {
309                reason: "unexpected end of TDEF buffer",
310            });
311        }
312        let slice = &self.buf[self.pos..self.pos + n];
313        self.pos += n;
314        Ok(slice)
315    }
316
317    fn skip(&mut self, n: usize) -> Result<(), FileError> {
318        if self.pos + n > self.buf.len() {
319            return Err(FileError::InvalidTableDef {
320                reason: "unexpected end of TDEF buffer",
321            });
322        }
323        self.pos += n;
324        Ok(())
325    }
326
327    // --- Positional reads (cursor position unchanged) ---
328
329    fn u8_at(&self, pos: usize) -> Result<u8, FileError> {
330        if pos >= self.buf.len() {
331            return Err(FileError::InvalidTableDef {
332                reason: "unexpected end of TDEF buffer",
333            });
334        }
335        Ok(self.buf[pos])
336    }
337
338    fn u16_le_at(&self, pos: usize) -> Result<u16, FileError> {
339        if pos + 2 > self.buf.len() {
340            return Err(FileError::InvalidTableDef {
341                reason: "unexpected end of TDEF buffer",
342            });
343        }
344        Ok(u16::from_le_bytes([self.buf[pos], self.buf[pos + 1]]))
345    }
346
347    fn u32_le_at(&self, pos: usize) -> Result<u32, FileError> {
348        if pos + 4 > self.buf.len() {
349            return Err(FileError::InvalidTableDef {
350                reason: "unexpected end of TDEF buffer",
351            });
352        }
353        Ok(u32::from_le_bytes([
354            self.buf[pos],
355            self.buf[pos + 1],
356            self.buf[pos + 2],
357            self.buf[pos + 3],
358        ]))
359    }
360}
361
362/// Read a sequence of names from the TDEF buffer via cursor.
363///
364/// Jet3 uses `[len: u8][Latin-1 bytes]`, Jet4+ uses `[len: u16 LE][UTF-16LE bytes]`.
365fn read_names(
366    cursor: &mut TdefCursor,
367    count: usize,
368    is_jet3: bool,
369) -> Result<Vec<String>, FileError> {
370    let mut names = Vec::with_capacity(count);
371    for _ in 0..count {
372        if is_jet3 {
373            let name_len = cursor.read_u8()? as usize;
374            let bytes = cursor.read_bytes(name_len)?;
375            names.push(encoding::decode_latin1(bytes));
376        } else {
377            let name_len = cursor.read_u16_le()? as usize;
378            let bytes = cursor.read_bytes(name_len)?;
379            names.push(encoding::decode_utf16le(bytes).map_err(|_| {
380                FileError::InvalidTableDef {
381                    reason: "invalid UTF-16LE name",
382                }
383            })?);
384        }
385    }
386    Ok(names)
387}
388
389/// Parse column definition entries from the TDEF buffer via cursor.
390fn parse_column_entries(
391    cursor: &mut TdefCursor,
392    span: usize,
393    count: usize,
394    is_jet3: bool,
395    format: &JetFormat,
396) -> Result<Vec<ColumnDef>, FileError> {
397    let mut columns = Vec::with_capacity(count);
398    for _ in 0..count {
399        let entry_start = cursor.position();
400
401        let col_type = ColumnType::try_from(cursor.u8_at(entry_start)?)?;
402
403        let (col_num, var_col_num) = if is_jet3 {
404            (
405                cursor.u8_at(entry_start + format.coldef_number_pos)? as u16,
406                cursor.u16_le_at(entry_start + format.coldef_var_col_index_pos)?,
407            )
408        } else {
409            (
410                cursor.u16_le_at(entry_start + format.coldef_number_pos)?,
411                cursor.u16_le_at(entry_start + format.coldef_var_col_index_pos)?,
412            )
413        };
414
415        let flags = cursor.u8_at(entry_start + format.coldef_flags_pos)?;
416        let is_fixed = (flags & crate::format::column_flags::FIXED) != 0;
417        let fixed_offset = cursor.u16_le_at(entry_start + format.coldef_fixed_data_pos)?;
418        let col_size = cursor.u16_le_at(entry_start + format.coldef_length_pos)?;
419        let scale = cursor.u8_at(entry_start + format.coldef_scale_pos)?;
420        let precision = cursor.u8_at(entry_start + format.coldef_precision_pos)?;
421
422        columns.push(ColumnDef {
423            name: String::new(), // filled by read_names
424            col_type,
425            col_num,
426            var_col_num,
427            fixed_offset,
428            col_size,
429            flags,
430            is_fixed,
431            scale,
432            precision,
433        });
434
435        cursor.set_position(entry_start + span);
436    }
437    Ok(columns)
438}
439
440/// Parse index column definitions from TDEF section [5].
441fn parse_index_column_defs(
442    cursor: &mut TdefCursor,
443    count: u32,
444    format: &JetFormat,
445) -> Result<Vec<PhysicalIndexEntry>, FileError> {
446    let mut idx_col_defs = Vec::with_capacity(count as usize);
447
448    for _ in 0..count {
449        cursor.skip(format.idx_col_skip_before)?;
450
451        let mut idx_columns = Vec::new();
452        for _ in 0..MAX_INDEX_COLUMNS {
453            let col_id = cursor.read_u16_le()?;
454            let order_flag = cursor.read_u8()?;
455
456            if col_id != 0xFFFF {
457                let order = if order_flag == 0x01 {
458                    IndexColumnOrder::Ascending
459                } else {
460                    IndexColumnOrder::Descending
461                };
462                idx_columns.push(IndexColumn {
463                    col_num: col_id,
464                    order,
465                });
466            }
467        }
468
469        cursor.skip(4)?; // usage map reference
470        let first_pg = cursor.read_u32_le()?;
471        cursor.skip(format.idx_col_skip_before_flags)?;
472        let idx_flags = cursor.read_u8()?;
473        cursor.skip(format.idx_col_skip_after_flags)?;
474
475        idx_col_defs.push((idx_columns, idx_flags, first_pg));
476    }
477
478    Ok(idx_col_defs)
479}
480
481/// Parse logical index definitions from TDEF section [6].
482fn parse_logical_indexes(
483    cursor: &mut TdefCursor,
484    count: u32,
485    format: &JetFormat,
486) -> Result<Vec<LogicalIndex>, FileError> {
487    let mut logical_indexes = Vec::with_capacity(count as usize);
488
489    for _ in 0..count {
490        let entry_start = cursor.position();
491
492        cursor.skip(format.idx_info_skip_before)?;
493        let index_num = cursor.read_u16_le()?;
494        cursor.skip(2)?; // padding
495        let index_col_entry = cursor.read_u32_le()?;
496        let fk_index_type = cursor.read_u8()?;
497        let fk_index_number = cursor.read_u32_le()?;
498        let fk_table_page = cursor.read_u32_le()?;
499        let update_action = cursor.read_u8()?;
500        let delete_action = cursor.read_u8()?;
501        let index_type = cursor.u8_at(entry_start + format.idx_info_type_offset)?;
502
503        logical_indexes.push(LogicalIndex {
504            index_num,
505            index_col_entry,
506            fk_index_type,
507            fk_index_number,
508            fk_table_page,
509            update_action,
510            delete_action,
511            index_type,
512        });
513
514        cursor.set_position(entry_start + format.idx_info_block_size);
515    }
516
517    Ok(logical_indexes)
518}
519
520/// Combine logical indexes, column definitions, and names into `IndexDef` entries.
521fn build_index_defs(
522    logical_indexes: &[LogicalIndex],
523    idx_col_defs: &[PhysicalIndexEntry],
524    idx_names: Vec<String>,
525) -> Vec<IndexDef> {
526    let mut indexes = Vec::with_capacity(logical_indexes.len());
527    for (i, logical) in logical_indexes.iter().enumerate() {
528        let name = idx_names.get(i).cloned().unwrap_or_default();
529
530        if logical.index_type == crate::format::index_type::FOREIGN_KEY {
531            indexes.push(IndexDef {
532                name,
533                index_num: logical.index_num,
534                index_type: logical.index_type,
535                columns: Vec::new(),
536                flags: 0,
537                first_data_page: 0,
538                foreign_key: Some(ForeignKeyReference {
539                    fk_index_type: logical.fk_index_type,
540                    fk_index_number: logical.fk_index_number,
541                    fk_table_page: logical.fk_table_page,
542                    update_action: logical.update_action,
543                    delete_action: logical.delete_action,
544                }),
545            });
546        } else {
547            let col_entry_idx = logical.index_col_entry as usize;
548            let (cols, flags, first_pg) = if col_entry_idx < idx_col_defs.len() {
549                idx_col_defs[col_entry_idx].clone()
550            } else {
551                log::warn!(
552                    "index '{}': column entry index {} out of range (max {})",
553                    name,
554                    col_entry_idx,
555                    idx_col_defs.len()
556                );
557                (Vec::new(), 0, 0)
558            };
559            indexes.push(IndexDef {
560                name,
561                index_num: logical.index_num,
562                index_type: logical.index_type,
563                columns: cols,
564                flags,
565                first_data_page: first_pg,
566                foreign_key: None,
567            });
568        }
569    }
570    indexes
571}
572
573// ---------------------------------------------------------------------------
574// Tests
575// ---------------------------------------------------------------------------
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580    use crate::format::ColumnType;
581    use crate::format::{column_flags, CATALOG_PAGE};
582
583    fn test_data_path(relative: &str) -> Option<std::path::PathBuf> {
584        let manifest_dir = env!("CARGO_MANIFEST_DIR");
585        let path = std::path::PathBuf::from(manifest_dir)
586            .join("../../testdata")
587            .join(relative);
588        if path.exists() {
589            Some(path)
590        } else {
591            None
592        }
593    }
594
595    macro_rules! skip_if_missing {
596        ($path:expr) => {
597            match test_data_path($path) {
598                Some(p) => p,
599                None => {
600                    eprintln!("SKIP: test data not found: {}", $path);
601                    return;
602                }
603            }
604        };
605    }
606
607    fn assert_msysobjects(tdef: &TableDef) {
608        assert!(
609            tdef.num_cols > 0,
610            "MSysObjects should have at least one column"
611        );
612
613        let col_names: Vec<&str> = tdef.columns.iter().map(|c| c.name.as_str()).collect();
614        assert!(
615            col_names.contains(&"Id"),
616            "MSysObjects should have 'Id' column, found: {col_names:?}"
617        );
618        assert!(
619            col_names.contains(&"Name"),
620            "MSysObjects should have 'Name' column, found: {col_names:?}"
621        );
622        assert!(
623            col_names.contains(&"Type"),
624            "MSysObjects should have 'Type' column, found: {col_names:?}"
625        );
626
627        assert!(
628            !tdef.data_pages.is_empty(),
629            "MSysObjects should have at least one data page"
630        );
631    }
632
633    #[test]
634    fn jet3_msysobjects() {
635        let path = skip_if_missing!("V1997/testV1997.mdb");
636        let mut reader = PageReader::open(&path).unwrap();
637        let tdef = read_table_def(&mut reader, "MSysObjects", CATALOG_PAGE).unwrap();
638        assert_msysobjects(&tdef);
639    }
640
641    #[test]
642    fn jet4_msysobjects() {
643        let path = skip_if_missing!("V2003/testV2003.mdb");
644        let mut reader = PageReader::open(&path).unwrap();
645        let tdef = read_table_def(&mut reader, "MSysObjects", CATALOG_PAGE).unwrap();
646        assert_msysobjects(&tdef);
647    }
648
649    #[test]
650    fn ace12_msysobjects() {
651        let path = skip_if_missing!("V2007/testV2007.accdb");
652        let mut reader = PageReader::open(&path).unwrap();
653        let tdef = read_table_def(&mut reader, "MSysObjects", CATALOG_PAGE).unwrap();
654        assert_msysobjects(&tdef);
655    }
656
657    #[test]
658    fn ace14_msysobjects() {
659        let path = skip_if_missing!("V2010/testV2010.accdb");
660        let mut reader = PageReader::open(&path).unwrap();
661        let tdef = read_table_def(&mut reader, "MSysObjects", CATALOG_PAGE).unwrap();
662        assert_msysobjects(&tdef);
663    }
664
665    #[test]
666    fn columns_sorted_by_col_num() {
667        let path = skip_if_missing!("V2003/testV2003.mdb");
668        let mut reader = PageReader::open(&path).unwrap();
669        let tdef = read_table_def(&mut reader, "MSysObjects", CATALOG_PAGE).unwrap();
670        for w in tdef.columns.windows(2) {
671            assert!(
672                w[0].col_num <= w[1].col_num,
673                "columns should be sorted by col_num"
674            );
675        }
676    }
677
678    #[test]
679    fn invalid_page_type_error() {
680        let path = skip_if_missing!("V2003/testV2003.mdb");
681        let mut reader = PageReader::open(&path).unwrap();
682        // Page 1 is typically a data/bitmap page, not TDEF
683        let result = read_table_def(&mut reader, "bad", 1);
684        assert!(result.is_err());
685    }
686
687    // -- Index tests ----------------------------------------------------------
688
689    /// Helper: find a user table's TDEF page from the catalog.
690    fn find_table_page(reader: &mut PageReader, table_name: &str) -> Option<u32> {
691        let catalog = crate::catalog::read_catalog(reader).ok()?;
692        catalog
693            .iter()
694            .find(|e| e.name == table_name)
695            .map(|e| e.table_page)
696    }
697
698    fn assert_user_table_indexes(path: &std::path::Path, table_name: &str) -> TableDef {
699        let mut reader = PageReader::open(path).unwrap();
700        let page = find_table_page(&mut reader, table_name)
701            .unwrap_or_else(|| panic!("table '{table_name}' not found in catalog"));
702        read_table_def(&mut reader, table_name, page).unwrap()
703    }
704
705    #[test]
706    fn jet4_index_count() {
707        let path = skip_if_missing!("V2003/testV2003.mdb");
708        let tdef = assert_user_table_indexes(&path, "Table1");
709        assert!(
710            !tdef.indexes.is_empty(),
711            "Table1 should have at least one index"
712        );
713    }
714
715    #[test]
716    fn jet3_index_count() {
717        let path = skip_if_missing!("V1997/testV1997.mdb");
718        let tdef = assert_user_table_indexes(&path, "Table1");
719        assert!(
720            !tdef.indexes.is_empty(),
721            "Jet3 Table1 should have at least one index"
722        );
723    }
724
725    #[test]
726    fn ace12_index_count() {
727        let path = skip_if_missing!("V2007/testV2007.accdb");
728        let tdef = assert_user_table_indexes(&path, "Table1");
729        assert!(
730            !tdef.indexes.is_empty(),
731            "ACE12 Table1 should have at least one index"
732        );
733    }
734
735    #[test]
736    fn ace14_index_count() {
737        let path = skip_if_missing!("V2010/testV2010.accdb");
738        let tdef = assert_user_table_indexes(&path, "Table1");
739        assert!(
740            !tdef.indexes.is_empty(),
741            "ACE14 Table1 should have at least one index"
742        );
743    }
744
745    #[test]
746    fn jet4_primary_key() {
747        let path = skip_if_missing!("V2003/testV2003.mdb");
748        let tdef = assert_user_table_indexes(&path, "Table1");
749
750        let pk = tdef
751            .indexes
752            .iter()
753            .find(|idx| idx.name == "PrimaryKey")
754            .expect("Table1 should have a PrimaryKey index");
755
756        assert_ne!(
757            pk.flags & crate::format::index_flags::UNIQUE,
758            0,
759            "PrimaryKey should have UNIQUE flag"
760        );
761        assert_ne!(
762            pk.flags & crate::format::index_flags::REQUIRED,
763            0,
764            "PrimaryKey should have REQUIRED flag"
765        );
766        assert!(
767            !pk.columns.is_empty(),
768            "PrimaryKey should have at least one column"
769        );
770    }
771
772    #[test]
773    fn jet4_index_columns() {
774        let path = skip_if_missing!("V2003/testV2003.mdb");
775        let tdef = assert_user_table_indexes(&path, "Table1");
776
777        for idx in &tdef.indexes {
778            if idx.index_type != crate::format::index_type::FOREIGN_KEY {
779                assert!(
780                    !idx.columns.is_empty(),
781                    "non-FK index '{}' should have columns",
782                    idx.name
783                );
784                for col in &idx.columns {
785                    assert!(
786                        (col.col_num as usize) < tdef.columns.len() + 256,
787                        "index column number should be reasonable"
788                    );
789                }
790            }
791        }
792    }
793
794    #[test]
795    fn index_fk_type() {
796        // indexTestV2003.mdb has FK (type=2) indexes
797        let path = skip_if_missing!("V2003/indexTestV2003.mdb");
798        let tdef = assert_user_table_indexes(&path, "Table1");
799
800        let fk_indexes: Vec<&IndexDef> = tdef
801            .indexes
802            .iter()
803            .filter(|idx| idx.index_type == crate::format::index_type::FOREIGN_KEY)
804            .collect();
805
806        assert!(
807            !fk_indexes.is_empty(),
808            "indexTest Table1 should have FK indexes"
809        );
810
811        for fk in &fk_indexes {
812            assert!(
813                fk.foreign_key.is_some(),
814                "FK index '{}' should have foreign_key info",
815                fk.name
816            );
817            assert!(
818                fk.columns.is_empty(),
819                "FK index '{}' should have no columns",
820                fk.name
821            );
822        }
823    }
824
825    #[test]
826    fn jet3_index_fk_type() {
827        let path = skip_if_missing!("V1997/indexTestV1997.mdb");
828        let tdef = assert_user_table_indexes(&path, "Table1");
829
830        let fk_indexes: Vec<&IndexDef> = tdef
831            .indexes
832            .iter()
833            .filter(|idx| idx.index_type == crate::format::index_type::FOREIGN_KEY)
834            .collect();
835
836        assert!(
837            !fk_indexes.is_empty(),
838            "Jet3 indexTest Table1 should have FK indexes"
839        );
840        for fk in &fk_indexes {
841            assert!(fk.foreign_key.is_some());
842        }
843    }
844
845    // -- is_replication_column tests ----------------------------------------
846
847    #[test]
848    fn is_replication_true() {
849        let col = ColumnDef {
850            name: "s_GUID".to_string(),
851            col_type: ColumnType::Guid,
852            col_num: 1,
853            var_col_num: 0,
854            fixed_offset: 0,
855            col_size: 16,
856            flags: column_flags::REPLICATION | column_flags::NULLABLE,
857            is_fixed: false,
858            precision: 0,
859            scale: 0,
860        };
861        assert!(is_replication_column(&col));
862    }
863
864    #[test]
865    fn is_replication_false() {
866        let col = ColumnDef {
867            name: "ID".to_string(),
868            col_type: ColumnType::Long,
869            col_num: 1,
870            var_col_num: 0,
871            fixed_offset: 0,
872            col_size: 4,
873            flags: column_flags::FIXED,
874            is_fixed: true,
875            precision: 0,
876            scale: 0,
877        };
878        assert!(!is_replication_column(&col));
879    }
880
881    #[test]
882    fn index_names_are_nonempty() {
883        let path = skip_if_missing!("V2003/testV2003.mdb");
884        let tdef = assert_user_table_indexes(&path, "Table1");
885
886        for idx in &tdef.indexes {
887            assert!(!idx.name.is_empty(), "index name should not be empty");
888        }
889    }
890
891    // -- read_names tests -----------------------------------------------------
892
893    #[test]
894    fn read_names_jet3_latin1() {
895        // Jet3 format: [len: u8][Latin-1 bytes]
896        let buf = [3, b'F', b'o', b'o', 3, b'B', b'a', b'r'];
897        let mut cursor = TdefCursor::new(&buf, 0);
898        let names = read_names(&mut cursor, 2, true).unwrap();
899        assert_eq!(names, vec!["Foo", "Bar"]);
900        assert_eq!(cursor.position(), 8);
901    }
902
903    #[test]
904    fn read_names_jet4_utf16le() {
905        // Jet4 format: [len: u16 LE][UTF-16LE bytes]
906        // "Ab" = 4 bytes UTF-16LE
907        let buf = [
908            4, 0, // len=4
909            b'A', 0, b'b', 0, // "Ab"
910            2, 0, // len=2
911            b'X', 0, // "X"
912        ];
913        let mut cursor = TdefCursor::new(&buf, 0);
914        let names = read_names(&mut cursor, 2, false).unwrap();
915        assert_eq!(names, vec!["Ab", "X"]);
916        assert_eq!(cursor.position(), 10);
917    }
918
919    #[test]
920    fn read_names_boundary_error() {
921        // Buffer too short for the name data
922        let buf = [3, b'A', b'B'];
923        let mut cursor = TdefCursor::new(&buf, 0);
924        let result = read_names(&mut cursor, 1, true);
925        assert!(result.is_err());
926    }
927
928    #[test]
929    fn read_names_empty_count() {
930        let buf = [];
931        let mut cursor = TdefCursor::new(&buf, 0);
932        let names = read_names(&mut cursor, 0, true).unwrap();
933        assert!(names.is_empty());
934        assert_eq!(cursor.position(), 0);
935    }
936
937    // -- parse_column_entries tests -------------------------------------------
938
939    #[test]
940    fn parse_column_entries_jet3() {
941        use crate::format::JET3;
942        // Build a minimal Jet3 column entry (18 bytes)
943        let mut entry = vec![0u8; JET3.tdef_column_entry_span];
944        entry[0] = ColumnType::Long.to_byte(); // col_type
945        entry[JET3.coldef_number_pos] = 5; // col_num (1 byte for Jet3)
946        entry[JET3.coldef_flags_pos] = column_flags::FIXED;
947        entry[JET3.coldef_length_pos] = 4;
948        entry[JET3.coldef_length_pos + 1] = 0;
949
950        let mut cursor = TdefCursor::new(&entry, 0);
951        let cols =
952            parse_column_entries(&mut cursor, JET3.tdef_column_entry_span, 1, true, &JET3).unwrap();
953        assert_eq!(cols.len(), 1);
954        assert_eq!(cols[0].col_type, ColumnType::Long);
955        assert_eq!(cols[0].col_num, 5);
956        assert!(cols[0].is_fixed);
957        assert_eq!(cols[0].col_size, 4);
958    }
959
960    #[test]
961    fn parse_column_entries_jet4() {
962        use crate::format::JET4;
963        // Build a minimal Jet4 column entry (25 bytes)
964        let mut entry = vec![0u8; JET4.tdef_column_entry_span];
965        entry[0] = ColumnType::Text.to_byte();
966        // col_num: 2 bytes LE
967        entry[JET4.coldef_number_pos] = 3;
968        entry[JET4.coldef_number_pos + 1] = 0;
969        entry[JET4.coldef_flags_pos] = column_flags::NULLABLE;
970        entry[JET4.coldef_length_pos] = 0xFF;
971        entry[JET4.coldef_length_pos + 1] = 0;
972
973        let mut cursor = TdefCursor::new(&entry, 0);
974        let cols = parse_column_entries(&mut cursor, JET4.tdef_column_entry_span, 1, false, &JET4)
975            .unwrap();
976        assert_eq!(cols.len(), 1);
977        assert_eq!(cols[0].col_type, ColumnType::Text);
978        assert_eq!(cols[0].col_num, 3);
979        assert!(!cols[0].is_fixed);
980        assert_eq!(cols[0].col_size, 255);
981    }
982
983    // -- TdefCursor unit tests ------------------------------------------------
984
985    #[test]
986    fn cursor_read_u8() {
987        let buf = [0xAB, 0xCD];
988        let mut cursor = TdefCursor::new(&buf, 0);
989        assert_eq!(cursor.read_u8().unwrap(), 0xAB);
990        assert_eq!(cursor.position(), 1);
991        assert_eq!(cursor.read_u8().unwrap(), 0xCD);
992        assert_eq!(cursor.position(), 2);
993    }
994
995    #[test]
996    fn cursor_read_u16_le() {
997        let buf = [0x34, 0x12, 0x78, 0x56];
998        let mut cursor = TdefCursor::new(&buf, 0);
999        assert_eq!(cursor.read_u16_le().unwrap(), 0x1234);
1000        assert_eq!(cursor.position(), 2);
1001        assert_eq!(cursor.read_u16_le().unwrap(), 0x5678);
1002        assert_eq!(cursor.position(), 4);
1003    }
1004
1005    #[test]
1006    fn cursor_read_u32_le() {
1007        let buf = [0x78, 0x56, 0x34, 0x12];
1008        let mut cursor = TdefCursor::new(&buf, 0);
1009        assert_eq!(cursor.read_u32_le().unwrap(), 0x12345678);
1010        assert_eq!(cursor.position(), 4);
1011    }
1012
1013    #[test]
1014    fn cursor_read_bytes() {
1015        let buf = [1, 2, 3, 4, 5];
1016        let mut cursor = TdefCursor::new(&buf, 1);
1017        let bytes = cursor.read_bytes(3).unwrap();
1018        assert_eq!(bytes, &[2, 3, 4]);
1019        assert_eq!(cursor.position(), 4);
1020    }
1021
1022    #[test]
1023    fn cursor_skip() {
1024        let buf = [0u8; 10];
1025        let mut cursor = TdefCursor::new(&buf, 0);
1026        cursor.skip(5).unwrap();
1027        assert_eq!(cursor.position(), 5);
1028        cursor.skip(5).unwrap();
1029        assert_eq!(cursor.position(), 10);
1030    }
1031
1032    #[test]
1033    fn cursor_out_of_bounds() {
1034        let buf = [0xAB];
1035        let mut cursor = TdefCursor::new(&buf, 0);
1036        assert!(cursor.read_u16_le().is_err());
1037        assert!(cursor.read_u32_le().is_err());
1038        cursor.read_u8().unwrap(); // consume the one byte
1039        assert!(cursor.read_u8().is_err());
1040        assert!(cursor.read_bytes(1).is_err());
1041        assert!(cursor.skip(1).is_err());
1042    }
1043
1044    #[test]
1045    fn cursor_u8_at() {
1046        let buf = [0x10, 0x20, 0x30];
1047        let cursor = TdefCursor::new(&buf, 0);
1048        assert_eq!(cursor.u8_at(1).unwrap(), 0x20);
1049        assert_eq!(cursor.position(), 0); // position unchanged
1050        assert!(cursor.u8_at(3).is_err());
1051    }
1052
1053    #[test]
1054    fn cursor_u16_le_at() {
1055        let buf = [0x00, 0x34, 0x12];
1056        let cursor = TdefCursor::new(&buf, 0);
1057        assert_eq!(cursor.u16_le_at(1).unwrap(), 0x1234);
1058        assert_eq!(cursor.position(), 0); // position unchanged
1059        assert!(cursor.u16_le_at(2).is_err());
1060    }
1061
1062    // -----------------------------------------------------------------------
1063    // build_index_defs tests
1064    // -----------------------------------------------------------------------
1065
1066    #[test]
1067    fn build_index_defs_normal_index() {
1068        let logical = vec![LogicalIndex {
1069            index_num: 1,
1070            index_col_entry: 0,
1071            fk_index_type: 0,
1072            fk_index_number: 0,
1073            fk_table_page: 0,
1074            update_action: 0,
1075            delete_action: 0,
1076            index_type: crate::format::index_type::NORMAL,
1077        }];
1078        let col = IndexColumn {
1079            col_num: 3,
1080            order: IndexColumnOrder::Ascending,
1081        };
1082        let physical: Vec<PhysicalIndexEntry> = vec![(vec![col], 0x01, 100)];
1083        let names = vec!["PK_Id".to_string()];
1084
1085        let result = build_index_defs(&logical, &physical, names);
1086        assert_eq!(result.len(), 1);
1087        assert_eq!(result[0].name, "PK_Id");
1088        assert_eq!(result[0].index_num, 1);
1089        assert_eq!(result[0].columns.len(), 1);
1090        assert_eq!(result[0].columns[0].col_num, 3);
1091        assert_eq!(result[0].flags, 0x01);
1092        assert_eq!(result[0].first_data_page, 100);
1093        assert!(result[0].foreign_key.is_none());
1094    }
1095
1096    #[test]
1097    fn build_index_defs_foreign_key() {
1098        let logical = vec![LogicalIndex {
1099            index_num: 2,
1100            index_col_entry: 0,
1101            fk_index_type: 1,
1102            fk_index_number: 5,
1103            fk_table_page: 42,
1104            update_action: 1,
1105            delete_action: 2,
1106            index_type: crate::format::index_type::FOREIGN_KEY,
1107        }];
1108        let physical: Vec<PhysicalIndexEntry> = vec![];
1109        let names = vec!["FK_Ref".to_string()];
1110
1111        let result = build_index_defs(&logical, &physical, names);
1112        assert_eq!(result.len(), 1);
1113        assert_eq!(result[0].name, "FK_Ref");
1114        assert!(result[0].columns.is_empty());
1115        let fk = result[0].foreign_key.as_ref().unwrap();
1116        assert_eq!(fk.fk_index_type, 1);
1117        assert_eq!(fk.fk_index_number, 5);
1118        assert_eq!(fk.fk_table_page, 42);
1119        assert_eq!(fk.update_action, 1);
1120        assert_eq!(fk.delete_action, 2);
1121    }
1122
1123    #[test]
1124    fn build_index_defs_out_of_range_warning() {
1125        // index_col_entry points beyond idx_col_defs → warning path
1126        let logical = vec![LogicalIndex {
1127            index_num: 3,
1128            index_col_entry: 99, // out of range
1129            fk_index_type: 0,
1130            fk_index_number: 0,
1131            fk_table_page: 0,
1132            update_action: 0,
1133            delete_action: 0,
1134            index_type: crate::format::index_type::NORMAL,
1135        }];
1136        let physical: Vec<PhysicalIndexEntry> = vec![]; // empty → 99 is out of range
1137        let names = vec!["BadIdx".to_string()];
1138
1139        let result = build_index_defs(&logical, &physical, names);
1140        assert_eq!(result.len(), 1);
1141        assert_eq!(result[0].name, "BadIdx");
1142        assert!(result[0].columns.is_empty());
1143        assert_eq!(result[0].flags, 0);
1144        assert_eq!(result[0].first_data_page, 0);
1145        assert!(result[0].foreign_key.is_none());
1146    }
1147
1148    #[test]
1149    fn build_index_defs_name_missing_uses_default() {
1150        // More logical indexes than names → fallback to empty string
1151        let logical = vec![
1152            LogicalIndex {
1153                index_num: 0,
1154                index_col_entry: 0,
1155                fk_index_type: 0,
1156                fk_index_number: 0,
1157                fk_table_page: 0,
1158                update_action: 0,
1159                delete_action: 0,
1160                index_type: crate::format::index_type::NORMAL,
1161            },
1162            LogicalIndex {
1163                index_num: 1,
1164                index_col_entry: 0,
1165                fk_index_type: 0,
1166                fk_index_number: 0,
1167                fk_table_page: 0,
1168                update_action: 0,
1169                delete_action: 0,
1170                index_type: crate::format::index_type::NORMAL,
1171            },
1172        ];
1173        let col = IndexColumn {
1174            col_num: 1,
1175            order: IndexColumnOrder::Ascending,
1176        };
1177        let physical: Vec<PhysicalIndexEntry> = vec![(vec![col], 0, 0)];
1178        let names = vec!["OnlyOne".to_string()]; // only 1 name for 2 indexes
1179
1180        let result = build_index_defs(&logical, &physical, names);
1181        assert_eq!(result.len(), 2);
1182        assert_eq!(result[0].name, "OnlyOne");
1183        assert_eq!(result[1].name, ""); // fallback to empty
1184    }
1185}