# Миграция из armour (fjall) в armdb
## Обзор
**armour** — обёртка над [fjall](https://github.com/fjall-rs/fjall) (LSM-tree с value log).
**armdb** — собственное хранилище (Sharded Bitcask, append-only log + in-memory index).
| Движок | fjall (LSM-tree) | Sharded Bitcask |
| I/O | mmap / buffered | `pread` + `io_uring` |
| Запись | 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
| 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::default(),
&migrations,
)?;
// TypedTree с хуком
let tree = db.open_typed_tree_hooked::<User, RapiraCodec, MyHook>(
Config::default(),
my_hook,
&migrations,
)?;
// ZeroTree
let tree = db.open_zero_tree::<Counters, { size_of::<Counters>() }>(
Config::default(),
&[],
)?;
// TypedMap (без порядка, O(1) lookup)
let map = db.open_typed_map::<Session, RapiraCodec>(
Config::default(),
&[],
)?;
```
`Db` автоматически:
- Управляет путями коллекций (`{path}/{name}:v{version}`)
- Запускает миграции при открытии
- Регистрирует коллекции для фонового compaction
- Сохраняет метаинформацию в `db.info`
---
## CRUD операции
### Сравнение API
| 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
})?;
// Возвращает TypedRef на новое значение
// fetch_update — то же, но возвращает TypedRef на старое значение
tree.fetch_update(&key, |old| modify(old))?;
```
| 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, &[]);
```
| 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**.
| Входные данные | `&[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
| 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
| Типы коллекций | 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::default(), &[])
.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();
// или
```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::default()` устанавливает `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 { reversed: false, .. }`.
### 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::default(), 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),
}
});
// СТАЛО:
})?;
```
### 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
```