# Миграция из armour (fjall) в armdb

## Обзор

**armour** — обёртка над [fjall](https://github.com/fjall-rs/fjall) (LSM-tree с value log).
**armdb** — собственное хранилище (Sharded Bitcask, append-only log + in-memory index).

| | armour (fjall) | armdb |
|--|:---:|:---:|
| Движок | fjall (LSM-tree) | Sharded Bitcask |
| I/O | mmap / buffered | `pread` + `pwrite` (io_uring — opt-in через `Config::io_backend`) |
| Запись | LSM memtable → flush → compaction | append to shard log |
| Чтение | bloom filter → block cache → disk | in-memory index (SkipList / HashMap) |
| Concurrency | Optimistic Transactions (OCC) | shard mutex |
| Коллекции | TypedTree | TypedTree, TypedMap, ZeroTree, ZeroMap, ConstTree, ConstMap, VarTree, VarMap |

---

## Типы коллекций

### armour

Единственный тип — `TypedTree<Item>` (и `TxTree<Item>` для транзакционного доступа).
Все данные хранятся как сериализованные байты в fjall keyspace.

### armdb

| Тип | Индекс | Значение | Ordered | I/O на чтение |
|-----|:------:|:--------:|:-------:|:--------------:|
| TypedTree | SkipList | `T` in-memory | да | нет |
| TypedMap | HashMap | `T` in-memory | нет | нет |
| ZeroTree | SkipList | `T` zerocopy | да | нет |
| ZeroMap | HashMap | `T` zerocopy | нет | нет |

**Когда что использовать:**
- **TypedTree** — сложные типы (`String`, `Vec`) + нужен порядок / prefix scan
- **TypedMap** — сложные типы + только point lookups (O(1))
- **ZeroTree** — `#[repr(C)]` структуры + порядок
- **ZeroMap** — `#[repr(C)]` структуры + point lookups

---

## Определение типа

### armour: `Record` trait

```rust
pub trait Record: Sized + GetType + 'static {
    type SelfId: Cid + Debug;
    type Value: Debug + Clone;  // обычно Vec<u8> или fjall::Slice
    const NAME: &'static str;
    const VERSION: u16 = 0;
    const MIGRATIONS: &'static [Migration<Self, Self::Value>] = &[];

    fn deser(bytes: &Self::Value) -> Self;
    fn ser(&self) -> Self::Value;
    fn deser_key(key: &[u8]) -> Self::SelfId;
}

// Макросы:
rapira_record!(User, UserID, Vec<u8>, "users");
zerocopy_record!(Counters, CounterID, Vec<u8>, "counters");
```

### armdb: `CollectionMeta` trait + `Codec`

```rust
pub trait CollectionMeta: GetType {
    type SelfId: Key;
    const NAME: &'static str;
    const VERSION: u16 = 0;
}

pub trait Codec<T>: Send + Sync {
    fn encode_to(&self, value: &T, buf: &mut Vec<u8>);
    fn decode_from(&self, bytes: &[u8]) -> DbResult<T>;
}
```

Ключевое отличие: в armdb сериализация отделена от типа. `Codec` — внешний параметр
(`RapiraCodec`, `BitcodeCodec`, custom). ZeroTree/ZeroMap не используют кодек вовсе
(zerocopy transmute).

---

## Открытие коллекций

### armour

```rust
// Non-transactional
let db = Db::new("data/");
let tree = TypedTree::<User>::open(&db, None);

// Transactional (с CAS/update)
let db = TxDb::new("data/");
let tree = TxTree::<User>::open(&db, None);

// С миграциями, хуками, индексами
let tree = TxTree::<User>::builder(&db, None)
    .add_update_fn(Box::new(|id, change| { /* ... */ }))
    .add_index(my_index)
    .build();
```

### armdb

```rust
let db = Db::open("data/")?;

// TypedTree с миграциями
let tree = db.open_typed_tree::<User, RapiraCodec>(
    Config::balanced().build(),
    &migrations,
)?;

// TypedTree с хуком
let tree = db.open_typed_tree_hooked::<User, RapiraCodec, MyHook>(
    Config::balanced().build(),
    my_hook,
    &migrations,
)?;

// ZeroTree
let tree = db.open_zero_tree::<Counters, { size_of::<Counters>() }>(
    Config::balanced().build(),
    &[],
)?;

// TypedMap (без порядка, O(1) lookup)
let map = db.open_typed_map::<Session, RapiraCodec>(
    Config::balanced().build(),
    &[],
)?;
```

`Db` автоматически:
- Управляет путями коллекций (`{path}/{name}:v{version}`)
- Запускает миграции при открытии
- Регистрирует коллекции для фонового compaction
- Сохраняет метаинформацию в `db.info`

---

## CRUD операции

### Сравнение API

| Операция | armour TypedTree | armour TxTree | armdb TypedTree/Map |
|----------|:---:|:---:|:---:|
| get | `get(id) -> Option<Item>` | `get(id) -> Option<Item>` | `get(key) -> Option<TypedRef<T>>` |
| put/upsert | `upsert(id, &item)` | `upsert(id, &item) -> Option<Item>` | `put(key, value) -> Option<TypedRef<T>>` |
| insert | `insert(id, &item)` | — | `insert(key, value) -> DbResult<()>` |
| delete | `remove(id)` | `remove(id) -> Option<Item>` | `delete(key) -> Option<TypedRef<T>>` |
| soft delete | `soft_remove(id)` | `soft_remove(id)` | — |
| take | `take(id) -> Option<Item>` | — | — |
| batch | `apply_batch(iter)` | `apply_batch(iter)` | — |

### Ключевые отличия

1. **Возврат значений.** armdb возвращает `TypedRef<T>` — guard-protected ссылку без `T: Clone`.
   armour возвращает `Option<Item>` (owned, требует десериализацию).

2. **Ownership.** armdb `put(key, value)` принимает `T` по значению (moved in).
   armour `upsert(id, &item)` принимает `&Item` и сериализует.

3. **Soft delete.** В armdb отсутствует. `delete()` — единственный вариант удаления.

4. **ZeroTree/ZeroMap.** Принимают `&T` (reference), возвращают `T` (copy).

---

## CAS и Update

### armour: только TxTree

CAS реализован через optimistic transactions fjall с retry-циклом:

```rust
// upsert_fn — closure может вызываться несколько раз при конфликте
tree.upsert_fn(id, |old: Option<Item>| -> (Option<Item>, Ret) {
    match old {
        Some(item) => (Some(modify(item)), ret),
        None => (Some(create()), ret),
    }
});

// update_fn — один вызов, ошибка при конфликте
tree.update_fn(id, |item: Item| -> DbResult<Item> {
    Ok(modify(item))
});
```

Нет явного `cas(key, expected, new)`. CAS-семантика достигается через `upsert_fn` с проверкой
внутри closure. `T: Clone` обязателен для retry.

### armdb: встроено во все коллекции

```rust
// CAS — атомарная проверка + замена (T: PartialEq)
tree.cas(&key, &expected_value, new_value)?;
// Ошибки: KeyNotFound, CasMismatch

// Update — closure вызывается ровно один раз под shard lock
tree.update(&key, |old: &T| -> T {
    T { count: old.count + 1, ..old.clone() }
})?;
// Возвращает TypedRef на новое значение

// fetch_update — то же, но возвращает TypedRef на старое значение
tree.fetch_update(&key, |old| modify(old))?;
```

| | armour (TxTree) | armdb |
|--|:---:|:---:|
| CAS | через closure + OCC retry | `cas(key, expected, new)` |
| Update | `upsert_fn` / `update_fn` | `update(key, fn)` / `fetch_update(key, fn)` |
| Retry | автоматический (loop) | не нужен (shard mutex) |
| Clone required | да (для retry) | нет |
| Доступен в | TxTree | все 8 типов коллекций |

### atomic() — shard-level multi-key

В armdb есть `atomic()` для нескольких операций под одним shard lock:

```rust
tree.atomic(&shard_key, |shard| {
    let old = shard.get(&key1);
    shard.put(&key2, new_value)?;
    shard.delete(&key3)?;
    Ok(())
})?;
```

Хуки **не** вызываются внутри `atomic()` блока.

---

## Хуки

### armour: `EventHandlers` (dynamic dispatch)

```rust
pub struct EventHandlers<Item: Record> {
    pub on_upsert: Option<Box<dyn Fn(&SelfId, Change<Item>) + Send + Sync>>,
    pub on_remove: Option<Box<dyn Fn(&SelfId, &Item) + Send + Sync>>,
    pub on_id_change: Option<Box<dyn Fn(&Item, &SelfId, &SelfId) + Send + Sync>>,
}

// Регистрация через builder:
TxTree::builder(&db, None)
    .add_update_fn(Box::new(|id, change| {
        // change.old: Option<&Item>, change.new: &Item
    }))
    .add_removed_fn(Box::new(|id, item| { /* ... */ }))
    .build();
```

### armdb: `TypedWriteHook` trait (static dispatch)

```rust
pub trait TypedWriteHook<K: Key, T>: Send + Sync {
    const NEEDS_OLD_VALUE: bool = true;
    const NEEDS_INIT: bool = false;

    fn on_write(&self, key: &K, old: Option<&T>, new: Option<&T>);
    fn on_init(&self, _key: &K, _value: &T) {}
}

// Реализация:
impl TypedWriteHook<UserId, User> for MyIndex {
    const NEEDS_INIT: bool = true;

    fn on_write(&self, key: &UserId, old: Option<&User>, new: Option<&User>) {
        // update secondary index
    }

    fn on_init(&self, key: &UserId, value: &User) {
        // rebuild index on open
    }
}

// Открытие:
db.open_typed_tree_hooked::<User, RapiraCodec, MyIndex>(config, my_index, &[]);
```

| | armour | armdb |
|--|:---:|:---:|
| Dispatch | dynamic (`Box<dyn Fn>`) | static (monomorphized) |
| Overhead при NoHook | проверка `Option` | zero (компилятор убирает) |
| Отдельные события | upsert, remove, id_change | единый `on_write(key, old, new)` |
| Init callback | нет | `on_init` (при открытии / миграции) |
| Типизация | `&Item` | `&T` |

---

## Миграции

### armour: `MigrationRes` (ключ + значение)

```rust
pub enum MigrationRes<Item: Record> {
    Unchanged(Item::SelfId, Item),
    Changed {
        migration_type: MigrationType, // Key, Value, Entry
        new_key: Item::SelfId,
        new_value: Item,
    },
    Deleted,
}

pub type MigrationFn<Item, Val> = fn(&[u8], &Val) -> MigrationRes<Item>;

// В Record trait:
const VERSION: u16 = 1;
const MIGRATIONS: &'static [Migration<Self, Self::Value>] = &[
    (0, |key_bytes, value_bytes| {
        let old = OldUser::deser(value_bytes);
        MigrationRes::Changed {
            migration_type: MigrationType::Value,
            new_key: UserID::from_bytes(key_bytes).unwrap(),
            new_value: User { name: old.name, email: String::new() },
        }
    }),
];
```

Миграция работает с **сырыми байтами** (`&[u8]`, `&Val`). Может менять и ключ, и значение.
Создаёт **новый keyspace** для целевой версии и копирует данные.

### armdb: `MigrateAction` (только значение)

```rust
pub enum MigrateAction<V> {
    Keep,
    Update(V),
    Delete,
}

pub type TypedMigrationFn<K, T> = fn(&K, &T) -> MigrateAction<T>;
pub type TypedMigration<K, T> = (u16, TypedMigrationFn<K, T>);

// При открытии:
let migrations: &[TypedMigration<UserId, User>] = &[
    (0, |_key, old| {
        MigrateAction::Update(User {
            name: old.name.clone(),
            email: String::new(),
        })
    }),
];

db.open_typed_tree::<User, RapiraCodec>(config, migrations)?;
```

Миграция работает с **типизированными значениями** (`&K`, `&T`). Может только обновить
или удалить значение. Ключ неизменяем. Миграция выполняется **in-place**.

| | armour | armdb |
|--|:---:|:---:|
| Входные данные | `&[u8]`, `&Value` (сырые) | `&K`, `&T` (типизированные) |
| Может менять ключ | да | нет |
| Результат | Unchanged / Changed / Deleted | Keep / Update / Delete |
| Стратегия | копирование в новый keyspace | in-place мутация |
| on_init после миграции | нет | да (для каждого Keep/Update) |

---

## Итерация

### armour

```rust
// Полный обход
for (key, value) in tree.iter() { /* key: Slice, value: Slice */ }

// Prefix scan
for entry in tree.scan_prefix(&prefix) { /* -> (Slice, Slice) */ }

// Range
for entry in tree.range_iter(start..end) { /* -> (Slice, Slice) */ }

// Десериализация вручную или через ReadTree helpers
let items: Vec<Item> = tree.try_scan_prefix(&prefix)?;
```

Итератор возвращает сырые байты. Десериализация — на стороне вызывающего или через
типизированные обёртки `ReadTree<Item>`.

### armdb

```rust
// TypedTree / ZeroTree:
for (key, value) in tree.iter() { /* key: K, value: &T */ }
for (key, value) in tree.prefix_iter(&prefix) { /* DoubleEndedIterator */ }
for (key, value) in tree.range(&start, &end) { }

// Reverse
let latest = tree.prefix_iter(&prefix).rev().take(10);

// TypedMap / ZeroMap: итерации нет (HashMap)
```

Итератор возвращает типизированные значения. Zero I/O (всё в памяти).
Map-типы не поддерживают итерацию.

---

## Что нет в armdb

| Возможность | armour | armdb | Альтернатива в armdb |
|-------------|:---:|:---:|:---:|
| Optimistic transactions (OCC) | TxTree | — | `cas()` / `update()` / `atomic()` |
| Soft delete | `soft_remove()` | — | `delete()` + отдельная коллекция |
| Встроенные secondary indexes | до 8 IndexMap | — | `TypedWriteHook` с `on_init` + `on_write` |
| Parallel iteration (rayon) | `par_iter()` | — | — |
| Hash-based replication validation | hashpoints | — | — |

## Что появилось в armdb

| Возможность | armdb | armour |
|-------------|:---:|:---:|
| Типы коллекций | 8 (Tree/Map × Typed/Zero/Const/Var) | 1 (TypedTree) |
| CAS / Update | все коллекции | только TxTree |
| `atomic()` shard-level | да | — |
| Background compaction | Compactor (configurable interval) | fjall managed |
| Репликация (log shipping) | ReplicationServer / ReplicationClient | ChangeEvent + custom |
| Шифрование (AES-256-GCM) | feature `encryption` | — |
| Zero-overhead hooks | compile-time monomorphization | dynamic dispatch |
| ZeroTree / ZeroMap | zerocopy, zero-cost transmute | — |

---

## Миграция типов (crate types)

Самый частый первый шаг — мигрировать crate с доменными моделями,
который определяет `Record`/`Cid` для всех сущностей.
Ниже — практическое руководство по этому этапу.

### Зависимости

```toml
# Cargo.toml (workspace)
[workspace.dependencies]
armdb = { version = "0.1", features = ["replication"] }
armour-core = "0.1"

# Cargo.toml (types crate)
[dependencies]
armdb.workspace = true
armour-core.workspace = true
```

Если проект ранее зависел от `armour` и вы перешли на `armdb` + `armour-core`,
убедитесь что `armour-derive` (proc-macro для `GetType`) корректно разрешается.
Проверьте какие пути генерирует derive:
- Старые версии armour-derive → `::armour::dyn_types::*` (нужен crate `armour` в scope)
- Новые версии armour-derive → `::armour_core::dyn_types::*` (нужен crate `armour_core`)

> **Важно:** Убедитесь что `armour_core` одной и той же версии во всех crate
> вашего workspace. Разные версии armour_core = разные трейты (`GetType`, `Key`) =
> ошибки компиляции вида "implements similarly named trait".
> Проверяйте: `cargo tree -d | grep armour-core`

### Маппинг импортов

```
armour::GetType          → armour_core::GetType
armour::Fuid             → armour_core::Fuid
armour::Id64             → armour_core::Id64
armour::Typ              → armour_core::Typ
armour::KeyScheme        → armour_core::KeyScheme
armour::KeyType          → armour_core::KeyType
armour::const_hasher     → armour_core::const_hasher
armour::hasher           → armour_core::hasher

armour::Record           → armdb::CollectionMeta
armour::Cid              → armdb::Key
armour::rapira_record!   → ручной impl CollectionMeta (см. ниже)
armour::zerocopy_cid!    → armdb::impl_key_zerocopy! (см. ниже)

armour::Slice            → удалить (не нужен)
armour::InlineArray      → удалить (не нужен)
armour::Entry<T>         → удалить, заменить на tuple (Key, Value)
armour::migrations::*    → удалить из types, миграции определяются в db crate
```

### `rapira_record!` → `CollectionMeta`

```rust
// БЫЛО:
rapira_record!(User, UserID, Slice, "users");

// СТАЛО:
impl armdb::CollectionMeta for User {
    type SelfId = UserID;
    const NAME: &'static str = "users";
}
```

`CollectionMeta` содержит только метаданные. Сериализация (`deser`/`ser`)
больше не часть типа — она задаётся через `Codec` при открытии коллекции в db crate.
`VERSION` по умолчанию 0, указывайте только если есть миграции.

### `Cid` → `Key`

Для типов с `FromBytes + IntoBytes + Immutable + Copy` — используйте `impl_key_zerocopy!`:

```rust
// БЫЛО:
zerocopy_cid!(LinkId, [KeyType::Fuid], ProfileId::GROUP_BITS, link_id);

// СТАЛО:
armdb::impl_key_zerocopy!(LinkId, KeyScheme::Typed(&[KeyType::Fuid]));
```

```rust
// БЫЛО: ручной impl Cid с zerocopy::transmute
impl Cid for FollowerKey {
    type B = [u8; 16];
    const TY: KeyScheme = KeyScheme::Typed(&[KeyType::Fuid, KeyType::Fuid]);
    const GROUP_BITS: u32 = ProfileId::GROUP_BITS;
    fn encode(&self) -> Self::B { zerocopy::transmute!(*self) }
    fn decode(bytes: &Self::B) -> Result<Self> { Ok(zerocopy::transmute!(*bytes)) }
    fn group_id(&self) -> u32 { self.0.group_id() }
}

// СТАЛО:
armdb::impl_key_zerocopy!(FollowerKey, KeyScheme::Typed(&[KeyType::Fuid, KeyType::Fuid]));
```

#### Ключи с нестандартным encoding

Если ключ использовал big-endian порядок для сортировки (например, `day.to_be_bytes()`),
нельзя просто использовать `impl_key_zerocopy!` — zerocopy будет использовать native endian.

Решения:
- (Приоритетный) Использовать `zerocopy::big_endian::U64` для полей, требующих BE порядка
- Хранить byte-array внутри (`#[repr(transparent)] struct Key([u8; N])`) + accessor-методы

#### Hash-based ключи

Если ключ — хеш (`u64` от xxh3 и т.п.), добавьте `#[repr(transparent)]` + zerocopy derives:

```rust
// БЫЛО:
pub struct UrlHash(pub u64);
impl Cid for UrlHash { ... }

// СТАЛО:
#[derive(Clone, Copy, FromBytes, IntoBytes, Immutable)]
#[repr(transparent)]
pub struct UrlHash(pub u64);

impl_key!(UrlHash, KeyScheme::Typed(&[KeyType::Array(8)]));
```

#### Padding при `#[repr(C)]`

Zerocopy не допускает padding в структурах с `IntoBytes`. Если в ключе есть поле с
alignment > 1 (например `Fuid` содержит `u64`, alignment = 8), то `#[repr(C)]` может
добавить padding:

```rust
#[repr(C)]
struct Key {
    profile_id: ProfileId, // size 8, align 8
    day: u32,              // size 4, align 4
}
// Итого 12 bytes, struct align 8 → padded до 16 → 4 bytes padding → IntoBytes fails
```

Решение: использовать типы с одинаковым size (`u64`) или `#[repr(transparent)]` с `[u8; N]` внутри (см. пример выше).

### `Entry<T>` → tuple

```rust
// БЫЛО:
pub type ProfileEntry = armour::Entry<Profile>;
let Entry { key, val } = entry;

// СТАЛО:
pub type ProfileEntry = (ProfileId, Profile);
let (key, val) = entry;
```

### Record с миграциями → CollectionMeta

```rust
// БЫЛО:
impl Record for User {
    type SelfId = UserID;
    type Value = Slice;
    const NAME: &'static str = "users";
    const VERSION: u16 = 1;
    const MIGRATIONS: &'static [Migration<Self, Self::Value>] = &[(0, migrate_user_v0)];
    fn deser(bytes: &Self::Value) -> Self { rapira::deserialize(bytes).unwrap() }
    fn ser(&self) -> Slice { rapira::serialize(self).into() }
}

// СТАЛО (в types crate):
impl armdb::CollectionMeta for User {
    type SelfId = UserID;
    const NAME: &'static str = "users";
}

// Миграции определяются при открытии коллекции (в db crate):
let migrations: &[TypedMigration<UserID, User>] = &[
    (0, |_key, old| MigrateAction::Update(migrate_user(old))),
];
db.open_typed_tree::<User, RapiraCodec>(config, migrations)?;
```

### GROUP_BITS

В armour: `Cid::GROUP_BITS` задавался на уровне ключа (`Fuid = 10`, `Id64 = 16`).
В armdb: `Key::GROUP_BITS` по умолчанию `0` для всех типов.

Шардирование в armdb настраивается через `Config` при открытии коллекции,
а не через GROUP_BITS ключа. При миграции типов просто не указывайте `group_bits`.

### Внешние типы как Key (orphan rule)

Если вам нужен внешний тип как ключ коллекции,
нельзя реализовать `armdb::Key` для него в вашем crate (orphan rule).
Используйте newtype-обёртку:

```rust
#[derive(Clone, Copy, FromBytes, IntoBytes, Immutable)]
#[repr(transparent)]
pub struct PubkeyKey(pub [u8; 32]);

impl PubkeyKey {
    pub fn from_pubkey(pk: &Pubkey) -> Self { Self(pk.to_bytes()) }
    pub fn to_pubkey(&self) -> Pubkey { Pubkey::new_from_array(self.0) }
}

impl_key!(PubkeyKey, KeyScheme::Typed(&[KeyType::Array(32)]));
```

### `GetType` для внешних типов

Если struct/enum с `#[derive(GetType)]` содержит поля внешних типов,
не реализующих `GetType`, есть варианты:

**a) Обновить версию зависимости** до той, для которой `armour_core` уже реализует `GetType`:
```toml
# armour_core реализует GetType для solana-pubkey v4
solana-pubkey = { version = "4.0", features = ["serde", "curve25519"] }
```
```toml
# и включите фичу в armour-core:
armour-core = { version = "0.1", features = ["solana"] }
```

