Working with SpreadsheetML tables

A SpreadsheetML table is a logical range over worksheet cells. The worksheet stores the actual cell values; a separate table definition part stores table metadata such as name, range, columns, and filtering.

Tables organize worksheet ranges as named datasets. They can expose filter and sort controls, structured references, calculated columns, style information, and automatic expansion behavior in spreadsheet applications.

Table markup

<table
  id="1"
  name="SalesTable"
  displayName="SalesTable"
  ref="A1:B10"
  totalsRowShown="0"
  xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
  <autoFilter ref="A1:B10"/>
  <tableColumns count="2">
    <tableColumn id="1" name="Region"/>
    <tableColumn id="2" name="Sales"/>
  </tableColumns>
</table>

The worksheet points to table definition parts with <tableParts/> and relationships.

The table part contains metadata only; the cell data remains in the worksheet. The ref attribute covers the full table range, including headers. id and name must be unique across table parts, and displayName must also be unique across workbook defined names because formulas can reference it.

Rust workflow

Create a table definition part, write table metadata, then add a tableParts reference to the worksheet:

#![allow(unused)]
fn main() {
pub fn add_table(
  path: &Path,
  sheet_name: &str,
  table_name: &str,
  range: &str,
  columns: &[&str],
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
  let mut document = SpreadsheetDocument::new_from_file_with_settings(path, lazy_settings())?;
  let workbook_part = document.workbook_part()?;
  let worksheet_part = worksheet_part_by_name(&document, &workbook_part, sheet_name)?;
  let table_part = worksheet_part.add_new_part_auto_id::<_, TableDefinitionPart>(&mut document)?;
  let relationship_id = worksheet_part
    .get_id_of_part(&document, &table_part)
    .expect("table relationship id")
    .to_string();
  let table_id = max_table_id(&worksheet_part, &document) + 1;

  table_part.set_data(
    &mut document,
    table_xml(table_id, table_name, range, columns).into_bytes(),
  )?;

  let worksheet_xml = worksheet_part.data_as_str(&document)?.unwrap_or_default();
  let worksheet_xml = ensure_relationship_namespace(worksheet_xml);
  let updated_xml = append_table_reference(&worksheet_xml, &relationship_id)?;
  worksheet_part.set_data(&mut document, updated_xml.into_bytes())?;

  let mut buffer = Cursor::new(Vec::new());
  document.save(&mut buffer)?;
  Ok(buffer.into_inner())
}
}

To keep autofilter enabled, include an autoFilter element, even if it has no active criteria. Table columns live under tableColumns, whose count must match the number of tableColumn children.

In ooxmlsdk, WorksheetPart::table_definition_parts(&document) traverses table definition parts, and generated schema types include Table, TableColumn, and AutoFilter.