1use std::collections::BTreeMap;
10use std::fmt::Write as _;
11
12use serde::{Deserialize, Serialize};
13
14use crate::error::{Result, SurqlError};
15
16use super::fields::FieldDefinition;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(rename_all = "UPPERCASE")]
21pub enum TableMode {
22 Schemafull,
24 Schemaless,
26 Drop,
28}
29
30impl TableMode {
31 pub fn as_str(self) -> &'static str {
33 match self {
34 Self::Schemafull => "SCHEMAFULL",
35 Self::Schemaless => "SCHEMALESS",
36 Self::Drop => "DROP",
37 }
38 }
39}
40
41impl std::fmt::Display for TableMode {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 f.write_str(self.as_str())
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
49#[serde(rename_all = "UPPERCASE")]
50pub enum IndexType {
51 Unique,
53 Search,
55 Standard,
57 Mtree,
59 Hnsw,
61}
62
63impl IndexType {
64 pub fn as_str(self) -> &'static str {
66 match self {
67 Self::Unique => "UNIQUE",
68 Self::Search => "SEARCH",
69 Self::Standard => "INDEX",
70 Self::Mtree => "MTREE",
71 Self::Hnsw => "HNSW",
72 }
73 }
74}
75
76impl std::fmt::Display for IndexType {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 f.write_str(self.as_str())
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
84#[serde(rename_all = "UPPERCASE")]
85pub enum MTreeDistanceType {
86 Cosine,
88 Euclidean,
90 Manhattan,
92 Minkowski,
94}
95
96impl MTreeDistanceType {
97 pub fn as_str(self) -> &'static str {
99 match self {
100 Self::Cosine => "COSINE",
101 Self::Euclidean => "EUCLIDEAN",
102 Self::Manhattan => "MANHATTAN",
103 Self::Minkowski => "MINKOWSKI",
104 }
105 }
106}
107
108impl std::fmt::Display for MTreeDistanceType {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 f.write_str(self.as_str())
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
116#[serde(rename_all = "UPPERCASE")]
117pub enum HnswDistanceType {
118 Chebyshev,
120 Cosine,
122 Euclidean,
124 Hamming,
126 Jaccard,
128 Manhattan,
130 Minkowski,
132 Pearson,
134}
135
136impl HnswDistanceType {
137 pub fn as_str(self) -> &'static str {
139 match self {
140 Self::Chebyshev => "CHEBYSHEV",
141 Self::Cosine => "COSINE",
142 Self::Euclidean => "EUCLIDEAN",
143 Self::Hamming => "HAMMING",
144 Self::Jaccard => "JACCARD",
145 Self::Manhattan => "MANHATTAN",
146 Self::Minkowski => "MINKOWSKI",
147 Self::Pearson => "PEARSON",
148 }
149 }
150}
151
152impl std::fmt::Display for HnswDistanceType {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 f.write_str(self.as_str())
155 }
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
160#[serde(rename_all = "UPPERCASE")]
161pub enum MTreeVectorType {
162 F64,
164 F32,
166 I64,
168 I32,
170 I16,
172}
173
174impl MTreeVectorType {
175 pub fn as_str(self) -> &'static str {
177 match self {
178 Self::F64 => "F64",
179 Self::F32 => "F32",
180 Self::I64 => "I64",
181 Self::I32 => "I32",
182 Self::I16 => "I16",
183 }
184 }
185}
186
187impl std::fmt::Display for MTreeVectorType {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 f.write_str(self.as_str())
190 }
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct IndexDefinition {
196 pub name: String,
198 pub columns: Vec<String>,
200 #[serde(rename = "type", default = "IndexDefinition::default_type")]
202 pub index_type: IndexType,
203 #[serde(skip_serializing_if = "Option::is_none", default)]
205 pub dimension: Option<u32>,
206 #[serde(skip_serializing_if = "Option::is_none", default)]
208 pub distance: Option<MTreeDistanceType>,
209 #[serde(skip_serializing_if = "Option::is_none", default)]
211 pub vector_type: Option<MTreeVectorType>,
212 #[serde(skip_serializing_if = "Option::is_none", default)]
214 pub hnsw_distance: Option<HnswDistanceType>,
215 #[serde(skip_serializing_if = "Option::is_none", default)]
217 pub efc: Option<u32>,
218 #[serde(skip_serializing_if = "Option::is_none", default)]
220 pub m: Option<u32>,
221}
222
223impl IndexDefinition {
224 fn default_type() -> IndexType {
225 IndexType::Standard
226 }
227
228 pub fn new<I, S>(name: impl Into<String>, columns: I) -> Self
230 where
231 I: IntoIterator<Item = S>,
232 S: Into<String>,
233 {
234 Self {
235 name: name.into(),
236 columns: columns.into_iter().map(Into::into).collect(),
237 index_type: IndexType::Standard,
238 dimension: None,
239 distance: None,
240 vector_type: None,
241 hnsw_distance: None,
242 efc: None,
243 m: None,
244 }
245 }
246
247 pub fn with_type(mut self, index_type: IndexType) -> Self {
249 self.index_type = index_type;
250 self
251 }
252
253 pub fn validate(&self) -> Result<()> {
258 if self.name.is_empty() {
259 return Err(SurqlError::Validation {
260 reason: "Index name cannot be empty".into(),
261 });
262 }
263 if self.columns.is_empty() {
264 return Err(SurqlError::Validation {
265 reason: format!("Index {:?} must have at least one column", self.name),
266 });
267 }
268 if matches!(self.index_type, IndexType::Mtree | IndexType::Hnsw) && self.dimension.is_none()
269 {
270 return Err(SurqlError::Validation {
271 reason: format!("Vector index {:?} requires a dimension", self.name),
272 });
273 }
274 Ok(())
275 }
276
277 pub fn to_surql(&self, table: &str) -> String {
279 self.to_surql_with_options(table, false)
280 }
281
282 pub fn to_surql_with_options(&self, table: &str, if_not_exists: bool) -> String {
284 let ine = if if_not_exists { " IF NOT EXISTS" } else { "" };
285 match self.index_type {
286 IndexType::Mtree => {
287 let field = self.columns.first().map_or("", String::as_str);
288 let dim = self.dimension.unwrap_or(0);
289 let mut sql = format!(
290 "DEFINE INDEX{ine} {name} ON TABLE {table} COLUMNS {field} MTREE DIMENSION {dim}",
291 ine = ine,
292 name = self.name,
293 table = table,
294 field = field,
295 dim = dim,
296 );
297 if let Some(d) = self.distance {
298 write!(sql, " DIST {}", d.as_str()).expect("writing to String cannot fail");
299 }
300 if let Some(vt) = self.vector_type {
301 write!(sql, " TYPE {}", vt.as_str()).expect("writing to String cannot fail");
302 }
303 sql.push(';');
304 sql
305 }
306 IndexType::Hnsw => {
307 let field = self.columns.first().map_or("", String::as_str);
308 let dim = self.dimension.unwrap_or(0);
309 let mut sql = format!(
310 "DEFINE INDEX{ine} {name} ON TABLE {table} COLUMNS {field} HNSW DIMENSION {dim}",
311 ine = ine,
312 name = self.name,
313 table = table,
314 field = field,
315 dim = dim,
316 );
317 if let Some(d) = self.hnsw_distance {
318 write!(sql, " DIST {}", d.as_str()).expect("writing to String cannot fail");
319 }
320 if let Some(vt) = self.vector_type {
321 write!(sql, " TYPE {}", vt.as_str()).expect("writing to String cannot fail");
322 }
323 if let Some(efc) = self.efc {
324 write!(sql, " EFC {efc}").expect("writing to String cannot fail");
325 }
326 if let Some(m) = self.m {
327 write!(sql, " M {m}").expect("writing to String cannot fail");
328 }
329 sql.push(';');
330 sql
331 }
332 _ => {
333 let columns = self.columns.join(", ");
334 let mut sql = format!(
335 "DEFINE INDEX{ine} {name} ON TABLE {table} COLUMNS {columns}",
336 ine = ine,
337 name = self.name,
338 table = table,
339 columns = columns,
340 );
341 match self.index_type {
342 IndexType::Unique => sql.push_str(" UNIQUE"),
343 IndexType::Search => sql.push_str(" SEARCH ANALYZER ascii"),
344 _ => {}
345 }
346 sql.push(';');
347 sql
348 }
349 }
350 }
351}
352
353#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
355pub struct EventDefinition {
356 pub name: String,
358 pub condition: String,
360 pub action: String,
362}
363
364impl EventDefinition {
365 pub fn new(
367 name: impl Into<String>,
368 condition: impl Into<String>,
369 action: impl Into<String>,
370 ) -> Self {
371 Self {
372 name: name.into(),
373 condition: condition.into(),
374 action: action.into(),
375 }
376 }
377
378 pub fn validate(&self) -> Result<()> {
380 if self.name.is_empty() {
381 return Err(SurqlError::Validation {
382 reason: "Event name cannot be empty".into(),
383 });
384 }
385 if self.condition.is_empty() {
386 return Err(SurqlError::Validation {
387 reason: format!("Event {:?} must have a condition", self.name),
388 });
389 }
390 if self.action.is_empty() {
391 return Err(SurqlError::Validation {
392 reason: format!("Event {:?} must have an action", self.name),
393 });
394 }
395 Ok(())
396 }
397
398 pub fn to_surql(&self, table: &str) -> String {
400 self.to_surql_with_options(table, false)
401 }
402
403 pub fn to_surql_with_options(&self, table: &str, if_not_exists: bool) -> String {
405 let ine = if if_not_exists { " IF NOT EXISTS" } else { "" };
406 format!(
407 "DEFINE EVENT{ine} {name} ON TABLE {table} WHEN {cond} THEN {act};",
408 ine = ine,
409 name = self.name,
410 table = table,
411 cond = self.condition,
412 act = self.action,
413 )
414 }
415}
416
417#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
419pub struct TableDefinition {
420 pub name: String,
422 #[serde(default = "TableDefinition::default_mode")]
424 pub mode: TableMode,
425 #[serde(default)]
427 pub fields: Vec<FieldDefinition>,
428 #[serde(default)]
430 pub indexes: Vec<IndexDefinition>,
431 #[serde(default)]
433 pub events: Vec<EventDefinition>,
434 #[serde(skip_serializing_if = "Option::is_none", default)]
436 pub permissions: Option<BTreeMap<String, String>>,
437 #[serde(default)]
439 pub drop: bool,
440}
441
442impl TableDefinition {
443 fn default_mode() -> TableMode {
444 TableMode::Schemafull
445 }
446
447 pub fn new(name: impl Into<String>) -> Self {
449 Self {
450 name: name.into(),
451 mode: TableMode::Schemafull,
452 fields: Vec::new(),
453 indexes: Vec::new(),
454 events: Vec::new(),
455 permissions: None,
456 drop: false,
457 }
458 }
459
460 pub fn with_mode(mut self, mode: TableMode) -> Self {
462 self.mode = mode;
463 self
464 }
465
466 pub fn with_fields<I>(mut self, fields: I) -> Self
468 where
469 I: IntoIterator<Item = FieldDefinition>,
470 {
471 self.fields.extend(fields);
472 self
473 }
474
475 pub fn with_indexes<I>(mut self, indexes: I) -> Self
477 where
478 I: IntoIterator<Item = IndexDefinition>,
479 {
480 self.indexes.extend(indexes);
481 self
482 }
483
484 pub fn with_events<I>(mut self, events: I) -> Self
486 where
487 I: IntoIterator<Item = EventDefinition>,
488 {
489 self.events.extend(events);
490 self
491 }
492
493 pub fn with_permissions<I, K, V>(mut self, permissions: I) -> Self
495 where
496 I: IntoIterator<Item = (K, V)>,
497 K: Into<String>,
498 V: Into<String>,
499 {
500 self.permissions = Some(
501 permissions
502 .into_iter()
503 .map(|(k, v)| (k.into(), v.into()))
504 .collect(),
505 );
506 self
507 }
508
509 pub fn with_drop(mut self, drop: bool) -> Self {
511 self.drop = drop;
512 self
513 }
514
515 pub fn validate(&self) -> Result<()> {
517 if self.name.is_empty() {
518 return Err(SurqlError::Validation {
519 reason: "Table name cannot be empty".into(),
520 });
521 }
522 for field in &self.fields {
523 field.validate()?;
524 }
525 for index in &self.indexes {
526 index.validate()?;
527 }
528 for event in &self.events {
529 event.validate()?;
530 }
531 Ok(())
532 }
533
534 pub fn to_surql(&self) -> String {
536 self.to_surql_with_options(false)
537 }
538
539 pub fn to_surql_with_options(&self, if_not_exists: bool) -> String {
541 let ine = if if_not_exists { " IF NOT EXISTS" } else { "" };
542 format!(
543 "DEFINE TABLE{ine} {name} {mode};",
544 ine = ine,
545 name = self.name,
546 mode = self.mode.as_str(),
547 )
548 }
549
550 pub fn to_surql_all(&self) -> Vec<String> {
555 self.to_surql_all_with_options(false)
556 }
557
558 pub fn to_surql_all_with_options(&self, if_not_exists: bool) -> Vec<String> {
560 let mut out =
561 Vec::with_capacity(1 + self.fields.len() + self.indexes.len() + self.events.len());
562 out.push(self.to_surql_with_options(if_not_exists));
563 for field in &self.fields {
564 out.push(field.to_surql_with_options(&self.name, if_not_exists));
565 }
566 for index in &self.indexes {
567 out.push(index.to_surql_with_options(&self.name, if_not_exists));
568 }
569 for event in &self.events {
570 out.push(event.to_surql_with_options(&self.name, if_not_exists));
571 }
572 if let Some(perms) = &self.permissions {
573 for (action, rule) in perms {
574 out.push(format!(
575 "DEFINE FIELD PERMISSIONS FOR {action} ON TABLE {name} WHERE {rule};",
576 action = action.to_uppercase(),
577 name = self.name,
578 rule = rule,
579 ));
580 }
581 }
582 out
583 }
584}
585
586pub fn table_schema(name: impl Into<String>) -> TableDefinition {
588 TableDefinition::new(name)
589}
590
591pub fn index<I, S>(name: impl Into<String>, columns: I) -> IndexDefinition
593where
594 I: IntoIterator<Item = S>,
595 S: Into<String>,
596{
597 IndexDefinition::new(name, columns)
598}
599
600pub fn unique_index<I, S>(name: impl Into<String>, columns: I) -> IndexDefinition
602where
603 I: IntoIterator<Item = S>,
604 S: Into<String>,
605{
606 IndexDefinition::new(name, columns).with_type(IndexType::Unique)
607}
608
609pub fn search_index<I, S>(name: impl Into<String>, columns: I) -> IndexDefinition
611where
612 I: IntoIterator<Item = S>,
613 S: Into<String>,
614{
615 IndexDefinition::new(name, columns).with_type(IndexType::Search)
616}
617
618pub fn mtree_index(
620 name: impl Into<String>,
621 column: impl Into<String>,
622 dimension: u32,
623 distance: MTreeDistanceType,
624 vector_type: MTreeVectorType,
625) -> IndexDefinition {
626 IndexDefinition {
627 name: name.into(),
628 columns: vec![column.into()],
629 index_type: IndexType::Mtree,
630 dimension: Some(dimension),
631 distance: Some(distance),
632 vector_type: Some(vector_type),
633 hnsw_distance: None,
634 efc: None,
635 m: None,
636 }
637}
638
639pub fn hnsw_index(
644 name: impl Into<String>,
645 column: impl Into<String>,
646 dimension: u32,
647 distance: HnswDistanceType,
648 vector_type: MTreeVectorType,
649 efc: Option<u32>,
650 m: Option<u32>,
651) -> IndexDefinition {
652 IndexDefinition {
653 name: name.into(),
654 columns: vec![column.into()],
655 index_type: IndexType::Hnsw,
656 dimension: Some(dimension),
657 distance: None,
658 vector_type: Some(vector_type),
659 hnsw_distance: Some(distance),
660 efc,
661 m,
662 }
663}
664
665pub fn event(
667 name: impl Into<String>,
668 condition: impl Into<String>,
669 action: impl Into<String>,
670) -> EventDefinition {
671 EventDefinition::new(name, condition, action)
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677 use crate::schema::fields::{int_field, string_field};
678
679 #[test]
680 fn table_mode_strings() {
681 assert_eq!(TableMode::Schemafull.as_str(), "SCHEMAFULL");
682 assert_eq!(TableMode::Schemaless.as_str(), "SCHEMALESS");
683 assert_eq!(TableMode::Drop.as_str(), "DROP");
684 }
685
686 #[test]
687 fn table_mode_display() {
688 assert_eq!(format!("{}", TableMode::Schemafull), "SCHEMAFULL");
689 }
690
691 #[test]
692 fn table_mode_serializes_uppercase() {
693 let json = serde_json::to_string(&TableMode::Schemaless).unwrap();
694 assert_eq!(json, "\"SCHEMALESS\"");
695 }
696
697 #[test]
698 fn index_type_strings() {
699 assert_eq!(IndexType::Unique.as_str(), "UNIQUE");
700 assert_eq!(IndexType::Standard.as_str(), "INDEX");
701 assert_eq!(IndexType::Mtree.as_str(), "MTREE");
702 assert_eq!(IndexType::Hnsw.as_str(), "HNSW");
703 }
704
705 #[test]
706 fn mtree_distance_display() {
707 assert_eq!(format!("{}", MTreeDistanceType::Cosine), "COSINE");
708 }
709
710 #[test]
711 fn hnsw_distance_display() {
712 assert_eq!(format!("{}", HnswDistanceType::Chebyshev), "CHEBYSHEV");
713 }
714
715 #[test]
716 fn mtree_vector_type_display() {
717 assert_eq!(format!("{}", MTreeVectorType::F32), "F32");
718 }
719
720 #[test]
721 fn table_to_surql_schemafull() {
722 let t = table_schema("user");
723 assert_eq!(t.to_surql(), "DEFINE TABLE user SCHEMAFULL;");
724 }
725
726 #[test]
727 fn table_to_surql_schemaless() {
728 let t = table_schema("log").with_mode(TableMode::Schemaless);
729 assert_eq!(t.to_surql(), "DEFINE TABLE log SCHEMALESS;");
730 }
731
732 #[test]
733 fn table_to_surql_if_not_exists() {
734 let t = table_schema("user");
735 assert_eq!(
736 t.to_surql_with_options(true),
737 "DEFINE TABLE IF NOT EXISTS user SCHEMAFULL;"
738 );
739 }
740
741 #[test]
742 fn table_to_surql_all_includes_fields() {
743 let t = table_schema("user").with_fields([
744 string_field("name").build_unchecked().unwrap(),
745 int_field("age").build_unchecked().unwrap(),
746 ]);
747 let stmts = t.to_surql_all();
748 assert_eq!(stmts[0], "DEFINE TABLE user SCHEMAFULL;");
749 assert!(stmts
750 .iter()
751 .any(|s| s.contains("DEFINE FIELD name ON TABLE user TYPE string")));
752 assert!(stmts
753 .iter()
754 .any(|s| s.contains("DEFINE FIELD age ON TABLE user TYPE int")));
755 }
756
757 #[test]
758 fn table_to_surql_all_includes_unique_index() {
759 let t = table_schema("user").with_indexes([unique_index("email_idx", ["email"])]);
760 let stmts = t.to_surql_all();
761 assert!(stmts
762 .iter()
763 .any(|s| s == "DEFINE INDEX email_idx ON TABLE user COLUMNS email UNIQUE;"));
764 }
765
766 #[test]
767 fn table_to_surql_all_includes_event() {
768 let t = table_schema("user").with_events([event(
769 "email_changed",
770 "$before.email != $after.email",
771 "CREATE audit_log",
772 )]);
773 let stmts = t.to_surql_all();
774 assert!(stmts
775 .iter()
776 .any(|s| s.starts_with("DEFINE EVENT email_changed ON TABLE user")));
777 }
778
779 #[test]
780 fn table_permissions_render_upper() {
781 let t = table_schema("user").with_permissions([("select", "$auth.id = id")]);
782 let stmts = t.to_surql_all();
783 assert!(stmts
784 .iter()
785 .any(|s| s.contains("FOR SELECT") && s.contains("$auth.id = id")));
786 }
787
788 #[test]
789 fn index_new_defaults_to_standard() {
790 let idx = index("title_idx", ["title"]);
791 assert_eq!(idx.index_type, IndexType::Standard);
792 }
793
794 #[test]
795 fn unique_index_to_surql() {
796 let idx = unique_index("email_idx", ["email"]);
797 assert_eq!(
798 idx.to_surql("user"),
799 "DEFINE INDEX email_idx ON TABLE user COLUMNS email UNIQUE;"
800 );
801 }
802
803 #[test]
804 fn standard_index_to_surql() {
805 let idx = index("title_idx", ["title"]);
806 assert_eq!(
807 idx.to_surql("post"),
808 "DEFINE INDEX title_idx ON TABLE post COLUMNS title;"
809 );
810 }
811
812 #[test]
813 fn search_index_to_surql() {
814 let idx = search_index("content_search", ["title", "content"]);
815 assert_eq!(
816 idx.to_surql("post"),
817 "DEFINE INDEX content_search ON TABLE post COLUMNS title, content SEARCH ANALYZER ascii;"
818 );
819 }
820
821 #[test]
822 fn mtree_index_to_surql() {
823 let idx = mtree_index(
824 "embedding_idx",
825 "embedding",
826 1536,
827 MTreeDistanceType::Cosine,
828 MTreeVectorType::F32,
829 );
830 let sql = idx.to_surql("doc");
831 assert!(sql.contains(
832 "DEFINE INDEX embedding_idx ON TABLE doc COLUMNS embedding MTREE DIMENSION 1536"
833 ));
834 assert!(sql.contains("DIST COSINE"));
835 assert!(sql.contains("TYPE F32"));
836 }
837
838 #[test]
839 fn hnsw_index_to_surql_with_efc_m() {
840 let idx = hnsw_index(
841 "feat_idx",
842 "features",
843 128,
844 HnswDistanceType::Cosine,
845 MTreeVectorType::F32,
846 Some(500),
847 Some(16),
848 );
849 let sql = idx.to_surql("doc");
850 assert!(sql.contains("HNSW DIMENSION 128"));
851 assert!(sql.contains("DIST COSINE"));
852 assert!(sql.contains("TYPE F32"));
853 assert!(sql.contains("EFC 500"));
854 assert!(sql.contains("M 16"));
855 }
856
857 #[test]
858 fn hnsw_index_without_efc_m_omits_them() {
859 let idx = hnsw_index(
860 "feat_idx",
861 "features",
862 64,
863 HnswDistanceType::Euclidean,
864 MTreeVectorType::F64,
865 None,
866 None,
867 );
868 let sql = idx.to_surql("doc");
869 assert!(!sql.contains("EFC"));
870 assert!(!sql.contains("M 12"));
871 }
872
873 #[test]
874 fn index_to_surql_if_not_exists() {
875 let idx = unique_index("email_idx", ["email"]);
876 assert_eq!(
877 idx.to_surql_with_options("user", true),
878 "DEFINE INDEX IF NOT EXISTS email_idx ON TABLE user COLUMNS email UNIQUE;"
879 );
880 }
881
882 #[test]
883 fn event_to_surql() {
884 let ev = event(
885 "email_changed",
886 "$before.email != $after.email",
887 "CREATE audit_log SET user = $value.id",
888 );
889 assert_eq!(
890 ev.to_surql("user"),
891 "DEFINE EVENT email_changed ON TABLE user WHEN $before.email != $after.email \
892 THEN CREATE audit_log SET user = $value.id;"
893 );
894 }
895
896 #[test]
897 fn event_to_surql_if_not_exists() {
898 let ev = event("n", "true", "do");
899 assert!(ev
900 .to_surql_with_options("t", true)
901 .starts_with("DEFINE EVENT IF NOT EXISTS n ON TABLE t"));
902 }
903
904 #[test]
905 fn event_validate_rejects_empty() {
906 assert!(event("", "c", "a").validate().is_err());
907 assert!(event("n", "", "a").validate().is_err());
908 assert!(event("n", "c", "").validate().is_err());
909 }
910
911 #[test]
912 fn index_validate_rejects_empty_name() {
913 let mut idx = unique_index("x", ["a"]);
914 idx.name = String::new();
915 assert!(idx.validate().is_err());
916 }
917
918 #[test]
919 fn index_validate_rejects_empty_columns() {
920 let idx = IndexDefinition::new("x", Vec::<String>::new()).with_type(IndexType::Unique);
921 assert!(idx.validate().is_err());
922 }
923
924 #[test]
925 fn index_validate_mtree_requires_dimension() {
926 let mut idx = IndexDefinition::new("x", ["v"]).with_type(IndexType::Mtree);
927 assert!(idx.validate().is_err());
928 idx.dimension = Some(64);
929 assert!(idx.validate().is_ok());
930 }
931
932 #[test]
933 fn index_validate_hnsw_requires_dimension() {
934 let idx = IndexDefinition::new("x", ["v"]).with_type(IndexType::Hnsw);
935 assert!(idx.validate().is_err());
936 }
937
938 #[test]
939 fn table_validate_rejects_empty_name() {
940 assert!(table_schema("").validate().is_err());
941 }
942
943 #[test]
944 fn table_validate_propagates_field_errors() {
945 let t = table_schema("user").with_fields([FieldDefinition::new(
946 "1bad",
947 crate::schema::fields::FieldType::String,
948 )]);
949 assert!(t.validate().is_err());
950 }
951
952 #[test]
953 fn table_statement_order_defines_table_first() {
954 let t = table_schema("user")
955 .with_fields([string_field("name").build_unchecked().unwrap()])
956 .with_indexes([unique_index("name_idx", ["name"])]);
957 let stmts = t.to_surql_all();
958 assert!(stmts[0].starts_with("DEFINE TABLE"));
959 }
960
961 #[test]
962 fn minimal_table_returns_single_statement() {
963 let t = table_schema("empty");
964 assert_eq!(t.to_surql_all().len(), 1);
965 }
966
967 #[test]
968 fn table_definition_clone_eq() {
969 let t1 = table_schema("user").with_mode(TableMode::Schemafull);
970 let t2 = t1.clone();
971 assert_eq!(t1, t2);
972 }
973}