Skip to main content

heliosdb_proxy/graphql/
mod.rs

1//! GraphQL-to-SQL Gateway
2//!
3//! Feature 12 of the HeliosProxy roadmap.
4//!
5//! This module provides a GraphQL gateway that automatically generates efficient SQL
6//! queries from GraphQL requests. It includes:
7//!
8//! - Automatic schema introspection from database tables
9//! - Efficient SQL generation with JOIN optimization
10//! - N+1 query prevention via DataLoader pattern
11//! - Query complexity analysis and limits
12//! - Branch-aware and time-travel queries (HeliosDB-Lite integration)
13//!
14//! # Architecture
15//!
16//! ```text
17//! GraphQL Query → Parse → Validate → Plan → Generate SQL → Execute → Shape Response
18//! ```
19//!
20//! # Example
21//!
22//! ```rust,ignore
23//! use heliosdb::proxy::graphql::{GraphQLEngine, GraphQLConfig};
24//!
25//! let config = GraphQLConfig::builder()
26//!     .endpoint("/graphql")
27//!     .playground(true)
28//!     .max_depth(10)
29//!     .build();
30//!
31//! let engine = GraphQLEngine::new(config, db_pool).await?;
32//!
33//! let response = engine.execute(GraphQLRequest {
34//!     query: "query { users { id name } }".to_string(),
35//!     variables: None,
36//!     operation_name: None,
37//! }).await?;
38//! ```
39
40pub mod config;
41pub mod engine;
42pub mod introspector;
43pub mod sql_generator;
44pub mod dataloader;
45pub mod resolver;
46pub mod validation;
47pub mod metrics;
48
49pub use config::{GraphQLConfig, GraphQLConfigBuilder, TableConfig, RelationshipConfig};
50pub use engine::{GraphQLEngine, GraphQLRequest, GraphQLResponse, GraphQLError};
51pub use introspector::{SchemaIntrospector, GraphQLSchema, GraphQLType, GraphQLField};
52pub use sql_generator::{SqlGenerator, SqlQuery, QueryPlan, Selection, Filter};
53pub use dataloader::{DataLoader, DataLoaderConfig, BatchResult};
54pub use resolver::{FieldResolver, ResolverContext, ResolverResult};
55pub use validation::{QueryValidator, ValidationError, ComplexityResult};
56pub use metrics::{GraphQLMetrics, QueryStats, OperationMetrics};
57
58use std::collections::HashMap;
59
60/// GraphQL operation type
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub enum OperationType {
63    /// Query (read-only)
64    Query,
65    /// Mutation (write)
66    Mutation,
67    /// Subscription (real-time)
68    Subscription,
69}
70
71impl std::fmt::Display for OperationType {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        match self {
74            OperationType::Query => write!(f, "query"),
75            OperationType::Mutation => write!(f, "mutation"),
76            OperationType::Subscription => write!(f, "subscription"),
77        }
78    }
79}
80
81/// Relationship type between tables
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
83pub enum RelationType {
84    /// One-to-one relationship
85    OneToOne,
86    /// One-to-many relationship
87    OneToMany,
88    /// Many-to-one relationship
89    ManyToOne,
90    /// Many-to-many relationship
91    ManyToMany,
92}
93
94impl RelationType {
95    /// Parse from string
96    pub fn from_str(s: &str) -> Option<Self> {
97        match s.to_lowercase().as_str() {
98            "one_to_one" | "onetoone" | "1:1" => Some(RelationType::OneToOne),
99            "one_to_many" | "onetomany" | "1:n" => Some(RelationType::OneToMany),
100            "many_to_one" | "manytoone" | "n:1" => Some(RelationType::ManyToOne),
101            "many_to_many" | "manytomany" | "n:n" => Some(RelationType::ManyToMany),
102            _ => None,
103        }
104    }
105
106    /// Returns true if this relationship returns multiple records
107    pub fn is_list(&self) -> bool {
108        matches!(self, RelationType::OneToMany | RelationType::ManyToMany)
109    }
110}
111
112/// GraphQL scalar types mapped from SQL
113#[derive(Debug, Clone, PartialEq, Eq, Hash)]
114pub enum GraphQLScalar {
115    /// GraphQL ID type
116    ID,
117    /// GraphQL String type
118    String,
119    /// GraphQL Int type
120    Int,
121    /// GraphQL Float type
122    Float,
123    /// GraphQL Boolean type
124    Boolean,
125    /// GraphQL DateTime type (custom scalar)
126    DateTime,
127    /// GraphQL Date type (custom scalar)
128    Date,
129    /// GraphQL Time type (custom scalar)
130    Time,
131    /// GraphQL JSON type (custom scalar)
132    JSON,
133    /// GraphQL Decimal type (custom scalar)
134    Decimal,
135    /// GraphQL BigInt type (custom scalar)
136    BigInt,
137    /// Custom scalar type
138    Custom(String),
139}
140
141impl GraphQLScalar {
142    /// Convert SQL type to GraphQL scalar
143    pub fn from_sql_type(sql_type: &str) -> Self {
144        let lower = sql_type.to_lowercase();
145
146        if lower.contains("serial") || lower == "uuid" {
147            GraphQLScalar::ID
148        } else if lower.contains("int") || lower == "smallint" {
149            if lower.contains("big") {
150                GraphQLScalar::BigInt
151            } else {
152                GraphQLScalar::Int
153            }
154        } else if lower.contains("float") || lower.contains("double") || lower == "real" {
155            GraphQLScalar::Float
156        } else if lower.contains("numeric") || lower.contains("decimal") {
157            GraphQLScalar::Decimal
158        } else if lower == "boolean" || lower == "bool" {
159            GraphQLScalar::Boolean
160        } else if lower.contains("timestamp") || lower == "datetime" {
161            GraphQLScalar::DateTime
162        } else if lower == "date" {
163            GraphQLScalar::Date
164        } else if lower == "time" {
165            GraphQLScalar::Time
166        } else if lower == "json" || lower == "jsonb" {
167            GraphQLScalar::JSON
168        } else {
169            GraphQLScalar::String
170        }
171    }
172
173    /// Get the GraphQL SDL representation
174    pub fn to_sdl(&self) -> &str {
175        match self {
176            GraphQLScalar::ID => "ID",
177            GraphQLScalar::String => "String",
178            GraphQLScalar::Int => "Int",
179            GraphQLScalar::Float => "Float",
180            GraphQLScalar::Boolean => "Boolean",
181            GraphQLScalar::DateTime => "DateTime",
182            GraphQLScalar::Date => "Date",
183            GraphQLScalar::Time => "Time",
184            GraphQLScalar::JSON => "JSON",
185            GraphQLScalar::Decimal => "Decimal",
186            GraphQLScalar::BigInt => "BigInt",
187            GraphQLScalar::Custom(name) => name,
188        }
189    }
190}
191
192/// Consistency level for GraphQL queries (HeliosDB integration)
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
194pub enum ConsistencyLevel {
195    /// Strong consistency - read from primary
196    Strong,
197    /// Eventual consistency - can read from replicas
198    #[default]
199    Eventual,
200    /// Bounded staleness - read within time window
201    Bounded,
202}
203
204/// Distance metric for vector searches
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
206pub enum DistanceMetric {
207    /// Cosine similarity
208    #[default]
209    Cosine,
210    /// Euclidean distance
211    Euclidean,
212    /// Dot product
213    DotProduct,
214}
215
216/// Branch context for HeliosDB branch-aware queries
217#[derive(Debug, Clone)]
218pub struct BranchContext {
219    /// Branch name
220    pub name: String,
221    /// As-of timestamp (for time-travel)
222    pub as_of: Option<std::time::SystemTime>,
223}
224
225impl Default for BranchContext {
226    fn default() -> Self {
227        Self {
228            name: "main".to_string(),
229            as_of: None,
230        }
231    }
232}
233
234/// GraphQL execution context
235#[derive(Debug, Clone)]
236pub struct ExecutionContext {
237    /// User identity (if authenticated)
238    pub user_id: Option<String>,
239    /// Roles for authorization
240    pub roles: Vec<String>,
241    /// Branch context
242    pub branch: BranchContext,
243    /// Consistency level
244    pub consistency: ConsistencyLevel,
245    /// Request headers
246    pub headers: HashMap<String, String>,
247    /// Custom metadata
248    pub metadata: HashMap<String, String>,
249}
250
251impl Default for ExecutionContext {
252    fn default() -> Self {
253        Self {
254            user_id: None,
255            roles: Vec::new(),
256            branch: BranchContext::default(),
257            consistency: ConsistencyLevel::default(),
258            headers: HashMap::new(),
259            metadata: HashMap::new(),
260        }
261    }
262}
263
264impl ExecutionContext {
265    /// Create a new execution context
266    pub fn new() -> Self {
267        Self::default()
268    }
269
270    /// Set the user ID
271    pub fn with_user(mut self, user_id: impl Into<String>) -> Self {
272        self.user_id = Some(user_id.into());
273        self
274    }
275
276    /// Add a role
277    pub fn with_role(mut self, role: impl Into<String>) -> Self {
278        self.roles.push(role.into());
279        self
280    }
281
282    /// Set the branch
283    pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
284        self.branch.name = branch.into();
285        self
286    }
287
288    /// Set the as-of timestamp for time-travel
289    pub fn with_as_of(mut self, timestamp: std::time::SystemTime) -> Self {
290        self.branch.as_of = Some(timestamp);
291        self
292    }
293
294    /// Set the consistency level
295    pub fn with_consistency(mut self, level: ConsistencyLevel) -> Self {
296        self.consistency = level;
297        self
298    }
299
300    /// Check if the user has a specific role
301    pub fn has_role(&self, role: &str) -> bool {
302        self.roles.iter().any(|r| r == role)
303    }
304
305    /// Check if the context is authenticated
306    pub fn is_authenticated(&self) -> bool {
307        self.user_id.is_some()
308    }
309}
310
311/// GraphQL error codes
312#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
313pub enum ErrorCode {
314    /// Parse error
315    ParseError,
316    /// Validation error
317    ValidationError,
318    /// Authorization error
319    Unauthorized,
320    /// Forbidden
321    Forbidden,
322    /// Not found
323    NotFound,
324    /// Internal server error
325    InternalError,
326    /// Query too complex
327    QueryTooComplex,
328    /// Rate limited
329    RateLimited,
330    /// Timeout
331    Timeout,
332}
333
334impl ErrorCode {
335    /// Get the HTTP status code equivalent
336    pub fn http_status(&self) -> u16 {
337        match self {
338            ErrorCode::ParseError | ErrorCode::ValidationError => 400,
339            ErrorCode::Unauthorized => 401,
340            ErrorCode::Forbidden => 403,
341            ErrorCode::NotFound => 404,
342            ErrorCode::QueryTooComplex | ErrorCode::RateLimited => 429,
343            ErrorCode::Timeout => 408,
344            ErrorCode::InternalError => 500,
345        }
346    }
347}
348
349/// Convert string to PascalCase
350pub fn to_pascal_case(s: &str) -> String {
351    s.split('_')
352        .filter(|part| !part.is_empty())
353        .map(|part| {
354            let mut chars = part.chars();
355            match chars.next() {
356                None => String::new(),
357                Some(first) => first.to_uppercase().chain(chars).collect(),
358            }
359        })
360        .collect()
361}
362
363/// Convert string to camelCase
364pub fn to_camel_case(s: &str) -> String {
365    let pascal = to_pascal_case(s);
366    let mut chars = pascal.chars();
367    match chars.next() {
368        None => String::new(),
369        Some(first) => first.to_lowercase().chain(chars).collect(),
370    }
371}
372
373/// Convert string to snake_case
374pub fn to_snake_case(s: &str) -> String {
375    let mut result = String::with_capacity(s.len() + 4);
376    let mut prev_was_upper = false;
377
378    for (i, c) in s.chars().enumerate() {
379        if c.is_uppercase() {
380            if i > 0 && !prev_was_upper {
381                result.push('_');
382            }
383            result.push(c.to_lowercase().next().unwrap());
384            prev_was_upper = true;
385        } else {
386            result.push(c);
387            prev_was_upper = false;
388        }
389    }
390
391    result
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397
398    #[test]
399    fn test_relation_type_from_str() {
400        assert_eq!(RelationType::from_str("one_to_one"), Some(RelationType::OneToOne));
401        assert_eq!(RelationType::from_str("1:n"), Some(RelationType::OneToMany));
402        assert_eq!(RelationType::from_str("n:1"), Some(RelationType::ManyToOne));
403        assert_eq!(RelationType::from_str("many_to_many"), Some(RelationType::ManyToMany));
404        assert_eq!(RelationType::from_str("invalid"), None);
405    }
406
407    #[test]
408    fn test_relation_type_is_list() {
409        assert!(!RelationType::OneToOne.is_list());
410        assert!(!RelationType::ManyToOne.is_list());
411        assert!(RelationType::OneToMany.is_list());
412        assert!(RelationType::ManyToMany.is_list());
413    }
414
415    #[test]
416    fn test_graphql_scalar_from_sql_type() {
417        assert_eq!(GraphQLScalar::from_sql_type("serial"), GraphQLScalar::ID);
418        assert_eq!(GraphQLScalar::from_sql_type("UUID"), GraphQLScalar::ID);
419        assert_eq!(GraphQLScalar::from_sql_type("INTEGER"), GraphQLScalar::Int);
420        assert_eq!(GraphQLScalar::from_sql_type("BIGINT"), GraphQLScalar::BigInt);
421        assert_eq!(GraphQLScalar::from_sql_type("FLOAT"), GraphQLScalar::Float);
422        assert_eq!(GraphQLScalar::from_sql_type("BOOLEAN"), GraphQLScalar::Boolean);
423        assert_eq!(GraphQLScalar::from_sql_type("TIMESTAMP"), GraphQLScalar::DateTime);
424        assert_eq!(GraphQLScalar::from_sql_type("JSONB"), GraphQLScalar::JSON);
425        assert_eq!(GraphQLScalar::from_sql_type("VARCHAR"), GraphQLScalar::String);
426    }
427
428    #[test]
429    fn test_to_pascal_case() {
430        assert_eq!(to_pascal_case("user_name"), "UserName");
431        assert_eq!(to_pascal_case("users"), "Users");
432        assert_eq!(to_pascal_case("post_comments"), "PostComments");
433    }
434
435    #[test]
436    fn test_to_camel_case() {
437        assert_eq!(to_camel_case("user_name"), "userName");
438        assert_eq!(to_camel_case("Users"), "users");
439        assert_eq!(to_camel_case("post_comments"), "postComments");
440    }
441
442    #[test]
443    fn test_to_snake_case() {
444        assert_eq!(to_snake_case("UserName"), "user_name");
445        assert_eq!(to_snake_case("postComments"), "post_comments");
446        assert_eq!(to_snake_case("ID"), "id");
447    }
448
449    #[test]
450    fn test_execution_context() {
451        let ctx = ExecutionContext::new()
452            .with_user("user123")
453            .with_role("admin")
454            .with_role("reader")
455            .with_branch("development")
456            .with_consistency(ConsistencyLevel::Strong);
457
458        assert_eq!(ctx.user_id, Some("user123".to_string()));
459        assert!(ctx.is_authenticated());
460        assert!(ctx.has_role("admin"));
461        assert!(ctx.has_role("reader"));
462        assert!(!ctx.has_role("writer"));
463        assert_eq!(ctx.branch.name, "development");
464        assert_eq!(ctx.consistency, ConsistencyLevel::Strong);
465    }
466
467    #[test]
468    fn test_error_code_http_status() {
469        assert_eq!(ErrorCode::ParseError.http_status(), 400);
470        assert_eq!(ErrorCode::Unauthorized.http_status(), 401);
471        assert_eq!(ErrorCode::Forbidden.http_status(), 403);
472        assert_eq!(ErrorCode::NotFound.http_status(), 404);
473        assert_eq!(ErrorCode::InternalError.http_status(), 500);
474    }
475}