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::Tuple(exprs) => {
1079                ctx.paren_open();
1080                for (i, expr) in exprs.iter().enumerate() {
1081                    if i > 0 {
1082                        ctx.comma();
1083                    }
1084                    self.render_expr(expr, ctx)?;
1085                }
1086                ctx.paren_close();
1087                Ok(())
1088            }
1089
1090            Expr::Param { type_hint } => {
1091                ctx.placeholder();
1092                if let Some(hint) = type_hint {
1093                    ctx.write("::");
1094                    ctx.write(hint);
1095                }
1096                Ok(())
1097            }
1098
1099            Expr::Raw { sql, params } => {
1100                if params.is_empty() {
1101                    ctx.keyword(sql);
1102                } else {
1103                    ctx.raw_with_params(sql, params);
1104                }
1105                Ok(())
1106            }
1107
1108            Expr::Custom(_) => Err(RenderError::unsupported(
1109                "CustomExpr",
1110                "custom expression must be handled by a wrapping renderer",
1111            )),
1112        }
1113    }
1114
1115    fn render_aggregate(&self, agg: &AggregationDef, ctx: &mut RenderCtx) -> RenderResult<()> {
1116        ctx.keyword(&agg.name).write("(");
1117        if agg.distinct {
1118            ctx.keyword("DISTINCT");
1119        }
1120        if let Some(expr) = &agg.expression {
1121            self.render_expr(expr, ctx)?;
1122        } else {
1123            ctx.write("*");
1124        }
1125        if let Some(args) = &agg.args {
1126            for arg in args {
1127                ctx.comma();
1128                self.render_expr(arg, ctx)?;
1129            }
1130        }
1131        if let Some(order_by) = &agg.order_by {
1132            ctx.keyword("ORDER BY");
1133            self.pg_order_by_list(order_by, ctx)?;
1134        }
1135        ctx.paren_close();
1136        if let Some(filter) = &agg.filter {
1137            ctx.keyword("FILTER").paren_open().keyword("WHERE");
1138            self.render_condition(filter, ctx)?;
1139            ctx.paren_close();
1140        }
1141        Ok(())
1142    }
1143
1144    fn render_window(&self, win: &WindowDef, ctx: &mut RenderCtx) -> RenderResult<()> {
1145        self.render_expr(&win.expression, ctx)?;
1146        ctx.keyword("OVER").paren_open();
1147        if let Some(partition_by) = &win.partition_by {
1148            ctx.keyword("PARTITION BY");
1149            for (i, expr) in partition_by.iter().enumerate() {
1150                if i > 0 {
1151                    ctx.comma();
1152                }
1153                self.render_expr(expr, ctx)?;
1154            }
1155        }
1156        if let Some(order_by) = &win.order_by {
1157            ctx.keyword("ORDER BY");
1158            self.pg_order_by_list(order_by, ctx)?;
1159        }
1160        if let Some(frame) = &win.frame {
1161            self.pg_window_frame(frame, ctx);
1162        }
1163        ctx.paren_close();
1164        Ok(())
1165    }
1166
1167    fn render_case(&self, case: &CaseDef, ctx: &mut RenderCtx) -> RenderResult<()> {
1168        ctx.keyword("CASE");
1169        for clause in &case.cases {
1170            ctx.keyword("WHEN");
1171            self.render_condition(&clause.condition, ctx)?;
1172            ctx.keyword("THEN");
1173            self.render_expr(&clause.result, ctx)?;
1174        }
1175        if let Some(default) = &case.default {
1176            ctx.keyword("ELSE");
1177            self.render_expr(default, ctx)?;
1178        }
1179        ctx.keyword("END");
1180        Ok(())
1181    }
1182
1183    // ── Conditions ───────────────────────────────────────────────────────
1184
1185    fn render_condition(&self, cond: &Conditions, ctx: &mut RenderCtx) -> RenderResult<()> {
1186        // Special case: negated + single Exists child → NOT EXISTS (...)
1187        if cond.negated
1188            && cond.children.len() == 1
1189            && matches!(cond.children[0], ConditionNode::Exists(_))
1190        {
1191            if let ConditionNode::Exists(query) = &cond.children[0] {
1192                ctx.keyword("NOT EXISTS").write("(");
1193                self.render_query(query, ctx)?;
1194                ctx.paren_close();
1195                return Ok(());
1196            }
1197        }
1198
1199        if cond.negated {
1200            ctx.keyword("NOT").paren_open();
1201        }
1202        let connector = match cond.connector {
1203            Connector::And => " AND ",
1204            Connector::Or => " OR ",
1205        };
1206        for (i, child) in cond.children.iter().enumerate() {
1207            if i > 0 {
1208                ctx.write(connector);
1209            }
1210            match child {
1211                ConditionNode::Comparison(comp) => {
1212                    if comp.negate {
1213                        ctx.keyword("NOT").paren_open();
1214                    }
1215                    self.render_compare_op(&comp.op, &comp.left, &comp.right, ctx)?;
1216                    if comp.negate {
1217                        ctx.paren_close();
1218                    }
1219                }
1220                ConditionNode::Group(group) => {
1221                    ctx.paren_open();
1222                    self.render_condition(group, ctx)?;
1223                    ctx.paren_close();
1224                }
1225                ConditionNode::Exists(query) => {
1226                    ctx.keyword("EXISTS").write("(");
1227                    self.render_query(query, ctx)?;
1228                    ctx.paren_close();
1229                }
1230                ConditionNode::Custom(_) => {
1231                    return Err(RenderError::unsupported(
1232                        "CustomCondition",
1233                        "custom condition must be handled by a wrapping renderer",
1234                    ));
1235                }
1236            }
1237        }
1238        if cond.negated {
1239            ctx.paren_close();
1240        }
1241        Ok(())
1242    }
1243
1244    fn render_compare_op(
1245        &self,
1246        op: &CompareOp,
1247        left: &Expr,
1248        right: &Expr,
1249        ctx: &mut RenderCtx,
1250    ) -> RenderResult<()> {
1251        self.render_expr(left, ctx)?;
1252        match op {
1253            CompareOp::Eq => ctx.write(" = "),
1254            CompareOp::Neq => ctx.write(" <> "),
1255            CompareOp::Gt => ctx.write(" > "),
1256            CompareOp::Gte => ctx.write(" >= "),
1257            CompareOp::Lt => ctx.write(" < "),
1258            CompareOp::Lte => ctx.write(" <= "),
1259            CompareOp::Like => ctx.keyword("LIKE"),
1260            CompareOp::ILike => ctx.keyword("ILIKE"),
1261            CompareOp::Contains | CompareOp::StartsWith | CompareOp::EndsWith => {
1262                ctx.keyword("LIKE");
1263                render_like_pattern(op, right, ctx)?;
1264                return Ok(());
1265            }
1266            CompareOp::IContains | CompareOp::IStartsWith | CompareOp::IEndsWith => {
1267                ctx.keyword("ILIKE");
1268                render_like_pattern(op, right, ctx)?;
1269                return Ok(());
1270            }
1271            CompareOp::In => {
1272                if let Expr::Value(Value::Array(items)) = right {
1273                    ctx.keyword("IN").paren_open();
1274                    for (i, item) in items.iter().enumerate() {
1275                        if i > 0 {
1276                            ctx.comma();
1277                        }
1278                        self.pg_value(item, ctx)?;
1279                    }
1280                    ctx.paren_close();
1281                } else {
1282                    ctx.keyword("IN");
1283                    self.render_expr(right, ctx)?;
1284                }
1285                return Ok(());
1286            }
1287            CompareOp::Between => {
1288                ctx.keyword("BETWEEN");
1289                if let Expr::Value(Value::Array(items)) = right {
1290                    if items.len() == 2 {
1291                        self.pg_value(&items[0], ctx)?;
1292                        ctx.keyword("AND");
1293                        self.pg_value(&items[1], ctx)?;
1294                    } else {
1295                        return Err(RenderError::unsupported(
1296                            "Between",
1297                            "BETWEEN requires exactly 2 values",
1298                        ));
1299                    }
1300                } else {
1301                    self.render_expr(right, ctx)?;
1302                }
1303                return Ok(());
1304            }
1305            CompareOp::IsNull => {
1306                ctx.keyword("IS NULL");
1307                return Ok(());
1308            }
1309            CompareOp::Similar => ctx.keyword("SIMILAR TO"),
1310            CompareOp::Regex => ctx.write(" ~ "),
1311            CompareOp::IRegex => ctx.write(" ~* "),
1312            CompareOp::JsonbContains => ctx.write(" @> "),
1313            CompareOp::JsonbContainedBy => ctx.write(" <@ "),
1314            CompareOp::JsonbHasKey => ctx.write(" ? "),
1315            CompareOp::JsonbHasAnyKey => {
1316                ctx.write(" ?| ");
1317                self.render_expr(right, ctx)?;
1318                ctx.write("::text[]");
1319                return Ok(());
1320            }
1321            CompareOp::JsonbHasAllKeys => {
1322                ctx.write(" ?& ");
1323                self.render_expr(right, ctx)?;
1324                ctx.write("::text[]");
1325                return Ok(());
1326            }
1327            CompareOp::FtsMatch => ctx.write(" @@ "),
1328            CompareOp::TrigramSimilar => {
1329                if self.param_style == ParamStyle::Percent {
1330                    ctx.write(" %% ")
1331                } else {
1332                    ctx.write(" % ")
1333                }
1334            }
1335            CompareOp::TrigramWordSimilar => {
1336                if self.param_style == ParamStyle::Percent {
1337                    ctx.write(" <%% ")
1338                } else {
1339                    ctx.write(" <% ")
1340                }
1341            }
1342            CompareOp::TrigramStrictWordSimilar => {
1343                if self.param_style == ParamStyle::Percent {
1344                    ctx.write(" <<%% ")
1345                } else {
1346                    ctx.write(" <<% ")
1347                }
1348            }
1349            CompareOp::RangeContains => ctx.write(" @> "),
1350            CompareOp::RangeContainedBy => ctx.write(" <@ "),
1351            CompareOp::RangeOverlap => ctx.write(" && "),
1352            CompareOp::RangeStrictlyLeft => ctx.write(" << "),
1353            CompareOp::RangeStrictlyRight => ctx.write(" >> "),
1354            CompareOp::RangeNotLeft => ctx.write(" &> "),
1355            CompareOp::RangeNotRight => ctx.write(" &< "),
1356            CompareOp::RangeAdjacent => ctx.write(" -|- "),
1357            CompareOp::Custom(_) => {
1358                return Err(RenderError::unsupported(
1359                    "CustomCompareOp",
1360                    "custom compare op must be handled by a wrapping renderer",
1361                ));
1362            }
1363        };
1364        self.render_expr(right, ctx)
1365    }
1366
1367    // ── Query (stub) ─────────────────────────────────────────────────────
1368
1369    fn render_query(&self, stmt: &QueryStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1370        // CTEs
1371        if let Some(ctes) = &stmt.ctes {
1372            self.render_ctes(ctes, ctx)?;
1373        }
1374
1375        // Set operation: render directly without SELECT wrapper
1376        if let Some(set_op) = &stmt.set_op {
1377            return self.pg_render_set_op(set_op, ctx);
1378        }
1379
1380        // SELECT
1381        ctx.keyword("SELECT");
1382
1383        // DISTINCT / DISTINCT ON
1384        if let Some(distinct) = &stmt.distinct {
1385            match distinct {
1386                DistinctDef::Distinct => {
1387                    ctx.keyword("DISTINCT");
1388                }
1389                DistinctDef::DistinctOn(exprs) => {
1390                    ctx.keyword("DISTINCT ON").paren_open();
1391                    for (i, expr) in exprs.iter().enumerate() {
1392                        if i > 0 {
1393                            ctx.comma();
1394                        }
1395                        self.render_expr(expr, ctx)?;
1396                    }
1397                    ctx.paren_close();
1398                }
1399            }
1400        }
1401
1402        // Columns
1403        self.render_select_columns(&stmt.columns, ctx)?;
1404
1405        // FROM
1406        if let Some(from) = &stmt.from {
1407            ctx.keyword("FROM");
1408            for (i, item) in from.iter().enumerate() {
1409                if i > 0 {
1410                    ctx.comma();
1411                }
1412                self.pg_render_from_item(item, ctx)?;
1413            }
1414        }
1415
1416        // JOINs
1417        if let Some(joins) = &stmt.joins {
1418            self.render_joins(joins, ctx)?;
1419        }
1420
1421        // WHERE
1422        if let Some(cond) = &stmt.where_clause {
1423            self.render_where(cond, ctx)?;
1424        }
1425
1426        // GROUP BY
1427        if let Some(group_by) = &stmt.group_by {
1428            self.pg_render_group_by(group_by, ctx)?;
1429        }
1430
1431        // HAVING
1432        if let Some(having) = &stmt.having {
1433            ctx.keyword("HAVING");
1434            self.render_condition(having, ctx)?;
1435        }
1436
1437        // WINDOW
1438        if let Some(windows) = &stmt.window {
1439            self.pg_render_window_clause(windows, ctx)?;
1440        }
1441
1442        // ORDER BY
1443        if let Some(order_by) = &stmt.order_by {
1444            self.render_order_by(order_by, ctx)?;
1445        }
1446
1447        // LIMIT / OFFSET
1448        if let Some(limit) = &stmt.limit {
1449            self.render_limit(limit, ctx)?;
1450        }
1451
1452        // FOR UPDATE / SHARE
1453        if let Some(locks) = &stmt.lock {
1454            for lock in locks {
1455                self.render_lock(lock, ctx)?;
1456            }
1457        }
1458
1459        Ok(())
1460    }
1461
1462    fn render_select_columns(
1463        &self,
1464        cols: &[SelectColumn],
1465        ctx: &mut RenderCtx,
1466    ) -> RenderResult<()> {
1467        for (i, col) in cols.iter().enumerate() {
1468            if i > 0 {
1469                ctx.comma();
1470            }
1471            match col {
1472                SelectColumn::Star(None) => {
1473                    ctx.keyword("*");
1474                }
1475                SelectColumn::Star(Some(table)) => {
1476                    ctx.ident(table).operator(".").keyword("*");
1477                }
1478                SelectColumn::Expr { expr, alias } => {
1479                    self.render_expr(expr, ctx)?;
1480                    if let Some(a) = alias {
1481                        ctx.keyword("AS").ident(a);
1482                    }
1483                }
1484                SelectColumn::Field { field, alias } => {
1485                    self.pg_field_ref(field, ctx);
1486                    if let Some(a) = alias {
1487                        ctx.keyword("AS").ident(a);
1488                    }
1489                }
1490            }
1491        }
1492        Ok(())
1493    }
1494    fn render_from(&self, source: &TableSource, ctx: &mut RenderCtx) -> RenderResult<()> {
1495        match source {
1496            TableSource::Table(schema_ref) => {
1497                self.pg_schema_ref(schema_ref, ctx);
1498                if let Some(alias) = &schema_ref.alias {
1499                    ctx.keyword("AS").ident(alias);
1500                }
1501            }
1502            TableSource::SubQuery(sq) => {
1503                ctx.paren_open();
1504                self.render_query(&sq.query, ctx)?;
1505                ctx.paren_close().keyword("AS").ident(&sq.alias);
1506            }
1507            TableSource::SetOp(set_op) => {
1508                ctx.paren_open();
1509                self.pg_render_set_op(set_op, ctx)?;
1510                ctx.paren_close();
1511            }
1512            TableSource::Lateral(inner) => {
1513                ctx.keyword("LATERAL");
1514                self.render_from(&inner.source, ctx)?;
1515            }
1516            TableSource::Function { name, args, alias } => {
1517                ctx.keyword(name).write("(");
1518                for (i, arg) in args.iter().enumerate() {
1519                    if i > 0 {
1520                        ctx.comma();
1521                    }
1522                    self.render_expr(arg, ctx)?;
1523                }
1524                ctx.paren_close();
1525                if let Some(a) = alias {
1526                    ctx.keyword("AS").ident(a);
1527                }
1528            }
1529            TableSource::Values {
1530                rows,
1531                alias,
1532                column_aliases,
1533            } => {
1534                ctx.paren_open().keyword("VALUES");
1535                for (i, row) in rows.iter().enumerate() {
1536                    if i > 0 {
1537                        ctx.comma();
1538                    }
1539                    ctx.paren_open();
1540                    for (j, val) in row.iter().enumerate() {
1541                        if j > 0 {
1542                            ctx.comma();
1543                        }
1544                        self.render_expr(val, ctx)?;
1545                    }
1546                    ctx.paren_close();
1547                }
1548                ctx.paren_close().keyword("AS").ident(alias);
1549                if let Some(cols) = column_aliases {
1550                    ctx.paren_open();
1551                    for (i, c) in cols.iter().enumerate() {
1552                        if i > 0 {
1553                            ctx.comma();
1554                        }
1555                        ctx.ident(c);
1556                    }
1557                    ctx.paren_close();
1558                }
1559            }
1560            TableSource::Custom(_) => {
1561                return Err(RenderError::unsupported(
1562                    "CustomTableSource",
1563                    "custom table source must be handled by a wrapping renderer",
1564                ));
1565            }
1566        }
1567        Ok(())
1568    }
1569    fn render_joins(&self, joins: &[JoinDef], ctx: &mut RenderCtx) -> RenderResult<()> {
1570        for join in joins {
1571            if join.natural {
1572                ctx.keyword("NATURAL");
1573            }
1574            ctx.keyword(match join.join_type {
1575                JoinType::Inner => "INNER JOIN",
1576                JoinType::Left => "LEFT JOIN",
1577                JoinType::Right => "RIGHT JOIN",
1578                JoinType::Full => "FULL JOIN",
1579                JoinType::Cross => "CROSS JOIN",
1580                JoinType::CrossApply => "CROSS JOIN LATERAL",
1581                JoinType::OuterApply => "LEFT JOIN LATERAL",
1582            });
1583            self.pg_render_from_item(&join.source, ctx)?;
1584            if !matches!(join.join_type, JoinType::Cross) {
1585                if let Some(condition) = &join.condition {
1586                    match condition {
1587                        JoinCondition::On(cond) => {
1588                            ctx.keyword("ON");
1589                            self.render_condition(cond, ctx)?;
1590                        }
1591                        JoinCondition::Using(cols) => {
1592                            ctx.keyword("USING").paren_open();
1593                            self.pg_comma_idents(cols, ctx);
1594                            ctx.paren_close();
1595                        }
1596                    }
1597                }
1598            }
1599            // CrossApply/OuterApply rendered as LATERAL need ON TRUE if no condition
1600            if matches!(join.join_type, JoinType::OuterApply) && join.condition.is_none() {
1601                ctx.keyword("ON TRUE");
1602            }
1603        }
1604        Ok(())
1605    }
1606    fn render_where(&self, cond: &Conditions, ctx: &mut RenderCtx) -> RenderResult<()> {
1607        ctx.keyword("WHERE");
1608        self.render_condition(cond, ctx)
1609    }
1610    fn render_order_by(&self, order: &[OrderByDef], ctx: &mut RenderCtx) -> RenderResult<()> {
1611        ctx.keyword("ORDER BY");
1612        self.pg_order_by_list(order, ctx)
1613    }
1614    fn render_limit(&self, limit: &LimitDef, ctx: &mut RenderCtx) -> RenderResult<()> {
1615        match &limit.kind {
1616            LimitKind::Limit(n) => {
1617                ctx.keyword("LIMIT");
1618                if ctx.parameterize() {
1619                    ctx.param(Value::BigInt(*n as i64));
1620                } else {
1621                    ctx.space().write(&n.to_string());
1622                }
1623            }
1624            LimitKind::FetchFirst {
1625                count,
1626                with_ties,
1627                percent,
1628            } => {
1629                if let Some(offset) = limit.offset {
1630                    ctx.keyword("OFFSET")
1631                        .space()
1632                        .write(&offset.to_string())
1633                        .keyword("ROWS");
1634                }
1635                ctx.keyword("FETCH FIRST");
1636                if *percent {
1637                    ctx.space().write(&count.to_string()).keyword("PERCENT");
1638                } else {
1639                    ctx.space().write(&count.to_string());
1640                }
1641                if *with_ties {
1642                    ctx.keyword("ROWS WITH TIES");
1643                } else {
1644                    ctx.keyword("ROWS ONLY");
1645                }
1646                return Ok(());
1647            }
1648            LimitKind::Top { count, .. } => {
1649                // PG doesn't support TOP, convert to LIMIT
1650                ctx.keyword("LIMIT");
1651                if ctx.parameterize() {
1652                    ctx.param(Value::BigInt(*count as i64));
1653                } else {
1654                    ctx.space().write(&count.to_string());
1655                }
1656            }
1657        }
1658        if let Some(offset) = limit.offset {
1659            ctx.keyword("OFFSET");
1660            if ctx.parameterize() {
1661                ctx.param(Value::BigInt(offset as i64));
1662            } else {
1663                ctx.space().write(&offset.to_string());
1664            }
1665        }
1666        Ok(())
1667    }
1668    fn render_ctes(&self, ctes: &[CteDef], ctx: &mut RenderCtx) -> RenderResult<()> {
1669        // Check if any CTE is recursive — PG uses WITH RECURSIVE once for all.
1670        let any_recursive = ctes.iter().any(|c| c.recursive);
1671        ctx.keyword("WITH");
1672        if any_recursive {
1673            ctx.keyword("RECURSIVE");
1674        }
1675        for (i, cte) in ctes.iter().enumerate() {
1676            if i > 0 {
1677                ctx.comma();
1678            }
1679            ctx.ident(&cte.name);
1680            if let Some(col_names) = &cte.column_names {
1681                ctx.paren_open();
1682                self.pg_comma_idents(col_names, ctx);
1683                ctx.paren_close();
1684            }
1685            ctx.keyword("AS");
1686            if let Some(mat) = &cte.materialized {
1687                match mat {
1688                    CteMaterialized::Materialized => {
1689                        ctx.keyword("MATERIALIZED");
1690                    }
1691                    CteMaterialized::NotMaterialized => {
1692                        ctx.keyword("NOT MATERIALIZED");
1693                    }
1694                }
1695            }
1696            ctx.paren_open();
1697            self.render_query(&cte.query, ctx)?;
1698            ctx.paren_close();
1699        }
1700        Ok(())
1701    }
1702    fn render_lock(&self, lock: &SelectLockDef, ctx: &mut RenderCtx) -> RenderResult<()> {
1703        ctx.keyword("FOR");
1704        ctx.keyword(match lock.strength {
1705            LockStrength::Update => "UPDATE",
1706            LockStrength::NoKeyUpdate => "NO KEY UPDATE",
1707            LockStrength::Share => "SHARE",
1708            LockStrength::KeyShare => "KEY SHARE",
1709        });
1710        if let Some(of) = &lock.of {
1711            ctx.keyword("OF");
1712            for (i, table) in of.iter().enumerate() {
1713                if i > 0 {
1714                    ctx.comma();
1715                }
1716                self.pg_schema_ref(table, ctx);
1717            }
1718        }
1719        if lock.nowait {
1720            ctx.keyword("NOWAIT");
1721        }
1722        if lock.skip_locked {
1723            ctx.keyword("SKIP LOCKED");
1724        }
1725        Ok(())
1726    }
1727
1728    // ── DML ──────────────────────────────────────────────────────────────
1729
1730    fn render_mutation(&self, stmt: &MutationStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1731        match stmt {
1732            MutationStmt::Insert(s) => self.render_insert(s, ctx),
1733            MutationStmt::Update(s) => self.render_update(s, ctx),
1734            MutationStmt::Delete(s) => self.render_delete(s, ctx),
1735            MutationStmt::Custom(_) => Err(RenderError::unsupported(
1736                "CustomMutation",
1737                "custom DML must be handled by a wrapping renderer",
1738            )),
1739        }
1740    }
1741
1742    fn render_insert(&self, stmt: &InsertStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1743        // CTEs
1744        if let Some(ctes) = &stmt.ctes {
1745            self.pg_render_ctes(ctes, ctx)?;
1746        }
1747
1748        ctx.keyword("INSERT INTO");
1749        self.pg_schema_ref(&stmt.table, ctx);
1750
1751        // Column list
1752        if let Some(cols) = &stmt.columns {
1753            ctx.paren_open();
1754            self.pg_comma_idents(cols, ctx);
1755            ctx.paren_close();
1756        }
1757
1758        // OVERRIDING
1759        if let Some(overriding) = &stmt.overriding {
1760            ctx.keyword(match overriding {
1761                OverridingKind::System => "OVERRIDING SYSTEM VALUE",
1762                OverridingKind::User => "OVERRIDING USER VALUE",
1763            });
1764        }
1765
1766        // Source
1767        match &stmt.source {
1768            InsertSource::Values(rows) => {
1769                ctx.keyword("VALUES");
1770                for (i, row) in rows.iter().enumerate() {
1771                    if i > 0 {
1772                        ctx.comma();
1773                    }
1774                    ctx.paren_open();
1775                    for (j, expr) in row.iter().enumerate() {
1776                        if j > 0 {
1777                            ctx.comma();
1778                        }
1779                        self.render_expr(expr, ctx)?;
1780                    }
1781                    ctx.paren_close();
1782                }
1783            }
1784            InsertSource::Select(query) => {
1785                self.render_query(query, ctx)?;
1786            }
1787            InsertSource::DefaultValues => {
1788                ctx.keyword("DEFAULT VALUES");
1789            }
1790        }
1791
1792        // ON CONFLICT
1793        if let Some(conflicts) = &stmt.on_conflict {
1794            for oc in conflicts {
1795                self.render_on_conflict(oc, ctx)?;
1796            }
1797        }
1798
1799        // RETURNING
1800        if let Some(returning) = &stmt.returning {
1801            self.render_returning(returning, ctx)?;
1802        }
1803
1804        Ok(())
1805    }
1806
1807    fn render_update(&self, stmt: &UpdateStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1808        // CTEs
1809        if let Some(ctes) = &stmt.ctes {
1810            self.pg_render_ctes(ctes, ctx)?;
1811        }
1812
1813        ctx.keyword("UPDATE");
1814
1815        // ONLY
1816        if stmt.only {
1817            ctx.keyword("ONLY");
1818        }
1819
1820        self.pg_schema_ref(&stmt.table, ctx);
1821
1822        // Alias
1823        if let Some(alias) = &stmt.table.alias {
1824            ctx.keyword("AS").ident(alias);
1825        }
1826
1827        // SET
1828        ctx.keyword("SET");
1829        for (i, (col, expr)) in stmt.assignments.iter().enumerate() {
1830            if i > 0 {
1831                ctx.comma();
1832            }
1833            ctx.ident(col).write(" = ");
1834            self.render_expr(expr, ctx)?;
1835        }
1836
1837        // FROM
1838        if let Some(from) = &stmt.from {
1839            ctx.keyword("FROM");
1840            for (i, source) in from.iter().enumerate() {
1841                if i > 0 {
1842                    ctx.comma();
1843                }
1844                self.render_from(source, ctx)?;
1845            }
1846        }
1847
1848        // WHERE
1849        if let Some(cond) = &stmt.where_clause {
1850            ctx.keyword("WHERE");
1851            self.render_condition(cond, ctx)?;
1852        }
1853
1854        // RETURNING
1855        if let Some(returning) = &stmt.returning {
1856            self.render_returning(returning, ctx)?;
1857        }
1858
1859        Ok(())
1860    }
1861
1862    fn render_delete(&self, stmt: &DeleteStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1863        // CTEs
1864        if let Some(ctes) = &stmt.ctes {
1865            self.pg_render_ctes(ctes, ctx)?;
1866        }
1867
1868        ctx.keyword("DELETE FROM");
1869
1870        // ONLY
1871        if stmt.only {
1872            ctx.keyword("ONLY");
1873        }
1874
1875        self.pg_schema_ref(&stmt.table, ctx);
1876
1877        // Alias
1878        if let Some(alias) = &stmt.table.alias {
1879            ctx.keyword("AS").ident(alias);
1880        }
1881
1882        // USING
1883        if let Some(using) = &stmt.using {
1884            ctx.keyword("USING");
1885            for (i, source) in using.iter().enumerate() {
1886                if i > 0 {
1887                    ctx.comma();
1888                }
1889                self.render_from(source, ctx)?;
1890            }
1891        }
1892
1893        // WHERE
1894        if let Some(cond) = &stmt.where_clause {
1895            ctx.keyword("WHERE");
1896            self.render_condition(cond, ctx)?;
1897        }
1898
1899        // RETURNING
1900        if let Some(returning) = &stmt.returning {
1901            self.render_returning(returning, ctx)?;
1902        }
1903
1904        Ok(())
1905    }
1906
1907    fn render_on_conflict(&self, oc: &OnConflictDef, ctx: &mut RenderCtx) -> RenderResult<()> {
1908        ctx.keyword("ON CONFLICT");
1909
1910        // Target
1911        if let Some(target) = &oc.target {
1912            match target {
1913                ConflictTarget::Columns {
1914                    columns,
1915                    where_clause,
1916                } => {
1917                    ctx.paren_open();
1918                    self.pg_comma_idents(columns, ctx);
1919                    ctx.paren_close();
1920                    if let Some(cond) = where_clause {
1921                        ctx.keyword("WHERE");
1922                        self.render_condition(cond, ctx)?;
1923                    }
1924                }
1925                ConflictTarget::Constraint(name) => {
1926                    ctx.keyword("ON CONSTRAINT").ident(name);
1927                }
1928            }
1929        }
1930
1931        // Action
1932        match &oc.action {
1933            ConflictAction::DoNothing => {
1934                ctx.keyword("DO NOTHING");
1935            }
1936            ConflictAction::DoUpdate {
1937                assignments,
1938                where_clause,
1939            } => {
1940                ctx.keyword("DO UPDATE SET");
1941                for (i, (col, expr)) in assignments.iter().enumerate() {
1942                    if i > 0 {
1943                        ctx.comma();
1944                    }
1945                    ctx.ident(col).write(" = ");
1946                    self.render_expr(expr, ctx)?;
1947                }
1948                if let Some(cond) = where_clause {
1949                    ctx.keyword("WHERE");
1950                    self.render_condition(cond, ctx)?;
1951                }
1952            }
1953        }
1954
1955        Ok(())
1956    }
1957
1958    fn render_returning(&self, cols: &[SelectColumn], ctx: &mut RenderCtx) -> RenderResult<()> {
1959        ctx.keyword("RETURNING");
1960        for (i, col) in cols.iter().enumerate() {
1961            if i > 0 {
1962                ctx.comma();
1963            }
1964            match col {
1965                SelectColumn::Star(None) => {
1966                    ctx.keyword("*");
1967                }
1968                SelectColumn::Star(Some(table)) => {
1969                    ctx.ident(table).operator(".").keyword("*");
1970                }
1971                SelectColumn::Expr { expr, alias } => {
1972                    self.render_expr(expr, ctx)?;
1973                    if let Some(a) = alias {
1974                        ctx.keyword("AS").ident(a);
1975                    }
1976                }
1977                SelectColumn::Field { field, alias } => {
1978                    self.pg_field_ref(field, ctx);
1979                    if let Some(a) = alias {
1980                        ctx.keyword("AS").ident(a);
1981                    }
1982                }
1983            }
1984        }
1985        Ok(())
1986    }
1987
1988    // ── TCL ──────────────────────────────────────────────────────────────
1989
1990    fn render_transaction(&self, stmt: &TransactionStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1991        match stmt {
1992            TransactionStmt::Begin(s) => self.pg_begin(s, ctx),
1993            TransactionStmt::Commit(s) => self.pg_commit(s, ctx),
1994            TransactionStmt::Rollback(s) => self.pg_rollback(s, ctx),
1995            TransactionStmt::Savepoint(s) => {
1996                ctx.keyword("SAVEPOINT").ident(&s.name);
1997                Ok(())
1998            }
1999            TransactionStmt::ReleaseSavepoint(s) => {
2000                ctx.keyword("RELEASE").keyword("SAVEPOINT").ident(&s.name);
2001                Ok(())
2002            }
2003            TransactionStmt::SetTransaction(s) => self.pg_set_transaction(s, ctx),
2004            TransactionStmt::LockTable(s) => self.pg_lock_table(s, ctx),
2005            TransactionStmt::PrepareTransaction(s) => {
2006                ctx.keyword("PREPARE")
2007                    .keyword("TRANSACTION")
2008                    .string_literal(&s.transaction_id);
2009                Ok(())
2010            }
2011            TransactionStmt::CommitPrepared(s) => {
2012                ctx.keyword("COMMIT")
2013                    .keyword("PREPARED")
2014                    .string_literal(&s.transaction_id);
2015                Ok(())
2016            }
2017            TransactionStmt::RollbackPrepared(s) => {
2018                ctx.keyword("ROLLBACK")
2019                    .keyword("PREPARED")
2020                    .string_literal(&s.transaction_id);
2021                Ok(())
2022            }
2023            TransactionStmt::Custom(_) => Err(RenderError::unsupported(
2024                "Custom TCL",
2025                "not supported by PostgresRenderer",
2026            )),
2027        }
2028    }
2029}
2030
2031// ==========================================================================
2032// PostgreSQL-specific helpers
2033// ==========================================================================
2034
2035impl PostgresRenderer {
2036    // ── TCL helpers ──────────────────────────────────────────────────────
2037
2038    fn pg_begin(&self, stmt: &BeginStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
2039        ctx.keyword("BEGIN");
2040        if let Some(modes) = &stmt.modes {
2041            self.pg_transaction_modes(modes, ctx);
2042        }
2043        Ok(())
2044    }
2045
2046    fn pg_commit(&self, stmt: &CommitStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
2047        ctx.keyword("COMMIT");
2048        if stmt.and_chain {
2049            ctx.keyword("AND").keyword("CHAIN");
2050        }
2051        Ok(())
2052    }
2053
2054    fn pg_rollback(&self, stmt: &RollbackStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
2055        ctx.keyword("ROLLBACK");
2056        if let Some(sp) = &stmt.to_savepoint {
2057            ctx.keyword("TO").keyword("SAVEPOINT").ident(sp);
2058        }
2059        if stmt.and_chain {
2060            ctx.keyword("AND").keyword("CHAIN");
2061        }
2062        Ok(())
2063    }
2064
2065    fn pg_set_transaction(
2066        &self,
2067        stmt: &SetTransactionStmt,
2068        ctx: &mut RenderCtx,
2069    ) -> RenderResult<()> {
2070        ctx.keyword("SET");
2071        match &stmt.scope {
2072            Some(TransactionScope::Session) => {
2073                ctx.keyword("SESSION")
2074                    .keyword("CHARACTERISTICS")
2075                    .keyword("AS")
2076                    .keyword("TRANSACTION");
2077            }
2078            _ => {
2079                ctx.keyword("TRANSACTION");
2080            }
2081        }
2082        if let Some(snap_id) = &stmt.snapshot_id {
2083            ctx.keyword("SNAPSHOT").string_literal(snap_id);
2084        } else {
2085            self.pg_transaction_modes(&stmt.modes, ctx);
2086        }
2087        Ok(())
2088    }
2089
2090    fn pg_transaction_modes(&self, modes: &[TransactionMode], ctx: &mut RenderCtx) {
2091        for (i, mode) in modes.iter().enumerate() {
2092            if i > 0 {
2093                ctx.comma();
2094            }
2095            match mode {
2096                TransactionMode::IsolationLevel(lvl) => {
2097                    ctx.keyword("ISOLATION").keyword("LEVEL");
2098                    ctx.keyword(match lvl {
2099                        IsolationLevel::ReadUncommitted => "READ UNCOMMITTED",
2100                        IsolationLevel::ReadCommitted => "READ COMMITTED",
2101                        IsolationLevel::RepeatableRead => "REPEATABLE READ",
2102                        IsolationLevel::Serializable => "SERIALIZABLE",
2103                        IsolationLevel::Snapshot => "SERIALIZABLE", // PG doesn't have SNAPSHOT
2104                    });
2105                }
2106                TransactionMode::ReadOnly => {
2107                    ctx.keyword("READ ONLY");
2108                }
2109                TransactionMode::ReadWrite => {
2110                    ctx.keyword("READ WRITE");
2111                }
2112                TransactionMode::Deferrable => {
2113                    ctx.keyword("DEFERRABLE");
2114                }
2115                TransactionMode::NotDeferrable => {
2116                    ctx.keyword("NOT DEFERRABLE");
2117                }
2118                TransactionMode::WithConsistentSnapshot => {} // MySQL only, skip
2119            }
2120        }
2121    }
2122
2123    fn pg_lock_table(&self, stmt: &LockTableStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
2124        ctx.keyword("LOCK").keyword("TABLE");
2125        for (i, def) in stmt.tables.iter().enumerate() {
2126            if i > 0 {
2127                ctx.comma();
2128            }
2129            if def.only {
2130                ctx.keyword("ONLY");
2131            }
2132            if let Some(schema) = &def.schema {
2133                ctx.ident(schema).operator(".");
2134            }
2135            ctx.ident(&def.table);
2136        }
2137        // Use mode from first table (PG applies one mode to all).
2138        if let Some(first) = stmt.tables.first() {
2139            ctx.keyword("IN");
2140            ctx.keyword(match first.mode {
2141                LockMode::AccessShare => "ACCESS SHARE",
2142                LockMode::RowShare => "ROW SHARE",
2143                LockMode::RowExclusive => "ROW EXCLUSIVE",
2144                LockMode::ShareUpdateExclusive => "SHARE UPDATE EXCLUSIVE",
2145                LockMode::Share => "SHARE",
2146                LockMode::ShareRowExclusive => "SHARE ROW EXCLUSIVE",
2147                LockMode::Exclusive => "EXCLUSIVE",
2148                LockMode::AccessExclusive => "ACCESS EXCLUSIVE",
2149                _ => "ACCESS EXCLUSIVE", // Non-PG modes default
2150            });
2151            ctx.keyword("MODE");
2152        }
2153        if stmt.nowait {
2154            ctx.keyword("NOWAIT");
2155        }
2156        Ok(())
2157    }
2158
2159    // ── Schema helpers ───────────────────────────────────────────────────
2160
2161    fn pg_schema_ref(&self, schema_ref: &qcraft_core::ast::common::SchemaRef, ctx: &mut RenderCtx) {
2162        if let Some(ns) = &schema_ref.namespace {
2163            ctx.ident(ns).operator(".");
2164        }
2165        ctx.ident(&schema_ref.name);
2166    }
2167
2168    fn pg_field_ref(&self, field_ref: &FieldRef, ctx: &mut RenderCtx) {
2169        if let Some(ns) = &field_ref.namespace {
2170            ctx.ident(ns).operator(".");
2171        }
2172        if !field_ref.table_name.is_empty() {
2173            ctx.ident(&field_ref.table_name).operator(".");
2174        }
2175        ctx.ident(&field_ref.field.name);
2176        let mut child = &field_ref.field.child;
2177        while let Some(c) = child {
2178            ctx.operator("->'")
2179                .write(&c.name.replace('\'', "''"))
2180                .write("'");
2181            child = &c.child;
2182        }
2183    }
2184
2185    fn pg_comma_idents(&self, names: &[String], ctx: &mut RenderCtx) {
2186        for (i, name) in names.iter().enumerate() {
2187            if i > 0 {
2188                ctx.comma();
2189            }
2190            ctx.ident(name);
2191        }
2192    }
2193
2194    fn pg_value(&self, val: &Value, ctx: &mut RenderCtx) -> RenderResult<()> {
2195        // In parameterized mode, NULL goes as a bind parameter (drivers handle
2196        // it correctly via the extended query protocol). Only IS NULL
2197        // comparisons need the keyword — that path is handled separately in
2198        // render_compare_op.
2199        if matches!(val, Value::Null) && !ctx.parameterize() {
2200            ctx.keyword("NULL");
2201            return Ok(());
2202        }
2203
2204        // In parameterized mode, send values as bind parameters (no casts —
2205        // the driver transmits types via the binary protocol and PG infers
2206        // from column context).
2207        if ctx.parameterize() {
2208            ctx.param(val.clone());
2209            return Ok(());
2210        }
2211
2212        // Inline literal mode (DDL defaults, TCL, etc.)
2213        self.pg_value_literal(val, ctx)
2214    }
2215
2216    fn pg_value_literal(&self, val: &Value, ctx: &mut RenderCtx) -> RenderResult<()> {
2217        match val {
2218            Value::Null => {
2219                ctx.keyword("NULL");
2220            }
2221            Value::Bool(b) => {
2222                ctx.keyword(if *b { "TRUE" } else { "FALSE" });
2223            }
2224            Value::Int(n) | Value::BigInt(n) => {
2225                ctx.keyword(&n.to_string());
2226            }
2227            Value::Float(f) => {
2228                ctx.keyword(&f.to_string());
2229            }
2230            Value::Str(s) => {
2231                ctx.string_literal(s);
2232            }
2233            Value::Bytes(b) => {
2234                ctx.write("'\\x");
2235                for byte in b {
2236                    ctx.write(&format!("{byte:02x}"));
2237                }
2238                ctx.write("'");
2239            }
2240            Value::Date(s) | Value::DateTime(s) | Value::Time(s) => {
2241                ctx.string_literal(s);
2242            }
2243            Value::Decimal(s) => {
2244                ctx.keyword(s);
2245            }
2246            Value::Uuid(s) => {
2247                ctx.string_literal(s);
2248            }
2249            Value::Json(s) => {
2250                ctx.string_literal(s);
2251                ctx.write("::json");
2252            }
2253            Value::Jsonb(s) => {
2254                ctx.string_literal(s);
2255                ctx.write("::jsonb");
2256            }
2257            Value::IpNetwork(s) => {
2258                ctx.string_literal(s);
2259                ctx.write("::inet");
2260            }
2261            Value::Array(items) => {
2262                ctx.keyword("ARRAY").write("[");
2263                for (i, item) in items.iter().enumerate() {
2264                    if i > 0 {
2265                        ctx.comma();
2266                    }
2267                    self.pg_value_literal(item, ctx)?;
2268                }
2269                ctx.write("]");
2270            }
2271            Value::Vector(values) => {
2272                let parts: Vec<String> = values.iter().map(|v| v.to_string()).collect();
2273                let literal = format!("[{}]", parts.join(","));
2274                ctx.string_literal(&literal);
2275                ctx.write("::vector");
2276            }
2277            Value::TimeDelta {
2278                years,
2279                months,
2280                days,
2281                seconds,
2282                microseconds,
2283            } => {
2284                ctx.keyword("INTERVAL");
2285                let mut parts = Vec::new();
2286                if *years != 0 {
2287                    parts.push(format!("{years} years"));
2288                }
2289                if *months != 0 {
2290                    parts.push(format!("{months} months"));
2291                }
2292                if *days != 0 {
2293                    parts.push(format!("{days} days"));
2294                }
2295                if *seconds != 0 {
2296                    parts.push(format!("{seconds} seconds"));
2297                }
2298                if *microseconds != 0 {
2299                    parts.push(format!("{microseconds} microseconds"));
2300                }
2301                if parts.is_empty() {
2302                    parts.push("0 seconds".into());
2303                }
2304                ctx.string_literal(&parts.join(" "));
2305            }
2306        }
2307        Ok(())
2308    }
2309
2310    fn pg_referential_action(&self, action: &ReferentialAction, ctx: &mut RenderCtx) {
2311        match action {
2312            ReferentialAction::NoAction => {
2313                ctx.keyword("NO ACTION");
2314            }
2315            ReferentialAction::Restrict => {
2316                ctx.keyword("RESTRICT");
2317            }
2318            ReferentialAction::Cascade => {
2319                ctx.keyword("CASCADE");
2320            }
2321            ReferentialAction::SetNull(cols) => {
2322                ctx.keyword("SET NULL");
2323                if let Some(cols) = cols {
2324                    ctx.paren_open();
2325                    self.pg_comma_idents(cols, ctx);
2326                    ctx.paren_close();
2327                }
2328            }
2329            ReferentialAction::SetDefault(cols) => {
2330                ctx.keyword("SET DEFAULT");
2331                if let Some(cols) = cols {
2332                    ctx.paren_open();
2333                    self.pg_comma_idents(cols, ctx);
2334                    ctx.paren_close();
2335                }
2336            }
2337        }
2338    }
2339
2340    fn pg_deferrable(&self, def: &DeferrableConstraint, ctx: &mut RenderCtx) {
2341        if def.deferrable {
2342            ctx.keyword("DEFERRABLE");
2343        } else {
2344            ctx.keyword("NOT DEFERRABLE");
2345        }
2346        if def.initially_deferred {
2347            ctx.keyword("INITIALLY DEFERRED");
2348        } else {
2349            ctx.keyword("INITIALLY IMMEDIATE");
2350        }
2351    }
2352
2353    fn pg_identity(&self, identity: &IdentityColumn, ctx: &mut RenderCtx) {
2354        if identity.always {
2355            ctx.keyword("GENERATED ALWAYS AS IDENTITY");
2356        } else {
2357            ctx.keyword("GENERATED BY DEFAULT AS IDENTITY");
2358        }
2359        let has_options = identity.start.is_some()
2360            || identity.increment.is_some()
2361            || identity.min_value.is_some()
2362            || identity.max_value.is_some()
2363            || identity.cycle
2364            || identity.cache.is_some();
2365        if has_options {
2366            ctx.paren_open();
2367            if let Some(start) = identity.start {
2368                ctx.keyword("START WITH").keyword(&start.to_string());
2369            }
2370            if let Some(inc) = identity.increment {
2371                ctx.keyword("INCREMENT BY").keyword(&inc.to_string());
2372            }
2373            if let Some(min) = identity.min_value {
2374                ctx.keyword("MINVALUE").keyword(&min.to_string());
2375            }
2376            if let Some(max) = identity.max_value {
2377                ctx.keyword("MAXVALUE").keyword(&max.to_string());
2378            }
2379            if identity.cycle {
2380                ctx.keyword("CYCLE");
2381            }
2382            if let Some(cache) = identity.cache {
2383                ctx.keyword("CACHE").write(&cache.to_string());
2384            }
2385            ctx.paren_close();
2386        }
2387    }
2388
2389    fn pg_render_ctes(&self, ctes: &[CteDef], ctx: &mut RenderCtx) -> RenderResult<()> {
2390        // Delegate to the trait method
2391        self.render_ctes(ctes, ctx)
2392    }
2393
2394    fn pg_render_from_item(&self, item: &FromItem, ctx: &mut RenderCtx) -> RenderResult<()> {
2395        if item.only {
2396            ctx.keyword("ONLY");
2397        }
2398        self.render_from(&item.source, ctx)?;
2399        if let Some(sample) = &item.sample {
2400            ctx.keyword("TABLESAMPLE");
2401            ctx.keyword(match sample.method {
2402                SampleMethod::Bernoulli => "BERNOULLI",
2403                SampleMethod::System => "SYSTEM",
2404                SampleMethod::Block => "SYSTEM", // Block maps to SYSTEM on PG
2405            });
2406            ctx.paren_open()
2407                .write(&sample.percentage.to_string())
2408                .paren_close();
2409            if let Some(seed) = sample.seed {
2410                ctx.keyword("REPEATABLE")
2411                    .paren_open()
2412                    .write(&seed.to_string())
2413                    .paren_close();
2414            }
2415        }
2416        Ok(())
2417    }
2418
2419    fn pg_render_group_by(&self, items: &[GroupByItem], ctx: &mut RenderCtx) -> RenderResult<()> {
2420        ctx.keyword("GROUP BY");
2421        for (i, item) in items.iter().enumerate() {
2422            if i > 0 {
2423                ctx.comma();
2424            }
2425            match item {
2426                GroupByItem::Expr(expr) => {
2427                    self.render_expr(expr, ctx)?;
2428                }
2429                GroupByItem::Rollup(exprs) => {
2430                    ctx.keyword("ROLLUP").paren_open();
2431                    for (j, expr) in exprs.iter().enumerate() {
2432                        if j > 0 {
2433                            ctx.comma();
2434                        }
2435                        self.render_expr(expr, ctx)?;
2436                    }
2437                    ctx.paren_close();
2438                }
2439                GroupByItem::Cube(exprs) => {
2440                    ctx.keyword("CUBE").paren_open();
2441                    for (j, expr) in exprs.iter().enumerate() {
2442                        if j > 0 {
2443                            ctx.comma();
2444                        }
2445                        self.render_expr(expr, ctx)?;
2446                    }
2447                    ctx.paren_close();
2448                }
2449                GroupByItem::GroupingSets(sets) => {
2450                    ctx.keyword("GROUPING SETS").paren_open();
2451                    for (j, set) in sets.iter().enumerate() {
2452                        if j > 0 {
2453                            ctx.comma();
2454                        }
2455                        ctx.paren_open();
2456                        for (k, expr) in set.iter().enumerate() {
2457                            if k > 0 {
2458                                ctx.comma();
2459                            }
2460                            self.render_expr(expr, ctx)?;
2461                        }
2462                        ctx.paren_close();
2463                    }
2464                    ctx.paren_close();
2465                }
2466            }
2467        }
2468        Ok(())
2469    }
2470
2471    fn pg_render_window_clause(
2472        &self,
2473        windows: &[WindowNameDef],
2474        ctx: &mut RenderCtx,
2475    ) -> RenderResult<()> {
2476        ctx.keyword("WINDOW");
2477        for (i, win) in windows.iter().enumerate() {
2478            if i > 0 {
2479                ctx.comma();
2480            }
2481            ctx.ident(&win.name).keyword("AS").paren_open();
2482            if let Some(base) = &win.base_window {
2483                ctx.ident(base);
2484            }
2485            if let Some(partition_by) = &win.partition_by {
2486                ctx.keyword("PARTITION BY");
2487                for (j, expr) in partition_by.iter().enumerate() {
2488                    if j > 0 {
2489                        ctx.comma();
2490                    }
2491                    self.render_expr(expr, ctx)?;
2492                }
2493            }
2494            if let Some(order_by) = &win.order_by {
2495                ctx.keyword("ORDER BY");
2496                self.pg_order_by_list(order_by, ctx)?;
2497            }
2498            if let Some(frame) = &win.frame {
2499                self.pg_window_frame(frame, ctx);
2500            }
2501            ctx.paren_close();
2502        }
2503        Ok(())
2504    }
2505
2506    fn pg_render_set_op(&self, set_op: &SetOpDef, ctx: &mut RenderCtx) -> RenderResult<()> {
2507        self.render_query(&set_op.left, ctx)?;
2508        ctx.keyword(match set_op.operation {
2509            SetOperationType::Union => "UNION",
2510            SetOperationType::UnionAll => "UNION ALL",
2511            SetOperationType::Intersect => "INTERSECT",
2512            SetOperationType::IntersectAll => "INTERSECT ALL",
2513            SetOperationType::Except => "EXCEPT",
2514            SetOperationType::ExceptAll => "EXCEPT ALL",
2515        });
2516        self.render_query(&set_op.right, ctx)
2517    }
2518
2519    fn pg_create_table(
2520        &self,
2521        schema: &SchemaDef,
2522        if_not_exists: bool,
2523        temporary: bool,
2524        unlogged: bool,
2525        opts: &PgCreateTableOpts<'_>,
2526        ctx: &mut RenderCtx,
2527    ) -> RenderResult<()> {
2528        let PgCreateTableOpts {
2529            tablespace,
2530            partition_by,
2531            inherits,
2532            using_method,
2533            with_options,
2534            on_commit,
2535        } = opts;
2536        ctx.keyword("CREATE");
2537        if temporary {
2538            ctx.keyword("TEMPORARY");
2539        }
2540        if unlogged {
2541            ctx.keyword("UNLOGGED");
2542        }
2543        ctx.keyword("TABLE");
2544        if if_not_exists {
2545            ctx.keyword("IF NOT EXISTS");
2546        }
2547        if let Some(ns) = &schema.namespace {
2548            ctx.ident(ns).operator(".");
2549        }
2550        ctx.ident(&schema.name);
2551
2552        // Columns + constraints + LIKE
2553        ctx.paren_open();
2554        let mut first = true;
2555        for col in &schema.columns {
2556            if !first {
2557                ctx.comma();
2558            }
2559            first = false;
2560            self.render_column_def(col, ctx)?;
2561        }
2562        if let Some(like_tables) = &schema.like_tables {
2563            for like in like_tables {
2564                if !first {
2565                    ctx.comma();
2566                }
2567                first = false;
2568                self.pg_like_table(like, ctx);
2569            }
2570        }
2571        if let Some(constraints) = &schema.constraints {
2572            for constraint in constraints {
2573                // Partial unique constraints are rendered as separate
2574                // CREATE UNIQUE INDEX statements, not inline.
2575                if self.is_partial_unique(constraint) {
2576                    continue;
2577                }
2578                if !first {
2579                    ctx.comma();
2580                }
2581                first = false;
2582                self.render_constraint(constraint, ctx)?;
2583            }
2584        }
2585        ctx.paren_close();
2586
2587        // INHERITS
2588        if let Some(parents) = inherits {
2589            ctx.keyword("INHERITS").paren_open();
2590            for (i, parent) in parents.iter().enumerate() {
2591                if i > 0 {
2592                    ctx.comma();
2593                }
2594                self.pg_schema_ref(parent, ctx);
2595            }
2596            ctx.paren_close();
2597        }
2598
2599        // PARTITION BY
2600        if let Some(part) = partition_by {
2601            ctx.keyword("PARTITION BY");
2602            ctx.keyword(match part.strategy {
2603                PartitionStrategy::Range => "RANGE",
2604                PartitionStrategy::List => "LIST",
2605                PartitionStrategy::Hash => "HASH",
2606            });
2607            ctx.paren_open();
2608            for (i, col) in part.columns.iter().enumerate() {
2609                if i > 0 {
2610                    ctx.comma();
2611                }
2612                match &col.expr {
2613                    IndexExpr::Column(name) => {
2614                        ctx.ident(name);
2615                    }
2616                    IndexExpr::Expression(expr) => {
2617                        ctx.paren_open();
2618                        self.render_expr(expr, ctx)?;
2619                        ctx.paren_close();
2620                    }
2621                }
2622                if let Some(collation) = &col.collation {
2623                    ctx.keyword("COLLATE").ident(collation);
2624                }
2625                if let Some(opclass) = &col.opclass {
2626                    ctx.keyword(opclass);
2627                }
2628            }
2629            ctx.paren_close();
2630        }
2631
2632        // USING method
2633        if let Some(method) = using_method {
2634            ctx.keyword("USING").keyword(method);
2635        }
2636
2637        // WITH (storage_parameter = value, ...)
2638        if let Some(opts) = with_options {
2639            ctx.keyword("WITH").paren_open();
2640            for (i, (key, value)) in opts.iter().enumerate() {
2641                if i > 0 {
2642                    ctx.comma();
2643                }
2644                ctx.write(key).write(" = ").write(value);
2645            }
2646            ctx.paren_close();
2647        }
2648
2649        // ON COMMIT
2650        if let Some(action) = on_commit {
2651            ctx.keyword("ON COMMIT");
2652            ctx.keyword(match action {
2653                OnCommitAction::PreserveRows => "PRESERVE ROWS",
2654                OnCommitAction::DeleteRows => "DELETE ROWS",
2655                OnCommitAction::Drop => "DROP",
2656            });
2657        }
2658
2659        // TABLESPACE
2660        if let Some(ts) = tablespace {
2661            ctx.keyword("TABLESPACE").ident(ts);
2662        }
2663
2664        Ok(())
2665    }
2666
2667    fn pg_like_table(&self, like: &LikeTableDef, ctx: &mut RenderCtx) {
2668        ctx.keyword("LIKE");
2669        self.pg_schema_ref(&like.source_table, ctx);
2670        for opt in &like.options {
2671            if opt.include {
2672                ctx.keyword("INCLUDING");
2673            } else {
2674                ctx.keyword("EXCLUDING");
2675            }
2676            ctx.keyword(match opt.kind {
2677                qcraft_core::ast::ddl::LikeOptionKind::Comments => "COMMENTS",
2678                qcraft_core::ast::ddl::LikeOptionKind::Compression => "COMPRESSION",
2679                qcraft_core::ast::ddl::LikeOptionKind::Constraints => "CONSTRAINTS",
2680                qcraft_core::ast::ddl::LikeOptionKind::Defaults => "DEFAULTS",
2681                qcraft_core::ast::ddl::LikeOptionKind::Generated => "GENERATED",
2682                qcraft_core::ast::ddl::LikeOptionKind::Identity => "IDENTITY",
2683                qcraft_core::ast::ddl::LikeOptionKind::Indexes => "INDEXES",
2684                qcraft_core::ast::ddl::LikeOptionKind::Statistics => "STATISTICS",
2685                qcraft_core::ast::ddl::LikeOptionKind::Storage => "STORAGE",
2686                qcraft_core::ast::ddl::LikeOptionKind::All => "ALL",
2687            });
2688        }
2689    }
2690
2691    fn pg_create_index(
2692        &self,
2693        schema_ref: &qcraft_core::ast::common::SchemaRef,
2694        index: &IndexDef,
2695        if_not_exists: bool,
2696        concurrently: bool,
2697        ctx: &mut RenderCtx,
2698    ) -> RenderResult<()> {
2699        ctx.keyword("CREATE");
2700        if index.unique {
2701            ctx.keyword("UNIQUE");
2702        }
2703        ctx.keyword("INDEX");
2704        if concurrently {
2705            ctx.keyword("CONCURRENTLY");
2706        }
2707        if if_not_exists {
2708            ctx.keyword("IF NOT EXISTS");
2709        }
2710        ctx.ident(&index.name).keyword("ON");
2711        self.pg_schema_ref(schema_ref, ctx);
2712
2713        if let Some(index_type) = &index.index_type {
2714            ctx.keyword("USING").keyword(index_type);
2715        }
2716
2717        ctx.paren_open();
2718        self.pg_index_columns(&index.columns, index.index_type.as_deref(), ctx)?;
2719        ctx.paren_close();
2720
2721        if let Some(include) = &index.include {
2722            ctx.keyword("INCLUDE").paren_open();
2723            self.pg_comma_idents(include, ctx);
2724            ctx.paren_close();
2725        }
2726
2727        if let Some(nd) = index.nulls_distinct {
2728            if !nd {
2729                ctx.keyword("NULLS NOT DISTINCT");
2730            }
2731        }
2732
2733        if let Some(params) = &index.parameters {
2734            ctx.keyword("WITH").paren_open();
2735            for (i, (key, value)) in params.iter().enumerate() {
2736                if i > 0 {
2737                    ctx.comma();
2738                }
2739                ctx.write(key).write(" = ").write(value);
2740            }
2741            ctx.paren_close();
2742        }
2743
2744        if let Some(ts) = &index.tablespace {
2745            ctx.keyword("TABLESPACE").ident(ts);
2746        }
2747
2748        if let Some(condition) = &index.condition {
2749            ctx.keyword("WHERE");
2750            self.render_condition(condition, ctx)?;
2751        }
2752
2753        Ok(())
2754    }
2755
2756    fn supports_ordering(index_type: Option<&str>) -> bool {
2757        match index_type {
2758            None => true, // default is btree
2759            Some(t) => t.eq_ignore_ascii_case("btree"),
2760        }
2761    }
2762
2763    fn pg_index_columns(
2764        &self,
2765        columns: &[IndexColumnDef],
2766        index_type: Option<&str>,
2767        ctx: &mut RenderCtx,
2768    ) -> RenderResult<()> {
2769        let ordered = Self::supports_ordering(index_type);
2770        for (i, col) in columns.iter().enumerate() {
2771            if i > 0 {
2772                ctx.comma();
2773            }
2774            match &col.expr {
2775                IndexExpr::Column(name) => {
2776                    ctx.ident(name);
2777                }
2778                IndexExpr::Expression(expr) => {
2779                    ctx.paren_open();
2780                    self.render_expr(expr, ctx)?;
2781                    ctx.paren_close();
2782                }
2783            }
2784            if let Some(collation) = &col.collation {
2785                ctx.keyword("COLLATE").ident(collation);
2786            }
2787            if let Some(opclass) = &col.opclass {
2788                ctx.keyword(opclass);
2789            }
2790            if ordered {
2791                if let Some(dir) = col.direction {
2792                    ctx.keyword(match dir {
2793                        OrderDir::Asc => "ASC",
2794                        OrderDir::Desc => "DESC",
2795                    });
2796                }
2797                if let Some(nulls) = col.nulls {
2798                    ctx.keyword(match nulls {
2799                        NullsOrder::First => "NULLS FIRST",
2800                        NullsOrder::Last => "NULLS LAST",
2801                    });
2802                }
2803            }
2804        }
2805        Ok(())
2806    }
2807
2808    fn pg_order_by_list(&self, order_by: &[OrderByDef], ctx: &mut RenderCtx) -> RenderResult<()> {
2809        for (i, ob) in order_by.iter().enumerate() {
2810            if i > 0 {
2811                ctx.comma();
2812            }
2813            self.render_expr(&ob.expr, ctx)?;
2814            ctx.keyword(match ob.direction {
2815                OrderDir::Asc => "ASC",
2816                OrderDir::Desc => "DESC",
2817            });
2818            if let Some(nulls) = &ob.nulls {
2819                ctx.keyword(match nulls {
2820                    NullsOrder::First => "NULLS FIRST",
2821                    NullsOrder::Last => "NULLS LAST",
2822                });
2823            }
2824        }
2825        Ok(())
2826    }
2827
2828    fn pg_window_frame(&self, frame: &WindowFrameDef, ctx: &mut RenderCtx) {
2829        ctx.keyword(match frame.frame_type {
2830            WindowFrameType::Rows => "ROWS",
2831            WindowFrameType::Range => "RANGE",
2832            WindowFrameType::Groups => "GROUPS",
2833        });
2834        if let Some(end) = &frame.end {
2835            ctx.keyword("BETWEEN");
2836            self.pg_frame_bound(&frame.start, ctx);
2837            ctx.keyword("AND");
2838            self.pg_frame_bound(end, ctx);
2839        } else {
2840            self.pg_frame_bound(&frame.start, ctx);
2841        }
2842    }
2843
2844    fn pg_frame_bound(&self, bound: &WindowFrameBound, ctx: &mut RenderCtx) {
2845        match bound {
2846            WindowFrameBound::CurrentRow => {
2847                ctx.keyword("CURRENT ROW");
2848            }
2849            WindowFrameBound::Preceding(None) => {
2850                ctx.keyword("UNBOUNDED PRECEDING");
2851            }
2852            WindowFrameBound::Preceding(Some(n)) => {
2853                ctx.keyword(&n.to_string()).keyword("PRECEDING");
2854            }
2855            WindowFrameBound::Following(None) => {
2856                ctx.keyword("UNBOUNDED FOLLOWING");
2857            }
2858            WindowFrameBound::Following(Some(n)) => {
2859                ctx.keyword(&n.to_string()).keyword("FOLLOWING");
2860            }
2861        }
2862    }
2863}