1pub struct SchemaItem {
3 pub ddl: &'static str,
5}
6inventory::collect!(SchemaItem);
7
8pub struct HnswSchemaItem {
10 pub index: HnswIndexDef,
11}
12inventory::collect!(HnswSchemaItem);
13
14pub trait SchemaDef {
16 const SCHEMA: &'static str;
18}
19
20#[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#[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#[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]
202macro_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]
218macro_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}