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