Skip to main content

cratestack_core/schema/
composite_key.rs

1//! Parsing for the model-level `@@id([...])` composite-primary-key
2//! attribute (Prisma's spelling). Mirrors [`crate::events::parse_emit_attribute`]'s
3//! shape: syntax parsing lives here in `cratestack-core` so both the
4//! parser's semantic checker and any other consumer share one
5//! implementation. The bracketed-field-list syntax itself is shared
6//! with `@@unique([...])` — see [`super::field_list`].
7
8use super::field_list::{FieldListSpec, parse_field_list};
9
10const SPEC: FieldListSpec = FieldListSpec {
11    prefix: "@@id(",
12    label: "composite id attribute",
13    example: "@@id([field1, field2])",
14};
15
16/// Parses `@@id([field1, field2, ...])` into its ordered list of local
17/// field names. Callers are responsible for checking that each name
18/// resolves to a real scalar field on the model.
19pub fn parse_composite_id_attribute(raw: &str) -> Result<Vec<String>, String> {
20    let fields = parse_field_list(raw, &SPEC)?;
21    if fields.len() < 2 {
22        return Err(format!(
23            "composite id attribute `{raw}` must list at least two fields; use a single-field `@id` instead"
24        ));
25    }
26    Ok(fields)
27}
28
29#[cfg(test)]
30mod tests {
31    use super::parse_composite_id_attribute;
32
33    #[test]
34    fn parses_two_fields() {
35        let fields = parse_composite_id_attribute("@@id([accountId, subject])").unwrap();
36        assert_eq!(fields, vec!["accountId".to_string(), "subject".to_string()]);
37    }
38
39    #[test]
40    fn rejects_missing_brackets() {
41        let error = parse_composite_id_attribute("@@id(accountId, subject)").unwrap_err();
42        assert!(error.contains("must list fields as"));
43    }
44
45    #[test]
46    fn rejects_single_field() {
47        let error = parse_composite_id_attribute("@@id([accountId])").unwrap_err();
48        assert!(error.contains("at least two fields"));
49    }
50
51    #[test]
52    fn rejects_duplicate_field() {
53        let error = parse_composite_id_attribute("@@id([accountId, accountId])").unwrap_err();
54        assert!(error.contains("more than once"));
55    }
56
57    #[test]
58    fn rejects_invalid_identifier() {
59        let error = parse_composite_id_attribute("@@id([account-id, subject])").unwrap_err();
60        assert!(error.contains("invalid field name"));
61    }
62}