pub const DEFAULT_BATCH_SIZE: u32 = 128;
pub const DEFAULT_WORKERS: usize = 2;
pub const DEFAULT_BULK_INDEXING_THRESHOLD: u64 = 2_000_000;
pub const DEFAULT_INDEXING_THRESHOLD: u64 = 20_000;
pub const DEFAULT_BATCH_DELAY_MS: u64 = 0;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MissingShardKey {
Error,
Skip,
Default(qql_core::ast::ShardKey),
}
impl MissingShardKey {
pub fn parse(raw: &str) -> Result<Self, String> {
let raw = raw.trim();
if raw.eq_ignore_ascii_case("error") {
return Ok(Self::Error);
}
if raw.eq_ignore_ascii_case("skip") {
return Ok(Self::Skip);
}
if let Some(value) = raw
.strip_prefix("default=")
.or_else(|| raw.strip_prefix("DEFAULT="))
{
return Ok(Self::Default(parse_shard_key_literal(value)));
}
Err("expected error, skip, or default=<key> (e.g. default=unknown or default=0)".into())
}
}
pub fn parse_shard_key_literal(raw: &str) -> qql_core::ast::ShardKey {
let raw = raw.trim();
if !raw.is_empty()
&& raw.bytes().all(|b| b.is_ascii_digit())
&& let Ok(n) = raw.parse::<u64>()
{
return qql_core::ast::ShardKey::Number(n);
}
qql_core::ast::ShardKey::Keyword(raw.to_string())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuantizeKind {
Scalar,
Binary,
Product,
Turbo,
}
impl QuantizeKind {
pub fn as_str(&self) -> &'static str {
match self {
Self::Scalar => "scalar",
Self::Binary => "binary",
Self::Product => "product",
Self::Turbo => "turbo",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct QuantizeSpec {
pub kind: QuantizeKind,
pub always_ram: bool,
pub quantile: f64,
pub compression: String,
pub encoding: String,
pub bits: String,
}
impl QuantizeSpec {
pub fn new(kind: QuantizeKind) -> Self {
Self {
kind,
always_ram: true,
quantile: 0.99,
compression: "x16".into(),
encoding: "one_bit".into(),
bits: "2".into(),
}
}
}
#[derive(Debug, Clone)]
pub struct MigrateOptions {
pub source_collection: String,
pub target_collection: String,
pub source_url: String,
pub target_url: String,
pub batch_size: u32,
pub workers: usize,
pub shard_number: Option<u64>,
pub replication_factor: Option<u64>,
pub sharding_method: Option<String>,
pub quantize: Option<QuantizeSpec>,
pub shard_key: Option<String>,
pub shard_key_field: Option<String>,
pub missing_shard_key: MissingShardKey,
pub bulk_indexing_threshold: u64,
pub cutover_alias: Option<String>,
pub drop_source_after_cutover: bool,
pub where_clause: Option<String>,
pub checkpoint_path: String,
pub resume: bool,
pub restart: bool,
pub dry_run: bool,
pub fast_bulk: bool,
pub verify: bool,
pub wait: bool,
pub recreate: bool,
pub batch_delay_ms: u64,
}
impl MigrateOptions {
pub fn fingerprint(&self) -> String {
let quant = self
.quantize
.as_ref()
.map(|q| q.kind.as_str())
.unwrap_or("-");
format!(
"to={},shards={:?},repl={:?},method={:?},quant={},shard_key={:?},shard_field={:?},missing={:?},bulk={},where={:?},fast_bulk={}",
self.target_collection,
self.shard_number,
self.replication_factor,
self.sharding_method,
quant,
self.shard_key,
self.shard_key_field,
self.missing_shard_key,
self.bulk_indexing_threshold,
self.where_clause,
self.fast_bulk,
)
}
pub fn uses_custom_sharding(&self) -> bool {
self.shard_key.is_some()
|| self.shard_key_field.is_some()
|| self
.sharding_method
.as_deref()
.is_some_and(|m| m.eq_ignore_ascii_case("custom"))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MigrateStats {
pub written: usize,
pub skipped: usize,
pub batches: usize,
pub source_count: u64,
pub target_count: Option<u64>,
pub verified: bool,
pub resumed: bool,
pub dry_run: bool,
pub cutover_alias: Option<String>,
pub source_dropped: bool,
pub plan: MigratePlan,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MigratePlan {
pub create: String,
pub indexes: Vec<String>,
pub shard_keys: Vec<String>,
pub restore_optimizers: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MigrateProgress {
pub phase: String,
pub collection: String,
pub written: usize,
pub skipped: usize,
pub batches: usize,
pub source_count: u64,
}