panproto_protocols/database/
redis.rs1use std::collections::HashMap;
6use std::hash::BuildHasher;
7
8use panproto_gat::Theory;
9use panproto_schema::{EdgeRule, Protocol, Schema, SchemaBuilder};
10
11use crate::emit::{IndentWriter, children_by_edge, find_roots};
12use crate::error::ProtocolError;
13use crate::theories;
14
15#[must_use]
17pub fn protocol() -> Protocol {
18 Protocol {
19 name: "redis".into(),
20 schema_theory: "ThRedisSchema".into(),
21 instance_theory: "ThRedisInstance".into(),
22 edge_rules: edge_rules(),
23 obj_kinds: vec![
24 "index".into(),
25 "field".into(),
26 "text".into(),
27 "tag".into(),
28 "numeric".into(),
29 "geo".into(),
30 "vector".into(),
31 ],
32 constraint_sorts: vec![],
33 has_order: true,
34 nominal_identity: true,
35 ..Protocol::default()
36 }
37}
38
39pub fn register_theories<S: BuildHasher>(registry: &mut HashMap<String, Theory, S>) {
41 theories::register_simple_graph_flat(registry, "ThRedisSchema", "ThRedisInstance");
42}
43
44pub fn parse_redis_schema(input: &str) -> Result<Schema, ProtocolError> {
55 let proto = protocol();
56 let mut builder = SchemaBuilder::new(&proto);
57
58 for line in input.lines() {
59 let trimmed = line.trim();
60 if trimmed.is_empty() || trimmed.starts_with('#') {
61 continue;
62 }
63
64 if trimmed.starts_with("FT.CREATE") {
65 builder = parse_ft_create(builder, trimmed)?;
66 }
67 }
68
69 let schema = builder.build()?;
70 Ok(schema)
71}
72
73fn parse_ft_create(mut builder: SchemaBuilder, line: &str) -> Result<SchemaBuilder, ProtocolError> {
75 let parts: Vec<&str> = line.split_whitespace().collect();
76 if parts.len() < 2 {
77 return Err(ProtocolError::Parse("invalid FT.CREATE".into()));
78 }
79
80 let index_name = parts[1];
81 builder = builder.vertex(index_name, "index", None)?;
82
83 let schema_idx = parts.iter().position(|p| p.eq_ignore_ascii_case("SCHEMA"));
85 if let Some(idx) = schema_idx {
86 let mut i = idx + 1;
87 while i < parts.len() {
88 let field_name = parts[i];
89 let field_type = parts.get(i + 1).copied().unwrap_or("TEXT");
90 let field_id = format!("{index_name}.{field_name}");
91 let kind = redis_type_to_kind(field_type);
92 builder = builder.vertex(&field_id, kind, None)?;
93 builder = builder.edge(index_name, &field_id, "prop", Some(field_name))?;
94 i += 2;
95 }
96 }
97
98 Ok(builder)
99}
100
101fn redis_type_to_kind(type_str: &str) -> &'static str {
103 match type_str.to_uppercase().as_str() {
104 "TEXT" => "text",
105 "TAG" => "tag",
106 "NUMERIC" => "numeric",
107 "GEO" => "geo",
108 "VECTOR" => "vector",
109 _ => "field",
110 }
111}
112
113fn kind_to_redis_type(kind: &str) -> &'static str {
115 match kind {
116 "text" => "TEXT",
117 "tag" => "TAG",
118 "numeric" => "NUMERIC",
119 "geo" => "GEO",
120 "vector" => "VECTOR",
121 _ => "TEXT",
122 }
123}
124
125pub fn emit_redis_schema(schema: &Schema) -> Result<String, ProtocolError> {
131 let structural = &["prop"];
132 let roots = find_roots(schema, structural);
133
134 let mut w = IndentWriter::new(" ");
135
136 for root in &roots {
137 if root.kind != "index" {
138 continue;
139 }
140 let fields = children_by_edge(schema, &root.id, "prop");
141 let field_strs: Vec<String> = fields
142 .iter()
143 .map(|(edge, child)| {
144 let name = edge.name.as_deref().unwrap_or(&child.id);
145 let type_str = kind_to_redis_type(&child.kind);
146 format!("{name} {type_str}")
147 })
148 .collect();
149 w.line(&format!(
150 "FT.CREATE {} ON HASH SCHEMA {}",
151 root.id,
152 field_strs.join(" ")
153 ));
154 }
155
156 Ok(w.finish())
157}
158
159fn edge_rules() -> Vec<EdgeRule> {
160 vec![EdgeRule {
161 edge_kind: "prop".into(),
162 src_kinds: vec!["index".into()],
163 tgt_kinds: vec![
164 "text".into(),
165 "tag".into(),
166 "numeric".into(),
167 "geo".into(),
168 "vector".into(),
169 "field".into(),
170 ],
171 }]
172}
173
174#[cfg(test)]
175#[allow(clippy::expect_used, clippy::unwrap_used)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn protocol_def() {
181 let p = protocol();
182 assert_eq!(p.name, "redis");
183 }
184
185 #[test]
186 fn register_theories_works() {
187 let mut registry = HashMap::new();
188 register_theories(&mut registry);
189 assert!(registry.contains_key("ThRedisSchema"));
190 assert!(registry.contains_key("ThRedisInstance"));
191 }
192
193 #[test]
194 fn parse_and_emit() {
195 let input = "FT.CREATE myidx ON HASH SCHEMA title TEXT name TAG age NUMERIC\n";
196 let schema = parse_redis_schema(input).expect("should parse");
197 assert!(schema.has_vertex("myidx"));
198 assert!(schema.has_vertex("myidx.title"));
199 assert!(schema.has_vertex("myidx.name"));
200 assert!(schema.has_vertex("myidx.age"));
201
202 let emitted = emit_redis_schema(&schema).expect("should emit");
203 assert!(emitted.contains("FT.CREATE myidx"));
204 assert!(emitted.contains("title TEXT"));
205 }
206
207 #[test]
208 fn roundtrip() {
209 let input = "FT.CREATE idx ON HASH SCHEMA f1 TEXT f2 NUMERIC\n";
210 let s1 = parse_redis_schema(input).expect("parse");
211 let emitted = emit_redis_schema(&s1).expect("emit");
212 let s2 = parse_redis_schema(&emitted).expect("re-parse");
213 assert_eq!(s1.vertex_count(), s2.vertex_count());
214 }
215}