Skip to main content

qcraft_postgres/
lib.rs

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