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