• 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

85.04
/src/ast/execute.rs
1
//! AST-based query execution
2
//!
3
//! This module provides execution methods for builders using the new AST system.
4
//! It bridges the gap between the existing builder infrastructure and the new
5
//! type-safe SQL generation and execution.
6
//!
7
//! # Parameter Binding
8
//!
9
//! All parameters are converted to text representation and passed with TEXTOID.
10
//! PostgreSQL handles type conversion via SQL-side cast expressions like `($1::integer)`.
11
//! This matches the original transpiler behavior exactly.
12

13
use crate::ast::{render, Param, ParamCollector, ParamValue};
14
use crate::error::{GraphQLError, GraphQLResult};
15
use pgrx::datum::DatumWithOid;
16
use pgrx::pg_sys::PgBuiltInOids;
17
use pgrx::spi::{self, Spi, SpiClient};
18
use pgrx::{IntoDatum, JsonB, PgOid};
19

20
/// Convert all collected parameters to pgrx datums (as text)
21
///
22
/// All parameters are converted to text representation and passed with TEXTOID
23
/// (or TEXTARRAYOID for arrays). PostgreSQL handles type conversion via the
24
/// SQL-side cast expressions like `($1::integer)`.
25
fn params_to_datums(params: &ParamCollector) -> Vec<DatumWithOid<'static>> {
416✔
26
    params.params().iter().map(param_to_datum).collect()
416✔
27
}
416✔
28

29
/// Convert a parameter to a pgrx DatumWithOid (as text)
30
fn param_to_datum(param: &Param) -> DatumWithOid<'static> {
709✔
31
    let type_oid = if param.sql_type.is_array {
709✔
32
        PgOid::BuiltIn(PgBuiltInOids::TEXTARRAYOID)
85✔
33
    } else {
34
        PgOid::BuiltIn(PgBuiltInOids::TEXTOID)
624✔
35
    };
36

37
    match &param.value {
709✔
38
        ParamValue::Null => DatumWithOid::null_oid(type_oid.value()),
17✔
39

40
        ParamValue::String(s) => {
276✔
41
            let datum = s.clone().into_datum();
276✔
42
            match datum {
276✔
43
                Some(d) => unsafe { DatumWithOid::new(d, type_oid.value()) },
276✔
NEW
44
                None => DatumWithOid::null_oid(type_oid.value()),
×
45
            }
46
        }
47

48
        ParamValue::Integer(i) => {
306✔
49
            let datum = i.to_string().into_datum();
306✔
50
            match datum {
306✔
51
                Some(d) => unsafe { DatumWithOid::new(d, type_oid.value()) },
306✔
NEW
52
                None => DatumWithOid::null_oid(type_oid.value()),
×
53
            }
54
        }
55

56
        ParamValue::Float(f) => {
12✔
57
            let datum = f.to_string().into_datum();
12✔
58
            match datum {
12✔
59
                Some(d) => unsafe { DatumWithOid::new(d, type_oid.value()) },
12✔
NEW
60
                None => DatumWithOid::null_oid(type_oid.value()),
×
61
            }
62
        }
63

64
        ParamValue::Bool(b) => {
17✔
65
            let datum = b.to_string().into_datum();
17✔
66
            match datum {
17✔
67
                Some(d) => unsafe { DatumWithOid::new(d, type_oid.value()) },
17✔
NEW
68
                None => DatumWithOid::null_oid(type_oid.value()),
×
69
            }
70
        }
71

NEW
72
        ParamValue::Json(j) => {
×
NEW
73
            let datum = j.to_string().into_datum();
×
NEW
74
            match datum {
×
NEW
75
                Some(d) => unsafe { DatumWithOid::new(d, type_oid.value()) },
×
NEW
76
                None => DatumWithOid::null_oid(type_oid.value()),
×
77
            }
78
        }
79

80
        ParamValue::Array(arr) => {
81✔
81
            // Convert array to PostgreSQL text array format
82
            let elements: Vec<Option<String>> = arr.iter().map(param_value_to_string).collect();
81✔
83
            let datum = elements.into_datum();
81✔
84
            match datum {
81✔
85
                Some(d) => unsafe { DatumWithOid::new(d, type_oid.value()) },
81✔
NEW
86
                None => DatumWithOid::null_oid(type_oid.value()),
×
87
            }
88
        }
89
    }
