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