• 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

70.43
/src/ast/render.rs
1
//! SQL string rendering
2
//!
3
//! This module converts AST nodes to SQL strings. It is the only place
4
//! in the codebase where SQL strings are constructed.
5
//!
6
//! # Architecture
7
//!
8
//! The rendering system is built around two key components:
9
//!
10
//! - [`Render`] trait: Implemented by all AST nodes to define how they render to SQL
11
//! - [`SqlRenderer`]: The rendering context that handles output buffering and formatting
12
//!
13
//! # Safety
14
//!
15
//! All identifiers are quoted using PostgreSQL's native `quote_ident()` function.
16
//! All literals are escaped using PostgreSQL's native `quote_literal()` function.
17

18
use super::cte::{Cte, CteQuery};
19
use super::expr::*;
20
use super::stmt::*;
21
use super::types::SqlType;
22
use pgrx::{direct_function_call, pg_sys, IntoDatum};
23
use std::fmt::Write;
24

25
/// Quote an identifier using PostgreSQL's native quote_ident() function
26
fn quote_ident(ident: &str) -> String {
28,110✔
27
    unsafe {
28
        direct_function_call::<String>(pg_sys::quote_ident, &[ident.into_datum()])
28,110✔
29
            .expect("quote_ident failed")
28,110✔
30
    }
31
}
28,110✔
32

33
/// Quote a literal using PostgreSQL's native quote_literal() function
34
fn quote_literal(lit: &str) -> String {
3,226✔
35
    unsafe {
36
        direct_function_call::<String>(pg_sys::quote_literal, &[lit.into_datum()])
3,226✔
37
            .expect("quote_literal failed")
3,226✔
38
    }
39
}
3,226✔
40

41
// =============================================================================
42
// Render Trait
43
// =============================================================================
44

45
/// Trait for AST nodes that can be rendered to SQL.
46
///
47
/// This trait allows each AST node type to define its own rendering logic
48
/// while sharing the common [`SqlRenderer`] infrastructure.
49
///
50
/// # Example
51
///
52
/// ```rust,ignore
53
/// use pg_graphql::ast::{Render, SqlRenderer, Expr};
54
///
55
/// let expr = Expr::int(42);
56
/// let mut renderer = SqlRenderer::new();
57
/// expr.render(&mut renderer);
58
/// let sql = renderer.into_sql();
59
/// assert_eq!(sql, "42");
60
/// ```
61
pub trait Render {
62
    /// Render this node to the given SQL renderer
63
    fn render(&self, renderer: &mut SqlRenderer);
64
}
65

66
// Implement Render for Stmt
67
impl Render for Stmt {
NEW
68
    fn render(&self, renderer: &mut SqlRenderer) {
×
NEW
69
        match self {
×
NEW
70
            Stmt::Select(s) => s.render(renderer),
×
NEW
71
            Stmt::Insert(s) => s.render(renderer),
×
NEW
72
            Stmt::Update(s) => s.render(renderer),
×
NEW
73
            Stmt::Delete(s) => s.render(renderer),
×
74
        }
NEW
75
    }
×
76
}
77

78
// Implement Render for SelectStmt
79
impl Render for SelectStmt {
NEW
80
    fn render(&self, renderer: &mut SqlRenderer) {
×
NEW
81
        renderer.render_select(self);
×
NEW
82
    }
×
83
}
84

85
// Implement Render for InsertStmt
86
impl Render for InsertStmt {
NEW
87
    fn render(&self, renderer: &mut SqlRenderer) {
×
NEW
88
        renderer.render_insert(self);
×
NEW
89
    }
×
90
}
91

92
// Implement Render for UpdateStmt
93
impl Render for UpdateStmt {
NEW
94
    fn render(&self, renderer: &mut SqlRenderer) {
×
NEW
95
        renderer.render_update(self);
×
NEW
96
    }
×
97
}
98

99
// Implement Render for DeleteStmt
100
impl Render for DeleteStmt {
NEW
101
    fn render(&self, renderer: &mut SqlRenderer) {
×
NEW
102
        renderer.render_delete(self);
×
NEW
103
    }
×
104
}
105

106
// Implement Render for Expr
107
impl Render for Expr {
NEW
108
    fn render(&self, renderer: &mut SqlRenderer) {
×
NEW
109
        renderer.render_expr(self);
×
NEW
110
    }
×
111
}
112

113
// Implement Render for Literal
114
impl Render for Literal {
NEW
115
    fn render(&self, renderer: &mut SqlRenderer) {
×
NEW
116
        renderer.render_literal(self);
×
NEW
117
    }
×
118
}
119

120
// Implement Render for Ident
121
impl Render for Ident {
NEW
122
    fn render(&self, renderer: &mut SqlRenderer) {
×
NEW
123
        renderer.write_ident(self);
×
NEW
124
    }
×
125
}
126

127
// =============================================================================
128
// Constants
129
// =============================================================================
130

131
/// Default buffer capacity for simple queries
132
const DEFAULT_BUFFER_CAPACITY: usize = 1024;
133

134
/// Buffer capacity for queries with CTEs (e.g., connection queries)
135
const CTE_BUFFER_CAPACITY: usize = 4096;
136

137
/// Buffer capacity for complex queries with many CTEs
138
const LARGE_BUFFER_CAPACITY: usize = 8192;
139

140
/// SQL renderer with optional pretty-printing
141
pub struct SqlRenderer {
142
    output: String,
143
    indent_level: usize,
144
    pretty: bool,
145
}
146

