use std::{collections::HashMap, sync::LazyLock};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum OperatorCategory {
Comparison,
String,
Null,
Array,
Vector,
Fulltext,
Containment,
Network,
DateRange,
Ltree,
Spatial,
Path,
}
#[derive(Debug, Clone)]
pub struct OperatorInfo {
pub name: &'static str,
pub sql_op: &'static str,
pub category: OperatorCategory,
pub requires_array: bool,
pub jsonb_operator: bool,
}
pub static OPERATOR_REGISTRY: LazyLock<HashMap<&'static str, OperatorInfo>> = LazyLock::new(|| {
let mut m = HashMap::new();
m.insert(
"eq",
OperatorInfo {
name: "eq",
sql_op: "=",
category: OperatorCategory::Comparison,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"ne",
OperatorInfo {
name: "ne",
sql_op: "!=",
category: OperatorCategory::Comparison,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"gt",
OperatorInfo {
name: "gt",
sql_op: ">",
category: OperatorCategory::Comparison,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"gte",
OperatorInfo {
name: "gte",
sql_op: ">=",
category: OperatorCategory::Comparison,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"lt",
OperatorInfo {
name: "lt",
sql_op: "<",
category: OperatorCategory::Comparison,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"lte",
OperatorInfo {
name: "lte",
sql_op: "<=",
category: OperatorCategory::Comparison,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"in",
OperatorInfo {
name: "in",
sql_op: "IN",
category: OperatorCategory::Comparison,
requires_array: true,
jsonb_operator: false,
},
);
m.insert(
"nin",
OperatorInfo {
name: "nin",
sql_op: "NOT IN",
category: OperatorCategory::Comparison,
requires_array: true,
jsonb_operator: false,
},
);
m.insert(
"like",
OperatorInfo {
name: "like",
sql_op: "LIKE",
category: OperatorCategory::String,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"ilike",
OperatorInfo {
name: "ilike",
sql_op: "ILIKE",
category: OperatorCategory::String,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"nlike",
OperatorInfo {
name: "nlike",
sql_op: "NOT LIKE",
category: OperatorCategory::String,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"nilike",
OperatorInfo {
name: "nilike",
sql_op: "NOT ILIKE",
category: OperatorCategory::String,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"regex",
OperatorInfo {
name: "regex",
sql_op: "~",
category: OperatorCategory::String,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"iregex",
OperatorInfo {
name: "iregex",
sql_op: "~*",
category: OperatorCategory::String,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"nregex",
OperatorInfo {
name: "nregex",
sql_op: "!~",
category: OperatorCategory::String,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"niregex",
OperatorInfo {
name: "niregex",
sql_op: "!~*",
category: OperatorCategory::String,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"is_null",
OperatorInfo {
name: "is_null",
sql_op: "IS NULL",
category: OperatorCategory::Null,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"is_not_null",
OperatorInfo {
name: "is_not_null",
sql_op: "IS NOT NULL",
category: OperatorCategory::Null,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"contains",
OperatorInfo {
name: "contains",
sql_op: "@>",
category: OperatorCategory::Containment,
requires_array: false,
jsonb_operator: true,
},
);
m.insert(
"contained_in",
OperatorInfo {
name: "contained_in",
sql_op: "<@",
category: OperatorCategory::Containment,
requires_array: false,
jsonb_operator: true,
},
);
m.insert(
"has_key",
OperatorInfo {
name: "has_key",
sql_op: "?",
category: OperatorCategory::Containment,
requires_array: false,
jsonb_operator: true,
},
);
m.insert(
"has_any_keys",
OperatorInfo {
name: "has_any_keys",
sql_op: "?|",
category: OperatorCategory::Containment,
requires_array: true,
jsonb_operator: true,
},
);
m.insert(
"has_all_keys",
OperatorInfo {
name: "has_all_keys",
sql_op: "?&",
category: OperatorCategory::Containment,
requires_array: true,
jsonb_operator: true,
},
);
m.insert(
"array_contains",
OperatorInfo {
name: "array_contains",
sql_op: "@>",
category: OperatorCategory::Array,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"array_contained_in",
OperatorInfo {
name: "array_contained_in",
sql_op: "<@",
category: OperatorCategory::Array,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"array_overlaps",
OperatorInfo {
name: "array_overlaps",
sql_op: "&&",
category: OperatorCategory::Array,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"cosine_distance",
OperatorInfo {
name: "cosine_distance",
sql_op: "<=>",
category: OperatorCategory::Vector,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"l2_distance",
OperatorInfo {
name: "l2_distance",
sql_op: "<->",
category: OperatorCategory::Vector,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"inner_product",
OperatorInfo {
name: "inner_product",
sql_op: "<#>",
category: OperatorCategory::Vector,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"l1_distance",
OperatorInfo {
name: "l1_distance",
sql_op: "<+>",
category: OperatorCategory::Vector,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"hamming_distance",
OperatorInfo {
name: "hamming_distance",
sql_op: "<~>",
category: OperatorCategory::Vector,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"jaccard_distance",
OperatorInfo {
name: "jaccard_distance",
sql_op: "<%>",
category: OperatorCategory::Vector,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"search",
OperatorInfo {
name: "search",
sql_op: "@@",
category: OperatorCategory::Fulltext,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"plainto_tsquery",
OperatorInfo {
name: "plainto_tsquery",
sql_op: "@@",
category: OperatorCategory::Fulltext,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"phraseto_tsquery",
OperatorInfo {
name: "phraseto_tsquery",
sql_op: "@@",
category: OperatorCategory::Fulltext,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"websearch_to_tsquery",
OperatorInfo {
name: "websearch_to_tsquery",
sql_op: "@@",
category: OperatorCategory::Fulltext,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"startswith",
OperatorInfo {
name: "startswith",
sql_op: "LIKE",
category: OperatorCategory::String,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"istartswith",
OperatorInfo {
name: "istartswith",
sql_op: "ILIKE",
category: OperatorCategory::String,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"endswith",
OperatorInfo {
name: "endswith",
sql_op: "LIKE",
category: OperatorCategory::String,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"iendswith",
OperatorInfo {
name: "iendswith",
sql_op: "ILIKE",
category: OperatorCategory::String,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"icontains",
OperatorInfo {
name: "icontains",
sql_op: "ILIKE",
category: OperatorCategory::String,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"imatches",
OperatorInfo {
name: "imatches",
sql_op: "~*",
category: OperatorCategory::String,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"not_matches",
OperatorInfo {
name: "not_matches",
sql_op: "!~",
category: OperatorCategory::String,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"isIPv4",
OperatorInfo {
name: "isIPv4",
sql_op: "family({}) = 4",
category: OperatorCategory::Network,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"isIPv6",
OperatorInfo {
name: "isIPv6",
sql_op: "family({}) = 6",
category: OperatorCategory::Network,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"isPrivate",
OperatorInfo {
name: "isPrivate",
sql_op: "CIDR_RANGE_CHECK",
category: OperatorCategory::Network,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"isPublic",
OperatorInfo {
name: "isPublic",
sql_op: "NOT_CIDR_RANGE_CHECK",
category: OperatorCategory::Network,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"inSubnet",
OperatorInfo {
name: "inSubnet",
sql_op: "{} <<= {}",
category: OperatorCategory::Network,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"notInSubnet",
OperatorInfo {
name: "notInSubnet",
sql_op: "NOT ({} <<= {})",
category: OperatorCategory::Network,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"subnet_contains",
OperatorInfo {
name: "subnet_contains",
sql_op: ">>",
category: OperatorCategory::Network,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"subnet_overlaps",
OperatorInfo {
name: "subnet_overlaps",
sql_op: "&&",
category: OperatorCategory::Network,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"contains_date",
OperatorInfo {
name: "contains_date",
sql_op: "@>",
category: OperatorCategory::DateRange,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"adjacent",
OperatorInfo {
name: "adjacent",
sql_op: "-|-",
category: OperatorCategory::DateRange,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"strictly_left",
OperatorInfo {
name: "strictly_left",
sql_op: "<<",
category: OperatorCategory::DateRange,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"strictly_right",
OperatorInfo {
name: "strictly_right",
sql_op: ">>",
category: OperatorCategory::DateRange,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"not_left",
OperatorInfo {
name: "not_left",
sql_op: "&>",
category: OperatorCategory::DateRange,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"not_right",
OperatorInfo {
name: "not_right",
sql_op: "&<",
category: OperatorCategory::DateRange,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"overlaps",
OperatorInfo {
name: "overlaps",
sql_op: "&&",
category: OperatorCategory::DateRange,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"ancestor_of",
OperatorInfo {
name: "ancestor_of",
sql_op: "@>",
category: OperatorCategory::Ltree,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"descendant_of",
OperatorInfo {
name: "descendant_of",
sql_op: "<@",
category: OperatorCategory::Ltree,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"matches_lquery",
OperatorInfo {
name: "matches_lquery",
sql_op: "~",
category: OperatorCategory::Ltree,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"matches_ltxtquery",
OperatorInfo {
name: "matches_ltxtquery",
sql_op: "@",
category: OperatorCategory::Ltree,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"matches_any_lquery",
OperatorInfo {
name: "matches_any_lquery",
sql_op: "?",
category: OperatorCategory::Ltree,
requires_array: true,
jsonb_operator: false,
},
);
m.insert(
"depth_eq",
OperatorInfo {
name: "depth_eq",
sql_op: "nlevel({}) =",
category: OperatorCategory::Path,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"depth_gt",
OperatorInfo {
name: "depth_gt",
sql_op: "nlevel({}) >",
category: OperatorCategory::Path,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"depth_lt",
OperatorInfo {
name: "depth_lt",
sql_op: "nlevel({}) <",
category: OperatorCategory::Path,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"isdescendant",
OperatorInfo {
name: "isdescendant",
sql_op: "<@",
category: OperatorCategory::Path,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"distance_within",
OperatorInfo {
name: "distance_within",
sql_op: "distance_within",
category: OperatorCategory::Spatial,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"strictly_contains",
OperatorInfo {
name: "strictly_contains",
sql_op: "@>",
category: OperatorCategory::Containment,
requires_array: false,
jsonb_operator: true,
},
);
m.insert(
"neq",
OperatorInfo {
name: "neq",
sql_op: "!=",
category: OperatorCategory::Comparison,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"isnull",
OperatorInfo {
name: "isnull",
sql_op: "IS NULL",
category: OperatorCategory::Null,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"array_eq",
OperatorInfo {
name: "array_eq",
sql_op: "=",
category: OperatorCategory::Array,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"array_neq",
OperatorInfo {
name: "array_neq",
sql_op: "!=",
category: OperatorCategory::Array,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"array_contained_by",
OperatorInfo {
name: "array_contained_by",
sql_op: "<@",
category: OperatorCategory::Array,
requires_array: false,
jsonb_operator: false,
},
);
m.insert(
"notin",
OperatorInfo {
name: "notin",
sql_op: "NOT IN",
category: OperatorCategory::Comparison,
requires_array: true,
jsonb_operator: false,
},
);
m
});
#[must_use]
pub fn get_operator_info(name: &str) -> Option<&'static OperatorInfo> {
OPERATOR_REGISTRY.get(name)
}
#[must_use]
pub fn is_operator(name: &str) -> bool {
OPERATOR_REGISTRY.contains_key(name)
}
#[must_use]
pub fn get_operators_by_category(category: OperatorCategory) -> Vec<&'static OperatorInfo> {
OPERATOR_REGISTRY.values().filter(|op| op.category == category).collect()
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn test_operator_registry_initialized() {
assert!(OPERATOR_REGISTRY.len() >= 40);
}
#[test]
fn test_comparison_operators() {
let operators = ["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin"];
for op_name in &operators {
let op = get_operator_info(op_name);
assert!(op.is_some(), "Operator {op_name} should exist");
let op = op.unwrap();
assert_eq!(op.category, OperatorCategory::Comparison);
assert!(!op.jsonb_operator);
}
}
#[test]
fn test_string_operators() {
let operators = [
"like", "ilike", "nlike", "nilike", "regex", "iregex", "nregex", "niregex",
];
for op_name in &operators {
let op = get_operator_info(op_name);
assert!(op.is_some(), "String operator {op_name} should exist");
let op = op.unwrap();
assert_eq!(op.category, OperatorCategory::String);
}
}
#[test]
fn test_null_operators() {
let op1 = get_operator_info("is_null").unwrap();
assert_eq!(op1.sql_op, "IS NULL");
assert_eq!(op1.category, OperatorCategory::Null);
let op2 = get_operator_info("is_not_null").unwrap();
assert_eq!(op2.sql_op, "IS NOT NULL");
assert_eq!(op2.category, OperatorCategory::Null);
}
#[test]
fn test_containment_operators() {
let operators = [
"contains",
"contained_in",
"has_key",
"has_any_keys",
"has_all_keys",
];
for op_name in &operators {
let op = get_operator_info(op_name);
assert!(op.is_some(), "Containment operator {op_name} should exist");
let op = op.unwrap();
assert_eq!(op.category, OperatorCategory::Containment);
assert!(op.jsonb_operator, "{op_name} should be JSONB operator");
}
}
#[test]
fn test_array_operators() {
let operators = ["array_contains", "array_contained_in", "array_overlaps"];
for op_name in &operators {
let op = get_operator_info(op_name);
assert!(op.is_some(), "Array operator {op_name} should exist");
let op = op.unwrap();
assert_eq!(op.category, OperatorCategory::Array);
}
}
#[test]
fn test_vector_operators() {
let operators = [
("cosine_distance", "<=>"),
("l2_distance", "<->"),
("inner_product", "<#>"),
("l1_distance", "<+>"),
("hamming_distance", "<~>"),
("jaccard_distance", "<%>"),
];
for (op_name, expected_sql) in &operators {
let op = get_operator_info(op_name);
assert!(op.is_some(), "Vector operator {op_name} should exist");
let op = op.unwrap();
assert_eq!(op.category, OperatorCategory::Vector);
assert_eq!(op.sql_op, *expected_sql);
}
}
#[test]
fn test_fulltext_operators() {
let operators = [
"search",
"plainto_tsquery",
"phraseto_tsquery",
"websearch_to_tsquery",
];
for op_name in &operators {
let op = get_operator_info(op_name);
assert!(op.is_some(), "Fulltext operator {op_name} should exist");
let op = op.unwrap();
assert_eq!(op.category, OperatorCategory::Fulltext);
assert_eq!(op.sql_op, "@@");
}
}
#[test]
fn test_is_operator() {
assert!(is_operator("eq"));
assert!(is_operator("contains"));
assert!(is_operator("cosine_distance"));
assert!(!is_operator("invalid_operator"));
assert!(!is_operator(""));
}
#[test]
fn test_get_operators_by_category() {
let comparison_ops = get_operators_by_category(OperatorCategory::Comparison);
assert!(comparison_ops.len() >= 8);
let vector_ops = get_operators_by_category(OperatorCategory::Vector);
assert!(vector_ops.len() >= 6);
let fulltext_ops = get_operators_by_category(OperatorCategory::Fulltext);
assert!(fulltext_ops.len() >= 4);
}
#[test]
fn test_requires_array_flag() {
assert!(get_operator_info("in").unwrap().requires_array);
assert!(get_operator_info("nin").unwrap().requires_array);
assert!(!get_operator_info("eq").unwrap().requires_array);
assert!(!get_operator_info("like").unwrap().requires_array);
}
#[test]
fn test_jsonb_operator_flag() {
assert!(get_operator_info("contains").unwrap().jsonb_operator);
assert!(get_operator_info("has_key").unwrap().jsonb_operator);
assert!(!get_operator_info("eq").unwrap().jsonb_operator);
assert!(!get_operator_info("like").unwrap().jsonb_operator);
}
}