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