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