1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum ColumnType {
13 Bool,
14 Int8,
15 Int16,
16 Int32,
17 Int64,
18 Float32,
19 Float64,
20 Text,
21 Bytes,
22 Json,
23 Date,
24 DateTime,
25 TimestampNanos,
26 Date64,
27 Time64,
28 Interval,
29 Decimal128,
30 Uuid,
32 JsonNative,
34 Array,
36 Embedding,
39 Sparse,
42}
43
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46#[serde(rename_all = "snake_case")]
47pub enum DefaultKind {
48 Static(serde_json::Value),
50 Now,
52 Uuid,
54 Sequence(String),
56 CustomName(String),
58}
59
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
62pub struct Column {
63 pub id: u32,
65 pub name: String,
67 pub storage_type: ColumnType,
69 pub application_type: ColumnType,
71 pub nullable: bool,
73 pub primary_key: bool,
75 pub default: Option<DefaultKind>,
77 pub generated: bool,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub enum_values: Option<Vec<String>>,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub min: Option<f64>,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub max: Option<f64>,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub min_length: Option<usize>,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub max_length: Option<usize>,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub regex: Option<String>,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub check_expr: Option<String>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub embedding_dim: Option<u32>,
103 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
105 pub encrypted: bool,
106 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
109 pub encrypted_indexable: bool,
110}
111
112impl Column {
113 pub fn new(id: u32, name: impl Into<String>, storage_type: ColumnType) -> Self {
115 Self {
116 id,
117 name: name.into(),
118 storage_type,
119 application_type: storage_type,
120 nullable: false,
121 primary_key: false,
122 default: None,
123 generated: false,
124 enum_values: None,
125 min: None,
126 max: None,
127 min_length: None,
128 max_length: None,
129 regex: None,
130 check_expr: None,
131 embedding_dim: None,
132 encrypted: false,
133 encrypted_indexable: false,
134 }
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
140#[serde(rename_all = "snake_case")]
141pub enum IndexKind {
142 #[default]
144 Bitmap,
145 Fm,
147 Ann,
149 Sparse,
151 MinHash,
154 LearnedRange,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161pub struct Index {
162 pub name: String,
163 pub columns: Vec<String>,
164 pub unique: bool,
165 #[serde(default)]
168 pub kind: IndexKind,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173pub struct UniqueConstraint {
174 pub name: String,
175 pub columns: Vec<String>,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
180pub struct ForeignKey {
181 pub name: String,
182 pub columns: Vec<String>,
183 pub references_table: String,
184 pub references_columns: Vec<String>,
185 #[serde(default)]
186 pub on_delete: ForeignKeyAction,
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
191#[serde(rename_all = "snake_case")]
192pub enum ForeignKeyAction {
193 #[default]
194 Restrict,
195 Cascade,
196 SetNull,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
201pub struct CheckConstraint {
202 pub name: String,
203 pub expr: String,
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
208pub struct Sequence {
209 pub name: String,
210 pub next_value: i64,
211}
212
213#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
215pub struct Table {
216 pub id: u32,
218 pub name: String,
219 pub columns: Vec<Column>,
220 pub primary_key: Vec<String>,
221 #[serde(default, skip_serializing_if = "Vec::is_empty")]
222 pub indexes: Vec<Index>,
223 #[serde(default, skip_serializing_if = "Vec::is_empty")]
224 pub foreign_keys: Vec<ForeignKey>,
225 #[serde(default, skip_serializing_if = "Vec::is_empty")]
226 pub unique_constraints: Vec<UniqueConstraint>,
227 #[serde(default, skip_serializing_if = "Vec::is_empty")]
228 pub check_constraints: Vec<CheckConstraint>,
229}
230
231impl Table {
232 pub fn column(&self, name: &str) -> Option<&Column> {
234 self.columns.iter().find(|c| c.name == name)
235 }
236
237 pub fn is_pk_column(&self, name: &str) -> bool {
239 self.primary_key.iter().any(|c| c == name)
240 }
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
245pub enum SchemaError {
246 #[error("duplicate table name \"{0}\"")]
247 DuplicateTableName(String),
248 #[error("duplicate table id {0}")]
249 DuplicateTableId(u32),
250 #[error("duplicate column name \"{1}\" in table \"{0}\"")]
251 DuplicateColumnName(String, String),
252 #[error("duplicate column id {1} in table \"{0}\"")]
253 DuplicateColumnId(String, u32),
254 #[error("primary key column \"{1}\" not found in table \"{0}\"")]
255 MissingPrimaryKeyColumn(String, String),
256 #[error("index \"{1}\" references unknown column \"{2}\" in table \"{0}\"")]
257 MissingIndexColumn(String, String, String),
258 #[error("unique constraint \"{1}\" references unknown column \"{2}\" in table \"{0}\"")]
259 MissingUniqueColumn(String, String, String),
260 #[error("foreign key \"{1}\" references unknown column \"{2}\" in table \"{0}\"")]
261 MissingForeignKeyColumn(String, String, String),
262 #[error("foreign key \"{1}\" references unknown table \"{2}\"")]
263 MissingReferencedTable(String, String, String),
264 #[error("foreign key \"{1}\" references unknown column \"{2}\" on table \"{3}\"")]
265 MissingReferencedColumn(String, String, String, String),
266}
267
268#[derive(Debug, Clone, PartialEq, Serialize)]
270pub struct Schema {
271 pub tables: Vec<Table>,
272 by_name: HashMap<String, usize>,
273 by_id: HashMap<u32, usize>,
274}
275
276impl<'de> serde::Deserialize<'de> for Schema {
277 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
278 where
279 D: serde::Deserializer<'de>,
280 {
281 #[derive(serde::Deserialize)]
282 struct SchemaHelper {
283 tables: Vec<Table>,
284 }
285 let helper = SchemaHelper::deserialize(deserializer)?;
286 Schema::new(helper.tables).map_err(serde::de::Error::custom)
287 }
288}
289
290fn synthesize_unique_from_indexes(table: &mut Table) {
295 let mut synthesized: Vec<UniqueConstraint> = Vec::new();
296 for idx in &table.indexes {
297 if !idx.unique {
298 continue;
299 }
300 let covered = table
301 .unique_constraints
302 .iter()
303 .chain(synthesized.iter())
304 .any(|u| u.columns == idx.columns);
305 if !covered {
306 synthesized.push(UniqueConstraint {
307 name: idx.name.clone(),
308 columns: idx.columns.clone(),
309 });
310 }
311 }
312 table.unique_constraints.extend(synthesized);
313}
314
315impl Schema {
316 pub fn new(mut tables: Vec<Table>) -> Result<Self, SchemaError> {
318 for table in &mut tables {
319 synthesize_unique_from_indexes(table);
320 }
321
322 let mut by_name = HashMap::with_capacity(tables.len());
323 let mut by_id = HashMap::with_capacity(tables.len());
324
325 for (idx, table) in tables.iter().enumerate() {
326 if by_name.contains_key(&table.name) {
327 return Err(SchemaError::DuplicateTableName(table.name.clone()));
328 }
329 if by_id.contains_key(&table.id) {
330 return Err(SchemaError::DuplicateTableId(table.id));
331 }
332 by_name.insert(table.name.clone(), idx);
333 by_id.insert(table.id, idx);
334 }
335
336 for table in &tables {
337 Self::validate_table(table, &by_name)?;
338 }
339
340 Ok(Self {
341 tables,
342 by_name,
343 by_id,
344 })
345 }
346
347 fn validate_table(
348 table: &Table,
349 table_names: &HashMap<String, usize>,
350 ) -> Result<(), SchemaError> {
351 let mut column_names = HashMap::with_capacity(table.columns.len());
352 let mut column_ids = HashMap::with_capacity(table.columns.len());
353
354 for col in &table.columns {
355 if column_names.contains_key(&col.name) {
356 return Err(SchemaError::DuplicateColumnName(
357 table.name.clone(),
358 col.name.clone(),
359 ));
360 }
361 if column_ids.contains_key(&col.id) {
362 return Err(SchemaError::DuplicateColumnId(table.name.clone(), col.id));
363 }
364 column_names.insert(col.name.clone(), col.id);
365 column_ids.insert(col.id, col.name.clone());
366 }
367
368 for pk in &table.primary_key {
369 if !column_names.contains_key(pk) {
370 return Err(SchemaError::MissingPrimaryKeyColumn(
371 table.name.clone(),
372 pk.clone(),
373 ));
374 }
375 }
376
377 for idx in &table.indexes {
378 for col in &idx.columns {
379 if !column_names.contains_key(col) {
380 return Err(SchemaError::MissingIndexColumn(
381 table.name.clone(),
382 idx.name.clone(),
383 col.clone(),
384 ));
385 }
386 }
387 }
388
389 for uq in &table.unique_constraints {
390 for col in &uq.columns {
391 if !column_names.contains_key(col) {
392 return Err(SchemaError::MissingUniqueColumn(
393 table.name.clone(),
394 uq.name.clone(),
395 col.clone(),
396 ));
397 }
398 }
399 }
400
401 for fk in &table.foreign_keys {
402 for col in &fk.columns {
403 if !column_names.contains_key(col) {
404 return Err(SchemaError::MissingForeignKeyColumn(
405 table.name.clone(),
406 fk.name.clone(),
407 col.clone(),
408 ));
409 }
410 }
411 if !table_names.contains_key(&fk.references_table) {
412 return Err(SchemaError::MissingReferencedTable(
413 table.name.clone(),
414 fk.name.clone(),
415 fk.references_table.clone(),
416 ));
417 }
418 }
419
420 Ok(())
421 }
422
423 pub fn table(&self, name: &str) -> Option<&Table> {
425 self.by_name.get(name).map(|&idx| &self.tables[idx])
426 }
427
428 pub fn table_by_id(&self, id: u32) -> Option<&Table> {
430 self.by_id.get(&id).map(|&idx| &self.tables[idx])
431 }
432
433 pub fn has_table(&self, name: &str) -> bool {
435 self.by_name.contains_key(name)
436 }
437
438 pub fn rename_table(&mut self, from: &str, to: &str) -> bool {
443 if from == to {
444 return self.has_table(from);
445 }
446 if !self.has_table(from) || self.has_table(to) {
447 return false;
448 }
449 let idx = *self.by_name.get(from).unwrap();
450 self.tables[idx].name = to.to_string();
451 self.by_name.remove(from);
452 self.by_name.insert(to.to_string(), idx);
453 true
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460
461 fn make_table(name: &str, id: u32) -> Table {
462 Table {
463 id,
464 name: name.into(),
465 columns: vec![Column::new(1, "id", ColumnType::Int64)],
466 primary_key: vec!["id".into()],
467 indexes: vec![],
468 foreign_keys: vec![],
469 unique_constraints: vec![],
470 check_constraints: vec![],
471 }
472 }
473
474 #[test]
475 fn schema_rejects_duplicate_table_name() {
476 let err = Schema::new(vec![make_table("a", 1), make_table("a", 2)]).unwrap_err();
477 assert!(matches!(err, SchemaError::DuplicateTableName(n) if n == "a"));
478 }
479
480 #[test]
481 fn schema_rejects_duplicate_table_id() {
482 let err = Schema::new(vec![make_table("a", 1), make_table("b", 1)]).unwrap_err();
483 assert!(matches!(err, SchemaError::DuplicateTableId(1)));
484 }
485
486 #[test]
487 fn schema_rejects_missing_pk_column() {
488 let t = Table {
489 id: 1,
490 name: "t".into(),
491 columns: vec![Column::new(1, "x", ColumnType::Text)],
492 primary_key: vec!["id".into()],
493 indexes: vec![],
494 foreign_keys: vec![],
495 unique_constraints: vec![],
496 check_constraints: vec![],
497 };
498 let err = Schema::new(vec![t]).unwrap_err();
499 assert!(matches!(err, SchemaError::MissingPrimaryKeyColumn(_, _)));
500 }
501
502 #[test]
503 fn unique_index_synthesizes_unique_constraint() {
504 let schema = Schema::new(vec![Table {
505 id: 1,
506 name: "users".into(),
507 columns: vec![
508 Column::new(1, "id", ColumnType::Int64),
509 Column::new(2, "email", ColumnType::Text),
510 Column::new(3, "handle", ColumnType::Text),
511 ],
512 primary_key: vec!["id".into()],
513 indexes: vec![
514 Index {
515 name: "idx_email".into(),
516 columns: vec!["email".into()],
517 unique: true,
518 kind: Default::default(),
519 },
520 Index {
522 name: "idx_handle".into(),
523 columns: vec!["handle".into()],
524 unique: false,
525 kind: Default::default(),
526 },
527 ],
528 foreign_keys: vec![],
529 unique_constraints: vec![],
530 check_constraints: vec![],
531 }])
532 .unwrap();
533 let table = schema.table("users").unwrap();
534 assert_eq!(table.unique_constraints.len(), 1);
535 assert_eq!(
536 table.unique_constraints[0].columns,
537 vec!["email".to_string()]
538 );
539 }
540
541 #[test]
542 fn unique_index_does_not_duplicate_existing_constraint() {
543 let schema = Schema::new(vec![Table {
544 id: 1,
545 name: "users".into(),
546 columns: vec![
547 Column::new(1, "id", ColumnType::Int64),
548 Column::new(2, "email", ColumnType::Text),
549 ],
550 primary_key: vec!["id".into()],
551 indexes: vec![Index {
552 name: "idx_email".into(),
553 columns: vec!["email".into()],
554 unique: true,
555 kind: Default::default(),
556 }],
557 foreign_keys: vec![],
558 unique_constraints: vec![UniqueConstraint {
559 name: "uq_email".into(),
560 columns: vec!["email".into()],
561 }],
562 check_constraints: vec![],
563 }])
564 .unwrap();
565 let table = schema.table("users").unwrap();
567 assert_eq!(table.unique_constraints.len(), 1);
568 assert_eq!(table.unique_constraints[0].name, "uq_email");
569 }
570
571 #[test]
572 fn schema_roundtrips_json() {
573 let schema = Schema::new(vec![Table {
574 id: 1,
575 name: "users".into(),
576 columns: vec![
577 Column::new(1, "id", ColumnType::Int64),
578 Column {
579 nullable: true,
580 ..Column::new(2, "email", ColumnType::Text)
581 },
582 ],
583 primary_key: vec!["id".into()],
584 indexes: vec![Index {
585 name: "idx_email".into(),
586 columns: vec!["email".into()],
587 unique: true,
588 kind: Default::default(),
589 }],
590 foreign_keys: vec![],
591 unique_constraints: vec![],
592 check_constraints: vec![CheckConstraint {
593 name: "chk_id_positive".into(),
594 expr: "id > 0".into(),
595 }],
596 }])
597 .unwrap();
598
599 let json = serde_json::to_string(&schema).unwrap();
600 let decoded: Schema = serde_json::from_str(&json).unwrap();
601 assert_eq!(decoded.tables.len(), 1);
602 assert_eq!(decoded.table("users").unwrap().columns.len(), 2);
603 }
604}