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            } => {
648                if let Some(n) = name {
649                    ctx.keyword("CONSTRAINT").ident(n);
650                }
651                ctx.keyword("PRIMARY KEY").paren_open();
652                self.pg_comma_idents(columns, ctx);
653                ctx.paren_close();
654                if let Some(inc) = include {
655                    ctx.keyword("INCLUDE").paren_open();
656                    self.pg_comma_idents(inc, ctx);
657                    ctx.paren_close();
658                }
659            }
660
661            ConstraintDef::ForeignKey {
662                name,
663                columns,
664                ref_table,
665                ref_columns,
666                on_delete,
667                on_update,
668                deferrable,
669                match_type,
670            } => {
671                if let Some(n) = name {
672                    ctx.keyword("CONSTRAINT").ident(n);
673                }
674                ctx.keyword("FOREIGN KEY").paren_open();
675                self.pg_comma_idents(columns, ctx);
676                ctx.paren_close().keyword("REFERENCES");
677                self.pg_schema_ref(ref_table, ctx);
678                ctx.paren_open();
679                self.pg_comma_idents(ref_columns, ctx);
680                ctx.paren_close();
681                if let Some(mt) = match_type {
682                    ctx.keyword(match mt {
683                        MatchType::Full => "MATCH FULL",
684                        MatchType::Partial => "MATCH PARTIAL",
685                        MatchType::Simple => "MATCH SIMPLE",
686                    });
687                }
688                if let Some(action) = on_delete {
689                    ctx.keyword("ON DELETE");
690                    self.pg_referential_action(action, ctx);
691                }
692                if let Some(action) = on_update {
693                    ctx.keyword("ON UPDATE");
694                    self.pg_referential_action(action, ctx);
695                }
696                if let Some(def) = deferrable {
697                    self.pg_deferrable(def, ctx);
698                }
699            }
700
701            ConstraintDef::Unique {
702                name,
703                columns,
704                include,
705                nulls_distinct,
706                condition: _, // Partial unique → rendered as separate CREATE INDEX
707            } => {
708                if let Some(n) = name {
709                    ctx.keyword("CONSTRAINT").ident(n);
710                }
711                ctx.keyword("UNIQUE");
712                if let Some(false) = nulls_distinct {
713                    ctx.keyword("NULLS NOT DISTINCT");
714                }
715                ctx.paren_open();
716                self.pg_comma_idents(columns, ctx);
717                ctx.paren_close();
718                if let Some(inc) = include {
719                    ctx.keyword("INCLUDE").paren_open();
720                    self.pg_comma_idents(inc, ctx);
721                    ctx.paren_close();
722                }
723            }
724
725            ConstraintDef::Check {
726                name,
727                condition,
728                no_inherit,
729                enforced: _, // PostgreSQL always enforces CHECK
730            } => {
731                if let Some(n) = name {
732                    ctx.keyword("CONSTRAINT").ident(n);
733                }
734                ctx.keyword("CHECK").paren_open();
735                self.render_condition(condition, ctx)?;
736                ctx.paren_close();
737                if *no_inherit {
738                    ctx.keyword("NO INHERIT");
739                }
740            }
741
742            ConstraintDef::Exclusion {
743                name,
744                elements,
745                index_method,
746                condition,
747            } => {
748                if let Some(n) = name {
749                    ctx.keyword("CONSTRAINT").ident(n);
750                }
751                ctx.keyword("EXCLUDE USING")
752                    .keyword(index_method)
753                    .paren_open();
754                for (i, elem) in elements.iter().enumerate() {
755                    if i > 0 {
756                        ctx.comma();
757                    }
758                    ctx.ident(&elem.column)
759                        .keyword("WITH")
760                        .keyword(&elem.operator);
761                }
762                ctx.paren_close();
763                if let Some(cond) = condition {
764                    ctx.keyword("WHERE").paren_open();
765                    self.render_condition(cond, ctx)?;
766                    ctx.paren_close();
767                }
768            }
769
770            ConstraintDef::Custom(_) => {
771                return Err(RenderError::unsupported(
772                    "CustomConstraint",
773                    "custom constraint must be handled by a wrapping renderer",
774                ));
775            }
776        }
777        Ok(())
778    }
779
780    fn render_index_def(&self, idx: &IndexDef, ctx: &mut RenderCtx) -> RenderResult<()> {
781        // Used for inline index rendering (inside CREATE TABLE context).
782        // Full CREATE INDEX is handled in pg_create_index.
783        ctx.ident(&idx.name);
784        if let Some(index_type) = &idx.index_type {
785            ctx.keyword("USING").keyword(index_type);
786        }
787        ctx.paren_open();
788        self.pg_index_columns(&idx.columns, ctx)?;
789        ctx.paren_close();
790        Ok(())
791    }
792
793    // ── Expressions (basic, needed for DDL) ──────────────────────────────
794
795    fn render_expr(&self, expr: &Expr, ctx: &mut RenderCtx) -> RenderResult<()> {
796        match expr {
797            Expr::Value(val) => self.pg_value(val, ctx),
798
799            Expr::Field(field_ref) => {
800                self.pg_field_ref(field_ref, ctx);
801                Ok(())
802            }
803
804            Expr::Binary { left, op, right } => {
805                self.render_expr(left, ctx)?;
806                // When using %s placeholders (psycopg), literal '%' must be
807                // escaped as '%%' so the driver doesn't treat it as a placeholder.
808                let mod_op = if self.param_style == ParamStyle::Percent {
809                    "%%"
810                } else {
811                    "%"
812                };
813                match op {
814                    BinaryOp::Custom(custom) => {
815                        render_custom_binary_op(custom.as_ref(), ctx)?;
816                    }
817                    _ => {
818                        ctx.keyword(match op {
819                            BinaryOp::Add => "+",
820                            BinaryOp::Sub => "-",
821                            BinaryOp::Mul => "*",
822                            BinaryOp::Div => "/",
823                            BinaryOp::Mod => mod_op,
824                            BinaryOp::BitwiseAnd => "&",
825                            BinaryOp::BitwiseOr => "|",
826                            BinaryOp::ShiftLeft => "<<",
827                            BinaryOp::ShiftRight => ">>",
828                            BinaryOp::Concat => "||",
829                            BinaryOp::Custom(_) => unreachable!(),
830                        });
831                    }
832                };
833                self.render_expr(right, ctx)
834            }
835
836            Expr::Unary { op, expr: inner } => {
837                match op {
838                    UnaryOp::Neg => ctx.write("-"),
839                    UnaryOp::Not => ctx.keyword("NOT"),
840                    UnaryOp::BitwiseNot => ctx.write("~"),
841                };
842                self.render_expr(inner, ctx)
843            }
844
845            Expr::Func { name, args } => {
846                ctx.keyword(name).write("(");
847                for (i, arg) in args.iter().enumerate() {
848                    if i > 0 {
849                        ctx.comma();
850                    }
851                    self.render_expr(arg, ctx)?;
852                }
853                ctx.paren_close();
854                Ok(())
855            }
856
857            Expr::Aggregate(agg) => self.render_aggregate(agg, ctx),
858
859            Expr::Cast {
860                expr: inner,
861                to_type,
862            } => {
863                self.render_expr(inner, ctx)?;
864                ctx.operator("::");
865                ctx.write(to_type);
866                Ok(())
867            }
868
869            Expr::Case(case) => self.render_case(case, ctx),
870
871            Expr::Window(win) => self.render_window(win, ctx),
872
873            Expr::Exists(query) => {
874                ctx.keyword("EXISTS").write("(");
875                self.render_query(query, ctx)?;
876                ctx.paren_close();
877                Ok(())
878            }
879
880            Expr::SubQuery(query) => {
881                ctx.paren_open();
882                self.render_query(query, ctx)?;
883                ctx.paren_close();
884                Ok(())
885            }
886
887            Expr::ArraySubQuery(query) => {
888                ctx.keyword("ARRAY").paren_open();
889                self.render_query(query, ctx)?;
890                ctx.paren_close();
891                Ok(())
892            }
893
894            Expr::Collate { expr, collation } => {
895                self.render_expr(expr, ctx)?;
896                ctx.keyword("COLLATE").ident(collation);
897                Ok(())
898            }
899
900            Expr::JsonArray(items) => {
901                ctx.keyword("jsonb_build_array").write("(");
902                for (i, item) in items.iter().enumerate() {
903                    if i > 0 {
904                        ctx.comma();
905                    }
906                    self.render_expr(item, ctx)?;
907                }
908                ctx.paren_close();
909                Ok(())
910            }
911
912            Expr::JsonObject(pairs) => {
913                ctx.keyword("jsonb_build_object").write("(");
914                for (i, (key, val)) in pairs.iter().enumerate() {
915                    if i > 0 {
916                        ctx.comma();
917                    }
918                    ctx.string_literal(key).comma();
919                    self.render_expr(val, ctx)?;
920                }
921                ctx.paren_close();
922                Ok(())
923            }
924
925            Expr::JsonAgg {
926                expr,
927                distinct,
928                filter,
929                order_by,
930            } => {
931                ctx.keyword("jsonb_agg").write("(");
932                if *distinct {
933                    ctx.keyword("DISTINCT");
934                }
935                self.render_expr(expr, ctx)?;
936                if let Some(ob) = order_by {
937                    ctx.keyword("ORDER BY");
938                    self.pg_order_by_list(ob, ctx)?;
939                }
940                ctx.paren_close();
941                if let Some(f) = filter {
942                    ctx.keyword("FILTER").paren_open().keyword("WHERE");
943                    self.render_condition(f, ctx)?;
944                    ctx.paren_close();
945                }
946                Ok(())
947            }
948
949            Expr::StringAgg {
950                expr,
951                delimiter,
952                distinct,
953                filter,
954                order_by,
955            } => {
956                ctx.keyword("string_agg").write("(");
957                if *distinct {
958                    ctx.keyword("DISTINCT");
959                }
960                self.render_expr(expr, ctx)?;
961                ctx.comma().string_literal(delimiter);
962                if let Some(ob) = order_by {
963                    ctx.keyword("ORDER BY");
964                    self.pg_order_by_list(ob, ctx)?;
965                }
966                ctx.paren_close();
967                if let Some(f) = filter {
968                    ctx.keyword("FILTER").paren_open().keyword("WHERE");
969                    self.render_condition(f, ctx)?;
970                    ctx.paren_close();
971                }
972                Ok(())
973            }
974
975            Expr::Now => {
976                ctx.keyword("now()");
977                Ok(())
978            }
979
980            Expr::Raw { sql, params } => {
981                if params.is_empty() {
982                    ctx.keyword(sql);
983                } else {
984                    ctx.raw_with_params(sql, params);
985                }
986                Ok(())
987            }
988
989            Expr::Custom(_) => Err(RenderError::unsupported(
990                "CustomExpr",
991                "custom expression must be handled by a wrapping renderer",
992            )),
993        }
994    }
995
996    fn render_aggregate(&self, agg: &AggregationDef, ctx: &mut RenderCtx) -> RenderResult<()> {
997        ctx.keyword(&agg.name).write("(");
998        if agg.distinct {
999            ctx.keyword("DISTINCT");
1000        }
1001        if let Some(expr) = &agg.expression {
1002            self.render_expr(expr, ctx)?;
1003        } else {
1004            ctx.write("*");
1005        }
1006        if let Some(args) = &agg.args {
1007            for arg in args {
1008                ctx.comma();
1009                self.render_expr(arg, ctx)?;
1010            }
1011        }
1012        if let Some(order_by) = &agg.order_by {
1013            ctx.keyword("ORDER BY");
1014            self.pg_order_by_list(order_by, ctx)?;
1015        }
1016        ctx.paren_close();
1017        if let Some(filter) = &agg.filter {
1018            ctx.keyword("FILTER").paren_open().keyword("WHERE");
1019            self.render_condition(filter, ctx)?;
1020            ctx.paren_close();
1021        }
1022        Ok(())
1023    }
1024
1025    fn render_window(&self, win: &WindowDef, ctx: &mut RenderCtx) -> RenderResult<()> {
1026        self.render_expr(&win.expression, ctx)?;
1027        ctx.keyword("OVER").paren_open();
1028        if let Some(partition_by) = &win.partition_by {
1029            ctx.keyword("PARTITION BY");
1030            for (i, expr) in partition_by.iter().enumerate() {
1031                if i > 0 {
1032                    ctx.comma();
1033                }
1034                self.render_expr(expr, ctx)?;
1035            }
1036        }
1037        if let Some(order_by) = &win.order_by {
1038            ctx.keyword("ORDER BY");
1039            self.pg_order_by_list(order_by, ctx)?;
1040        }
1041        if let Some(frame) = &win.frame {
1042            self.pg_window_frame(frame, ctx);
1043        }
1044        ctx.paren_close();
1045        Ok(())
1046    }
1047
1048    fn render_case(&self, case: &CaseDef, ctx: &mut RenderCtx) -> RenderResult<()> {
1049        ctx.keyword("CASE");
1050        for clause in &case.cases {
1051            ctx.keyword("WHEN");
1052            self.render_condition(&clause.condition, ctx)?;
1053            ctx.keyword("THEN");
1054            self.render_expr(&clause.result, ctx)?;
1055        }
1056        if let Some(default) = &case.default {
1057            ctx.keyword("ELSE");
1058            self.render_expr(default, ctx)?;
1059        }
1060        ctx.keyword("END");
1061        Ok(())
1062    }
1063
1064    // ── Conditions ───────────────────────────────────────────────────────
1065
1066    fn render_condition(&self, cond: &Conditions, ctx: &mut RenderCtx) -> RenderResult<()> {
1067        // Special case: negated + single Exists child → NOT EXISTS (...)
1068        if cond.negated
1069            && cond.children.len() == 1
1070            && matches!(cond.children[0], ConditionNode::Exists(_))
1071        {
1072            if let ConditionNode::Exists(query) = &cond.children[0] {
1073                ctx.keyword("NOT EXISTS").write("(");
1074                self.render_query(query, ctx)?;
1075                ctx.paren_close();
1076                return Ok(());
1077            }
1078        }
1079
1080        if cond.negated {
1081            ctx.keyword("NOT").paren_open();
1082        }
1083        let connector = match cond.connector {
1084            Connector::And => " AND ",
1085            Connector::Or => " OR ",
1086        };
1087        for (i, child) in cond.children.iter().enumerate() {
1088            if i > 0 {
1089                ctx.write(connector);
1090            }
1091            match child {
1092                ConditionNode::Comparison(comp) => {
1093                    if comp.negate {
1094                        ctx.keyword("NOT").paren_open();
1095                    }
1096                    self.render_compare_op(&comp.op, &comp.left, &comp.right, ctx)?;
1097                    if comp.negate {
1098                        ctx.paren_close();
1099                    }
1100                }
1101                ConditionNode::Group(group) => {
1102                    ctx.paren_open();
1103                    self.render_condition(group, ctx)?;
1104                    ctx.paren_close();
1105                }
1106                ConditionNode::Exists(query) => {
1107                    ctx.keyword("EXISTS").write("(");
1108                    self.render_query(query, ctx)?;
1109                    ctx.paren_close();
1110                }
1111                ConditionNode::Custom(_) => {
1112                    return Err(RenderError::unsupported(
1113                        "CustomCondition",
1114                        "custom condition must be handled by a wrapping renderer",
1115                    ));
1116                }
1117            }
1118        }
1119        if cond.negated {
1120            ctx.paren_close();
1121        }
1122        Ok(())
1123    }
1124
1125    fn render_compare_op(
1126        &self,
1127        op: &CompareOp,
1128        left: &Expr,
1129        right: &Expr,
1130        ctx: &mut RenderCtx,
1131    ) -> RenderResult<()> {
1132        self.render_expr(left, ctx)?;
1133        match op {
1134            CompareOp::Eq => ctx.write(" = "),
1135            CompareOp::Neq => ctx.write(" <> "),
1136            CompareOp::Gt => ctx.write(" > "),
1137            CompareOp::Gte => ctx.write(" >= "),
1138            CompareOp::Lt => ctx.write(" < "),
1139            CompareOp::Lte => ctx.write(" <= "),
1140            CompareOp::Like => ctx.keyword("LIKE"),
1141            CompareOp::ILike => ctx.keyword("ILIKE"),
1142            CompareOp::Contains | CompareOp::StartsWith | CompareOp::EndsWith => {
1143                ctx.keyword("LIKE");
1144                render_like_pattern(op, right, ctx)?;
1145                return Ok(());
1146            }
1147            CompareOp::IContains | CompareOp::IStartsWith | CompareOp::IEndsWith => {
1148                ctx.keyword("ILIKE");
1149                render_like_pattern(op, right, ctx)?;
1150                return Ok(());
1151            }
1152            CompareOp::In => {
1153                if let Expr::Value(Value::Array(items)) = right {
1154                    ctx.keyword("IN").paren_open();
1155                    for (i, item) in items.iter().enumerate() {
1156                        if i > 0 {
1157                            ctx.comma();
1158                        }
1159                        self.pg_value(item, ctx)?;
1160                    }
1161                    ctx.paren_close();
1162                } else {
1163                    ctx.keyword("IN");
1164                    self.render_expr(right, ctx)?;
1165                }
1166                return Ok(());
1167            }
1168            CompareOp::Between => {
1169                ctx.keyword("BETWEEN");
1170                if let Expr::Value(Value::Array(items)) = right {
1171                    if items.len() == 2 {
1172                        self.pg_value(&items[0], ctx)?;
1173                        ctx.keyword("AND");
1174                        self.pg_value(&items[1], ctx)?;
1175                    } else {
1176                        return Err(RenderError::unsupported(
1177                            "Between",
1178                            "BETWEEN requires exactly 2 values",
1179                        ));
1180                    }
1181                } else {
1182                    self.render_expr(right, ctx)?;
1183                }
1184                return Ok(());
1185            }
1186            CompareOp::IsNull => {
1187                ctx.keyword("IS NULL");
1188                return Ok(());
1189            }
1190            CompareOp::Similar => ctx.keyword("SIMILAR TO"),
1191            CompareOp::Regex => ctx.write(" ~ "),
1192            CompareOp::IRegex => ctx.write(" ~* "),
1193            CompareOp::JsonbContains => ctx.write(" @> "),
1194            CompareOp::JsonbContainedBy => ctx.write(" <@ "),
1195            CompareOp::JsonbHasKey => ctx.write(" ? "),
1196            CompareOp::JsonbHasAnyKey => {
1197                ctx.write(" ?| ");
1198                self.render_expr(right, ctx)?;
1199                ctx.write("::text[]");
1200                return Ok(());
1201            }
1202            CompareOp::JsonbHasAllKeys => {
1203                ctx.write(" ?& ");
1204                self.render_expr(right, ctx)?;
1205                ctx.write("::text[]");
1206                return Ok(());
1207            }
1208            CompareOp::FtsMatch => ctx.write(" @@ "),
1209            CompareOp::TrigramSimilar => {
1210                if self.param_style == ParamStyle::Percent {
1211                    ctx.write(" %% ")
1212                } else {
1213                    ctx.write(" % ")
1214                }
1215            }
1216            CompareOp::TrigramWordSimilar => {
1217                if self.param_style == ParamStyle::Percent {
1218                    ctx.write(" <%% ")
1219                } else {
1220                    ctx.write(" <% ")
1221                }
1222            }
1223            CompareOp::TrigramStrictWordSimilar => {
1224                if self.param_style == ParamStyle::Percent {
1225                    ctx.write(" <<%% ")
1226                } else {
1227                    ctx.write(" <<% ")
1228                }
1229            }
1230            CompareOp::RangeContains => ctx.write(" @> "),
1231            CompareOp::RangeContainedBy => ctx.write(" <@ "),
1232            CompareOp::RangeOverlap => ctx.write(" && "),
1233            CompareOp::RangeStrictlyLeft => ctx.write(" << "),
1234            CompareOp::RangeStrictlyRight => ctx.write(" >> "),
1235            CompareOp::RangeNotLeft => ctx.write(" &> "),
1236            CompareOp::RangeNotRight => ctx.write(" &< "),
1237            CompareOp::RangeAdjacent => ctx.write(" -|- "),
1238            CompareOp::Custom(_) => {
1239                return Err(RenderError::unsupported(
1240                    "CustomCompareOp",
1241                    "custom compare op must be handled by a wrapping renderer",
1242                ));
1243            }
1244        };
1245        self.render_expr(right, ctx)
1246    }
1247
1248    // ── Query (stub) ─────────────────────────────────────────────────────
1249
1250    fn render_query(&self, stmt: &QueryStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1251        // CTEs
1252        if let Some(ctes) = &stmt.ctes {
1253            self.render_ctes(ctes, ctx)?;
1254        }
1255
1256        // SELECT
1257        ctx.keyword("SELECT");
1258
1259        // DISTINCT / DISTINCT ON
1260        if let Some(distinct) = &stmt.distinct {
1261            match distinct {
1262                DistinctDef::Distinct => {
1263                    ctx.keyword("DISTINCT");
1264                }
1265                DistinctDef::DistinctOn(exprs) => {
1266                    ctx.keyword("DISTINCT ON").paren_open();
1267                    for (i, expr) in exprs.iter().enumerate() {
1268                        if i > 0 {
1269                            ctx.comma();
1270                        }
1271                        self.render_expr(expr, ctx)?;
1272                    }
1273                    ctx.paren_close();
1274                }
1275            }
1276        }
1277
1278        // Columns
1279        self.render_select_columns(&stmt.columns, ctx)?;
1280
1281        // FROM
1282        if let Some(from) = &stmt.from {
1283            ctx.keyword("FROM");
1284            for (i, item) in from.iter().enumerate() {
1285                if i > 0 {
1286                    ctx.comma();
1287                }
1288                self.pg_render_from_item(item, ctx)?;
1289            }
1290        }
1291
1292        // JOINs
1293        if let Some(joins) = &stmt.joins {
1294            self.render_joins(joins, ctx)?;
1295        }
1296
1297        // WHERE
1298        if let Some(cond) = &stmt.where_clause {
1299            self.render_where(cond, ctx)?;
1300        }
1301
1302        // GROUP BY
1303        if let Some(group_by) = &stmt.group_by {
1304            self.pg_render_group_by(group_by, ctx)?;
1305        }
1306
1307        // HAVING
1308        if let Some(having) = &stmt.having {
1309            ctx.keyword("HAVING");
1310            self.render_condition(having, ctx)?;
1311        }
1312
1313        // WINDOW
1314        if let Some(windows) = &stmt.window {
1315            self.pg_render_window_clause(windows, ctx)?;
1316        }
1317
1318        // ORDER BY
1319        if let Some(order_by) = &stmt.order_by {
1320            self.render_order_by(order_by, ctx)?;
1321        }
1322
1323        // LIMIT / OFFSET
1324        if let Some(limit) = &stmt.limit {
1325            self.render_limit(limit, ctx)?;
1326        }
1327
1328        // FOR UPDATE / SHARE
1329        if let Some(locks) = &stmt.lock {
1330            for lock in locks {
1331                self.render_lock(lock, ctx)?;
1332            }
1333        }
1334
1335        Ok(())
1336    }
1337
1338    fn render_select_columns(
1339        &self,
1340        cols: &[SelectColumn],
1341        ctx: &mut RenderCtx,
1342    ) -> RenderResult<()> {
1343        for (i, col) in cols.iter().enumerate() {
1344            if i > 0 {
1345                ctx.comma();
1346            }
1347            match col {
1348                SelectColumn::Star(None) => {
1349                    ctx.keyword("*");
1350                }
1351                SelectColumn::Star(Some(table)) => {
1352                    ctx.ident(table).operator(".").keyword("*");
1353                }
1354                SelectColumn::Expr { expr, alias } => {
1355                    self.render_expr(expr, ctx)?;
1356                    if let Some(a) = alias {
1357                        ctx.keyword("AS").ident(a);
1358                    }
1359                }
1360                SelectColumn::Field { field, alias } => {
1361                    self.pg_field_ref(field, ctx);
1362                    if let Some(a) = alias {
1363                        ctx.keyword("AS").ident(a);
1364                    }
1365                }
1366            }
1367        }
1368        Ok(())
1369    }
1370    fn render_from(&self, source: &TableSource, ctx: &mut RenderCtx) -> RenderResult<()> {
1371        match source {
1372            TableSource::Table(schema_ref) => {
1373                self.pg_schema_ref(schema_ref, ctx);
1374                if let Some(alias) = &schema_ref.alias {
1375                    ctx.keyword("AS").ident(alias);
1376                }
1377            }
1378            TableSource::SubQuery(sq) => {
1379                ctx.paren_open();
1380                self.render_query(&sq.query, ctx)?;
1381                ctx.paren_close().keyword("AS").ident(&sq.alias);
1382            }
1383            TableSource::SetOp(set_op) => {
1384                ctx.paren_open();
1385                self.pg_render_set_op(set_op, ctx)?;
1386                ctx.paren_close();
1387            }
1388            TableSource::Lateral(inner) => {
1389                ctx.keyword("LATERAL");
1390                self.render_from(&inner.source, ctx)?;
1391            }
1392            TableSource::Function { name, args, alias } => {
1393                ctx.keyword(name).write("(");
1394                for (i, arg) in args.iter().enumerate() {
1395                    if i > 0 {
1396                        ctx.comma();
1397                    }
1398                    self.render_expr(arg, ctx)?;
1399                }
1400                ctx.paren_close();
1401                if let Some(a) = alias {
1402                    ctx.keyword("AS").ident(a);
1403                }
1404            }
1405            TableSource::Values {
1406                rows,
1407                alias,
1408                column_aliases,
1409            } => {
1410                ctx.paren_open().keyword("VALUES");
1411                for (i, row) in rows.iter().enumerate() {
1412                    if i > 0 {
1413                        ctx.comma();
1414                    }
1415                    ctx.paren_open();
1416                    for (j, val) in row.iter().enumerate() {
1417                        if j > 0 {
1418                            ctx.comma();
1419                        }
1420                        self.render_expr(val, ctx)?;
1421                    }
1422                    ctx.paren_close();
1423                }
1424                ctx.paren_close().keyword("AS").ident(alias);
1425                if let Some(cols) = column_aliases {
1426                    ctx.paren_open();
1427                    for (i, c) in cols.iter().enumerate() {
1428                        if i > 0 {
1429                            ctx.comma();
1430                        }
1431                        ctx.ident(c);
1432                    }
1433                    ctx.paren_close();
1434                }
1435            }
1436            TableSource::Custom(_) => {
1437                return Err(RenderError::unsupported(
1438                    "CustomTableSource",
1439                    "custom table source must be handled by a wrapping renderer",
1440                ));
1441            }
1442        }
1443        Ok(())
1444    }
1445    fn render_joins(&self, joins: &[JoinDef], ctx: &mut RenderCtx) -> RenderResult<()> {
1446        for join in joins {
1447            if join.natural {
1448                ctx.keyword("NATURAL");
1449            }
1450            ctx.keyword(match join.join_type {
1451                JoinType::Inner => "INNER JOIN",
1452                JoinType::Left => "LEFT JOIN",
1453                JoinType::Right => "RIGHT JOIN",
1454                JoinType::Full => "FULL JOIN",
1455                JoinType::Cross => "CROSS JOIN",
1456                JoinType::CrossApply => "CROSS JOIN LATERAL",
1457                JoinType::OuterApply => "LEFT JOIN LATERAL",
1458            });
1459            self.pg_render_from_item(&join.source, ctx)?;
1460            if !matches!(join.join_type, JoinType::Cross) {
1461                if let Some(condition) = &join.condition {
1462                    match condition {
1463                        JoinCondition::On(cond) => {
1464                            ctx.keyword("ON");
1465                            self.render_condition(cond, ctx)?;
1466                        }
1467                        JoinCondition::Using(cols) => {
1468                            ctx.keyword("USING").paren_open();
1469                            self.pg_comma_idents(cols, ctx);
1470                            ctx.paren_close();
1471                        }
1472                    }
1473                }
1474            }
1475            // CrossApply/OuterApply rendered as LATERAL need ON TRUE if no condition
1476            if matches!(join.join_type, JoinType::OuterApply) && join.condition.is_none() {
1477                ctx.keyword("ON TRUE");
1478            }
1479        }
1480        Ok(())
1481    }
1482    fn render_where(&self, cond: &Conditions, ctx: &mut RenderCtx) -> RenderResult<()> {
1483        ctx.keyword("WHERE");
1484        self.render_condition(cond, ctx)
1485    }
1486    fn render_order_by(&self, order: &[OrderByDef], ctx: &mut RenderCtx) -> RenderResult<()> {
1487        ctx.keyword("ORDER BY");
1488        self.pg_order_by_list(order, ctx)
1489    }
1490    fn render_limit(&self, limit: &LimitDef, ctx: &mut RenderCtx) -> RenderResult<()> {
1491        match &limit.kind {
1492            LimitKind::Limit(n) => {
1493                ctx.keyword("LIMIT");
1494                if ctx.parameterize() {
1495                    ctx.param(Value::BigInt(*n as i64));
1496                } else {
1497                    ctx.space().write(&n.to_string());
1498                }
1499            }
1500            LimitKind::FetchFirst {
1501                count,
1502                with_ties,
1503                percent,
1504            } => {
1505                if let Some(offset) = limit.offset {
1506                    ctx.keyword("OFFSET")
1507                        .space()
1508                        .write(&offset.to_string())
1509                        .keyword("ROWS");
1510                }
1511                ctx.keyword("FETCH FIRST");
1512                if *percent {
1513                    ctx.space().write(&count.to_string()).keyword("PERCENT");
1514                } else {
1515                    ctx.space().write(&count.to_string());
1516                }
1517                if *with_ties {
1518                    ctx.keyword("ROWS WITH TIES");
1519                } else {
1520                    ctx.keyword("ROWS ONLY");
1521                }
1522                return Ok(());
1523            }
1524            LimitKind::Top { count, .. } => {
1525                // PG doesn't support TOP, convert to LIMIT
1526                ctx.keyword("LIMIT");
1527                if ctx.parameterize() {
1528                    ctx.param(Value::BigInt(*count as i64));
1529                } else {
1530                    ctx.space().write(&count.to_string());
1531                }
1532            }
1533        }
1534        if let Some(offset) = limit.offset {
1535            ctx.keyword("OFFSET");
1536            if ctx.parameterize() {
1537                ctx.param(Value::BigInt(offset as i64));
1538            } else {
1539                ctx.space().write(&offset.to_string());
1540            }
1541        }
1542        Ok(())
1543    }
1544    fn render_ctes(&self, ctes: &[CteDef], ctx: &mut RenderCtx) -> RenderResult<()> {
1545        // Check if any CTE is recursive — PG uses WITH RECURSIVE once for all.
1546        let any_recursive = ctes.iter().any(|c| c.recursive);
1547        ctx.keyword("WITH");
1548        if any_recursive {
1549            ctx.keyword("RECURSIVE");
1550        }
1551        for (i, cte) in ctes.iter().enumerate() {
1552            if i > 0 {
1553                ctx.comma();
1554            }
1555            ctx.ident(&cte.name);
1556            if let Some(col_names) = &cte.column_names {
1557                ctx.paren_open();
1558                self.pg_comma_idents(col_names, ctx);
1559                ctx.paren_close();
1560            }
1561            ctx.keyword("AS");
1562            if let Some(mat) = &cte.materialized {
1563                match mat {
1564                    CteMaterialized::Materialized => {
1565                        ctx.keyword("MATERIALIZED");
1566                    }
1567                    CteMaterialized::NotMaterialized => {
1568                        ctx.keyword("NOT MATERIALIZED");
1569                    }
1570                }
1571            }
1572            ctx.paren_open();
1573            self.render_query(&cte.query, ctx)?;
1574            ctx.paren_close();
1575        }
1576        Ok(())
1577    }
1578    fn render_lock(&self, lock: &SelectLockDef, ctx: &mut RenderCtx) -> RenderResult<()> {
1579        ctx.keyword("FOR");
1580        ctx.keyword(match lock.strength {
1581            LockStrength::Update => "UPDATE",
1582            LockStrength::NoKeyUpdate => "NO KEY UPDATE",
1583            LockStrength::Share => "SHARE",
1584            LockStrength::KeyShare => "KEY SHARE",
1585        });
1586        if let Some(of) = &lock.of {
1587            ctx.keyword("OF");
1588            for (i, table) in of.iter().enumerate() {
1589                if i > 0 {
1590                    ctx.comma();
1591                }
1592                self.pg_schema_ref(table, ctx);
1593            }
1594        }
1595        if lock.nowait {
1596            ctx.keyword("NOWAIT");
1597        }
1598        if lock.skip_locked {
1599            ctx.keyword("SKIP LOCKED");
1600        }
1601        Ok(())
1602    }
1603
1604    // ── DML ──────────────────────────────────────────────────────────────
1605
1606    fn render_mutation(&self, stmt: &MutationStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1607        match stmt {
1608            MutationStmt::Insert(s) => self.render_insert(s, ctx),
1609            MutationStmt::Update(s) => self.render_update(s, ctx),
1610            MutationStmt::Delete(s) => self.render_delete(s, ctx),
1611            MutationStmt::Custom(_) => Err(RenderError::unsupported(
1612                "CustomMutation",
1613                "custom DML must be handled by a wrapping renderer",
1614            )),
1615        }
1616    }
1617
1618    fn render_insert(&self, stmt: &InsertStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1619        // CTEs
1620        if let Some(ctes) = &stmt.ctes {
1621            self.pg_render_ctes(ctes, ctx)?;
1622        }
1623
1624        ctx.keyword("INSERT INTO");
1625        self.pg_schema_ref(&stmt.table, ctx);
1626
1627        // Column list
1628        if let Some(cols) = &stmt.columns {
1629            ctx.paren_open();
1630            self.pg_comma_idents(cols, ctx);
1631            ctx.paren_close();
1632        }
1633
1634        // OVERRIDING
1635        if let Some(overriding) = &stmt.overriding {
1636            ctx.keyword(match overriding {
1637                OverridingKind::System => "OVERRIDING SYSTEM VALUE",
1638                OverridingKind::User => "OVERRIDING USER VALUE",
1639            });
1640        }
1641
1642        // Source
1643        match &stmt.source {
1644            InsertSource::Values(rows) => {
1645                ctx.keyword("VALUES");
1646                for (i, row) in rows.iter().enumerate() {
1647                    if i > 0 {
1648                        ctx.comma();
1649                    }
1650                    ctx.paren_open();
1651                    for (j, expr) in row.iter().enumerate() {
1652                        if j > 0 {
1653                            ctx.comma();
1654                        }
1655                        self.render_expr(expr, ctx)?;
1656                    }
1657                    ctx.paren_close();
1658                }
1659            }
1660            InsertSource::Select(query) => {
1661                self.render_query(query, ctx)?;
1662            }
1663            InsertSource::DefaultValues => {
1664                ctx.keyword("DEFAULT VALUES");
1665            }
1666        }
1667
1668        // ON CONFLICT
1669        if let Some(conflicts) = &stmt.on_conflict {
1670            for oc in conflicts {
1671                self.render_on_conflict(oc, ctx)?;
1672            }
1673        }
1674
1675        // RETURNING
1676        if let Some(returning) = &stmt.returning {
1677            self.render_returning(returning, ctx)?;
1678        }
1679
1680        Ok(())
1681    }
1682
1683    fn render_update(&self, stmt: &UpdateStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1684        // CTEs
1685        if let Some(ctes) = &stmt.ctes {
1686            self.pg_render_ctes(ctes, ctx)?;
1687        }
1688
1689        ctx.keyword("UPDATE");
1690
1691        // ONLY
1692        if stmt.only {
1693            ctx.keyword("ONLY");
1694        }
1695
1696        self.pg_schema_ref(&stmt.table, ctx);
1697
1698        // Alias
1699        if let Some(alias) = &stmt.table.alias {
1700            ctx.keyword("AS").ident(alias);
1701        }
1702
1703        // SET
1704        ctx.keyword("SET");
1705        for (i, (col, expr)) in stmt.assignments.iter().enumerate() {
1706            if i > 0 {
1707                ctx.comma();
1708            }
1709            ctx.ident(col).write(" = ");
1710            self.render_expr(expr, ctx)?;
1711        }
1712
1713        // FROM
1714        if let Some(from) = &stmt.from {
1715            ctx.keyword("FROM");
1716            for (i, source) in from.iter().enumerate() {
1717                if i > 0 {
1718                    ctx.comma();
1719                }
1720                self.render_from(source, ctx)?;
1721            }
1722        }
1723
1724        // WHERE
1725        if let Some(cond) = &stmt.where_clause {
1726            ctx.keyword("WHERE");
1727            self.render_condition(cond, ctx)?;
1728        }
1729
1730        // RETURNING
1731        if let Some(returning) = &stmt.returning {
1732            self.render_returning(returning, ctx)?;
1733        }
1734
1735        Ok(())
1736    }
1737
1738    fn render_delete(&self, stmt: &DeleteStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1739        // CTEs
1740        if let Some(ctes) = &stmt.ctes {
1741            self.pg_render_ctes(ctes, ctx)?;
1742        }
1743
1744        ctx.keyword("DELETE FROM");
1745
1746        // ONLY
1747        if stmt.only {
1748            ctx.keyword("ONLY");
1749        }
1750
1751        self.pg_schema_ref(&stmt.table, ctx);
1752
1753        // Alias
1754        if let Some(alias) = &stmt.table.alias {
1755            ctx.keyword("AS").ident(alias);
1756        }
1757
1758        // USING
1759        if let Some(using) = &stmt.using {
1760            ctx.keyword("USING");
1761            for (i, source) in using.iter().enumerate() {
1762                if i > 0 {
1763                    ctx.comma();
1764                }
1765                self.render_from(source, ctx)?;
1766            }
1767        }
1768
1769        // WHERE
1770        if let Some(cond) = &stmt.where_clause {
1771            ctx.keyword("WHERE");
1772            self.render_condition(cond, ctx)?;
1773        }
1774
1775        // RETURNING
1776        if let Some(returning) = &stmt.returning {
1777            self.render_returning(returning, ctx)?;
1778        }
1779
1780        Ok(())
1781    }
1782
1783    fn render_on_conflict(&self, oc: &OnConflictDef, ctx: &mut RenderCtx) -> RenderResult<()> {
1784        ctx.keyword("ON CONFLICT");
1785
1786        // Target
1787        if let Some(target) = &oc.target {
1788            match target {
1789                ConflictTarget::Columns {
1790                    columns,
1791                    where_clause,
1792                } => {
1793                    ctx.paren_open();
1794                    self.pg_comma_idents(columns, ctx);
1795                    ctx.paren_close();
1796                    if let Some(cond) = where_clause {
1797                        ctx.keyword("WHERE");
1798                        self.render_condition(cond, ctx)?;
1799                    }
1800                }
1801                ConflictTarget::Constraint(name) => {
1802                    ctx.keyword("ON CONSTRAINT").ident(name);
1803                }
1804            }
1805        }
1806
1807        // Action
1808        match &oc.action {
1809            ConflictAction::DoNothing => {
1810                ctx.keyword("DO NOTHING");
1811            }
1812            ConflictAction::DoUpdate {
1813                assignments,
1814                where_clause,
1815            } => {
1816                ctx.keyword("DO UPDATE SET");
1817                for (i, (col, expr)) in assignments.iter().enumerate() {
1818                    if i > 0 {
1819                        ctx.comma();
1820                    }
1821                    ctx.ident(col).write(" = ");
1822                    self.render_expr(expr, ctx)?;
1823                }
1824                if let Some(cond) = where_clause {
1825                    ctx.keyword("WHERE");
1826                    self.render_condition(cond, ctx)?;
1827                }
1828            }
1829        }
1830
1831        Ok(())
1832    }
1833
1834    fn render_returning(&self, cols: &[SelectColumn], ctx: &mut RenderCtx) -> RenderResult<()> {
1835        ctx.keyword("RETURNING");
1836        for (i, col) in cols.iter().enumerate() {
1837            if i > 0 {
1838                ctx.comma();
1839            }
1840            match col {
1841                SelectColumn::Star(None) => {
1842                    ctx.keyword("*");
1843                }
1844                SelectColumn::Star(Some(table)) => {
1845                    ctx.ident(table).operator(".").keyword("*");
1846                }
1847                SelectColumn::Expr { expr, alias } => {
1848                    self.render_expr(expr, ctx)?;
1849                    if let Some(a) = alias {
1850                        ctx.keyword("AS").ident(a);
1851                    }
1852                }
1853                SelectColumn::Field { field, alias } => {
1854                    self.pg_field_ref(field, ctx);
1855                    if let Some(a) = alias {
1856                        ctx.keyword("AS").ident(a);
1857                    }
1858                }
1859            }
1860        }
1861        Ok(())
1862    }
1863
1864    // ── TCL ──────────────────────────────────────────────────────────────
1865
1866    fn render_transaction(&self, stmt: &TransactionStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1867        match stmt {
1868            TransactionStmt::Begin(s) => self.pg_begin(s, ctx),
1869            TransactionStmt::Commit(s) => self.pg_commit(s, ctx),
1870            TransactionStmt::Rollback(s) => self.pg_rollback(s, ctx),
1871            TransactionStmt::Savepoint(s) => {
1872                ctx.keyword("SAVEPOINT").ident(&s.name);
1873                Ok(())
1874            }
1875            TransactionStmt::ReleaseSavepoint(s) => {
1876                ctx.keyword("RELEASE").keyword("SAVEPOINT").ident(&s.name);
1877                Ok(())
1878            }
1879            TransactionStmt::SetTransaction(s) => self.pg_set_transaction(s, ctx),
1880            TransactionStmt::LockTable(s) => self.pg_lock_table(s, ctx),
1881            TransactionStmt::PrepareTransaction(s) => {
1882                ctx.keyword("PREPARE")
1883                    .keyword("TRANSACTION")
1884                    .string_literal(&s.transaction_id);
1885                Ok(())
1886            }
1887            TransactionStmt::CommitPrepared(s) => {
1888                ctx.keyword("COMMIT")
1889                    .keyword("PREPARED")
1890                    .string_literal(&s.transaction_id);
1891                Ok(())
1892            }
1893            TransactionStmt::RollbackPrepared(s) => {
1894                ctx.keyword("ROLLBACK")
1895                    .keyword("PREPARED")
1896                    .string_literal(&s.transaction_id);
1897                Ok(())
1898            }
1899            TransactionStmt::Custom(_) => Err(RenderError::unsupported(
1900                "Custom TCL",
1901                "not supported by PostgresRenderer",
1902            )),
1903        }
1904    }
1905}
1906
1907// ==========================================================================
1908// PostgreSQL-specific helpers
1909// ==========================================================================
1910
1911impl PostgresRenderer {
1912    // ── TCL helpers ──────────────────────────────────────────────────────
1913
1914    fn pg_begin(&self, stmt: &BeginStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1915        ctx.keyword("BEGIN");
1916        if let Some(modes) = &stmt.modes {
1917            self.pg_transaction_modes(modes, ctx);
1918        }
1919        Ok(())
1920    }
1921
1922    fn pg_commit(&self, stmt: &CommitStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1923        ctx.keyword("COMMIT");
1924        if stmt.and_chain {
1925            ctx.keyword("AND").keyword("CHAIN");
1926        }
1927        Ok(())
1928    }
1929
1930    fn pg_rollback(&self, stmt: &RollbackStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1931        ctx.keyword("ROLLBACK");
1932        if let Some(sp) = &stmt.to_savepoint {
1933            ctx.keyword("TO").keyword("SAVEPOINT").ident(sp);
1934        }
1935        if stmt.and_chain {
1936            ctx.keyword("AND").keyword("CHAIN");
1937        }
1938        Ok(())
1939    }
1940
1941    fn pg_set_transaction(
1942        &self,
1943        stmt: &SetTransactionStmt,
1944        ctx: &mut RenderCtx,
1945    ) -> RenderResult<()> {
1946        ctx.keyword("SET");
1947        match &stmt.scope {
1948            Some(TransactionScope::Session) => {
1949                ctx.keyword("SESSION")
1950                    .keyword("CHARACTERISTICS")
1951                    .keyword("AS")
1952                    .keyword("TRANSACTION");
1953            }
1954            _ => {
1955                ctx.keyword("TRANSACTION");
1956            }
1957        }
1958        if let Some(snap_id) = &stmt.snapshot_id {
1959            ctx.keyword("SNAPSHOT").string_literal(snap_id);
1960        } else {
1961            self.pg_transaction_modes(&stmt.modes, ctx);
1962        }
1963        Ok(())
1964    }
1965
1966    fn pg_transaction_modes(&self, modes: &[TransactionMode], ctx: &mut RenderCtx) {
1967        for (i, mode) in modes.iter().enumerate() {
1968            if i > 0 {
1969                ctx.comma();
1970            }
1971            match mode {
1972                TransactionMode::IsolationLevel(lvl) => {
1973                    ctx.keyword("ISOLATION").keyword("LEVEL");
1974                    ctx.keyword(match lvl {
1975                        IsolationLevel::ReadUncommitted => "READ UNCOMMITTED",
1976                        IsolationLevel::ReadCommitted => "READ COMMITTED",
1977                        IsolationLevel::RepeatableRead => "REPEATABLE READ",
1978                        IsolationLevel::Serializable => "SERIALIZABLE",
1979                        IsolationLevel::Snapshot => "SERIALIZABLE", // PG doesn't have SNAPSHOT
1980                    });
1981                }
1982                TransactionMode::ReadOnly => {
1983                    ctx.keyword("READ ONLY");
1984                }
1985                TransactionMode::ReadWrite => {
1986                    ctx.keyword("READ WRITE");
1987                }
1988                TransactionMode::Deferrable => {
1989                    ctx.keyword("DEFERRABLE");
1990                }
1991                TransactionMode::NotDeferrable => {
1992                    ctx.keyword("NOT DEFERRABLE");
1993                }
1994                TransactionMode::WithConsistentSnapshot => {} // MySQL only, skip
1995            }
1996        }
1997    }
1998
1999    fn pg_lock_table(&self, stmt: &LockTableStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
2000        ctx.keyword("LOCK").keyword("TABLE");
2001        for (i, def) in stmt.tables.iter().enumerate() {
2002            if i > 0 {
2003                ctx.comma();
2004            }
2005            if def.only {
2006                ctx.keyword("ONLY");
2007            }
2008            if let Some(schema) = &def.schema {
2009                ctx.ident(schema).operator(".");
2010            }
2011            ctx.ident(&def.table);
2012        }
2013        // Use mode from first table (PG applies one mode to all).
2014        if let Some(first) = stmt.tables.first() {
2015            ctx.keyword("IN");
2016            ctx.keyword(match first.mode {
2017                LockMode::AccessShare => "ACCESS SHARE",
2018                LockMode::RowShare => "ROW SHARE",
2019                LockMode::RowExclusive => "ROW EXCLUSIVE",
2020                LockMode::ShareUpdateExclusive => "SHARE UPDATE EXCLUSIVE",
2021                LockMode::Share => "SHARE",
2022                LockMode::ShareRowExclusive => "SHARE ROW EXCLUSIVE",
2023                LockMode::Exclusive => "EXCLUSIVE",
2024                LockMode::AccessExclusive => "ACCESS EXCLUSIVE",
2025                _ => "ACCESS EXCLUSIVE", // Non-PG modes default
2026            });
2027            ctx.keyword("MODE");
2028        }
2029        if stmt.nowait {
2030            ctx.keyword("NOWAIT");
2031        }
2032        Ok(())
2033    }
2034
2035    // ── Schema helpers ───────────────────────────────────────────────────
2036
2037    fn pg_schema_ref(&self, schema_ref: &qcraft_core::ast::common::SchemaRef, ctx: &mut RenderCtx) {
2038        if let Some(ns) = &schema_ref.namespace {
2039            ctx.ident(ns).operator(".");
2040        }
2041        ctx.ident(&schema_ref.name);
2042    }
2043
2044    fn pg_field_ref(&self, field_ref: &FieldRef, ctx: &mut RenderCtx) {
2045        if let Some(ns) = &field_ref.namespace {
2046            ctx.ident(ns).operator(".");
2047        }
2048        ctx.ident(&field_ref.table_name)
2049            .operator(".")
2050            .ident(&field_ref.field.name);
2051        let mut child = &field_ref.field.child;
2052        while let Some(c) = child {
2053            ctx.operator("->'")
2054                .write(&c.name.replace('\'', "''"))
2055                .write("'");
2056            child = &c.child;
2057        }
2058    }
2059
2060    fn pg_comma_idents(&self, names: &[String], ctx: &mut RenderCtx) {
2061        for (i, name) in names.iter().enumerate() {
2062            if i > 0 {
2063                ctx.comma();
2064            }
2065            ctx.ident(name);
2066        }
2067    }
2068
2069    fn pg_value(&self, val: &Value, ctx: &mut RenderCtx) -> RenderResult<()> {
2070        // In parameterized mode, NULL goes as a bind parameter (drivers handle
2071        // it correctly via the extended query protocol). Only IS NULL
2072        // comparisons need the keyword — that path is handled separately in
2073        // render_compare_op.
2074        if matches!(val, Value::Null) && !ctx.parameterize() {
2075            ctx.keyword("NULL");
2076            return Ok(());
2077        }
2078
2079        // In parameterized mode, send values as bind parameters (no casts —
2080        // the driver transmits types via the binary protocol and PG infers
2081        // from column context).
2082        if ctx.parameterize() {
2083            ctx.param(val.clone());
2084            return Ok(());
2085        }
2086
2087        // Inline literal mode (DDL defaults, TCL, etc.)
2088        self.pg_value_literal(val, ctx)
2089    }
2090
2091    fn pg_value_literal(&self, val: &Value, ctx: &mut RenderCtx) -> RenderResult<()> {
2092        match val {
2093            Value::Null => {
2094                ctx.keyword("NULL");
2095            }
2096            Value::Bool(b) => {
2097                ctx.keyword(if *b { "TRUE" } else { "FALSE" });
2098            }
2099            Value::Int(n) | Value::BigInt(n) => {
2100                ctx.keyword(&n.to_string());
2101            }
2102            Value::Float(f) => {
2103                ctx.keyword(&f.to_string());
2104            }
2105            Value::Str(s) => {
2106                ctx.string_literal(s);
2107            }
2108            Value::Bytes(b) => {
2109                ctx.write("'\\x");
2110                for byte in b {
2111                    ctx.write(&format!("{byte:02x}"));
2112                }
2113                ctx.write("'");
2114            }
2115            Value::Date(s) | Value::DateTime(s) | Value::Time(s) => {
2116                ctx.string_literal(s);
2117            }
2118            Value::Decimal(s) => {
2119                ctx.keyword(s);
2120            }
2121            Value::Uuid(s) => {
2122                ctx.string_literal(s);
2123            }
2124            Value::Json(s) => {
2125                ctx.string_literal(s);
2126                ctx.write("::json");
2127            }
2128            Value::Jsonb(s) => {
2129                ctx.string_literal(s);
2130                ctx.write("::jsonb");
2131            }
2132            Value::IpNetwork(s) => {
2133                ctx.string_literal(s);
2134                ctx.write("::inet");
2135            }
2136            Value::Array(items) => {
2137                ctx.keyword("ARRAY").write("[");
2138                for (i, item) in items.iter().enumerate() {
2139                    if i > 0 {
2140                        ctx.comma();
2141                    }
2142                    self.pg_value_literal(item, ctx)?;
2143                }
2144                ctx.write("]");
2145            }
2146            Value::Vector(values) => {
2147                let parts: Vec<String> = values.iter().map(|v| v.to_string()).collect();
2148                let literal = format!("[{}]", parts.join(","));
2149                ctx.string_literal(&literal);
2150                ctx.write("::vector");
2151            }
2152            Value::TimeDelta {
2153                years,
2154                months,
2155                days,
2156                seconds,
2157                microseconds,
2158            } => {
2159                ctx.keyword("INTERVAL");
2160                let mut parts = Vec::new();
2161                if *years != 0 {
2162                    parts.push(format!("{years} years"));
2163                }
2164                if *months != 0 {
2165                    parts.push(format!("{months} months"));
2166                }
2167                if *days != 0 {
2168                    parts.push(format!("{days} days"));
2169                }
2170                if *seconds != 0 {
2171                    parts.push(format!("{seconds} seconds"));
2172                }
2173                if *microseconds != 0 {
2174                    parts.push(format!("{microseconds} microseconds"));
2175                }
2176                if parts.is_empty() {
2177                    parts.push("0 seconds".into());
2178                }
2179                ctx.string_literal(&parts.join(" "));
2180            }
2181        }
2182        Ok(())
2183    }
2184
2185    fn pg_referential_action(&self, action: &ReferentialAction, ctx: &mut RenderCtx) {
2186        match action {
2187            ReferentialAction::NoAction => {
2188                ctx.keyword("NO ACTION");
2189            }
2190            ReferentialAction::Restrict => {
2191                ctx.keyword("RESTRICT");
2192            }
2193            ReferentialAction::Cascade => {
2194                ctx.keyword("CASCADE");
2195            }
2196            ReferentialAction::SetNull(cols) => {
2197                ctx.keyword("SET NULL");
2198                if let Some(cols) = cols {
2199                    ctx.paren_open();
2200                    self.pg_comma_idents(cols, ctx);
2201                    ctx.paren_close();
2202                }
2203            }
2204            ReferentialAction::SetDefault(cols) => {
2205                ctx.keyword("SET DEFAULT");
2206                if let Some(cols) = cols {
2207                    ctx.paren_open();
2208                    self.pg_comma_idents(cols, ctx);
2209                    ctx.paren_close();
2210                }
2211            }
2212        }
2213    }
2214
2215    fn pg_deferrable(&self, def: &DeferrableConstraint, ctx: &mut RenderCtx) {
2216        if def.deferrable {
2217            ctx.keyword("DEFERRABLE");
2218        } else {
2219            ctx.keyword("NOT DEFERRABLE");
2220        }
2221        if def.initially_deferred {
2222            ctx.keyword("INITIALLY DEFERRED");
2223        } else {
2224            ctx.keyword("INITIALLY IMMEDIATE");
2225        }
2226    }
2227
2228    fn pg_identity(&self, identity: &IdentityColumn, ctx: &mut RenderCtx) {
2229        if identity.always {
2230            ctx.keyword("GENERATED ALWAYS AS IDENTITY");
2231        } else {
2232            ctx.keyword("GENERATED BY DEFAULT AS IDENTITY");
2233        }
2234        let has_options = identity.start.is_some()
2235            || identity.increment.is_some()
2236            || identity.min_value.is_some()
2237            || identity.max_value.is_some()
2238            || identity.cycle
2239            || identity.cache.is_some();
2240        if has_options {
2241            ctx.paren_open();
2242            if let Some(start) = identity.start {
2243                ctx.keyword("START WITH").keyword(&start.to_string());
2244            }
2245            if let Some(inc) = identity.increment {
2246                ctx.keyword("INCREMENT BY").keyword(&inc.to_string());
2247            }
2248            if let Some(min) = identity.min_value {
2249                ctx.keyword("MINVALUE").keyword(&min.to_string());
2250            }
2251            if let Some(max) = identity.max_value {
2252                ctx.keyword("MAXVALUE").keyword(&max.to_string());
2253            }
2254            if identity.cycle {
2255                ctx.keyword("CYCLE");
2256            }
2257            if let Some(cache) = identity.cache {
2258                ctx.keyword("CACHE").write(&cache.to_string());
2259            }
2260            ctx.paren_close();
2261        }
2262    }
2263
2264    fn pg_render_ctes(&self, ctes: &[CteDef], ctx: &mut RenderCtx) -> RenderResult<()> {
2265        // Delegate to the trait method
2266        self.render_ctes(ctes, ctx)
2267    }
2268
2269    fn pg_render_from_item(&self, item: &FromItem, ctx: &mut RenderCtx) -> RenderResult<()> {
2270        if item.only {
2271            ctx.keyword("ONLY");
2272        }
2273        self.render_from(&item.source, ctx)?;
2274        if let Some(sample) = &item.sample {
2275            ctx.keyword("TABLESAMPLE");
2276            ctx.keyword(match sample.method {
2277                SampleMethod::Bernoulli => "BERNOULLI",
2278                SampleMethod::System => "SYSTEM",
2279                SampleMethod::Block => "SYSTEM", // Block maps to SYSTEM on PG
2280            });
2281            ctx.paren_open()
2282                .write(&sample.percentage.to_string())
2283                .paren_close();
2284            if let Some(seed) = sample.seed {
2285                ctx.keyword("REPEATABLE")
2286                    .paren_open()
2287                    .write(&seed.to_string())
2288                    .paren_close();
2289            }
2290        }
2291        Ok(())
2292    }
2293
2294    fn pg_render_group_by(&self, items: &[GroupByItem], ctx: &mut RenderCtx) -> RenderResult<()> {
2295        ctx.keyword("GROUP BY");
2296        for (i, item) in items.iter().enumerate() {
2297            if i > 0 {
2298                ctx.comma();
2299            }
2300            match item {
2301                GroupByItem::Expr(expr) => {
2302                    self.render_expr(expr, ctx)?;
2303                }
2304                GroupByItem::Rollup(exprs) => {
2305                    ctx.keyword("ROLLUP").paren_open();
2306                    for (j, expr) in exprs.iter().enumerate() {
2307                        if j > 0 {
2308                            ctx.comma();
2309                        }
2310                        self.render_expr(expr, ctx)?;
2311                    }
2312                    ctx.paren_close();
2313                }
2314                GroupByItem::Cube(exprs) => {
2315                    ctx.keyword("CUBE").paren_open();
2316                    for (j, expr) in exprs.iter().enumerate() {
2317                        if j > 0 {
2318                            ctx.comma();
2319                        }
2320                        self.render_expr(expr, ctx)?;
2321                    }
2322                    ctx.paren_close();
2323                }
2324                GroupByItem::GroupingSets(sets) => {
2325                    ctx.keyword("GROUPING SETS").paren_open();
2326                    for (j, set) in sets.iter().enumerate() {
2327                        if j > 0 {
2328                            ctx.comma();
2329                        }
2330                        ctx.paren_open();
2331                        for (k, expr) in set.iter().enumerate() {
2332                            if k > 0 {
2333                                ctx.comma();
2334                            }
2335                            self.render_expr(expr, ctx)?;
2336                        }
2337                        ctx.paren_close();
2338                    }
2339                    ctx.paren_close();
2340                }
2341            }
2342        }
2343        Ok(())
2344    }
2345
2346    fn pg_render_window_clause(
2347        &self,
2348        windows: &[WindowNameDef],
2349        ctx: &mut RenderCtx,
2350    ) -> RenderResult<()> {
2351        ctx.keyword("WINDOW");
2352        for (i, win) in windows.iter().enumerate() {
2353            if i > 0 {
2354                ctx.comma();
2355            }
2356            ctx.ident(&win.name).keyword("AS").paren_open();
2357            if let Some(base) = &win.base_window {
2358                ctx.ident(base);
2359            }
2360            if let Some(partition_by) = &win.partition_by {
2361                ctx.keyword("PARTITION BY");
2362                for (j, expr) in partition_by.iter().enumerate() {
2363                    if j > 0 {
2364                        ctx.comma();
2365                    }
2366                    self.render_expr(expr, ctx)?;
2367                }
2368            }
2369            if let Some(order_by) = &win.order_by {
2370                ctx.keyword("ORDER BY");
2371                self.pg_order_by_list(order_by, ctx)?;
2372            }
2373            if let Some(frame) = &win.frame {
2374                self.pg_window_frame(frame, ctx);
2375            }
2376            ctx.paren_close();
2377        }
2378        Ok(())
2379    }
2380
2381    fn pg_render_set_op(&self, set_op: &SetOpDef, ctx: &mut RenderCtx) -> RenderResult<()> {
2382        self.render_query(&set_op.left, ctx)?;
2383        ctx.keyword(match set_op.operation {
2384            SetOperationType::Union => "UNION",
2385            SetOperationType::UnionAll => "UNION ALL",
2386            SetOperationType::Intersect => "INTERSECT",
2387            SetOperationType::IntersectAll => "INTERSECT ALL",
2388            SetOperationType::Except => "EXCEPT",
2389            SetOperationType::ExceptAll => "EXCEPT ALL",
2390        });
2391        self.render_query(&set_op.right, ctx)
2392    }
2393
2394    fn pg_create_table(
2395        &self,
2396        schema: &SchemaDef,
2397        if_not_exists: bool,
2398        temporary: bool,
2399        unlogged: bool,
2400        opts: &PgCreateTableOpts<'_>,
2401        ctx: &mut RenderCtx,
2402    ) -> RenderResult<()> {
2403        let PgCreateTableOpts {
2404            tablespace,
2405            partition_by,
2406            inherits,
2407            using_method,
2408            with_options,
2409            on_commit,
2410        } = opts;
2411        ctx.keyword("CREATE");
2412        if temporary {
2413            ctx.keyword("TEMPORARY");
2414        }
2415        if unlogged {
2416            ctx.keyword("UNLOGGED");
2417        }
2418        ctx.keyword("TABLE");
2419        if if_not_exists {
2420            ctx.keyword("IF NOT EXISTS");
2421        }
2422        if let Some(ns) = &schema.namespace {
2423            ctx.ident(ns).operator(".");
2424        }
2425        ctx.ident(&schema.name);
2426
2427        // Columns + constraints + LIKE
2428        ctx.paren_open();
2429        let mut first = true;
2430        for col in &schema.columns {
2431            if !first {
2432                ctx.comma();
2433            }
2434            first = false;
2435            self.render_column_def(col, ctx)?;
2436        }
2437        if let Some(like_tables) = &schema.like_tables {
2438            for like in like_tables {
2439                if !first {
2440                    ctx.comma();
2441                }
2442                first = false;
2443                self.pg_like_table(like, ctx);
2444            }
2445        }
2446        if let Some(constraints) = &schema.constraints {
2447            for constraint in constraints {
2448                if !first {
2449                    ctx.comma();
2450                }
2451                first = false;
2452                self.render_constraint(constraint, ctx)?;
2453            }
2454        }
2455        ctx.paren_close();
2456
2457        // INHERITS
2458        if let Some(parents) = inherits {
2459            ctx.keyword("INHERITS").paren_open();
2460            for (i, parent) in parents.iter().enumerate() {
2461                if i > 0 {
2462                    ctx.comma();
2463                }
2464                self.pg_schema_ref(parent, ctx);
2465            }
2466            ctx.paren_close();
2467        }
2468
2469        // PARTITION BY
2470        if let Some(part) = partition_by {
2471            ctx.keyword("PARTITION BY");
2472            ctx.keyword(match part.strategy {
2473                PartitionStrategy::Range => "RANGE",
2474                PartitionStrategy::List => "LIST",
2475                PartitionStrategy::Hash => "HASH",
2476            });
2477            ctx.paren_open();
2478            for (i, col) in part.columns.iter().enumerate() {
2479                if i > 0 {
2480                    ctx.comma();
2481                }
2482                match &col.expr {
2483                    IndexExpr::Column(name) => {
2484                        ctx.ident(name);
2485                    }
2486                    IndexExpr::Expression(expr) => {
2487                        ctx.paren_open();
2488                        self.render_expr(expr, ctx)?;
2489                        ctx.paren_close();
2490                    }
2491                }
2492                if let Some(collation) = &col.collation {
2493                    ctx.keyword("COLLATE").ident(collation);
2494                }
2495                if let Some(opclass) = &col.opclass {
2496                    ctx.keyword(opclass);
2497                }
2498            }
2499            ctx.paren_close();
2500        }
2501
2502        // USING method
2503        if let Some(method) = using_method {
2504            ctx.keyword("USING").keyword(method);
2505        }
2506
2507        // WITH (storage_parameter = value, ...)
2508        if let Some(opts) = with_options {
2509            ctx.keyword("WITH").paren_open();
2510            for (i, (key, value)) in opts.iter().enumerate() {
2511                if i > 0 {
2512                    ctx.comma();
2513                }
2514                ctx.write(key).write(" = ").write(value);
2515            }
2516            ctx.paren_close();
2517        }
2518
2519        // ON COMMIT
2520        if let Some(action) = on_commit {
2521            ctx.keyword("ON COMMIT");
2522            ctx.keyword(match action {
2523                OnCommitAction::PreserveRows => "PRESERVE ROWS",
2524                OnCommitAction::DeleteRows => "DELETE ROWS",
2525                OnCommitAction::Drop => "DROP",
2526            });
2527        }
2528
2529        // TABLESPACE
2530        if let Some(ts) = tablespace {
2531            ctx.keyword("TABLESPACE").ident(ts);
2532        }
2533
2534        // Partial unique constraints → separate CREATE UNIQUE INDEX statements
2535        if let Some(constraints) = &schema.constraints {
2536            for constraint in constraints {
2537                if let ConstraintDef::Unique {
2538                    name,
2539                    columns,
2540                    condition: Some(cond),
2541                    ..
2542                } = constraint
2543                {
2544                    ctx.write(";");
2545                    ctx.keyword("CREATE UNIQUE INDEX");
2546                    if let Some(n) = name {
2547                        ctx.ident(n);
2548                    }
2549                    ctx.keyword("ON");
2550                    if let Some(ns) = &schema.namespace {
2551                        ctx.ident(ns).operator(".");
2552                    }
2553                    ctx.ident(&schema.name);
2554                    ctx.paren_open();
2555                    self.pg_comma_idents(columns, ctx);
2556                    ctx.paren_close();
2557                    ctx.keyword("WHERE");
2558                    self.render_condition(cond, ctx)?;
2559                }
2560            }
2561        }
2562
2563        Ok(())
2564    }
2565
2566    fn pg_like_table(&self, like: &LikeTableDef, ctx: &mut RenderCtx) {
2567        ctx.keyword("LIKE");
2568        self.pg_schema_ref(&like.source_table, ctx);
2569        for opt in &like.options {
2570            if opt.include {
2571                ctx.keyword("INCLUDING");
2572            } else {
2573                ctx.keyword("EXCLUDING");
2574            }
2575            ctx.keyword(match opt.kind {
2576                qcraft_core::ast::ddl::LikeOptionKind::Comments => "COMMENTS",
2577                qcraft_core::ast::ddl::LikeOptionKind::Compression => "COMPRESSION",
2578                qcraft_core::ast::ddl::LikeOptionKind::Constraints => "CONSTRAINTS",
2579                qcraft_core::ast::ddl::LikeOptionKind::Defaults => "DEFAULTS",
2580                qcraft_core::ast::ddl::LikeOptionKind::Generated => "GENERATED",
2581                qcraft_core::ast::ddl::LikeOptionKind::Identity => "IDENTITY",
2582                qcraft_core::ast::ddl::LikeOptionKind::Indexes => "INDEXES",
2583                qcraft_core::ast::ddl::LikeOptionKind::Statistics => "STATISTICS",
2584                qcraft_core::ast::ddl::LikeOptionKind::Storage => "STORAGE",
2585                qcraft_core::ast::ddl::LikeOptionKind::All => "ALL",
2586            });
2587        }
2588    }
2589
2590    fn pg_create_index(
2591        &self,
2592        schema_ref: &qcraft_core::ast::common::SchemaRef,
2593        index: &IndexDef,
2594        if_not_exists: bool,
2595        concurrently: bool,
2596        ctx: &mut RenderCtx,
2597    ) -> RenderResult<()> {
2598        ctx.keyword("CREATE");
2599        if index.unique {
2600            ctx.keyword("UNIQUE");
2601        }
2602        ctx.keyword("INDEX");
2603        if concurrently {
2604            ctx.keyword("CONCURRENTLY");
2605        }
2606        if if_not_exists {
2607            ctx.keyword("IF NOT EXISTS");
2608        }
2609        ctx.ident(&index.name).keyword("ON");
2610        self.pg_schema_ref(schema_ref, ctx);
2611
2612        if let Some(index_type) = &index.index_type {
2613            ctx.keyword("USING").keyword(index_type);
2614        }
2615
2616        ctx.paren_open();
2617        self.pg_index_columns(&index.columns, ctx)?;
2618        ctx.paren_close();
2619
2620        if let Some(include) = &index.include {
2621            ctx.keyword("INCLUDE").paren_open();
2622            self.pg_comma_idents(include, ctx);
2623            ctx.paren_close();
2624        }
2625
2626        if let Some(nd) = index.nulls_distinct {
2627            if !nd {
2628                ctx.keyword("NULLS NOT DISTINCT");
2629            }
2630        }
2631
2632        if let Some(params) = &index.parameters {
2633            ctx.keyword("WITH").paren_open();
2634            for (i, (key, value)) in params.iter().enumerate() {
2635                if i > 0 {
2636                    ctx.comma();
2637                }
2638                ctx.write(key).write(" = ").write(value);
2639            }
2640            ctx.paren_close();
2641        }
2642
2643        if let Some(ts) = &index.tablespace {
2644            ctx.keyword("TABLESPACE").ident(ts);
2645        }
2646
2647        if let Some(condition) = &index.condition {
2648            ctx.keyword("WHERE");
2649            self.render_condition(condition, ctx)?;
2650        }
2651
2652        Ok(())
2653    }
2654
2655    fn pg_index_columns(
2656        &self,
2657        columns: &[IndexColumnDef],
2658        ctx: &mut RenderCtx,
2659    ) -> RenderResult<()> {
2660        for (i, col) in columns.iter().enumerate() {
2661            if i > 0 {
2662                ctx.comma();
2663            }
2664            match &col.expr {
2665                IndexExpr::Column(name) => {
2666                    ctx.ident(name);
2667                }
2668                IndexExpr::Expression(expr) => {
2669                    ctx.paren_open();
2670                    self.render_expr(expr, ctx)?;
2671                    ctx.paren_close();
2672                }
2673            }
2674            if let Some(collation) = &col.collation {
2675                ctx.keyword("COLLATE").ident(collation);
2676            }
2677            if let Some(opclass) = &col.opclass {
2678                ctx.keyword(opclass);
2679            }
2680            if let Some(dir) = col.direction {
2681                ctx.keyword(match dir {
2682                    OrderDir::Asc => "ASC",
2683                    OrderDir::Desc => "DESC",
2684                });
2685            }
2686            if let Some(nulls) = col.nulls {
2687                ctx.keyword(match nulls {
2688                    NullsOrder::First => "NULLS FIRST",
2689                    NullsOrder::Last => "NULLS LAST",
2690                });
2691            }
2692        }
2693        Ok(())
2694    }
2695
2696    fn pg_order_by_list(&self, order_by: &[OrderByDef], ctx: &mut RenderCtx) -> RenderResult<()> {
2697        for (i, ob) in order_by.iter().enumerate() {
2698            if i > 0 {
2699                ctx.comma();
2700            }
2701            self.render_expr(&ob.expr, ctx)?;
2702            ctx.keyword(match ob.direction {
2703                OrderDir::Asc => "ASC",
2704                OrderDir::Desc => "DESC",
2705            });
2706            if let Some(nulls) = &ob.nulls {
2707                ctx.keyword(match nulls {
2708                    NullsOrder::First => "NULLS FIRST",
2709                    NullsOrder::Last => "NULLS LAST",
2710                });
2711            }
2712        }
2713        Ok(())
2714    }
2715
2716    fn pg_window_frame(&self, frame: &WindowFrameDef, ctx: &mut RenderCtx) {
2717        ctx.keyword(match frame.frame_type {
2718            WindowFrameType::Rows => "ROWS",
2719            WindowFrameType::Range => "RANGE",
2720            WindowFrameType::Groups => "GROUPS",
2721        });
2722        if let Some(end) = &frame.end {
2723            ctx.keyword("BETWEEN");
2724            self.pg_frame_bound(&frame.start, ctx);
2725            ctx.keyword("AND");
2726            self.pg_frame_bound(end, ctx);
2727        } else {
2728            self.pg_frame_bound(&frame.start, ctx);
2729        }
2730    }
2731
2732    fn pg_frame_bound(&self, bound: &WindowFrameBound, ctx: &mut RenderCtx) {
2733        match bound {
2734            WindowFrameBound::CurrentRow => {
2735                ctx.keyword("CURRENT ROW");
2736            }
2737            WindowFrameBound::Preceding(None) => {
2738                ctx.keyword("UNBOUNDED PRECEDING");
2739            }
2740            WindowFrameBound::Preceding(Some(n)) => {
2741                ctx.keyword(&n.to_string()).keyword("PRECEDING");
2742            }
2743            WindowFrameBound::Following(None) => {
2744                ctx.keyword("UNBOUNDED FOLLOWING");
2745            }
2746            WindowFrameBound::Following(Some(n)) => {
2747                ctx.keyword(&n.to_string()).keyword("FOLLOWING");
2748            }
2749        }
2750    }
2751}