eventuary_postgres/
relation.rs1use eventuary_core::{Error, Result};
2
3#[derive(Debug, Clone, Eq, PartialEq, Hash)]
4pub struct PgRelationName {
5 schema: Option<String>,
6 table: String,
7}
8
9impl PgRelationName {
10 pub fn new(name: impl Into<String>) -> Result<Self> {
11 let raw = name.into();
12 let parts: Vec<&str> = raw.split('.').collect();
13 match parts.len() {
14 1 => {
15 validate_identifier(parts[0])?;
16 Ok(Self {
17 schema: None,
18 table: parts[0].to_owned(),
19 })
20 }
21 2 => {
22 validate_identifier(parts[0])?;
23 validate_identifier(parts[1])?;
24 Ok(Self {
25 schema: Some(parts[0].to_owned()),
26 table: parts[1].to_owned(),
27 })
28 }
29 _ => Err(Error::Config(format!(
30 "invalid postgres relation name: {raw}"
31 ))),
32 }
33 }
34
35 pub fn schema(&self) -> Option<&str> {
36 self.schema.as_deref()
37 }
38
39 pub fn table(&self) -> &str {
40 &self.table
41 }
42
43 pub fn render(&self) -> String {
44 match &self.schema {
45 Some(schema) => format!("\"{}\".\"{}\"", schema, self.table),
46 None => format!("\"{}\"", self.table),
47 }
48 }
49}
50
51fn validate_identifier(identifier: &str) -> Result<()> {
52 if identifier.is_empty() || identifier.len() > 63 {
53 return Err(Error::Config(format!(
54 "invalid postgres identifier length: {identifier:?}"
55 )));
56 }
57 let mut chars = identifier.chars();
58 let Some(first) = chars.next() else {
59 return Err(Error::Config("empty postgres identifier".to_owned()));
60 };
61 if !(first == '_' || first.is_ascii_alphabetic()) {
62 return Err(Error::Config(format!(
63 "postgres identifier must start with letter or underscore: {identifier:?}"
64 )));
65 }
66 if !chars.all(|c| c == '_' || c.is_ascii_alphanumeric()) {
67 return Err(Error::Config(format!(
68 "postgres identifier may only contain letters, digits, underscores: {identifier:?}"
69 )));
70 }
71 Ok(())
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn renders_simple_relation() {
80 let relation = PgRelationName::new("events").unwrap();
81 assert_eq!(relation.render(), "\"events\"");
82 }
83
84 #[test]
85 fn renders_schema_qualified_relation() {
86 let relation = PgRelationName::new("eventuary.events").unwrap();
87 assert_eq!(relation.render(), "\"eventuary\".\"events\"");
88 }
89
90 #[test]
91 fn rejects_invalid_relation() {
92 assert!(PgRelationName::new("events;drop").is_err());
93 assert!(PgRelationName::new("public.events.extra").is_err());
94 assert!(PgRelationName::new("9events").is_err());
95 assert!(PgRelationName::new("").is_err());
96 }
97}