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