• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / pg_graphql / 19839478330

01 Dec 2025 10:20PM UTC coverage: 83.863% (-7.5%) from 91.36%
19839478330

Pull #613

github

web-flow
Merge 73f7cb2de into 7951d27bb
Pull Request #613: Transpiler re-write to use an AST vs string concatenation

3931 of 5222 new or added lines in 20 files covered. (75.28%)

2 existing lines in 1 file now uncovered.

10160 of 12115 relevant lines covered (83.86%)

919.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

31.96
/src/ast/params.rs
1
//! Parameter handling for prepared statements
2
//!
3
//! This module provides a pgrx-independent way to collect and manage
4
//! query parameters. The actual conversion to pgrx Datums happens
5
//! in the executor module.
6

7
use super::expr::{Expr, ParamRef};
8
use super::types::SqlType;
9

10
/// A parameter value that can be rendered to SQL
11
///
12
/// This is intentionally decoupled from pgrx to allow the AST module
13
/// to be tested independently.
14
#[derive(Debug, Clone, PartialEq)]
15
pub enum ParamValue {
16
    /// SQL NULL
17
    Null,
18
    /// Boolean value
19
    Bool(bool),
20
    /// String value
21
    String(String),
22
    /// Integer value
23
    Integer(i64),
24
    /// Floating point value
25
    Float(f64),
26
    /// Array of values (for array parameters)
27
    Array(Vec<ParamValue>),
28
    /// JSON value (stored as serde_json::Value)
29
    Json(serde_json::Value),
30
}
31

32
impl ParamValue {
33
    /// Check if this is a null value
NEW
34
    pub fn is_null(&self) -> bool {
×
NEW
35
        matches!(self, Self::Null)
×
NEW
36
    }
×
37

38
    /// Convert to a string representation for SQL
NEW
39
    pub fn to_sql_literal(&self) -> Option<String> {
×
NEW
40
        match self {
×
NEW
41
            Self::Null => None,
×
NEW
42
            Self::Bool(b) => Some(b.to_string()),
×
NEW
43
            Self::String(s) => Some(s.clone()),
×
NEW
44
            Self::Integer(n) => Some(n.to_string()),
×
NEW
45
            Self::Float(f) => Some(f.to_string()),
×
NEW
46
            Self::Array(arr) => {
×
NEW
47
                let elements: Vec<String> = arr.iter().filter_map(|v| v.to_sql_literal()).collect();
×
NEW
48
                Some(format!("{{{}}}", elements.join(",")))
×
49
            }
NEW
50
            Self::Json(v) => Some(v.to_string()),
×
51
        }
NEW
52
    }
×
53
}
54

55
impl From<bool> for ParamValue {
NEW
56
    fn from(b: bool) -> Self {
×
NEW
57
        Self::Bool(b)
×
NEW
58
    }
×
59
}
60

61
impl From<String> for ParamValue {
NEW
62
    fn from(s: String) -> Self {
×
NEW
63
        Self::String(s)
×
NEW
64
    }
×
65
}
66

67
impl From<&str> for ParamValue {
NEW
68
    fn from(s: &str) -> Self {
×
NEW
69
        Self::String(s.to_string())
×
NEW
70
    }
×
71
}
72

73
impl From<i64> for ParamValue {
NEW
74
    fn from(n: i64) -> Self {
×
NEW
75
        Self::Integer(n)
×
NEW
76
    }
×
77
}
78

79
impl From<i32> for ParamValue {
NEW
80
    fn from(n: i32) -> Self {
×
NEW
81
        Self::Integer(n as i64)
×
NEW
82
    }
×
83
}
84

85
impl From<f64> for ParamValue {
NEW
86
    fn from(f: f64) -> Self {
×
NEW
87
        Self::Float(f)
×
NEW
88
    }
×
89
}
90

91
impl From<serde_json::Value> for ParamValue {
NEW
92
    fn from(v: serde_json::Value) -> Self {
×
NEW
93
        Self::Json(v)
×
NEW
94
    }
×
95
}
96

