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}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148pub struct Index {
149 pub name: String,
150 pub columns: Vec<String>,
151 pub unique: bool,
152 #[serde(default)]
155 pub kind: IndexKind,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160pub struct UniqueConstraint {
161 pub name: String,
162 pub columns: Vec<String>,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167pub struct ForeignKey {
168 pub name: String,
169 pub columns: Vec<String>,
170 pub references_table: String,
171 pub references_columns: Vec<String>,
172 #[serde(default)]
173 pub on_delete: ForeignKeyAction,
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
178#[serde(rename_all = "snake_case")]
179pub enum ForeignKeyAction {
180 #[default]
181 Restrict,
182 Cascade,
183 SetNull,
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
188pub struct CheckConstraint {
189 pub name: String,
190 pub expr: String,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct Sequence {
196 pub name: String,
197 pub next_value: i64,
198}
199
200#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
202pub struct Table {
203 pub id: u32,
205 pub name: String,
206 pub columns: Vec<Column>,
207 pub primary_key: Vec<String>,
208 #[serde(default, skip_serializing_if = "Vec::is_empty")]
209 pub indexes: Vec<Index>,
210 #[serde(default, skip_serializing_if = "Vec::is_empty")]
211 pub foreign_keys: Vec<ForeignKey>,
212 #[serde(default, skip_serializing_if = "Vec::is_empty")]
213 pub unique_constraints: Vec<UniqueConstraint>,
214 #[serde(default, skip_serializing_if = "Vec::is_empty")]
215 pub check_constraints: Vec<CheckConstraint>,
216}
217
218impl Table {
219 pub fn column(&self, name: &str) -> Option<&Column> {
221 self.columns.iter().find(|c| c.name == name)
222 }
223
224 pub fn is_pk_column(&self, name: &str) -> bool {
226 self.primary_key.iter().any(|c| c == name)
227 }
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
232pub enum SchemaError {
233 #[error("duplicate table name \"{0}\"")]
234 DuplicateTableName(String),
235 #[error("duplicate table id {0}")]
236 DuplicateTableId(u32),
237 #[error("duplicate column name \"{1}\" in table \"{0}\"")]
238 DuplicateColumnName(String, String),
239 #[error("duplicate column id {1} in table \"{0}\"")]
240 DuplicateColumnId(String, u32),
241 #[error("primary key column \"{1}\" not found in table \"{0}\"")]
242 MissingPrimaryKeyColumn(String, String),
243 #[error("index \"{1}\" references unknown column \"{2}\" in table \"{0}\"")]
244 MissingIndexColumn(String, String, String),
245 #[error("unique constraint \"{1}\" references unknown column \"{2}\" in table \"{0}\"")]
246 MissingUniqueColumn(String, String, String),
247 #[error("foreign key \"{1}\" references unknown column \"{2}\" in table \"{0}\"")]
248 MissingForeignKeyColumn(String, String, String),
249 #[error("foreign key \"{1}\" references unknown table \"{2}\"")]
250 MissingReferencedTable(String, String, String),
251 #[error("foreign key \"{1}\" references unknown column \"{2}\" on table \"{3}\"")]
252 MissingReferencedColumn(String, String, String, String),
253}
254
255#[derive(Debug, Clone, PartialEq, Serialize)]
257pub struct Schema {
258 pub tables: Vec<Table>,
259 by_name: HashMap<String, usize>,
260 by_id: HashMap<u32, usize>,
261}
262
263impl<'de> serde::Deserialize<'de> for Schema {
264 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
265 where
266 D: serde::Deserializer<'de>,
267 {
268 #[derive(serde::Deserialize)]
269 struct SchemaHelper {
270 tables: Vec<Table>,
271 }
272 let helper = SchemaHelper::deserialize(deserializer)?;
273 Schema::new(helper.tables).map_err(serde::de::Error::custom)
274 }
275}
276
277fn synthesize_unique_from_indexes(table: &mut Table) {
282 let mut synthesized: Vec<UniqueConstraint> = Vec::new();
283 for idx in &table.indexes {
284 if !idx.unique {
285 continue;
286 }
287 let covered = table
288 .unique_constraints
289 .iter()
290 .chain(synthesized.iter())
291 .any(|u| u.columns == idx.columns);
292 if !covered {
293 synthesized.push(UniqueConstraint {
294 name: idx.name.clone(),
295 columns: idx.columns.clone(),
296 });
297 }
298 }
299 table.unique_constraints.extend(synthesized);
300}
301
302impl Schema {
303 pub fn new(mut tables: Vec<Table>) -> Result<Self, SchemaError> {
305 for table in &mut tables {
306 synthesize_unique_from_indexes(table);
307 }
308
309 let mut by_name = HashMap::with_capacity(tables.len());
310 let mut by_id = HashMap::with_capacity(tables.len());
311
312 for (idx, table) in tables.iter().enumerate() {
313 if by_name.contains_key(&table.name) {
314 return Err(SchemaError::DuplicateTableName(table.name.clone()));
315 }
316 if by_id.contains_key(&table.id) {
317 return Err(SchemaError::DuplicateTableId(table.id));
318 }
319 by_name.insert(table.name.clone(), idx);
320 by_id.insert(table.id, idx);
321 }
322
323 for table in &tables {
324 Self::validate_table(table, &by_name)?;
325 }
326
327 Ok(Self {
328 tables,
329 by_name,
330 by_id,
331 })
332 }
333
334 fn validate_table(
335 table: &Table,
336 table_names: &HashMap<String, usize>,
337 ) -> Result<(), SchemaError> {
338 let mut column_names = HashMap::with_capacity(table.columns.len());
339 let mut column_ids = HashMap::with_capacity(table.columns.len());
340
341 for col in &table.columns {
342 if column_names.contains_key(&col.name) {
343 return Err(SchemaError::DuplicateColumnName(
344 table.name.clone(),
345 col.name.clone(),
346 ));
347 }
348 if column_ids.contains_key(&col.id) {
349 return Err(SchemaError::DuplicateColumnId(table.name.clone(), col.id));
350 }
351 column_names.insert(col.name.clone(), col.id);
352 column_ids.insert(col.id, col.name.clone());
353 }
354
355 for pk in &table.primary_key {
356 if !column_names.contains_key(pk) {
357 return Err(SchemaError::MissingPrimaryKeyColumn(
358 table.name.clone(),
359 pk.clone(),
360 ));
361 }
362 }
363
364 for idx in &table.indexes {
365 for col in &idx.columns {
366 if !column_names.contains_key(col) {
367 return Err(SchemaError::MissingIndexColumn(
368 table.name.clone(),
369 idx.name.clone(),
370 col.clone(),
371 ));
372 }
373 }
374 }
375
376 for uq in &table.unique_constraints {
377 for col in &uq.columns {
378 if !column_names.contains_key(col) {
379 return Err(SchemaError::MissingUniqueColumn(
380 table.name.clone(),
381 uq.name.clone(),
382 col.clone(),
383 ));
384 }
385 }
386 }
387
388 for fk in &table.foreign_keys {
389 for col in &fk.columns {
390 if !column_names.contains_key(col) {
391 return Err(SchemaError::MissingForeignKeyColumn(
392 table.name.clone(),
393 fk.name.clone(),
394 col.clone(),
395 ));
396 }
397 }
398 if !table_names.contains_key(&fk.references_table) {
399 return Err(SchemaError::MissingReferencedTable(
400 table.name.clone(),
401 fk.name.clone(),
402 fk.references_table.clone(),
403 ));
404 }
405 }
406
407 Ok(())
408 }
409
410 pub fn table(&self, name: &str) -> Option<&Table> {
412 self.by_name.get(name).map(|&idx| &self.tables[idx])
413 }
414
415 pub fn table_by_id(&self, id: u32) -> Option<&Table> {
417 self.by_id.get(&id).map(|&idx| &self.tables[idx])
418 }
419
420 pub fn has_table(&self, name: &str) -> bool {
422 self.by_name.contains_key(name)
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 fn make_table(name: &str, id: u32) -> Table {
431 Table {
432 id,
433 name: name.into(),
434 columns: vec![Column::new(1, "id", ColumnType::Int64)],
435 primary_key: vec!["id".into()],
436 indexes: vec![],
437 foreign_keys: vec![],
438 unique_constraints: vec![],
439 check_constraints: vec![],
440 }
441 }
442
443 #[test]
444 fn schema_rejects_duplicate_table_name() {
445 let err = Schema::new(vec![make_table("a", 1), make_table("a", 2)]).unwrap_err();
446 assert!(matches!(err, SchemaError::DuplicateTableName(n) if n == "a"));
447 }
448
449 #[test]
450 fn schema_rejects_duplicate_table_id() {
451 let err = Schema::new(vec![make_table("a", 1), make_table("b", 1)]).unwrap_err();
452 assert!(matches!(err, SchemaError::DuplicateTableId(1)));
453 }
454
455 #[test]
456 fn schema_rejects_missing_pk_column() {
457 let t = Table {
458 id: 1,
459 name: "t".into(),
460 columns: vec![Column::new(1, "x", ColumnType::Text)],
461 primary_key: vec!["id".into()],
462 indexes: vec![],
463 foreign_keys: vec![],
464 unique_constraints: vec![],
465 check_constraints: vec![],
466 };
467 let err = Schema::new(vec![t]).unwrap_err();
468 assert!(matches!(err, SchemaError::MissingPrimaryKeyColumn(_, _)));
469 }
470
471 #[test]
472 fn unique_index_synthesizes_unique_constraint() {
473 let schema = Schema::new(vec![Table {
474 id: 1,
475 name: "users".into(),
476 columns: vec![
477 Column::new(1, "id", ColumnType::Int64),
478 Column::new(2, "email", ColumnType::Text),
479 Column::new(3, "handle", ColumnType::Text),
480 ],
481 primary_key: vec!["id".into()],
482 indexes: vec![
483 Index {
484 name: "idx_email".into(),
485 columns: vec!["email".into()],
486 unique: true,
487 kind: Default::default(),
488 },
489 Index {
491 name: "idx_handle".into(),
492 columns: vec!["handle".into()],
493 unique: false,
494 kind: Default::default(),
495 },
496 ],
497 foreign_keys: vec![],
498 unique_constraints: vec![],
499 check_constraints: vec![],
500 }])
501 .unwrap();
502 let table = schema.table("users").unwrap();
503 assert_eq!(table.unique_constraints.len(), 1);
504 assert_eq!(
505 table.unique_constraints[0].columns,
506 vec!["email".to_string()]
507 );
508 }
509
510 #[test]
511 fn unique_index_does_not_duplicate_existing_constraint() {
512 let schema = Schema::new(vec![Table {
513 id: 1,
514 name: "users".into(),
515 columns: vec![
516 Column::new(1, "id", ColumnType::Int64),
517 Column::new(2, "email", ColumnType::Text),
518 ],
519 primary_key: vec!["id".into()],
520 indexes: vec![Index {
521 name: "idx_email".into(),
522 columns: vec!["email".into()],
523 unique: true,
524 kind: Default::default(),
525 }],
526 foreign_keys: vec![],
527 unique_constraints: vec![UniqueConstraint {
528 name: "uq_email".into(),
529 columns: vec!["email".into()],
530 }],
531 check_constraints: vec![],
532 }])
533 .unwrap();
534 let table = schema.table("users").unwrap();
536 assert_eq!(table.unique_constraints.len(), 1);
537 assert_eq!(table.unique_constraints[0].name, "uq_email");
538 }
539
540 #[test]
541 fn schema_roundtrips_json() {
542 let schema = Schema::new(vec![Table {
543 id: 1,
544 name: "users".into(),
545 columns: vec![
546 Column::new(1, "id", ColumnType::Int64),
547 Column {
548 nullable: true,
549 ..Column::new(2, "email", ColumnType::Text)
550 },
551 ],
552 primary_key: vec!["id".into()],
553 indexes: vec![Index {
554 name: "idx_email".into(),
555 columns: vec!["email".into()],
556 unique: true,
557 kind: Default::default(),
558 }],
559 foreign_keys: vec![],
560 unique_constraints: vec![],
561 check_constraints: vec![CheckConstraint {
562 name: "chk_id_positive".into(),
563 expr: "id > 0".into(),
564 }],
565 }])
566 .unwrap();
567
568 let json = serde_json::to_string(&schema).unwrap();
569 let decoded: Schema = serde_json::from_str(&json).unwrap();
570 assert_eq!(decoded.tables.len(), 1);
571 assert_eq!(decoded.table("users").unwrap().columns.len(), 2);
572 }
573}