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