147
impl SqlRenderer {
148
    /// Create a new renderer with compact output
NEW
149
    pub fn new() -> Self {
×
NEW
150
        Self {
×
NEW
151
            output: String::with_capacity(DEFAULT_BUFFER_CAPACITY),
×
NEW
152
            indent_level: 0,
×
NEW
153
            pretty: false,
×
NEW
154
        }
×
NEW
155
    }
×
156

157
    /// Create a new renderer with a specific buffer capacity
158
    pub fn with_capacity(capacity: usize) -> Self {
416✔
159
        Self {
416✔
160
            output: String::with_capacity(capacity),
416✔
161
            indent_level: 0,
416✔
162
            pretty: false,
416✔
163
        }
416✔
164
    }
416✔
165

166
    /// Create a new renderer with pretty-printed output
NEW
167
    pub fn pretty() -> Self {
×
NEW
168
        Self {
×
NEW
169
            output: String::with_capacity(DEFAULT_BUFFER_CAPACITY),
×
NEW
170
            indent_level: 0,
×
NEW
171
            pretty: true,
×
NEW
172
        }
×
NEW
173
    }
×
174

175
    /// Estimate appropriate buffer capacity based on statement complexity
176
    pub fn estimate_capacity(stmt: &Stmt) -> usize {
416✔
177
        match stmt {
416✔
178
            Stmt::Select(s) => {
416✔
179
                let cte_count = s.ctes.len();
416✔
180
                if cte_count >= 5 {
416✔
181
                    LARGE_BUFFER_CAPACITY
243✔
182
                } else if cte_count > 0 {
173✔
183
                    CTE_BUFFER_CAPACITY
72✔
184
                } else {
185
                    DEFAULT_BUFFER_CAPACITY
101✔
186
                }
187
            }
NEW
188
            Stmt::Insert(s) => {
×
NEW
189
                let cte_count = s.ctes.len();
×
NEW
190
                if cte_count > 0 {
×
NEW
191
                    CTE_BUFFER_CAPACITY
×
192
                } else {
NEW
193
                    DEFAULT_BUFFER_CAPACITY
×
194
                }
195
            }
NEW
196
            Stmt::Update(s) => {
×
NEW
197
                let cte_count = s.ctes.len();
×
NEW
198
                if cte_count > 0 {
×
NEW
199
                    CTE_BUFFER_CAPACITY
×
200
                } else {
NEW
201
                    DEFAULT_BUFFER_CAPACITY
×
202
                }
203
            }
NEW
204
            Stmt::Delete(s) => {
×
NEW
205
                let cte_count = s.ctes.len();
×
NEW
206
                if cte_count > 0 {
×
NEW
207
                    CTE_BUFFER_CAPACITY
×
208
                } else {
NEW
209
                    DEFAULT_BUFFER_CAPACITY
×
210
                }
211
            }
212
        }
213
    }
416✔
214

215
    /// Render a statement and return the SQL string
216
    pub fn render_stmt(&mut self, stmt: &Stmt) -> &str {
416✔
217
        match stmt {
416✔
218
            Stmt::Select(s) => self.render_select(s),
416✔
NEW
219
            Stmt::Insert(s) => self.render_insert(s),
×
NEW
220
            Stmt::Update(s) => self.render_update(s),
×
NEW
221
            Stmt::Delete(s) => self.render_delete(s),
×
222
        }
223
        &self.output
416✔
224
    }
416✔
225

226
    /// Take ownership of the rendered SQL string
227
    pub fn into_sql(self) -> String {
416✔
228
        self.output
416✔
229
    }
416✔
230

231
    // =========================================================================
232
    // Statement rendering
233
    // =========================================================================
234

235
    fn render_select(&mut self, stmt: &SelectStmt) {
3,763✔
236
        self.render_ctes(&stmt.ctes);
3,763✔
237
        self.write("select ");
3,763✔
238
        self.render_select_columns(&stmt.columns);
3,763✔
239

240
        if let Some(from) = &stmt.from {
3,763✔
241
            self.newline();
2,863✔
242
            self.write("from ");
2,863✔
243
            self.render_from(from);
2,863✔
244
        }
2,863✔
245

246
        if let Some(where_clause) = &stmt.where_clause {
3,763✔
247
            self.newline();
505✔
248
            self.write("where ");
505✔
249
            self.render_expr(where_clause);
505✔
250
        }
3,258✔
251

252
        if !stmt.group_by.is_empty() {
3,763✔
253
            self.newline();
267✔
254
            self.write("group by ");
267✔
255
            self.render_expr_list(&stmt.group_by);
267✔
256
        }
3,496✔
257

258
        if let Some(having) = &stmt.having {
3,763✔
NEW
259
            self.newline();
×
NEW
260
            self.write("having ");
×
NEW
261
            self.render_expr(having);
×
262
        }
3,763✔
263

264
        if !stmt.order_by.is_empty() {
3,763✔
265
            self.newline();
774✔
266
            self.write("order by ");
774✔
267
            self.render_order_by(&stmt.order_by);
774✔
268
        }
2,989✔
269

270
        if let Some(limit) = stmt.limit {
3,763✔
271
            self.newline();
860✔
272
            write!(self.output, "limit {}", limit).unwrap();
860✔
273
        }
2,903✔
274

275
        if let Some(offset) = stmt.offset {
3,763✔
276
            self.newline();
6✔
277
            write!(self.output, "offset {}", offset).unwrap();
6✔
278
        }
3,757✔
279
    }
3,763✔
280

281
    fn render_insert(&mut self, stmt: &InsertStmt) {
29✔
282
        self.render_ctes(&stmt.ctes);
29✔
283
        self.write("insert into ");
29✔
284
        self.render_table_name(stmt.schema.as_ref(), &stmt.table);
29✔
285

286
        if !stmt.columns.is_empty() {
29✔
287
            self.write("(");
29✔
288
            for (i, col) in stmt.columns.iter().enumerate() {
60✔
289
                if i > 0 {
60✔
290
                    self.write(", ");
31✔
291
                }
31✔
292
                self.write_ident(col);
60✔
293
            }
294
            self.write(")");
29✔
NEW
295
        }
×
296

297
        self.newline();
29✔
298
        match &stmt.values {
29✔
299
            InsertValues::Values(rows) => {
29✔
300
                self.write("values ");
29✔
301
                for (i, row) in rows.iter().enumerate() {
33✔
302
                    if i > 0 {
33✔
303
                        self.write(", ");
4✔
304
                    }
29✔
305
                    self.write("(");
33✔
306
                    self.render_expr_list(row);
33✔
307
                    self.write(")");
33✔
308
                }
309
            }
NEW
310
            InsertValues::Query(query) => {
×
NEW
311
                self.render_select(query);
×
NEW
312
            }
×
NEW
313
            InsertValues::DefaultValues => {
×
NEW
314
                self.write("default values");
×
NEW
315
            }
×
316
        }
317

318
        if let Some(on_conflict) = &stmt.on_conflict {
29✔
NEW
319
            self.newline();
×
NEW
320
            self.render_on_conflict(on_conflict);
×
321
        }
29✔
322

323
        if !stmt.returning.is_empty() {
29✔
324
            self.newline();
29✔
325
            self.write("returning ");
29✔
326
            self.render_select_columns(&stmt.returning);
29✔
327
        }
29✔
328
    }
29✔
329

330
    fn render_update(&mut self, stmt: &UpdateStmt) {
26✔
331
        self.render_ctes(&stmt.ctes);
26✔
332
        self.write("update ");
26✔
333
        self.render_table_name(stmt.schema.as_ref(), &stmt.table);
26✔
334

335
        if let Some(alias) = &stmt.alias {
26✔
336
            self.write(" as ");
26✔
337
            self.write_ident(alias);
26✔
338
        }
26✔
339

340
        self.newline();
26✔
341
        self.write("set ");
26✔
342
        for (i, (col, expr)) in stmt.set.iter().enumerate() {
48✔
343
            if i > 0 {
48✔
344
                self.write(", ");
22✔
345
            }
26✔
346
            self.write_ident(col);
48✔
347
            self.write(" = ");
48✔
348
            self.render_expr(expr);
48✔
349
        }
350

351
        if let Some(from) = &stmt.from {
26✔
NEW
352
            self.newline();
×
NEW
353
            self.write("from ");
×
NEW
354
            self.render_from(from);
×
355
        }
26✔
356

357
        if let Some(where_clause) = &stmt.where_clause {
26✔
358
            self.newline();
18✔
359
            self.write("where ");
18✔
360
            self.render_expr(where_clause);
18✔
361
        }
18✔
362

363
        if !stmt.returning.is_empty() {
26✔
364
            self.newline();
26✔
365
            self.write("returning ");
26✔
366
            self.render_select_columns(&stmt.returning);
26✔
367
        }
26✔
368
    }
26✔
369

370
    fn render_delete(&mut self, stmt: &DeleteStmt) {
17✔
371
        self.render_ctes(&stmt.ctes);
17✔
372
        self.write("delete from ");
17✔
373
        self.render_table_name(stmt.schema.as_ref(), &stmt.table);
17✔
374

375
        if let Some(alias) = &stmt.alias {
17✔
376
            self.write(" as ");
17✔
377
            self.write_ident(alias);
17✔
378
        }
17✔
379

380
        if let Some(using) = &stmt.using {
17✔
NEW
381
            self.newline();
×
NEW
382
            self.write("using ");
×
NEW
383
            self.render_from(using);
×
384
        }
17✔
385

386
        if let Some(where_clause) = &stmt.where_clause {
17✔
387
            self.newline();
13✔
388
            self.write("where ");
13✔
389
            self.render_expr(where_clause);
13✔
390
        }
13✔
391

392
        if !stmt.returning.is_empty() {
17✔
393
            self.newline();
17✔
394
            self.write("returning ");
17✔
395
            self.render_select_columns(&stmt.returning);
17✔
396
        }
17✔
397
    }
17✔
398

399
    // =========================================================================
400
    // CTE rendering
401
    // =========================================================================
402

403
    fn render_ctes(&mut self, ctes: &[Cte]) {
3,835✔
404
        if ctes.is_empty() {
3,835✔
405
            return;
2,989✔
406
        }
846✔
407

408
        self.write("with ");
846✔
409
        for (i, cte) in ctes.iter().enumerate() {
2,577✔
410
            if i > 0 {
2,577✔
411
                self.write(", ");
1,731✔
412
            }
1,731✔
413
            self.render_cte(cte);
2,577✔
414
        }
415
        self.newline();
846✔
416
    }
3,835✔
417

418
    fn render_cte(&mut self, cte: &Cte) {
2,577✔
419
        self.write_ident(&cte.name);
2,577✔
420

421
        if let Some(columns) = &cte.columns {
2,577✔
422
            self.write("(");
1,731✔
423
            for (i, col) in columns.iter().enumerate() {
1,731✔
424
                if i > 0 {
1,731✔
NEW
425
                    self.write(", ");
×
426
                }
1,731✔
427
                self.write_ident(col);
1,731✔
428
            }
429
            self.write(")");
1,731✔
430
        }
846✔
431

432
        self.write(" as ");
2,577✔
433

434
        if let Some(materialized) = cte.materialized {
2,577✔
NEW
435
            if materialized {
×
NEW
436
                self.write("materialized ");
×
NEW
437
            } else {
×
NEW
438
                self.write("not materialized ");
×
NEW
439
            }
×
440
        }
2,577✔
441

442
        self.write("(");
2,577✔
443
        self.indent();
2,577✔
444
        self.newline();
2,577✔
445

446
        match &cte.query {
2,577✔
447
            CteQuery::Select(s) => self.render_select(s),
2,505✔
448
            CteQuery::Insert(s) => self.render_insert(s),
29✔
449
            CteQuery::Update(s) => self.render_update(s),
26✔
450
            CteQuery::Delete(s) => self.render_delete(s),
17✔
451
        }
452

453
        self.dedent();
2,577✔
454
        self.newline();
2,577✔
455
        self.write(")");
2,577✔
456
    }
2,577✔
457

458
    // =========================================================================
459
    // FROM clause rendering
460
    // =========================================================================
461

462
    fn render_from(&mut self, from: &FromClause) {
6,633✔
463
        match from {
6,633✔
464
            FromClause::Table {
465
                schema,
4,223✔
466
                name,
4,223✔
467
                alias,
4,223✔
468
            } => {
469
                self.render_table_name(schema.as_ref(), name);
4,223✔
470
                if let Some(alias) = alias {
4,223✔
471
                    self.write(" ");
1,435✔
472
                    self.write_ident(alias);
1,435✔
473
                }
2,788✔
474
            }
475
            FromClause::Subquery { query, alias } => {
507✔
476
                self.write("(");
507✔
477
                self.render_select(query);
507✔
478
                self.write(") ");
507✔
479
                self.write_ident(alias);
507✔
480
            }
507✔
481
            FromClause::Function { call, alias } => {
18✔
482
                self.render_function_call(call);
18✔
483
                self.write(" ");
18✔
484
                self.write_ident(alias);
18✔
485
            }
18✔
486
            FromClause::Join {
487
                left,
1,885✔
488
                join_type,
1,885✔
489
                right,
1,885✔
490
                on,
1,885✔
491
            } => {
492
                self.render_from(left);
1,885✔
493
                self.newline();
1,885✔
494
                self.write(join_type.as_sql());
1,885✔
495
                self.write(" ");
1,885✔
496
                self.render_from(right);
1,885✔
497
                if let Some(on_expr) = on {
1,885✔
498
                    self.write(" on ");
1,885✔
499
                    self.render_expr(on_expr);
1,885✔
500
                }
1,885✔
501
            }
NEW
502
            FromClause::CrossJoin { left, right } => {
×
NEW
503
                self.render_from(left);
×
NEW
504
                self.write(" cross join ");
×
NEW
505
                self.render_from(right);
×
NEW
506
            }
×
NEW
507
            FromClause::Lateral { subquery, alias } => {
×
NEW
508
                self.write("lateral (");
×
NEW
509
                self.render_select(subquery);
×
NEW
510
                self.write(") ");
×
NEW
511
                self.write_ident(alias);
×
NEW
512
            }
×
513
        }
514
    }
6,633✔
515

516
    // =========================================================================
517
    // Expression rendering
518
    // =========================================================================
519

520
    fn render_expr(&mut self, expr: &Expr) {
24,934✔
521
        match expr {
24,934✔
522
            Expr::Column(col) => {
7,666✔
523
                if let Some(table) = &col.table_alias {
7,666✔
524
                    self.write_ident(table);
6,476✔
525
                    self.write(".");
6,476✔
526
                }
6,476✔
527
                self.write_ident(&col.column);
7,666✔
528
            }
529

530
            Expr::Literal(lit) => self.render_literal(lit),
7,021✔
531

532
            Expr::Param(p) => {
1,022✔
533
                write!(self.output, "(${}", p.index).unwrap();
1,022✔
534
                self.write("::");
1,022✔
535
                self.render_type(&p.type_cast);
1,022✔
536
                self.write(")");
1,022✔
537
            }
1,022✔
538

539
            Expr::BinaryOp { left, op, right } => {
2,039✔
540
                self.render_expr(left);
2,039✔
541
                self.write(" ");
2,039✔
542
                self.write(op.as_sql());
2,039✔
543
                // The ANY operator needs parentheses around the right operand: col = any(arr)
544
                if *op == BinaryOperator::Any {
2,039✔
545
                    self.write("(");
288✔
546
                    self.render_expr(right);
288✔
547
                    self.write(")");
288✔
548
                } else {
1,751✔
549
                    self.write(" ");
1,751✔
550
                    self.render_expr(right);
1,751✔
551
                }
1,751✔
552
            }
553

554
            Expr::UnaryOp { op, expr } => {
281✔
555
                self.write(op.as_sql());
281✔
556
                self.write("(");
281✔
557
                self.render_expr(expr);
281✔
558
                self.write(")");
281✔
559
            }
281✔
560

561
            Expr::FunctionCall(call) => {
1,303✔
562
                self.render_function_call(call);
1,303✔
563
            }
1,303✔
564

565
            Expr::Aggregate(agg) => {
1,419✔
566
                self.render_aggregate(agg);
1,419✔
567
            }
1,419✔
568

569
            Expr::Case(case) => {
323✔
570
                self.render_case(case);
323✔
571
            }
323✔
572

573
            Expr::Subquery(query) => {
68✔
574
                self.write("(");
68✔
575
                self.render_select(query);
68✔
576
                self.write(")");
68✔
577
            }
68✔
578

NEW
579
            Expr::Array(exprs) => {
×
NEW
580
                self.write("array[");
×
NEW
581
                self.render_expr_list(exprs);
×
NEW
582
                self.write("]");
×
NEW
583
            }
×
584

585
            Expr::Cast { expr, target_type } => {
729✔
586
                self.render_expr(expr);
729✔
587
                self.write("::");
729✔
588
                self.render_type(target_type);
729✔
589
            }
729✔
590

591
            Expr::IsNull { expr, negated } => {
646✔
592
                self.render_expr(expr);
646✔
593
                if *negated {
646✔
594
                    self.write(" is not null");
413✔
595
                } else {
413✔
596
                    self.write(" is null");
233✔
597
                }
233✔
598
            }
599

600
            Expr::InList {
NEW
601
                expr,
×
NEW
602
                list,
×
NEW
603
                negated,
×
604
            } => {
NEW
605
                self.render_expr(expr);
×
NEW
606
                if *negated {
×
NEW
607
                    self.write(" not in (");
×
NEW
608
                } else {
×
NEW
609
                    self.write(" in (");
×
NEW
610
                }
×
NEW
611
                self.render_expr_list(list);
×
NEW
612
                self.write(")");
×
613
            }
614

615
            Expr::Between {
NEW
616
                expr,
×
NEW
617
                low,
×
NEW
618
                high,
×
NEW
619
                negated,
×
620
            } => {
NEW
621
                self.render_expr(expr);
×
NEW
622
                if *negated {
×
NEW
623
                    self.write(" not between ");
×
NEW
624
                } else {
×
NEW
625
                    self.write(" between ");
×
NEW
626
                }
×
NEW
627
                self.render_expr(low);
×
NEW
628
                self.write(" and ");
×
NEW
629
                self.render_expr(high);
×
630
            }
631

632
            Expr::Exists { subquery, negated } => {
267✔
633
                if *negated {
267✔
NEW
634
                    self.write("not ");
×
635
                }
267✔
636
                self.write("exists (");
267✔
637
                self.render_select(subquery);
267✔
638
                self.write(")");
267✔
639
            }
640

641
            Expr::JsonBuild(json) => {
1,043✔
642
                self.render_json_build(json);
1,043✔
643
            }
1,043✔
644

645
            Expr::Coalesce(exprs) => {
885✔
646
                self.write("coalesce(");
885✔
647
                self.render_expr_list(exprs);
885✔
648
                self.write(")");
885✔
649
            }
885✔
650

651
            Expr::Nested(inner) => {
146✔
652
                self.write("(");
146✔
653
                self.render_expr(inner);
146✔
654
                self.write(")");
146✔
655
            }
146✔
656

657
            // Raw SQL - SECURITY SENSITIVE
658
            // This variant is only available in test code (#[cfg(test)]).
659
            // It outputs the string directly without any escaping.
660
            // NEVER expose this outside of tests - use proper AST nodes instead.
661
            #[cfg(test)]
662
            Expr::Raw(sql) => {
663
                self.write(sql);
664
            }
665

666
            Expr::ArrayIndex { array, index } => {
36✔
667
                self.write("(");
36✔
668
                self.render_expr(array);
36✔
669
                self.write(")[");
36✔
670
                self.render_expr(index);
36✔
671
                self.write("]");
36✔
672
            }
36✔
673

674
            Expr::FunctionCallWithOrderBy {
675
                name,
36✔
676
                args,
36✔
677
                order_by,
36✔
678
            } => {
679
                self.write(name);
36✔
680
                self.write("(");
36✔
681
                for (i, arg) in args.iter().enumerate() {
36✔
682
                    if i > 0 {
36✔
NEW
683
                        self.write(", ");
×
684
                    }
36✔
685
                    self.render_expr(arg);
36✔
686
                }
687
                if !order_by.is_empty() {
36✔
688
                    self.write(" order by ");
36✔
689
                    self.render_order_by(order_by);
36✔
690
                }
36✔
691
                self.write(")");
36✔
692
            }
693

694
            Expr::Row(exprs) => {
4✔
695
                // ROW constructor: ROW(expr1, expr2, ...) or just (expr1, expr2, ...)
4✔
696
                // We use the explicit ROW keyword for clarity
4✔
697
                self.write("row(");
4✔
698
                self.render_expr_list(exprs);
4✔
699
                self.write(")");
4✔
700
            }
4✔
701
        }
702
    }
24,934✔
703

704
    fn render_literal(&mut self, lit: &Literal) {
7,021✔
705
        match lit {
7,021✔
706
            Literal::Null => self.write("null"),
291✔
707
            Literal::Bool(b) => self.write(if *b { "true" } else { "false" }),
2,354✔
708
            Literal::Integer(n) => write!(self.output, "{}", n).unwrap(),
1,147✔
NEW
709
            Literal::Float(f) => write!(self.output, "{}", f).unwrap(),
×
710
            Literal::String(s) => self.write_literal(s),
3,226✔
711
            Literal::Default => self.write("default"),
3✔
712
        }
713
    }
7,021✔
714

715
    fn render_function_call(&mut self, call: &FunctionCall) {
1,321✔
716
        if let Some(schema) = &call.schema {
1,321✔
717
            self.write_ident(schema);
157✔
718
            self.write(".");
157✔
719
        }
1,164✔
720
        self.write_ident(&call.name);
1,321✔
721
        self.write("(");
1,321✔
722

723
        for (i, arg) in call.args.iter().enumerate() {
1,321✔
724
            if i > 0 {
1,214✔
725
                self.write(", ");
469✔
726
            }
745✔
727
            match arg {
1,214✔
728
                FunctionArg::Unnamed(expr) => self.render_expr(expr),
1,086✔
729
                FunctionArg::Named { name, value } => {
128✔
730
                    self.write_ident(name);
128✔
731
                    self.write(" => ");
128✔
732
                    self.render_expr(value);
128✔
733
                }
128✔
734
            }
735
        }
736

737
        if let Some(order_by) = &call.order_by {
1,321✔
NEW
738
            if !order_by.is_empty() {
×
NEW
739
                self.write(" order by ");
×
NEW
740
                self.render_order_by(order_by);
×
NEW
741
            }
×
742
        }
1,321✔
743

744
        self.write(")");
1,321✔
745

746
        if let Some(filter) = &call.filter {
1,321✔
NEW
747
            self.write(" filter (where ");
×
NEW
748
            self.render_expr(filter);
×
NEW
749
            self.write(")");
×
750
        }
1,321✔
751
    }
1,321✔
752

753
    fn render_aggregate(&mut self, agg: &AggregateExpr) {
1,419✔
754
        self.write(agg.function.as_sql());
1,419✔
755
        self.write("(");
1,419✔
756

757
        if agg.distinct {
1,419✔
NEW
758
            self.write("distinct ");
×
759
        }
1,419✔
760

761
        // Special case for count(*)
762
        if agg.args.is_empty() && matches!(agg.function, AggregateFunction::Count) {
1,419✔
NEW
763
            self.write("*");
×
764
        } else {
1,419✔
765
            self.render_expr_list(&agg.args);
1,419✔
766
        }
1,419✔
767

768
        if let Some(order_by) = &agg.order_by {
1,419✔
769
            if !order_by.is_empty() {
240✔
770
                self.write(" order by ");
240✔
771
                self.render_order_by(order_by);
240✔
772
            }
240✔
773
        }
1,179✔
774

775
        self.write(")");
1,419✔
776

777
        if let Some(filter) = &agg.filter {
1,419✔
778
            self.write(" filter (where ");
240✔
779
            self.render_expr(filter);
240✔
780
            self.write(")");
240✔
781
        }
1,179✔
782
    }
1,419✔
783

784
    fn render_case(&mut self, case: &CaseExpr) {
323✔
785
        self.write("case");
323✔
786

787
        if let Some(operand) = &case.operand {
323✔
NEW
788
            self.write(" ");
×
NEW
789
            self.render_expr(operand);
×
790
        }
323✔
791

792
        for (when_expr, then_expr) in &case.when_clauses {
650✔
793
            self.write(" when ");
327✔
794
            self.render_expr(when_expr);
327✔
795
            self.write(" then ");
327✔
796
            self.render_expr(then_expr);
327✔
797
        }
327✔
798

799
        if let Some(else_clause) = &case.else_clause {
323✔
800
            self.write(" else ");
323✔
801
            self.render_expr(else_clause);
323✔
802
        }
323✔
803

804
        self.write(" end");
323✔
805
    }
323✔
806

807
    fn render_json_build(&mut self, json: &JsonBuildExpr) {
1,043✔
808
        match json {
1,043✔
809
            JsonBuildExpr::Object(pairs) => {
1,043✔
810
                self.write("jsonb_build_object(");
1,043✔
811
                for (i, (key, value)) in pairs.iter().enumerate() {
1,842✔
812
                    if i > 0 {
1,842✔
813
                        self.write(", ");
818✔
814
                    }
1,024✔
815
                    self.render_expr(key);
1,842✔
816
                    self.write(", ");
1,842✔
817
                    self.render_expr(value);
1,842✔
818
                }
819
                self.write(")");
1,043✔
820
            }
NEW
821
            JsonBuildExpr::Array(exprs) => {
×
NEW
822
                self.write("jsonb_build_array(");
×
NEW
823
                self.render_expr_list(exprs);
×
NEW
824
                self.write(")");
×
NEW
825
            }
×
826
        }
827
    }
1,043✔
828

NEW
829
    fn render_on_conflict(&mut self, on_conflict: &OnConflict) {
×
NEW
830
        self.write("on conflict ");
×
831

NEW
832
        match &on_conflict.target {
×
NEW
833
            OnConflictTarget::Columns(cols) => {
×
NEW
834
                self.write("(");
×
NEW
835
                for (i, col) in cols.iter().enumerate() {
×
NEW
836
                    if i > 0 {
×
NEW
837
                        self.write(", ");
×
NEW
838
                    }
×
NEW
839
                    self.write_ident(col);
×
840
                }
NEW
841
                self.write(") ");
×
842
            }
NEW
843
            OnConflictTarget::Constraint(name) => {
×
NEW
844
                self.write("on constraint ");
×
NEW
845
                self.write_ident(name);
×
NEW
846
                self.write(" ");
×
NEW
847
            }
×
848
        }
849

NEW
850
        match &on_conflict.action {
×
NEW
851
            OnConflictAction::DoNothing => {
×
NEW
852
                self.write("do nothing");
×
NEW
853
            }
×
NEW
854
            OnConflictAction::DoUpdate { set, where_clause } => {
×
NEW
855
                self.write("do update set ");
×
NEW
856
                for (i, (col, expr)) in set.iter().enumerate() {
×
NEW
857
                    if i > 0 {
×
NEW
858
                        self.write(", ");
×
NEW
859
                    }
×
NEW
860
                    self.write_ident(col);
×
NEW
861
                    self.write(" = ");
×
NEW
862
                    self.render_expr(expr);
×
863
                }
NEW
864
                if let Some(where_clause) = where_clause {
×
NEW
865
                    self.write(" where ");
×
NEW
866
                    self.render_expr(where_clause);
×
NEW
867
                }
×
868
            }
869
        }
NEW
870
    }
×
871

872
    // =========================================================================
873
    // Helper methods
874
    // =========================================================================
875

876
    fn render_select_columns(&mut self, columns: &[SelectColumn]) {
3,835✔
877
        for (i, col) in columns.iter().enumerate() {
4,655✔
878
            if i > 0 {
4,655✔
879
                self.write(", ");
820✔
880
            }
3,835✔
881
            match col {
4,655✔
882
                SelectColumn::Expr { expr, alias } => {
4,655✔
883
                    self.render_expr(expr);
4,655✔
884
                    if let Some(alias) = alias {
4,655✔
885
                        self.write(" as ");
480✔
886
                        self.write_ident(alias);
480✔
887
                    }
4,175✔
888
                }
NEW
889
                SelectColumn::Star => self.write("*"),
×
NEW
890
                SelectColumn::QualifiedStar { table } => {
×
NEW
891
                    self.write_ident(table);
×
NEW
892
                    self.write(".*");
×
NEW
893
                }
×
894
            }
895
        }
896
    }
3,835✔
897

898
    fn render_expr_list(&mut self, exprs: &[Expr]) {
2,608✔
899
        for (i, expr) in exprs.iter().enumerate() {
4,332✔
900
            if i > 0 {
4,332✔
901
                self.write(", ");
1,724✔
902
            }
2,608✔
903
            self.render_expr(expr);
4,332✔
904
        }
905
    }
2,608✔
906

907
    fn render_order_by(&mut self, order_by: &[OrderByExpr]) {
1,050✔
908
        for (i, ob) in order_by.iter().enumerate() {
1,375✔
909
            if i > 0 {
1,375✔
910
                self.write(", ");
325✔
911
            }
1,050✔
912
            self.render_expr(&ob.expr);
1,375✔
913
            if let Some(dir) = &ob.direction {
1,375✔
914
                self.write(" ");
1,375✔
915
                self.write(dir.as_sql());
1,375✔
916
            }
1,375✔
917
            if let Some(nulls) = &ob.nulls {
1,375✔
918
                self.write(" ");
1,375✔
919
                self.write(nulls.as_sql());
1,375✔
920
            }
1,375✔
921
        }
922
    }
1,050✔
923

924
    fn render_table_name(&mut self, schema: Option<&Ident>, name: &Ident) {
4,295✔
925
        if let Some(schema) = schema {
4,295✔
926
            self.write_ident(schema);
1,168✔
927
            self.write(".");
1,168✔
928
        }
3,127✔
929
        self.write_ident(name);
4,295✔
930
    }
4,295✔
931

932
    fn render_type(&mut self, sql_type: &SqlType) {
1,751✔
933
        // Type names come from trusted sources (database schema metadata loaded via
934
        // SQL queries), so we output them directly without quoting.
935
        if let Some(schema) = &sql_type.schema {
1,751✔
936
            self.output.push_str(schema);
19✔
937
            self.output.push('.');
19✔
938
        }
1,732✔
939
        self.output.push_str(&sql_type.name);
1,751✔
940
        if sql_type.is_array {
1,751✔
941
            self.output.push_str("[]");
107✔
942
        }
1,644✔
943
    }
1,751✔
944

945
    // =========================================================================
946
    // Low-level output methods
947
    // =========================================================================
948

949
    fn write(&mut self, s: &str) {
78,108✔
950
        self.output.push_str(s);
78,108✔
951
    }
78,108✔
952

953
    fn write_ident(&mut self, ident: &Ident) {
28,110✔
954
        // Use PostgreSQL's native quote_ident() for proper escaping
955
        self.output.push_str(&quote_ident(ident.as_str()));
28,110✔
956
    }
28,110✔
957

958
    fn write_literal(&mut self, s: &str) {
3,226✔
959
        // Use PostgreSQL's native quote_literal() for proper escaping
960
        self.output.push_str(&quote_literal(s));
3,226✔
961
    }
3,226✔
962

963
    fn newline(&mut self) {
13,318✔
964
        if self.pretty {
13,318✔
NEW
965
            self.output.push('\n');
×
NEW
966
            for _ in 0..self.indent_level {
×
NEW
967
                self.output.push_str("    ");
×
NEW
968
            }
×
969
        } else {
13,318✔
970
            self.output.push(' ');
13,318✔
971
        }
13,318✔
972
    }
13,318✔
973

974
    fn indent(&mut self) {
2,577✔
975
        self.indent_level += 1;
2,577✔
976
    }
2,577✔
977

978
    fn dedent(&mut self) {
2,577✔
979
        if self.indent_level > 0 {
2,577✔
980
            self.indent_level -= 1;
2,577✔
981
        }
2,577✔
982
    }
2,577✔
983
}
984