Это **рекомендуемый** подход — не требует обходных путей.

**b) `#[get_type(unimplemented)]`** на полях struct (если обновить версию нельзя):
```rust
#[derive(GetType)]
pub struct MyStruct {
    #[get_type(unimplemented)]
    pub owner: Pubkey,
}
```

> **Ограничение:** `#[get_type(unimplemented)]` работает только для полей struct,
> но НЕ для полей enum-вариантов. Для enum с внешними типами в полях —
> используйте подход (a), либо напишите `impl GetType` вручную.

### Закомментированные / неиспользуемые модули

Если в проекте есть закомментированные модули (`// pub mod old_feature;`),
не тратьте время на их миграцию — они всё равно не компилируются.
Мигрируйте только активный код.

---

## Миграция DB-слоя (crate db)

Второй шаг — мигрировать crate, который открывает коллекции и работает с данными.

### Db инициализация

```rust
// БЫЛО:
use armour::logdb::db::Db;
let db = Db::new(path.join("db"));

// СТАЛО:
use armdb::armour::Db;
let db = Db::open(path.join("db")).expect("db open");
```

`Db::open` возвращает `Result`. `Db` не реализует `Clone` — оберните в `Arc<Db>`.

### Открытие коллекций

```rust
// БЫЛО:
pub type UsersTree = TypedTree<User>;
let tree = UsersTree::open(db, None);

// СТАЛО:
pub type UsersTree = Arc<armdb::TypedTree<UserID, User, armdb::RapiraCodec>>;
let tree = db.open_typed_tree::<User, RapiraCodec>(Config::balanced().build(), &[])
    .expect("open User");
```

