use super::ascii_equal_lower;
use crate::ast::{CollectionConfig, OptimizationThreads, Value};
use crate::error::QqlError;
use alloc::string::String;
pub fn config_value<'a>(config: &'a [(String, Value)], key: &str) -> Option<&'a Value> {
for (k, v) in config {
if ascii_equal_lower(k, key) {
return Some(v);
}
}
None
}
pub fn config_has_key(config: &[(String, Value)], key: &str) -> bool {
config_value(config, key).is_some()
}
pub fn config_bool(config: &[(String, Value)], key: &str) -> Option<bool> {
match config_value(config, key)? {
Value::Bool(b) => Some(*b),
_ => None,
}
}
use crate::error::Span;
fn validation_err(
message: impl Into<alloc::borrow::Cow<'static, str>>,
position: usize,
) -> QqlError {
QqlError::validation(
"QQL-VALIDATION-CONFIG",
message,
Some(Span::point(position)),
)
}
pub fn config_positive_u64(
config: &[(String, Value)],
key: &str,
pos: usize,
) -> Result<Option<u64>, QqlError> {
match config_value(config, key) {
None => Ok(None),
Some(Value::Int(n)) if *n > 0 => Ok(Some(*n as u64)),
Some(Value::Float(n)) if *n > 0.0 && *n == (*n as u64) as f64 => Ok(Some(*n as u64)),
_ => Err(validation_err(
alloc::format!("{} must be a positive integer", key),
pos,
)),
}
}
pub fn config_non_negative_u64(
config: &[(String, Value)],
key: &str,
pos: usize,
) -> Result<Option<u64>, QqlError> {
match config_value(config, key) {
None => Ok(None),
Some(Value::Int(n)) if *n >= 0 => Ok(Some(*n as u64)),
Some(Value::Float(n)) if *n >= 0.0 && *n == (*n as u64) as f64 => Ok(Some(*n as u64)),
_ => Err(validation_err(
alloc::format!("{} must be a non-negative integer", key),
pos,
)),
}
}
pub fn config_float_range(
config: &[(String, Value)],
key: &str,
min: f64,
max: f64,
) -> Option<f64> {
match config_value(config, key)? {
Value::Int(n) => {
let f = *n as f64;
if (min..=max).contains(&f) {
Some(f)
} else {
None
}
}
Value::Float(f) => {
if (min..=max).contains(f) {
Some(*f)
} else {
None
}
}
_ => None,
}
}
pub fn config_max_optimization_threads(
config: &[(String, Value)],
key: &str,
) -> Option<OptimizationThreads> {
match config_value(config, key)? {
Value::Int(n) if *n > 0 => Some(OptimizationThreads {
auto_: false,
value: *n as u64,
}),
Value::Str(s) if ascii_equal_lower(s, "auto") => Some(OptimizationThreads {
auto_: true,
value: 0,
}),
_ => None,
}
}
pub fn is_integer_val(value: &Value) -> bool {
match value {
Value::Int(_) => true,
Value::Float(f) => *f >= 0.0 && *f == (*f as u64) as f64,
_ => false,
}
}
pub fn validate_hnsw_value(key: &str, value: &Value, pos: usize) -> Result<(), QqlError> {
let lower = key.to_ascii_lowercase();
match lower.as_str() {
"m" | "ef_construct" | "full_scan_threshold" | "max_indexing_threads" | "payload_m" => {
if !is_integer_val(value) {
return Err(validation_err(
alloc::format!("{} must be an integer", key),
pos,
));
}
}
"on_disk" | "inline_storage" if !matches!(value, Value::Bool(_)) => {
return Err(validation_err(
alloc::format!("{} must be true or false", key),
pos,
));
}
"memory" => validate_memory_value(key, value, pos, true)?,
_ => {}
}
Ok(())
}
pub fn validate_vectors_value(key: &str, value: &Value, pos: usize) -> Result<(), QqlError> {
let lower = key.to_ascii_lowercase();
match lower.as_str() {
"on_disk" if !matches!(value, Value::Bool(_)) => {
return Err(validation_err(
alloc::format!("{} must be true or false", key),
pos,
));
}
"memory" => validate_memory_value(key, value, pos, true)?,
"datatype" => match value {
Value::Str(s) if crate::ast::VectorDatatype::parse(s).is_some() => {}
Value::Str(_) => {
return Err(validation_err(
alloc::format!("{key} must be float32, float16, uint8, or turbo4"),
pos,
));
}
_ => {
return Err(validation_err(
alloc::format!("{key} must be a string (float32, float16, uint8, or turbo4)"),
pos,
));
}
},
_ => {}
}
Ok(())
}
fn validate_memory_value(
key: &str,
value: &Value,
pos: usize,
allow_pinned: bool,
) -> Result<(), QqlError> {
match value {
Value::Str(s) => match crate::ast::MemoryPlacement::parse(s) {
Some(crate::ast::MemoryPlacement::Pinned) if !allow_pinned => Err(validation_err(
alloc::format!("{key} does not support 'pinned'"),
pos,
)),
Some(_) => Ok(()),
None => Err(validation_err(
alloc::format!("{key} must be 'cold', 'cached', or 'pinned'"),
pos,
)),
},
_ => Err(validation_err(
alloc::format!("{key} must be a string ('cold', 'cached', or 'pinned')"),
pos,
)),
}
}
pub fn validate_optimizers_value(key: &str, value: &Value, pos: usize) -> Result<(), QqlError> {
let lower = key.to_ascii_lowercase();
match lower.as_str() {
"deleted_threshold" => {
if !matches!(value, Value::Int(_) | Value::Float(_)) {
return Err(validation_err(
alloc::format!("{} must be a number", key),
pos,
));
}
}
"vacuum_min_vector_number"
| "default_segment_number"
| "max_segment_size"
| "memmap_threshold"
| "indexing_threshold"
| "flush_interval_sec" => {
if !is_integer_val(value) {
return Err(validation_err(
alloc::format!("{} must be an integer", key),
pos,
));
}
}
"max_optimization_threads" => {
if !is_integer_val(value) && !matches!(value, Value::Str(_)) {
return Err(validation_err(
alloc::format!("{} must be a positive integer or 'auto'", key),
pos,
));
}
}
"prevent_unoptimized" if !matches!(value, Value::Bool(_)) => {
return Err(validation_err(
alloc::format!("{} must be true or false", key),
pos,
));
}
_ => {}
}
Ok(())
}
pub fn validate_params_value(key: &str, value: &Value, pos: usize) -> Result<(), QqlError> {
let lower = key.to_ascii_lowercase();
match lower.as_str() {
"replication_factor"
| "write_consistency_factor"
| "read_fan_out_factor"
| "read_fan_out_delay_ms"
| "shard_number" => {
if !matches!(value, Value::Int(_)) {
return Err(validation_err(
alloc::format!("{} must be an integer", key),
pos,
));
}
}
"on_disk_payload" if !matches!(value, Value::Bool(_)) => {
return Err(validation_err(
alloc::format!("{} must be true or false", key),
pos,
));
}
"payload_memory" => validate_memory_value(key, value, pos, false)?,
"sharding_method" => match value {
Value::Str(s) if s.eq_ignore_ascii_case("auto") || s.eq_ignore_ascii_case("custom") => {
}
Value::Str(_) => {
return Err(validation_err(
"sharding_method must be 'auto' or 'custom'",
pos,
));
}
_ => {
return Err(validation_err(
"sharding_method must be a string ('auto' or 'custom')",
pos,
));
}
},
"shard_keys" => match value {
Value::List(items) if items.is_empty() => {
return Err(validation_err(
"shard_keys must be a non-empty list of strings",
pos,
));
}
Value::List(items) => {
for item in items {
if !matches!(item, Value::Str(_)) {
return Err(validation_err(
"shard_keys entries must all be strings",
pos,
));
}
}
}
_ => {
return Err(validation_err("shard_keys must be a list of strings", pos));
}
},
_ => {}
}
Ok(())
}
pub fn merge_collection_config(
current: &mut CollectionConfig,
new: CollectionConfig,
pos: usize,
) -> Result<(), QqlError> {
if new.vectors.is_some() {
if current.vectors.is_some() {
return Err(QqlError::syntax("VECTOR clause may only appear once", pos));
}
current.vectors = new.vectors;
}
if new.hnsw.is_some() {
if current.hnsw.is_some() {
return Err(QqlError::syntax("HNSW clause may only appear once", pos));
}
current.hnsw = new.hnsw;
}
if new.optimizers.is_some() {
if current.optimizers.is_some() {
return Err(QqlError::syntax(
"OPTIMIZERS clause may only appear once",
pos,
));
}
current.optimizers = new.optimizers;
}
if new.params.is_some() {
if current.params.is_some() {
return Err(QqlError::syntax("PARAMS clause may only appear once", pos));
}
current.params = new.params;
}
if new.quantization.is_some() {
if current.quantization.is_some() {
return Err(QqlError::syntax(
"QUANTIZATION clause may only appear once",
pos,
));
}
current.quantization = new.quantization;
}
if new.quantization_update.is_some() {
if current.quantization_update.is_some() {
return Err(QqlError::syntax(
"QUANTIZATION clause may only appear once",
pos,
));
}
current.quantization_update = new.quantization_update;
}
Ok(())
}
pub fn check_deleted_threshold(value: &Value, pos: usize) -> Result<(), QqlError> {
match value {
Value::Int(n) => {
let f = *n as f64;
if !(0.0..=1.0).contains(&f) {
return Err(QqlError::syntax(
"deleted_threshold must be between 0.0 and 1.0",
pos,
));
}
}
Value::Float(f) if !(0.0..=1.0).contains(f) => {
return Err(QqlError::syntax(
"deleted_threshold must be between 0.0 and 1.0",
pos,
));
}
_ => {}
}
Ok(())
}
pub fn validate_index_options(options: &[(String, Value)], pos: usize) -> Result<(), QqlError> {
for (k, v) in options {
let lower = k.to_ascii_lowercase();
match lower.as_str() {
"is_tenant" | "on_disk" | "enable_hnsw" | "lowercase" | "ascii_folding"
| "phrase_matching" | "lookup" | "range" | "is_principal" | "prefix" => {
if !matches!(v, Value::Bool(_)) {
return Err(QqlError::syntax(
alloc::format!("{} must be true or false", k),
pos,
));
}
}
"min_token_len" | "max_token_len" => {
if !matches!(v, Value::Int(n) if *n >= 0) {
return Err(QqlError::syntax(
alloc::format!("{} must be a non-negative integer", k),
pos,
));
}
}
"tokenizer" | "stemmer" => {
if !matches!(v, Value::Str(_)) {
return Err(QqlError::syntax(
alloc::format!("{} must be a string", k),
pos,
));
}
}
"memory" => validate_memory_value(k, v, pos, true)?,
"stopwords" => match v {
Value::List(items) => {
for item in items {
if !matches!(item, Value::Str(_)) {
return Err(QqlError::syntax(
alloc::format!("{} must be a list of strings", k),
pos,
));
}
}
}
_ => {
return Err(QqlError::syntax(
alloc::format!("{} must be a list of strings", k),
pos,
));
}
},
_ => {
return Err(QqlError::syntax(
alloc::format!("unknown index option: {}", k),
pos,
));
}
}
}
Ok(())
}