use super::ascii_equal;
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(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>>, span: Span) -> QqlError {
QqlError::validation("QQL-VALIDATION-CONFIG", message, Some(span))
}
pub fn config_positive_u64(
config: &[(String, Value)],
key: &str,
span: Span,
) -> 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),
span,
)),
}
}
pub fn config_non_negative_u64(
config: &[(String, Value)],
key: &str,
span: Span,
) -> 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),
span,
)),
}
}
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(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, span: Span) -> 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),
span,
));
}
}
"on_disk" | "inline_storage" if !matches!(value, Value::Bool(_)) => {
return Err(validation_err(
alloc::format!("{} must be true or false", key),
span,
));
}
"memory" => validate_memory_value(key, value, span, true)?,
_ => {}
}
Ok(())
}
pub fn validate_vectors_value(key: &str, value: &Value, span: Span) -> 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),
span,
));
}
"memory" => validate_memory_value(key, value, span, 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"),
span,
));
}
_ => {
return Err(validation_err(
alloc::format!("{key} must be a string (float32, float16, uint8, or turbo4)"),
span,
));
}
},
_ => {}
}
Ok(())
}
fn validate_memory_value(
key: &str,
value: &Value,
span: Span,
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'"),
span,
)),
Some(_) => Ok(()),
None => Err(validation_err(
alloc::format!("{key} must be 'cold', 'cached', or 'pinned'"),
span,
)),
},
_ => Err(validation_err(
alloc::format!("{key} must be a string ('cold', 'cached', or 'pinned')"),
span,
)),
}
}
pub fn validate_optimizers_value(key: &str, value: &Value, span: Span) -> 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),
span,
));
}
}
"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),
span,
));
}
}
"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),
span,
));
}
}
"prevent_unoptimized" if !matches!(value, Value::Bool(_)) => {
return Err(validation_err(
alloc::format!("{} must be true or false", key),
span,
));
}
_ => {}
}
Ok(())
}
pub fn validate_params_value(key: &str, value: &Value, span: Span) -> 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),
span,
));
}
}
"on_disk_payload" if !matches!(value, Value::Bool(_)) => {
return Err(validation_err(
alloc::format!("{} must be true or false", key),
span,
));
}
"payload_memory" => validate_memory_value(key, value, span, 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'",
span,
));
}
_ => {
return Err(validation_err(
"sharding_method must be a string ('auto' or 'custom')",
span,
));
}
},
"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 or non-negative integers",
span,
));
}
Value::List(items) => {
for item in items {
let ok = match item {
Value::Str(_) => true,
Value::Int(n) => *n >= 0,
Value::Param(..) | Value::PositionalParam(..) => true,
_ => false,
};
if !ok {
return Err(validation_err(
"shard_keys entries must all be strings or non-negative integers",
span,
));
}
}
}
_ => {
return Err(validation_err(
"shard_keys must be a list of strings or non-negative integers",
span,
));
}
},
_ => {}
}
Ok(())
}
pub fn merge_collection_config(
current: &mut CollectionConfig,
new: CollectionConfig,
span: Span,
) -> Result<(), QqlError> {
if new.vectors.is_some() {
if current.vectors.is_some() {
return Err(validation_err("VECTOR clause may only appear once", span));
}
current.vectors = new.vectors;
}
if new.hnsw.is_some() {
if current.hnsw.is_some() {
return Err(validation_err("HNSW clause may only appear once", span));
}
current.hnsw = new.hnsw;
}
if new.optimizers.is_some() {
if current.optimizers.is_some() {
return Err(validation_err(
"OPTIMIZERS clause may only appear once",
span,
));
}
current.optimizers = new.optimizers;
}
if new.params.is_some() {
if current.params.is_some() {
return Err(validation_err("PARAMS clause may only appear once", span));
}
current.params = new.params;
}
if new.quantization.is_some() {
if current.quantization.is_some() {
return Err(validation_err(
"QUANTIZATION clause may only appear once",
span,
));
}
current.quantization = new.quantization;
}
if new.quantization_update.is_some() {
if current.quantization_update.is_some() {
return Err(validation_err(
"QUANTIZATION clause may only appear once",
span,
));
}
current.quantization_update = new.quantization_update;
}
if new.wal.is_some() {
if current.wal.is_some() {
return Err(validation_err("WAL clause may only appear once", span));
}
current.wal = new.wal;
}
if new.strict_mode.is_some() {
if current.strict_mode.is_some() {
return Err(validation_err(
"STRICT_MODE clause may only appear once",
span,
));
}
current.strict_mode = new.strict_mode;
}
if new.metadata.is_some() {
if current.metadata.is_some() {
return Err(validation_err("METADATA clause may only appear once", span));
}
current.metadata = new.metadata;
}
for diff in new.vector_diffs {
if current.vector_diffs.iter().any(|d| d.name == diff.name) {
return Err(validation_err(
alloc::format!("VECTOR diff '{}' may only appear once", diff.name),
span,
));
}
current.vector_diffs.push(diff);
}
for diff in new.sparse_vector_diffs {
if current
.sparse_vector_diffs
.iter()
.any(|d| d.name == diff.name)
{
return Err(validation_err(
alloc::format!("SPARSE vector diff '{}' may only appear once", diff.name),
span,
));
}
current.sparse_vector_diffs.push(diff);
}
Ok(())
}
pub fn check_deleted_threshold(value: &Value, span: Span) -> Result<(), QqlError> {
match value {
Value::Int(n) => {
let f = *n as f64;
if !(0.0..=1.0).contains(&f) {
return Err(validation_err(
"deleted_threshold must be between 0.0 and 1.0",
span,
));
}
}
Value::Float(f) if !(0.0..=1.0).contains(f) => {
return Err(validation_err(
"deleted_threshold must be between 0.0 and 1.0",
span,
));
}
_ => {}
}
Ok(())
}
pub fn validate_index_options(options: &[(String, Value)], span: Span) -> 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(validation_err(
alloc::format!("{} must be true or false", k),
span,
));
}
}
"min_token_len" | "max_token_len" => {
if !matches!(v, Value::Int(n) if *n >= 0) {
return Err(validation_err(
alloc::format!("{} must be a non-negative integer", k),
span,
));
}
}
"tokenizer" | "stemmer" => {
if !matches!(v, Value::Str(_)) {
return Err(validation_err(
alloc::format!("{} must be a string", k),
span,
));
}
}
"memory" => validate_memory_value(k, v, span, true)?,
"stopwords" => match v {
Value::List(items) => {
for item in items {
if !matches!(item, Value::Str(_)) {
return Err(validation_err(
alloc::format!("{} must be a list of strings", k),
span,
));
}
}
}
Value::Str(_) => {}
Value::Dict(entries) => {
for (entry_key, entry_value) in entries {
if entry_key.eq_ignore_ascii_case("languages") {
match entry_value {
Value::List(items) => {
for item in items {
if !matches!(item, Value::Str(_)) {
return Err(validation_err(
"stopwords languages must be a list of strings",
span,
));
}
}
}
_ => {
return Err(validation_err(
"stopwords languages must be a list of strings",
span,
));
}
}
} else if entry_key.eq_ignore_ascii_case("custom") {
match entry_value {
Value::List(items) => {
for item in items {
if !matches!(item, Value::Str(_)) {
return Err(validation_err(
"stopwords custom must be a list of strings",
span,
));
}
}
}
_ => {
return Err(validation_err(
"stopwords custom must be a list of strings",
span,
));
}
}
} else {
return Err(validation_err(
alloc::format!(
"unknown stopwords set key '{entry_key}'. Expected: languages, custom"
),
span,
));
}
}
}
_ => {
return Err(validation_err(
alloc::format!(
"{} must be a list of strings, a language name, or {{languages: […], custom: […]}}",
k
),
span,
));
}
},
_ => {
return Err(validation_err(
alloc::format!("unknown index option: {}", k),
span,
));
}
}
}
Ok(())
}
pub const STRICT_MODE_KEYS: &[&str] = &[
"enabled",
"max_query_limit",
"max_timeout",
"unindexed_filtering_retrieve",
"unindexed_filtering_update",
"search_max_hnsw_ef",
"search_allow_exact",
"search_max_oversampling",
"upsert_max_batchsize",
"search_max_batchsize",
"max_collection_vector_size_bytes",
"read_rate_limit",
"write_rate_limit",
"max_collection_payload_size_bytes",
"max_points_count",
"filter_max_conditions",
"condition_max_size",
"multivector_config",
"sparse_config",
"max_payload_index_count",
"max_resident_memory_percent",
];
pub fn is_strict_mode_key(key: &str) -> bool {
STRICT_MODE_KEYS.iter().any(|known| ascii_equal(known, key))
}
pub fn validate_wal_value(key: &str, value: &Value, span: Span) -> Result<(), QqlError> {
if !matches!(value, Value::Int(_)) {
return Err(validation_err(
alloc::format!("{} must be an integer", key),
span,
));
}
Ok(())
}
pub fn validate_strict_mode_value(key: &str, value: &Value, span: Span) -> Result<(), QqlError> {
let lower = key.to_ascii_lowercase();
match lower.as_str() {
"enabled"
| "unindexed_filtering_retrieve"
| "unindexed_filtering_update"
| "search_allow_exact" => {
if !matches!(value, Value::Bool(_)) {
return Err(validation_err(
alloc::format!("{} must be true or false", key),
span,
));
}
}
"search_max_oversampling" => {
if !matches!(value, Value::Int(_) | Value::Float(_)) {
return Err(validation_err(
alloc::format!("{} must be a number", key),
span,
));
}
}
"multivector_config" | "sparse_config" => {
if !matches!(value, Value::Dict(_)) {
return Err(validation_err(
alloc::format!("{} must be an object", key),
span,
));
}
}
_ => {
if !matches!(value, Value::Int(_)) {
return Err(validation_err(
alloc::format!("{} must be an integer", key),
span,
));
}
}
}
Ok(())
}