armdb TypedTree параметризован `<K, T, C>` (ключ, значение, кодек) и обёрнут в `Arc`.

### CRUD: ключевые отличия

```rust
// armour                           // armdb
tree.get(id)     -> Option<T>       tree.get(&id)     -> Option<TypedRef<T>>
tree.get_or_err(id) -> T            tree.get_or_err(&id) -> TypedRef<T>
tree.upsert(id, &val) -> ()         tree.put(&id, val) -> Option<TypedRef<T>>
tree.insert(id, &val)               tree.insert(id, val)
tree.remove(id)                     tree.delete(&id)
tree.iter()      -> Entry<T>        tree.iter()      -> (K, &T)
tree.scan_prefix(b)                 tree.prefix_iter(b)
tree.range_iter(range)              tree.range_bounds(start, end)
```

**TypedRef** — guard-protected ссылка (deref к `&T`). Для owned значения:
```rust
let user: User = (*tree.get(&id).unwrap()).clone();
// или
let user = tree.get(&id).map(|r| (*r).clone());
```

**put** принимает значение по ownership (move), не по ссылке:
```rust
tree.put(&id, profile.clone())?;  // clone если нужно сохранить оригинал
```

**get** возвращает `Option`, не `Result`. `?` не нужен:
```rust
// БЫЛО:  let user = self.get(id)?;
// СТАЛО: let user = tree.get(&id).map(|r| (*r).clone());
```

