Skip to main content

qcraft_postgres/
lib.rs

1use qcraft_core::ast::common::{FieldRef, NullsOrder, OrderByDef, OrderDir, SchemaRef};
2use qcraft_core::ast::conditions::{CompareOp, ConditionNode, Conditions, Connector};
3use qcraft_core::ast::ddl::{
4    ColumnDef, ConstraintDef, DeferrableConstraint, FieldType, IdentityColumn, IndexColumnDef,
5    IndexDef, IndexExpr, LikeTableDef, MatchType, OnCommitAction, PartitionByDef,
6    PartitionStrategy, ReferentialAction, SchemaDef, SchemaMutationStmt,
7};
8use qcraft_core::ast::dml::{
9    ConflictAction, ConflictTarget, DeleteStmt, InsertSource, InsertStmt, MutationStmt,
10    OnConflictDef, OverridingKind, UpdateStmt,
11};
12use qcraft_core::ast::expr::{
13    AggregationDef, BinaryOp, CaseDef, Expr, UnaryOp, WindowDef, WindowFrameBound, WindowFrameDef,
14    WindowFrameType,
15};
16use qcraft_core::ast::query::{
17    CteDef, CteMaterialized, DistinctDef, FromItem, GroupByItem, JoinCondition, JoinDef, JoinType,
18    LimitDef, LimitKind, LockStrength, QueryStmt, SampleMethod, SelectColumn, SelectLockDef,
19    SetOpDef, SetOperationType, TableSource, WindowNameDef,
20};
21use qcraft_core::ast::tcl::{
22    BeginStmt, CommitStmt, IsolationLevel, LockMode, LockTableStmt, RollbackStmt,
23    SetTransactionStmt, TransactionMode, TransactionScope, TransactionStmt,
24};
25use qcraft_core::ast::value::Value;
26use qcraft_core::error::{RenderError, RenderResult};
27use qcraft_core::render::ctx::{ParamStyle, RenderCtx};
28use qcraft_core::render::escape_like_value;
29use qcraft_core::render::renderer::Renderer;
30
31fn render_like_pattern(op: &CompareOp, right: &Expr, ctx: &mut RenderCtx) -> RenderResult<()> {
32    let raw = match right {
33        Expr::Value(Value::Str(s)) => s.as_str(),
34        _ => {
35            return Err(RenderError::unsupported(
36                "CompareOp",
37                "Contains/StartsWith/EndsWith require a string value on the right side",
38            ));
39        }
40    };
41    let escaped = escape_like_value(raw);
42    let pattern = match op {
43        CompareOp::Contains | CompareOp::IContains => format!("%{escaped}%"),
44        CompareOp::StartsWith | CompareOp::IStartsWith => format!("{escaped}%"),
45        CompareOp::EndsWith | CompareOp::IEndsWith => format!("%{escaped}"),
46        _ => unreachable!(),
47    };
48    if ctx.parameterize() {
49        ctx.param(Value::Str(pattern));
50    } else {
51        ctx.string_literal(&pattern);
52    }
53    Ok(())
54}
55
56struct PgCreateTableOpts<'a> {
57    tablespace: Option<&'a str>,
58    partition_by: Option<&'a PartitionByDef>,
59    inherits: Option<&'a [SchemaRef]>,
60    using_method: Option<&'a str>,
61    with_options: Option<&'a [(String, String)]>,
62    on_commit: Option<&'a OnCommitAction>,
63}
64
65pub struct PostgresRenderer {
66    param_style: ParamStyle,
67}
68
69impl PostgresRenderer {
70    pub fn new() -> Self {
71        Self {
72            param_style: ParamStyle::Dollar,
73        }
74    }
75
76    /// Use `%s` placeholders (psycopg / DB-API 2.0) instead of `$1`.
77    pub fn with_param_style(mut self, style: ParamStyle) -> Self {
78        self.param_style = style;
79        self
80    }
81
82    /// Convenience: render a DDL statement to SQL string + params.
83    pub fn render_schema_stmt(
84        &self,
85        stmt: &SchemaMutationStmt,
86    ) -> RenderResult<(String, Vec<Value>)> {
87        let mut ctx = RenderCtx::new(self.param_style);
88        self.render_schema_mutation(stmt, &mut ctx)?;
89        Ok(ctx.finish())
90    }
91
92    /// Convenience: render a TCL statement to SQL string + params.
93    pub fn render_transaction_stmt(
94        &self,
95        stmt: &TransactionStmt,
96    ) -> RenderResult<(String, Vec<Value>)> {
97        let mut ctx = RenderCtx::new(self.param_style);
98        self.render_transaction(stmt, &mut ctx)?;
99        Ok(ctx.finish())
100    }
101
102    /// Convenience: render a DML statement to SQL string + params.
103    pub fn render_mutation_stmt(&self, stmt: &MutationStmt) -> RenderResult<(String, Vec<Value>)> {
104        let mut ctx = RenderCtx::new(self.param_style).with_parameterize(true);
105        self.render_mutation(stmt, &mut ctx)?;
106        Ok(ctx.finish())
107    }
108
109    /// Convenience: render a SELECT query to SQL string + params.
110    pub fn render_query_stmt(&self, stmt: &QueryStmt) -> RenderResult<(String, Vec<Value>)> {
111        let mut ctx = RenderCtx::new(self.param_style).with_parameterize(true);
112        self.render_query(stmt, &mut ctx)?;
113        Ok(ctx.finish())
114    }
115}
116
117impl Default for PostgresRenderer {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123// ==========================================================================
124// Renderer trait implementation
125// ==========================================================================
126
127impl Renderer for PostgresRenderer {
128    // ── DDL ──────────────────────────────────────────────────────────────
129
130    fn render_schema_mutation(
131        &self,
132        stmt: &SchemaMutationStmt,
133        ctx: &mut RenderCtx,
134    ) -> RenderResult<()> {
135        match stmt {
136            SchemaMutationStmt::CreateTable {
137                schema,
138                if_not_exists,
139                temporary,
140                unlogged,
141                tablespace,
142                partition_by,
143                inherits,
144                using_method,
145                with_options,
146                on_commit,
147                table_options: _, // PG uses WITH options instead
148                without_rowid: _, // SQLite-specific — Ignore
149                strict: _,        // SQLite-specific — Ignore
150            } => self.pg_create_table(
151                schema,
152                *if_not_exists,
153                *temporary,
154                *unlogged,
155                &PgCreateTableOpts {
156                    tablespace: tablespace.as_deref(),
157                    partition_by: partition_by.as_ref(),
158                    inherits: inherits.as_deref(),
159                    using_method: using_method.as_deref(),
160                    with_options: with_options.as_deref(),
161                    on_commit: on_commit.as_ref(),
162                },
163                ctx,
164            ),
165
166            SchemaMutationStmt::DropTable {
167                schema_ref,
168                if_exists,
169                cascade,
170            } => {
171                ctx.keyword("DROP TABLE");
172                if *if_exists {
173                    ctx.keyword("IF EXISTS");
174                }
175                self.pg_schema_ref(schema_ref, ctx);
176                if *cascade {
177                    ctx.keyword("CASCADE");
178                }
179                Ok(())
180            }
181
182            SchemaMutationStmt::RenameTable {
183                schema_ref,
184                new_name,
185            } => {
186                ctx.keyword("ALTER TABLE");
187                self.pg_schema_ref(schema_ref, ctx);
188                ctx.keyword("RENAME TO").ident(new_name);
189                Ok(())
190            }
191
192            SchemaMutationStmt::TruncateTable {
193                schema_ref,
194                restart_identity,
195                cascade,
196            } => {
197                ctx.keyword("TRUNCATE TABLE");
198                self.pg_schema_ref(schema_ref, ctx);
199                if *restart_identity {
200                    ctx.keyword("RESTART IDENTITY");
201                }
202                if *cascade {
203                    ctx.keyword("CASCADE");
204                }
205                Ok(())
206            }
207
208            SchemaMutationStmt::AddColumn {
209                schema_ref,
210                column,
211                if_not_exists,
212                position: _, // PostgreSQL doesn't support FIRST/AFTER
213            } => {
214                ctx.keyword("ALTER TABLE");
215                self.pg_schema_ref(schema_ref, ctx);
216                ctx.keyword("ADD COLUMN");
217                if *if_not_exists {
218                    ctx.keyword("IF NOT EXISTS");
219                }
220                self.render_column_def(column, ctx)
221            }
222
223            SchemaMutationStmt::DropColumn {
224                schema_ref,
225                name,
226                if_exists,
227                cascade,
228            } => {
229                ctx.keyword("ALTER TABLE");
230                self.pg_schema_ref(schema_ref, ctx);
231                ctx.keyword("DROP COLUMN");
232                if *if_exists {
233                    ctx.keyword("IF EXISTS");
234                }
235                ctx.ident(name);
236                if *cascade {
237                    ctx.keyword("CASCADE");
238                }
239                Ok(())
240            }
241
242            SchemaMutationStmt::RenameColumn {
243                schema_ref,
244                old_name,
245                new_name,
246            } => {
247                ctx.keyword("ALTER TABLE");
248                self.pg_schema_ref(schema_ref, ctx);
249                ctx.keyword("RENAME COLUMN")
250                    .ident(old_name)
251                    .keyword("TO")
252                    .ident(new_name);
253                Ok(())
254            }
255
256            SchemaMutationStmt::AlterColumnType {
257                schema_ref,
258                column_name,
259                new_type,
260                using_expr,
261            } => {
262                ctx.keyword("ALTER TABLE");
263                self.pg_schema_ref(schema_ref, ctx);
264                ctx.keyword("ALTER COLUMN")
265                    .ident(column_name)
266                    .keyword("SET DATA TYPE");
267                self.render_column_type(new_type, ctx)?;
268                if let Some(expr) = using_expr {
269                    ctx.keyword("USING");
270                    self.render_expr(expr, ctx)?;
271                }
272                Ok(())
273            }
274
275            SchemaMutationStmt::AlterColumnDefault {
276                schema_ref,
277                column_name,
278                default,
279            } => {
280                ctx.keyword("ALTER TABLE");
281                self.pg_schema_ref(schema_ref, ctx);
282                ctx.keyword("ALTER COLUMN").ident(column_name);
283                match default {
284                    Some(expr) => {
285                        ctx.keyword("SET DEFAULT");
286                        self.render_expr(expr, ctx)?;
287                    }
288                    None => {
289                        ctx.keyword("DROP DEFAULT");
290                    }
291                }
292                Ok(())
293            }
294
295            SchemaMutationStmt::AlterColumnNullability {
296                schema_ref,
297                column_name,
298                not_null,
299            } => {
300                ctx.keyword("ALTER TABLE");
301                self.pg_schema_ref(schema_ref, ctx);
302                ctx.keyword("ALTER COLUMN").ident(column_name);
303                if *not_null {
304                    ctx.keyword("SET NOT NULL");
305                } else {
306                    ctx.keyword("DROP NOT NULL");
307                }
308                Ok(())
309            }
310
311            SchemaMutationStmt::AddConstraint {
312                schema_ref,
313                constraint,
314                not_valid,
315            } => {
316                ctx.keyword("ALTER TABLE");
317                self.pg_schema_ref(schema_ref, ctx);
318                ctx.keyword("ADD");
319                self.render_constraint(constraint, ctx)?;
320                if *not_valid {
321                    ctx.keyword("NOT VALID");
322                }
323                Ok(())
324            }
325
326            SchemaMutationStmt::DropConstraint {
327                schema_ref,
328                constraint_name,
329                if_exists,
330                cascade,
331            } => {
332                ctx.keyword("ALTER TABLE");
333                self.pg_schema_ref(schema_ref, ctx);
334                ctx.keyword("DROP CONSTRAINT");
335                if *if_exists {
336                    ctx.keyword("IF EXISTS");
337                }
338                ctx.ident(constraint_name);
339                if *cascade {
340                    ctx.keyword("CASCADE");
341                }
342                Ok(())
343            }
344
345            SchemaMutationStmt::RenameConstraint {
346                schema_ref,
347                old_name,
348                new_name,
349            } => {
350                ctx.keyword("ALTER TABLE");
351                self.pg_schema_ref(schema_ref, ctx);
352                ctx.keyword("RENAME CONSTRAINT")
353                    .ident(old_name)
354                    .keyword("TO")
355                    .ident(new_name);
356                Ok(())
357            }
358
359            SchemaMutationStmt::ValidateConstraint {
360                schema_ref,
361                constraint_name,
362            } => {
363                ctx.keyword("ALTER TABLE");
364                self.pg_schema_ref(schema_ref, ctx);
365                ctx.keyword("VALIDATE CONSTRAINT").ident(constraint_name);
366                Ok(())
367            }
368
369            SchemaMutationStmt::CreateIndex {
370                schema_ref,
371                index,
372                if_not_exists,
373                concurrently,
374            } => self.pg_create_index(schema_ref, index, *if_not_exists, *concurrently, ctx),
375
376            SchemaMutationStmt::DropIndex {
377                schema_ref: _,
378                index_name,
379                if_exists,
380                concurrently,
381                cascade,
382            } => {
383                ctx.keyword("DROP INDEX");
384                if *concurrently {
385                    ctx.keyword("CONCURRENTLY");
386                }
387                if *if_exists {
388                    ctx.keyword("IF EXISTS");
389                }
390                ctx.ident(index_name);
391                if *cascade {
392                    ctx.keyword("CASCADE");
393                }
394                Ok(())
395            }
396
397            SchemaMutationStmt::CreateExtension {
398                name,
399                if_not_exists,
400                schema,
401                version,
402                cascade,
403            } => {
404                ctx.keyword("CREATE EXTENSION");
405                if *if_not_exists {
406                    ctx.keyword("IF NOT EXISTS");
407                }
408                ctx.ident(name);
409                if let Some(s) = schema {
410                    ctx.keyword("SCHEMA").ident(s);
411                }
412                if let Some(v) = version {
413                    ctx.keyword("VERSION").string_literal(v);
414                }
415                if *cascade {
416                    ctx.keyword("CASCADE");
417                }
418                Ok(())
419            }
420
421            SchemaMutationStmt::DropExtension {
422                name,
423                if_exists,
424                cascade,
425            } => {
426                ctx.keyword("DROP EXTENSION");
427                if *if_exists {
428                    ctx.keyword("IF EXISTS");
429                }
430                ctx.ident(name);
431                if *cascade {
432                    ctx.keyword("CASCADE");
433                }
434                Ok(())
435            }
436
437            SchemaMutationStmt::Custom(_) => Err(RenderError::unsupported(
438                "CustomSchemaMutation",
439                "custom DDL must be handled by a wrapping renderer",
440            )),
441        }
442    }
443
444    fn render_column_def(&self, col: &ColumnDef, ctx: &mut RenderCtx) -> RenderResult<()> {
445        ctx.ident(&col.name);
446        self.render_column_type(&col.field_type, ctx)?;
447
448        if let Some(storage) = &col.storage {
449            ctx.keyword("STORAGE").keyword(storage);
450        }
451
452        if let Some(compression) = &col.compression {
453            ctx.keyword("COMPRESSION").keyword(compression);
454        }
455
456        if let Some(collation) = &col.collation {
457            ctx.keyword("COLLATE").ident(collation);
458        }
459
460        if col.not_null {
461            ctx.keyword("NOT NULL");
462        }
463
464        if let Some(default) = &col.default {
465            ctx.keyword("DEFAULT");
466            self.render_expr(default, ctx)?;
467        }
468
469        if let Some(identity) = &col.identity {
470            self.pg_identity(identity, ctx);
471        }
472
473        if let Some(generated) = &col.generated {
474            ctx.keyword("GENERATED ALWAYS AS").space().paren_open();
475            self.render_expr(&generated.expr, ctx)?;
476            ctx.paren_close().keyword("STORED");
477        }
478
479        Ok(())
480    }
481
482    fn render_column_type(&self, ty: &FieldType, ctx: &mut RenderCtx) -> RenderResult<()> {
483        match ty {
484            FieldType::Scalar(name) => {
485                ctx.keyword(name);
486            }
487            FieldType::Parameterized { name, params } => {
488                ctx.keyword(name).write("(");
489                for (i, p) in params.iter().enumerate() {
490                    if i > 0 {
491                        ctx.comma();
492                    }
493                    ctx.write(p);
494                }
495                ctx.paren_close();
496            }
497            FieldType::Array(inner) => {
498                self.render_column_type(inner, ctx)?;
499                ctx.write("[]");
500            }
501            FieldType::Vector(dim) => {
502                ctx.keyword("VECTOR")
503                    .write("(")
504                    .write(&dim.to_string())
505                    .paren_close();
506            }
507            FieldType::Custom(_) => {
508                return Err(RenderError::unsupported(
509                    "CustomFieldType",
510                    "custom field type must be handled by a wrapping renderer",
511                ));
512            }
513        }
514        Ok(())
515    }
516
517    fn render_constraint(&self, c: &ConstraintDef, ctx: &mut RenderCtx) -> RenderResult<()> {
518        match c {
519            ConstraintDef::PrimaryKey {
520                name,
521                columns,
522                include,
523                autoincrement: _, // SQLite-specific — Ignore
524            } => {
525                if let Some(n) = name {
526                    ctx.keyword("CONSTRAINT").ident(n);
527                }
528                ctx.keyword("PRIMARY KEY").paren_open();
529                self.pg_comma_idents(columns, ctx);
530                ctx.paren_close();
531                if let Some(inc) = include {
532                    ctx.keyword("INCLUDE").paren_open();
533                    self.pg_comma_idents(inc, ctx);
534                    ctx.paren_close();
535                }
536            }
537
538            ConstraintDef::ForeignKey {
539                name,
540                columns,
541                ref_table,
542                ref_columns,
543                on_delete,
544                on_update,
545                deferrable,
546                match_type,
547            } => {
548                if let Some(n) = name {
549                    ctx.keyword("CONSTRAINT").ident(n);
550                }
551                ctx.keyword("FOREIGN KEY").paren_open();
552                self.pg_comma_idents(columns, ctx);
553                ctx.paren_close().keyword("REFERENCES");
554                self.pg_schema_ref(ref_table, ctx);
555                ctx.paren_open();
556                self.pg_comma_idents(ref_columns, ctx);
557                ctx.paren_close();
558                if let Some(mt) = match_type {
559                    ctx.keyword(match mt {
560                        MatchType::Full => "MATCH FULL",
561                        MatchType::Partial => "MATCH PARTIAL",
562                        MatchType::Simple => "MATCH SIMPLE",
563                    });
564                }
565                if let Some(action) = on_delete {
566                    ctx.keyword("ON DELETE");
567                    self.pg_referential_action(action, ctx);
568                }
569                if let Some(action) = on_update {
570                    ctx.keyword("ON UPDATE");
571                    self.pg_referential_action(action, ctx);
572                }
573                if let Some(def) = deferrable {
574                    self.pg_deferrable(def, ctx);
575                }
576            }
577
578            ConstraintDef::Unique {
579                name,
580                columns,
581                include,
582                nulls_distinct,
583                condition: _, // Partial unique → rendered as separate CREATE INDEX
584            } => {
585                if let Some(n) = name {
586                    ctx.keyword("CONSTRAINT").ident(n);
587                }
588                ctx.keyword("UNIQUE");
589                if let Some(false) = nulls_distinct {
590                    ctx.keyword("NULLS NOT DISTINCT");
591                }
592                ctx.paren_open();
593                self.pg_comma_idents(columns, ctx);
594                ctx.paren_close();
595                if let Some(inc) = include {
596                    ctx.keyword("INCLUDE").paren_open();
597                    self.pg_comma_idents(inc, ctx);
598                    ctx.paren_close();
599                }
600            }
601
602            ConstraintDef::Check {
603                name,
604                condition,
605                no_inherit,
606                enforced: _, // PostgreSQL always enforces CHECK
607            } => {
608                if let Some(n) = name {
609                    ctx.keyword("CONSTRAINT").ident(n);
610                }
611                ctx.keyword("CHECK").paren_open();
612                self.render_condition(condition, ctx)?;
613                ctx.paren_close();
614                if *no_inherit {
615                    ctx.keyword("NO INHERIT");
616                }
617            }
618
619            ConstraintDef::Exclusion {
620                name,
621                elements,
622                index_method,
623                condition,
624            } => {
625                if let Some(n) = name {
626                    ctx.keyword("CONSTRAINT").ident(n);
627                }
628                ctx.keyword("EXCLUDE USING")
629                    .keyword(index_method)
630                    .paren_open();
631                for (i, elem) in elements.iter().enumerate() {
632                    if i > 0 {
633                        ctx.comma();
634                    }
635                    ctx.ident(&elem.column)
636                        .keyword("WITH")
637                        .keyword(&elem.operator);
638                }
639                ctx.paren_close();
640                if let Some(cond) = condition {
641                    ctx.keyword("WHERE").paren_open();
642                    self.render_condition(cond, ctx)?;
643                    ctx.paren_close();
644                }
645            }
646
647            ConstraintDef::Custom(_) => {
648                return Err(RenderError::unsupported(
649                    "CustomConstraint",
650                    "custom constraint must be handled by a wrapping renderer",
651                ));
652            }
653        }
654        Ok(())
655    }
656
657    fn render_index_def(&self, idx: &IndexDef, ctx: &mut RenderCtx) -> RenderResult<()> {
658        // Used for inline index rendering (inside CREATE TABLE context).
659        // Full CREATE INDEX is handled in pg_create_index.
660        ctx.ident(&idx.name);
661        if let Some(index_type) = &idx.index_type {
662            ctx.keyword("USING").keyword(index_type);
663        }
664        ctx.paren_open();
665        self.pg_index_columns(&idx.columns, ctx)?;
666        ctx.paren_close();
667        Ok(())
668    }
669
670    // ── Expressions (basic, needed for DDL) ──────────────────────────────
671
672    fn render_expr(&self, expr: &Expr, ctx: &mut RenderCtx) -> RenderResult<()> {
673        match expr {
674            Expr::Value(val) => self.pg_value(val, ctx),
675
676            Expr::Field(field_ref) => {
677                self.pg_field_ref(field_ref, ctx);
678                Ok(())
679            }
680
681            Expr::Binary { left, op, right } => {
682                self.render_expr(left, ctx)?;
683                // When using %s placeholders (psycopg), literal '%' must be
684                // escaped as '%%' so the driver doesn't treat it as a placeholder.
685                let mod_op = if self.param_style == ParamStyle::Percent {
686                    "%%"
687                } else {
688                    "%"
689                };
690                ctx.keyword(match op {
691                    BinaryOp::Add => "+",
692                    BinaryOp::Sub => "-",
693                    BinaryOp::Mul => "*",
694                    BinaryOp::Div => "/",
695                    BinaryOp::Mod => mod_op,
696                    BinaryOp::BitwiseAnd => "&",
697                    BinaryOp::BitwiseOr => "|",
698                    BinaryOp::ShiftLeft => "<<",
699                    BinaryOp::ShiftRight => ">>",
700                    BinaryOp::Concat => "||",
701                });
702                self.render_expr(right, ctx)
703            }
704
705            Expr::Unary { op, expr: inner } => {
706                match op {
707                    UnaryOp::Neg => ctx.write("-"),
708                    UnaryOp::Not => ctx.keyword("NOT"),
709                    UnaryOp::BitwiseNot => ctx.write("~"),
710                };
711                self.render_expr(inner, ctx)
712            }
713
714            Expr::Func { name, args } => {
715                ctx.keyword(name).write("(");
716                for (i, arg) in args.iter().enumerate() {
717                    if i > 0 {
718                        ctx.comma();
719                    }
720                    self.render_expr(arg, ctx)?;
721                }
722                ctx.paren_close();
723                Ok(())
724            }
725
726            Expr::Aggregate(agg) => self.render_aggregate(agg, ctx),
727
728            Expr::Cast {
729                expr: inner,
730                to_type,
731            } => {
732                self.render_expr(inner, ctx)?;
733                ctx.operator("::");
734                ctx.write(to_type);
735                Ok(())
736            }
737
738            Expr::Case(case) => self.render_case(case, ctx),
739
740            Expr::Window(win) => self.render_window(win, ctx),
741
742            Expr::Exists(query) => {
743                ctx.keyword("EXISTS").paren_open();
744                self.render_query(query, ctx)?;
745                ctx.paren_close();
746                Ok(())
747            }
748
749            Expr::SubQuery(query) => {
750                ctx.paren_open();
751                self.render_query(query, ctx)?;
752                ctx.paren_close();
753                Ok(())
754            }
755
756            Expr::ArraySubQuery(query) => {
757                ctx.keyword("ARRAY").paren_open();
758                self.render_query(query, ctx)?;
759                ctx.paren_close();
760                Ok(())
761            }
762
763            Expr::Collate { expr, collation } => {
764                self.render_expr(expr, ctx)?;
765                ctx.keyword("COLLATE").ident(collation);
766                Ok(())
767            }
768
769            Expr::Raw { sql, params } => {
770                ctx.keyword(sql);
771                // Raw params are already embedded in the SQL string
772                let _ = params;
773                Ok(())
774            }
775
776            Expr::Custom(_) => Err(RenderError::unsupported(
777                "CustomExpr",
778                "custom expression must be handled by a wrapping renderer",
779            )),
780        }
781    }
782
783    fn render_aggregate(&self, agg: &AggregationDef, ctx: &mut RenderCtx) -> RenderResult<()> {
784        ctx.keyword(&agg.name).write("(");
785        if agg.distinct {
786            ctx.keyword("DISTINCT");
787        }
788        if let Some(expr) = &agg.expression {
789            self.render_expr(expr, ctx)?;
790        } else {
791            ctx.write("*");
792        }
793        if let Some(args) = &agg.args {
794            for arg in args {
795                ctx.comma();
796                self.render_expr(arg, ctx)?;
797            }
798        }
799        if let Some(order_by) = &agg.order_by {
800            ctx.keyword("ORDER BY");
801            self.pg_order_by_list(order_by, ctx)?;
802        }
803        ctx.paren_close();
804        if let Some(filter) = &agg.filter {
805            ctx.keyword("FILTER").paren_open().keyword("WHERE");
806            self.render_condition(filter, ctx)?;
807            ctx.paren_close();
808        }
809        Ok(())
810    }
811
812    fn render_window(&self, win: &WindowDef, ctx: &mut RenderCtx) -> RenderResult<()> {
813        self.render_expr(&win.expression, ctx)?;
814        ctx.keyword("OVER").paren_open();
815        if let Some(partition_by) = &win.partition_by {
816            ctx.keyword("PARTITION BY");
817            for (i, expr) in partition_by.iter().enumerate() {
818                if i > 0 {
819                    ctx.comma();
820                }
821                self.render_expr(expr, ctx)?;
822            }
823        }
824        if let Some(order_by) = &win.order_by {
825            ctx.keyword("ORDER BY");
826            self.pg_order_by_list(order_by, ctx)?;
827        }
828        if let Some(frame) = &win.frame {
829            self.pg_window_frame(frame, ctx);
830        }
831        ctx.paren_close();
832        Ok(())
833    }
834
835    fn render_case(&self, case: &CaseDef, ctx: &mut RenderCtx) -> RenderResult<()> {
836        ctx.keyword("CASE");
837        for clause in &case.cases {
838            ctx.keyword("WHEN");
839            self.render_condition(&clause.condition, ctx)?;
840            ctx.keyword("THEN");
841            self.render_expr(&clause.result, ctx)?;
842        }
843        if let Some(default) = &case.default {
844            ctx.keyword("ELSE");
845            self.render_expr(default, ctx)?;
846        }
847        ctx.keyword("END");
848        Ok(())
849    }
850
851    // ── Conditions ───────────────────────────────────────────────────────
852
853    fn render_condition(&self, cond: &Conditions, ctx: &mut RenderCtx) -> RenderResult<()> {
854        if cond.negated {
855            ctx.keyword("NOT").paren_open();
856        }
857        let connector = match cond.connector {
858            Connector::And => " AND ",
859            Connector::Or => " OR ",
860        };
861        for (i, child) in cond.children.iter().enumerate() {
862            if i > 0 {
863                ctx.write(connector);
864            }
865            match child {
866                ConditionNode::Comparison(comp) => {
867                    if comp.negate {
868                        ctx.keyword("NOT").paren_open();
869                    }
870                    self.render_compare_op(&comp.op, &comp.left, &comp.right, ctx)?;
871                    if comp.negate {
872                        ctx.paren_close();
873                    }
874                }
875                ConditionNode::Group(group) => {
876                    ctx.paren_open();
877                    self.render_condition(group, ctx)?;
878                    ctx.paren_close();
879                }
880                ConditionNode::Exists(query) => {
881                    ctx.keyword("EXISTS").paren_open();
882                    self.render_query(query, ctx)?;
883                    ctx.paren_close();
884                }
885                ConditionNode::Custom(_) => {
886                    return Err(RenderError::unsupported(
887                        "CustomCondition",
888                        "custom condition must be handled by a wrapping renderer",
889                    ));
890                }
891            }
892        }
893        if cond.negated {
894            ctx.paren_close();
895        }
896        Ok(())
897    }
898
899    fn render_compare_op(
900        &self,
901        op: &CompareOp,
902        left: &Expr,
903        right: &Expr,
904        ctx: &mut RenderCtx,
905    ) -> RenderResult<()> {
906        self.render_expr(left, ctx)?;
907        match op {
908            CompareOp::Eq => ctx.write(" = "),
909            CompareOp::Neq => ctx.write(" <> "),
910            CompareOp::Gt => ctx.write(" > "),
911            CompareOp::Gte => ctx.write(" >= "),
912            CompareOp::Lt => ctx.write(" < "),
913            CompareOp::Lte => ctx.write(" <= "),
914            CompareOp::Like => ctx.keyword("LIKE"),
915            CompareOp::ILike => ctx.keyword("ILIKE"),
916            CompareOp::Contains | CompareOp::StartsWith | CompareOp::EndsWith => {
917                ctx.keyword("LIKE");
918                render_like_pattern(op, right, ctx)?;
919                return Ok(());
920            }
921            CompareOp::IContains | CompareOp::IStartsWith | CompareOp::IEndsWith => {
922                ctx.keyword("ILIKE");
923                render_like_pattern(op, right, ctx)?;
924                return Ok(());
925            }
926            CompareOp::In => ctx.keyword("IN"),
927            CompareOp::Between => {
928                ctx.keyword("BETWEEN");
929                self.render_expr(right, ctx)?;
930                return Ok(());
931            }
932            CompareOp::IsNull => {
933                ctx.keyword("IS NULL");
934                return Ok(());
935            }
936            CompareOp::Similar => ctx.keyword("SIMILAR TO"),
937            CompareOp::Regex => ctx.write(" ~ "),
938            CompareOp::IRegex => ctx.write(" ~* "),
939            CompareOp::JsonbContains => ctx.write(" @> "),
940            CompareOp::JsonbContainedBy => ctx.write(" <@ "),
941            CompareOp::JsonbHasKey => ctx.write(" ? "),
942            CompareOp::JsonbHasAnyKey => ctx.write(" ?| "),
943            CompareOp::JsonbHasAllKeys => ctx.write(" ?& "),
944            CompareOp::FtsMatch => ctx.write(" @@ "),
945            CompareOp::TrigramSimilar => {
946                if self.param_style == ParamStyle::Percent {
947                    ctx.write(" %% ")
948                } else {
949                    ctx.write(" % ")
950                }
951            }
952            CompareOp::TrigramWordSimilar => {
953                if self.param_style == ParamStyle::Percent {
954                    ctx.write(" <%% ")
955                } else {
956                    ctx.write(" <% ")
957                }
958            }
959            CompareOp::TrigramStrictWordSimilar => {
960                if self.param_style == ParamStyle::Percent {
961                    ctx.write(" <<%% ")
962                } else {
963                    ctx.write(" <<% ")
964                }
965            }
966            CompareOp::RangeContains => ctx.write(" @> "),
967            CompareOp::RangeContainedBy => ctx.write(" <@ "),
968            CompareOp::RangeOverlap => ctx.write(" && "),
969            CompareOp::Custom(_) => {
970                return Err(RenderError::unsupported(
971                    "CustomCompareOp",
972                    "custom compare op must be handled by a wrapping renderer",
973                ));
974            }
975        };
976        self.render_expr(right, ctx)
977    }
978
979    // ── Query (stub) ─────────────────────────────────────────────────────
980
981    fn render_query(&self, stmt: &QueryStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
982        // CTEs
983        if let Some(ctes) = &stmt.ctes {
984            self.render_ctes(ctes, ctx)?;
985        }
986
987        // SELECT
988        ctx.keyword("SELECT");
989
990        // DISTINCT / DISTINCT ON
991        if let Some(distinct) = &stmt.distinct {
992            match distinct {
993                DistinctDef::Distinct => {
994                    ctx.keyword("DISTINCT");
995                }
996                DistinctDef::DistinctOn(exprs) => {
997                    ctx.keyword("DISTINCT ON").paren_open();
998                    for (i, expr) in exprs.iter().enumerate() {
999                        if i > 0 {
1000                            ctx.comma();
1001                        }
1002                        self.render_expr(expr, ctx)?;
1003                    }
1004                    ctx.paren_close();
1005                }
1006            }
1007        }
1008
1009        // Columns
1010        self.render_select_columns(&stmt.columns, ctx)?;
1011
1012        // FROM
1013        if let Some(from) = &stmt.from {
1014            ctx.keyword("FROM");
1015            for (i, item) in from.iter().enumerate() {
1016                if i > 0 {
1017                    ctx.comma();
1018                }
1019                self.pg_render_from_item(item, ctx)?;
1020            }
1021        }
1022
1023        // JOINs
1024        if let Some(joins) = &stmt.joins {
1025            self.render_joins(joins, ctx)?;
1026        }
1027
1028        // WHERE
1029        if let Some(cond) = &stmt.where_clause {
1030            self.render_where(cond, ctx)?;
1031        }
1032
1033        // GROUP BY
1034        if let Some(group_by) = &stmt.group_by {
1035            self.pg_render_group_by(group_by, ctx)?;
1036        }
1037
1038        // HAVING
1039        if let Some(having) = &stmt.having {
1040            ctx.keyword("HAVING");
1041            self.render_condition(having, ctx)?;
1042        }
1043
1044        // WINDOW
1045        if let Some(windows) = &stmt.window {
1046            self.pg_render_window_clause(windows, ctx)?;
1047        }
1048
1049        // ORDER BY
1050        if let Some(order_by) = &stmt.order_by {
1051            self.render_order_by(order_by, ctx)?;
1052        }
1053
1054        // LIMIT / OFFSET
1055        if let Some(limit) = &stmt.limit {
1056            self.render_limit(limit, ctx)?;
1057        }
1058
1059        // FOR UPDATE / SHARE
1060        if let Some(locks) = &stmt.lock {
1061            for lock in locks {
1062                self.render_lock(lock, ctx)?;
1063            }
1064        }
1065
1066        Ok(())
1067    }
1068
1069    fn render_select_columns(
1070        &self,
1071        cols: &[SelectColumn],
1072        ctx: &mut RenderCtx,
1073    ) -> RenderResult<()> {
1074        for (i, col) in cols.iter().enumerate() {
1075            if i > 0 {
1076                ctx.comma();
1077            }
1078            match col {
1079                SelectColumn::Star(None) => {
1080                    ctx.keyword("*");
1081                }
1082                SelectColumn::Star(Some(table)) => {
1083                    ctx.ident(table).operator(".").keyword("*");
1084                }
1085                SelectColumn::Expr { expr, alias } => {
1086                    self.render_expr(expr, ctx)?;
1087                    if let Some(a) = alias {
1088                        ctx.keyword("AS").ident(a);
1089                    }
1090                }
1091                SelectColumn::Field { field, alias } => {
1092                    self.pg_field_ref(field, ctx);
1093                    if let Some(a) = alias {
1094                        ctx.keyword("AS").ident(a);
1095                    }
1096                }
1097            }
1098        }
1099        Ok(())
1100    }
1101    fn render_from(&self, source: &TableSource, ctx: &mut RenderCtx) -> RenderResult<()> {
1102        match source {
1103            TableSource::Table(schema_ref) => {
1104                self.pg_schema_ref(schema_ref, ctx);
1105                if let Some(alias) = &schema_ref.alias {
1106                    ctx.keyword("AS").ident(alias);
1107                }
1108            }
1109            TableSource::SubQuery(sq) => {
1110                ctx.paren_open();
1111                self.render_query(&sq.query, ctx)?;
1112                ctx.paren_close().keyword("AS").ident(&sq.alias);
1113            }
1114            TableSource::SetOp(set_op) => {
1115                ctx.paren_open();
1116                self.pg_render_set_op(set_op, ctx)?;
1117                ctx.paren_close();
1118            }
1119            TableSource::Lateral(inner) => {
1120                ctx.keyword("LATERAL");
1121                self.render_from(&inner.source, ctx)?;
1122            }
1123            TableSource::Function { name, args, alias } => {
1124                ctx.keyword(name).write("(");
1125                for (i, arg) in args.iter().enumerate() {
1126                    if i > 0 {
1127                        ctx.comma();
1128                    }
1129                    self.render_expr(arg, ctx)?;
1130                }
1131                ctx.paren_close();
1132                if let Some(a) = alias {
1133                    ctx.keyword("AS").ident(a);
1134                }
1135            }
1136            TableSource::Values {
1137                rows,
1138                alias,
1139                column_aliases,
1140            } => {
1141                ctx.paren_open().keyword("VALUES");
1142                for (i, row) in rows.iter().enumerate() {
1143                    if i > 0 {
1144                        ctx.comma();
1145                    }
1146                    ctx.paren_open();
1147                    for (j, val) in row.iter().enumerate() {
1148                        if j > 0 {
1149                            ctx.comma();
1150                        }
1151                        self.render_expr(val, ctx)?;
1152                    }
1153                    ctx.paren_close();
1154                }
1155                ctx.paren_close().keyword("AS").ident(alias);
1156                if let Some(cols) = column_aliases {
1157                    ctx.paren_open();
1158                    for (i, c) in cols.iter().enumerate() {
1159                        if i > 0 {
1160                            ctx.comma();
1161                        }
1162                        ctx.ident(c);
1163                    }
1164                    ctx.paren_close();
1165                }
1166            }
1167            TableSource::Custom(_) => {
1168                return Err(RenderError::unsupported(
1169                    "CustomTableSource",
1170                    "custom table source must be handled by a wrapping renderer",
1171                ));
1172            }
1173        }
1174        Ok(())
1175    }
1176    fn render_joins(&self, joins: &[JoinDef], ctx: &mut RenderCtx) -> RenderResult<()> {
1177        for join in joins {
1178            if join.natural {
1179                ctx.keyword("NATURAL");
1180            }
1181            ctx.keyword(match join.join_type {
1182                JoinType::Inner => "INNER JOIN",
1183                JoinType::Left => "LEFT JOIN",
1184                JoinType::Right => "RIGHT JOIN",
1185                JoinType::Full => "FULL JOIN",
1186                JoinType::Cross => "CROSS JOIN",
1187                JoinType::CrossApply => "CROSS JOIN LATERAL",
1188                JoinType::OuterApply => "LEFT JOIN LATERAL",
1189            });
1190            self.pg_render_from_item(&join.source, ctx)?;
1191            if let Some(condition) = &join.condition {
1192                match condition {
1193                    JoinCondition::On(cond) => {
1194                        ctx.keyword("ON");
1195                        self.render_condition(cond, ctx)?;
1196                    }
1197                    JoinCondition::Using(cols) => {
1198                        ctx.keyword("USING").paren_open();
1199                        self.pg_comma_idents(cols, ctx);
1200                        ctx.paren_close();
1201                    }
1202                }
1203            }
1204            // CrossApply/OuterApply rendered as LATERAL need ON TRUE if no condition
1205            if matches!(join.join_type, JoinType::OuterApply) && join.condition.is_none() {
1206                ctx.keyword("ON TRUE");
1207            }
1208        }
1209        Ok(())
1210    }
1211    fn render_where(&self, cond: &Conditions, ctx: &mut RenderCtx) -> RenderResult<()> {
1212        ctx.keyword("WHERE");
1213        self.render_condition(cond, ctx)
1214    }
1215    fn render_order_by(&self, order: &[OrderByDef], ctx: &mut RenderCtx) -> RenderResult<()> {
1216        ctx.keyword("ORDER BY");
1217        self.pg_order_by_list(order, ctx)
1218    }
1219    fn render_limit(&self, limit: &LimitDef, ctx: &mut RenderCtx) -> RenderResult<()> {
1220        match &limit.kind {
1221            LimitKind::Limit(n) => {
1222                ctx.keyword("LIMIT").space().write(&n.to_string());
1223            }
1224            LimitKind::FetchFirst {
1225                count,
1226                with_ties,
1227                percent,
1228            } => {
1229                if let Some(offset) = limit.offset {
1230                    ctx.keyword("OFFSET")
1231                        .space()
1232                        .write(&offset.to_string())
1233                        .keyword("ROWS");
1234                }
1235                ctx.keyword("FETCH FIRST");
1236                if *percent {
1237                    ctx.space().write(&count.to_string()).keyword("PERCENT");
1238                } else {
1239                    ctx.space().write(&count.to_string());
1240                }
1241                if *with_ties {
1242                    ctx.keyword("ROWS WITH TIES");
1243                } else {
1244                    ctx.keyword("ROWS ONLY");
1245                }
1246                return Ok(());
1247            }
1248            LimitKind::Top { count, .. } => {
1249                // PG doesn't support TOP, convert to LIMIT
1250                ctx.keyword("LIMIT").space().write(&count.to_string());
1251            }
1252        }
1253        if let Some(offset) = limit.offset {
1254            ctx.keyword("OFFSET").space().write(&offset.to_string());
1255        }
1256        Ok(())
1257    }
1258    fn render_ctes(&self, ctes: &[CteDef], ctx: &mut RenderCtx) -> RenderResult<()> {
1259        // Check if any CTE is recursive — PG uses WITH RECURSIVE once for all.
1260        let any_recursive = ctes.iter().any(|c| c.recursive);
1261        ctx.keyword("WITH");
1262        if any_recursive {
1263            ctx.keyword("RECURSIVE");
1264        }
1265        for (i, cte) in ctes.iter().enumerate() {
1266            if i > 0 {
1267                ctx.comma();
1268            }
1269            ctx.ident(&cte.name);
1270            if let Some(col_names) = &cte.column_names {
1271                ctx.paren_open();
1272                self.pg_comma_idents(col_names, ctx);
1273                ctx.paren_close();
1274            }
1275            ctx.keyword("AS");
1276            if let Some(mat) = &cte.materialized {
1277                match mat {
1278                    CteMaterialized::Materialized => {
1279                        ctx.keyword("MATERIALIZED");
1280                    }
1281                    CteMaterialized::NotMaterialized => {
1282                        ctx.keyword("NOT MATERIALIZED");
1283                    }
1284                }
1285            }
1286            ctx.paren_open();
1287            self.render_query(&cte.query, ctx)?;
1288            ctx.paren_close();
1289        }
1290        Ok(())
1291    }
1292    fn render_lock(&self, lock: &SelectLockDef, ctx: &mut RenderCtx) -> RenderResult<()> {
1293        ctx.keyword("FOR");
1294        ctx.keyword(match lock.strength {
1295            LockStrength::Update => "UPDATE",
1296            LockStrength::NoKeyUpdate => "NO KEY UPDATE",
1297            LockStrength::Share => "SHARE",
1298            LockStrength::KeyShare => "KEY SHARE",
1299        });
1300        if let Some(of) = &lock.of {
1301            ctx.keyword("OF");
1302            for (i, table) in of.iter().enumerate() {
1303                if i > 0 {
1304                    ctx.comma();
1305                }
1306                self.pg_schema_ref(table, ctx);
1307            }
1308        }
1309        if lock.nowait {
1310            ctx.keyword("NOWAIT");
1311        }
1312        if lock.skip_locked {
1313            ctx.keyword("SKIP LOCKED");
1314        }
1315        Ok(())
1316    }
1317
1318    // ── DML ──────────────────────────────────────────────────────────────
1319
1320    fn render_mutation(&self, stmt: &MutationStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1321        match stmt {
1322            MutationStmt::Insert(s) => self.render_insert(s, ctx),
1323            MutationStmt::Update(s) => self.render_update(s, ctx),
1324            MutationStmt::Delete(s) => self.render_delete(s, ctx),
1325            MutationStmt::Custom(_) => Err(RenderError::unsupported(
1326                "CustomMutation",
1327                "custom DML must be handled by a wrapping renderer",
1328            )),
1329        }
1330    }
1331
1332    fn render_insert(&self, stmt: &InsertStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1333        // CTEs
1334        if let Some(ctes) = &stmt.ctes {
1335            self.pg_render_ctes(ctes, ctx)?;
1336        }
1337
1338        ctx.keyword("INSERT INTO");
1339        self.pg_schema_ref(&stmt.table, ctx);
1340
1341        // Column list
1342        if let Some(cols) = &stmt.columns {
1343            ctx.paren_open();
1344            self.pg_comma_idents(cols, ctx);
1345            ctx.paren_close();
1346        }
1347
1348        // OVERRIDING
1349        if let Some(overriding) = &stmt.overriding {
1350            ctx.keyword(match overriding {
1351                OverridingKind::System => "OVERRIDING SYSTEM VALUE",
1352                OverridingKind::User => "OVERRIDING USER VALUE",
1353            });
1354        }
1355
1356        // Source
1357        match &stmt.source {
1358            InsertSource::Values(rows) => {
1359                ctx.keyword("VALUES");
1360                for (i, row) in rows.iter().enumerate() {
1361                    if i > 0 {
1362                        ctx.comma();
1363                    }
1364                    ctx.paren_open();
1365                    for (j, expr) in row.iter().enumerate() {
1366                        if j > 0 {
1367                            ctx.comma();
1368                        }
1369                        self.render_expr(expr, ctx)?;
1370                    }
1371                    ctx.paren_close();
1372                }
1373            }
1374            InsertSource::Select(query) => {
1375                self.render_query(query, ctx)?;
1376            }
1377            InsertSource::DefaultValues => {
1378                ctx.keyword("DEFAULT VALUES");
1379            }
1380        }
1381
1382        // ON CONFLICT
1383        if let Some(conflicts) = &stmt.on_conflict {
1384            for oc in conflicts {
1385                self.render_on_conflict(oc, ctx)?;
1386            }
1387        }
1388
1389        // RETURNING
1390        if let Some(returning) = &stmt.returning {
1391            self.render_returning(returning, ctx)?;
1392        }
1393
1394        Ok(())
1395    }
1396
1397    fn render_update(&self, stmt: &UpdateStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1398        // CTEs
1399        if let Some(ctes) = &stmt.ctes {
1400            self.pg_render_ctes(ctes, ctx)?;
1401        }
1402
1403        ctx.keyword("UPDATE");
1404
1405        // ONLY
1406        if stmt.only {
1407            ctx.keyword("ONLY");
1408        }
1409
1410        self.pg_schema_ref(&stmt.table, ctx);
1411
1412        // Alias
1413        if let Some(alias) = &stmt.table.alias {
1414            ctx.keyword("AS").ident(alias);
1415        }
1416
1417        // SET
1418        ctx.keyword("SET");
1419        for (i, (col, expr)) in stmt.assignments.iter().enumerate() {
1420            if i > 0 {
1421                ctx.comma();
1422            }
1423            ctx.ident(col).write(" = ");
1424            self.render_expr(expr, ctx)?;
1425        }
1426
1427        // FROM
1428        if let Some(from) = &stmt.from {
1429            ctx.keyword("FROM");
1430            for (i, source) in from.iter().enumerate() {
1431                if i > 0 {
1432                    ctx.comma();
1433                }
1434                self.render_from(source, ctx)?;
1435            }
1436        }
1437
1438        // WHERE
1439        if let Some(cond) = &stmt.where_clause {
1440            ctx.keyword("WHERE");
1441            self.render_condition(cond, ctx)?;
1442        }
1443
1444        // RETURNING
1445        if let Some(returning) = &stmt.returning {
1446            self.render_returning(returning, ctx)?;
1447        }
1448
1449        Ok(())
1450    }
1451
1452    fn render_delete(&self, stmt: &DeleteStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1453        // CTEs
1454        if let Some(ctes) = &stmt.ctes {
1455            self.pg_render_ctes(ctes, ctx)?;
1456        }
1457
1458        ctx.keyword("DELETE FROM");
1459
1460        // ONLY
1461        if stmt.only {
1462            ctx.keyword("ONLY");
1463        }
1464
1465        self.pg_schema_ref(&stmt.table, ctx);
1466
1467        // Alias
1468        if let Some(alias) = &stmt.table.alias {
1469            ctx.keyword("AS").ident(alias);
1470        }
1471
1472        // USING
1473        if let Some(using) = &stmt.using {
1474            ctx.keyword("USING");
1475            for (i, source) in using.iter().enumerate() {
1476                if i > 0 {
1477                    ctx.comma();
1478                }
1479                self.render_from(source, ctx)?;
1480            }
1481        }
1482
1483        // WHERE
1484        if let Some(cond) = &stmt.where_clause {
1485            ctx.keyword("WHERE");
1486            self.render_condition(cond, ctx)?;
1487        }
1488
1489        // RETURNING
1490        if let Some(returning) = &stmt.returning {
1491            self.render_returning(returning, ctx)?;
1492        }
1493
1494        Ok(())
1495    }
1496
1497    fn render_on_conflict(&self, oc: &OnConflictDef, ctx: &mut RenderCtx) -> RenderResult<()> {
1498        ctx.keyword("ON CONFLICT");
1499
1500        // Target
1501        if let Some(target) = &oc.target {
1502            match target {
1503                ConflictTarget::Columns {
1504                    columns,
1505                    where_clause,
1506                } => {
1507                    ctx.paren_open();
1508                    self.pg_comma_idents(columns, ctx);
1509                    ctx.paren_close();
1510                    if let Some(cond) = where_clause {
1511                        ctx.keyword("WHERE");
1512                        self.render_condition(cond, ctx)?;
1513                    }
1514                }
1515                ConflictTarget::Constraint(name) => {
1516                    ctx.keyword("ON CONSTRAINT").ident(name);
1517                }
1518            }
1519        }
1520
1521        // Action
1522        match &oc.action {
1523            ConflictAction::DoNothing => {
1524                ctx.keyword("DO NOTHING");
1525            }
1526            ConflictAction::DoUpdate {
1527                assignments,
1528                where_clause,
1529            } => {
1530                ctx.keyword("DO UPDATE SET");
1531                for (i, (col, expr)) in assignments.iter().enumerate() {
1532                    if i > 0 {
1533                        ctx.comma();
1534                    }
1535                    ctx.ident(col).write(" = ");
1536                    self.render_expr(expr, ctx)?;
1537                }
1538                if let Some(cond) = where_clause {
1539                    ctx.keyword("WHERE");
1540                    self.render_condition(cond, ctx)?;
1541                }
1542            }
1543        }
1544
1545        Ok(())
1546    }
1547
1548    fn render_returning(&self, cols: &[SelectColumn], ctx: &mut RenderCtx) -> RenderResult<()> {
1549        ctx.keyword("RETURNING");
1550        for (i, col) in cols.iter().enumerate() {
1551            if i > 0 {
1552                ctx.comma();
1553            }
1554            match col {
1555                SelectColumn::Star(None) => {
1556                    ctx.keyword("*");
1557                }
1558                SelectColumn::Star(Some(table)) => {
1559                    ctx.ident(table).operator(".").keyword("*");
1560                }
1561                SelectColumn::Expr { expr, alias } => {
1562                    self.render_expr(expr, ctx)?;
1563                    if let Some(a) = alias {
1564                        ctx.keyword("AS").ident(a);
1565                    }
1566                }
1567                SelectColumn::Field { field, alias } => {
1568                    self.pg_field_ref(field, ctx);
1569                    if let Some(a) = alias {
1570                        ctx.keyword("AS").ident(a);
1571                    }
1572                }
1573            }
1574        }
1575        Ok(())
1576    }
1577
1578    // ── TCL ──────────────────────────────────────────────────────────────
1579
1580    fn render_transaction(&self, stmt: &TransactionStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1581        match stmt {
1582            TransactionStmt::Begin(s) => self.pg_begin(s, ctx),
1583            TransactionStmt::Commit(s) => self.pg_commit(s, ctx),
1584            TransactionStmt::Rollback(s) => self.pg_rollback(s, ctx),
1585            TransactionStmt::Savepoint(s) => {
1586                ctx.keyword("SAVEPOINT").ident(&s.name);
1587                Ok(())
1588            }
1589            TransactionStmt::ReleaseSavepoint(s) => {
1590                ctx.keyword("RELEASE").keyword("SAVEPOINT").ident(&s.name);
1591                Ok(())
1592            }
1593            TransactionStmt::SetTransaction(s) => self.pg_set_transaction(s, ctx),
1594            TransactionStmt::LockTable(s) => self.pg_lock_table(s, ctx),
1595            TransactionStmt::PrepareTransaction(s) => {
1596                ctx.keyword("PREPARE")
1597                    .keyword("TRANSACTION")
1598                    .string_literal(&s.transaction_id);
1599                Ok(())
1600            }
1601            TransactionStmt::CommitPrepared(s) => {
1602                ctx.keyword("COMMIT")
1603                    .keyword("PREPARED")
1604                    .string_literal(&s.transaction_id);
1605                Ok(())
1606            }
1607            TransactionStmt::RollbackPrepared(s) => {
1608                ctx.keyword("ROLLBACK")
1609                    .keyword("PREPARED")
1610                    .string_literal(&s.transaction_id);
1611                Ok(())
1612            }
1613            TransactionStmt::Custom(_) => Err(RenderError::unsupported(
1614                "Custom TCL",
1615                "not supported by PostgresRenderer",
1616            )),
1617        }
1618    }
1619}
1620
1621// ==========================================================================
1622// PostgreSQL-specific helpers
1623// ==========================================================================
1624
1625impl PostgresRenderer {
1626    // ── TCL helpers ──────────────────────────────────────────────────────
1627
1628    fn pg_begin(&self, stmt: &BeginStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1629        ctx.keyword("BEGIN");
1630        if let Some(modes) = &stmt.modes {
1631            self.pg_transaction_modes(modes, ctx);
1632        }
1633        Ok(())
1634    }
1635
1636    fn pg_commit(&self, stmt: &CommitStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1637        ctx.keyword("COMMIT");
1638        if stmt.and_chain {
1639            ctx.keyword("AND").keyword("CHAIN");
1640        }
1641        Ok(())
1642    }
1643
1644    fn pg_rollback(&self, stmt: &RollbackStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1645        ctx.keyword("ROLLBACK");
1646        if let Some(sp) = &stmt.to_savepoint {
1647            ctx.keyword("TO").keyword("SAVEPOINT").ident(sp);
1648        }
1649        if stmt.and_chain {
1650            ctx.keyword("AND").keyword("CHAIN");
1651        }
1652        Ok(())
1653    }
1654
1655    fn pg_set_transaction(
1656        &self,
1657        stmt: &SetTransactionStmt,
1658        ctx: &mut RenderCtx,
1659    ) -> RenderResult<()> {
1660        ctx.keyword("SET");
1661        match &stmt.scope {
1662            Some(TransactionScope::Session) => {
1663                ctx.keyword("SESSION")
1664                    .keyword("CHARACTERISTICS")
1665                    .keyword("AS")
1666                    .keyword("TRANSACTION");
1667            }
1668            _ => {
1669                ctx.keyword("TRANSACTION");
1670            }
1671        }
1672        if let Some(snap_id) = &stmt.snapshot_id {
1673            ctx.keyword("SNAPSHOT").string_literal(snap_id);
1674        } else {
1675            self.pg_transaction_modes(&stmt.modes, ctx);
1676        }
1677        Ok(())
1678    }
1679
1680    fn pg_transaction_modes(&self, modes: &[TransactionMode], ctx: &mut RenderCtx) {
1681        for (i, mode) in modes.iter().enumerate() {
1682            if i > 0 {
1683                ctx.comma();
1684            }
1685            match mode {
1686                TransactionMode::IsolationLevel(lvl) => {
1687                    ctx.keyword("ISOLATION").keyword("LEVEL");
1688                    ctx.keyword(match lvl {
1689                        IsolationLevel::ReadUncommitted => "READ UNCOMMITTED",
1690                        IsolationLevel::ReadCommitted => "READ COMMITTED",
1691                        IsolationLevel::RepeatableRead => "REPEATABLE READ",
1692                        IsolationLevel::Serializable => "SERIALIZABLE",
1693                        IsolationLevel::Snapshot => "SERIALIZABLE", // PG doesn't have SNAPSHOT
1694                    });
1695                }
1696                TransactionMode::ReadOnly => {
1697                    ctx.keyword("READ ONLY");
1698                }
1699                TransactionMode::ReadWrite => {
1700                    ctx.keyword("READ WRITE");
1701                }
1702                TransactionMode::Deferrable => {
1703                    ctx.keyword("DEFERRABLE");
1704                }
1705                TransactionMode::NotDeferrable => {
1706                    ctx.keyword("NOT DEFERRABLE");
1707                }
1708                TransactionMode::WithConsistentSnapshot => {} // MySQL only, skip
1709            }
1710        }
1711    }
1712
1713    fn pg_lock_table(&self, stmt: &LockTableStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1714        ctx.keyword("LOCK").keyword("TABLE");
1715        for (i, def) in stmt.tables.iter().enumerate() {
1716            if i > 0 {
1717                ctx.comma();
1718            }
1719            if def.only {
1720                ctx.keyword("ONLY");
1721            }
1722            if let Some(schema) = &def.schema {
1723                ctx.ident(schema).operator(".");
1724            }
1725            ctx.ident(&def.table);
1726        }
1727        // Use mode from first table (PG applies one mode to all).
1728        if let Some(first) = stmt.tables.first() {
1729            ctx.keyword("IN");
1730            ctx.keyword(match first.mode {
1731                LockMode::AccessShare => "ACCESS SHARE",
1732                LockMode::RowShare => "ROW SHARE",
1733                LockMode::RowExclusive => "ROW EXCLUSIVE",
1734                LockMode::ShareUpdateExclusive => "SHARE UPDATE EXCLUSIVE",
1735                LockMode::Share => "SHARE",
1736                LockMode::ShareRowExclusive => "SHARE ROW EXCLUSIVE",
1737                LockMode::Exclusive => "EXCLUSIVE",
1738                LockMode::AccessExclusive => "ACCESS EXCLUSIVE",
1739                _ => "ACCESS EXCLUSIVE", // Non-PG modes default
1740            });
1741            ctx.keyword("MODE");
1742        }
1743        if stmt.nowait {
1744            ctx.keyword("NOWAIT");
1745        }
1746        Ok(())
1747    }
1748
1749    // ── Schema helpers ───────────────────────────────────────────────────
1750
1751    fn pg_schema_ref(&self, schema_ref: &qcraft_core::ast::common::SchemaRef, ctx: &mut RenderCtx) {
1752        if let Some(ns) = &schema_ref.namespace {
1753            ctx.ident(ns).operator(".");
1754        }
1755        ctx.ident(&schema_ref.name);
1756    }
1757
1758    fn pg_field_ref(&self, field_ref: &FieldRef, ctx: &mut RenderCtx) {
1759        ctx.ident(&field_ref.table_name)
1760            .operator(".")
1761            .ident(&field_ref.field.name);
1762    }
1763
1764    fn pg_comma_idents(&self, names: &[String], ctx: &mut RenderCtx) {
1765        for (i, name) in names.iter().enumerate() {
1766            if i > 0 {
1767                ctx.comma();
1768            }
1769            ctx.ident(name);
1770        }
1771    }
1772
1773    fn pg_value(&self, val: &Value, ctx: &mut RenderCtx) -> RenderResult<()> {
1774        // NULL is always rendered as keyword, never as parameter.
1775        if matches!(val, Value::Null) {
1776            ctx.keyword("NULL");
1777            return Ok(());
1778        }
1779
1780        // In parameterized mode, send values as bind parameters (no casts —
1781        // the driver transmits types via the binary protocol and PG infers
1782        // from column context).
1783        if ctx.parameterize() {
1784            ctx.param(val.clone());
1785            return Ok(());
1786        }
1787
1788        // Inline literal mode (DDL defaults, TCL, etc.)
1789        self.pg_value_literal(val, ctx)
1790    }
1791
1792    fn pg_value_literal(&self, val: &Value, ctx: &mut RenderCtx) -> RenderResult<()> {
1793        match val {
1794            Value::Null => {
1795                ctx.keyword("NULL");
1796            }
1797            Value::Bool(b) => {
1798                ctx.keyword(if *b { "TRUE" } else { "FALSE" });
1799            }
1800            Value::Int(n) => {
1801                ctx.keyword(&n.to_string());
1802            }
1803            Value::Float(f) => {
1804                ctx.keyword(&f.to_string());
1805            }
1806            Value::Str(s) => {
1807                ctx.string_literal(s);
1808            }
1809            Value::Bytes(b) => {
1810                ctx.write("'\\x");
1811                for byte in b {
1812                    ctx.write(&format!("{byte:02x}"));
1813                }
1814                ctx.write("'");
1815            }
1816            Value::Date(s) | Value::DateTime(s) | Value::Time(s) => {
1817                ctx.string_literal(s);
1818            }
1819            Value::Decimal(s) => {
1820                ctx.keyword(s);
1821            }
1822            Value::Uuid(s) => {
1823                ctx.string_literal(s);
1824            }
1825            Value::Json(s) => {
1826                ctx.string_literal(s);
1827                ctx.write("::json");
1828            }
1829            Value::Jsonb(s) => {
1830                ctx.string_literal(s);
1831                ctx.write("::jsonb");
1832            }
1833            Value::IpNetwork(s) => {
1834                ctx.string_literal(s);
1835                ctx.write("::inet");
1836            }
1837            Value::Array(items) => {
1838                ctx.keyword("ARRAY").write("[");
1839                for (i, item) in items.iter().enumerate() {
1840                    if i > 0 {
1841                        ctx.comma();
1842                    }
1843                    self.pg_value_literal(item, ctx)?;
1844                }
1845                ctx.write("]");
1846            }
1847            Value::Vector(values) => {
1848                let parts: Vec<String> = values.iter().map(|v| v.to_string()).collect();
1849                let literal = format!("[{}]", parts.join(","));
1850                ctx.string_literal(&literal);
1851                ctx.write("::vector");
1852            }
1853            Value::TimeDelta {
1854                years,
1855                months,
1856                days,
1857                seconds,
1858                microseconds,
1859            } => {
1860                ctx.keyword("INTERVAL");
1861                let mut parts = Vec::new();
1862                if *years != 0 {
1863                    parts.push(format!("{years} years"));
1864                }
1865                if *months != 0 {
1866                    parts.push(format!("{months} months"));
1867                }
1868                if *days != 0 {
1869                    parts.push(format!("{days} days"));
1870                }
1871                if *seconds != 0 {
1872                    parts.push(format!("{seconds} seconds"));
1873                }
1874                if *microseconds != 0 {
1875                    parts.push(format!("{microseconds} microseconds"));
1876                }
1877                if parts.is_empty() {
1878                    parts.push("0 seconds".into());
1879                }
1880                ctx.string_literal(&parts.join(" "));
1881            }
1882        }
1883        Ok(())
1884    }
1885
1886    fn pg_referential_action(&self, action: &ReferentialAction, ctx: &mut RenderCtx) {
1887        match action {
1888            ReferentialAction::NoAction => {
1889                ctx.keyword("NO ACTION");
1890            }
1891            ReferentialAction::Restrict => {
1892                ctx.keyword("RESTRICT");
1893            }
1894            ReferentialAction::Cascade => {
1895                ctx.keyword("CASCADE");
1896            }
1897            ReferentialAction::SetNull(cols) => {
1898                ctx.keyword("SET NULL");
1899                if let Some(cols) = cols {
1900                    ctx.paren_open();
1901                    self.pg_comma_idents(cols, ctx);
1902                    ctx.paren_close();
1903                }
1904            }
1905            ReferentialAction::SetDefault(cols) => {
1906                ctx.keyword("SET DEFAULT");
1907                if let Some(cols) = cols {
1908                    ctx.paren_open();
1909                    self.pg_comma_idents(cols, ctx);
1910                    ctx.paren_close();
1911                }
1912            }
1913        }
1914    }
1915
1916    fn pg_deferrable(&self, def: &DeferrableConstraint, ctx: &mut RenderCtx) {
1917        if def.deferrable {
1918            ctx.keyword("DEFERRABLE");
1919        } else {
1920            ctx.keyword("NOT DEFERRABLE");
1921        }
1922        if def.initially_deferred {
1923            ctx.keyword("INITIALLY DEFERRED");
1924        } else {
1925            ctx.keyword("INITIALLY IMMEDIATE");
1926        }
1927    }
1928
1929    fn pg_identity(&self, identity: &IdentityColumn, ctx: &mut RenderCtx) {
1930        if identity.always {
1931            ctx.keyword("GENERATED ALWAYS AS IDENTITY");
1932        } else {
1933            ctx.keyword("GENERATED BY DEFAULT AS IDENTITY");
1934        }
1935        let has_options = identity.start.is_some()
1936            || identity.increment.is_some()
1937            || identity.min_value.is_some()
1938            || identity.max_value.is_some()
1939            || identity.cycle
1940            || identity.cache.is_some();
1941        if has_options {
1942            ctx.paren_open();
1943            if let Some(start) = identity.start {
1944                ctx.keyword("START WITH").keyword(&start.to_string());
1945            }
1946            if let Some(inc) = identity.increment {
1947                ctx.keyword("INCREMENT BY").keyword(&inc.to_string());
1948            }
1949            if let Some(min) = identity.min_value {
1950                ctx.keyword("MINVALUE").keyword(&min.to_string());
1951            }
1952            if let Some(max) = identity.max_value {
1953                ctx.keyword("MAXVALUE").keyword(&max.to_string());
1954            }
1955            if identity.cycle {
1956                ctx.keyword("CYCLE");
1957            }
1958            if let Some(cache) = identity.cache {
1959                ctx.keyword("CACHE").write(&cache.to_string());
1960            }
1961            ctx.paren_close();
1962        }
1963    }
1964
1965    fn pg_render_ctes(&self, ctes: &[CteDef], ctx: &mut RenderCtx) -> RenderResult<()> {
1966        // Delegate to the trait method
1967        self.render_ctes(ctes, ctx)
1968    }
1969
1970    fn pg_render_from_item(&self, item: &FromItem, ctx: &mut RenderCtx) -> RenderResult<()> {
1971        if item.only {
1972            ctx.keyword("ONLY");
1973        }
1974        self.render_from(&item.source, ctx)?;
1975        if let Some(sample) = &item.sample {
1976            ctx.keyword("TABLESAMPLE");
1977            ctx.keyword(match sample.method {
1978                SampleMethod::Bernoulli => "BERNOULLI",
1979                SampleMethod::System => "SYSTEM",
1980                SampleMethod::Block => "SYSTEM", // Block maps to SYSTEM on PG
1981            });
1982            ctx.paren_open()
1983                .write(&sample.percentage.to_string())
1984                .paren_close();
1985            if let Some(seed) = sample.seed {
1986                ctx.keyword("REPEATABLE")
1987                    .paren_open()
1988                    .write(&seed.to_string())
1989                    .paren_close();
1990            }
1991        }
1992        Ok(())
1993    }
1994
1995    fn pg_render_group_by(&self, items: &[GroupByItem], ctx: &mut RenderCtx) -> RenderResult<()> {
1996        ctx.keyword("GROUP BY");
1997        for (i, item) in items.iter().enumerate() {
1998            if i > 0 {
1999                ctx.comma();
2000            }
2001            match item {
2002                GroupByItem::Expr(expr) => {
2003                    self.render_expr(expr, ctx)?;
2004                }
2005                GroupByItem::Rollup(exprs) => {
2006                    ctx.keyword("ROLLUP").paren_open();
2007                    for (j, expr) in exprs.iter().enumerate() {
2008                        if j > 0 {
2009                            ctx.comma();
2010                        }
2011                        self.render_expr(expr, ctx)?;
2012                    }
2013                    ctx.paren_close();
2014                }
2015                GroupByItem::Cube(exprs) => {
2016                    ctx.keyword("CUBE").paren_open();
2017                    for (j, expr) in exprs.iter().enumerate() {
2018                        if j > 0 {
2019                            ctx.comma();
2020                        }
2021                        self.render_expr(expr, ctx)?;
2022                    }
2023                    ctx.paren_close();
2024                }
2025                GroupByItem::GroupingSets(sets) => {
2026                    ctx.keyword("GROUPING SETS").paren_open();
2027                    for (j, set) in sets.iter().enumerate() {
2028                        if j > 0 {
2029                            ctx.comma();
2030                        }
2031                        ctx.paren_open();
2032                        for (k, expr) in set.iter().enumerate() {
2033                            if k > 0 {
2034                                ctx.comma();
2035                            }
2036                            self.render_expr(expr, ctx)?;
2037                        }
2038                        ctx.paren_close();
2039                    }
2040                    ctx.paren_close();
2041                }
2042            }
2043        }
2044        Ok(())
2045    }
2046
2047    fn pg_render_window_clause(
2048        &self,
2049        windows: &[WindowNameDef],
2050        ctx: &mut RenderCtx,
2051    ) -> RenderResult<()> {
2052        ctx.keyword("WINDOW");
2053        for (i, win) in windows.iter().enumerate() {
2054            if i > 0 {
2055                ctx.comma();
2056            }
2057            ctx.ident(&win.name).keyword("AS").paren_open();
2058            if let Some(base) = &win.base_window {
2059                ctx.ident(base);
2060            }
2061            if let Some(partition_by) = &win.partition_by {
2062                ctx.keyword("PARTITION BY");
2063                for (j, expr) in partition_by.iter().enumerate() {
2064                    if j > 0 {
2065                        ctx.comma();
2066                    }
2067                    self.render_expr(expr, ctx)?;
2068                }
2069            }
2070            if let Some(order_by) = &win.order_by {
2071                ctx.keyword("ORDER BY");
2072                self.pg_order_by_list(order_by, ctx)?;
2073            }
2074            if let Some(frame) = &win.frame {
2075                self.pg_window_frame(frame, ctx);
2076            }
2077            ctx.paren_close();
2078        }
2079        Ok(())
2080    }
2081
2082    fn pg_render_set_op(&self, set_op: &SetOpDef, ctx: &mut RenderCtx) -> RenderResult<()> {
2083        self.render_query(&set_op.left, ctx)?;
2084        ctx.keyword(match set_op.operation {
2085            SetOperationType::Union => "UNION",
2086            SetOperationType::UnionAll => "UNION ALL",
2087            SetOperationType::Intersect => "INTERSECT",
2088            SetOperationType::IntersectAll => "INTERSECT ALL",
2089            SetOperationType::Except => "EXCEPT",
2090            SetOperationType::ExceptAll => "EXCEPT ALL",
2091        });
2092        self.render_query(&set_op.right, ctx)
2093    }
2094
2095    fn pg_create_table(
2096        &self,
2097        schema: &SchemaDef,
2098        if_not_exists: bool,
2099        temporary: bool,
2100        unlogged: bool,
2101        opts: &PgCreateTableOpts<'_>,
2102        ctx: &mut RenderCtx,
2103    ) -> RenderResult<()> {
2104        let PgCreateTableOpts {
2105            tablespace,
2106            partition_by,
2107            inherits,
2108            using_method,
2109            with_options,
2110            on_commit,
2111        } = opts;
2112        ctx.keyword("CREATE");
2113        if temporary {
2114            ctx.keyword("TEMPORARY");
2115        }
2116        if unlogged {
2117            ctx.keyword("UNLOGGED");
2118        }
2119        ctx.keyword("TABLE");
2120        if if_not_exists {
2121            ctx.keyword("IF NOT EXISTS");
2122        }
2123        if let Some(ns) = &schema.namespace {
2124            ctx.ident(ns).operator(".");
2125        }
2126        ctx.ident(&schema.name);
2127
2128        // Columns + constraints + LIKE
2129        ctx.paren_open();
2130        let mut first = true;
2131        for col in &schema.columns {
2132            if !first {
2133                ctx.comma();
2134            }
2135            first = false;
2136            self.render_column_def(col, ctx)?;
2137        }
2138        if let Some(like_tables) = &schema.like_tables {
2139            for like in like_tables {
2140                if !first {
2141                    ctx.comma();
2142                }
2143                first = false;
2144                self.pg_like_table(like, ctx);
2145            }
2146        }
2147        if let Some(constraints) = &schema.constraints {
2148            for constraint in constraints {
2149                if !first {
2150                    ctx.comma();
2151                }
2152                first = false;
2153                self.render_constraint(constraint, ctx)?;
2154            }
2155        }
2156        ctx.paren_close();
2157
2158        // INHERITS
2159        if let Some(parents) = inherits {
2160            ctx.keyword("INHERITS").paren_open();
2161            for (i, parent) in parents.iter().enumerate() {
2162                if i > 0 {
2163                    ctx.comma();
2164                }
2165                self.pg_schema_ref(parent, ctx);
2166            }
2167            ctx.paren_close();
2168        }
2169
2170        // PARTITION BY
2171        if let Some(part) = partition_by {
2172            ctx.keyword("PARTITION BY");
2173            ctx.keyword(match part.strategy {
2174                PartitionStrategy::Range => "RANGE",
2175                PartitionStrategy::List => "LIST",
2176                PartitionStrategy::Hash => "HASH",
2177            });
2178            ctx.paren_open();
2179            for (i, col) in part.columns.iter().enumerate() {
2180                if i > 0 {
2181                    ctx.comma();
2182                }
2183                match &col.expr {
2184                    IndexExpr::Column(name) => {
2185                        ctx.ident(name);
2186                    }
2187                    IndexExpr::Expression(expr) => {
2188                        ctx.paren_open();
2189                        self.render_expr(expr, ctx)?;
2190                        ctx.paren_close();
2191                    }
2192                }
2193                if let Some(collation) = &col.collation {
2194                    ctx.keyword("COLLATE").ident(collation);
2195                }
2196                if let Some(opclass) = &col.opclass {
2197                    ctx.keyword(opclass);
2198                }
2199            }
2200            ctx.paren_close();
2201        }
2202
2203        // USING method
2204        if let Some(method) = using_method {
2205            ctx.keyword("USING").keyword(method);
2206        }
2207
2208        // WITH (storage_parameter = value, ...)
2209        if let Some(opts) = with_options {
2210            ctx.keyword("WITH").paren_open();
2211            for (i, (key, value)) in opts.iter().enumerate() {
2212                if i > 0 {
2213                    ctx.comma();
2214                }
2215                ctx.write(key).write(" = ").write(value);
2216            }
2217            ctx.paren_close();
2218        }
2219
2220        // ON COMMIT
2221        if let Some(action) = on_commit {
2222            ctx.keyword("ON COMMIT");
2223            ctx.keyword(match action {
2224                OnCommitAction::PreserveRows => "PRESERVE ROWS",
2225                OnCommitAction::DeleteRows => "DELETE ROWS",
2226                OnCommitAction::Drop => "DROP",
2227            });
2228        }
2229
2230        // TABLESPACE
2231        if let Some(ts) = tablespace {
2232            ctx.keyword("TABLESPACE").ident(ts);
2233        }
2234
2235        Ok(())
2236    }
2237
2238    fn pg_like_table(&self, like: &LikeTableDef, ctx: &mut RenderCtx) {
2239        ctx.keyword("LIKE");
2240        self.pg_schema_ref(&like.source_table, ctx);
2241        for opt in &like.options {
2242            if opt.include {
2243                ctx.keyword("INCLUDING");
2244            } else {
2245                ctx.keyword("EXCLUDING");
2246            }
2247            ctx.keyword(match opt.kind {
2248                qcraft_core::ast::ddl::LikeOptionKind::Comments => "COMMENTS",
2249                qcraft_core::ast::ddl::LikeOptionKind::Compression => "COMPRESSION",
2250                qcraft_core::ast::ddl::LikeOptionKind::Constraints => "CONSTRAINTS",
2251                qcraft_core::ast::ddl::LikeOptionKind::Defaults => "DEFAULTS",
2252                qcraft_core::ast::ddl::LikeOptionKind::Generated => "GENERATED",
2253                qcraft_core::ast::ddl::LikeOptionKind::Identity => "IDENTITY",
2254                qcraft_core::ast::ddl::LikeOptionKind::Indexes => "INDEXES",
2255                qcraft_core::ast::ddl::LikeOptionKind::Statistics => "STATISTICS",
2256                qcraft_core::ast::ddl::LikeOptionKind::Storage => "STORAGE",
2257                qcraft_core::ast::ddl::LikeOptionKind::All => "ALL",
2258            });
2259        }
2260    }
2261
2262    fn pg_create_index(
2263        &self,
2264        schema_ref: &qcraft_core::ast::common::SchemaRef,
2265        index: &IndexDef,
2266        if_not_exists: bool,
2267        concurrently: bool,
2268        ctx: &mut RenderCtx,
2269    ) -> RenderResult<()> {
2270        ctx.keyword("CREATE");
2271        if index.unique {
2272            ctx.keyword("UNIQUE");
2273        }
2274        ctx.keyword("INDEX");
2275        if concurrently {
2276            ctx.keyword("CONCURRENTLY");
2277        }
2278        if if_not_exists {
2279            ctx.keyword("IF NOT EXISTS");
2280        }
2281        ctx.ident(&index.name).keyword("ON");
2282        self.pg_schema_ref(schema_ref, ctx);
2283
2284        if let Some(index_type) = &index.index_type {
2285            ctx.keyword("USING").keyword(index_type);
2286        }
2287
2288        ctx.paren_open();
2289        self.pg_index_columns(&index.columns, ctx)?;
2290        ctx.paren_close();
2291
2292        if let Some(include) = &index.include {
2293            ctx.keyword("INCLUDE").paren_open();
2294            self.pg_comma_idents(include, ctx);
2295            ctx.paren_close();
2296        }
2297
2298        if let Some(nd) = index.nulls_distinct {
2299            if !nd {
2300                ctx.keyword("NULLS NOT DISTINCT");
2301            }
2302        }
2303
2304        if let Some(params) = &index.parameters {
2305            ctx.keyword("WITH").paren_open();
2306            for (i, (key, value)) in params.iter().enumerate() {
2307                if i > 0 {
2308                    ctx.comma();
2309                }
2310                ctx.write(key).write(" = ").write(value);
2311            }
2312            ctx.paren_close();
2313        }
2314
2315        if let Some(ts) = &index.tablespace {
2316            ctx.keyword("TABLESPACE").ident(ts);
2317        }
2318
2319        if let Some(condition) = &index.condition {
2320            ctx.keyword("WHERE");
2321            self.render_condition(condition, ctx)?;
2322        }
2323
2324        Ok(())
2325    }
2326
2327    fn pg_index_columns(
2328        &self,
2329        columns: &[IndexColumnDef],
2330        ctx: &mut RenderCtx,
2331    ) -> RenderResult<()> {
2332        for (i, col) in columns.iter().enumerate() {
2333            if i > 0 {
2334                ctx.comma();
2335            }
2336            match &col.expr {
2337                IndexExpr::Column(name) => {
2338                    ctx.ident(name);
2339                }
2340                IndexExpr::Expression(expr) => {
2341                    ctx.paren_open();
2342                    self.render_expr(expr, ctx)?;
2343                    ctx.paren_close();
2344                }
2345            }
2346            if let Some(collation) = &col.collation {
2347                ctx.keyword("COLLATE").ident(collation);
2348            }
2349            if let Some(opclass) = &col.opclass {
2350                ctx.keyword(opclass);
2351            }
2352            if let Some(dir) = col.direction {
2353                ctx.keyword(match dir {
2354                    OrderDir::Asc => "ASC",
2355                    OrderDir::Desc => "DESC",
2356                });
2357            }
2358            if let Some(nulls) = col.nulls {
2359                ctx.keyword(match nulls {
2360                    NullsOrder::First => "NULLS FIRST",
2361                    NullsOrder::Last => "NULLS LAST",
2362                });
2363            }
2364        }
2365        Ok(())
2366    }
2367
2368    fn pg_order_by_list(&self, order_by: &[OrderByDef], ctx: &mut RenderCtx) -> RenderResult<()> {
2369        for (i, ob) in order_by.iter().enumerate() {
2370            if i > 0 {
2371                ctx.comma();
2372            }
2373            self.render_expr(&ob.expr, ctx)?;
2374            ctx.keyword(match ob.direction {
2375                OrderDir::Asc => "ASC",
2376                OrderDir::Desc => "DESC",
2377            });
2378            if let Some(nulls) = &ob.nulls {
2379                ctx.keyword(match nulls {
2380                    NullsOrder::First => "NULLS FIRST",
2381                    NullsOrder::Last => "NULLS LAST",
2382                });
2383            }
2384        }
2385        Ok(())
2386    }
2387
2388    fn pg_window_frame(&self, frame: &WindowFrameDef, ctx: &mut RenderCtx) {
2389        ctx.keyword(match frame.frame_type {
2390            WindowFrameType::Rows => "ROWS",
2391            WindowFrameType::Range => "RANGE",
2392            WindowFrameType::Groups => "GROUPS",
2393        });
2394        if let Some(end) = &frame.end {
2395            ctx.keyword("BETWEEN");
2396            self.pg_frame_bound(&frame.start, ctx);
2397            ctx.keyword("AND");
2398            self.pg_frame_bound(end, ctx);
2399        } else {
2400            self.pg_frame_bound(&frame.start, ctx);
2401        }
2402    }
2403
2404    fn pg_frame_bound(&self, bound: &WindowFrameBound, ctx: &mut RenderCtx) {
2405        match bound {
2406            WindowFrameBound::CurrentRow => {
2407                ctx.keyword("CURRENT ROW");
2408            }
2409            WindowFrameBound::Preceding(None) => {
2410                ctx.keyword("UNBOUNDED PRECEDING");
2411            }
2412            WindowFrameBound::Preceding(Some(n)) => {
2413                ctx.keyword(&n.to_string()).keyword("PRECEDING");
2414            }
2415            WindowFrameBound::Following(None) => {
2416                ctx.keyword("UNBOUNDED FOLLOWING");
2417            }
2418            WindowFrameBound::Following(Some(n)) => {
2419                ctx.keyword(&n.to_string()).keyword("FOLLOWING");
2420            }
2421        }
2422    }
2423}