Skip to main content

qcraft_postgres/
lib.rs

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