90
}
709✔
91

92
/// Convert a ParamValue to Option<String> for array element conversion
93
fn param_value_to_string(value: &ParamValue) -> Option<String> {
131✔
94
    match value {
131✔
95
        ParamValue::Null => None,
3✔
96
        ParamValue::String(s) => Some(s.clone()),
85✔
97
        ParamValue::Integer(i) => Some(i.to_string()),
35✔
98
        ParamValue::Float(f) => Some(f.to_string()),
6✔
99
        ParamValue::Bool(b) => Some(b.to_string()),
2✔
NEW
100
        ParamValue::Json(j) => Some(j.to_string()),
×
NEW
101
        ParamValue::Array(_) => value.to_sql_literal(),
×
102
    }
103
}
131✔
104

105
/// Execute a query builder using the AST path
106
///
107
/// This trait can be implemented alongside existing execute methods
108
/// to allow gradual migration to the new AST system.
109
pub trait AstExecutable {
110
    type Ast;
111

112
    fn to_ast(&self, params: &mut ParamCollector) -> GraphQLResult<Self::Ast>;
113

114
    fn execute_via_ast(&self) -> GraphQLResult<serde_json::Value>
329✔
115
    where
329✔
116
        Self::Ast: AsStatement,
329✔
117
    {
118
        let mut params = ParamCollector::new();
329✔
119
        let ast = self.to_ast(&mut params)?;
329✔
120

121
        // Render the AST to SQL
122
        let sql = ast.render_sql();
326✔
123

124
        // Convert parameters to pgrx format (all as text)
125
        let pgrx_params = params_to_datums(&params);
326✔
126

127
        // Execute via SPI
128
        let spi_result: Result<Option<JsonB>, spi::Error> = Spi::connect(|c| {
326✔
129
            let val = c.select(&sql, Some(1), &pgrx_params)?;
326✔
130
            if val.is_empty() {
326✔
131
                Ok(None)
1✔
132
            } else {
133
                val.first().get::<JsonB>(1)
325✔
134
            }
135
        });
326✔
136

137
        match spi_result {
326✔
138
            Ok(Some(jsonb)) => Ok(jsonb.0),
322✔
139
            Ok(None) => Ok(serde_json::Value::Null),
4✔
NEW
140
            Err(e) => Err(GraphQLError::internal(format!("{}", e))),
×
141
        }
142
    }
329✔
143

144
    fn execute_mutation_via_ast<'conn, 'c>(
90✔
145
        &self,
90✔
146
        conn: &'c mut SpiClient<'conn>,
90✔
147
    ) -> GraphQLResult<(serde_json::Value, &'c mut SpiClient<'conn>)>
90✔
148
    where
90✔
149
        Self::Ast: AsStatement,
90✔
150
    {
151
        let mut params = ParamCollector::new();
90✔
152
        let ast = self.to_ast(&mut params)?;
90✔
153

154
        // Render the AST to SQL
155
        let sql = ast.render_sql();
90✔
156

157
        // Convert parameters to pgrx format (all as text)
158
        let pgrx_params = params_to_datums(&params);
90✔
159

160
        // Execute via SPI update (for mutations)
161
        let res_q = conn.update(&sql, None, &pgrx_params).map_err(|_| {
90✔
NEW
162
            GraphQLError::sql_execution("Internal Error: Failed to execute AST-generated mutation")
×
NEW
163
        })?;
×
164

165
        let res: JsonB = match res_q.first().get::<JsonB>(1) {
90✔
166
            Ok(Some(dat)) => dat,
86✔
167
            Ok(None) => JsonB(serde_json::Value::Null),
4✔
NEW
168
            Err(e) => {
×
NEW
169
                return Err(GraphQLError::sql_generation(format!(
×
NEW
170
                    "Internal Error: Failed to load result from AST query: {e}"
×
NEW
171
                )));
×
172
            }
173
        };
174

175
        Ok((res.0, conn))
90✔
176
    }
90✔
177
}
178

