///|
/// B+ Tree Attribute Index
/// Balanced tree for efficient read/write operations with range query support.

///|
/// Helper to create a single-byte Bytes
fn make_exists_key() -> Bytes {
  let w = @binary.BinaryWriter::new()
  w.push_byte(b'\x01')
  w.concat()
}

///|
/// B+ Tree leaf entry
priv struct BPLeafEntry {
  key : Bytes
  value : Array[@types.VectorId]
}

///|
/// B+ Tree branch entry
priv struct BPBranchEntry {
  key : Bytes
  child_idx : Int // Index into nodes array
}

///|
/// B+ Tree node
priv enum BPNode {
  Leaf(Array[BPLeafEntry], Int) // entries, next_leaf_idx (-1 if none)
  Branch(Array[BPBranchEntry], Int) // entries, rightmost_idx
}

///|
/// B+ Tree structure
struct BPTree {
  nodes : Array[BPNode]
  mut root_idx : Int
  order : Int // Max entries per node
}

///|
/// B+ Tree Attribute Index container
pub struct BPTreeAttrIndex {
  field_trees : Map[String, BPTree] // Per-field B+ trees
  exists_trees : Map[String, BPTree] // Existence B+ trees
  data : Map[@types.VectorId, @types.Attrs] // Direct storage for attrs
  order : Int // Node order (default 128)
}

///|
/// Create a new B+ tree
fn BPTree::new(order : Int) -> BPTree {
  // Start with empty leaf as root
  { nodes: [Leaf([], -1)], root_idx: 0, order }
}

///|
/// Compare two byte arrays lexicographically
fn bytes_compare(a : Bytes, b : Bytes) -> Int {
  let min_len = if a.length() < b.length() { a.length() } else { b.length() }
  for i in 0.. Bytes {
  let w = @binary.BinaryWriter::new()
  match value {
    Null => w.push_byte(b'\x00')
    Bool(v) => {
      w.push_byte(b'\x01')
      w.push_byte(if v { b'\x01' } else { b'\x00' })
    }
    Int(n) => {
      // Order-preserving encoding for signed integers
      // Flip sign bit for correct ordering
      w.push_byte(b'\x02') // type tag
      let u = n.reinterpret_as_uint64() ^ 0x8000000000000000UL
      // Write big-endian for order preservation
      for i = 7; i >= 0; i = i - 1 {
        let shift = i * 8
        w.push_byte(((u >> shift) & 0xFFUL).to_byte())
      }
    }
    Float(f) => {
      // Order-preserving encoding for floats
      w.push_byte(b'\x03') // type tag
      let bits = f.reinterpret_as_uint64()
      // Flip bits for negative numbers, flip sign for positive
      let u = if (bits & 0x8000000000000000UL) != 0UL {
        bits ^ 0xFFFFFFFFFFFFFFFFUL
      } else {
        bits ^ 0x8000000000000000UL
      }
      for i = 7; i >= 0; i = i - 1 {
        let shift = i * 8
        w.push_byte(((u >> shift) & 0xFFUL).to_byte())
      }
    }
    String(s) => {
      w.push_byte(b'\x04') // type tag
      // Encode full UTF-8 to preserve Unicode
      w.push_bytes(@utf8.encode(s.view()))
    }
  }
  w.concat()
}

///|
/// Write a String in a backend-stable format
fn write_string(w : @binary.BinaryWriter, value : String) -> Unit {
  let chars = value.to_array()
  w.push_u32(chars.length().reinterpret_as_uint())
  for c in chars {
    w.push_u32(c.to_uint())
  }
}

///|
/// Read a String written by write_string
fn read_string(r : @binary.BinaryReader) -> String {
  let len = r.read_u32().reinterpret_as_int()
  let chars : Array[Char] = []
  for _ in 0.. Unit {
  w.push_u32(data.length().reinterpret_as_uint())
  w.push_bytes(data)
}

///|
/// Read length-prefixed raw bytes
fn read_prefixed_bytes(r : @binary.BinaryReader) -> Bytes {
  let len = r.read_u32().reinterpret_as_int()
  r.read_bytes(len)
}

///|
/// On-disk format for VectorIds inside a BPTreeAttrIndex / BPTree leaf.
///
/// Three concrete layouts have shipped over time:
///   - V1Int64       — version <= 1: fixed 8-byte i64
///   - V2TagByte     — version == 2 in trkbt10/vcdb <= 0.3.0: 1-byte tag
///                     (0x00 = Int64 + 8 bytes; 0x01 = Bytes16 + 16 bytes).
///                     Used by indexion's cache files written via the
///                     old release; we keep reading them.
///   - V3WireBytes   — version >= 2 in this branch: u32 length + N wire
///                     bytes via VectorId::from_wire_bytes. Same encoding
///                     was originally introduced under the version=2 label,
///                     which is what makes the v2 disambiguation necessary.
///
/// Detection (only relevant when version == 2): peek the first byte that
/// belongs to the next VectorId. V2TagByte always starts with 0x00 or
/// 0x01; V3WireBytes starts with the LE low byte of u32 length, which is
/// 0x08 for Int64Id (wire length 8) or 0x10 for Bytes16Id (wire length 16)
/// — no overlap.
priv enum IdFormat {
  V1Int64
  V2TagByte
  V3WireBytes
}

///|
/// Read a single VectorId in whichever historical layout matches `fmt`.
fn read_vector_id_with_format(
  r : @binary.BinaryReader,
  fmt : IdFormat,
) -> @types.VectorId? {
  match fmt {
    V1Int64 => Some(@types.Int64Id(r.read_i64()))
    V2TagByte =>
      match r.read_byte() {
        b'\x00' => Some(@types.Int64Id(r.read_i64()))
        b'\x01' =>
          Some(
            @types.Bytes16Id({
              b0: r.read_byte(),
              b1: r.read_byte(),
              b2: r.read_byte(),
              b3: r.read_byte(),
              b4: r.read_byte(),
              b5: r.read_byte(),
              b6: r.read_byte(),
              b7: r.read_byte(),
              b8: r.read_byte(),
              b9: r.read_byte(),
              b10: r.read_byte(),
              b11: r.read_byte(),
              b12: r.read_byte(),
              b13: r.read_byte(),
              b14: r.read_byte(),
              b15: r.read_byte(),
            }),
          )
        // Unknown tag — surface as "no id" rather than aborting deep in
        // the reader; the caller already tolerates missing ids.
        _ => None
      }
    V3WireBytes => {
      let wire_len = r.read_u32().reinterpret_as_int()
      let wire = r.read_bytes(wire_len)
      @types.VectorId::from_wire_bytes(wire)
    }
  }
}

///|
/// Pick the right `IdFormat` based on the version u32 from the file
/// header and, for version==2, a peek at the first ID byte. `peek_byte`
/// may be None when the index is empty (no data entries to peek); in
/// that case we default to V3WireBytes since an empty file written by
/// the current code is what locally-built deployments will produce.
fn detect_id_format(version : UInt, peek_byte : Byte?) -> IdFormat {
  if version <= 1U {
    V1Int64
  } else if version >= 3U {
    V3WireBytes
  } else {
    match peek_byte {
      Some(b'\x00') | Some(b'\x01') => V2TagByte
      _ => V3WireBytes
    }
  }
}

///|
/// Write a typed attribute value
fn write_attr_value(w : @binary.BinaryWriter, value : @types.AttrValue) -> Unit {
  match value {
    Null => w.push_u32(0U)
    Bool(b) => {
      w.push_u32(1U)
      w.push_u32(if b { 1U } else { 0U })
    }
    Int(n) => {
      w.push_u32(2U)
      w.push_i64(n)
    }
    Float(f) => {
      w.push_u32(3U)
      w.push_f64(f)
    }
    String(s) => {
      w.push_u32(4U)
      write_string(w, s)
    }
  }
}

///|
/// Read a typed attribute value
fn read_attr_value(r : @binary.BinaryReader) -> @types.AttrValue {
  match r.read_u32() {
    0U => @types.Null
    1U => @types.Bool(r.read_u32() != 0U)
    2U => @types.Int(r.read_i64())
    3U => @types.Float(r.read_f64())
    _ => @types.String(read_string(r))
  }
}

///|
/// Write attrs payload
fn write_attrs(w : @binary.BinaryWriter, attrs : @types.Attrs) -> Unit {
  let keys = attrs.keys()
  w.push_u32(keys.length().reinterpret_as_uint())
  for key in keys {
    write_string(w, key)
    match attrs.get(key) {
      Some(value) => write_attr_value(w, value)
      None => write_attr_value(w, @types.Null)
    }
  }
}

///|
/// Read attrs payload
fn read_attrs(r : @binary.BinaryReader) -> @types.Attrs {
  let attrs = @types.empty_attrs()
  let count = r.read_u32().reinterpret_as_int()
  for _ in 0.. Bytes {
  let w = @binary.BinaryWriter::new()
  w.push_u32(tree.order.reinterpret_as_uint())
  w.push_i32(tree.root_idx)
  w.push_u32(tree.nodes.length().reinterpret_as_uint())
  for node in tree.nodes {
    match node {
      Leaf(entries, next) => {
        w.push_u32(0U)
        w.push_u32(entries.length().reinterpret_as_uint())
        for entry in entries {
          write_prefixed_bytes(w, entry.key)
          w.push_u32(entry.value.length().reinterpret_as_uint())
          for id in entry.value {
            let wire = id.to_wire_bytes()
            w.push_u32(wire.length().reinterpret_as_uint())
            w.push_bytes(wire)
          }
        }
        w.push_i32(next)
      }
      Branch(entries, rightmost) => {
        w.push_u32(1U)
        w.push_u32(entries.length().reinterpret_as_uint())
        for entry in entries {
          write_prefixed_bytes(w, entry.key)
          w.push_i32(entry.child_idx)
        }
        w.push_i32(rightmost)
      }
    }
  }
  w.concat()
}

///|
/// Deserialize a B+ tree node graph. `fmt` is the VectorId layout
/// detected at the enclosing BPTreeAttrIndex header; tree leaves use the
/// same layout as the data map, so we just propagate it.
fn deserialize_bptree(data : Bytes, fmt : IdFormat) -> BPTree {
  let r = @binary.BinaryReader::new(data)
  let order = r.read_u32().reinterpret_as_int()
  let root_idx = r.read_i32()
  let node_count = r.read_u32().reinterpret_as_int()
  let nodes : Array[BPNode] = []
  for _ in 0.. value.push(id)
            None => ()
          }
        }
        entries.push({ key, value })
      }
      let next = r.read_i32()
      nodes.push(Leaf(entries, next))
    } else {
      let entries : Array[BPBranchEntry] = []
      for _ in 0.. Int {
  if entries.is_empty() {
    return rightmost
  }
  for i in 0.. Int {
  for i in 0.. Array[Int] {
  let path : Array[Int] = []
  let mut current = tree.root_idx
  while true {
    path.push(current)
    match tree.nodes[current] {
      Leaf(_, _) => break
      Branch(entries, rightmost) =>
        current = select_branch_child(entries, rightmost, key)
    }
  }
  path
}

///|
/// Find leaf node containing key
fn find_leaf(tree : BPTree, key : Bytes) -> Int {
  let path = find_leaf_path(tree, key)
  path[path.length() - 1]
}

///|
/// Find entry in leaf
fn find_entry(entries : Array[BPLeafEntry], key : Bytes) -> Int {
  for i in 0.. Unit {
  for existing in list {
    if existing == id {
      return
    }
  }
  list.push(id)
}

///|
/// Remove ID from posting list
fn remove_from_posting(
  list : Array[@types.VectorId],
  id : @types.VectorId,
) -> Unit {
  for i in 0.. Bool {
  match tree.nodes[node_idx] {
    Leaf(entries, _) => entries.length() > tree.order
    Branch(entries, _) => entries.length() > tree.order
  }
}

///|
/// Split an overflowing leaf node and return the new right leaf and separator key
fn split_leaf(tree : BPTree, leaf_idx : Int) -> (Int, Bytes) {
  match tree.nodes[leaf_idx] {
    Leaf(entries, next) => {
      let split_at = entries.length() / 2
      let right_entries : Array[BPLeafEntry] = []
      for i in split_at.. split_at {
        let _ = entries.pop()
      }
      let split_key = right_entries[0].key
      let new_leaf_idx = tree.nodes.length()
      tree.nodes.push(Leaf(right_entries, next))
      tree.nodes[leaf_idx] = Leaf(entries, new_leaf_idx)
      (new_leaf_idx, split_key)
    }
    _ => abort("split_leaf expects a leaf node")
  }
}

///|
/// Insert a promoted separator into a branch node
fn insert_into_branch(
  tree : BPTree,
  branch_idx : Int,
  key : Bytes,
  child_idx : Int,
) -> Unit {
  match tree.nodes[branch_idx] {
    Branch(entries, rightmost) => {
      let insert_pos = find_branch_insert_pos(entries, key)
      let new_entries : Array[BPBranchEntry] = []
      let mut new_rightmost = rightmost
      if insert_pos == entries.length() {
        for entry in entries {
          new_entries.push(entry)
        }
        new_entries.push({ key, child_idx: rightmost })
        new_rightmost = child_idx
      } else {
        for i in 0.. abort("insert_into_branch expects a branch node")
  }
}

///|
/// Split an overflowing branch node and return the new right branch and promoted key
fn split_branch(tree : BPTree, branch_idx : Int) -> (Int, Bytes) {
  match tree.nodes[branch_idx] {
    Branch(entries, rightmost) => {
      let mid = entries.length() / 2
      let promoted = entries[mid].key
      let left_rightmost = entries[mid].child_idx
      let right_entries : Array[BPBranchEntry] = []
      for i in (mid + 1).. mid {
        let _ = entries.pop()
      }
      let new_branch_idx = tree.nodes.length()
      tree.nodes.push(Branch(right_entries, rightmost))
      tree.nodes[branch_idx] = Branch(entries, left_rightmost)
      (new_branch_idx, promoted)
    }
    _ => abort("split_branch expects a branch node")
  }
}

///|
/// Create a new root after splitting the previous root
fn split_root(
  tree : BPTree,
  left_idx : Int,
  right_idx : Int,
  split_key : Bytes,
) -> Unit {
  let new_root_idx = tree.nodes.length()
  tree.nodes.push(Branch([{ key: split_key, child_idx: left_idx }], right_idx))
  tree.root_idx = new_root_idx
}

///|
/// Propagate node splits up the insertion path
fn handle_overflow(tree : BPTree, path : Array[Int]) -> Unit {
  if path.is_empty() {
    return
  }
  let mut level = path.length() - 1
  let mut current_idx = path[level]
  while is_overflow(tree, current_idx) {
    let (new_idx, split_key) = match tree.nodes[current_idx] {
      Leaf(_, _) => split_leaf(tree, current_idx)
      Branch(_, _) => split_branch(tree, current_idx)
    }
    if level == 0 {
      split_root(tree, current_idx, new_idx, split_key)
      return
    }
    let parent_idx = path[level - 1]
    insert_into_branch(tree, parent_idx, split_key, new_idx)
    current_idx = parent_idx
    level = level - 1
  }
}

///|
/// Insert posting into B+ tree
fn bptree_insert(tree : BPTree, key : Bytes, id : @types.VectorId) -> Unit {
  let path = find_leaf_path(tree, key)
  let leaf_idx = path[path.length() - 1]
  match tree.nodes[leaf_idx] {
    Leaf(entries, next) => {
      // Find existing entry
      let entry_idx = find_entry(entries, key)
      if entry_idx >= 0 {
        // Add to existing posting list
        insert_into_posting(entries[entry_idx].value, id)
      } else {
        // Create new entry
        let new_list : Array[@types.VectorId] = [id]
        entries.push({ key, value: new_list })
        // Sort entries by key
        entries.sort_by(fn(a, b) { bytes_compare(a.key, b.key) })
        if is_overflow(tree, leaf_idx) {
          handle_overflow(tree, path)
        }
      }
      let _ = next
    }
    _ => ()
  }
}

///|
/// Remove posting from B+ tree
fn bptree_remove(tree : BPTree, key : Bytes, id : @types.VectorId) -> Unit {
  let leaf_idx = find_leaf(tree, key)
  match tree.nodes[leaf_idx] {
    Leaf(entries, _) => {
      let entry_idx = find_entry(entries, key)
      if entry_idx >= 0 {
        remove_from_posting(entries[entry_idx].value, id)
        // Remove entry if posting list is empty
        if entries[entry_idx].value.is_empty() {
          // Swap with last and pop
          let last = entries.length() - 1
          if entry_idx != last {
            entries[entry_idx] = entries[last]
          }
          let _ = entries.pop()
          // Re-sort after removal
          entries.sort_by(fn(a, b) { bytes_compare(a.key, b.key) })
        }
      }
    }
    _ => ()
  }
}

///|
/// Lookup posting list for key
fn bptree_lookup(tree : BPTree, key : Bytes) -> Array[@types.VectorId]? {
  let leaf_idx = find_leaf(tree, key)
  match tree.nodes[leaf_idx] {
    Leaf(entries, _) => {
      let entry_idx = find_entry(entries, key)
      if entry_idx >= 0 {
        Some(entries[entry_idx].value)
      } else {
        None
      }
    }
    _ => None
  }
}

///|
/// Range query in B+ tree
fn bptree_range(
  tree : BPTree,
  lo : Bytes?,
  hi : Bytes?,
  range : @types.NumericRange,
) -> Array[@types.VectorId] {
  let result : Array[@types.VectorId] = []
  let mut current = match lo {
    Some(start_key) => find_leaf(tree, start_key)
    None => {
      let mut leftmost = tree.root_idx
      while true {
        match tree.nodes[leftmost] {
          Leaf(_, _) => break
          Branch(entries, rightmost) =>
            leftmost = if entries.is_empty() {
              rightmost
            } else {
              entries[0].child_idx
            }
        }
      }
      leftmost
    }
  }
  // Scan through linked leaves
  while current >= 0 {
    match tree.nodes[current] {
      Leaf(entries, next) => {
        for entry in entries {
          // Check lower bound
          match lo {
            Some(l) => if bytes_compare(entry.key, l) < 0 { continue }
            None => ()
          }
          // Check upper bound
          match hi {
            Some(h) => if bytes_compare(entry.key, h) > 0 { break }
            None => ()
          }
          // Extract numeric value and check range
          if entry.key.length() > 1 && entry.key[0] == b'\x02' {
            // Int type
            let mut u = 0UL
            for i in 1..<9 {
              u = (u << 8) | entry.key[i].to_uint64()
            }
            u = u ^ 0x8000000000000000UL
            let n = u.reinterpret_as_int64().to_double()
            if range.matches(n) {
              for id in entry.value {
                result.push(id)
              }
            }
          } else if entry.key.length() > 1 && entry.key[0] == b'\x03' {
            // Float type
            let mut bits = 0UL
            for i in 1..<9 {
              bits = (bits << 8) | entry.key[i].to_uint64()
            }
            // Reverse the order-preserving transformation
            bits = if (bits & 0x8000000000000000UL) != 0UL {
              bits ^ 0x8000000000000000UL
            } else {
              bits ^ 0xFFFFFFFFFFFFFFFFUL
            }
            let f = bits.reinterpret_as_double()
            if range.matches(f) {
              for id in entry.value {
                result.push(id)
              }
            }
          }
        }
        current = next
      }
      _ => break
    }
  }
  result
}

///|
/// Create a new BPTreeAttrIndex
pub fn BPTreeAttrIndex::new(order? : Int = 128) -> BPTreeAttrIndex {
  { field_trees: {}, exists_trees: {}, data: {}, order }
}

///|
/// Get or create field tree
fn get_or_create_field_tree(index : BPTreeAttrIndex, field : String) -> BPTree {
  match index.field_trees.get(field) {
    Some(tree) => tree
    None => {
      let tree = BPTree::new(index.order)
      index.field_trees.set(field, tree)
      tree
    }
  }
}

///|
/// Get or create exists tree
fn get_or_create_exists_tree(index : BPTreeAttrIndex, field : String) -> BPTree {
  match index.exists_trees.get(field) {
    Some(tree) => tree
    None => {
      let tree = BPTree::new(index.order)
      index.exists_trees.set(field, tree)
      tree
    }
  }
}

///|
/// Set attributes for an ID
pub fn BPTreeAttrIndex::set_attrs(
  self : BPTreeAttrIndex,
  id : @types.VectorId,
  attrs : @types.Attrs,
) -> Unit {
  // Remove old attrs from trees
  match self.data.get(id) {
    None => ()
    Some(old_attrs) =>
      for_each_attr(old_attrs, fn(key, value) {
        // Remove from field tree
        match self.field_trees.get(key) {
          None => ()
          Some(tree) => bptree_remove(tree, encode_attr_value(value), id)
        }
        // Remove from exists tree
        match self.exists_trees.get(key) {
          None => ()
          Some(tree) => {
            let exists_key = make_exists_key()
            bptree_remove(tree, exists_key, id)
          }
        }
      })
  }
  // Add new attrs to trees
  for_each_attr(attrs, fn(key, value) {
    // Add to field tree
    let field_tree = get_or_create_field_tree(self, key)
    bptree_insert(field_tree, encode_attr_value(value), id)
    // Add to exists tree
    if attr_value_counts_as_exists(value) {
      let exists_tree = get_or_create_exists_tree(self, key)
      let exists_key = make_exists_key()
      bptree_insert(exists_tree, exists_key, id)
    }
  })
  // Store attrs (defensive copy to prevent external mutation)
  self.data.set(id, attrs.copy())
}

///|
/// Get attributes for an ID
pub fn BPTreeAttrIndex::get_attrs(
  self : BPTreeAttrIndex,
  id : @types.VectorId,
) -> @types.Attrs? {
  self.data.get(id)
}

///|
/// Remove an ID from the index
pub fn BPTreeAttrIndex::remove_id(
  self : BPTreeAttrIndex,
  id : @types.VectorId,
) -> Unit {
  match self.data.get(id) {
    None => ()
    Some(old_attrs) => {
      for_each_attr(old_attrs, fn(key, value) {
        match self.field_trees.get(key) {
          None => ()
          Some(tree) => bptree_remove(tree, encode_attr_value(value), id)
        }
        match self.exists_trees.get(key) {
          None => ()
          Some(tree) => {
            let exists_key = make_exists_key()
            bptree_remove(tree, exists_key, id)
          }
        }
      })
      self.data.remove(id)
    }
  }
}

///|
/// Find IDs where key equals value
pub fn BPTreeAttrIndex::eq(
  self : BPTreeAttrIndex,
  key : String,
  value : @types.AttrValue,
) -> Array[@types.VectorId] {
  match self.field_trees.get(key) {
    None => []
    Some(tree) => {
      let encoded = encode_attr_value(value)
      match bptree_lookup(tree, encoded) {
        None => []
        Some(list) => list.copy()
      }
    }
  }
}

///|
/// Find IDs where key exists
pub fn BPTreeAttrIndex::exists(
  self : BPTreeAttrIndex,
  key : String,
) -> Array[@types.VectorId] {
  match self.exists_trees.get(key) {
    None => []
    Some(tree) => {
      let exists_key = make_exists_key()
      match bptree_lookup(tree, exists_key) {
        None => []
        Some(list) => list.copy()
      }
    }
  }
}

///|
/// Find IDs matching a numeric range
pub fn BPTreeAttrIndex::range(
  self : BPTreeAttrIndex,
  key : String,
  range : @types.NumericRange,
) -> Array[@types.VectorId]? {
  match self.field_trees.get(key) {
    None => Some([])
    Some(tree) => {
      let list = bptree_range(tree, None, None, range)
      Some(list.copy())
    }
  }
}

///|
/// Serialize a B+ tree attribute index, including node graphs and stored attrs
pub fn BPTreeAttrIndex::serialize(self : BPTreeAttrIndex) -> Bytes {
  let w = @binary.BinaryWriter::new()
  // v3 = length-prefixed VectorId wire bytes (current).
  // v2 also meant "length-prefixed" in this codebase but trkbt10/vcdb <=
  // 0.3.0 shipped a different "v2" layout (1-byte tag + fixed-size id).
  // Bumping to v3 here keeps the two encodings under distinct labels so
  // readers can route without ambiguous peek-based detection.
  w.push_u32(3U)
  w.push_u32(self.order.reinterpret_as_uint())
  let data_count = self.data.length()
  w.push_u32(data_count.reinterpret_as_uint())
  for id_val, attrs in self.data {
    let wire = id_val.to_wire_bytes()
    w.push_u32(wire.length().reinterpret_as_uint())
    w.push_bytes(wire)
    write_attrs(w, attrs)
  }
  let field_tree_count = self.field_trees.length()
  w.push_u32(field_tree_count.reinterpret_as_uint())
  for field, tree in self.field_trees {
    write_string(w, field)
    write_prefixed_bytes(w, serialize_bptree(tree))
  }
  let exists_tree_count = self.exists_trees.length()
  w.push_u32(exists_tree_count.reinterpret_as_uint())
  for field, tree in self.exists_trees {
    write_string(w, field)
    write_prefixed_bytes(w, serialize_bptree(tree))
  }
  w.concat()
}

///|
/// Deserialize a B+ tree attribute index from serialized bytes.
///
/// Three on-disk layouts are supported (see `IdFormat` for the history).
/// The legacy v2-tag-byte layout shipped by trkbt10/vcdb <= 0.3.0 is
/// distinguished from the current length-prefixed layout by peeking the
/// first byte of the first VectorId — the two encodings never share a
/// possible first byte, so the choice is unambiguous when data exists.
/// Empty indices default to the current layout.
pub fn BPTreeAttrIndex::deserialize(data : Bytes) -> BPTreeAttrIndex {
  let r = @binary.BinaryReader::new(data)
  let version = r.read_u32()
  let order = r.read_u32().reinterpret_as_int()
  let data_count = r.read_u32().reinterpret_as_int()
  let peek = if data_count > 0 { Some(r.peek_byte()) } else { None }
  let fmt = detect_id_format(version, peek)
  let data_map : Map[@types.VectorId, @types.Attrs] = {}
  for _ in 0.. data_map.set(id, read_attrs(r))
      None => {
        // Unknown id encoding — keep the stream aligned by consuming
        // the trailing attrs even though we can't index them.
        let _ = read_attrs(r)
      }
    }
  }
  let field_trees : Map[String, BPTree] = {}
  let field_tree_count = r.read_u32().reinterpret_as_int()
  for _ in 0.. Int {
  self.data.length()
}

///|
/// Return every indexed ID (from the data map).
pub fn BPTreeAttrIndex::all_ids(
  self : BPTreeAttrIndex,
) -> Array[@types.VectorId] {
  let result : Array[@types.VectorId] = []
  for id_val, _ in self.data {
    result.push(id_val)
  }
  result
}