# Lazy Migrations & Performance

## Производительность обхода коллекций

### fjall + rapira (sequential scan, single-threaded)

Путь данных при итерации:
```
fjall LSM-tree → block cache/disk → Guard::into_inner()
  → (key_bytes, value_bytes)
  → Cid::from_bytes(&key)        — O(1) для фиксированных ID
  → rapira::deserialize(value)   — линейный проход по буферу
  → Entry<Item>
```

| Размер записи | Горячий кэш | Холодный старт (NVMe) |
|---------------|-------------|-----------------------|
| ~100 байт     | ~3M записей/сек | ~1.5-2M записей/сек |
| ~500 байт     | ~1.5M записей/сек | ~500K-1M записей/сек |
| ~1 КБ         | ~800K записей/сек | ~300-500K записей/сек |

Скорость десериализации по форматам:

| Формат           | Throughput        | Примечание |
|------------------|-------------------|------------|
| rapira           | ~200-500 МБ/с     | линейный проход, минимум аллокаций |
| zerocopy/bytemuck| zero-copy         | transmute/cast_slice, 0 ns overhead |
| bincode          | ~150-400 МБ/с     | похож на rapira, чуть больше overhead |
| bitcode          | ~300-600 МБ/с     | compact encoding |

### Оценка для 10M записей

**Full scan (compute_hashpoints при старте):**

| Размер записи | Горячий кэш | Холодный старт (NVMe) |
|---------------|-------------|-----------------------|
| ~100 байт (~1 ГБ) | ~3-4 сек | ~5-7 сек |
| ~500 байт (~5 ГБ) | ~7-10 сек | ~10-20 сек |
| ~1 КБ (~10 ГБ) | ~12-18 сек | ~25-40 сек |

**Миграция 10M записей (read + deser + transform + ser + write):**
Примерно 3-5x медленнее чистого scan из-за write amplification в LSM.

| Размер записи | Время (NVMe) |
|---------------|-------------|
| ~100 байт     | ~10-20 сек |
| ~500 байт     | ~30-60 сек |
| ~1 КБ         | ~1-2 мин |

### Сравнение с PostgreSQL (10M × ~500 байт)

| Операция | fjall + rapira | PostgreSQL |
|----------|---------------|------------|
| Full scan | 7-20 сек | 5-15 сек (seq scan) |
| Миграция (transform values) | 30-60 сек | 30-120 сек (UPDATE + VACUUM) |
| Миграция (add field + default) | 30-60 сек (всегда rewrite) | ~0 сек (PG 11+, metadata only) |
| Online миграция | нет (blocking) | да (CONCURRENTLY) |

Основные отличия:
- Postgres умеет "бесплатные" ALTER TABLE ADD COLUMN DEFAULT (metadata only, без rewrite)
- Postgres поддерживает online миграции (CREATE INDEX CONCURRENTLY, backfill)
- fjall быстрее на чистом sequential scan (нет MVCC overhead, tuple headers, TOAST)
- При 100M+ записей разница растёт: fjall scan ~30-60 сек vs Postgres SELECT * ~60-150 сек

---

## Варианты ленивой миграции

### 1. Version byte в значении

Версия хранится как первый байт значения — никаких флагов:

```
[version: u8][payload...]
```

При чтении:
```rust
fn deser(bytes: &Slice) -> Self {
    let version = bytes[0];
    if version == CURRENT_VERSION {
        rapira::deserialize(&bytes[1..])                   // быстрый путь
    } else {
        rapira::deserialize_versioned(&bytes[1..], version) // старый формат
    }
}
```

| Плюсы | Минусы |
|-------|--------|
| +1 байт на запись | Изменение формата всех записей |
| Нет проблем с iter() | Нужно менять rapira_record! макрос |
| Нет 2x storage overhead | Миграция "размазана" навсегда (или нужен write-back) |
| Startup: 0 сек | |

Rapira уже поддерживает `from_slice_versioned(slice, version)` — пробрасывает версию через все вложенные типы.

### 2. Dual keyspace + read-through

Минимальные изменения к текущему коду — уже создаются source/target keyspace в builder.rs:

```rust
// Сейчас (blocking):
if needs_migration {
    run_migration(source, target, migration_fn);  // блокирует старт
}

// Ленивый вариант:
if needs_migration {
    self.pending_source = Some(source);
    spawn_background_migration(source, target, migration_fn);
}

// В get():
fn get(&self, key: &Key) -> Option<Item> {
    self.target.get(key)
        .or_else(|| self.pending_source.as_ref()?.get(key).map(|old| {
            let migrated = migrate(old);
            self.target.insert(key, &migrated);
            migrated
        }))
}
```

БД определяет мигрирована ли запись по наличию в target keyspace — никаких флагов.

| Плюсы | Минусы |
|-------|--------|
| Минимум изменений в коде | 2x storage на время миграции |
| Startup: 0 сек | Два lookup на get() пока идёт миграция |
| Старый формат не меняется | iter() требует merge iterator |

### 3. Background drain (рекомендуемый)

Комбинация dual keyspace + фоновая миграция:

1. Сервер стартует мгновенно с двумя keyspace
2. get() делает read-through с write-back в target
3. Отдельный поток мигрирует записи пачками
4. compute_hashpoints запускается после завершения drain
5. По завершению — source keyspace удаляется

| Плюсы | Минусы |
|-------|--------|
| Startup: 0 сек | 2x storage на время миграции |
| Предсказуемое завершение (~30-60 сек для 10M) | Сложность merge iterator для iter() |
| Нет изменений формата | Нужна синхронизация между drain и read-through |

### Сравнение подходов

| | Version byte | Dual keyspace | Background drain |
|--|-------------|---------------|-----------------|
| Startup time | 0 сек | 0 сек | 0 сек |
| Доп. хранилище | +1 байт/запись | 2x временно | 2x временно |
| Сложность get() | минимальная | два lookup | два lookup |
| Сложность iter() | нулевая | merge iterator | merge iterator |
| Полная миграция | никогда / write-back | по мере обращений | background ~30-60 сек |

### Проблема iter() при dual keyspace

При ленивой миграции с dual keyspace полный обход требует merge iterator:

```rust
fn iter(&self) -> impl Iterator<Item = Entry<Item>> {
    let target_iter = self.target.iter();
    if let Some(source) = &self.pending_source {
        MergeIter::new(target_iter, source.iter(), migration_fn)
    } else {
        target_iter
    }
}
```

Аналогично тому, как fjall внутри делает merge для SST-уровней в LSM-tree.
