Skip to main content

qcraft_sqlite/
lib.rs

1use qcraft_core::ast::common::{FieldRef, NullsOrder, OrderByDef, OrderDir};
2use qcraft_core::ast::conditions::{CompareOp, ConditionNode, Conditions, Connector};
3use qcraft_core::ast::ddl::{
4    ColumnDef, ConstraintDef, DeferrableConstraint, FieldType, IndexColumnDef, IndexDef, IndexExpr,
5    ReferentialAction, SchemaDef, SchemaMutationStmt,
6};
7use qcraft_core::ast::dml::{
8    ConflictAction, ConflictResolution, ConflictTarget, DeleteStmt, InsertSource, InsertStmt,
9    MutationStmt, OnConflictDef, UpdateStmt,
10};
11use qcraft_core::ast::expr::{
12    AggregationDef, BinaryOp, CaseDef, Expr, UnaryOp, WindowDef, WindowFrameBound, WindowFrameDef,
13    WindowFrameType,
14};
15use qcraft_core::ast::query::{
16    CteDef, DistinctDef, FromItem, GroupByItem, JoinCondition, JoinDef, JoinType, LimitDef,
17    LimitKind, QueryStmt, SelectColumn, SelectLockDef, SetOpDef, SetOperationType, SqliteIndexHint,
18    TableSource, WindowNameDef,
19};
20use qcraft_core::ast::tcl::{SqliteLockType, TransactionStmt};
21use qcraft_core::ast::value::Value;
22use qcraft_core::error::{RenderError, RenderResult};
23use qcraft_core::render::ctx::{ParamStyle, RenderCtx};
24use qcraft_core::render::escape_like_value;
25use qcraft_core::render::renderer::Renderer;
26
27fn render_like_pattern(op: &CompareOp, right: &Expr, ctx: &mut RenderCtx) -> RenderResult<()> {
28    let raw = match right {
29        Expr::Value(Value::Str(s)) => s.as_str(),
30        _ => {
31            return Err(RenderError::unsupported(
32                "CompareOp",
33                "Contains/StartsWith/EndsWith require a string value on the right side",
34            ));
35        }
36    };
37    let escaped = escape_like_value(raw);
38    let pattern = match op {
39        CompareOp::Contains | CompareOp::IContains => format!("%{escaped}%"),
40        CompareOp::StartsWith | CompareOp::IStartsWith => format!("{escaped}%"),
41        CompareOp::EndsWith | CompareOp::IEndsWith => format!("%{escaped}"),
42        _ => unreachable!(),
43    };
44    if ctx.parameterize() {
45        ctx.param(Value::Str(pattern));
46    } else {
47        ctx.string_literal(&pattern);
48    }
49    Ok(())
50}
51
52pub struct SqliteRenderer;
53
54impl SqliteRenderer {
55    pub fn new() -> Self {
56        Self
57    }
58
59    pub fn render_schema_stmt(
60        &self,
61        stmt: &SchemaMutationStmt,
62    ) -> RenderResult<Vec<(String, Vec<Value>)>> {
63        let mut ctx = RenderCtx::new(ParamStyle::QMark);
64        self.render_schema_mutation(stmt, &mut ctx)?;
65        Ok(vec![ctx.finish()])
66    }
67
68    pub fn render_transaction_stmt(
69        &self,
70        stmt: &TransactionStmt,
71    ) -> RenderResult<(String, Vec<Value>)> {
72        let mut ctx = RenderCtx::new(ParamStyle::QMark);
73        self.render_transaction(stmt, &mut ctx)?;
74        Ok(ctx.finish())
75    }
76
77    pub fn render_mutation_stmt(&self, stmt: &MutationStmt) -> RenderResult<(String, Vec<Value>)> {
78        let mut ctx = RenderCtx::new(ParamStyle::QMark).with_parameterize(true);
79        self.render_mutation(stmt, &mut ctx)?;
80        Ok(ctx.finish())
81    }
82
83    pub fn render_query_stmt(&self, stmt: &QueryStmt) -> RenderResult<(String, Vec<Value>)> {
84        let mut ctx = RenderCtx::new(ParamStyle::QMark).with_parameterize(true);
85        self.render_query(stmt, &mut ctx)?;
86        Ok(ctx.finish())
87    }
88}
89
90impl Default for SqliteRenderer {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96// ==========================================================================
97// Renderer trait implementation
98// ==========================================================================
99
100impl Renderer for SqliteRenderer {
101    // ── DDL ──────────────────────────────────────────────────────────────
102
103    fn render_schema_mutation(
104        &self,
105        stmt: &SchemaMutationStmt,
106        ctx: &mut RenderCtx,
107    ) -> RenderResult<()> {
108        match stmt {
109            SchemaMutationStmt::CreateTable {
110                schema,
111                if_not_exists,
112                temporary,
113                unlogged: _,
114                tablespace: _,
115                partition_by: _,  // SQLite doesn't support PARTITION BY
116                inherits: _,      // SQLite doesn't support INHERITS
117                using_method: _,  // SQLite doesn't support USING method
118                with_options: _,  // SQLite doesn't support WITH options
119                on_commit: _,     // SQLite doesn't support ON COMMIT
120                table_options: _, // SQLite doesn't support generic table options
121                without_rowid,
122                strict,
123            } => self.sqlite_create_table(
124                schema,
125                *if_not_exists,
126                *temporary,
127                *without_rowid,
128                *strict,
129                ctx,
130            ),
131
132            SchemaMutationStmt::DropTable {
133                schema_ref,
134                if_exists,
135                cascade: _, // SQLite doesn't support CASCADE — Ignore
136            } => {
137                ctx.keyword("DROP TABLE");
138                if *if_exists {
139                    ctx.keyword("IF EXISTS");
140                }
141                self.sqlite_schema_ref(schema_ref, ctx);
142                Ok(())
143            }
144
145            SchemaMutationStmt::RenameTable {
146                schema_ref,
147                new_name,
148            } => {
149                ctx.keyword("ALTER TABLE");
150                self.sqlite_schema_ref(schema_ref, ctx);
151                ctx.keyword("RENAME TO").ident(new_name);
152                Ok(())
153            }
154
155            SchemaMutationStmt::TruncateTable {
156                schema_ref,
157                restart_identity: _, // SQLite doesn't have RESTART IDENTITY
158                cascade: _,          // SQLite doesn't support CASCADE
159            } => {
160                // SQLite has no TRUNCATE — use DELETE FROM (equivalent semantics)
161                ctx.keyword("DELETE FROM");
162                self.sqlite_schema_ref(schema_ref, ctx);
163                Ok(())
164            }
165
166            SchemaMutationStmt::AddColumn {
167                schema_ref,
168                column,
169                if_not_exists: _, // SQLite ADD COLUMN doesn't support IF NOT EXISTS
170                position: _,      // SQLite doesn't support FIRST/AFTER
171            } => {
172                ctx.keyword("ALTER TABLE");
173                self.sqlite_schema_ref(schema_ref, ctx);
174                ctx.keyword("ADD COLUMN");
175                self.render_column_def(column, ctx)
176            }
177
178            SchemaMutationStmt::DropColumn {
179                schema_ref,
180                name,
181                if_exists: _, // SQLite DROP COLUMN doesn't support IF EXISTS
182                cascade: _,   // SQLite doesn't support CASCADE
183            } => {
184                ctx.keyword("ALTER TABLE");
185                self.sqlite_schema_ref(schema_ref, ctx);
186                ctx.keyword("DROP COLUMN").ident(name);
187                Ok(())
188            }
189
190            SchemaMutationStmt::RenameColumn {
191                schema_ref,
192                old_name,
193                new_name,
194            } => {
195                ctx.keyword("ALTER TABLE");
196                self.sqlite_schema_ref(schema_ref, ctx);
197                ctx.keyword("RENAME COLUMN")
198                    .ident(old_name)
199                    .keyword("TO")
200                    .ident(new_name);
201                Ok(())
202            }
203
204            // SQLite does NOT support these ALTER operations — Error
205            SchemaMutationStmt::AlterColumnType { .. } => Err(RenderError::unsupported(
206                "AlterColumnType",
207                "SQLite does not support ALTER COLUMN TYPE. Use the 12-step table rebuild procedure.",
208            )),
209            SchemaMutationStmt::AlterColumnDefault { .. } => Err(RenderError::unsupported(
210                "AlterColumnDefault",
211                "SQLite does not support ALTER COLUMN DEFAULT. Use the 12-step table rebuild procedure.",
212            )),
213            SchemaMutationStmt::AlterColumnNullability { .. } => Err(RenderError::unsupported(
214                "AlterColumnNullability",
215                "SQLite does not support ALTER COLUMN NOT NULL. Use the 12-step table rebuild procedure.",
216            )),
217            SchemaMutationStmt::AddConstraint { .. } => Err(RenderError::unsupported(
218                "AddConstraint",
219                "SQLite does not support ADD CONSTRAINT. Use the 12-step table rebuild procedure.",
220            )),
221            SchemaMutationStmt::DropConstraint { .. } => Err(RenderError::unsupported(
222                "DropConstraint",
223                "SQLite does not support DROP CONSTRAINT. Use the 12-step table rebuild procedure.",
224            )),
225            SchemaMutationStmt::RenameConstraint { .. } => Err(RenderError::unsupported(
226                "RenameConstraint",
227                "SQLite does not support RENAME CONSTRAINT.",
228            )),
229            SchemaMutationStmt::ValidateConstraint { .. } => Err(RenderError::unsupported(
230                "ValidateConstraint",
231                "SQLite does not support VALIDATE CONSTRAINT.",
232            )),
233
234            // ── Index operations ──
235            SchemaMutationStmt::CreateIndex {
236                schema_ref,
237                index,
238                if_not_exists,
239                concurrently: _, // SQLite doesn't support CONCURRENTLY — Ignore
240            } => self.sqlite_create_index(schema_ref, index, *if_not_exists, ctx),
241
242            SchemaMutationStmt::DropIndex {
243                schema_ref: _,
244                index_name,
245                if_exists,
246                concurrently: _, // Ignore
247                cascade: _,      // Ignore
248            } => {
249                ctx.keyword("DROP INDEX");
250                if *if_exists {
251                    ctx.keyword("IF EXISTS");
252                }
253                ctx.ident(index_name);
254                Ok(())
255            }
256
257            // SQLite doesn't have extensions
258            SchemaMutationStmt::CreateExtension { .. } => Err(RenderError::unsupported(
259                "CreateExtension",
260                "SQLite does not support extensions.",
261            )),
262            SchemaMutationStmt::DropExtension { .. } => Err(RenderError::unsupported(
263                "DropExtension",
264                "SQLite does not support extensions.",
265            )),
266
267            SchemaMutationStmt::CreateCollation { .. } => Err(RenderError::unsupported(
268                "CreateCollation",
269                "SQLite does not support CREATE COLLATION. Use sqlite3_create_collation() C API instead.",
270            )),
271            SchemaMutationStmt::DropCollation { .. } => Err(RenderError::unsupported(
272                "DropCollation",
273                "SQLite does not support DROP COLLATION.",
274            )),
275
276            SchemaMutationStmt::Custom(_) => Err(RenderError::unsupported(
277                "CustomSchemaMutation",
278                "custom DDL must be handled by a wrapping renderer",
279            )),
280        }
281    }
282
283    fn render_column_def(&self, col: &ColumnDef, ctx: &mut RenderCtx) -> RenderResult<()> {
284        ctx.ident(&col.name);
285        self.render_column_type(&col.field_type, ctx)?;
286
287        if let Some(collation) = &col.collation {
288            ctx.keyword("COLLATE").ident(collation);
289        }
290
291        if col.not_null {
292            ctx.keyword("NOT NULL");
293        }
294
295        if let Some(default) = &col.default {
296            ctx.keyword("DEFAULT").paren_open();
297            self.render_expr(default, ctx)?;
298            ctx.paren_close();
299        }
300
301        // Identity is handled at CREATE TABLE level (rendered as PRIMARY KEY AUTOINCREMENT inline)
302        // Nothing to render here — just skip
303
304        if let Some(generated) = &col.generated {
305            ctx.keyword("GENERATED ALWAYS AS").space().paren_open();
306            // SQLite generated columns only allow unqualified column names
307            self.render_expr_unqualified(&generated.expr, ctx)?;
308            ctx.paren_close();
309            if generated.stored {
310                ctx.keyword("STORED");
311            } else {
312                ctx.keyword("VIRTUAL");
313            }
314        }
315
316        Ok(())
317    }
318
319    fn render_column_type(&self, ty: &FieldType, ctx: &mut RenderCtx) -> RenderResult<()> {
320        match ty {
321            FieldType::Scalar(name) => {
322                ctx.keyword(name);
323            }
324            FieldType::Parameterized { name, params } => {
325                ctx.keyword(name).write("(");
326                for (i, p) in params.iter().enumerate() {
327                    if i > 0 {
328                        ctx.comma();
329                    }
330                    ctx.write(p);
331                }
332                ctx.paren_close();
333            }
334            FieldType::Array(_) => {
335                return Err(RenderError::unsupported(
336                    "ArrayType",
337                    "SQLite does not support array types.",
338                ));
339            }
340            FieldType::Vector(_) => {
341                return Err(RenderError::unsupported(
342                    "VectorType",
343                    "SQLite does not support vector types.",
344                ));
345            }
346            FieldType::Custom(_) => {
347                return Err(RenderError::unsupported(
348                    "CustomFieldType",
349                    "custom field type must be handled by a wrapping renderer",
350                ));
351            }
352        }
353        Ok(())
354    }
355
356    fn render_constraint(&self, c: &ConstraintDef, ctx: &mut RenderCtx) -> RenderResult<()> {
357        match c {
358            ConstraintDef::PrimaryKey {
359                name,
360                columns,
361                include: _, // SQLite doesn't support INCLUDE — Ignore
362            } => {
363                if let Some(n) = name {
364                    ctx.keyword("CONSTRAINT").ident(n);
365                }
366                ctx.keyword("PRIMARY KEY").paren_open();
367                self.sqlite_comma_idents(columns, ctx);
368                ctx.paren_close();
369            }
370
371            ConstraintDef::ForeignKey {
372                name,
373                columns,
374                ref_table,
375                ref_columns,
376                on_delete,
377                on_update,
378                deferrable,
379                match_type: _, // SQLite accepts MATCH but it's a no-op — Ignore
380            } => {
381                if let Some(n) = name {
382                    ctx.keyword("CONSTRAINT").ident(n);
383                }
384                ctx.keyword("FOREIGN KEY").paren_open();
385                self.sqlite_comma_idents(columns, ctx);
386                ctx.paren_close().keyword("REFERENCES");
387                self.sqlite_schema_ref(ref_table, ctx);
388                ctx.paren_open();
389                self.sqlite_comma_idents(ref_columns, ctx);
390                ctx.paren_close();
391                if let Some(action) = on_delete {
392                    ctx.keyword("ON DELETE");
393                    self.sqlite_referential_action(action, ctx)?;
394                }
395                if let Some(action) = on_update {
396                    ctx.keyword("ON UPDATE");
397                    self.sqlite_referential_action(action, ctx)?;
398                }
399                if let Some(def) = deferrable {
400                    self.sqlite_deferrable(def, ctx);
401                }
402            }
403
404            ConstraintDef::Unique {
405                name,
406                columns,
407                include: _,        // Ignore
408                nulls_distinct: _, // Ignore
409                condition: _,      // Ignore
410            } => {
411                if let Some(n) = name {
412                    ctx.keyword("CONSTRAINT").ident(n);
413                }
414                ctx.keyword("UNIQUE").paren_open();
415                self.sqlite_comma_idents(columns, ctx);
416                ctx.paren_close();
417            }
418
419            ConstraintDef::Check {
420                name,
421                condition,
422                no_inherit: _, // Ignore
423                enforced: _,   // Ignore
424            } => {
425                if let Some(n) = name {
426                    ctx.keyword("CONSTRAINT").ident(n);
427                }
428                ctx.keyword("CHECK").paren_open();
429                self.render_condition(condition, ctx)?;
430                ctx.paren_close();
431            }
432
433            ConstraintDef::Exclusion { .. } => {
434                return Err(RenderError::unsupported(
435                    "ExclusionConstraint",
436                    "SQLite does not support EXCLUDE constraints.",
437                ));
438            }
439
440            ConstraintDef::Custom(_) => {
441                return Err(RenderError::unsupported(
442                    "CustomConstraint",
443                    "custom constraint must be handled by a wrapping renderer",
444                ));
445            }
446        }
447        Ok(())
448    }
449
450    fn render_index_def(&self, idx: &IndexDef, ctx: &mut RenderCtx) -> RenderResult<()> {
451        ctx.ident(&idx.name);
452        ctx.paren_open();
453        self.sqlite_index_columns(&idx.columns, ctx)?;
454        ctx.paren_close();
455        Ok(())
456    }
457
458    // ── Expressions (basic, needed for DDL) ──────────────────────────────
459
460    fn render_expr(&self, expr: &Expr, ctx: &mut RenderCtx) -> RenderResult<()> {
461        match expr {
462            Expr::Value(val) => self.sqlite_value(val, ctx),
463
464            Expr::Field(field_ref) => {
465                self.sqlite_field_ref(field_ref, ctx);
466                Ok(())
467            }
468
469            Expr::Binary { left, op, right } => {
470                self.render_expr(left, ctx)?;
471                match op {
472                    BinaryOp::Custom(_) => {
473                        return Err(RenderError::unsupported(
474                            "CustomBinaryOp",
475                            "SQLite does not support custom binary operators.",
476                        ));
477                    }
478                    _ => {
479                        ctx.keyword(match op {
480                            BinaryOp::Add => "+",
481                            BinaryOp::Sub => "-",
482                            BinaryOp::Mul => "*",
483                            BinaryOp::Div => "/",
484                            BinaryOp::Mod => "%",
485                            BinaryOp::BitwiseAnd => "&",
486                            BinaryOp::BitwiseOr => "|",
487                            BinaryOp::ShiftLeft => "<<",
488                            BinaryOp::ShiftRight => ">>",
489                            BinaryOp::Concat => "||",
490                            BinaryOp::Custom(_) => unreachable!(),
491                        });
492                    }
493                };
494                self.render_expr(right, ctx)
495            }
496
497            Expr::Unary { op, expr: inner } => {
498                match op {
499                    UnaryOp::Neg => ctx.write("-"),
500                    UnaryOp::Not => ctx.keyword("NOT"),
501                    UnaryOp::BitwiseNot => ctx.write("~"),
502                };
503                self.render_expr(inner, ctx)
504            }
505
506            Expr::Func { name, args } => {
507                ctx.keyword(name).write("(");
508                for (i, arg) in args.iter().enumerate() {
509                    if i > 0 {
510                        ctx.comma();
511                    }
512                    self.render_expr(arg, ctx)?;
513                }
514                ctx.paren_close();
515                Ok(())
516            }
517
518            Expr::Aggregate(agg) => self.render_aggregate(agg, ctx),
519
520            Expr::Cast {
521                expr: inner,
522                to_type,
523            } => {
524                ctx.keyword("CAST").write("(");
525                self.render_expr(inner, ctx)?;
526                ctx.keyword("AS").keyword(to_type).paren_close();
527                Ok(())
528            }
529
530            Expr::Case(case) => self.render_case(case, ctx),
531            Expr::Window(win) => self.render_window(win, ctx),
532
533            Expr::Exists(query) => {
534                ctx.keyword("EXISTS").write("(");
535                self.render_query(query, ctx)?;
536                ctx.paren_close();
537                Ok(())
538            }
539
540            Expr::SubQuery(query) => {
541                ctx.paren_open();
542                self.render_query(query, ctx)?;
543                ctx.paren_close();
544                Ok(())
545            }
546
547            Expr::ArraySubQuery(_) => Err(RenderError::unsupported(
548                "ArraySubQuery",
549                "SQLite does not support ARRAY subqueries.",
550            )),
551
552            Expr::Collate { expr, collation } => {
553                self.render_expr(expr, ctx)?;
554                ctx.keyword("COLLATE").keyword(collation);
555                Ok(())
556            }
557
558            Expr::JsonArray(items) => {
559                ctx.keyword("json_array").write("(");
560                for (i, item) in items.iter().enumerate() {
561                    if i > 0 {
562                        ctx.comma();
563                    }
564                    self.render_expr(item, ctx)?;
565                }
566                ctx.paren_close();
567                Ok(())
568            }
569
570            Expr::JsonObject(pairs) => {
571                ctx.keyword("json_object").write("(");
572                for (i, (key, val)) in pairs.iter().enumerate() {
573                    if i > 0 {
574                        ctx.comma();
575                    }
576                    ctx.string_literal(key).comma();
577                    self.render_expr(val, ctx)?;
578                }
579                ctx.paren_close();
580                Ok(())
581            }
582
583            Expr::JsonAgg {
584                expr,
585                distinct,
586                filter,
587                order_by,
588            } => {
589                ctx.keyword("json_group_array").write("(");
590                if *distinct {
591                    ctx.keyword("DISTINCT");
592                }
593                self.render_expr(expr, ctx)?;
594                if let Some(ob) = order_by {
595                    ctx.keyword("ORDER BY");
596                    self.sqlite_order_by_list(ob, ctx)?;
597                }
598                ctx.paren_close();
599                if let Some(f) = filter {
600                    ctx.keyword("FILTER").paren_open().keyword("WHERE");
601                    self.render_condition(f, ctx)?;
602                    ctx.paren_close();
603                }
604                Ok(())
605            }
606
607            Expr::StringAgg {
608                expr,
609                delimiter,
610                distinct,
611                filter,
612                order_by,
613            } => {
614                ctx.keyword("group_concat").write("(");
615                if *distinct {
616                    ctx.keyword("DISTINCT");
617                }
618                self.render_expr(expr, ctx)?;
619                ctx.comma().string_literal(delimiter);
620                if let Some(ob) = order_by {
621                    ctx.keyword("ORDER BY");
622                    self.sqlite_order_by_list(ob, ctx)?;
623                }
624                ctx.paren_close();
625                if let Some(f) = filter {
626                    ctx.keyword("FILTER").paren_open().keyword("WHERE");
627                    self.render_condition(f, ctx)?;
628                    ctx.paren_close();
629                }
630                Ok(())
631            }
632
633            Expr::Now => {
634                ctx.keyword("datetime")
635                    .write("(")
636                    .string_literal("now")
637                    .paren_close();
638                Ok(())
639            }
640
641            Expr::CurrentTimestamp => {
642                ctx.keyword("CURRENT_TIMESTAMP");
643                Ok(())
644            }
645            Expr::CurrentDate => {
646                ctx.keyword("CURRENT_DATE");
647                Ok(())
648            }
649            Expr::CurrentTime => {
650                ctx.keyword("CURRENT_TIME");
651                Ok(())
652            }
653
654            Expr::JsonPathText { expr, path } => {
655                self.render_expr(expr, ctx)?;
656                ctx.operator("->>'")
657                    .write(&path.replace('\'', "''"))
658                    .write("'");
659                Ok(())
660            }
661
662            Expr::Tuple(exprs) => {
663                ctx.paren_open();
664                for (i, expr) in exprs.iter().enumerate() {
665                    if i > 0 {
666                        ctx.comma();
667                    }
668                    self.render_expr(expr, ctx)?;
669                }
670                ctx.paren_close();
671                Ok(())
672            }
673
674            Expr::Param { type_hint: _ } => {
675                ctx.placeholder();
676                Ok(())
677            }
678
679            Expr::Raw { sql, params } => {
680                if params.is_empty() {
681                    ctx.keyword(sql);
682                } else {
683                    ctx.raw_with_params(sql, params);
684                }
685                Ok(())
686            }
687
688            Expr::Custom(_) => Err(RenderError::unsupported(
689                "CustomExpr",
690                "custom expression must be handled by a wrapping renderer",
691            )),
692        }
693    }
694
695    fn render_aggregate(&self, agg: &AggregationDef, ctx: &mut RenderCtx) -> RenderResult<()> {
696        ctx.keyword(&agg.name).write("(");
697        if agg.distinct {
698            ctx.keyword("DISTINCT");
699        }
700        if let Some(expr) = &agg.expression {
701            self.render_expr(expr, ctx)?;
702        } else {
703            ctx.write("*");
704        }
705        if let Some(args) = &agg.args {
706            for arg in args {
707                ctx.comma();
708                self.render_expr(arg, ctx)?;
709            }
710        }
711        if let Some(order_by) = &agg.order_by {
712            ctx.keyword("ORDER BY");
713            self.sqlite_order_by_list(order_by, ctx)?;
714        }
715        ctx.paren_close();
716        if let Some(filter) = &agg.filter {
717            ctx.keyword("FILTER").paren_open().keyword("WHERE");
718            self.render_condition(filter, ctx)?;
719            ctx.paren_close();
720        }
721        Ok(())
722    }
723
724    fn render_window(&self, win: &WindowDef, ctx: &mut RenderCtx) -> RenderResult<()> {
725        self.render_expr(&win.expression, ctx)?;
726        ctx.keyword("OVER").paren_open();
727        if let Some(partition_by) = &win.partition_by {
728            ctx.keyword("PARTITION BY");
729            for (i, expr) in partition_by.iter().enumerate() {
730                if i > 0 {
731                    ctx.comma();
732                }
733                self.render_expr(expr, ctx)?;
734            }
735        }
736        if let Some(order_by) = &win.order_by {
737            ctx.keyword("ORDER BY");
738            self.sqlite_order_by_list(order_by, ctx)?;
739        }
740        ctx.paren_close();
741        Ok(())
742    }
743
744    fn render_case(&self, case: &CaseDef, ctx: &mut RenderCtx) -> RenderResult<()> {
745        ctx.keyword("CASE");
746        for clause in &case.cases {
747            ctx.keyword("WHEN");
748            self.render_condition(&clause.condition, ctx)?;
749            ctx.keyword("THEN");
750            self.render_expr(&clause.result, ctx)?;
751        }
752        if let Some(default) = &case.default {
753            ctx.keyword("ELSE");
754            self.render_expr(default, ctx)?;
755        }
756        ctx.keyword("END");
757        Ok(())
758    }
759
760    // ── Conditions ───────────────────────────────────────────────────────
761
762    fn render_condition(&self, cond: &Conditions, ctx: &mut RenderCtx) -> RenderResult<()> {
763        // Special case: negated + single Exists child → NOT EXISTS (...)
764        if cond.negated
765            && cond.children.len() == 1
766            && matches!(cond.children[0], ConditionNode::Exists(_))
767        {
768            if let ConditionNode::Exists(query) = &cond.children[0] {
769                ctx.keyword("NOT EXISTS").write("(");
770                self.render_query(query, ctx)?;
771                ctx.paren_close();
772                return Ok(());
773            }
774        }
775
776        if cond.negated {
777            ctx.keyword("NOT").paren_open();
778        }
779        let connector = match cond.connector {
780            Connector::And => " AND ",
781            Connector::Or => " OR ",
782        };
783        for (i, child) in cond.children.iter().enumerate() {
784            if i > 0 {
785                ctx.write(connector);
786            }
787            match child {
788                ConditionNode::Comparison(comp) => {
789                    if comp.negate {
790                        ctx.keyword("NOT").paren_open();
791                    }
792                    self.render_compare_op(&comp.op, &comp.left, &comp.right, ctx)?;
793                    if comp.negate {
794                        ctx.paren_close();
795                    }
796                }
797                ConditionNode::Group(group) => {
798                    ctx.paren_open();
799                    self.render_condition(group, ctx)?;
800                    ctx.paren_close();
801                }
802                ConditionNode::Exists(query) => {
803                    ctx.keyword("EXISTS").write("(");
804                    self.render_query(query, ctx)?;
805                    ctx.paren_close();
806                }
807                ConditionNode::Custom(_) => {
808                    return Err(RenderError::unsupported(
809                        "CustomCondition",
810                        "custom condition must be handled by a wrapping renderer",
811                    ));
812                }
813            }
814        }
815        if cond.negated {
816            ctx.paren_close();
817        }
818        Ok(())
819    }
820
821    fn render_compare_op(
822        &self,
823        op: &CompareOp,
824        left: &Expr,
825        right: &Expr,
826        ctx: &mut RenderCtx,
827    ) -> RenderResult<()> {
828        let needs_lower = matches!(
829            op,
830            CompareOp::ILike | CompareOp::IContains | CompareOp::IStartsWith | CompareOp::IEndsWith
831        );
832        if needs_lower {
833            ctx.keyword("LOWER").write("(");
834        }
835        self.render_expr(left, ctx)?;
836        if needs_lower {
837            ctx.paren_close();
838        }
839        match op {
840            CompareOp::Eq => ctx.write(" = "),
841            CompareOp::Neq => ctx.write(" <> "),
842            CompareOp::Gt => ctx.write(" > "),
843            CompareOp::Gte => ctx.write(" >= "),
844            CompareOp::Lt => ctx.write(" < "),
845            CompareOp::Lte => ctx.write(" <= "),
846            CompareOp::Like => ctx.keyword("LIKE"),
847            CompareOp::Contains | CompareOp::StartsWith | CompareOp::EndsWith => {
848                ctx.keyword("LIKE");
849                render_like_pattern(op, right, ctx)?;
850                ctx.keyword("ESCAPE").string_literal("\\");
851                return Ok(());
852            }
853            CompareOp::IContains | CompareOp::IStartsWith | CompareOp::IEndsWith => {
854                ctx.keyword("LIKE");
855                ctx.keyword("LOWER").write("(");
856                render_like_pattern(op, right, ctx)?;
857                ctx.paren_close();
858                ctx.keyword("ESCAPE").string_literal("\\");
859                return Ok(());
860            }
861            CompareOp::In => {
862                if let Expr::Value(Value::Array(items)) = right {
863                    ctx.keyword("IN").paren_open();
864                    for (i, item) in items.iter().enumerate() {
865                        if i > 0 {
866                            ctx.comma();
867                        }
868                        self.sqlite_value(item, ctx)?;
869                    }
870                    ctx.paren_close();
871                } else {
872                    ctx.keyword("IN");
873                    self.render_expr(right, ctx)?;
874                }
875                return Ok(());
876            }
877            CompareOp::Between => {
878                ctx.keyword("BETWEEN");
879                if let Expr::Value(Value::Array(items)) = right {
880                    if items.len() == 2 {
881                        self.sqlite_value(&items[0], ctx)?;
882                        ctx.keyword("AND");
883                        self.sqlite_value(&items[1], ctx)?;
884                    } else {
885                        return Err(RenderError::unsupported(
886                            "Between",
887                            "BETWEEN requires exactly 2 values",
888                        ));
889                    }
890                } else {
891                    self.render_expr(right, ctx)?;
892                }
893                return Ok(());
894            }
895            CompareOp::IsNull => {
896                ctx.keyword("IS NULL");
897                return Ok(());
898            }
899            CompareOp::Regex => ctx.keyword("REGEXP"),
900            CompareOp::IRegex => {
901                ctx.keyword("REGEXP").string_literal("(?i)").keyword("||");
902                self.render_expr(right, ctx)?;
903                return Ok(());
904            }
905            CompareOp::ILike => {
906                ctx.keyword("LIKE").keyword("LOWER").write("(");
907                self.render_expr(right, ctx)?;
908                ctx.paren_close();
909                return Ok(());
910            }
911            // SQLite doesn't natively support this — Error
912            CompareOp::Similar => {
913                return Err(RenderError::unsupported(
914                    "CompareOp",
915                    "SQLite does not support SIMILAR TO.",
916                ));
917            }
918            CompareOp::JsonbContains
919            | CompareOp::JsonbContainedBy
920            | CompareOp::JsonbHasKey
921            | CompareOp::JsonbHasAnyKey
922            | CompareOp::JsonbHasAllKeys
923            | CompareOp::FtsMatch
924            | CompareOp::TrigramSimilar
925            | CompareOp::TrigramWordSimilar
926            | CompareOp::TrigramStrictWordSimilar
927            | CompareOp::RangeContains
928            | CompareOp::RangeContainedBy
929            | CompareOp::RangeOverlap
930            | CompareOp::RangeStrictlyLeft
931            | CompareOp::RangeStrictlyRight
932            | CompareOp::RangeNotLeft
933            | CompareOp::RangeNotRight
934            | CompareOp::RangeAdjacent => {
935                return Err(RenderError::unsupported(
936                    "CompareOp",
937                    "SQLite does not support PostgreSQL-specific operators (JSONB, FTS, trigram, range).",
938                ));
939            }
940            CompareOp::Custom(_) => {
941                return Err(RenderError::unsupported(
942                    "CustomCompareOp",
943                    "custom compare op must be handled by a wrapping renderer",
944                ));
945            }
946        };
947        self.render_expr(right, ctx)
948    }
949
950    // ── Query (stub) ─────────────────────────────────────────────────────
951
952    fn render_query(&self, stmt: &QueryStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
953        // CTEs
954        if let Some(ctes) = &stmt.ctes {
955            self.render_ctes(ctes, ctx)?;
956        }
957
958        // Set operation: render directly without SELECT wrapper
959        if let Some(set_op) = &stmt.set_op {
960            return self.sqlite_render_set_op(set_op, ctx);
961        }
962
963        // SELECT
964        ctx.keyword("SELECT");
965
966        // DISTINCT
967        if let Some(distinct) = &stmt.distinct {
968            match distinct {
969                DistinctDef::Distinct => {
970                    ctx.keyword("DISTINCT");
971                }
972                DistinctDef::DistinctOn(_) => {
973                    return Err(RenderError::unsupported(
974                        "DISTINCT ON",
975                        "not supported in SQLite",
976                    ));
977                }
978            }
979        }
980
981        // Columns
982        self.render_select_columns(&stmt.columns, ctx)?;
983
984        // FROM
985        if let Some(from) = &stmt.from {
986            ctx.keyword("FROM");
987            for (i, item) in from.iter().enumerate() {
988                if i > 0 {
989                    ctx.comma();
990                }
991                self.sqlite_render_from_item(item, ctx)?;
992            }
993        }
994
995        // JOINs
996        if let Some(joins) = &stmt.joins {
997            self.render_joins(joins, ctx)?;
998        }
999
1000        // WHERE
1001        if let Some(cond) = &stmt.where_clause {
1002            self.render_where(cond, ctx)?;
1003        }
1004
1005        // GROUP BY
1006        if let Some(group_by) = &stmt.group_by {
1007            self.sqlite_render_group_by(group_by, ctx)?;
1008        }
1009
1010        // HAVING
1011        if let Some(having) = &stmt.having {
1012            ctx.keyword("HAVING");
1013            self.render_condition(having, ctx)?;
1014        }
1015
1016        // WINDOW
1017        if let Some(windows) = &stmt.window {
1018            self.sqlite_render_window_clause(windows, ctx)?;
1019        }
1020
1021        // ORDER BY
1022        if let Some(order_by) = &stmt.order_by {
1023            self.render_order_by(order_by, ctx)?;
1024        }
1025
1026        // LIMIT / OFFSET
1027        if let Some(limit) = &stmt.limit {
1028            self.render_limit(limit, ctx)?;
1029        }
1030
1031        // FOR UPDATE — not supported in SQLite
1032        if let Some(locks) = &stmt.lock {
1033            if !locks.is_empty() {
1034                return Err(RenderError::unsupported(
1035                    "FOR UPDATE/SHARE",
1036                    "row locking not supported in SQLite",
1037                ));
1038            }
1039        }
1040
1041        Ok(())
1042    }
1043
1044    fn render_select_columns(
1045        &self,
1046        cols: &[SelectColumn],
1047        ctx: &mut RenderCtx,
1048    ) -> RenderResult<()> {
1049        for (i, col) in cols.iter().enumerate() {
1050            if i > 0 {
1051                ctx.comma();
1052            }
1053            match col {
1054                SelectColumn::Star(None) => {
1055                    ctx.keyword("*");
1056                }
1057                SelectColumn::Star(Some(table)) => {
1058                    ctx.ident(table).operator(".").keyword("*");
1059                }
1060                SelectColumn::Expr { expr, alias } => {
1061                    self.render_expr(expr, ctx)?;
1062                    if let Some(a) = alias {
1063                        ctx.keyword("AS").ident(a);
1064                    }
1065                }
1066                SelectColumn::Field { field, alias } => {
1067                    self.sqlite_field_ref(field, ctx);
1068                    if let Some(a) = alias {
1069                        ctx.keyword("AS").ident(a);
1070                    }
1071                }
1072            }
1073        }
1074        Ok(())
1075    }
1076    fn render_from(&self, source: &TableSource, ctx: &mut RenderCtx) -> RenderResult<()> {
1077        match source {
1078            TableSource::Table(schema_ref) => {
1079                self.sqlite_schema_ref(schema_ref, ctx);
1080                if let Some(alias) = &schema_ref.alias {
1081                    ctx.keyword("AS").ident(alias);
1082                }
1083            }
1084            TableSource::SubQuery(sq) => {
1085                ctx.paren_open();
1086                self.render_query(&sq.query, ctx)?;
1087                ctx.paren_close().keyword("AS").ident(&sq.alias);
1088            }
1089            TableSource::SetOp(set_op) => {
1090                ctx.paren_open();
1091                self.sqlite_render_set_op(set_op, ctx)?;
1092                ctx.paren_close();
1093            }
1094            TableSource::Lateral(_) => {
1095                return Err(RenderError::unsupported(
1096                    "LATERAL",
1097                    "LATERAL subqueries not supported in SQLite",
1098                ));
1099            }
1100            TableSource::Function { name, args, alias } => {
1101                ctx.keyword(name).write("(");
1102                for (i, arg) in args.iter().enumerate() {
1103                    if i > 0 {
1104                        ctx.comma();
1105                    }
1106                    self.render_expr(arg, ctx)?;
1107                }
1108                ctx.paren_close();
1109                if let Some(a) = alias {
1110                    ctx.keyword("AS").ident(a);
1111                }
1112            }
1113            TableSource::Values {
1114                rows,
1115                alias,
1116                columns,
1117            } => {
1118                // SQLite does not support AS t(col1, col2) syntax.
1119                // Wrap in: (SELECT column1 AS "c1", column2 AS "c2"
1120                //           FROM (VALUES (...), (...))) AS "t"
1121                ctx.paren_open().keyword("SELECT");
1122                for (i, c) in columns.iter().enumerate() {
1123                    if i > 0 {
1124                        ctx.comma();
1125                    }
1126                    ctx.keyword(&format!("column{}", i + 1))
1127                        .keyword("AS")
1128                        .ident(c);
1129                }
1130                ctx.keyword("FROM").paren_open().keyword("VALUES");
1131                for (i, row) in rows.iter().enumerate() {
1132                    if i > 0 {
1133                        ctx.comma();
1134                    }
1135                    ctx.paren_open();
1136                    for (j, val) in row.iter().enumerate() {
1137                        if j > 0 {
1138                            ctx.comma();
1139                        }
1140                        self.render_expr(val, ctx)?;
1141                    }
1142                    ctx.paren_close();
1143                }
1144                ctx.paren_close().paren_close().keyword("AS").ident(alias);
1145            }
1146            TableSource::Custom(_) => {
1147                return Err(RenderError::unsupported(
1148                    "CustomTableSource",
1149                    "custom table source must be handled by a wrapping renderer",
1150                ));
1151            }
1152        }
1153        Ok(())
1154    }
1155    fn render_joins(&self, joins: &[JoinDef], ctx: &mut RenderCtx) -> RenderResult<()> {
1156        for join in joins {
1157            if join.natural {
1158                ctx.keyword("NATURAL");
1159            }
1160            ctx.keyword(match join.join_type {
1161                JoinType::Inner => "INNER JOIN",
1162                JoinType::Left => "LEFT JOIN",
1163                JoinType::Right => "RIGHT JOIN",
1164                JoinType::Full => "FULL JOIN",
1165                JoinType::Cross => "CROSS JOIN",
1166                JoinType::CrossApply | JoinType::OuterApply => {
1167                    return Err(RenderError::unsupported(
1168                        "APPLY",
1169                        "CROSS/OUTER APPLY not supported in SQLite",
1170                    ));
1171                }
1172            });
1173            self.sqlite_render_from_item(&join.source, ctx)?;
1174            if !matches!(join.join_type, JoinType::Cross) {
1175                if let Some(condition) = &join.condition {
1176                    match condition {
1177                        JoinCondition::On(cond) => {
1178                            ctx.keyword("ON");
1179                            self.render_condition(cond, ctx)?;
1180                        }
1181                        JoinCondition::Using(cols) => {
1182                            ctx.keyword("USING").paren_open();
1183                            self.sqlite_comma_idents(cols, ctx);
1184                            ctx.paren_close();
1185                        }
1186                    }
1187                }
1188            }
1189        }
1190        Ok(())
1191    }
1192    fn render_where(&self, cond: &Conditions, ctx: &mut RenderCtx) -> RenderResult<()> {
1193        ctx.keyword("WHERE");
1194        self.render_condition(cond, ctx)
1195    }
1196    fn render_order_by(&self, order: &[OrderByDef], ctx: &mut RenderCtx) -> RenderResult<()> {
1197        ctx.keyword("ORDER BY");
1198        self.sqlite_order_by_list(order, ctx)
1199    }
1200    fn render_limit(&self, limit: &LimitDef, ctx: &mut RenderCtx) -> RenderResult<()> {
1201        match &limit.kind {
1202            LimitKind::Limit(n) => {
1203                ctx.keyword("LIMIT");
1204                if ctx.parameterize() {
1205                    ctx.param(Value::BigInt(*n as i64));
1206                } else {
1207                    ctx.space().write(&n.to_string());
1208                }
1209            }
1210            LimitKind::FetchFirst {
1211                count, with_ties, ..
1212            } => {
1213                if *with_ties {
1214                    return Err(RenderError::unsupported(
1215                        "FETCH FIRST WITH TIES",
1216                        "not supported in SQLite",
1217                    ));
1218                }
1219                // Convert FETCH FIRST to LIMIT
1220                ctx.keyword("LIMIT");
1221                if ctx.parameterize() {
1222                    ctx.param(Value::BigInt(*count as i64));
1223                } else {
1224                    ctx.space().write(&count.to_string());
1225                }
1226            }
1227            LimitKind::Top {
1228                count, with_ties, ..
1229            } => {
1230                if *with_ties {
1231                    return Err(RenderError::unsupported(
1232                        "TOP WITH TIES",
1233                        "not supported in SQLite",
1234                    ));
1235                }
1236                // Convert TOP to LIMIT
1237                ctx.keyword("LIMIT");
1238                if ctx.parameterize() {
1239                    ctx.param(Value::BigInt(*count as i64));
1240                } else {
1241                    ctx.space().write(&count.to_string());
1242                }
1243            }
1244        }
1245        if let Some(offset) = limit.offset {
1246            ctx.keyword("OFFSET");
1247            if ctx.parameterize() {
1248                ctx.param(Value::BigInt(offset as i64));
1249            } else {
1250                ctx.space().write(&offset.to_string());
1251            }
1252        }
1253        Ok(())
1254    }
1255    fn render_ctes(&self, ctes: &[CteDef], ctx: &mut RenderCtx) -> RenderResult<()> {
1256        let any_recursive = ctes.iter().any(|c| c.recursive);
1257        ctx.keyword("WITH");
1258        if any_recursive {
1259            ctx.keyword("RECURSIVE");
1260        }
1261        for (i, cte) in ctes.iter().enumerate() {
1262            if i > 0 {
1263                ctx.comma();
1264            }
1265            ctx.ident(&cte.name);
1266            if let Some(col_names) = &cte.column_names {
1267                ctx.paren_open();
1268                self.sqlite_comma_idents(col_names, ctx);
1269                ctx.paren_close();
1270            }
1271            // SQLite ignores MATERIALIZED hints
1272            ctx.keyword("AS").paren_open();
1273            self.render_query(&cte.query, ctx)?;
1274            ctx.paren_close();
1275        }
1276        Ok(())
1277    }
1278    fn render_lock(&self, _lock: &SelectLockDef, _ctx: &mut RenderCtx) -> RenderResult<()> {
1279        Err(RenderError::unsupported(
1280            "FOR UPDATE/SHARE",
1281            "row locking not supported in SQLite",
1282        ))
1283    }
1284
1285    // ── DML ──────────────────────────────────────────────────────────────
1286
1287    fn render_mutation(&self, stmt: &MutationStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1288        match stmt {
1289            MutationStmt::Insert(s) => self.render_insert(s, ctx),
1290            MutationStmt::Update(s) => self.render_update(s, ctx),
1291            MutationStmt::Delete(s) => self.render_delete(s, ctx),
1292            MutationStmt::Custom(_) => Err(RenderError::unsupported(
1293                "CustomMutation",
1294                "custom DML must be handled by a wrapping renderer",
1295            )),
1296        }
1297    }
1298
1299    fn render_insert(&self, stmt: &InsertStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1300        // CTEs
1301        if let Some(ctes) = &stmt.ctes {
1302            self.sqlite_render_ctes(ctes, ctx)?;
1303        }
1304
1305        // INSERT OR REPLACE / OR IGNORE / etc.
1306        if let Some(cr) = &stmt.conflict_resolution {
1307            ctx.keyword("INSERT OR");
1308            ctx.keyword(match cr {
1309                ConflictResolution::Rollback => "ROLLBACK",
1310                ConflictResolution::Abort => "ABORT",
1311                ConflictResolution::Fail => "FAIL",
1312                ConflictResolution::Ignore => "IGNORE",
1313                ConflictResolution::Replace => "REPLACE",
1314            });
1315            ctx.keyword("INTO");
1316        } else {
1317            ctx.keyword("INSERT INTO");
1318        }
1319
1320        self.sqlite_schema_ref(&stmt.table, ctx);
1321
1322        // Alias
1323        if let Some(alias) = &stmt.table.alias {
1324            ctx.keyword("AS").ident(alias);
1325        }
1326
1327        // Column list
1328        if let Some(cols) = &stmt.columns {
1329            ctx.paren_open();
1330            self.sqlite_comma_idents(cols, ctx);
1331            ctx.paren_close();
1332        }
1333
1334        // Source
1335        match &stmt.source {
1336            InsertSource::Values(rows) => {
1337                ctx.keyword("VALUES");
1338                for (i, row) in rows.iter().enumerate() {
1339                    if i > 0 {
1340                        ctx.comma();
1341                    }
1342                    ctx.paren_open();
1343                    for (j, expr) in row.iter().enumerate() {
1344                        if j > 0 {
1345                            ctx.comma();
1346                        }
1347                        self.render_expr(expr, ctx)?;
1348                    }
1349                    ctx.paren_close();
1350                }
1351            }
1352            InsertSource::Select(query) => {
1353                self.render_query(query, ctx)?;
1354            }
1355            InsertSource::DefaultValues => {
1356                ctx.keyword("DEFAULT VALUES");
1357            }
1358        }
1359
1360        // ON CONFLICT
1361        if let Some(conflicts) = &stmt.on_conflict {
1362            for oc in conflicts {
1363                self.render_on_conflict(oc, ctx)?;
1364            }
1365        }
1366
1367        // RETURNING
1368        if let Some(returning) = &stmt.returning {
1369            self.render_returning(returning, ctx)?;
1370        }
1371
1372        Ok(())
1373    }
1374
1375    fn render_update(&self, stmt: &UpdateStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1376        // CTEs
1377        if let Some(ctes) = &stmt.ctes {
1378            self.sqlite_render_ctes(ctes, ctx)?;
1379        }
1380
1381        // UPDATE OR REPLACE / OR IGNORE / etc.
1382        if let Some(cr) = &stmt.conflict_resolution {
1383            ctx.keyword("UPDATE OR");
1384            ctx.keyword(match cr {
1385                ConflictResolution::Rollback => "ROLLBACK",
1386                ConflictResolution::Abort => "ABORT",
1387                ConflictResolution::Fail => "FAIL",
1388                ConflictResolution::Ignore => "IGNORE",
1389                ConflictResolution::Replace => "REPLACE",
1390            });
1391        } else {
1392            ctx.keyword("UPDATE");
1393        }
1394
1395        self.sqlite_schema_ref(&stmt.table, ctx);
1396
1397        // Alias
1398        if let Some(alias) = &stmt.table.alias {
1399            ctx.keyword("AS").ident(alias);
1400        }
1401
1402        // SET
1403        ctx.keyword("SET");
1404        for (i, (col, expr)) in stmt.assignments.iter().enumerate() {
1405            if i > 0 {
1406                ctx.comma();
1407            }
1408            ctx.ident(col).write(" = ");
1409            self.render_expr(expr, ctx)?;
1410        }
1411
1412        // FROM (SQLite 3.33+)
1413        if let Some(from) = &stmt.from {
1414            ctx.keyword("FROM");
1415            for (i, source) in from.iter().enumerate() {
1416                if i > 0 {
1417                    ctx.comma();
1418                }
1419                self.render_from(source, ctx)?;
1420            }
1421        }
1422
1423        // WHERE
1424        if let Some(cond) = &stmt.where_clause {
1425            ctx.keyword("WHERE");
1426            self.render_condition(cond, ctx)?;
1427        }
1428
1429        // RETURNING
1430        if let Some(returning) = &stmt.returning {
1431            self.render_returning(returning, ctx)?;
1432        }
1433
1434        // ORDER BY
1435        if let Some(order_by) = &stmt.order_by {
1436            ctx.keyword("ORDER BY");
1437            self.sqlite_order_by_list(order_by, ctx)?;
1438        }
1439
1440        // LIMIT / OFFSET
1441        if let Some(limit) = stmt.limit {
1442            ctx.keyword("LIMIT").keyword(&limit.to_string());
1443            if let Some(offset) = stmt.offset {
1444                ctx.keyword("OFFSET").keyword(&offset.to_string());
1445            }
1446        }
1447
1448        Ok(())
1449    }
1450
1451    fn render_delete(&self, stmt: &DeleteStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1452        // CTEs
1453        if let Some(ctes) = &stmt.ctes {
1454            self.sqlite_render_ctes(ctes, ctx)?;
1455        }
1456
1457        ctx.keyword("DELETE FROM");
1458
1459        self.sqlite_schema_ref(&stmt.table, ctx);
1460
1461        // Alias
1462        if let Some(alias) = &stmt.table.alias {
1463            ctx.keyword("AS").ident(alias);
1464        }
1465
1466        // SQLite doesn't support USING — ignore
1467        // (SQLite has no JOIN syntax in DELETE; use subqueries in WHERE)
1468
1469        // WHERE
1470        if let Some(cond) = &stmt.where_clause {
1471            ctx.keyword("WHERE");
1472            self.render_condition(cond, ctx)?;
1473        }
1474
1475        // RETURNING
1476        if let Some(returning) = &stmt.returning {
1477            self.render_returning(returning, ctx)?;
1478        }
1479
1480        // ORDER BY
1481        if let Some(order_by) = &stmt.order_by {
1482            ctx.keyword("ORDER BY");
1483            self.sqlite_order_by_list(order_by, ctx)?;
1484        }
1485
1486        // LIMIT / OFFSET
1487        if let Some(limit) = stmt.limit {
1488            ctx.keyword("LIMIT").keyword(&limit.to_string());
1489            if let Some(offset) = stmt.offset {
1490                ctx.keyword("OFFSET").keyword(&offset.to_string());
1491            }
1492        }
1493
1494        Ok(())
1495    }
1496
1497    fn render_on_conflict(&self, oc: &OnConflictDef, ctx: &mut RenderCtx) -> RenderResult<()> {
1498        ctx.keyword("ON CONFLICT");
1499
1500        // Target
1501        if let Some(target) = &oc.target {
1502            match target {
1503                ConflictTarget::Columns {
1504                    columns,
1505                    where_clause,
1506                } => {
1507                    ctx.paren_open();
1508                    self.sqlite_comma_idents(columns, ctx);
1509                    ctx.paren_close();
1510                    if let Some(cond) = where_clause {
1511                        ctx.keyword("WHERE");
1512                        self.render_condition(cond, ctx)?;
1513                    }
1514                }
1515                ConflictTarget::Constraint(_) => {
1516                    return Err(RenderError::unsupported(
1517                        "OnConstraint",
1518                        "SQLite does not support ON CONFLICT ON CONSTRAINT. Use column list instead.",
1519                    ));
1520                }
1521            }
1522        }
1523
1524        // Action
1525        match &oc.action {
1526            ConflictAction::DoNothing => {
1527                ctx.keyword("DO NOTHING");
1528            }
1529            ConflictAction::DoUpdate {
1530                assignments,
1531                where_clause,
1532            } => {
1533                ctx.keyword("DO UPDATE SET");
1534                for (i, (col, expr)) in assignments.iter().enumerate() {
1535                    if i > 0 {
1536                        ctx.comma();
1537                    }
1538                    ctx.ident(col).write(" = ");
1539                    self.render_expr(expr, ctx)?;
1540                }
1541                if let Some(cond) = where_clause {
1542                    ctx.keyword("WHERE");
1543                    self.render_condition(cond, ctx)?;
1544                }
1545            }
1546        }
1547
1548        Ok(())
1549    }
1550
1551    fn render_returning(&self, cols: &[SelectColumn], ctx: &mut RenderCtx) -> RenderResult<()> {
1552        ctx.keyword("RETURNING");
1553        for (i, col) in cols.iter().enumerate() {
1554            if i > 0 {
1555                ctx.comma();
1556            }
1557            match col {
1558                SelectColumn::Star(None) => {
1559                    ctx.keyword("*");
1560                }
1561                SelectColumn::Star(Some(table)) => {
1562                    ctx.ident(table).operator(".").keyword("*");
1563                }
1564                SelectColumn::Expr { expr, alias } => {
1565                    self.render_expr(expr, ctx)?;
1566                    if let Some(a) = alias {
1567                        ctx.keyword("AS").ident(a);
1568                    }
1569                }
1570                SelectColumn::Field { field, alias } => {
1571                    self.sqlite_field_ref(field, ctx);
1572                    if let Some(a) = alias {
1573                        ctx.keyword("AS").ident(a);
1574                    }
1575                }
1576            }
1577        }
1578        Ok(())
1579    }
1580
1581    // ── TCL ──────────────────────────────────────────────────────────────
1582
1583    fn render_transaction(&self, stmt: &TransactionStmt, ctx: &mut RenderCtx) -> RenderResult<()> {
1584        match stmt {
1585            TransactionStmt::Begin(s) => {
1586                ctx.keyword("BEGIN");
1587                if let Some(lock_type) = &s.lock_type {
1588                    ctx.keyword(match lock_type {
1589                        SqliteLockType::Deferred => "DEFERRED",
1590                        SqliteLockType::Immediate => "IMMEDIATE",
1591                        SqliteLockType::Exclusive => "EXCLUSIVE",
1592                    });
1593                }
1594                ctx.keyword("TRANSACTION");
1595                Ok(())
1596            }
1597            TransactionStmt::Commit(_) => {
1598                ctx.keyword("COMMIT");
1599                Ok(())
1600            }
1601            TransactionStmt::Rollback(s) => {
1602                ctx.keyword("ROLLBACK");
1603                if let Some(sp) = &s.to_savepoint {
1604                    ctx.keyword("TO").keyword("SAVEPOINT").ident(sp);
1605                }
1606                Ok(())
1607            }
1608            TransactionStmt::Savepoint(s) => {
1609                ctx.keyword("SAVEPOINT").ident(&s.name);
1610                Ok(())
1611            }
1612            TransactionStmt::ReleaseSavepoint(s) => {
1613                ctx.keyword("RELEASE").keyword("SAVEPOINT").ident(&s.name);
1614                Ok(())
1615            }
1616            TransactionStmt::SetTransaction(_) => Err(RenderError::unsupported(
1617                "SET TRANSACTION",
1618                "not supported in SQLite",
1619            )),
1620            TransactionStmt::LockTable(_) => Err(RenderError::unsupported(
1621                "LOCK TABLE",
1622                "not supported in SQLite (use BEGIN EXCLUSIVE)",
1623            )),
1624            TransactionStmt::PrepareTransaction(_) => Err(RenderError::unsupported(
1625                "PREPARE TRANSACTION",
1626                "not supported in SQLite",
1627            )),
1628            TransactionStmt::CommitPrepared(_) => Err(RenderError::unsupported(
1629                "COMMIT PREPARED",
1630                "not supported in SQLite",
1631            )),
1632            TransactionStmt::RollbackPrepared(_) => Err(RenderError::unsupported(
1633                "ROLLBACK PREPARED",
1634                "not supported in SQLite",
1635            )),
1636            TransactionStmt::Custom(_) => Err(RenderError::unsupported(
1637                "Custom TCL",
1638                "not supported by SqliteRenderer",
1639            )),
1640        }
1641    }
1642}
1643
1644// ==========================================================================
1645// SQLite-specific helpers
1646// ==========================================================================
1647
1648impl SqliteRenderer {
1649    fn sqlite_schema_ref(
1650        &self,
1651        schema_ref: &qcraft_core::ast::common::SchemaRef,
1652        ctx: &mut RenderCtx,
1653    ) {
1654        if let Some(ns) = &schema_ref.namespace {
1655            ctx.ident(ns).operator(".");
1656        }
1657        ctx.ident(&schema_ref.name);
1658    }
1659
1660    /// Render an expression with all FieldRef table names stripped.
1661    /// Used for SQLite generated column expressions which only allow
1662    /// unqualified column references.
1663    fn render_expr_unqualified(&self, expr: &Expr, ctx: &mut RenderCtx) -> RenderResult<()> {
1664        match expr {
1665            Expr::Field(field_ref) => {
1666                ctx.ident(&field_ref.field.name);
1667                let mut child = &field_ref.field.child;
1668                while let Some(c) = child {
1669                    ctx.operator("->'")
1670                        .write(&c.name.replace('\'', "''"))
1671                        .write("'");
1672                    child = &c.child;
1673                }
1674                Ok(())
1675            }
1676            // For any other expr, delegate to normal render_expr
1677            other => self.render_expr(other, ctx),
1678        }
1679    }
1680
1681    fn sqlite_field_ref(&self, field_ref: &FieldRef, ctx: &mut RenderCtx) {
1682        if let Some(ns) = &field_ref.namespace {
1683            ctx.ident(ns).operator(".");
1684        }
1685        if !field_ref.table_name.is_empty() {
1686            ctx.ident(&field_ref.table_name).operator(".");
1687        }
1688        ctx.ident(&field_ref.field.name);
1689        let mut child = &field_ref.field.child;
1690        while let Some(c) = child {
1691            ctx.operator("->'")
1692                .write(&c.name.replace('\'', "''"))
1693                .write("'");
1694            child = &c.child;
1695        }
1696    }
1697
1698    fn sqlite_comma_idents(&self, names: &[String], ctx: &mut RenderCtx) {
1699        for (i, name) in names.iter().enumerate() {
1700            if i > 0 {
1701                ctx.comma();
1702            }
1703            ctx.ident(name);
1704        }
1705    }
1706
1707    fn sqlite_value(&self, val: &Value, ctx: &mut RenderCtx) -> RenderResult<()> {
1708        if matches!(val, Value::Null) && !ctx.parameterize() {
1709            ctx.keyword("NULL");
1710            return Ok(());
1711        }
1712
1713        // Array in inline literal mode → JSON string (no native array type).
1714        if let Value::Array(items) = val {
1715            if !ctx.parameterize() {
1716                let json = Self::array_to_json(items);
1717                ctx.string_literal(&json);
1718                return Ok(());
1719            }
1720        }
1721
1722        // Unsupported types always error, regardless of parameterize mode.
1723        if let Value::Vector(_) = val {
1724            return Err(RenderError::unsupported(
1725                "VectorValue",
1726                "SQLite does not support vector type.",
1727            ));
1728        }
1729
1730        // In parameterized mode, send values as bind parameters.
1731        if ctx.parameterize() {
1732            ctx.param(val.clone());
1733            return Ok(());
1734        }
1735
1736        // Inline literal mode (DDL defaults, etc.)
1737        self.sqlite_value_literal(val, ctx)
1738    }
1739
1740    fn sqlite_value_literal(&self, val: &Value, ctx: &mut RenderCtx) -> RenderResult<()> {
1741        match val {
1742            Value::Null => {
1743                ctx.keyword("NULL");
1744            }
1745            Value::Bool(b) => {
1746                ctx.keyword(if *b { "1" } else { "0" });
1747            }
1748            Value::Int(n) | Value::BigInt(n) => {
1749                ctx.keyword(&n.to_string());
1750            }
1751            Value::Float(f) => {
1752                ctx.keyword(&f.to_string());
1753            }
1754            Value::Str(s) => {
1755                ctx.string_literal(s);
1756            }
1757            Value::Bytes(b) => {
1758                ctx.write("X'");
1759                for byte in b {
1760                    ctx.write(&format!("{byte:02x}"));
1761                }
1762                ctx.write("'");
1763            }
1764            Value::Date(s) | Value::DateTime(s) | Value::Time(s) => {
1765                ctx.string_literal(s);
1766            }
1767            Value::Decimal(s) => {
1768                ctx.keyword(s);
1769            }
1770            Value::Uuid(s) => {
1771                ctx.string_literal(s);
1772            }
1773            Value::Json(s) | Value::Jsonb(s) => {
1774                ctx.string_literal(s);
1775            }
1776            Value::IpNetwork(s) => {
1777                ctx.string_literal(s);
1778            }
1779            Value::TimeDelta {
1780                years,
1781                months,
1782                days,
1783                seconds,
1784                microseconds,
1785            } => {
1786                let mut parts = Vec::new();
1787                if *years != 0 {
1788                    parts.push(format!("{years} years"));
1789                }
1790                if *months != 0 {
1791                    parts.push(format!("{months} months"));
1792                }
1793                if *days != 0 {
1794                    parts.push(format!("{days} days"));
1795                }
1796                if *seconds != 0 {
1797                    parts.push(format!("{seconds} seconds"));
1798                }
1799                if *microseconds != 0 {
1800                    parts.push(format!("{microseconds} microseconds"));
1801                }
1802                if parts.is_empty() {
1803                    parts.push("0 seconds".into());
1804                }
1805                ctx.string_literal(&parts.join(" "));
1806            }
1807            _ => {
1808                // Vector — already caught in sqlite_value
1809                unreachable!()
1810            }
1811        }
1812        Ok(())
1813    }
1814
1815    fn array_to_json(items: &[Value]) -> String {
1816        let mut s = String::from("[");
1817        for (i, item) in items.iter().enumerate() {
1818            if i > 0 {
1819                s.push_str(", ");
1820            }
1821            Self::value_to_json(item, &mut s);
1822        }
1823        s.push(']');
1824        s
1825    }
1826
1827    fn value_to_json(val: &Value, s: &mut String) {
1828        match val {
1829            Value::Null => s.push_str("null"),
1830            Value::Bool(b) => s.push_str(if *b { "true" } else { "false" }),
1831            Value::Int(n) | Value::BigInt(n) => s.push_str(&n.to_string()),
1832            Value::Float(f) => s.push_str(&f.to_string()),
1833            Value::Str(v) => {
1834                s.push('"');
1835                for ch in v.chars() {
1836                    match ch {
1837                        '"' => s.push_str("\\\""),
1838                        '\\' => s.push_str("\\\\"),
1839                        '\n' => s.push_str("\\n"),
1840                        '\r' => s.push_str("\\r"),
1841                        '\t' => s.push_str("\\t"),
1842                        c => s.push(c),
1843                    }
1844                }
1845                s.push('"');
1846            }
1847            Value::Array(items) => {
1848                s.push('[');
1849                for (i, item) in items.iter().enumerate() {
1850                    if i > 0 {
1851                        s.push_str(", ");
1852                    }
1853                    Self::value_to_json(item, s);
1854                }
1855                s.push(']');
1856            }
1857            // Date, DateTime, Time, Uuid, Json, Jsonb, etc. → string
1858            Value::Date(v)
1859            | Value::DateTime(v)
1860            | Value::Time(v)
1861            | Value::Uuid(v)
1862            | Value::Decimal(v)
1863            | Value::IpNetwork(v) => {
1864                s.push('"');
1865                s.push_str(v);
1866                s.push('"');
1867            }
1868            Value::Json(v) | Value::Jsonb(v) => {
1869                // Already JSON — embed directly
1870                s.push_str(v);
1871            }
1872            _ => s.push_str("null"),
1873        }
1874    }
1875
1876    fn sqlite_referential_action(
1877        &self,
1878        action: &ReferentialAction,
1879        ctx: &mut RenderCtx,
1880    ) -> RenderResult<()> {
1881        match action {
1882            ReferentialAction::NoAction => {
1883                ctx.keyword("NO ACTION");
1884            }
1885            ReferentialAction::Restrict => {
1886                ctx.keyword("RESTRICT");
1887            }
1888            ReferentialAction::Cascade => {
1889                ctx.keyword("CASCADE");
1890            }
1891            ReferentialAction::SetNull(cols) => {
1892                ctx.keyword("SET NULL");
1893                if cols.is_some() {
1894                    return Err(RenderError::unsupported(
1895                        "SetNullColumns",
1896                        "SQLite does not support SET NULL with column list.",
1897                    ));
1898                }
1899            }
1900            ReferentialAction::SetDefault(cols) => {
1901                ctx.keyword("SET DEFAULT");
1902                if cols.is_some() {
1903                    return Err(RenderError::unsupported(
1904                        "SetDefaultColumns",
1905                        "SQLite does not support SET DEFAULT with column list.",
1906                    ));
1907                }
1908            }
1909        }
1910        Ok(())
1911    }
1912
1913    fn sqlite_deferrable(&self, def: &DeferrableConstraint, ctx: &mut RenderCtx) {
1914        if def.deferrable {
1915            ctx.keyword("DEFERRABLE");
1916        } else {
1917            ctx.keyword("NOT DEFERRABLE");
1918        }
1919        if def.initially_deferred {
1920            ctx.keyword("INITIALLY DEFERRED");
1921        } else {
1922            ctx.keyword("INITIALLY IMMEDIATE");
1923        }
1924    }
1925
1926    fn sqlite_render_ctes(&self, ctes: &[CteDef], ctx: &mut RenderCtx) -> RenderResult<()> {
1927        self.render_ctes(ctes, ctx)
1928    }
1929
1930    fn sqlite_render_from_item(&self, item: &FromItem, ctx: &mut RenderCtx) -> RenderResult<()> {
1931        // SQLite ignores ONLY (PG-specific)
1932        self.render_from(&item.source, ctx)?;
1933        // Index hints
1934        if let Some(hint) = &item.index_hint {
1935            match hint {
1936                SqliteIndexHint::IndexedBy(name) => {
1937                    ctx.keyword("INDEXED BY").ident(name);
1938                }
1939                SqliteIndexHint::NotIndexed => {
1940                    ctx.keyword("NOT INDEXED");
1941                }
1942            }
1943        }
1944        // TABLESAMPLE
1945        if item.sample.is_some() {
1946            return Err(RenderError::unsupported(
1947                "TABLESAMPLE",
1948                "not supported in SQLite",
1949            ));
1950        }
1951        Ok(())
1952    }
1953
1954    fn sqlite_render_group_by(
1955        &self,
1956        items: &[GroupByItem],
1957        ctx: &mut RenderCtx,
1958    ) -> RenderResult<()> {
1959        ctx.keyword("GROUP BY");
1960        for (i, item) in items.iter().enumerate() {
1961            if i > 0 {
1962                ctx.comma();
1963            }
1964            match item {
1965                GroupByItem::Expr(expr) => {
1966                    self.render_expr(expr, ctx)?;
1967                }
1968                GroupByItem::Rollup(_) => {
1969                    return Err(RenderError::unsupported(
1970                        "ROLLUP",
1971                        "not supported in SQLite",
1972                    ));
1973                }
1974                GroupByItem::Cube(_) => {
1975                    return Err(RenderError::unsupported("CUBE", "not supported in SQLite"));
1976                }
1977                GroupByItem::GroupingSets(_) => {
1978                    return Err(RenderError::unsupported(
1979                        "GROUPING SETS",
1980                        "not supported in SQLite",
1981                    ));
1982                }
1983            }
1984        }
1985        Ok(())
1986    }
1987
1988    fn sqlite_render_window_clause(
1989        &self,
1990        windows: &[WindowNameDef],
1991        ctx: &mut RenderCtx,
1992    ) -> RenderResult<()> {
1993        ctx.keyword("WINDOW");
1994        for (i, win) in windows.iter().enumerate() {
1995            if i > 0 {
1996                ctx.comma();
1997            }
1998            ctx.ident(&win.name).keyword("AS").paren_open();
1999            if let Some(base) = &win.base_window {
2000                ctx.ident(base);
2001            }
2002            if let Some(partition_by) = &win.partition_by {
2003                ctx.keyword("PARTITION BY");
2004                for (j, expr) in partition_by.iter().enumerate() {
2005                    if j > 0 {
2006                        ctx.comma();
2007                    }
2008                    self.render_expr(expr, ctx)?;
2009                }
2010            }
2011            if let Some(order_by) = &win.order_by {
2012                ctx.keyword("ORDER BY");
2013                self.sqlite_order_by_list(order_by, ctx)?;
2014            }
2015            if let Some(frame) = &win.frame {
2016                self.sqlite_window_frame(frame, ctx);
2017            }
2018            ctx.paren_close();
2019        }
2020        Ok(())
2021    }
2022
2023    fn sqlite_window_frame(&self, frame: &WindowFrameDef, ctx: &mut RenderCtx) {
2024        ctx.keyword(match frame.frame_type {
2025            WindowFrameType::Rows => "ROWS",
2026            WindowFrameType::Range => "RANGE",
2027            WindowFrameType::Groups => "GROUPS",
2028        });
2029        if let Some(end) = &frame.end {
2030            ctx.keyword("BETWEEN");
2031            self.sqlite_frame_bound(&frame.start, ctx);
2032            ctx.keyword("AND");
2033            self.sqlite_frame_bound(end, ctx);
2034        } else {
2035            self.sqlite_frame_bound(&frame.start, ctx);
2036        }
2037    }
2038
2039    fn sqlite_frame_bound(&self, bound: &WindowFrameBound, ctx: &mut RenderCtx) {
2040        match bound {
2041            WindowFrameBound::CurrentRow => {
2042                ctx.keyword("CURRENT ROW");
2043            }
2044            WindowFrameBound::Preceding(None) => {
2045                ctx.keyword("UNBOUNDED PRECEDING");
2046            }
2047            WindowFrameBound::Preceding(Some(n)) => {
2048                ctx.keyword(&n.to_string()).keyword("PRECEDING");
2049            }
2050            WindowFrameBound::Following(None) => {
2051                ctx.keyword("UNBOUNDED FOLLOWING");
2052            }
2053            WindowFrameBound::Following(Some(n)) => {
2054                ctx.keyword(&n.to_string()).keyword("FOLLOWING");
2055            }
2056        }
2057    }
2058
2059    fn sqlite_render_set_op(&self, set_op: &SetOpDef, ctx: &mut RenderCtx) -> RenderResult<()> {
2060        self.render_query(&set_op.left, ctx)?;
2061        ctx.keyword(match set_op.operation {
2062            SetOperationType::Union => "UNION",
2063            SetOperationType::UnionAll => "UNION ALL",
2064            SetOperationType::Intersect => "INTERSECT",
2065            SetOperationType::Except => "EXCEPT",
2066            SetOperationType::IntersectAll => {
2067                return Err(RenderError::unsupported(
2068                    "INTERSECT ALL",
2069                    "not supported in SQLite",
2070                ));
2071            }
2072            SetOperationType::ExceptAll => {
2073                return Err(RenderError::unsupported(
2074                    "EXCEPT ALL",
2075                    "not supported in SQLite",
2076                ));
2077            }
2078        });
2079        self.render_query(&set_op.right, ctx)
2080    }
2081
2082    fn sqlite_create_table(
2083        &self,
2084        schema: &SchemaDef,
2085        if_not_exists: bool,
2086        temporary: bool,
2087        without_rowid: bool,
2088        strict: bool,
2089        ctx: &mut RenderCtx,
2090    ) -> RenderResult<()> {
2091        ctx.keyword("CREATE");
2092        if temporary {
2093            ctx.keyword("TEMP");
2094        }
2095        ctx.keyword("TABLE");
2096        if if_not_exists {
2097            ctx.keyword("IF NOT EXISTS");
2098        }
2099        if let Some(ns) = &schema.namespace {
2100            ctx.ident(ns).operator(".");
2101        }
2102        ctx.ident(&schema.name);
2103
2104        // Collect PK column names for identity detection
2105        let pk_columns: Vec<&str> = schema
2106            .constraints
2107            .as_ref()
2108            .and_then(|cs| {
2109                cs.iter().find_map(|c| {
2110                    if let ConstraintDef::PrimaryKey { columns, .. } = c {
2111                        Some(columns.iter().map(|s| s.as_str()).collect::<Vec<_>>())
2112                    } else {
2113                        None
2114                    }
2115                })
2116            })
2117            .unwrap_or_default();
2118
2119        // Find identity column that should be rendered as PRIMARY KEY AUTOINCREMENT
2120        let identity_pk_col = schema.columns.iter().find_map(|col| {
2121            if col.identity.is_some() {
2122                if pk_columns.contains(&col.name.as_str()) {
2123                    Some(col.name.as_str())
2124                } else {
2125                    None
2126                }
2127            } else {
2128                None
2129            }
2130        });
2131
2132        // Validate: identity without PK is an error in SQLite
2133        for col in &schema.columns {
2134            if col.identity.is_some() && !pk_columns.contains(&col.name.as_str()) {
2135                return Err(RenderError::unsupported(
2136                    "IdentityColumn",
2137                    "SQLite requires identity columns to be PRIMARY KEY. Add a PrimaryKey constraint for this column.",
2138                ));
2139            }
2140        }
2141
2142        ctx.paren_open();
2143        let mut first = true;
2144        for col in &schema.columns {
2145            if !first {
2146                ctx.comma();
2147            }
2148            first = false;
2149            self.render_column_def(col, ctx)?;
2150            // Inline PRIMARY KEY AUTOINCREMENT on the identity column
2151            if identity_pk_col == Some(col.name.as_str()) {
2152                ctx.keyword("PRIMARY KEY AUTOINCREMENT");
2153            }
2154        }
2155        if let Some(constraints) = &schema.constraints {
2156            for constraint in constraints {
2157                // Skip single-column PK if it was inlined with AUTOINCREMENT
2158                if let ConstraintDef::PrimaryKey { columns, .. } = constraint {
2159                    if columns.len() == 1 && identity_pk_col == Some(columns[0].as_str()) {
2160                        continue;
2161                    }
2162                }
2163                if !first {
2164                    ctx.comma();
2165                }
2166                first = false;
2167                self.render_constraint(constraint, ctx)?;
2168            }
2169        }
2170        ctx.paren_close();
2171
2172        // SQLite table modifiers
2173        let mut modifiers = Vec::new();
2174        if without_rowid {
2175            modifiers.push("WITHOUT ROWID");
2176        }
2177        if strict {
2178            modifiers.push("STRICT");
2179        }
2180        if !modifiers.is_empty() {
2181            for (i, m) in modifiers.iter().enumerate() {
2182                if i > 0 {
2183                    ctx.comma();
2184                }
2185                ctx.keyword(m);
2186            }
2187        }
2188
2189        Ok(())
2190    }
2191
2192    fn sqlite_create_index(
2193        &self,
2194        schema_ref: &qcraft_core::ast::common::SchemaRef,
2195        index: &IndexDef,
2196        if_not_exists: bool,
2197        ctx: &mut RenderCtx,
2198    ) -> RenderResult<()> {
2199        ctx.keyword("CREATE");
2200        if index.unique {
2201            ctx.keyword("UNIQUE");
2202        }
2203        ctx.keyword("INDEX");
2204        if if_not_exists {
2205            ctx.keyword("IF NOT EXISTS");
2206        }
2207        ctx.ident(&index.name).keyword("ON");
2208        self.sqlite_schema_ref(schema_ref, ctx);
2209
2210        // SQLite doesn't support USING method — Ignore index_type
2211
2212        ctx.paren_open();
2213        self.sqlite_index_columns(&index.columns, ctx)?;
2214        ctx.paren_close();
2215
2216        // SQLite doesn't support INCLUDE, NULLS DISTINCT, WITH params, TABLESPACE — Ignore
2217
2218        if let Some(condition) = &index.condition {
2219            ctx.keyword("WHERE");
2220            self.render_condition(condition, ctx)?;
2221        }
2222
2223        Ok(())
2224    }
2225
2226    fn sqlite_index_columns(
2227        &self,
2228        columns: &[IndexColumnDef],
2229        ctx: &mut RenderCtx,
2230    ) -> RenderResult<()> {
2231        for (i, col) in columns.iter().enumerate() {
2232            if i > 0 {
2233                ctx.comma();
2234            }
2235            match &col.expr {
2236                IndexExpr::Column(name) => {
2237                    ctx.ident(name);
2238                }
2239                IndexExpr::Expression(expr) => {
2240                    ctx.paren_open();
2241                    self.render_expr(expr, ctx)?;
2242                    ctx.paren_close();
2243                }
2244            }
2245            if let Some(collation) = &col.collation {
2246                ctx.keyword("COLLATE").ident(collation);
2247            }
2248            // SQLite doesn't support operator classes — Ignore opclass
2249            if let Some(dir) = col.direction {
2250                ctx.keyword(match dir {
2251                    OrderDir::Asc => "ASC",
2252                    OrderDir::Desc => "DESC",
2253                });
2254            }
2255            // SQLite doesn't support NULLS FIRST/LAST — Ignore
2256        }
2257        Ok(())
2258    }
2259
2260    fn sqlite_order_by_list(
2261        &self,
2262        order_by: &[OrderByDef],
2263        ctx: &mut RenderCtx,
2264    ) -> RenderResult<()> {
2265        for (i, ob) in order_by.iter().enumerate() {
2266            if i > 0 {
2267                ctx.comma();
2268            }
2269            self.render_expr(&ob.expr, ctx)?;
2270            ctx.keyword(match ob.direction {
2271                OrderDir::Asc => "ASC",
2272                OrderDir::Desc => "DESC",
2273            });
2274            if let Some(nulls) = &ob.nulls {
2275                ctx.keyword(match nulls {
2276                    NullsOrder::First => "NULLS FIRST",
2277                    NullsOrder::Last => "NULLS LAST",
2278                });
2279            }
2280        }
2281        Ok(())
2282    }
2283}