Skip to main content

cratestack_core/schema/
composite_unique.rs

1//! Parsing for the model-level `@@unique([...])` composite-unique
2//! attribute (Prisma's spelling). Shares its syntax rules with
3//! `@@id([...])` via [`super::field_list`]; what differs is the
4//! semantics — a composite unique constraint, not the primary key —
5//! and the error text pointing at the single-field alternative.
6
7use super::field_list::{FieldListSpec, parse_field_list};
8
9const SPEC: FieldListSpec = FieldListSpec {
10    prefix: "@@unique(",
11    label: "composite unique attribute",
12    example: "@@unique([field1, field2])",
13};
14
15/// Parses `@@unique([field1, field2, ...])` into its ordered list of
16/// local field names. Callers are responsible for checking that each
17/// name resolves to a real scalar field on the model.
18///
19/// The order is significant: it is the column order of the emitted
20/// unique index, and it feeds the index name
21/// (`<table>_<col1>_<col2>_key`), so reordering the list is a rename.
22pub fn parse_composite_unique_attribute(raw: &str) -> Result<Vec<String>, String> {
23    let fields = parse_field_list(raw, &SPEC)?;
24    if fields.len() < 2 {
25        return Err(format!(
26            "composite unique attribute `{raw}` must list at least two fields; use a field-level `@unique` instead"
27        ));
28    }
29    Ok(fields)
30}
31
32#[cfg(test)]
33mod tests {
34    use super::parse_composite_unique_attribute;
35
36    #[test]
37    fn parses_two_fields() {
38        let fields = parse_composite_unique_attribute("@@unique([tenantId, name])").unwrap();
39        assert_eq!(fields, vec!["tenantId".to_string(), "name".to_string()]);
40    }
41
42    #[test]
43    fn parses_three_fields_in_declared_order() {
44        let fields =
45            parse_composite_unique_attribute("@@unique([tenantId, name, environment])").unwrap();
46        assert_eq!(
47            fields,
48            vec![
49                "tenantId".to_string(),
50                "name".to_string(),
51                "environment".to_string(),
52            ]
53        );
54    }
55
56    #[test]
57    fn rejects_missing_brackets() {
58        let error = parse_composite_unique_attribute("@@unique(tenantId, name)").unwrap_err();
59        assert!(error.contains("must list fields as"), "error: {error}");
60        assert!(
61            error.contains("composite unique attribute"),
62            "error: {error}"
63        );
64    }
65
66    #[test]
67    fn rejects_single_field() {
68        let error = parse_composite_unique_attribute("@@unique([email])").unwrap_err();
69        assert!(error.contains("at least two fields"), "error: {error}");
70        assert!(error.contains("field-level `@unique`"), "error: {error}");
71    }
72
73    #[test]
74    fn rejects_duplicate_field() {
75        let error = parse_composite_unique_attribute("@@unique([name, name])").unwrap_err();
76        assert!(error.contains("more than once"), "error: {error}");
77    }
78
79    #[test]
80    fn rejects_invalid_identifier() {
81        let error = parse_composite_unique_attribute("@@unique([tenant-id, name])").unwrap_err();
82        assert!(error.contains("invalid field name"), "error: {error}");
83    }
84
85    #[test]
86    fn rejects_other_attributes() {
87        let error = parse_composite_unique_attribute("@@id([a, b])").unwrap_err();
88        assert!(error.contains("unsupported composite unique attribute"));
89    }
90}