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::Number(n) => {
391 if n.fract() == 0.0 && *n >= i64::MIN as f64 && *n <= i64::MAX as f64 {
392 DataType::Integer
393 } else {
394 DataType::Float
395 }
396 }
397 JsonValue::String(_) => DataType::Text,
398 JsonValue::Array(_) => DataType::Array,
399 JsonValue::Object(_) => DataType::Json,
400 }
401}
402
403fn cast_context_label(context: CastContext) -> &'static str {
405 match context {
406 CastContext::Implicit => "implicit",
407 CastContext::Assignment => "assignment",
408 CastContext::Explicit => "explicit",
409 }
410}
411
412fn operator_kind_label(kind: OperatorKind) -> &'static str {
414 match kind {
415 OperatorKind::Infix => "infix",
416 OperatorKind::Prefix => "prefix",
417 OperatorKind::Postfix => "postfix",
418 }
419}
420
421pub fn casts_from(ty: DataType) -> Vec<(DataType, CastContext, bool)> {
424 CAST_CATALOG
425 .iter()
426 .filter(|cast| cast.src == ty)
427 .map(|cast| (cast.target, cast.context, cast.lossy))
428 .collect()
429}
430
431pub fn operators_for(ty: DataType) -> Vec<&'static crate::operator_catalog::OperatorEntry> {
436 OPERATOR_CATALOG
437 .iter()
438 .filter(|entry| {
439 let lhs_matches = entry.kind != OperatorKind::Prefix && entry.lhs_type == ty;
440 lhs_matches || entry.rhs_type == ty
441 })
442 .collect()
443}
444
445pub fn type_facts_json(ty: DataType) -> JsonValue {
449 let casts: Vec<JsonValue> = casts_from(ty)
450 .into_iter()
451 .map(|(target, context, lossy)| {
452 let mut obj = Map::new();
453 obj.insert("target".to_string(), JsonValue::String(target.to_string()));
454 obj.insert(
455 "context".to_string(),
456 JsonValue::String(cast_context_label(context).to_string()),
457 );
458 obj.insert("lossy".to_string(), JsonValue::Bool(lossy));
459 JsonValue::Object(obj)
460 })
461 .collect();
462
463 let operators: Vec<JsonValue> = operators_for(ty)
464 .into_iter()
465 .map(|entry| {
466 let mut obj = Map::new();
467 obj.insert(
468 "symbol".to_string(),
469 JsonValue::String(entry.name.to_string()),
470 );
471 obj.insert(
472 "kind".to_string(),
473 JsonValue::String(operator_kind_label(entry.kind).to_string()),
474 );
475 let lhs = if entry.kind == OperatorKind::Prefix {
478 JsonValue::Null
479 } else {
480 JsonValue::String(entry.lhs_type.to_string())
481 };
482 obj.insert("lhs".to_string(), lhs);
483 obj.insert(
484 "rhs".to_string(),
485 JsonValue::String(entry.rhs_type.to_string()),
486 );
487 obj.insert(
488 "returns".to_string(),
489 JsonValue::String(entry.return_type.to_string()),
490 );
491 JsonValue::Object(obj)
492 })
493 .collect();
494
495 let mut obj = Map::new();
496 obj.insert(
497 "canonical_type".to_string(),
498 JsonValue::String(ty.to_string()),
499 );
500 obj.insert(
501 "category".to_string(),
502 JsonValue::String(category_label(ty.category()).to_string()),
503 );
504 obj.insert(
505 "is_preferred".to_string(),
506 JsonValue::Bool(ty.is_preferred()),
507 );
508 obj.insert("casts".to_string(), JsonValue::Array(casts));
509 obj.insert("operators".to_string(), JsonValue::Array(operators));
510 JsonValue::Object(obj)
511}
512
513pub fn type_of_json(type_name: Option<&str>, value: Option<&JsonValue>) -> Option<JsonValue> {
520 let ty = match (type_name, value) {
521 (Some(name), _) => resolve_type_name(name)?,
522 (None, Some(literal)) => infer_literal_type(literal),
523 (None, None) => return None,
524 };
525 Some(type_facts_json(ty))
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531
532 #[test]
537 fn catalog_matches_authorities() {
538 let mut expected: Vec<DataType> = Vec::new();
539 let mut record = |ty: DataType| {
540 if matches!(ty, DataType::Unknown | DataType::Nullable) {
541 return;
542 }
543 if !expected.contains(&ty) {
544 expected.push(ty);
545 }
546 };
547 for entry in FUNCTION_CATALOG {
548 for &arg in entry.arg_types {
549 record(arg);
550 }
551 record(entry.return_type);
552 }
553 for entry in OPERATOR_CATALOG {
554 record(entry.lhs_type);
555 record(entry.rhs_type);
556 record(entry.return_type);
557 }
558 for entry in CAST_CATALOG {
559 record(entry.src);
560 record(entry.target);
561 }
562 expected.sort_by_key(|ty| *ty as u8);
563
564 assert_eq!(
565 catalogued_value_types(),
566 expected,
567 "the published value-type catalog drifted from the function/operator/cast \
568authorities in reddb-io-types"
569 );
570 }
571
572 #[test]
574 fn catalog_excludes_sentinels() {
575 let types = catalogued_value_types();
576 assert!(!types.is_empty(), "value-type catalog must not be empty");
577 assert!(
578 !types.contains(&DataType::Unknown),
579 "Unknown is a catalog placeholder, not a value type"
580 );
581 assert!(
582 !types.contains(&DataType::Nullable),
583 "Nullable is a prefix-operator marker, not a value type"
584 );
585 }
586
587 #[test]
590 fn catalog_is_sorted_and_unique() {
591 let types = catalogued_value_types();
592 let mut sorted = types.clone();
593 sorted.sort_by_key(|ty| *ty as u8);
594 assert_eq!(types, sorted, "catalogued_value_types must be sorted");
595 let mut deduped = types.clone();
596 deduped.dedup();
597 assert_eq!(
598 deduped.len(),
599 types.len(),
600 "catalog must not contain duplicates"
601 );
602 }
603
604 #[test]
607 fn reference_lists_every_value_type() {
608 let reference = type_reference_markdown();
609 for ty in catalogued_value_types() {
610 assert!(
611 reference.contains(&format!("`{ty}`")),
612 "value type {ty} from the catalogs is missing from the generated type \
613reference"
614 );
615 }
616 }
617
618 #[test]
620 fn reference_lists_every_operator() {
621 let reference = type_reference_markdown();
622 for symbol in operator_symbols() {
623 assert!(
624 reference.contains(&format!("`{symbol}`")),
625 "operator {symbol:?} is missing from the generated type reference"
626 );
627 }
628 }
629
630 #[test]
633 fn multi_model_map_covers_every_paradigm() {
634 let names: Vec<&str> = MULTI_MODEL_MAP.iter().map(|m| m.name).collect();
635 for expected in [
636 "Documents",
637 "Key-value",
638 "Queues",
639 "Graph nodes & edges",
640 "Vault secrets",
641 "Config",
642 "RQL-tabular",
643 ] {
644 assert!(
645 names.contains(&expected),
646 "multi-model map is missing the {expected:?} paradigm"
647 );
648 }
649 for paradigm in MULTI_MODEL_MAP {
650 assert!(
651 !paradigm.categories.is_empty(),
652 "paradigm {:?} must link to at least one type category",
653 paradigm.name
654 );
655 }
656 }
657
658 #[test]
660 fn reference_includes_multi_model_map() {
661 let reference = type_reference_markdown();
662 for paradigm in MULTI_MODEL_MAP {
663 assert!(
664 reference.contains(paradigm.name),
665 "paradigm {:?} is missing from the generated reference",
666 paradigm.name
667 );
668 for &category in paradigm.categories {
669 assert!(
670 reference.contains(category_label(category)),
671 "category {:?} for paradigm {:?} is missing from the reference",
672 category_label(category),
673 paradigm.name
674 );
675 }
676 }
677 }
678
679 #[test]
681 fn reference_is_deterministic() {
682 assert_eq!(type_reference_markdown(), type_reference_markdown());
683 }
684
685 #[test]
687 fn llms_section_wraps_reference() {
688 let section = type_llms_section();
689 assert!(section.starts_with(LLMS_BEGIN_MARKER));
690 assert!(section.ends_with(LLMS_END_MARKER));
691 assert!(section.contains(&type_reference_markdown()));
692 }
693
694 #[test]
697 fn resolve_type_name_accepts_canonical_and_aliases() {
698 assert_eq!(resolve_type_name("INTEGER"), Some(DataType::Integer));
699 assert_eq!(resolve_type_name("int"), Some(DataType::Integer));
700 assert_eq!(resolve_type_name("string"), Some(DataType::Text));
701 assert_eq!(resolve_type_name("TEXT"), Some(DataType::Text));
702 assert_eq!(resolve_type_name("not-a-type"), None);
703 }
704
705 #[test]
707 fn infer_literal_type_maps_json_shapes() {
708 assert_eq!(infer_literal_type(&JsonValue::Null), DataType::Nullable);
709 assert_eq!(
710 infer_literal_type(&JsonValue::Bool(true)),
711 DataType::Boolean
712 );
713 assert_eq!(
714 infer_literal_type(&JsonValue::Number(42.0)),
715 DataType::Integer
716 );
717 assert_eq!(infer_literal_type(&JsonValue::Number(3.5)), DataType::Float);
718 assert_eq!(
719 infer_literal_type(&JsonValue::String("hi".to_string())),
720 DataType::Text
721 );
722 assert_eq!(
723 infer_literal_type(&JsonValue::Array(vec![])),
724 DataType::Array
725 );
726 assert_eq!(
727 infer_literal_type(&JsonValue::Object(Map::new())),
728 DataType::Json
729 );
730 }
731
732 #[test]
735 fn casts_from_reads_catalog_rows() {
736 let casts = casts_from(DataType::Integer);
737 let widen = casts
738 .iter()
739 .find(|(target, _, _)| *target == DataType::Float)
740 .expect("INTEGER → FLOAT cast present in catalog");
741 assert_eq!(widen.1, CastContext::Implicit);
742 assert!(!widen.2, "INTEGER → FLOAT must be lossless");
743 for (target, _, _) in &casts {
745 assert!(CAST_CATALOG
746 .iter()
747 .any(|c| c.src == DataType::Integer && c.target == *target));
748 }
749 }
750
751 #[test]
754 fn operators_for_matches_either_operand() {
755 let ops = operators_for(DataType::Integer);
756 assert!(
757 ops.iter().any(|e| e.name == "+"),
758 "INTEGER should accept the + operator"
759 );
760 for entry in &ops {
765 if entry.lhs_type == DataType::Nullable {
766 assert_eq!(entry.kind, OperatorKind::Prefix);
767 assert_eq!(entry.rhs_type, DataType::Integer);
768 } else {
769 assert!(entry.lhs_type == DataType::Integer || entry.rhs_type == DataType::Integer);
770 }
771 }
772 }
773
774 #[test]
777 fn type_facts_json_reports_canonical_casts_and_operators() {
778 let facts = type_facts_json(DataType::Integer);
779 assert_eq!(
780 facts.get("canonical_type").and_then(JsonValue::as_str),
781 Some("INTEGER")
782 );
783 assert_eq!(
784 facts.get("category").and_then(JsonValue::as_str),
785 Some("Numeric")
786 );
787 let casts = facts.get("casts").and_then(JsonValue::as_array).unwrap();
788 assert!(
789 casts
790 .iter()
791 .any(|c| c.get("target").and_then(JsonValue::as_str) == Some("FLOAT")),
792 "INTEGER → FLOAT must appear in the casts"
793 );
794 let operators = facts
795 .get("operators")
796 .and_then(JsonValue::as_array)
797 .unwrap();
798 assert!(
799 operators
800 .iter()
801 .any(|o| o.get("symbol").and_then(JsonValue::as_str) == Some("+")),
802 "the + operator must appear in the operators"
803 );
804 }
805
806 #[test]
809 fn type_of_json_resolves_name_and_value() {
810 let by_name = type_of_json(Some("int"), None).expect("known type name");
812 assert_eq!(
813 by_name.get("canonical_type").and_then(JsonValue::as_str),
814 Some("INTEGER")
815 );
816 let by_value = type_of_json(None, Some(&JsonValue::Bool(false))).expect("value");
818 assert_eq!(
819 by_value.get("canonical_type").and_then(JsonValue::as_str),
820 Some("BOOLEAN")
821 );
822 assert!(type_of_json(Some("frobnicate"), None).is_none());
824 assert!(type_of_json(None, None).is_none());
826 }
827}