1use crate::cast_catalog::{CastContext, CAST_CATALOG};
29use crate::function_catalog::FUNCTION_CATALOG;
30use crate::json::{Map, Value as JsonValue};
31use crate::operator_catalog::{OperatorKind, OPERATOR_CATALOG};
32use crate::types::{DataType, TypeCategory};
33
34pub const RESOURCE_URI: &str = "reddb://knowledge/types";
36
37pub const RESOURCE_TITLE: &str = "RedDB Type & Multi-Model Reference";
39
40pub const RESOURCE_DESCRIPTION: &str =
42 "Generated value-type catalog (function/operator/cast authorities) plus the multi-model map.";
43
44pub const LLMS_BEGIN_MARKER: &str = "<!-- BEGIN GENERATED: types -->";
49pub const LLMS_END_MARKER: &str = "<!-- END GENERATED: types -->";
51
52pub struct ModelParadigm {
56 pub name: &'static str,
58 pub summary: &'static str,
60 pub categories: &'static [TypeCategory],
63}
64
65pub const MULTI_MODEL_MAP: &[ModelParadigm] = &[
69 ModelParadigm {
70 name: "Documents",
71 summary: "Schemaless JSON-shaped entities addressed by collection + entity id; \
72nested fields are typed value-by-value from the catalog below.",
73 categories: &[
74 TypeCategory::Json,
75 TypeCategory::String,
76 TypeCategory::Numeric,
77 TypeCategory::Boolean,
78 ],
79 },
80 ModelParadigm {
81 name: "Key-value",
82 summary: "Flat collection + key → value pairs for caches and counters; any \
83catalogued value type can be the stored payload.",
84 categories: &[
85 TypeCategory::String,
86 TypeCategory::Numeric,
87 TypeCategory::Json,
88 ],
89 },
90 ModelParadigm {
91 name: "Queues",
92 summary: "Ordered FIFO/priority message streams (LPUSH/RPUSH/LPOP/RPOP, ACK/NACK); \
93each message body is a catalogued value, usually text or JSON.",
94 categories: &[
95 TypeCategory::String,
96 TypeCategory::Json,
97 TypeCategory::TimeSpan,
98 ],
99 },
100 ModelParadigm {
101 name: "Graph nodes & edges",
102 summary: "Property graph of nodes and edges; references between them are first-class \
103value types and properties draw from the full catalog.",
104 categories: &[
105 TypeCategory::Reference,
106 TypeCategory::String,
107 TypeCategory::Numeric,
108 ],
109 },
110 ModelParadigm {
111 name: "Vault secrets",
112 summary: "Encrypted secrets and password hashes that the expression layer treats as \
113opaque — coercion must be opted into explicitly.",
114 categories: &[TypeCategory::Opaque, TypeCategory::String],
115 },
116 ModelParadigm {
117 name: "Config",
118 summary: "Hierarchical, resolvable configuration entries; values are catalogued \
119scalars and JSON resolved per environment.",
120 categories: &[
121 TypeCategory::String,
122 TypeCategory::Boolean,
123 TypeCategory::Numeric,
124 TypeCategory::Json,
125 ],
126 },
127 ModelParadigm {
128 name: "RQL-tabular",
129 summary: "Relational tables with typed columns queried through RQL; columns bind \
130directly to the value types and categories of the catalog below.",
131 categories: &[
132 TypeCategory::Numeric,
133 TypeCategory::String,
134 TypeCategory::Boolean,
135 TypeCategory::DateTime,
136 TypeCategory::Domain,
137 ],
138 },
139];
140
141fn push_value_type(acc: &mut Vec<DataType>, candidate: DataType) {
148 if matches!(candidate, DataType::Unknown | DataType::Nullable) {
149 return;
150 }
151 if !acc.contains(&candidate) {
152 acc.push(candidate);
153 }
154}
155
156pub fn catalogued_value_types() -> Vec<DataType> {
165 let mut types: Vec<DataType> = Vec::new();
166 for entry in FUNCTION_CATALOG {
167 for &arg in entry.arg_types {
168 push_value_type(&mut types, arg);
169 }
170 push_value_type(&mut types, entry.return_type);
171 }
172 for entry in OPERATOR_CATALOG {
173 push_value_type(&mut types, entry.lhs_type);
174 push_value_type(&mut types, entry.rhs_type);
175 push_value_type(&mut types, entry.return_type);
176 }
177 for entry in CAST_CATALOG {
178 push_value_type(&mut types, entry.src);
179 push_value_type(&mut types, entry.target);
180 }
181 types.sort_by_key(|ty| *ty as u8);
182 types
183}
184
185pub fn operator_symbols() -> Vec<&'static str> {
190 let mut names: Vec<&'static str> = OPERATOR_CATALOG.iter().map(|entry| entry.name).collect();
191 names.sort_unstable();
192 names.dedup();
193 names
194}
195
196pub fn implicit_casts() -> Vec<(DataType, DataType)> {
200 let mut pairs: Vec<(DataType, DataType)> = CAST_CATALOG
201 .iter()
202 .filter(|cast| cast.context == CastContext::Implicit)
203 .map(|cast| (cast.src, cast.target))
204 .collect();
205 pairs.sort_by_key(|(src, target)| (*src as u8, *target as u8));
206 pairs
207}
208
209fn category_label(category: TypeCategory) -> &'static str {
211 match category {
212 TypeCategory::Numeric => "Numeric",
213 TypeCategory::String => "String",
214 TypeCategory::Boolean => "Boolean",
215 TypeCategory::DateTime => "DateTime",
216 TypeCategory::TimeSpan => "TimeSpan",
217 TypeCategory::Array => "Array",
218 TypeCategory::Network => "Network",
219 TypeCategory::Geo => "Geo",
220 TypeCategory::Domain => "Domain",
221 TypeCategory::Uuid => "Uuid",
222 TypeCategory::Opaque => "Opaque",
223 TypeCategory::Reference => "Reference",
224 TypeCategory::Vector => "Vector",
225 TypeCategory::Json => "Json",
226 TypeCategory::Unknown => "Unknown",
227 }
228}
229
230const CATEGORY_ORDER: &[TypeCategory] = &[
232 TypeCategory::Numeric,
233 TypeCategory::String,
234 TypeCategory::Boolean,
235 TypeCategory::DateTime,
236 TypeCategory::TimeSpan,
237 TypeCategory::Domain,
238 TypeCategory::Network,
239 TypeCategory::Geo,
240 TypeCategory::Uuid,
241 TypeCategory::Json,
242 TypeCategory::Vector,
243 TypeCategory::Array,
244 TypeCategory::Reference,
245 TypeCategory::Opaque,
246];
247
248fn render_code_list<I, S>(items: I) -> String
249where
250 I: IntoIterator<Item = S>,
251 S: AsRef<str>,
252{
253 items
254 .into_iter()
255 .map(|item| format!("`{}`", item.as_ref()))
256 .collect::<Vec<_>>()
257 .join(", ")
258}
259
260pub fn type_reference_markdown() -> String {
265 let types = catalogued_value_types();
266 let operators = operator_symbols();
267 let casts = implicit_casts();
268
269 let mut out = String::new();
270 out.push_str("# RedDB Type & Multi-Model Reference\n\n");
271 out.push_str(
272 "RedDB is a multi-model store (documents, key-value, queues, graph, vault, \
273config, and RQL-tabular collections) layered over one logical type system.\n\n",
274 );
275 out.push_str(
276 "This reference is generated from the `reddb-io-types` function, operator, and \
277cast catalogs. Do not edit by hand — regenerate from the engine.\n\n",
278 );
279
280 out.push_str(&format!("## Value types ({})\n\n", types.len()));
282 out.push_str(
283 "Every concrete value type the engine's type-system authorities reference, \
284grouped by coercion category:\n\n",
285 );
286 for &category in CATEGORY_ORDER {
287 let mut names: Vec<String> = types
288 .iter()
289 .filter(|ty| ty.category() == category)
290 .map(|ty| ty.to_string())
291 .collect();
292 names.dedup();
293 if names.is_empty() {
294 continue;
295 }
296 out.push_str(&format!("### {} types\n\n", category_label(category)));
297 out.push_str(&render_code_list(&names));
298 out.push_str("\n\n");
299 }
300
301 out.push_str(&format!("## Operators ({})\n\n", operators.len()));
303 out.push_str("The type system resolves these built-in operators:\n\n");
304 out.push_str(&render_code_list(&operators));
305 out.push_str("\n\n");
306
307 out.push_str(&format!("## Implicit casts ({})\n\n", casts.len()));
309 out.push_str(
310 "Lossless widenings the engine inserts silently — usable anywhere without an \
311explicit `CAST`:\n\n",
312 );
313 for (src, target) in &casts {
314 out.push_str(&format!("- `{src}` → `{target}`\n"));
315 }
316 out.push('\n');
317
318 out.push_str("## Multi-model map\n\n");
320 out.push_str(
321 "RedDB stores several paradigms over the value-type catalog above. Each \
322paradigm's `Type families` point back into that catalog by category:\n\n",
323 );
324 for paradigm in MULTI_MODEL_MAP {
325 out.push_str(&format!("### {}\n\n", paradigm.name));
326 out.push_str(paradigm.summary);
327 out.push_str("\n\n");
328 let families: Vec<&str> = paradigm
329 .categories
330 .iter()
331 .map(|&category| category_label(category))
332 .collect();
333 out.push_str(&format!(
334 "Type families: {}\n\n",
335 render_code_list(&families)
336 ));
337 }
338
339 while out.ends_with("\n\n") {
341 out.pop();
342 }
343 out
344}
345
346pub fn type_llms_section() -> String {
350 format!(
351 "{begin}\n{body}\n{end}",
352 begin = LLMS_BEGIN_MARKER,
353 body = type_reference_markdown(),
354 end = LLMS_END_MARKER,
355 )
356}
357
358pub fn resolve_type_name(name: &str) -> Option<DataType> {
375 DataType::from_sql_name(name)
376}
377
378pub fn infer_literal_type(value: &JsonValue) -> DataType {
387 match value {
388 JsonValue::Null => DataType::Nullable,
389 JsonValue::Bool(_) => DataType::Boolean,
390 JsonValue::Integer(_) => DataType::Integer,
391 JsonValue::Number(n) => {
392 if n.fract() == 0.0 && *n >= i64::MIN as f64 && *n <= i64::MAX as f64 {
393 DataType::Integer
394 } else {
395 DataType::Float
396 }
397 }
398 JsonValue::Decimal(_) => DataType::DecimalText,
399 JsonValue::String(_) => DataType::Text,
400 JsonValue::Array(_) => DataType::Array,
401 JsonValue::Object(_) => DataType::Json,
402 }
403}
404
405fn cast_context_label(context: CastContext) -> &'static str {
407 match context {
408 CastContext::Implicit => "implicit",
409 CastContext::Assignment => "assignment",
410 CastContext::Explicit => "explicit",
411 }
412}
413
414fn operator_kind_label(kind: OperatorKind) -> &'static str {
416 match kind {
417 OperatorKind::Infix => "infix",
418 OperatorKind::Prefix => "prefix",
419 OperatorKind::Postfix => "postfix",
420 }
421}
422
423pub fn casts_from(ty: DataType) -> Vec<(DataType, CastContext, bool)> {
426 CAST_CATALOG
427 .iter()
428 .filter(|cast| cast.src == ty)
429 .map(|cast| (cast.target, cast.context, cast.lossy))
430 .collect()
431}
432
433pub fn operators_for(ty: DataType) -> Vec<&'static crate::operator_catalog::OperatorEntry> {
438 OPERATOR_CATALOG
439 .iter()
440 .filter(|entry| {
441 let lhs_matches = entry.kind != OperatorKind::Prefix && entry.lhs_type == ty;
442 lhs_matches || entry.rhs_type == ty
443 })
444 .collect()
445}
446
447pub fn type_facts_json(ty: DataType) -> JsonValue {
451 let casts: Vec<JsonValue> = casts_from(ty)
452 .into_iter()
453 .map(|(target, context, lossy)| {
454 let mut obj = Map::new();
455 obj.insert("target".to_string(), JsonValue::String(target.to_string()));
456 obj.insert(
457 "context".to_string(),
458 JsonValue::String(cast_context_label(context).to_string()),
459 );
460 obj.insert("lossy".to_string(), JsonValue::Bool(lossy));
461 JsonValue::Object(obj)
462 })
463 .collect();
464
465 let operators: Vec<JsonValue> = operators_for(ty)
466 .into_iter()
467 .map(|entry| {
468 let mut obj = Map::new();
469 obj.insert(
470 "symbol".to_string(),
471 JsonValue::String(entry.name.to_string()),
472 );
473 obj.insert(
474 "kind".to_string(),
475 JsonValue::String(operator_kind_label(entry.kind).to_string()),
476 );
477 let lhs = if entry.kind == OperatorKind::Prefix {
480 JsonValue::Null
481 } else {
482 JsonValue::String(entry.lhs_type.to_string())
483 };
484 obj.insert("lhs".to_string(), lhs);
485 obj.insert(
486 "rhs".to_string(),
487 JsonValue::String(entry.rhs_type.to_string()),
488 );
489 obj.insert(
490 "returns".to_string(),
491 JsonValue::String(entry.return_type.to_string()),
492 );
493 JsonValue::Object(obj)
494 })
495 .collect();
496
497 let mut obj = Map::new();
498 obj.insert(
499 "canonical_type".to_string(),
500 JsonValue::String(ty.to_string()),
501 );
502 obj.insert(
503 "category".to_string(),
504 JsonValue::String(category_label(ty.category()).to_string()),
505 );
506 obj.insert(
507 "is_preferred".to_string(),
508 JsonValue::Bool(ty.is_preferred()),
509 );
510 obj.insert("casts".to_string(), JsonValue::Array(casts));
511 obj.insert("operators".to_string(), JsonValue::Array(operators));
512 JsonValue::Object(obj)
513}
514
515pub fn type_of_json(type_name: Option<&str>, value: Option<&JsonValue>) -> Option<JsonValue> {
522 let ty = match (type_name, value) {
523 (Some(name), _) => resolve_type_name(name)?,
524 (None, Some(literal)) => infer_literal_type(literal),
525 (None, None) => return None,
526 };
527 Some(type_facts_json(ty))
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533
534 #[test]
539 fn catalog_matches_authorities() {
540 let mut expected: Vec<DataType> = Vec::new();
541 let mut record = |ty: DataType| {
542 if matches!(ty, DataType::Unknown | DataType::Nullable) {
543 return;
544 }
545 if !expected.contains(&ty) {
546 expected.push(ty);
547 }
548 };
549 for entry in FUNCTION_CATALOG {
550 for &arg in entry.arg_types {
551 record(arg);
552 }
553 record(entry.return_type);
554 }
555 for entry in OPERATOR_CATALOG {
556 record(entry.lhs_type);
557 record(entry.rhs_type);
558 record(entry.return_type);
559 }
560 for entry in CAST_CATALOG {
561 record(entry.src);
562 record(entry.target);
563 }
564 expected.sort_by_key(|ty| *ty as u8);
565
566 assert_eq!(
567 catalogued_value_types(),
568 expected,
569 "the published value-type catalog drifted from the function/operator/cast \
570authorities in reddb-io-types"
571 );
572 }
573
574 #[test]
576 fn catalog_excludes_sentinels() {
577 let types = catalogued_value_types();
578 assert!(!types.is_empty(), "value-type catalog must not be empty");
579 assert!(
580 !types.contains(&DataType::Unknown),
581 "Unknown is a catalog placeholder, not a value type"
582 );
583 assert!(
584 !types.contains(&DataType::Nullable),
585 "Nullable is a prefix-operator marker, not a value type"
586 );
587 }
588
589 #[test]
592 fn catalog_is_sorted_and_unique() {
593 let types = catalogued_value_types();
594 let mut sorted = types.clone();
595 sorted.sort_by_key(|ty| *ty as u8);
596 assert_eq!(types, sorted, "catalogued_value_types must be sorted");
597 let mut deduped = types.clone();
598 deduped.dedup();
599 assert_eq!(
600 deduped.len(),
601 types.len(),
602 "catalog must not contain duplicates"
603 );
604 }
605
606 #[test]
609 fn reference_lists_every_value_type() {
610 let reference = type_reference_markdown();
611 for ty in catalogued_value_types() {
612 assert!(
613 reference.contains(&format!("`{ty}`")),
614 "value type {ty} from the catalogs is missing from the generated type \
615reference"
616 );
617 }
618 }
619
620 #[test]
622 fn reference_lists_every_operator() {
623 let reference = type_reference_markdown();
624 for symbol in operator_symbols() {
625 assert!(
626 reference.contains(&format!("`{symbol}`")),
627 "operator {symbol:?} is missing from the generated type reference"
628 );
629 }
630 }
631
632 #[test]
635 fn multi_model_map_covers_every_paradigm() {
636 let names: Vec<&str> = MULTI_MODEL_MAP.iter().map(|m| m.name).collect();
637 for expected in [
638 "Documents",
639 "Key-value",
640 "Queues",
641 "Graph nodes & edges",
642 "Vault secrets",
643 "Config",
644 "RQL-tabular",
645 ] {
646 assert!(
647 names.contains(&expected),
648 "multi-model map is missing the {expected:?} paradigm"
649 );
650 }
651 for paradigm in MULTI_MODEL_MAP {
652 assert!(
653 !paradigm.categories.is_empty(),
654 "paradigm {:?} must link to at least one type category",
655 paradigm.name
656 );
657 }
658 }
659
660 #[test]
662 fn reference_includes_multi_model_map() {
663 let reference = type_reference_markdown();
664 for paradigm in MULTI_MODEL_MAP {
665 assert!(
666 reference.contains(paradigm.name),
667 "paradigm {:?} is missing from the generated reference",
668 paradigm.name
669 );
670 for &category in paradigm.categories {
671 assert!(
672 reference.contains(category_label(category)),
673 "category {:?} for paradigm {:?} is missing from the reference",
674 category_label(category),
675 paradigm.name
676 );
677 }
678 }
679 }
680
681 #[test]
683 fn reference_is_deterministic() {
684 assert_eq!(type_reference_markdown(), type_reference_markdown());
685 }
686
687 #[test]
689 fn llms_section_wraps_reference() {
690 let section = type_llms_section();
691 assert!(section.starts_with(LLMS_BEGIN_MARKER));
692 assert!(section.ends_with(LLMS_END_MARKER));
693 assert!(section.contains(&type_reference_markdown()));
694 }
695
696 #[test]
699 fn resolve_type_name_accepts_canonical_and_aliases() {
700 assert_eq!(resolve_type_name("INTEGER"), Some(DataType::Integer));
701 assert_eq!(resolve_type_name("int"), Some(DataType::Integer));
702 assert_eq!(resolve_type_name("string"), Some(DataType::Text));
703 assert_eq!(resolve_type_name("TEXT"), Some(DataType::Text));
704 assert_eq!(resolve_type_name("not-a-type"), None);
705 }
706
707 #[test]
709 fn infer_literal_type_maps_json_shapes() {
710 assert_eq!(infer_literal_type(&JsonValue::Null), DataType::Nullable);
711 assert_eq!(
712 infer_literal_type(&JsonValue::Bool(true)),
713 DataType::Boolean
714 );
715 assert_eq!(
716 infer_literal_type(&JsonValue::Number(42.0)),
717 DataType::Integer
718 );
719 assert_eq!(infer_literal_type(&JsonValue::Number(3.5)), DataType::Float);
720 assert_eq!(
721 infer_literal_type(&JsonValue::String("hi".to_string())),
722 DataType::Text
723 );
724 assert_eq!(
725 infer_literal_type(&JsonValue::Array(vec![])),
726 DataType::Array
727 );
728 assert_eq!(
729 infer_literal_type(&JsonValue::Object(Map::new())),
730 DataType::Json
731 );
732 }
733
734 #[test]
737 fn casts_from_reads_catalog_rows() {
738 let casts = casts_from(DataType::Integer);
739 let widen = casts
740 .iter()
741 .find(|(target, _, _)| *target == DataType::Float)
742 .expect("INTEGER → FLOAT cast present in catalog");
743 assert_eq!(widen.1, CastContext::Implicit);
744 assert!(!widen.2, "INTEGER → FLOAT must be lossless");
745 for (target, _, _) in &casts {
747 assert!(CAST_CATALOG
748 .iter()
749 .any(|c| c.src == DataType::Integer && c.target == *target));
750 }
751 }
752
753 #[test]
756 fn operators_for_matches_either_operand() {
757 let ops = operators_for(DataType::Integer);
758 assert!(
759 ops.iter().any(|e| e.name == "+"),
760 "INTEGER should accept the + operator"
761 );
762 for entry in &ops {
767 if entry.lhs_type == DataType::Nullable {
768 assert_eq!(entry.kind, OperatorKind::Prefix);
769 assert_eq!(entry.rhs_type, DataType::Integer);
770 } else {
771 assert!(entry.lhs_type == DataType::Integer || entry.rhs_type == DataType::Integer);
772 }
773 }
774 }
775
776 #[test]
779 fn type_facts_json_reports_canonical_casts_and_operators() {
780 let facts = type_facts_json(DataType::Integer);
781 assert_eq!(
782 facts.get("canonical_type").and_then(JsonValue::as_str),
783 Some("INTEGER")
784 );
785 assert_eq!(
786 facts.get("category").and_then(JsonValue::as_str),
787 Some("Numeric")
788 );
789 let casts = facts.get("casts").and_then(JsonValue::as_array).unwrap();
790 assert!(
791 casts
792 .iter()
793 .any(|c| c.get("target").and_then(JsonValue::as_str) == Some("FLOAT")),
794 "INTEGER → FLOAT must appear in the casts"
795 );
796 let operators = facts
797 .get("operators")
798 .and_then(JsonValue::as_array)
799 .unwrap();
800 assert!(
801 operators
802 .iter()
803 .any(|o| o.get("symbol").and_then(JsonValue::as_str) == Some("+")),
804 "the + operator must appear in the operators"
805 );
806 }
807
808 #[test]
811 fn type_of_json_resolves_name_and_value() {
812 let by_name = type_of_json(Some("int"), None).expect("known type name");
814 assert_eq!(
815 by_name.get("canonical_type").and_then(JsonValue::as_str),
816 Some("INTEGER")
817 );
818 let by_value = type_of_json(None, Some(&JsonValue::Bool(false))).expect("value");
820 assert_eq!(
821 by_value.get("canonical_type").and_then(JsonValue::as_str),
822 Some("BOOLEAN")
823 );
824 assert!(type_of_json(Some("frobnicate"), None).is_none());
826 assert!(type_of_json(None, None).is_none());
828 }
829}