985
impl Default for SqlRenderer {
NEW
986
    fn default() -> Self {
×
NEW
987
        Self::new()
×
NEW
988
    }
×
989
}
990

991
// =========================================================================
992
// Convenience functions
993
// =========================================================================
994

995
/// Render a statement to a compact SQL string
996
///
997
/// Uses estimated buffer capacity based on statement complexity to reduce allocations.
998
pub fn render(stmt: &Stmt) -> String {
416✔
999
    let capacity = SqlRenderer::estimate_capacity(stmt);
416✔
1000
    let mut renderer = SqlRenderer::with_capacity(capacity);
416✔
1001
    renderer.render_stmt(stmt);
416✔
1002
    renderer.into_sql()
416✔
1003
}
416✔
1004

1005
/// Render a statement to a pretty-printed SQL string
NEW
1006
pub fn render_pretty(stmt: &Stmt) -> String {
×
NEW
1007
    let capacity = SqlRenderer::estimate_capacity(stmt);
×
NEW
1008
    let mut renderer = SqlRenderer {
×
NEW
1009
        output: String::with_capacity(capacity),
×
NEW
1010
        indent_level: 0,
×
NEW
1011
        pretty: true,
×
NEW
1012
    };
×
NEW
1013
    renderer.render_stmt(stmt);
×
NEW
1014
    renderer.into_sql()
×
NEW
1015
}
×
1016

