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