use std::ops::Bound;
use crate::document::{Document, Value};
use crate::engine::{KvOp, StorageBackend, WriteTxn};
use crate::error::TalaDbError;
use crate::index::{
CompoundIndexDef, IndexDef, META_COMPOUND_TABLE, META_INDEX_ARRAYS_TABLE, META_INDEXES_TABLE,
META_VERSION_KEY, META_VERSION_TABLE, compound_table_name, docs_table_name,
encode_compound_keys, encode_index_keys, index_table_name, meta_key,
};
pub struct Migration {
pub from_version: u32,
pub to_version: u32,
pub description: &'static str,
pub up: fn(&mut dyn WriteTxn) -> Result<(), TalaDbError>,
}
pub const CURRENT_SCHEMA_VERSION: u32 = 2;
pub const BUILTIN_MIGRATIONS: &[Migration] = &[
Migration {
from_version: 0,
to_version: 1,
description: "re-encode secondary indexes after string null-terminator fix",
up: rebuild_all_secondary_indexes,
},
Migration {
from_version: 1,
to_version: 2,
description: "drop replication tables and rebuild indexes for array containment",
up: migrate_v1_to_v2,
},
];
fn drop_replication_tables(txn: &mut dyn WriteTxn) -> Result<(), TalaDbError> {
let doomed: Vec<String> = txn
.list_tables()?
.into_iter()
.filter(|name| {
crate::index::REMOVED_TABLE_PREFIXES
.iter()
.any(|p| name.starts_with(p))
})
.collect();
for name in doomed {
txn.delete_table(&name)?;
}
Ok(())
}
fn migrate_v1_to_v2(txn: &mut dyn WriteTxn) -> Result<(), TalaDbError> {
drop_replication_tables(txn)?;
rebuild_all_secondary_indexes(txn)
}
pub fn read_version(txn: &dyn crate::engine::ReadTxn) -> Result<u32, TalaDbError> {
match txn.get(META_VERSION_TABLE, META_VERSION_KEY)? {
Some(bytes) => Ok(postcard::from_bytes(&bytes)?),
None => Ok(0),
}
}
fn write_version(txn: &mut dyn WriteTxn, version: u32) -> Result<(), TalaDbError> {
let bytes = postcard::to_allocvec(&version)?;
txn.put(META_VERSION_TABLE, META_VERSION_KEY, &bytes)?;
Ok(())
}
pub fn run_migrations(
backend: &dyn StorageBackend,
migrations: &[Migration],
) -> Result<(), TalaDbError> {
for pair in migrations.windows(2) {
if pair[0].to_version != pair[1].from_version {
return Err(TalaDbError::Migration(format!(
"migration gap: {} -> {} then {} -> {}",
pair[0].from_version, pair[0].to_version, pair[1].from_version, pair[1].to_version,
)));
}
}
let rtxn = backend.begin_read()?;
let mut current_version = read_version(rtxn.as_ref())?;
drop(rtxn);
for migration in migrations {
if migration.from_version < current_version {
continue; }
if migration.from_version != current_version {
return Err(TalaDbError::Migration(format!(
"migration out of order: db is at v{}, migration starts at v{}",
current_version, migration.from_version
)));
}
let mut wtxn = backend.begin_write()?;
(migration.up)(wtxn.as_mut())?;
write_version(wtxn.as_mut(), migration.to_version)?;
wtxn.commit()?;
current_version = migration.to_version;
}
Ok(())
}
fn rebuild_all_secondary_indexes(txn: &mut dyn WriteTxn) -> Result<(), TalaDbError> {
rebuild_secondary_indexes(txn)?;
rebuild_compound_indexes(txn)?;
Ok(())
}
pub fn rebuild_secondary_indexes(txn: &mut dyn WriteTxn) -> Result<(), TalaDbError> {
let meta = txn.range(META_INDEXES_TABLE, Bound::Unbounded, Bound::Unbounded)?;
let mut defs: Vec<IndexDef> = Vec::with_capacity(meta.len());
for (_, v) in meta {
defs.push(postcard::from_bytes(&v)?);
}
for def in &defs {
let idx_table = index_table_name(&def.collection, &def.field);
let old_keys = txn.range(&idx_table, Bound::Unbounded, Bound::Unbounded)?;
let docs_table = docs_table_name(&def.collection);
let docs = txn.range(&docs_table, Bound::Unbounded, Bound::Unbounded)?;
let mut new_keys: Vec<Vec<u8>> = Vec::with_capacity(docs.len());
let mut arrays = 0u64;
for (_, doc_bytes) in docs {
let doc: Document = postcard::from_bytes(&doc_bytes)?;
if let Some(val) = doc.get(&def.field) {
arrays += u64::from(matches!(val, Value::Array(_)));
new_keys.extend(encode_index_keys(val, doc.id));
}
}
let ops: Vec<KvOp<'_>> = old_keys
.iter()
.map(|(k, _)| KvOp::Delete(k.as_slice()))
.chain(new_keys.iter().map(|k| KvOp::Put(k.as_slice(), &[])))
.collect();
txn.apply_batch(&idx_table, &ops)?;
txn.put(
META_INDEX_ARRAYS_TABLE,
meta_key(&def.collection, &def.field).as_bytes(),
&arrays.to_le_bytes(),
)?;
}
Ok(())
}
pub fn rebuild_compound_indexes(txn: &mut dyn WriteTxn) -> Result<(), TalaDbError> {
let meta = txn.range(META_COMPOUND_TABLE, Bound::Unbounded, Bound::Unbounded)?;
let mut defs: Vec<CompoundIndexDef> = Vec::with_capacity(meta.len());
for (_, v) in meta {
defs.push(postcard::from_bytes(&v)?);
}
for def in &defs {
let fields: Vec<&str> = def.fields.iter().map(std::string::String::as_str).collect();
let ctable = compound_table_name(&def.collection, &fields);
let old_keys = txn.range(&ctable, Bound::Unbounded, Bound::Unbounded)?;
let docs_table = docs_table_name(&def.collection);
let docs = txn.range(&docs_table, Bound::Unbounded, Bound::Unbounded)?;
let mut new_keys: Vec<Vec<u8>> = Vec::with_capacity(docs.len());
for (_, doc_bytes) in docs {
let doc: Document = postcard::from_bytes(&doc_bytes)?;
let vals: Option<Vec<&Value>> = def.fields.iter().map(|f| doc.get(f)).collect();
if let Some(v) = vals {
new_keys.extend(encode_compound_keys(&v, doc.id)?);
}
}
let ops: Vec<KvOp<'_>> = old_keys
.iter()
.map(|(k, _)| KvOp::Delete(k.as_slice()))
.chain(new_keys.iter().map(|k| KvOp::Put(k.as_slice(), &[])))
.collect();
txn.apply_batch(&ctable, &ops)?;
}
Ok(())
}