1017
/// Render just a SELECT statement
NEW
1018
pub fn render_select(stmt: &SelectStmt) -> String {
×
NEW
1019
    render(&Stmt::Select(stmt.clone()))
×
NEW
1020
}
×
1021

1022
/// Render just an expression
NEW
1023
pub fn render_expr(expr: &Expr) -> String {
×
NEW
1024
    let mut renderer = SqlRenderer::new();
×
NEW
1025
    renderer.render_expr(expr);
×
NEW
1026
    renderer.into_sql()
×
NEW
1027
}
×
1028

1029
#[cfg(test)]
1030
mod tests {
1031
    use super::*;
1032
    use crate::ast::*;
1033

1034
    #[test]
1035
    fn test_render_simple_select() {
1036
        let stmt =
1037
            SelectStmt::columns(vec![SelectColumn::star()]).with_from(FromClause::table("users"));
1038

1039
        let sql = render(&Stmt::Select(stmt));
1040
        assert!(sql.contains("select *"));
1041
        assert!(sql.contains("from \"users\""));
1042
    }
1043

1044
    #[test]
1045
    fn test_render_select_with_where() {
1046
        let stmt = SelectStmt::columns(vec![
1047
            SelectColumn::expr(Expr::qualified_column("t", "id")),
1048
            SelectColumn::expr(Expr::qualified_column("t", "name")),
1049
        ])
1050
        .with_from(FromClause::table("users").with_alias("t"))
1051
        .with_where(Expr::qualified_column("t", "active").eq(Expr::bool(true)));
1052

1053
        let sql = render(&Stmt::Select(stmt));
1054
        assert!(sql.contains("\"t\".\"id\""));
1055
        assert!(sql.contains("\"t\".\"name\""));
1056
        assert!(sql.contains("where"));
1057
        assert!(sql.contains("\"t\".\"active\" = true"));
1058
    }
1059

1060
    #[test]
1061
    fn test_render_insert() {
1062
        let stmt = InsertStmt::new(
1063
            "users",
1064
            vec![Ident::new("name"), Ident::new("email")],
1065
            InsertValues::Values(vec![vec![
1066
                Expr::string("Alice"),
1067
                Expr::string("alice@example.com"),
1068
            ]]),
1069
        )
1070
        .with_schema("public")
1071
        .with_returning(vec![SelectColumn::expr(Expr::column("id"))]);
1072

1073
        let sql = render(&Stmt::Insert(stmt));
1074
        assert!(sql.contains("insert into \"public\".\"users\""));
1075
        assert!(sql.contains("(\"name\", \"email\")"));
1076
        assert!(sql.contains("values"));
1077
        assert!(sql.contains("returning"));
1078
    }
1079

1080
    #[test]
1081
    fn test_render_update() {
1082
        let stmt = UpdateStmt::new("users", vec![(Ident::new("name"), Expr::string("Bob"))])
1083
            .with_where(Expr::column("id").eq(Expr::int(1)));
1084

1085
        let sql = render(&Stmt::Update(stmt));
1086
        assert!(sql.contains("update \"users\""));
1087
        assert!(sql.contains("set \"name\" ="));
1088
        assert!(sql.contains("where"));
1089
    }
1090

1091
    #[test]
1092
    fn test_render_delete() {
1093
        let stmt = DeleteStmt::new("users").with_where(Expr::column("id").eq(Expr::int(1)));
1094

1095
        let sql = render(&Stmt::Delete(stmt));
1096
        assert!(sql.contains("delete from \"users\""));
1097
        assert!(sql.contains("where"));
1098
    }
1099

1100
    #[test]
1101
    fn test_render_cte() {
1102
        let inner_select = SelectStmt::columns(vec![SelectColumn::star()])
1103
            .with_from(FromClause::table("users"))
1104
            .with_where(Expr::column("active").eq(Expr::bool(true)));
1105

1106
        let outer_select = SelectStmt::columns(vec![SelectColumn::star()])
1107
            .with_from(FromClause::table("active_users"))
1108
            .with_ctes(vec![Cte::select("active_users", inner_select)]);
1109

1110
        let sql = render(&Stmt::Select(outer_select));
1111
        assert!(sql.contains("with \"active_users\" as"));
1112
        assert!(sql.contains("from \"active_users\""));
1113
    }
1114

1115
    #[test]
1116
    fn test_render_param() {
1117
        let param_ref = ParamRef {
1118
            index: 1,
1119
            type_cast: SqlType::text(),
1120
        };
1121
        let expr = Expr::Param(param_ref);
1122
        let sql = render_expr(&expr);
1123
        assert_eq!(sql, "($1::text)");
1124
    }
1125

1126
    #[test]
1127
    fn test_render_jsonb_build_object() {
1128
        let expr = Expr::jsonb_build_object(vec![
1129
            (Expr::string("name"), Expr::string("Alice")),
1130
            (Expr::string("age"), Expr::int(30)),
1131
        ]);
1132
        let sql = render_expr(&expr);
1133
        assert!(sql.contains("jsonb_build_object"));
1134
        assert!(sql.contains("'name'"));
1135
        assert!(sql.contains("'Alice'"));
1136
    }
1137

1138
    #[test]
1139
    fn test_render_aggregate() {
1140
        let agg = AggregateExpr::new(AggregateFunction::Count, vec![]);
1141
        let expr = Expr::Aggregate(agg);
1142
        let sql = render_expr(&expr);
1143
        assert_eq!(sql, "count(*)");
1144
    }
1145

1146
    #[test]
1147
    fn test_render_case() {
1148
        let case = CaseExpr::searched(
1149
            vec![
1150
                (
1151
                    Expr::column("status").eq(Expr::string("active")),
1152
                    Expr::int(1),
1153
                ),
1154
                (
1155
                    Expr::column("status").eq(Expr::string("inactive")),
1156
                    Expr::int(0),
1157
                ),
1158
            ],
1159
            Some(Expr::int(-1)),
1160
        );
1161
        let expr = Expr::Case(case);
1162
        let sql = render_expr(&expr);
1163
        assert!(sql.contains("case"));
1164
        assert!(sql.contains("when"));
1165
        assert!(sql.contains("then"));
1166
        assert!(sql.contains("else"));
1167
        assert!(sql.contains("end"));
1168
    }
1169

1170
    #[test]
1171
    fn test_ident_quoting() {
1172
        let ident = Ident::new("user\"name");
1173
        let mut renderer = SqlRenderer::new();
1174
        renderer.write_ident(&ident);
1175
        let sql = renderer.into_sql();
1176
        assert_eq!(sql, "\"user\"\"name\"");
1177
    }
1178

1179
    #[test]
1180
    fn test_string_literal_with_quotes() {
1181
        let mut renderer = SqlRenderer::new();
1182
        renderer.write_literal("it's a test");
1183
        let sql = renderer.into_sql();
1184
        assert!(sql.contains("$__$"));
1185
    }
1186
}
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