ant_types/schema.rs
1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::ids::{Namespace, TypeName};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
9pub enum SpgTypeKind {
10 BasicType,
11 StandardType,
12 EntityType,
13 IndexType,
14 ConceptType,
15 EventType,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
20pub enum IndexKind {
21 Text,
22 Vector,
23 TextAndVector,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub enum ValueType {
28 Text,
29 /// 64-bit integer (SQL BIGINT). Wire names "Integer"/"Long" keep
30 /// mapping here for backward compatibility with existing schemas.
31 Long,
32 /// 64-bit float (SQL DOUBLE PRECISION / FLOAT8).
33 Float,
34 /// Calendar date (SQL DATE). Values validate + normalize to
35 /// `YYYY-MM-DD`; invalid input coerces to Null.
36 Date,
37 Bool,
38 // --- SQL-parity additions (values stay wire-compatible scalars;
39 // the schema carries the SQL fidelity, coerce.rs validates and
40 // normalizes on ingress) ---
41 /// SQL SMALLINT: range-checked to i16 on ingress, stored as Long.
42 SmallInt,
43 /// SQL INT/INTEGER (32-bit): range-checked to i32, stored as Long.
44 /// Wire name "Int32"/"Int" (plain "Integer" stays Long, see above).
45 Int32,
46 /// SQL DECIMAL/NUMERIC. Values coerce to [`crate::Decimal`] — i128
47 /// unscaled digits plus a scale, exact, never `f64`.
48 ///
49 /// UNPARAMETERIZED, deliberately. The declared `(precision, scale)`
50 /// stays in the source-schema mapping rather than here, because the
51 /// value already carries its own exact scale and reports its own
52 /// precision, which is enough for storage, comparison and
53 /// round-tripping. Adding them here would change the serde shape of
54 /// this variant from the string `"Decimal"` to a struct, and every
55 /// stored schema record and `.ant` schema_type payload is written
56 /// in the current shape.
57 ///
58 /// REVISIT WHEN: the SQL auto-mapper needs to VALIDATE values
59 /// against declared column types — rejecting a scale-6 value
60 /// written into a `DECIMAL(10,4)` column, rather than storing it at
61 /// whatever scale it arrived with. That check cannot be made from
62 /// the value alone; it needs the declaration, and at that point the
63 /// declaration has to live here. Doing it will need a
64 /// backward-compatible deserializer that still accepts the bare
65 /// `"Decimal"` string.
66 Decimal,
67 /// SQL TIME: normalized `HH:MM:SS.ffffff` (fixed 6-digit fraction
68 /// so lexicographic order == chronological order).
69 Time,
70 /// SQL TIMESTAMP/TIMESTAMPTZ: normalized UTC RFC3339 with fixed
71 /// 6-digit fraction (`YYYY-MM-DDTHH:MM:SS.ffffffZ`) so lexicographic
72 /// order == chronological order. Offset-less input is taken as UTC.
73 Timestamp,
74 /// UUID/UNIQUEIDENTIFIER: validated 8-4-4-4-12 hex, lowercased.
75 Uuid,
76 /// BLOB/BYTEA/VARBINARY: base64 text, charset/padding validated.
77 Bytes,
78 /// JSON/JSONB, document-store subdocuments: stored as real JSON
79 /// (PropertyValue::Json), not stringified.
80 Json,
81 /// Typed array (Postgres arrays, document-store arrays): every
82 /// element coerced against the inner type; stored as a JSON array.
83 Array(Box<ValueType>),
84 Ref(TypeName),
85}
86
87impl ValueType {
88 /// Parse an OpenSPG basic-type name (e.g. "Text", "Integer", "Float")
89 /// or a user-defined type reference.
90 pub fn from_object_type_name(name: &str) -> Self {
91 // Array<Inner> (any nesting depth) before the flat names.
92 if let Some(inner) = name
93 .strip_prefix("Array<")
94 .and_then(|r| r.strip_suffix('>'))
95 {
96 return Self::Array(Box::new(Self::from_object_type_name(inner)));
97 }
98 match name {
99 "Text" => Self::Text,
100 // Compat: existing schemas declared "Integer" meaning i64.
101 "Integer" | "Long" | "BigInt" => Self::Long,
102 "Float" | "Double" => Self::Float,
103 "Date" => Self::Date,
104 "Boolean" | "Bool" => Self::Bool,
105 "SmallInt" | "Int16" => Self::SmallInt,
106 "Int32" | "Int" => Self::Int32,
107 "Decimal" | "Numeric" => Self::Decimal,
108 "Time" => Self::Time,
109 "Timestamp" | "DateTime" | "TimestampTz" => Self::Timestamp,
110 "Uuid" | "UUID" | "Guid" => Self::Uuid,
111 "Bytes" | "Binary" | "Blob" => Self::Bytes,
112 "Json" | "JSONB" | "Object" => Self::Json,
113 // Anything else — treat as a reference to another SPG type.
114 other => Self::Ref(TypeName(other.to_string())),
115 }
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120pub struct PropertyDef {
121 pub name: String,
122 pub name_zh: Option<String>,
123 pub value_type: ValueType,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub index: Option<IndexKind>,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129pub struct RelationDef {
130 pub name: String,
131 pub name_zh: Option<String>,
132 pub target: TypeName,
133 /// Properties carried on the edge itself (rare; M1 leaves empty).
134 #[serde(default)]
135 pub properties: Vec<PropertyDef>,
136}
137
138impl RelationDef {
139 pub fn properties_lookup(&self) -> std::collections::BTreeMap<&str, &PropertyDef> {
140 self.properties
141 .iter()
142 .map(|p| (p.name.as_str(), p))
143 .collect()
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148pub struct SchemaType {
149 pub kind: SpgTypeKind,
150 /// Namespace-qualified name, e.g. `Antares.Deal`.
151 pub name: TypeName,
152 /// Chinese display name if provided by marklang.
153 pub name_zh: Option<String>,
154 pub properties: Vec<PropertyDef>,
155 pub relations: Vec<RelationDef>,
156}
157
158impl SchemaType {
159 pub fn new(kind: SpgTypeKind, name: TypeName) -> Self {
160 Self {
161 kind,
162 name,
163 name_zh: None,
164 properties: Vec::new(),
165 relations: Vec::new(),
166 }
167 }
168}
169
170#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
171pub struct Schema {
172 pub types: BTreeMap<TypeName, SchemaType>,
173}
174
175impl Schema {
176 pub fn get(&self, name: &TypeName) -> Option<&SchemaType> {
177 self.types.get(name)
178 }
179
180 /// Insert (replacing on conflict) a list of `SchemaType`s.
181 pub fn upsert_all(&mut self, types: impl IntoIterator<Item = SchemaType>) {
182 for t in types {
183 self.types.insert(t.name.clone(), t);
184 }
185 }
186
187 /// All declared relation names that resolve to a real SPG type (i.e.
188 /// not BASIC/STANDARD value-typed properties). Used by the planner.
189 pub fn relation_names(&self) -> Vec<&str> {
190 self.types
191 .values()
192 .flat_map(|t| t.relations.iter().map(|r| r.name.as_str()))
193 .collect()
194 }
195}
196
197/// Convenience: namespace-qualify an unqualified label (`Deal`) into
198/// `Antares.Deal` using the project's namespace.
199pub fn qualify(ns: &Namespace, label: &str) -> TypeName {
200 TypeName::qualified(ns, label)
201}