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 normalized.starts_with('_') {
66        // PostgreSQL array types start with underscore (e.g., _int4)
67        let inner_type = &normalized[1..];
68        return GraphQLType::List(Box::new(pg_type_to_graphql(inner_type)));
69    }
70
71    if normalized.ends_with("[]") {
72        // Alternative array syntax (e.g., integer[])
73        let inner_type = normalized.trim_end_matches("[]");
74        return GraphQLType::List(Box::new(pg_type_to_graphql(inner_type)));
75    }
76
77    match normalized.as_str() {
78        // Integer types
79        "integer" | "int" | "int4" | "smallint" | "int2" => GraphQLType::Int,
80
81        // BigInt types
82        "bigint" | "int8" => GraphQLType::BigInt,
83
84        // Float types
85        "real" | "float4" | "double precision" | "float8" => GraphQLType::Float,
86
87        // Numeric/Decimal types
88        "numeric" | "decimal" => GraphQLType::BigDecimal,
89
90        // Boolean
91        "boolean" | "bool" => GraphQLType::Boolean,
92
93        // String types
94        "text" | "varchar" | "character varying" | "char" | "character" | "bpchar" => {
95            GraphQLType::String
96        }
97
98        // JSON types
99        "json" | "jsonb" => GraphQLType::Json,
100
101        // UUID
102        "uuid" => GraphQLType::Uuid,
103
104        // Date/Time types
105        "timestamp" | "timestamp without time zone" | "timestamptz"
106        | "timestamp with time zone" => GraphQLType::DateTime,
107        "date" => GraphQLType::Date,
108        "time" | "time without time zone" | "timetz" | "time with time zone" => {
109            GraphQLType::Time
110        }
111
112        // Default to String for unknown types
113        _ => GraphQLType::String,
114    }
115}
116
117/// Check if a PostgreSQL type is nullable in GraphQL context.
118pub fn is_nullable_type(nullable: bool, is_pk: bool) -> bool {
119    // Primary keys are never null in GraphQL
120    if is_pk {
121        return false;
122    }
123    nullable
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use pretty_assertions::assert_eq;
130
131    #[test]
132    fn test_pg_to_graphql_integer_types() {
133        assert_eq!(pg_type_to_graphql("integer"), GraphQLType::Int);
134        assert_eq!(pg_type_to_graphql("int4"), GraphQLType::Int);
135        assert_eq!(pg_type_to_graphql("int"), GraphQLType::Int);
136        assert_eq!(pg_type_to_graphql("smallint"), GraphQLType::Int);
137        assert_eq!(pg_type_to_graphql("int2"), GraphQLType::Int);
138    }
139
140    #[test]
141    fn test_pg_to_graphql_bigint() {
142        assert_eq!(pg_type_to_graphql("bigint"), GraphQLType::BigInt);
143        assert_eq!(pg_type_to_graphql("int8"), GraphQLType::BigInt);
144    }
145
146    #[test]
147    fn test_pg_to_graphql_float_types() {
148        assert_eq!(pg_type_to_graphql("real"), GraphQLType::Float);
149        assert_eq!(pg_type_to_graphql("float4"), GraphQLType::Float);
150        assert_eq!(pg_type_to_graphql("double precision"), GraphQLType::Float);
151        assert_eq!(pg_type_to_graphql("float8"), GraphQLType::Float);
152    }
153
154    #[test]
155    fn test_pg_to_graphql_numeric_types() {
156        assert_eq!(pg_type_to_graphql("numeric"), GraphQLType::BigDecimal);
157        assert_eq!(pg_type_to_graphql("decimal"), GraphQLType::BigDecimal);
158    }
159
160    #[test]
161    fn test_pg_to_graphql_string_types() {
162        assert_eq!(pg_type_to_graphql("text"), GraphQLType::String);
163        assert_eq!(pg_type_to_graphql("varchar"), GraphQLType::String);
164        assert_eq!(pg_type_to_graphql("character varying"), GraphQLType::String);
165        assert_eq!(pg_type_to_graphql("char"), GraphQLType::String);
166        assert_eq!(pg_type_to_graphql("bpchar"), GraphQLType::String);
167    }
168
169    #[test]
170    fn test_pg_to_graphql_boolean() {
171        assert_eq!(pg_type_to_graphql("boolean"), GraphQLType::Boolean);
172        assert_eq!(pg_type_to_graphql("bool"), GraphQLType::Boolean);
173    }
174
175    #[test]
176    fn test_pg_to_graphql_json() {
177        assert_eq!(pg_type_to_graphql("json"), GraphQLType::Json);
178        assert_eq!(pg_type_to_graphql("jsonb"), GraphQLType::Json);
179    }
180
181    #[test]
182    fn test_pg_to_graphql_uuid() {
183        assert_eq!(pg_type_to_graphql("uuid"), GraphQLType::Uuid);
184    }
185
186    #[test]
187    fn test_pg_to_graphql_datetime_types() {
188        assert_eq!(pg_type_to_graphql("timestamp"), GraphQLType::DateTime);
189        assert_eq!(pg_type_to_graphql("timestamptz"), GraphQLType::DateTime);
190        assert_eq!(
191            pg_type_to_graphql("timestamp with time zone"),
192            GraphQLType::DateTime
193        );
194        assert_eq!(
195            pg_type_to_graphql("timestamp without time zone"),
196            GraphQLType::DateTime
197        );
198    }
199
200    #[test]
201    fn test_pg_to_graphql_date() {
202        assert_eq!(pg_type_to_graphql("date"), GraphQLType::Date);
203    }
204
205    #[test]
206    fn test_pg_to_graphql_time() {
207        assert_eq!(pg_type_to_graphql("time"), GraphQLType::Time);
208        assert_eq!(pg_type_to_graphql("timetz"), GraphQLType::Time);
209        assert_eq!(
210            pg_type_to_graphql("time with time zone"),
211            GraphQLType::Time
212        );
213    }
214
215    #[test]
216    fn test_pg_to_graphql_array_types_underscore() {
217        assert_eq!(
218            pg_type_to_graphql("_int4"),
219            GraphQLType::List(Box::new(GraphQLType::Int))
220        );
221        assert_eq!(
222            pg_type_to_graphql("_text"),
223            GraphQLType::List(Box::new(GraphQLType::String))
224        );
225        assert_eq!(
226            pg_type_to_graphql("_uuid"),
227            GraphQLType::List(Box::new(GraphQLType::Uuid))
228        );
229    }
230
231    #[test]
232    fn test_pg_to_graphql_array_types_bracket() {
233        assert_eq!(
234            pg_type_to_graphql("integer[]"),
235            GraphQLType::List(Box::new(GraphQLType::Int))
236        );
237        assert_eq!(
238            pg_type_to_graphql("text[]"),
239            GraphQLType::List(Box::new(GraphQLType::String))
240        );
241    }
242
243    #[test]
244    fn test_pg_to_graphql_unknown_defaults_to_string() {
245        assert_eq!(pg_type_to_graphql("customtype"), GraphQLType::String);
246        assert_eq!(pg_type_to_graphql("my_domain"), GraphQLType::String);
247    }
248
249    #[test]
250    fn test_pg_to_graphql_case_insensitive() {
251        assert_eq!(pg_type_to_graphql("INTEGER"), GraphQLType::Int);
252        assert_eq!(pg_type_to_graphql("Text"), GraphQLType::String);
253        assert_eq!(pg_type_to_graphql("BOOLEAN"), GraphQLType::Boolean);
254    }
255
256    #[test]
257    fn test_graphql_type_display() {
258        assert_eq!(format!("{}", GraphQLType::Int), "Int");
259        assert_eq!(format!("{}", GraphQLType::String), "String");
260        assert_eq!(
261            format!("{}", GraphQLType::List(Box::new(GraphQLType::Int))),
262            "[Int]"
263        );
264    }
265
266    #[test]
267    fn test_is_nullable_type() {
268        // PK is never nullable
269        assert!(!is_nullable_type(true, true));
270        assert!(!is_nullable_type(false, true));
271
272        // Non-PK follows the nullable flag
273        assert!(is_nullable_type(true, false));
274        assert!(!is_nullable_type(false, false));
275    }
276}