Skip to main content

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}