Skip to main content

uqa_graph/
age_names.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Apache AGE graph and label name validation.
8//!
9//! AGE validates names with anchored regular expressions built from the
10//! Unicode `ID_Start` and `ID_Continue` classes (`name_validation.c`):
11//!
12//! - graph names: `^[ID_Start_][ID_Continue.-]*[ID_Continue]$`, 3 to 63 bytes
13//! - label names: `^[ID_Start_][ID_Continue]*$`, 1 to 63 bytes
14//!
15//! Lengths are byte lengths because AGE measures them with `strlen`.
16
17use icu_properties::props::{IdContinue, IdStart};
18use icu_properties::CodePointSetData;
19
20/// AGE `MIN_GRAPH_NAME_LEN`.
21pub const MIN_GRAPH_NAME_LEN: usize = 3;
22/// AGE `MAX_GRAPH_NAME_LEN`.
23pub const MAX_GRAPH_NAME_LEN: usize = 63;
24/// AGE `MIN_LABEL_NAME_LEN`.
25pub const MIN_LABEL_NAME_LEN: usize = 1;
26/// AGE `MAX_LABEL_NAME_LEN` (`NAMEDATALEN - 1`).
27pub const MAX_LABEL_NAME_LEN: usize = 63;
28
29/// Name of the AGE default vertex label that every graph owns.
30pub const VERTEX_DEFAULT_LABEL_NAME: &str = "_ag_label_vertex";
31/// Name of the AGE default edge label that every graph owns.
32pub const EDGE_DEFAULT_LABEL_NAME: &str = "_ag_label_edge";
33
34fn is_id_start(c: char) -> bool {
35    c == '_' || CodePointSetData::new::<IdStart>().contains(c)
36}
37
38fn is_id_continue(c: char) -> bool {
39    CodePointSetData::new::<IdContinue>().contains(c)
40}
41
42/// AGE `is_valid_graph_name`: 3 to 63 bytes, an `ID_Start` character or
43/// underscore first, `ID_Continue` characters plus `.` and `-` in the
44/// middle, and an `ID_Continue` character last.
45#[must_use]
46pub fn is_valid_graph_name(name: &str) -> bool {
47    if name.len() < MIN_GRAPH_NAME_LEN || name.len() > MAX_GRAPH_NAME_LEN {
48        return false;
49    }
50    let mut chars = name.chars();
51    let Some(first) = chars.next() else {
52        return false;
53    };
54    if !is_id_start(first) {
55        return false;
56    }
57    let rest: Vec<char> = chars.collect();
58    // The anchored pattern needs a distinct last character, so a single
59    // multi-byte character that clears the byte minimum is still invalid.
60    let Some((last, middle)) = rest.split_last() else {
61        return false;
62    };
63    middle
64        .iter()
65        .all(|c| is_id_continue(*c) || *c == '.' || *c == '-')
66        && is_id_continue(*last)
67}
68
69/// AGE `is_valid_label_name`: 1 to 63 bytes, an `ID_Start` character or
70/// underscore first, and `ID_Continue` characters after it.
71#[must_use]
72pub fn is_valid_label_name(name: &str) -> bool {
73    if name.len() < MIN_LABEL_NAME_LEN || name.len() > MAX_LABEL_NAME_LEN {
74        return false;
75    }
76    let mut chars = name.chars();
77    chars.next().is_some_and(is_id_start) && chars.all(is_id_continue)
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn graph_names_follow_age_rules() {
86        assert!(is_valid_graph_name("demo"));
87        assert!(is_valid_graph_name("_g1"));
88        assert!(is_valid_graph_name("g_c1"));
89        assert!(is_valid_graph_name("my.graph-2"));
90        assert!(is_valid_graph_name("한글그래프"));
91        assert!(!is_valid_graph_name("g1"), "shorter than three bytes");
92        assert!(!is_valid_graph_name("1abc"), "must not start with a digit");
93        assert!(!is_valid_graph_name("abc."), "must not end with a dot");
94        assert!(!is_valid_graph_name("abc-"), "must not end with a dash");
95        assert!(!is_valid_graph_name("a b"), "no spaces");
96        assert!(
97            !is_valid_graph_name(&"x".repeat(64)),
98            "longer than 63 bytes"
99        );
100        assert!(is_valid_graph_name(&"x".repeat(63)));
101    }
102
103    #[test]
104    fn label_names_follow_age_rules() {
105        assert!(is_valid_label_name("Person"));
106        assert!(is_valid_label_name("_"));
107        assert!(is_valid_label_name("v1"));
108        assert!(is_valid_label_name("KNOWS"));
109        assert!(is_valid_label_name(VERTEX_DEFAULT_LABEL_NAME));
110        assert!(!is_valid_label_name(""));
111        assert!(!is_valid_label_name("1v"));
112        assert!(!is_valid_label_name("has-dash"));
113        assert!(!is_valid_label_name("has.dot"));
114        assert!(!is_valid_label_name(&"x".repeat(64)));
115    }
116}