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