Skip to main content

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}