Skip to main content

postrust_graphql/
types.rs

1//! PostgreSQL to GraphQL type mapping.
2
3use std::fmt;
4
5/// Represents a GraphQL type.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum GraphQLType {
8    /// GraphQL Int (32-bit signed integer)
9    Int,
10    /// GraphQL Float (double-precision floating point)
11    Float,
12    /// GraphQL String
13    String,
14    /// GraphQL Boolean
15    Boolean,
16    /// GraphQL ID
17    Id,
18    /// Custom BigInt scalar (64-bit integer)
19    BigInt,
20    /// Custom BigDecimal scalar (arbitrary precision)
21    BigDecimal,
22    /// Custom JSON scalar
23    Json,
24    /// Custom UUID scalar
25    Uuid,
26    /// Custom Date scalar
27    Date,
28    /// Custom DateTime scalar
29    DateTime,
30    /// Custom Time scalar
31    Time,
32    /// List type wrapping another type
33    List(Box<GraphQLType>),
34    /// Custom/unknown type (falls back to String)
35    Custom(std::string::String),
36}
37
38impl fmt::Display for GraphQLType {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            GraphQLType::Int => write!(f, "Int"),
42            GraphQLType::Float => write!(f, "Float"),
43            GraphQLType::String => write!(f, "String"),
44            GraphQLType::Boolean => write!(f, "Boolean"),
45            GraphQLType::Id => write!(f, "ID"),
46            GraphQLType::BigInt => write!(f, "BigInt"),
47            GraphQLType::BigDecimal => write!(f, "BigDecimal"),
48            GraphQLType::Json => write!(f, "JSON"),
49            GraphQLType::Uuid => write!(f, "UUID"),
50            GraphQLType::Date => write!(f, "Date"),
51            GraphQLType::DateTime => write!(f, "DateTime"),
52            GraphQLType::Time => write!(f, "Time"),
53            GraphQLType::List(inner) => write!(f, "[{}]", inner),
54            GraphQLType::Custom(name) => write!(f, "{}", name),
55        }
56    }
57}
58
59/// Maps a PostgreSQL type name to a GraphQL type.
60pub fn pg_type_to_graphql(pg_type: &str) -> GraphQLType {
61    // Normalize the type name
62    let normalized = pg_type.to_lowercase().trim().to_string();
63
64    // Check for array types first
65    if let Some(inner_type) = normalized.strip_prefix('_') {
66        // PostgreSQL array types start with underscore (e.g., _int4)
67        return GraphQLType::List(Box::new(pg_type_to_graphql(inner_type)));
68    }
69
70    if normalized.ends_with("[]") {
71        // Alternative array syntax (e.g., integer[])
72        let inner_type = normalized.trim_end_matches("[]");
73        return GraphQLType::List(Box::new(pg_type_to_graphql(inner_type)));
74    }
75
76    match normalized.as_str() {
77        // Integer types
78        "integer" | "int" | "int4" | "smallint" | "int2" => GraphQLType::Int,
79
80        // BigInt types
81        "bigint" | "int8" => GraphQLType::BigInt,
82
83        // Float types
84        "real" | "float4" | "double precision" | "float8" => GraphQLType::Float,
85
86        // Numeric/Decimal types
87        "numeric" | "decimal" => GraphQLType::BigDecimal,
88
89        // Boolean
90        "boolean" | "bool" => GraphQLType::Boolean,
91
92        // String types
93        "text" | "varchar" | "character varying" | "char" | "character" | "bpchar" => {
94            GraphQLType::String
95        }
96
97        // JSON types
98        "json" | "jsonb" => GraphQLType::Json,
99
100        // UUID
101        "uuid" => GraphQLType::Uuid,
102
103        // Date/Time types
104        "timestamp"
105        | "timestamp without time zone"
106        | "timestamptz"
107        | "timestamp with time zone" => GraphQLType::DateTime,
108        "date" => GraphQLType::Date,
109        "time" | "time without time zone" | "timetz" | "time with time zone" => GraphQLType::Time,
110
111        // Default to String for unknown types
112        _ => GraphQLType::String,
113    }
114}
115
116/// Check if a PostgreSQL type is nullable in GraphQL context.
117pub fn is_nullable_type(nullable: bool, is_pk: bool) -> bool {
118    // Primary keys are never null in GraphQL
119    if is_pk {
120        return false;
121    }
122    nullable
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use pretty_assertions::assert_eq;
129
130    #[test]
131    fn test_pg_to_graphql_integer_types() {
132        assert_eq!(pg_type_to_graphql("integer"), GraphQLType::Int);
133        assert_eq!(pg_type_to_graphql("int4"), GraphQLType::Int);
134        assert_eq!(pg_type_to_graphql("int"), GraphQLType::Int);
135        assert_eq!(pg_type_to_graphql("smallint"), GraphQLType::Int);
136        assert_eq!(pg_type_to_graphql("int2"), GraphQLType::Int);
137    }
138
139    #[test]
140    fn test_pg_to_graphql_bigint() {
141        assert_eq!(pg_type_to_graphql("bigint"), GraphQLType::BigInt);
142        assert_eq!(pg_type_to_graphql("int8"), GraphQLType::BigInt);
143    }
144
145    #[test]
146    fn test_pg_to_graphql_float_types() {
147        assert_eq!(pg_type_to_graphql("real"), GraphQLType::Float);
148        assert_eq!(pg_type_to_graphql("float4"), GraphQLType::Float);
149        assert_eq!(pg_type_to_graphql("double precision"), GraphQLType::Float);
150        assert_eq!(pg_type_to_graphql("float8"), GraphQLType::Float);
151    }
152
153    #[test]
154    fn test_pg_to_graphql_numeric_types() {
155        assert_eq!(pg_type_to_graphql("numeric"), GraphQLType::BigDecimal);
156        assert_eq!(pg_type_to_graphql("decimal"), GraphQLType::BigDecimal);
157    }
158
159    #[test]
160    fn test_pg_to_graphql_string_types() {
161        assert_eq!(pg_type_to_graphql("text"), GraphQLType::String);
162        assert_eq!(pg_type_to_graphql("varchar"), GraphQLType::String);
163        assert_eq!(pg_type_to_graphql("character varying"), GraphQLType::String);
164        assert_eq!(pg_type_to_graphql("char"), GraphQLType::String);
165        assert_eq!(pg_type_to_graphql("bpchar"), GraphQLType::String);
166    }
167
168    #[test]
169    fn test_pg_to_graphql_boolean() {
170        assert_eq!(pg_type_to_graphql("boolean"), GraphQLType::Boolean);
171        assert_eq!(pg_type_to_graphql("bool"), GraphQLType::Boolean);
172    }
173
174    #[test]
175    fn test_pg_to_graphql_json() {
176        assert_eq!(pg_type_to_graphql("json"), GraphQLType::Json);
177        assert_eq!(pg_type_to_graphql("jsonb"), GraphQLType::Json);
178    }
179
180    #[test]
181    fn test_pg_to_graphql_uuid() {
182        assert_eq!(pg_type_to_graphql("uuid"), GraphQLType::Uuid);
183    }
184
185    #[test]
186    fn test_pg_to_graphql_datetime_types() {
187        assert_eq!(pg_type_to_graphql("timestamp"), GraphQLType::DateTime);
188        assert_eq!(pg_type_to_graphql("timestamptz"), GraphQLType::DateTime);
189        assert_eq!(
190            pg_type_to_graphql("timestamp with time zone"),
191            GraphQLType::DateTime
192        );
193        assert_eq!(
194            pg_type_to_graphql("timestamp without time zone"),
195            GraphQLType::DateTime
196        );
197    }
198
199    #[test]
200    fn test_pg_to_graphql_date() {
201        assert_eq!(pg_type_to_graphql("date"), GraphQLType::Date);
202    }
203
204    #[test]
205    fn test_pg_to_graphql_time() {
206        assert_eq!(pg_type_to_graphql("time"), GraphQLType::Time);
207        assert_eq!(pg_type_to_graphql("timetz"), GraphQLType::Time);
208        assert_eq!(pg_type_to_graphql("time with time zone"), GraphQLType::Time);
209    }
210
211    #[test]
212    fn test_pg_to_graphql_array_types_underscore() {
213        assert_eq!(
214            pg_type_to_graphql("_int4"),
215            GraphQLType::List(Box::new(GraphQLType::Int))
216        );
217        assert_eq!(
218            pg_type_to_graphql("_text"),
219            GraphQLType::List(Box::new(GraphQLType::String))
220        );
221        assert_eq!(
222            pg_type_to_graphql("_uuid"),
223            GraphQLType::List(Box::new(GraphQLType::Uuid))
224        );
225    }
226
227    #[test]
228    fn test_pg_to_graphql_array_types_bracket() {
229        assert_eq!(
230            pg_type_to_graphql("integer[]"),
231            GraphQLType::List(Box::new(GraphQLType::Int))
232        );
233        assert_eq!(
234            pg_type_to_graphql("text[]"),
235            GraphQLType::List(Box::new(GraphQLType::String))
236        );
237    }
238
239    #[test]
240    fn test_pg_to_graphql_unknown_defaults_to_string() {
241        assert_eq!(pg_type_to_graphql("customtype"), GraphQLType::String);
242        assert_eq!(pg_type_to_graphql("my_domain"), GraphQLType::String);
243    }
244
245    #[test]
246    fn test_pg_to_graphql_case_insensitive() {
247        assert_eq!(pg_type_to_graphql("INTEGER"), GraphQLType::Int);
248        assert_eq!(pg_type_to_graphql("Text"), GraphQLType::String);
249        assert_eq!(pg_type_to_graphql("BOOLEAN"), GraphQLType::Boolean);
250    }
251
252    #[test]
253    fn test_graphql_type_display() {
254        assert_eq!(format!("{}", GraphQLType::Int), "Int");
255        assert_eq!(format!("{}", GraphQLType::String), "String");
256        assert_eq!(
257            format!("{}", GraphQLType::List(Box::new(GraphQLType::Int))),
258            "[Int]"
259        );
260    }
261
262    #[test]
263    fn test_is_nullable_type() {
264        // PK is never nullable
265        assert!(!is_nullable_type(true, true));
266        assert!(!is_nullable_type(false, true));
267
268        // Non-PK follows the nullable flag
269        assert!(is_nullable_type(true, false));
270        assert!(!is_nullable_type(false, false));
271    }
272}