1use cratefield_core::ConfigError;
15use serde_json::Value;
16
17pub const MAX_IDENTIFIER_CHARS: usize = 63;
21
22pub const RESERVED_PREFIXES: &[&str] = &["harness_", "sqlite_", "pg_", "cf_"];
26
27pub const RESERVED_WORDS: &[&str] = &[
53 "abort",
54 "action",
55 "add",
56 "after",
57 "all",
58 "alter",
59 "always",
60 "analyse",
61 "analyze",
62 "and",
63 "any",
64 "array",
65 "as",
66 "asc",
67 "asymmetric",
68 "attach",
69 "authorization",
70 "autoincrement",
71 "before",
72 "begin",
73 "between",
74 "binary",
75 "both",
76 "by",
77 "cascade",
78 "case",
79 "cast",
80 "check",
81 "cmax",
82 "cmin",
83 "collate",
84 "collation",
85 "column",
86 "commit",
87 "concurrently",
88 "conflict",
89 "constraint",
90 "create",
91 "cross",
92 "ctid",
93 "current",
94 "current_catalog",
95 "current_date",
96 "current_role",
97 "current_schema",
98 "current_time",
99 "current_timestamp",
100 "current_user",
101 "database",
102 "default",
103 "deferrable",
104 "deferred",
105 "delete",
106 "desc",
107 "detach",
108 "distinct",
109 "do",
110 "drop",
111 "each",
112 "else",
113 "end",
114 "escape",
115 "except",
116 "exclude",
117 "exclusive",
118 "exists",
119 "explain",
120 "fail",
121 "false",
122 "fetch",
123 "filter",
124 "first",
125 "following",
126 "for",
127 "foreign",
128 "freeze",
129 "from",
130 "full",
131 "generated",
132 "glob",
133 "grant",
134 "group",
135 "groups",
136 "having",
137 "if",
138 "ignore",
139 "ilike",
140 "immediate",
141 "in",
142 "index",
143 "indexed",
144 "initially",
145 "inner",
146 "insert",
147 "instead",
148 "intersect",
149 "into",
150 "is",
151 "isnull",
152 "join",
153 "key",
154 "last",
155 "lateral",
156 "leading",
157 "left",
158 "like",
159 "limit",
160 "localtime",
161 "localtimestamp",
162 "match",
163 "materialized",
164 "natural",
165 "no",
166 "not",
167 "nothing",
168 "notnull",
169 "null",
170 "nulls",
171 "of",
172 "offset",
173 "on",
174 "only",
175 "or",
176 "order",
177 "others",
178 "outer",
179 "over",
180 "overlaps",
181 "partition",
182 "placing",
183 "plan",
184 "pragma",
185 "preceding",
186 "primary",
187 "query",
188 "raise",
189 "range",
190 "recursive",
191 "references",
192 "regexp",
193 "reindex",
194 "release",
195 "rename",
196 "replace",
197 "restrict",
198 "returning",
199 "right",
200 "rollback",
201 "row",
202 "rows",
203 "savepoint",
204 "select",
205 "session_user",
206 "set",
207 "similar",
208 "some",
209 "symmetric",
210 "system_user",
211 "table",
212 "tableoid",
213 "tablesample",
214 "temp",
215 "temporary",
216 "then",
217 "ties",
218 "to",
219 "trailing",
220 "transaction",
221 "trigger",
222 "true",
223 "unbounded",
224 "union",
225 "unique",
226 "update",
227 "user",
228 "using",
229 "vacuum",
230 "values",
231 "variadic",
232 "verbose",
233 "view",
234 "virtual",
235 "when",
236 "where",
237 "window",
238 "with",
239 "without",
240 "xmax",
241 "xmin",
242];
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub enum TextFormat {
249 Email,
253 Url,
256}
257
258impl TextFormat {
259 #[must_use]
261 pub fn as_str(self) -> &'static str {
262 match self {
263 TextFormat::Email => "email",
264 TextFormat::Url => "url",
265 }
266 }
267}
268
269#[derive(Debug, Clone, PartialEq)]
272pub enum FieldKind {
273 Text {
276 min_len: Option<u32>,
277 max_len: Option<u32>,
278 format: Option<TextFormat>,
279 },
280 Integer { min: Option<i64>, max: Option<i64> },
282 Real { min: Option<f64>, max: Option<f64> },
284 Boolean,
286 Timestamp,
289 Uuid,
291 Json,
293 Enum { values: Vec<String> },
295}
296
297impl FieldKind {
298 #[must_use]
316 pub fn can_hold_a_subject(&self) -> bool {
317 matches!(self, FieldKind::Text { .. } | FieldKind::Uuid)
318 }
319
320 #[must_use]
322 pub fn as_str(&self) -> &'static str {
323 match self {
324 FieldKind::Text { .. } => "text",
325 FieldKind::Integer { .. } => "integer",
326 FieldKind::Real { .. } => "real",
327 FieldKind::Boolean => "boolean",
328 FieldKind::Timestamp => "timestamp",
329 FieldKind::Uuid => "uuid",
330 FieldKind::Json => "json",
331 FieldKind::Enum { .. } => "enum",
332 }
333 }
334
335 #[must_use]
337 pub fn text() -> Self {
338 FieldKind::Text {
339 min_len: None,
340 max_len: None,
341 format: None,
342 }
343 }
344
345 #[must_use]
347 pub fn integer() -> Self {
348 FieldKind::Integer {
349 min: None,
350 max: None,
351 }
352 }
353
354 #[must_use]
356 pub fn real() -> Self {
357 FieldKind::Real {
358 min: None,
359 max: None,
360 }
361 }
362}
363
364#[derive(Debug, Clone, PartialEq)]
366pub struct FieldDef {
367 pub name: String,
369 pub kind: FieldKind,
370 pub required: bool,
373 pub unique: bool,
376 pub indexed: bool,
378 pub default: Option<Value>,
380}
381
382impl FieldDef {
383 #[must_use]
385 pub fn new(name: impl Into<String>, kind: FieldKind) -> Self {
386 Self {
387 name: name.into(),
388 kind,
389 required: false,
390 unique: false,
391 indexed: false,
392 default: None,
393 }
394 }
395
396 #[must_use]
397 pub fn required(mut self) -> Self {
398 self.required = true;
399 self
400 }
401
402 #[must_use]
403 pub fn unique(mut self) -> Self {
404 self.unique = true;
405 self
406 }
407
408 #[must_use]
409 pub fn indexed(mut self) -> Self {
410 self.indexed = true;
411 self
412 }
413
414 #[must_use]
415 pub fn default_value(mut self, value: Value) -> Self {
416 self.default = Some(value);
417 self
418 }
419
420 #[must_use]
423 pub fn must_be_present(&self) -> bool {
424 self.required && self.default.is_none()
425 }
426}
427
428#[derive(Debug, Clone, PartialEq, Eq)]
431pub struct ForeignKey {
432 pub field: String,
434 pub references: String,
437}
438
439impl ForeignKey {
440 #[must_use]
441 pub fn new(field: impl Into<String>, references: impl Into<String>) -> Self {
442 Self {
443 field: field.into(),
444 references: references.into(),
445 }
446 }
447}
448
449#[derive(Debug, Clone, PartialEq)]
451pub struct TableDef {
452 pub name: String,
454 pub fields: Vec<FieldDef>,
457 pub primary_key: Vec<String>,
460 pub foreign_keys: Vec<ForeignKey>,
461}
462
463impl TableDef {
464 #[must_use]
466 pub fn new(
467 name: impl Into<String>,
468 primary_key: impl Into<String>,
469 fields: Vec<FieldDef>,
470 ) -> Self {
471 Self {
472 name: name.into(),
473 fields,
474 primary_key: vec![primary_key.into()],
475 foreign_keys: Vec::new(),
476 }
477 }
478
479 #[must_use]
480 pub fn foreign_key(mut self, key: ForeignKey) -> Self {
481 self.foreign_keys.push(key);
482 self
483 }
484
485 #[must_use]
487 pub fn field(&self, name: &str) -> Option<&FieldDef> {
488 self.fields.iter().find(|field| field.name == name)
489 }
490
491 #[must_use]
493 pub fn is_primary_key(&self, name: &str) -> bool {
494 self.primary_key.iter().any(|key| key == name)
495 }
496}
497
498#[derive(Debug, Clone, PartialEq, Default)]
500pub struct Schema {
501 pub tables: Vec<TableDef>,
505}
506
507impl Schema {
508 #[must_use]
510 pub fn new(tables: Vec<TableDef>) -> Self {
511 let mut tables = tables;
512 tables.sort_by(|a, b| a.name.cmp(&b.name));
513 Self { tables }
514 }
515
516 #[must_use]
519 pub fn is_empty(&self) -> bool {
520 self.tables.is_empty()
521 }
522
523 #[must_use]
525 pub fn table(&self, name: &str) -> Option<&TableDef> {
526 self.tables.iter().find(|table| table.name == name)
527 }
528
529 pub fn validate(&self) -> Result<(), ConfigError> {
546 let mut errors = ConfigError::new();
547 let mut seen_tables: Vec<String> = Vec::new();
548
549 for table in &self.tables {
550 check_identifier(&mut errors, "table", &table.name, &table.name);
551 if seen_tables.iter().any(|seen| seen == &table.name) {
552 errors.push(format!("table `{}`: declared twice", table.name));
553 }
554 seen_tables.push(table.name.clone());
555 self.validate_table(&mut errors, table);
556 }
557
558 self.validate_index_names(&mut errors);
559 self.validate_no_reference_cycle(&mut errors);
560
561 errors.into_result()
562 }
563
564 fn validate_index_names(&self, errors: &mut ConfigError) {
572 let mut generated: Vec<(String, String, String)> = Vec::new();
573 for table in &self.tables {
574 for field in &table.fields {
575 if !crate::ddl::needs_own_index(table, field) {
576 continue;
577 }
578 let index = crate::ddl::index_name(&table.name, &field.name);
579 if let Some((_, first_table, first_field)) =
580 generated.iter().find(|(name, _, _)| name == &index)
581 {
582 errors.push(format!(
583 "table `{}`, field `{}`: the generated index name `{index}` is already \
584 generated by table `{first_table}`, field `{first_field}`",
585 table.name, field.name
586 ));
587 }
588 if self.table(&index).is_some() {
589 errors.push(format!(
590 "table `{}`, field `{}`: the generated index name `{index}` is also a \
591 declared table name, and both engines keep indexes and tables in one \
592 namespace",
593 table.name, field.name
594 ));
595 }
596 generated.push((index, table.name.clone(), field.name.clone()));
597 }
598 }
599 }
600
601 fn validate_no_reference_cycle(&self, errors: &mut ConfigError) {
606 let (_, cyclic) = crate::ddl::creation_order(self);
607 let Some(first) = cyclic.first() else {
608 return;
609 };
610 let names: Vec<&str> = cyclic.iter().map(|table| table.name.as_str()).collect();
611 let path = self.walk_cycle(&first.name, &names);
612 errors.push(format!(
613 "table `{}`: the foreign keys form a reference cycle, {}, so there is no order that \
614 creates a table after the tables it references",
615 first.name,
616 path.join(" -> ")
617 ));
618 }
619
620 fn walk_cycle(&self, start: &str, unresolved: &[&str]) -> Vec<String> {
625 let mut path: Vec<&str> = Vec::new();
626 let mut current = start;
627 loop {
628 if let Some(at) = path.iter().position(|name| *name == current) {
629 let mut cycle: Vec<String> =
630 path[at..].iter().map(|&name| name.to_owned()).collect();
631 cycle.push(current.to_owned());
632 return cycle;
633 }
634 path.push(current);
635 let next = self.table(current).and_then(|table| {
636 table
637 .foreign_keys
638 .iter()
639 .map(|key| key.references.as_str())
640 .filter(|name| unresolved.contains(name))
641 .min()
642 });
643 match next {
644 Some(next) => current = next,
645 None => return path.iter().map(|&name| name.to_owned()).collect(),
648 }
649 }
650 }
651
652 fn validate_table(&self, errors: &mut ConfigError, table: &TableDef) {
653 if table.fields.is_empty() {
654 errors.push(format!("table `{}`: declares no fields", table.name));
655 }
656
657 let mut seen_fields: Vec<String> = Vec::new();
658 for field in &table.fields {
659 check_identifier(errors, "field", &field.name, &table.name);
660 if seen_fields.iter().any(|seen| seen == &field.name) {
661 errors.push(format!(
662 "table `{}`: field `{}` is declared twice",
663 table.name, field.name
664 ));
665 }
666 seen_fields.push(field.name.clone());
667 validate_field(errors, table, field);
668 }
669
670 validate_primary_key(errors, table);
671
672 for key in &table.foreign_keys {
673 self.validate_foreign_key(errors, table, key);
674 }
675 }
676
677 fn validate_foreign_key(&self, errors: &mut ConfigError, table: &TableDef, key: &ForeignKey) {
678 let Some(local) = table.field(&key.field) else {
679 errors.push(format!(
680 "table `{}`: foreign key on `{}`, which is not a declared field",
681 table.name, key.field
682 ));
683 return;
684 };
685 let Some(target) = self.table(&key.references) else {
686 errors.push(format!(
687 "table `{}`: foreign key `{}` references table `{}`, which is not declared",
688 table.name, key.field, key.references
689 ));
690 return;
691 };
692 let [target_key] = target.primary_key.as_slice() else {
693 errors.push(format!(
694 "table `{}`: foreign key `{}` references table `{}`, whose primary key is not a \
695 single column",
696 table.name, key.field, target.name
697 ));
698 return;
699 };
700 let Some(target_field) = target.field(target_key) else {
701 return;
703 };
704 if local.kind.as_str() != target_field.kind.as_str() {
705 errors.push(format!(
706 "table `{}`: foreign key `{}` is {} but `{}.{}` is {}",
707 table.name,
708 key.field,
709 local.kind.as_str(),
710 target.name,
711 target_field.name,
712 target_field.kind.as_str()
713 ));
714 }
715 }
716}
717
718fn validate_primary_key(errors: &mut ConfigError, table: &TableDef) {
719 if table.primary_key.is_empty() {
720 errors.push(format!("table `{}`: no primary key", table.name));
721 return;
722 }
723 let mut seen: Vec<&String> = Vec::new();
724 for key in &table.primary_key {
725 match table.field(key) {
726 None => errors.push(format!(
727 "table `{}`: primary key names `{key}`, which is not a declared field",
728 table.name
729 )),
730 Some(field) if !field.required && field.default.is_none() => errors.push(format!(
737 "table `{}`: primary key `{key}` must be required or have a default, because a \
738 primary-key column is NOT NULL",
739 table.name
740 )),
741 Some(_) => {}
742 }
743 if seen.contains(&key) {
744 errors.push(format!(
745 "table `{}`: primary key names `{key}` twice",
746 table.name
747 ));
748 }
749 seen.push(key);
750 }
751}
752
753fn validate_field(errors: &mut ConfigError, table: &TableDef, field: &FieldDef) {
754 let at = format!("table `{}`, field `{}`", table.name, field.name);
755 match &field.kind {
756 FieldKind::Text {
757 min_len, max_len, ..
758 } => {
759 if let (Some(min), Some(max)) = (min_len, max_len)
760 && min > max
761 {
762 errors.push(format!("{at}: min_len {min} is greater than max_len {max}"));
763 }
764 if *max_len == Some(0) {
765 errors.push(format!("{at}: max_len 0 accepts nothing"));
766 }
767 }
768 FieldKind::Integer { min, max } => {
769 if let (Some(min), Some(max)) = (min, max)
770 && min > max
771 {
772 errors.push(format!("{at}: min {min} is greater than max {max}"));
773 }
774 }
775 FieldKind::Real { min, max } => {
776 if let (Some(min), Some(max)) = (min, max)
777 && min > max
778 {
779 errors.push(format!("{at}: min {min} is greater than max {max}"));
780 }
781 for (label, bound) in [("min", min), ("max", max)] {
782 if bound.is_some_and(|value| !value.is_finite()) {
783 errors.push(format!("{at}: {label} is not a finite number"));
784 }
785 }
786 }
787 FieldKind::Enum { values } => {
788 if values.is_empty() {
789 errors.push(format!("{at}: an enum must declare at least one value"));
790 }
791 let mut seen: Vec<&String> = Vec::new();
792 for value in values {
793 if seen.contains(&value) {
794 errors.push(format!("{at}: enum value `{value}` is declared twice"));
795 }
796 seen.push(value);
797 if let Some(position) = value.chars().position(char::is_control) {
807 errors.push(format!(
808 "{at}: enum value `{}` holds a control character at position {position}",
809 value.escape_debug()
810 ));
811 }
812 }
813 }
814 FieldKind::Boolean | FieldKind::Timestamp | FieldKind::Uuid | FieldKind::Json => {}
815 }
816
817 if let Some(text) = field.default.as_ref().and_then(serde_json::Value::as_str)
823 && let Some(position) = text.chars().position(char::is_control)
824 {
825 errors.push(format!(
826 "{at}: the default `{}` holds a control character at position {position}",
827 text.escape_debug()
828 ));
829 }
830 if let Some(default) = &field.default
831 && let Err(error) = crate::value::check_value(&field.kind, default)
832 {
833 errors.push(format!("{at}: the default {default} {}", error.message));
834 }
835
836 if crate::ddl::needs_own_index(table, field) {
839 let index = crate::ddl::index_name(&table.name, &field.name);
840 if index.chars().count() > MAX_IDENTIFIER_CHARS {
841 errors.push(format!(
842 "{at}: the generated index name `{index}` is longer than \
843 {MAX_IDENTIFIER_CHARS} characters"
844 ));
845 }
846 }
847}
848
849fn check_identifier(errors: &mut ConfigError, what: &str, name: &str, at: &str) {
852 let where_ = if what == "table" {
853 format!("table `{name}`")
854 } else {
855 format!("table `{at}`, field `{name}`")
856 };
857
858 if !is_identifier(name) {
859 errors.push(format!(
860 "{where_}: a {what} name must match [a-z][a-z0-9_]* and hold no double or trailing \
861 underscore"
862 ));
863 } else if name.chars().count() > MAX_IDENTIFIER_CHARS {
864 errors.push(format!(
865 "{where_}: a {what} name is at most {MAX_IDENTIFIER_CHARS} characters"
866 ));
867 }
868
869 if RESERVED_WORDS.contains(&name) {
870 errors.push(format!("{where_}: `{name}` is a reserved SQL word"));
871 }
872 if let Some(prefix) = RESERVED_PREFIXES
873 .iter()
874 .find(|prefix| name.starts_with(*prefix))
875 {
876 errors.push(format!(
877 "{where_}: the `{prefix}` prefix is reserved for the harness"
878 ));
879 }
880 if let Some(fragment) = cratefield_core::card_data_hit(name) {
883 errors.push(format!(
884 "{where_}: `{fragment}` looks like card data. With a normal Stripe integration the \
885 card never reaches a backend, so store only Stripe's identifiers"
886 ));
887 }
888}
889
890#[must_use]
894pub fn is_identifier(name: &str) -> bool {
895 let mut chars = name.chars();
896 let Some(first) = chars.next() else {
897 return false;
898 };
899 if !first.is_ascii_lowercase() {
900 return false;
901 }
902 if name.ends_with('_') || name.contains("__") {
903 return false;
904 }
905 chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
906}