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