97
impl<T: Into<ParamValue>> From<Option<T>> for ParamValue {
NEW
98
    fn from(opt: Option<T>) -> Self {
×
NEW
99
        match opt {
×
NEW
100
            Some(v) => v.into(),
×
NEW
101
            None => Self::Null,
×
102
        }
NEW
103
    }
×
104
}
105

106
/// A collected parameter with its index, value, and type
107
#[derive(Debug, Clone)]
108
pub struct Param {
109
    /// 1-indexed parameter number
110
    pub index: usize,
111
    /// The parameter value
112
    pub value: ParamValue,
113
    /// The SQL type for casting
114
    pub sql_type: SqlType,
115
}
116

117
impl Param {
NEW
118
    pub fn new(index: usize, value: ParamValue, sql_type: SqlType) -> Self {
×
NEW
119
        Self {
×
NEW
120
            index,
×
NEW
121
            value,
×
NEW
122
            sql_type,
×
NEW
123
        }
×
NEW
124
    }
×
125
}
126

127
/// Collects parameters during AST construction
128
///
129
/// This allows building parameterized queries without worrying about
130
/// parameter numbering. Each call to `add()` returns an expression
131
/// that references the parameter.
132
#[derive(Debug, Default)]
133
pub struct ParamCollector {
134
    params: Vec<Param>,
135
}
136

137
impl ParamCollector {
138
    /// Create a new empty parameter collector
139
    pub fn new() -> Self {
419✔
140
        Self { params: Vec::new() }
419✔
141
    }
419✔
142

143
    /// Add a parameter and return an expression that references it
144
    ///
145
    /// Parameters are 1-indexed in SQL ($1, $2, etc.)
146
    pub fn add(&mut self, value: ParamValue, sql_type: SqlType) -> Expr {
710✔
147
        let index = self.params.len() + 1; // 1-indexed for SQL
710✔
148
        self.params.push(Param {
710✔
149
            index,
710✔
150
            value,
710✔
151
            sql_type: sql_type.clone(),
710✔
152
        });
710✔
153
        Expr::Param(ParamRef {
710✔
154
            index,
710✔
155
            type_cast: sql_type,
710✔
156
        })
710✔
157
    }
710✔
158

159
    /// Add a parameter from a serde_json::Value
160
    ///
161
    /// This is a convenience method for the common case of converting
162
    /// GraphQL input values to parameters.
NEW
163
    pub fn add_json(&mut self, value: &serde_json::Value, sql_type: SqlType) -> Expr {
×
NEW
164
        let param_value = json_to_param_value(value);
×
NEW
165
        self.add(param_value, sql_type)
×
NEW
166
    }
×
167

168
    /// Get all collected parameters
NEW
169
    pub fn into_params(self) -> Vec<Param> {
×
NEW
170
        self.params
×
NEW
171
    }
×
172

173
    /// Get parameters as a slice
174
    pub fn params(&self) -> &[Param] {
416✔
175
        &self.params
416✔
176
    }
416✔
177

178
    /// Get the number of collected parameters
NEW
179
    pub fn len(&self) -> usize {
×
NEW
180
        self.params.len()
×
NEW
181
    }
×
182

183
    /// Check if no parameters have been collected
NEW
184
    pub fn is_empty(&self) -> bool {
×
NEW
185
        self.params.is_empty()
×
NEW
186
    }
×
187

188
    /// Get the next parameter index that would be assigned
NEW
189
    pub fn next_index(&self) -> usize {
×
NEW
190
        self.params.len() + 1
×
NEW
191
    }
×
192
}
193