179
/// Trait for types that can be rendered as SQL statements
180
pub trait AsStatement {
181
    fn render_sql(&self) -> String;
182
}
183

184
// Implement AsStatement for our AST result types
185
impl AsStatement for crate::ast::InsertAst {
186
    fn render_sql(&self) -> String {
29✔
187
        render(&self.stmt)
29✔
188
    }
29✔
189
}
190

191
impl AsStatement for crate::ast::UpdateAst {
192
    fn render_sql(&self) -> String {
26✔
193
        render(&self.stmt)
26✔
194
    }
26✔
195
}
196

197
impl AsStatement for crate::ast::DeleteAst {
198
    fn render_sql(&self) -> String {
17✔
199
        render(&self.stmt)
17✔
200
    }
17✔
201
}
202

203
impl AsStatement for crate::ast::NodeAst {
204
    fn render_sql(&self) -> String {
8✔
205
        render(&self.stmt)
8✔
206
    }
8✔
207
}
208

209
impl AsStatement for crate::ast::ConnectionAst {
210
    fn render_sql(&self) -> String {
243✔
211
        render(&self.stmt)
243✔
212
    }
243✔
213
}
214

215
impl AsStatement for crate::ast::FunctionCallAst {
216
    fn render_sql(&self) -> String {
93✔
217
        render(&self.stmt)
93✔
218
    }
93✔
219
}
220

221
// Implement AstExecutable for builders
222
// These connect the ToAst implementations from transpile_*.rs to the execution trait
223

224
use crate::ast::ToAst;
225
use crate::builder::{
226
    ConnectionBuilder, DeleteBuilder, FunctionCallBuilder, InsertBuilder, NodeBuilder,
227
    UpdateBuilder,
228
};
229

230
impl AstExecutable for InsertBuilder {
231
    type Ast = crate::ast::InsertAst;
232

233
    fn to_ast(&self, params: &mut ParamCollector) -> GraphQLResult<Self::Ast> {
29✔
234
        <Self as ToAst>::to_ast(self, params)
29✔
235
    }
29✔
236
}
237

238
impl AstExecutable for UpdateBuilder {
239
    type Ast = crate::ast::UpdateAst;
240

241
    fn to_ast(&self, params: &mut ParamCollector) -> GraphQLResult<Self::Ast> {
26✔
242
        <Self as ToAst>::to_ast(self, params)
26✔
243
    }
26✔
244
}
245

246
impl AstExecutable for DeleteBuilder {
247
    type Ast = crate::ast::DeleteAst;
248

249
    fn to_ast(&self, params: &mut ParamCollector) -> GraphQLResult<Self::Ast> {
17✔
250
        <Self as ToAst>::to_ast(self, params)
17✔
251
    }
17✔
252
}
253

254
impl AstExecutable for NodeBuilder {
255
    type Ast = crate::ast::NodeAst;
256

257
    fn to_ast(&self, params: &mut ParamCollector) -> GraphQLResult<Self::Ast> {
8✔
258
        <Self as ToAst>::to_ast(self, params)
8✔
259
    }
8✔
260
}
261

262
impl AstExecutable for ConnectionBuilder {
263
    type Ast = crate::ast::ConnectionAst;
264

265
    fn to_ast(&self, params: &mut ParamCollector) -> GraphQLResult<Self::Ast> {
246✔
266
        <Self as ToAst>::to_ast(self, params)
246✔
267
    }
246✔
268
}
269

270
impl AstExecutable for FunctionCallBuilder {
271
    type Ast = crate::ast::FunctionCallAst;
272

273
    fn to_ast(&self, params: &mut ParamCollector) -> GraphQLResult<Self::Ast> {
93✔
274
        <Self as ToAst>::to_ast(self, params)
93✔
275
    }
93✔
276
}
277

278
#[cfg(test)]
279
mod tests {
280
    // Tests that don't require pgrx runtime can go here
281
    // Full integration tests require pg_regress
282
}
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