### Key requires Ord

`TypedTree` (SkipList) требует `Key + Ord`. Добавьте derives на ключевые типы:
```rust
#[derive(PartialEq, Eq, PartialOrd, Ord, FromBytes, IntoBytes, Immutable)]
pub struct MyKey { ... }
```

### Порядок итерации: `reversed: true`

> **Критически важно.** `Config::balanced().build()` (и остальные workload-пresets) устанавливает `reversed: true`.
> Это значит `iter()` / `prefix_iter()` идут **от новых к старым** (DESC).
> В armour порядок был ASC (от старых к новым).

Если код использовал `.rev()` для получения "новые первые" — **уберите `.rev()`**,
т.к. порядок по умолчанию уже DESC:
```rust
// БЫЛО (armour, ASC по умолчанию):
tree.iter().rev().take(10)  // последние 10

// СТАЛО (armdb, DESC по умолчанию):
tree.iter().take(10)        // последние 10
```

Если нужен ASC порядок — используйте `.rev()` или `Config::balanced().reversed(false).build()`.

### builder + build_with_handler → TypedWriteHook

armour использовал closure для построения индексов при загрузке:
```rust
let tree = TypedTree::builder(db, None)
    .build_with_handler(move |&id, val| {
        index.insert_sync(val.field.clone(), id);
    });
```