194
/// Convert a serde_json::Value to a ParamValue
195
pub fn json_to_param_value(value: &serde_json::Value) -> ParamValue {
841✔
196
    match value {
841✔
197
        serde_json::Value::Null => ParamValue::Null,
20✔
198
        serde_json::Value::Bool(b) => ParamValue::Bool(*b),
19✔
199
        serde_json::Value::Number(n) => {
359✔
200
            if let Some(i) = n.as_i64() {
359✔
201
                ParamValue::Integer(i)
341✔
202
            } else if let Some(f) = n.as_f64() {
18✔
203
                ParamValue::Float(f)
18✔
204
            } else {
205
                // Fallback to string representation
NEW
206
                ParamValue::String(n.to_string())
×
207
            }
208
        }
209
        serde_json::Value::String(s) => ParamValue::String(s.clone()),
362✔
210
        serde_json::Value::Array(arr) => {
81✔
211
            ParamValue::Array(arr.iter().map(json_to_param_value).collect())
81✔
212
        }
213
        serde_json::Value::Object(_) => {
214
            // Store objects as JSON
NEW
215
            ParamValue::Json(value.clone())
×
216
        }
217
    }
218
}
841✔
219

220
#[cfg(test)]
221
mod tests {
222
    use super::*;
223

224
    #[test]
225
    fn test_param_collector() {
226
        let mut collector = ParamCollector::new();
227

228
        let expr1 = collector.add(ParamValue::String("hello".into()), SqlType::text());
229
        let expr2 = collector.add(ParamValue::Integer(42), SqlType::integer());
230

231
        assert_eq!(collector.len(), 2);
232

233
        match &expr1 {
234
            Expr::Param(p) => {
235
                assert_eq!(p.index, 1);
236
                assert_eq!(p.type_cast.name, "text");
237
            }
238
            _ => panic!("Expected Param"),
239
        }
240

241
        match &expr2 {
242
            Expr::Param(p) => {
243
                assert_eq!(p.index, 2);
244
                assert_eq!(p.type_cast.name, "integer");
245
            }
246
            _ => panic!("Expected Param"),
247
        }
248

249
        let params = collector.into_params();
250
        assert_eq!(params.len(), 2);
251
        assert!(matches!(params[0].value, ParamValue::String(_)));
252
        assert!(matches!(params[1].value, ParamValue::Integer(42)));
253
    }
254

255
    #[test]
256
    fn test_json_to_param_value() {
257
        assert!(matches!(
258
            json_to_param_value(&serde_json::Value::Null),
259
            ParamValue::Null
260
        ));
261

262
        assert!(matches!(
263
            json_to_param_value(&serde_json::json!(true)),
264
            ParamValue::Bool(true)
265
        ));
266

267
        assert!(matches!(
268
            json_to_param_value(&serde_json::json!(42)),
269
            ParamValue::Integer(42)
270
        ));
271

272
        assert!(matches!(
273
            json_to_param_value(&serde_json::json!("hello")),
274
            ParamValue::String(s) if s == "hello"
275
        ));
276

277
        let arr = json_to_param_value(&serde_json::json!([1, 2, 3]));
278
        match arr {
279
            ParamValue::Array(v) => assert_eq!(v.len(), 3),
280
            _ => panic!("Expected Array"),
281
        }
282
    }
283

284
    #[test]
285
    fn test_param_value_to_sql_literal() {
286
        assert_eq!(ParamValue::Null.to_sql_literal(), None);
287
        assert_eq!(
288
            ParamValue::Bool(true).to_sql_literal(),
289
            Some("true".to_string())
290
        );
291
        assert_eq!(
292
            ParamValue::Integer(42).to_sql_literal(),
293
            Some("42".to_string())
294
        );
295
        assert_eq!(
296
            ParamValue::String("hello".into()).to_sql_literal(),
297
            Some("hello".to_string())
298
        );
299
    }
300

301
    #[test]
302
    fn test_param_value_from() {
303
        let _: ParamValue = true.into();
304
        let _: ParamValue = "hello".into();
305
        let _: ParamValue = 42i32.into();
306
        let _: ParamValue = 42i64.into();
307
        let _: ParamValue = 3.14f64.into();
308
        let _: ParamValue = None::<i32>.into();
309
        let _: ParamValue = Some(42i32).into();
310
    }
311
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc