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