В armdb — определите struct с `TypedWriteHook`:
```rust
struct MyHook {
    index: Arc<HashIndex<String, MyKey>>,
}

impl armdb::TypedWriteHook<MyKey, MyValue> for MyHook {
    const NEEDS_INIT: bool = true;

    fn on_init(&self, key: &MyKey, val: &MyValue) {
        // Вызывается для каждой записи при открытии коллекции.
        // Заменяет build_with_handler closure.
        let _ = self.index.insert_sync(val.field.clone(), *key);
    }

    fn on_write(&self, key: &MyKey, old: Option<&MyValue>, new: Option<&MyValue>) {
        // Вызывается при каждом put/insert/delete.
        // old=None → insert, new=None → delete, оба Some → update.
        if let Some(old) = old {
            self.index.remove_sync(&old.field);
        }
        if let Some(new) = new {
            let _ = self.index.insert_sync(new.field.clone(), *key);
        }
    }
}

let hook = MyHook { index: index.clone() };
let tree = db.open_typed_tree_hooked::<MyValue, RapiraCodec, MyHook>(
    Config::balanced().build(), hook, &[]
).expect("open MyValue");
```

**Преимущества over `build_with_handler`:**
- `on_write` автоматически обновляет индексы при записи — не нужно дублировать
  логику индексации в каждом методе (insert, update, delete)
- `on_init` заменяет ручной `for (k, v) in tree.iter()` цикл

**Когда `on_write` оставить пустым:**
- Если индекс имеет сложную структуру (например, `HashMap<String, Vec<Id>>`)
  и инкрементальное обновление нетривиально — проще обновлять вручную в методах.
  `on_init` всё равно используйте для загрузки при открытии.

### TxTree → TypedTree

armour TxTree (транзакционный) не имеет аналога в armdb.
Замените на обычный TypedTree + `update()` / `cas()`:

```rust
// БЫЛО:
tree.upsert_fn(key, |old: Option<T>| -> (Option<T>, bool) {
    match old {
        Some(mut item) => { item.count += 1; (Some(item), true) }
        None => (None, false),
    }
});

// СТАЛО:
tree.update(&key, |old: &T| -> T {
    T { count: old.count + 1, ..old.clone() }
})?;
```

### ReadTree → убрать

`ReadTree<T>` из armour не имеет аналога. TypedTree в armdb предоставляет
тот же read API. Уберите `Deref` к `ReadTree`, обращайтесь через `self.tree` напрямую.

### DbError

armdb `DbError` имеет другие варианты, чем armour:
```rust
// armour:  DbError::NotFound
// armdb:   DbError::KeyNotFound, DbError::KeyExists, DbError::CasMismatch
```
