Skip to main content

appdb/model/
schema.rs

1/// Inventory item for one schema DDL statement.
2pub struct SchemaItem {
3    /// Raw DDL submitted into the schema inventory.
4    pub ddl: &'static str,
5}
6inventory::collect!(SchemaItem);
7
8/// Inventory item for one HNSW vector index definition.
9pub struct HnswSchemaItem {
10    pub index: HnswIndexDef,
11}
12inventory::collect!(HnswSchemaItem);
13
14/// Trait implemented by types that register a schema statement.
15pub trait SchemaDef {
16    /// DDL statement submitted during database initialization.
17    const SCHEMA: &'static str;
18}
19
20/// Primitive scalar representation used by SurrealDB vector indexes.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum VectorIndexType {
23    F64,
24    F32,
25    F16,
26    I64,
27    I32,
28    I16,
29    I8,
30    U64,
31    U32,
32    U16,
33    U8,
34}
35
36impl VectorIndexType {
37    pub const fn as_surql(self) -> &'static str {
38        match self {
39            Self::F64 => "F64",
40            Self::F32 => "F32",
41            Self::F16 => "F16",
42            Self::I64 => "I64",
43            Self::I32 => "I32",
44            Self::I16 => "I16",
45            Self::I8 => "I8",
46            Self::U64 => "U64",
47            Self::U32 => "U32",
48            Self::U16 => "U16",
49            Self::U8 => "U8",
50        }
51    }
52}
53
54/// Distance metric used by SurrealDB vector indexes.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum VectorDistance {
57    Euclidean,
58    Cosine,
59    InnerProduct,
60    CosineNormalized,
61}
62
63impl VectorDistance {
64    pub const fn as_surql(self) -> &'static str {
65        match self {
66            Self::Euclidean => "EUCLIDEAN",
67            Self::Cosine => "COSINE",
68            Self::InnerProduct => "INNER_PRODUCT",
69            Self::CosineNormalized => "COSINE_NORMALIZED",
70        }
71    }
72}
73
74/// Definition for one SurrealDB HNSW vector index.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub struct HnswIndexDef {
77    pub name: &'static str,
78    pub table: &'static str,
79    pub field: &'static str,
80    pub dimension: usize,
81    pub vector_type: Option<VectorIndexType>,
82    pub distance: Option<VectorDistance>,
83    pub ef_construction: Option<usize>,
84    pub m: Option<usize>,
85    pub concurrently: bool,
86    pub defer: bool,
87}
88
89impl HnswIndexDef {
90    pub const fn new(
91        name: &'static str,
92        table: &'static str,
93        field: &'static str,
94        dimension: usize,
95    ) -> Self {
96        Self {
97            name,
98            table,
99            field,
100            dimension,
101            vector_type: None,
102            distance: None,
103            ef_construction: None,
104            m: None,
105            concurrently: false,
106            defer: false,
107        }
108    }
109
110    pub const fn vector_type(mut self, vector_type: VectorIndexType) -> Self {
111        self.vector_type = Some(vector_type);
112        self
113    }
114
115    pub const fn distance(mut self, distance: VectorDistance) -> Self {
116        self.distance = Some(distance);
117        self
118    }
119
120    pub const fn ef_construction(mut self, ef_construction: usize) -> Self {
121        self.ef_construction = Some(ef_construction);
122        self
123    }
124
125    pub const fn m(mut self, m: usize) -> Self {
126        self.m = Some(m);
127        self
128    }
129
130    pub const fn concurrently(mut self) -> Self {
131        self.concurrently = true;
132        self
133    }
134
135    pub const fn deferred(mut self) -> Self {
136        self.defer = true;
137        self
138    }
139
140    pub fn ddl(self) -> String {
141        assert!(
142            self.dimension > 0,
143            "HNSW vector index dimension must be greater than zero"
144        );
145        assert!(
146            self.name.bytes().all(is_schema_identifier_byte),
147            "HNSW vector index name must be a plain SurrealQL identifier"
148        );
149        assert!(
150            self.table.bytes().all(is_schema_identifier_byte),
151            "HNSW vector index table must be a plain SurrealQL identifier"
152        );
153        assert!(
154            self.field.bytes().all(is_schema_field_byte),
155            "HNSW vector index field must be a plain SurrealQL field path"
156        );
157
158        let mut ddl = format!(
159            "DEFINE INDEX IF NOT EXISTS {} ON {} FIELDS {} HNSW DIMENSION {}",
160            self.name, self.table, self.field, self.dimension
161        );
162        if let Some(vector_type) = self.vector_type {
163            ddl.push_str(" TYPE ");
164            ddl.push_str(vector_type.as_surql());
165        }
166        if let Some(distance) = self.distance {
167            ddl.push_str(" DIST ");
168            ddl.push_str(distance.as_surql());
169        }
170        if let Some(ef_construction) = self.ef_construction {
171            ddl.push_str(" EFC ");
172            ddl.push_str(&ef_construction.to_string());
173        }
174        if let Some(m) = self.m {
175            ddl.push_str(" M ");
176            ddl.push_str(&m.to_string());
177        }
178        if self.concurrently {
179            ddl.push_str(" CONCURRENTLY");
180        }
181        if self.defer {
182            ddl.push_str(" DEFER");
183        }
184        ddl.push(';');
185        ddl
186    }
187}
188
189fn is_schema_identifier_byte(byte: u8) -> bool {
190    byte.is_ascii_alphanumeric() || byte == b'_'
191}
192
193fn is_schema_field_byte(byte: u8) -> bool {
194    is_schema_identifier_byte(byte) || byte == b'.'
195}
196
197#[cfg(test)]
198#[path = "schema_tests.rs"]
199mod tests;
200
201#[macro_export]
202/// Registers a schema DDL string for a type.
203macro_rules! impl_schema {
204    ($ty:ty, $ddl:expr) => {
205        impl $crate::model::schema::SchemaDef for $ty {
206            const SCHEMA: &'static str = $ddl;
207        }
208
209        inventory::submit! {
210            $crate::model::schema::SchemaItem {
211                ddl: < $ty as $crate::model::schema::SchemaDef >::SCHEMA,
212            }
213        }
214    };
215}
216
217#[macro_export]
218/// Registers a SurrealDB HNSW vector index for a type.
219macro_rules! impl_hnsw_index {
220    (
221        $ty:ty,
222        name: $name:expr,
223        table: $table:expr,
224        field: $field:expr,
225        dimension: $dimension:expr
226        $(, vector_type: $vector_type:expr)?
227        $(, distance: $distance:expr)?
228        $(, ef_construction: $ef_construction:expr)?
229        $(, m: $m:expr)?
230        $(, concurrently: $concurrently:expr)?
231        $(, defer: $defer:expr)?
232        $(,)?
233    ) => {
234        impl $crate::model::schema::SchemaDef for $ty {
235            const SCHEMA: &'static str = "";
236        }
237
238        ::inventory::submit! {
239            $crate::model::schema::HnswSchemaItem {
240                index: $crate::model::schema::HnswIndexDef {
241                    name: $name,
242                    table: $table,
243                    field: $field,
244                    dimension: $dimension,
245                    vector_type: $crate::impl_hnsw_index!(@optional; $($vector_type)?),
246                    distance: $crate::impl_hnsw_index!(@optional; $($distance)?),
247                    ef_construction: $crate::impl_hnsw_index!(@optional; $($ef_construction)?),
248                    m: $crate::impl_hnsw_index!(@optional; $($m)?),
249                    concurrently: $crate::impl_hnsw_index!(@optional_bool; $($concurrently)?),
250                    defer: $crate::impl_hnsw_index!(@optional_bool; $($defer)?),
251                },
252            }
253        }
254    };
255    (@optional;) => {
256        None
257    };
258    (@optional; $value:expr) => {
259        Some($value)
260    };
261    (@optional_bool;) => {
262        false
263    };
264    (@optional_bool; $value:expr) => {
265        $value
266    };
267}