Skip to main content

fsqlite_planner/
codegen.rs

1//! AST-to-VDBE bytecode compilation (§10.6).
2//!
3//! Translates parsed SQL statements into VDBE register-based instructions
4//! using the `ProgramBuilder` from `fsqlite-types`. Handles SELECT, INSERT,
5//! UPDATE, and DELETE with correct opcode patterns matching C SQLite behavior.
6
7use fsqlite_ast::{
8    BinaryOp as AstBinaryOp, ColumnRef, ConflictAction, DeleteStatement, Expr, FunctionArgs,
9    InsertSource, InsertStatement, Literal, PlaceholderType, QualifiedTableRef, ResultColumn,
10    SelectCore, SelectStatement, Span, UnaryOp as AstUnaryOp, UpdateStatement,
11};
12use fsqlite_parser::expr::parse_expr as parse_sql_expr;
13use fsqlite_types::opcode::{Label, Opcode, P4, ProgramBuilder};
14
15// ---------------------------------------------------------------------------
16// INSERT conflict-mode p5 flags (must match fsqlite-vdbe/src/codegen.rs)
17// ---------------------------------------------------------------------------
18
19/// ROLLBACK on conflict.
20const OE_ROLLBACK: u16 = 1;
21/// ABORT on conflict (default).
22const OE_ABORT: u16 = 2;
23/// FAIL on conflict.
24const OE_FAIL: u16 = 3;
25/// IGNORE conflicting row.
26const OE_IGNORE: u16 = 4;
27/// REPLACE conflicting row.
28const OE_REPLACE: u16 = 5;
29
30/// Convert AST `ConflictAction` to p5 OE_* flag value.
31fn conflict_action_to_oe(action: Option<&ConflictAction>) -> u16 {
32    match action {
33        Some(ConflictAction::Rollback) => OE_ROLLBACK,
34        None | Some(ConflictAction::Abort) => OE_ABORT,
35        Some(ConflictAction::Fail) => OE_FAIL,
36        Some(ConflictAction::Ignore) => OE_IGNORE,
37        Some(ConflictAction::Replace) => OE_REPLACE,
38    }
39}
40
41// ---------------------------------------------------------------------------
42// Schema metadata (minimal info needed for codegen)
43// ---------------------------------------------------------------------------
44
45/// Column metadata needed by the code generator.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct ColumnInfo {
48    /// Column name.
49    pub name: String,
50    /// Type affinity character: 'd' (integer), 'e' (real), 'B' (blob),
51    /// 'C' (text), 'A' (numeric). Lowercase = exact, uppercase = heuristic.
52    pub affinity: char,
53    /// Default value expression as SQL text (e.g. "'open'", "0").
54    pub default_value: Option<String>,
55}
56
57/// Index metadata needed for codegen (index-scan SELECT).
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct IndexSchema {
60    /// Index name.
61    pub name: String,
62    /// Root page number.
63    pub root_page: i32,
64    /// Indexed column names (leftmost first).
65    pub columns: Vec<String>,
66    /// Whether this index enforces a UNIQUE constraint.
67    pub is_unique: bool,
68}
69
70/// Minimal table schema needed by the code generator.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct TableSchema {
73    /// Table name.
74    pub name: String,
75    /// Root page of the table's B-tree.
76    pub root_page: i32,
77    /// Column definitions in storage order.
78    pub columns: Vec<ColumnInfo>,
79    /// Available indexes.
80    pub indexes: Vec<IndexSchema>,
81}
82
83impl TableSchema {
84    /// Build an affinity string for `MakeRecord` (one char per column).
85    #[must_use]
86    pub fn affinity_string(&self) -> String {
87        self.columns.iter().map(|c| c.affinity).collect()
88    }
89
90    /// Find a column's 0-based index by name (case-insensitive).
91    #[must_use]
92    pub fn column_index(&self, name: &str) -> Option<usize> {
93        self.columns
94            .iter()
95            .position(|c| c.name.eq_ignore_ascii_case(name))
96    }
97
98    /// Find an index by a column name (returns first index whose leftmost
99    /// column matches).
100    #[must_use]
101    pub fn index_for_column(&self, col_name: &str) -> Option<&IndexSchema> {
102        self.indexes.iter().find(|idx| {
103            idx.columns
104                .first()
105                .is_some_and(|c| c.eq_ignore_ascii_case(col_name))
106        })
107    }
108}
109
110/// Configuration for the code generator.
111#[derive(Debug, Clone, Default, PartialEq, Eq)]
112pub struct CodegenContext {
113    /// Whether we're in `BEGIN CONCURRENT` mode.
114    /// When true, `OP_NewRowid` uses the snapshot-independent allocator.
115    pub concurrent_mode: bool,
116}
117
118/// Errors during code generation.
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub enum CodegenError {
121    /// Table not found in schema.
122    TableNotFound(String),
123    /// Column not found in table.
124    ColumnNotFound { table: String, column: String },
125    /// Unsupported AST construct for this codegen pass.
126    Unsupported(String),
127}
128
129impl std::fmt::Display for CodegenError {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        match self {
132            Self::TableNotFound(name) => write!(f, "table not found: {name}"),
133            Self::ColumnNotFound { table, column } => {
134                write!(f, "column {column} not found in table {table}")
135            }
136            Self::Unsupported(msg) => write!(f, "unsupported: {msg}"),
137        }
138    }
139}
140
141impl std::error::Error for CodegenError {}
142
143// ---------------------------------------------------------------------------
144// Schema lookup helper
145// ---------------------------------------------------------------------------
146
147fn find_table<'a>(schema: &'a [TableSchema], name: &str) -> Result<&'a TableSchema, CodegenError> {
148    schema
149        .iter()
150        .find(|t| t.name.eq_ignore_ascii_case(name))
151        .ok_or_else(|| CodegenError::TableNotFound(name.to_owned()))
152}
153
154fn table_name_from_qualified(qtr: &QualifiedTableRef) -> &str {
155    &qtr.name.name
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159enum BindParamRef {
160    Anonymous,
161    Numbered(i32),
162}
163
164// ---------------------------------------------------------------------------
165// SELECT codegen
166// ---------------------------------------------------------------------------
167
168/// Generate VDBE bytecode for a SELECT statement.
169///
170/// Handles three patterns:
171/// 1. **Expression-only**: `SELECT 1+1`, `SELECT abs(-5)` (no FROM)
172/// 2. **Rowid lookup**: `SELECT cols FROM t WHERE rowid = ?`
173/// 3. **Full table scan**: `SELECT cols FROM t`
174///
175/// Returns the cursor number used (for composability).
176#[allow(clippy::too_many_lines)]
177pub fn codegen_select(
178    b: &mut ProgramBuilder,
179    stmt: &SelectStatement,
180    schema: &[TableSchema],
181    _ctx: &CodegenContext,
182) -> Result<(), CodegenError> {
183    let core = match &stmt.body.select {
184        SelectCore::Select { .. } => &stmt.body.select,
185        SelectCore::Values(rows) => return codegen_select_values(b, rows),
186    };
187
188    let (columns, from, where_clause) = match core {
189        SelectCore::Select {
190            columns,
191            from,
192            where_clause,
193            ..
194        } => (columns, from, where_clause),
195        SelectCore::Values(_) => unreachable!(),
196    };
197
198    // Handle SELECT without FROM (expression-only queries like SELECT 1+1).
199    if from.is_none() {
200        return codegen_select_no_from(b, columns);
201    }
202    let from_clause = from.as_ref().expect("checked above");
203
204    let table_name = single_table_select_source_name(&from_clause.source)?;
205
206    let table = find_table(schema, table_name)?;
207    let cursor = 0_i32;
208
209    // Labels for control flow.
210    let end_label = b.emit_label();
211    let done_label = b.emit_label();
212
213    // Init: jump to end (standard SQLite pattern).
214    b.emit_jump_to_label(Opcode::Init, 0, 0, end_label, P4::None, 0);
215
216    // Transaction (read-only, p2=0).
217    b.emit_op(Opcode::Transaction, 0, 0, 0, P4::None, 0);
218
219    // Determine output columns and allocate registers.
220    let out_col_count = result_column_count(columns, table);
221    let out_regs = b.alloc_regs(out_col_count);
222
223    // Check for rowid-equality WHERE clause.
224    let rowid_param = extract_rowid_bind_param(where_clause.as_deref());
225    // Check for index-usable WHERE clause.
226    let index_eq = if rowid_param.is_none() {
227        extract_column_eq_bind(where_clause.as_deref())
228    } else {
229        None
230    };
231    if where_clause.is_some() && rowid_param.is_none() && index_eq.is_none() {
232        return Err(CodegenError::Unsupported(
233            "SELECT WHERE currently supports only `rowid = ?` or `indexed_col = ?`".to_owned(),
234        ));
235    }
236
237    let mut index_cursor_to_close: Option<i32> = None;
238
239    if let Some(param_idx) = rowid_param {
240        // --- Rowid-seek SELECT ---
241        let rowid_reg = b.alloc_reg();
242        b.emit_op(Opcode::Variable, param_idx, rowid_reg, 0, P4::None, 0);
243        b.emit_op(
244            Opcode::OpenRead,
245            cursor,
246            table.root_page,
247            0,
248            P4::Table(table.name.clone()),
249            0,
250        );
251        b.emit_jump_to_label(
252            Opcode::SeekRowid,
253            cursor,
254            rowid_reg,
255            done_label,
256            P4::None,
257            0,
258        );
259
260        // Read columns.
261        emit_column_reads(b, cursor, columns, table, out_regs)?;
262
263        // ResultRow.
264        b.emit_op(Opcode::ResultRow, out_regs, out_col_count, 0, P4::None, 0);
265    } else if let Some((col_name, param_idx)) = &index_eq {
266        // --- Index-seek SELECT ---
267        let Some(idx_schema) = table.index_for_column(col_name) else {
268            return codegen_select_column_eq_scan(
269                b,
270                cursor,
271                table,
272                columns,
273                out_regs,
274                out_col_count,
275                done_label,
276                end_label,
277                col_name,
278                *param_idx,
279            );
280        };
281        let idx_cursor = 1_i32;
282        index_cursor_to_close = Some(idx_cursor);
283        let param_reg = b.alloc_reg();
284        b.emit_op(Opcode::Variable, *param_idx, param_reg, 0, P4::None, 0);
285        b.emit_jump_to_label(Opcode::IsNull, param_reg, 0, done_label, P4::None, 0);
286
287        // Build probe key `[value, i64::MIN]` so SeekGE lands at first duplicate.
288        let min_rowid_reg = b.alloc_reg();
289        b.emit_op(Opcode::Int64, 0, min_rowid_reg, 0, P4::Int64(i64::MIN), 0);
290        let probe_key_reg = b.alloc_reg();
291        b.emit_op(Opcode::MakeRecord, param_reg, 2, probe_key_reg, P4::None, 0);
292
293        b.emit_op(
294            Opcode::OpenRead,
295            cursor,
296            table.root_page,
297            0,
298            P4::Table(table.name.clone()),
299            0,
300        );
301        b.emit_op(
302            Opcode::OpenRead,
303            idx_cursor,
304            idx_schema.root_page,
305            0,
306            P4::Index(idx_schema.name.clone()),
307            0,
308        );
309        b.emit_jump_to_label(
310            Opcode::SeekGE,
311            idx_cursor,
312            probe_key_reg,
313            done_label,
314            P4::None,
315            0,
316        );
317
318        let loop_start = b.current_addr();
319        b.emit_jump_to_label(
320            Opcode::IdxGT,
321            idx_cursor,
322            probe_key_reg,
323            done_label,
324            P4::None,
325            1,
326        );
327
328        let rowid_reg = b.alloc_reg();
329        b.emit_op(Opcode::IdxRowid, idx_cursor, rowid_reg, 0, P4::None, 0);
330        let skip_row_label = b.emit_label();
331        b.emit_jump_to_label(
332            Opcode::SeekRowid,
333            cursor,
334            rowid_reg,
335            skip_row_label,
336            P4::None,
337            0,
338        );
339
340        // Read columns.
341        emit_column_reads(b, cursor, columns, table, out_regs)?;
342
343        // ResultRow.
344        b.emit_op(Opcode::ResultRow, out_regs, out_col_count, 0, P4::None, 0);
345        b.resolve_label(skip_row_label);
346
347        #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
348        let loop_target = loop_start as i32;
349        b.emit_op(Opcode::Next, idx_cursor, loop_target, 0, P4::None, 0);
350    } else {
351        // --- Full table scan ---
352        return codegen_select_full_scan(
353            b,
354            cursor,
355            table,
356            columns,
357            out_regs,
358            out_col_count,
359            done_label,
360            end_label,
361        );
362    }
363
364    // Done: Close + Halt.
365    b.resolve_label(done_label);
366    if let Some(idx_cursor) = index_cursor_to_close {
367        b.emit_op(Opcode::Close, idx_cursor, 0, 0, P4::None, 0);
368    }
369    b.emit_op(Opcode::Close, cursor, 0, 0, P4::None, 0);
370    b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
371
372    // End target for Init jump.
373    b.resolve_label(end_label);
374
375    Ok(())
376}
377
378/// Codegen for a top-level `VALUES (..), (..)` SELECT core.
379fn codegen_select_values(b: &mut ProgramBuilder, rows: &[Vec<Expr>]) -> Result<(), CodegenError> {
380    let end_label = b.emit_label();
381
382    // Init: jump to end.
383    b.emit_jump_to_label(Opcode::Init, 0, 0, end_label, P4::None, 0);
384
385    // Read-only transaction.
386    b.emit_op(Opcode::Transaction, 0, 0, 0, P4::None, 0);
387
388    let Some(first_row) = rows.first() else {
389        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
390        b.resolve_label(end_label);
391        return Ok(());
392    };
393
394    let out_col_count = i32::try_from(first_row.len())
395        .map_err(|_| CodegenError::Unsupported("VALUES row has too many columns".to_owned()))?;
396
397    if rows.iter().any(|row| row.len() != first_row.len()) {
398        return Err(CodegenError::Unsupported(
399            "VALUES rows must have the same arity".to_owned(),
400        ));
401    }
402
403    let out_regs = b.alloc_regs(out_col_count);
404    for row in rows {
405        for (reg, expr) in (out_regs..).zip(row.iter()) {
406            emit_expr(b, expr, reg)?;
407        }
408        b.emit_op(Opcode::ResultRow, out_regs, out_col_count, 0, P4::None, 0);
409    }
410
411    b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
412
413    // End target for Init jump.
414    b.resolve_label(end_label);
415
416    Ok(())
417}
418
419fn single_table_select_source_name(
420    source: &fsqlite_ast::TableOrSubquery,
421) -> Result<&str, CodegenError> {
422    match source {
423        fsqlite_ast::TableOrSubquery::Table { name, .. } => Ok(&name.name),
424        fsqlite_ast::TableOrSubquery::ParenJoin(inner) if inner.joins.is_empty() => {
425            single_table_select_source_name(&inner.source)
426        }
427        fsqlite_ast::TableOrSubquery::ParenJoin(_) => Err(CodegenError::Unsupported(
428            "parenthesized JOIN source in single-table SELECT".to_owned(),
429        )),
430        fsqlite_ast::TableOrSubquery::Subquery { .. } => Err(CodegenError::Unsupported(
431            "subquery FROM source in single-table SELECT".to_owned(),
432        )),
433        fsqlite_ast::TableOrSubquery::TableFunction { .. } => Err(CodegenError::Unsupported(
434            "table-valued function FROM source in single-table SELECT".to_owned(),
435        )),
436    }
437}
438
439/// Codegen for a full table scan SELECT.
440#[allow(clippy::too_many_arguments)]
441fn codegen_select_full_scan(
442    b: &mut ProgramBuilder,
443    cursor: i32,
444    table: &TableSchema,
445    columns: &[ResultColumn],
446    out_regs: i32,
447    out_col_count: i32,
448    done_label: Label,
449    end_label: Label,
450) -> Result<(), CodegenError> {
451    b.emit_op(
452        Opcode::OpenRead,
453        cursor,
454        table.root_page,
455        0,
456        P4::Table(table.name.clone()),
457        0,
458    );
459
460    // Rewind to first row; jump to done if table is empty.
461    let loop_start = b.current_addr();
462    b.emit_jump_to_label(Opcode::Rewind, cursor, 0, done_label, P4::None, 0);
463
464    // Read columns.
465    emit_column_reads(b, cursor, columns, table, out_regs)?;
466
467    // ResultRow.
468    b.emit_op(Opcode::ResultRow, out_regs, out_col_count, 0, P4::None, 0);
469
470    // Next: jump back to start of loop body (the instruction after Rewind).
471    #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
472    let loop_body = (loop_start + 1) as i32;
473    b.emit_op(Opcode::Next, cursor, loop_body, 0, P4::None, 0);
474
475    // Done: Close + Halt.
476    b.resolve_label(done_label);
477    b.emit_op(Opcode::Close, cursor, 0, 0, P4::None, 0);
478    b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
479
480    // End target for Init jump.
481    b.resolve_label(end_label);
482
483    Ok(())
484}
485
486/// Codegen for `SELECT ... FROM table WHERE column = ?` when no usable index exists.
487#[allow(
488    clippy::too_many_arguments,
489    clippy::cast_possible_truncation,
490    clippy::cast_possible_wrap
491)]
492fn codegen_select_column_eq_scan(
493    b: &mut ProgramBuilder,
494    cursor: i32,
495    table: &TableSchema,
496    columns: &[ResultColumn],
497    out_regs: i32,
498    out_col_count: i32,
499    done_label: Label,
500    end_label: Label,
501    filter_column: &str,
502    param_idx: i32,
503) -> Result<(), CodegenError> {
504    let filter_col_idx =
505        table
506            .column_index(filter_column)
507            .ok_or_else(|| CodegenError::ColumnNotFound {
508                table: table.name.clone(),
509                column: filter_column.to_owned(),
510            })?;
511    let param_reg = b.alloc_reg();
512    let value_reg = b.alloc_reg();
513    b.emit_op(Opcode::Variable, param_idx, param_reg, 0, P4::None, 0);
514    b.emit_op(
515        Opcode::OpenRead,
516        cursor,
517        table.root_page,
518        0,
519        P4::Table(table.name.clone()),
520        0,
521    );
522
523    let loop_start = b.current_addr();
524    b.emit_jump_to_label(Opcode::Rewind, cursor, 0, done_label, P4::None, 0);
525
526    let skip_row_label = b.emit_label();
527    b.emit_op(
528        Opcode::Column,
529        cursor,
530        filter_col_idx as i32,
531        value_reg,
532        P4::None,
533        0,
534    );
535    b.emit_jump_to_label(
536        Opcode::Ne,
537        param_reg,
538        value_reg,
539        skip_row_label,
540        P4::None,
541        0x10,
542    );
543    emit_column_reads(b, cursor, columns, table, out_regs)?;
544    b.emit_op(Opcode::ResultRow, out_regs, out_col_count, 0, P4::None, 0);
545    b.resolve_label(skip_row_label);
546
547    let loop_body = (loop_start + 1) as i32;
548    b.emit_op(Opcode::Next, cursor, loop_body, 0, P4::None, 0);
549
550    b.resolve_label(done_label);
551    b.emit_op(Opcode::Close, cursor, 0, 0, P4::None, 0);
552    b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
553    b.resolve_label(end_label);
554
555    Ok(())
556}
557
558/// Codegen for `SELECT <expr>, ...` without a FROM clause.
559///
560/// Examples: `SELECT 1+1`, `SELECT abs(-5)`, `SELECT CURRENT_TIMESTAMP`.
561/// Produces exactly one result row with the evaluated expressions.
562#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
563fn codegen_select_no_from(
564    b: &mut ProgramBuilder,
565    columns: &[ResultColumn],
566) -> Result<(), CodegenError> {
567    // Star or TableStar don't make sense without a table.
568    for col in columns {
569        if matches!(col, ResultColumn::Star | ResultColumn::TableStar(_)) {
570            return Err(CodegenError::Unsupported(
571                "SELECT * without FROM".to_owned(),
572            ));
573        }
574    }
575
576    let out_col_count = columns.len() as i32;
577    let end_label = b.emit_label();
578
579    // Init: jump to end.
580    b.emit_jump_to_label(Opcode::Init, 0, 0, end_label, P4::None, 0);
581
582    // Read-only transaction.
583    b.emit_op(Opcode::Transaction, 0, 0, 0, P4::None, 0);
584
585    // Allocate output registers and evaluate each expression.
586    let out_regs = b.alloc_regs(out_col_count);
587    for (reg, col) in (out_regs..).zip(columns.iter()) {
588        if let ResultColumn::Expr { expr, .. } = col {
589            emit_expr(b, expr, reg)?;
590        }
591    }
592
593    // Emit a single result row.
594    b.emit_op(Opcode::ResultRow, out_regs, out_col_count, 0, P4::None, 0);
595
596    // Halt.
597    b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
598
599    // End target for Init jump.
600    b.resolve_label(end_label);
601
602    Ok(())
603}
604
605// ---------------------------------------------------------------------------
606// INSERT codegen
607// ---------------------------------------------------------------------------
608
609/// Generate VDBE bytecode for an INSERT statement.
610///
611/// Pattern: `INSERT INTO t VALUES (?, ?, ...)`
612///
613/// Init → Transaction(write) → OpenWrite → NewRowid → Variable* →
614/// MakeRecord → Insert → Close → Halt
615pub fn codegen_insert(
616    b: &mut ProgramBuilder,
617    stmt: &InsertStatement,
618    schema: &[TableSchema],
619    ctx: &CodegenContext,
620) -> Result<(), CodegenError> {
621    let table = find_table(schema, &stmt.table.name)?;
622    let cursor = 0_i32;
623
624    let end_label = b.emit_label();
625
626    // Init.
627    b.emit_jump_to_label(Opcode::Init, 0, 0, end_label, P4::None, 0);
628
629    // Transaction (write, p2=1).
630    b.emit_op(Opcode::Transaction, 0, 1, 0, P4::None, 0);
631
632    // OpenWrite.
633    b.emit_op(
634        Opcode::OpenWrite,
635        cursor,
636        table.root_page,
637        0,
638        P4::Table(table.name.clone()),
639        0,
640    );
641
642    let oe_flag = conflict_action_to_oe(stmt.or_conflict.as_ref());
643
644    match &stmt.source {
645        InsertSource::Values(rows) => {
646            if rows.is_empty() {
647                return Err(CodegenError::Unsupported("empty VALUES".to_owned()));
648            }
649            codegen_insert_values(
650                b,
651                rows,
652                &stmt.columns,
653                cursor,
654                table,
655                &stmt.returning,
656                ctx,
657                oe_flag,
658            )?;
659        }
660        InsertSource::Select(select_stmt) => {
661            codegen_insert_select(
662                b,
663                select_stmt,
664                &stmt.columns,
665                cursor,
666                table,
667                schema,
668                &stmt.returning,
669                ctx,
670                oe_flag,
671            )?;
672        }
673        InsertSource::DefaultValues => {
674            codegen_insert_default_values(b, cursor, table, &stmt.returning, ctx, oe_flag)?;
675        }
676    }
677
678    // Close + Halt.
679    b.emit_op(Opcode::Close, cursor, 0, 0, P4::None, 0);
680    b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
681
682    // End label.
683    b.resolve_label(end_label);
684
685    Ok(())
686}
687
688fn insert_target_indices(
689    insert_columns: &[String],
690    table: &TableSchema,
691) -> Result<Vec<usize>, CodegenError> {
692    if insert_columns.is_empty() {
693        return Ok((0..table.columns.len()).collect());
694    }
695
696    insert_columns
697        .iter()
698        .map(|column| {
699            table
700                .column_index(column)
701                .ok_or_else(|| CodegenError::ColumnNotFound {
702                    table: table.name.clone(),
703                    column: column.clone(),
704                })
705        })
706        .collect()
707}
708
709fn insert_default_exprs(table: &TableSchema) -> Result<Vec<Expr>, CodegenError> {
710    table
711        .columns
712        .iter()
713        .map(|col| default_value_to_expr(table, col))
714        .collect()
715}
716
717/// Convert a column's DEFAULT value SQL text to an AST expression.
718fn default_value_to_expr(table: &TableSchema, col: &ColumnInfo) -> Result<Expr, CodegenError> {
719    let span = Span::ZERO;
720    let Some(dv) = col.default_value.as_deref() else {
721        return Ok(Expr::Literal(Literal::Null, span));
722    };
723    parse_default_expr(dv).map_err(|err| {
724        CodegenError::Unsupported(format!(
725            "failed to parse DEFAULT expression `{}` for {}.{}: {err}",
726            dv.trim(),
727            table.name,
728            col.name
729        ))
730    })
731}
732
733/// Parse column DEFAULT SQL text into an expression AST.
734fn parse_default_expr(default_sql: &str) -> Result<Expr, fsqlite_parser::ParseError> {
735    let trimmed = default_sql.trim();
736    parse_sql_expr(trimmed)
737}
738
739fn expand_insert_values_row(
740    row_values: &[Expr],
741    insert_columns: &[String],
742    table: &TableSchema,
743) -> Result<Vec<Expr>, CodegenError> {
744    if insert_columns.is_empty() {
745        return Ok(row_values.to_vec());
746    }
747
748    let target_indices = insert_target_indices(insert_columns, table)?;
749    if row_values.len() != target_indices.len() {
750        return Err(CodegenError::Unsupported(format!(
751            "INSERT VALUES column count mismatch: {} expressions for {} target columns",
752            row_values.len(),
753            target_indices.len(),
754        )));
755    }
756
757    let mut expanded = insert_default_exprs(table)?;
758    let mut filled_targets = vec![false; table.columns.len()];
759    for (source_idx, target_idx) in target_indices.into_iter().enumerate() {
760        if filled_targets[target_idx] {
761            continue;
762        }
763        expanded[target_idx] = row_values[source_idx].clone();
764        filled_targets[target_idx] = true;
765    }
766    Ok(expanded)
767}
768
769#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
770fn emit_insert_target_regs_from_source(
771    b: &mut ProgramBuilder,
772    source_reg_base: i32,
773    source_count: usize,
774    insert_columns: &[String],
775    target_table: &TableSchema,
776) -> Result<(i32, i32), CodegenError> {
777    if insert_columns.is_empty() {
778        let count = i32::try_from(source_count)
779            .map_err(|_| CodegenError::Unsupported("too many INSERT source columns".to_owned()))?;
780        return Ok((source_reg_base, count));
781    }
782
783    let target_indices = insert_target_indices(insert_columns, target_table)?;
784    if source_count != target_indices.len() {
785        return Err(CodegenError::Unsupported(format!(
786            "INSERT source column count mismatch: {source_count} values for {} target columns",
787            target_indices.len(),
788        )));
789    }
790
791    let target_count = i32::try_from(target_table.columns.len())
792        .map_err(|_| CodegenError::Unsupported("too many target columns".to_owned()))?;
793    let val_regs = b.alloc_regs(target_count);
794    let default_exprs = insert_default_exprs(target_table)?;
795    let mut source_for_target = vec![None; target_table.columns.len()];
796    for (source_idx, target_idx) in target_indices.into_iter().enumerate() {
797        source_for_target[target_idx].get_or_insert(source_idx);
798    }
799
800    for (target_idx, default_expr) in default_exprs.iter().enumerate() {
801        let target_reg = val_regs + i32::try_from(target_idx).unwrap_or(0);
802        if let Some(source_idx) = source_for_target[target_idx] {
803            let source_reg = source_reg_base + i32::try_from(source_idx).unwrap_or(0);
804            b.emit_op(Opcode::Copy, source_reg, target_reg, 0, P4::None, 0);
805        } else {
806            emit_expr(b, default_expr, target_reg)?;
807        }
808    }
809
810    Ok((val_regs, target_count))
811}
812
813/// Emit the INSERT loop for `VALUES (row), (row), ...` (planner path).
814#[allow(
815    clippy::too_many_arguments,
816    clippy::cast_possible_truncation,
817    clippy::cast_possible_wrap,
818    clippy::unnecessary_wraps
819)]
820fn codegen_insert_values(
821    b: &mut ProgramBuilder,
822    rows: &[Vec<Expr>],
823    insert_columns: &[String],
824    cursor: i32,
825    table: &TableSchema,
826    returning: &[ResultColumn],
827    ctx: &CodegenContext,
828    oe_flag: u16,
829) -> Result<(), CodegenError> {
830    let rowid_reg = b.alloc_reg();
831    let concurrent_flag = i32::from(ctx.concurrent_mode);
832
833    let mut param_idx = 1_i32;
834
835    for row_values in rows {
836        let expanded_values = expand_insert_values_row(row_values, insert_columns, table)?;
837        let n_cols = expanded_values.len();
838        let val_regs = b.alloc_regs(n_cols as i32);
839        b.emit_op(
840            Opcode::NewRowid,
841            cursor,
842            rowid_reg,
843            concurrent_flag,
844            P4::None,
845            0,
846        );
847
848        for (i, val_expr) in expanded_values.iter().enumerate() {
849            let reg = val_regs + i as i32;
850            match val_expr {
851                Expr::Placeholder(pt, _) => {
852                    #[allow(clippy::cast_possible_wrap)]
853                    let idx = if let fsqlite_ast::PlaceholderType::Numbered(n) = pt {
854                        *n as i32
855                    } else {
856                        let p = param_idx;
857                        param_idx += 1;
858                        p
859                    };
860                    b.emit_op(Opcode::Variable, idx, reg, 0, P4::None, 0);
861                }
862                _ => {
863                    // All other expressions (literals, arithmetic, function
864                    // calls, CASE, CAST, etc.) are handled by emit_expr.
865                    emit_expr(b, val_expr, reg)?;
866                }
867            }
868        }
869
870        let rec_reg = b.alloc_reg();
871        let n_cols_i32 = n_cols as i32;
872        b.emit_op(
873            Opcode::MakeRecord,
874            val_regs,
875            n_cols_i32,
876            rec_reg,
877            P4::Affinity(table.affinity_string()),
878            0,
879        );
880
881        b.emit_op(
882            Opcode::Insert,
883            cursor,
884            rec_reg,
885            rowid_reg,
886            P4::Table(table.name.clone()),
887            oe_flag,
888        );
889
890        if !returning.is_empty() {
891            b.emit_op(Opcode::ResultRow, rowid_reg, 1, 0, P4::None, 0);
892        }
893    }
894
895    Ok(())
896}
897
898/// Emit an INSERT with DEFAULT VALUES.
899#[allow(
900    clippy::cast_possible_truncation,
901    clippy::cast_possible_wrap,
902    clippy::unnecessary_wraps
903)]
904fn codegen_insert_default_values(
905    b: &mut ProgramBuilder,
906    cursor: i32,
907    table: &TableSchema,
908    returning: &[ResultColumn],
909    ctx: &CodegenContext,
910    oe_flag: u16,
911) -> Result<(), CodegenError> {
912    let rowid_reg = b.alloc_reg();
913    let concurrent_flag = i32::from(ctx.concurrent_mode);
914    let n_cols = table.columns.len() as i32;
915
916    b.emit_op(
917        Opcode::NewRowid,
918        cursor,
919        rowid_reg,
920        concurrent_flag,
921        P4::None,
922        0,
923    );
924
925    // Allocate registers for columns and evaluate each declared default.
926    let val_regs = b.alloc_regs(n_cols);
927    let default_exprs = insert_default_exprs(table)?;
928    for (idx, default_expr) in default_exprs.iter().enumerate() {
929        let reg = val_regs + idx as i32;
930        emit_expr(b, default_expr, reg)?;
931    }
932
933    let rec_reg = b.alloc_reg();
934    b.emit_op(
935        Opcode::MakeRecord,
936        val_regs,
937        n_cols,
938        rec_reg,
939        P4::Affinity(table.affinity_string()),
940        0,
941    );
942
943    b.emit_op(
944        Opcode::Insert,
945        cursor,
946        rec_reg,
947        rowid_reg,
948        P4::Table(table.name.clone()),
949        oe_flag,
950    );
951
952    if !returning.is_empty() {
953        b.emit_op(Opcode::ResultRow, rowid_reg, 1, 0, P4::None, 0);
954    }
955
956    Ok(())
957}
958
959/// Emit the INSERT loop for `INSERT INTO target SELECT ... FROM source` (planner path).
960///
961/// Opens the source table for reading, scans all rows, and inserts each
962/// into the target table.
963#[allow(
964    clippy::too_many_arguments,
965    clippy::cast_possible_truncation,
966    clippy::cast_possible_wrap
967)]
968fn codegen_insert_select(
969    b: &mut ProgramBuilder,
970    select_stmt: &SelectStatement,
971    insert_columns: &[String],
972    write_cursor: i32,
973    target_table: &TableSchema,
974    schema: &[TableSchema],
975    returning: &[ResultColumn],
976    ctx: &CodegenContext,
977    oe_flag: u16,
978) -> Result<(), CodegenError> {
979    if !select_stmt.body.compounds.is_empty() {
980        return Err(CodegenError::Unsupported(
981            "INSERT ... SELECT with compounds (UNION, etc.)".to_owned(),
982        ));
983    }
984
985    // Extract columns, FROM, and WHERE from the inner SELECT.
986    let (columns, from, where_clause) = match &select_stmt.body.select {
987        SelectCore::Select {
988            columns,
989            from,
990            where_clause,
991            ..
992        } => (columns, from, where_clause),
993        SelectCore::Values(rows) => {
994            return codegen_insert_values(
995                b,
996                rows,
997                insert_columns,
998                write_cursor,
999                target_table,
1000                returning,
1001                ctx,
1002                oe_flag,
1003            );
1004        }
1005    };
1006
1007    // Handle INSERT ... SELECT <exprs> (no FROM clause).
1008    let Some(from_clause) = from.as_ref() else {
1009        return codegen_insert_select_expr_only(
1010            b,
1011            columns,
1012            where_clause.as_deref(),
1013            insert_columns,
1014            write_cursor,
1015            target_table,
1016            returning,
1017            ctx,
1018            oe_flag,
1019        );
1020    };
1021
1022    let src_table_name = match &from_clause.source {
1023        fsqlite_ast::TableOrSubquery::Table { name, .. } => &name.name,
1024        _ => {
1025            return Err(CodegenError::Unsupported(
1026                "INSERT ... SELECT from non-table source".to_owned(),
1027            ));
1028        }
1029    };
1030
1031    let src_table = find_table(schema, src_table_name)?;
1032    let read_cursor = write_cursor + 1;
1033
1034    let n_source_cols = result_column_count(columns, src_table);
1035    let rowid_reg = b.alloc_reg();
1036    let source_regs = b.alloc_regs(n_source_cols);
1037    let rec_reg = b.alloc_reg();
1038    let concurrent_flag = i32::from(ctx.concurrent_mode);
1039
1040    let done_label = b.emit_label();
1041
1042    // OpenRead on source table.
1043    b.emit_op(
1044        Opcode::OpenRead,
1045        read_cursor,
1046        src_table.root_page,
1047        0,
1048        P4::Table(src_table.name.clone()),
1049        0,
1050    );
1051
1052    // Rewind to first row; jump to done if source is empty.
1053    let loop_start = b.current_addr();
1054    b.emit_jump_to_label(Opcode::Rewind, read_cursor, 0, done_label, P4::None, 0);
1055
1056    // Read projected columns from source into val_regs.
1057    emit_column_reads(b, read_cursor, columns, src_table, source_regs)?;
1058    let (val_regs, n_cols) = emit_insert_target_regs_from_source(
1059        b,
1060        source_regs,
1061        n_source_cols as usize,
1062        insert_columns,
1063        target_table,
1064    )?;
1065
1066    // NewRowid for target table.
1067    b.emit_op(
1068        Opcode::NewRowid,
1069        write_cursor,
1070        rowid_reg,
1071        concurrent_flag,
1072        P4::None,
1073        0,
1074    );
1075
1076    // MakeRecord from the read column values.
1077    b.emit_op(
1078        Opcode::MakeRecord,
1079        val_regs,
1080        n_cols,
1081        rec_reg,
1082        P4::Affinity(target_table.affinity_string()),
1083        0,
1084    );
1085
1086    // Insert into target table.
1087    b.emit_op(
1088        Opcode::Insert,
1089        write_cursor,
1090        rec_reg,
1091        rowid_reg,
1092        P4::Table(target_table.name.clone()),
1093        oe_flag,
1094    );
1095
1096    // RETURNING clause: emit ResultRow with rowid if present.
1097    if !returning.is_empty() {
1098        b.emit_op(Opcode::ResultRow, rowid_reg, 1, 0, P4::None, 0);
1099    }
1100
1101    // Next: advance to next source row.
1102    let loop_body = (loop_start + 1) as i32;
1103    b.emit_op(Opcode::Next, read_cursor, loop_body, 0, P4::None, 0);
1104
1105    // Done: close source cursor.
1106    b.resolve_label(done_label);
1107    b.emit_op(Opcode::Close, read_cursor, 0, 0, P4::None, 0);
1108
1109    Ok(())
1110}
1111
1112/// Emit INSERT bytecode for `INSERT INTO target SELECT expr1, expr2, ...`
1113/// (no FROM clause — expression-only single-row insert).
1114#[allow(
1115    clippy::too_many_arguments,
1116    clippy::cast_possible_truncation,
1117    clippy::cast_possible_wrap
1118)]
1119fn codegen_insert_select_expr_only(
1120    b: &mut ProgramBuilder,
1121    columns: &[ResultColumn],
1122    where_clause: Option<&Expr>,
1123    insert_columns: &[String],
1124    write_cursor: i32,
1125    target_table: &TableSchema,
1126    returning: &[ResultColumn],
1127    ctx: &CodegenContext,
1128    oe_flag: u16,
1129) -> Result<(), CodegenError> {
1130    // Count output columns: Expr → 1, Star/TableStar → 1 (yields NULL without table).
1131    let n_source_cols = columns
1132        .iter()
1133        .map(|c| match c {
1134            ResultColumn::Expr { .. } | ResultColumn::Star | ResultColumn::TableStar(_) => 1i32,
1135        })
1136        .sum::<i32>();
1137
1138    if n_source_cols == 0 {
1139        return Err(CodegenError::Unsupported(
1140            "INSERT ... SELECT with no columns".to_owned(),
1141        ));
1142    }
1143
1144    let rowid_reg = b.alloc_reg();
1145    let source_regs = b.alloc_regs(n_source_cols);
1146    let rec_reg = b.alloc_reg();
1147    let concurrent_flag = i32::from(ctx.concurrent_mode);
1148    let done_label = b.emit_label();
1149
1150    // Optional WHERE filter (e.g., INSERT INTO t SELECT 1 WHERE condition).
1151    if let Some(where_expr) = where_clause {
1152        let filter_reg = b.alloc_reg();
1153        emit_expr(b, where_expr, filter_reg)?;
1154        b.emit_jump_to_label(Opcode::IfNot, filter_reg, 1, done_label, P4::None, 0);
1155    }
1156
1157    // Evaluate each result column expression into val_regs.
1158    let mut reg = source_regs;
1159    for col in columns {
1160        match col {
1161            ResultColumn::Expr { expr, .. } => {
1162                emit_expr(b, expr, reg)?;
1163                reg += 1;
1164            }
1165            ResultColumn::Star | ResultColumn::TableStar(_) => {
1166                // Star without a table produces NULL.
1167                b.emit_op(Opcode::Null, 0, reg, 0, P4::None, 0);
1168                reg += 1;
1169            }
1170        }
1171    }
1172    let (val_regs, n_cols) = emit_insert_target_regs_from_source(
1173        b,
1174        source_regs,
1175        usize::try_from(n_source_cols).unwrap_or(0),
1176        insert_columns,
1177        target_table,
1178    )?;
1179
1180    // Auto-generate rowid.
1181    b.emit_op(
1182        Opcode::NewRowid,
1183        write_cursor,
1184        rowid_reg,
1185        concurrent_flag,
1186        P4::None,
1187        0,
1188    );
1189
1190    // MakeRecord from evaluated column values.
1191    b.emit_op(
1192        Opcode::MakeRecord,
1193        val_regs,
1194        n_cols,
1195        rec_reg,
1196        P4::Affinity(target_table.affinity_string()),
1197        0,
1198    );
1199
1200    // Insert into target table.
1201    b.emit_op(
1202        Opcode::Insert,
1203        write_cursor,
1204        rec_reg,
1205        rowid_reg,
1206        P4::Table(target_table.name.clone()),
1207        oe_flag,
1208    );
1209
1210    // RETURNING clause: emit ResultRow with rowid if present.
1211    if !returning.is_empty() {
1212        b.emit_op(Opcode::ResultRow, rowid_reg, 1, 0, P4::None, 0);
1213    }
1214
1215    b.resolve_label(done_label);
1216    Ok(())
1217}
1218
1219// ---------------------------------------------------------------------------
1220// UPDATE codegen
1221// ---------------------------------------------------------------------------
1222
1223/// Generate VDBE bytecode for an UPDATE statement.
1224///
1225/// Pattern: `UPDATE t SET col = ? WHERE rowid = ?`
1226///
1227/// Reads ALL existing columns, replaces changed ones, writes back complete
1228/// record (no partial patches — this is normative per §10.6).
1229#[allow(clippy::too_many_lines)]
1230pub fn codegen_update(
1231    b: &mut ProgramBuilder,
1232    stmt: &UpdateStatement,
1233    schema: &[TableSchema],
1234    _ctx: &CodegenContext,
1235) -> Result<(), CodegenError> {
1236    let table_name = table_name_from_qualified(&stmt.table);
1237    let table = find_table(schema, table_name)?;
1238    let cursor = 0_i32;
1239    let n_cols = table.columns.len();
1240
1241    let end_label = b.emit_label();
1242    let done_label = b.emit_label();
1243
1244    // Init.
1245    b.emit_jump_to_label(Opcode::Init, 0, 0, end_label, P4::None, 0);
1246
1247    // Transaction (write).
1248    b.emit_op(Opcode::Transaction, 0, 1, 0, P4::None, 0);
1249
1250    // Bind parameters: new values first, then rowid.
1251    // For UPDATE t SET b = ? WHERE rowid = ?, we have two bind params.
1252    let mut param_idx = 1_i32;
1253
1254    // Allocate registers for new values.
1255    let new_val_regs: Vec<(usize, i32)> = stmt
1256        .assignments
1257        .iter()
1258        .map(|assign| {
1259            let col_name = match &assign.target {
1260                fsqlite_ast::AssignmentTarget::Column(name) => name.as_str(),
1261                fsqlite_ast::AssignmentTarget::ColumnList(_) => {
1262                    return Err(CodegenError::Unsupported(
1263                        "multi-column SET (a, b) = (...) assignment is not yet supported"
1264                            .to_owned(),
1265                    ));
1266                }
1267            };
1268            let col_idx =
1269                table
1270                    .column_index(col_name)
1271                    .ok_or_else(|| CodegenError::ColumnNotFound {
1272                        table: table.name.clone(),
1273                        column: col_name.to_owned(),
1274                    })?;
1275            let reg = b.alloc_reg();
1276            Ok((col_idx, reg))
1277        })
1278        .collect::<Result<Vec<_>, CodegenError>>()?;
1279
1280    // Emit ops for new values.
1281    for (i, assign) in stmt.assignments.iter().enumerate() {
1282        let (_col_idx, reg) = new_val_regs[i];
1283        match &assign.value {
1284            Expr::Placeholder(pt, _) => {
1285                #[allow(clippy::cast_possible_wrap)]
1286                let idx = if let fsqlite_ast::PlaceholderType::Numbered(n) = pt {
1287                    // Keep param_idx ahead of numbered placeholders so
1288                    // subsequent anonymous `?` parameters get the next index.
1289                    param_idx = param_idx.max(*n as i32 + 1);
1290                    *n as i32
1291                } else {
1292                    let p = param_idx;
1293                    param_idx += 1;
1294                    p
1295                };
1296                b.emit_op(Opcode::Variable, idx, reg, 0, P4::None, 0);
1297            }
1298            _ => {
1299                // All other expressions (literals, arithmetic, function
1300                // calls, CASE, CAST, etc.) are handled by emit_expr.
1301                emit_expr(b, &assign.value, reg)?;
1302            }
1303        }
1304    }
1305
1306    // Rowid bind parameter (required).
1307    let rowid_bind = extract_rowid_bind(stmt.where_clause.as_ref()).ok_or_else(|| {
1308        CodegenError::Unsupported("UPDATE currently supports only `WHERE rowid = ?`".to_owned())
1309    })?;
1310    let rowid_reg = b.alloc_reg();
1311    let rowid_param = match rowid_bind {
1312        BindParamRef::Anonymous => param_idx,
1313        BindParamRef::Numbered(idx) => idx,
1314    };
1315    b.emit_op(Opcode::Variable, rowid_param, rowid_reg, 0, P4::None, 0);
1316
1317    // OpenWrite.
1318    b.emit_op(
1319        Opcode::OpenWrite,
1320        cursor,
1321        table.root_page,
1322        0,
1323        P4::Table(table.name.clone()),
1324        0,
1325    );
1326
1327    // NotExists: if rowid doesn't exist, jump to done.
1328    b.emit_jump_to_label(
1329        Opcode::NotExists,
1330        cursor,
1331        rowid_reg,
1332        done_label,
1333        P4::None,
1334        0,
1335    );
1336
1337    // Read ALL existing columns into registers.
1338    #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
1339    let col_regs = b.alloc_regs(n_cols as i32);
1340    for i in 0..n_cols {
1341        #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
1342        b.emit_op(
1343            Opcode::Column,
1344            cursor,
1345            i as i32,
1346            col_regs + i as i32,
1347            P4::None,
1348            0,
1349        );
1350    }
1351
1352    // Overwrite changed columns with new values.
1353    for (col_idx, new_reg) in &new_val_regs {
1354        #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
1355        let target = col_regs + *col_idx as i32;
1356        b.emit_op(Opcode::Copy, *new_reg, target, 0, P4::None, 0);
1357    }
1358
1359    // MakeRecord with ALL columns.
1360    let rec_reg = b.alloc_reg();
1361    #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
1362    let n_cols_i32 = n_cols as i32;
1363    b.emit_op(
1364        Opcode::MakeRecord,
1365        col_regs,
1366        n_cols_i32,
1367        rec_reg,
1368        P4::Affinity(table.affinity_string()),
1369        0,
1370    );
1371
1372    // Insert with REPLACE flag (p5=0x08 in C SQLite, we use 0x08).
1373    b.emit_op(
1374        Opcode::Insert,
1375        cursor,
1376        rec_reg,
1377        rowid_reg,
1378        P4::Table(table.name.clone()),
1379        0x08, // OPFLAG_ISUPDATE
1380    );
1381
1382    // Done: Close + Halt.
1383    b.resolve_label(done_label);
1384    b.emit_op(Opcode::Close, cursor, 0, 0, P4::None, 0);
1385    b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
1386
1387    // End label.
1388    b.resolve_label(end_label);
1389
1390    Ok(())
1391}
1392
1393// ---------------------------------------------------------------------------
1394// DELETE codegen
1395// ---------------------------------------------------------------------------
1396
1397/// Generate VDBE bytecode for a DELETE statement.
1398///
1399/// Pattern: `DELETE FROM t WHERE rowid = ?`
1400///
1401/// Init → Transaction(write) → Variable → OpenWrite →
1402/// NotExists → Delete → Close → Halt
1403pub fn codegen_delete(
1404    b: &mut ProgramBuilder,
1405    stmt: &DeleteStatement,
1406    schema: &[TableSchema],
1407    _ctx: &CodegenContext,
1408) -> Result<(), CodegenError> {
1409    let table_name = table_name_from_qualified(&stmt.table);
1410    let table = find_table(schema, table_name)?;
1411    let cursor = 0_i32;
1412
1413    let end_label = b.emit_label();
1414    let done_label = b.emit_label();
1415
1416    // Init.
1417    b.emit_jump_to_label(Opcode::Init, 0, 0, end_label, P4::None, 0);
1418
1419    // Transaction (write).
1420    b.emit_op(Opcode::Transaction, 0, 1, 0, P4::None, 0);
1421
1422    // Bind rowid parameter.
1423    let rowid_reg = b.alloc_reg();
1424    b.emit_op(Opcode::Variable, 1, rowid_reg, 0, P4::None, 0);
1425
1426    // OpenWrite.
1427    b.emit_op(
1428        Opcode::OpenWrite,
1429        cursor,
1430        table.root_page,
1431        0,
1432        P4::Table(table.name.clone()),
1433        0,
1434    );
1435
1436    // NotExists: if rowid not found, skip delete.
1437    b.emit_jump_to_label(
1438        Opcode::NotExists,
1439        cursor,
1440        rowid_reg,
1441        done_label,
1442        P4::None,
1443        0,
1444    );
1445
1446    // Delete at cursor position.
1447    b.emit_op(
1448        Opcode::Delete,
1449        cursor,
1450        0,
1451        0,
1452        P4::Table(table.name.clone()),
1453        0,
1454    );
1455
1456    // Done: Close + Halt.
1457    b.resolve_label(done_label);
1458    b.emit_op(Opcode::Close, cursor, 0, 0, P4::None, 0);
1459    b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
1460
1461    // End label.
1462    b.resolve_label(end_label);
1463
1464    Ok(())
1465}
1466
1467// ---------------------------------------------------------------------------
1468// Helper functions
1469// ---------------------------------------------------------------------------
1470
1471/// Count result columns (handling `SELECT *`).
1472#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
1473fn result_column_count(columns: &[ResultColumn], table: &TableSchema) -> i32 {
1474    let mut count = 0i32;
1475    for col in columns {
1476        match col {
1477            ResultColumn::Star | ResultColumn::TableStar(_) => {
1478                count += table.columns.len() as i32;
1479            }
1480            ResultColumn::Expr { .. } => count += 1,
1481        }
1482    }
1483    count
1484}
1485
1486/// Emit Column instructions to read result columns into registers.
1487#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
1488fn emit_column_reads(
1489    b: &mut ProgramBuilder,
1490    cursor: i32,
1491    columns: &[ResultColumn],
1492    table: &TableSchema,
1493    base_reg: i32,
1494) -> Result<(), CodegenError> {
1495    let mut reg = base_reg;
1496    for col in columns {
1497        match col {
1498            ResultColumn::Star | ResultColumn::TableStar(_) => {
1499                for i in 0..table.columns.len() {
1500                    b.emit_op(Opcode::Column, cursor, i as i32, reg, P4::None, 0);
1501                    reg += 1;
1502                }
1503            }
1504            ResultColumn::Expr { expr, .. } => {
1505                if let Expr::Column(col_ref, _) = expr {
1506                    if is_rowid_ref(col_ref) {
1507                        // Emit OP_Rowid: p1=cursor, p2=target register.
1508                        b.emit_op(Opcode::Rowid, cursor, reg, 0, P4::None, 0);
1509                    } else {
1510                        let col_idx = table.column_index(&col_ref.column).ok_or_else(|| {
1511                            CodegenError::ColumnNotFound {
1512                                table: table.name.clone(),
1513                                column: col_ref.column.to_string(),
1514                            }
1515                        })?;
1516                        b.emit_op(Opcode::Column, cursor, col_idx as i32, reg, P4::None, 0);
1517                    }
1518                } else {
1519                    // For non-column expressions (literals, placeholders, etc.),
1520                    // evaluate the expression directly rather than reading a column.
1521                    emit_expr(b, expr, reg)?;
1522                }
1523                reg += 1;
1524            }
1525        }
1526    }
1527    Ok(())
1528}
1529
1530/// Check if a WHERE clause is a simple `rowid = ?` bind parameter.
1531///
1532/// Returns the 1-based bind parameter index if so.
1533fn extract_rowid_bind_param(where_clause: Option<&Expr>) -> Option<i32> {
1534    extract_rowid_bind(where_clause).map(|bind| match bind {
1535        BindParamRef::Anonymous => 1,
1536        BindParamRef::Numbered(idx) => idx,
1537    })
1538}
1539
1540/// Check if a WHERE clause is a simple `rowid = ?` bind parameter.
1541fn extract_rowid_bind(where_clause: Option<&Expr>) -> Option<BindParamRef> {
1542    let expr = where_clause?;
1543    if let Expr::BinaryOp {
1544        left,
1545        op: fsqlite_ast::BinaryOp::Eq,
1546        right,
1547        ..
1548    } = expr
1549    {
1550        // Check left = rowid column, right = bind param.
1551        if is_rowid_expr(left) {
1552            return bind_param_ref(right);
1553        }
1554        if is_rowid_expr(right) {
1555            return bind_param_ref(left);
1556        }
1557    }
1558    None
1559}
1560
1561/// Check if a WHERE clause is `col = ?` for an indexed column.
1562fn extract_column_eq_bind(where_clause: Option<&Expr>) -> Option<(String, i32)> {
1563    let expr = where_clause?;
1564    if let Expr::BinaryOp {
1565        left,
1566        op: fsqlite_ast::BinaryOp::Eq,
1567        right,
1568        ..
1569    } = expr
1570    {
1571        if let (Some(col_name), Some(param_idx)) = (column_name(left), bind_param_index(right)) {
1572            return Some((col_name, param_idx));
1573        }
1574        if let (Some(col_name), Some(param_idx)) = (column_name(right), bind_param_index(left)) {
1575            return Some((col_name, param_idx));
1576        }
1577    }
1578    None
1579}
1580
1581/// Extract a column name from an expression if it's a simple column reference.
1582fn column_name(expr: &Expr) -> Option<String> {
1583    if let Expr::Column(col_ref, _) = expr {
1584        if !is_rowid_ref(col_ref) {
1585            return Some(col_ref.column.to_string());
1586        }
1587    }
1588    None
1589}
1590
1591/// Check if an expression is a rowid reference.
1592fn is_rowid_expr(expr: &Expr) -> bool {
1593    if let Expr::Column(col_ref, _) = expr {
1594        is_rowid_ref(col_ref)
1595    } else {
1596        false
1597    }
1598}
1599
1600fn is_rowid_ref(col_ref: &ColumnRef) -> bool {
1601    let name = col_ref.column.to_ascii_lowercase();
1602    name == "rowid" || name == "_rowid_" || name == "oid"
1603}
1604
1605/// Extract a bind parameter index from a `?` or `?NNN` placeholder.
1606fn bind_param_index(expr: &Expr) -> Option<i32> {
1607    bind_param_ref(expr).map(|bind| match bind {
1608        BindParamRef::Anonymous => 1,
1609        BindParamRef::Numbered(idx) => idx,
1610    })
1611}
1612
1613/// Extract a bind parameter while preserving anonymous vs numbered form.
1614fn bind_param_ref(expr: &Expr) -> Option<BindParamRef> {
1615    if let Expr::Placeholder(pt, _) = expr {
1616        match pt {
1617            PlaceholderType::Anonymous => Some(BindParamRef::Anonymous),
1618            PlaceholderType::Numbered(n) =>
1619            {
1620                #[allow(clippy::cast_possible_wrap)]
1621                Some(BindParamRef::Numbered(*n as i32))
1622            }
1623            _ => None,
1624        }
1625    } else {
1626        None
1627    }
1628}
1629
1630/// Map AST binary operator to VDBE opcode.
1631fn binary_op_to_opcode(op: AstBinaryOp) -> Opcode {
1632    match op {
1633        AstBinaryOp::Add => Opcode::Add,
1634        AstBinaryOp::Subtract => Opcode::Subtract,
1635        AstBinaryOp::Multiply => Opcode::Multiply,
1636        AstBinaryOp::Divide => Opcode::Divide,
1637        AstBinaryOp::Modulo => Opcode::Remainder,
1638        AstBinaryOp::Concat => Opcode::Concat,
1639        AstBinaryOp::BitAnd => Opcode::BitAnd,
1640        AstBinaryOp::BitOr => Opcode::BitOr,
1641        AstBinaryOp::ShiftLeft => Opcode::ShiftLeft,
1642        AstBinaryOp::ShiftRight => Opcode::ShiftRight,
1643        AstBinaryOp::And => Opcode::And,
1644        AstBinaryOp::Or => Opcode::Or,
1645        AstBinaryOp::Eq | AstBinaryOp::Is => Opcode::Eq,
1646        AstBinaryOp::Ne | AstBinaryOp::IsNot => Opcode::Ne,
1647        AstBinaryOp::Lt => Opcode::Lt,
1648        AstBinaryOp::Le => Opcode::Le,
1649        AstBinaryOp::Gt => Opcode::Gt,
1650        AstBinaryOp::Ge => Opcode::Ge,
1651    }
1652}
1653
1654/// Returns true if `op` is a comparison that produces 1/0 in the result register.
1655fn is_comparison_op(op: AstBinaryOp) -> bool {
1656    matches!(
1657        op,
1658        AstBinaryOp::Eq
1659            | AstBinaryOp::Ne
1660            | AstBinaryOp::Lt
1661            | AstBinaryOp::Le
1662            | AstBinaryOp::Gt
1663            | AstBinaryOp::Ge
1664            | AstBinaryOp::Is
1665            | AstBinaryOp::IsNot
1666    )
1667}
1668
1669/// Emit a comparison that produces 1 (true) or 0 (false) in `dest`.
1670fn emit_comparison_expr(
1671    b: &mut ProgramBuilder,
1672    left: &Expr,
1673    op: AstBinaryOp,
1674    right: &Expr,
1675    dest: i32,
1676) -> Result<(), CodegenError> {
1677    let lhs = b.alloc_temp();
1678    let rhs = b.alloc_temp();
1679    emit_expr(b, left, lhs)?;
1680    emit_expr(b, right, rhs)?;
1681
1682    let opcode = binary_op_to_opcode(op);
1683    // IS / IS NOT need the NULLEQ flag (0x80) so NULL IS NULL → true.
1684    let p5 = if matches!(op, AstBinaryOp::Is | AstBinaryOp::IsNot) {
1685        0x80
1686    } else {
1687        0
1688    };
1689    let true_label = b.emit_label();
1690    let done_label = b.emit_label();
1691
1692    // Jump to true_label if comparison holds.
1693    b.emit_jump_to_label(opcode, rhs, lhs, true_label, P4::None, p5);
1694    b.emit_op(Opcode::Integer, 0, dest, 0, P4::None, 0);
1695    b.emit_jump_to_label(Opcode::Goto, 0, 0, done_label, P4::None, 0);
1696    b.resolve_label(true_label);
1697    b.emit_op(Opcode::Integer, 1, dest, 0, P4::None, 0);
1698    b.resolve_label(done_label);
1699
1700    b.free_temp(lhs);
1701    b.free_temp(rhs);
1702    Ok(())
1703}
1704
1705/// Convert a SQL type name to a single-char affinity code for CAST.
1706fn type_to_affinity(type_name: &str) -> char {
1707    // SQLite affinity characters: A=Blob/None, B=Text, C=Numeric, D=Integer, E=Real
1708    let upper = type_name.to_uppercase();
1709    if upper.contains("INT") {
1710        'D' // integer
1711    } else if upper.contains("CHAR")
1712        || upper.contains("CLOB")
1713        || upper.contains("TEXT")
1714        || upper.contains("VARCHAR")
1715    {
1716        'B' // text
1717    } else if upper.contains("BLOB") || upper.is_empty() {
1718        'A' // blob (no affinity)
1719    } else if upper.contains("REAL") || upper.contains("FLOA") || upper.contains("DOUB") {
1720        'E' // real
1721    } else {
1722        'C' // default: numeric
1723    }
1724}
1725
1726/// Emit an expression value into a register.
1727///
1728/// Handles bind parameters, literals, binary/unary ops, IS NULL, CAST,
1729/// function calls, CASE, and COLLATE expressions.
1730#[allow(
1731    clippy::cast_possible_truncation,
1732    clippy::cast_possible_wrap,
1733    clippy::too_many_lines
1734)]
1735fn emit_expr(b: &mut ProgramBuilder, expr: &Expr, reg: i32) -> Result<(), CodegenError> {
1736    match expr {
1737        Expr::Placeholder(pt, _) => {
1738            let idx = match pt {
1739                fsqlite_ast::PlaceholderType::Numbered(n) => *n as i32,
1740                _ => 1, // Anonymous or other — will be renumbered by caller.
1741            };
1742            b.emit_op(Opcode::Variable, idx, reg, 0, P4::None, 0);
1743            Ok(())
1744        }
1745        Expr::Literal(lit, _) => match lit {
1746            Literal::Integer(n) => {
1747                if let Ok(as_i32) = i32::try_from(*n) {
1748                    b.emit_op(Opcode::Integer, as_i32, reg, 0, P4::None, 0);
1749                } else {
1750                    b.emit_op(Opcode::Int64, 0, reg, 0, P4::Int64(*n), 0);
1751                }
1752                Ok(())
1753            }
1754            Literal::Float(f) => {
1755                b.emit_op(Opcode::Real, 0, reg, 0, P4::Real(*f), 0);
1756                Ok(())
1757            }
1758            Literal::String(s) => {
1759                b.emit_op(Opcode::String8, 0, reg, 0, P4::Str(s.clone()), 0);
1760                Ok(())
1761            }
1762            Literal::Blob(bytes) => {
1763                b.emit_op(
1764                    Opcode::Blob,
1765                    bytes.len() as i32,
1766                    reg,
1767                    0,
1768                    P4::Blob(bytes.clone()),
1769                    0,
1770                );
1771                Ok(())
1772            }
1773            Literal::Null => {
1774                b.emit_op(Opcode::Null, 0, reg, 0, P4::None, 0);
1775                Ok(())
1776            }
1777            Literal::True => {
1778                b.emit_op(Opcode::Integer, 1, reg, 0, P4::None, 0);
1779                Ok(())
1780            }
1781            Literal::False => {
1782                b.emit_op(Opcode::Integer, 0, reg, 0, P4::None, 0);
1783                Ok(())
1784            }
1785            Literal::CurrentTime => {
1786                // Emit PureFunc call to time('now').
1787                let arg_reg = b.alloc_temp();
1788                b.emit_op(Opcode::String8, 0, arg_reg, 0, P4::Str("now".to_owned()), 0);
1789                b.emit_op(
1790                    Opcode::PureFunc,
1791                    0,
1792                    arg_reg,
1793                    reg,
1794                    P4::FuncName("TIME".to_owned()),
1795                    1,
1796                );
1797                b.free_temp(arg_reg);
1798                Ok(())
1799            }
1800            Literal::CurrentDate => {
1801                let arg_reg = b.alloc_temp();
1802                b.emit_op(Opcode::String8, 0, arg_reg, 0, P4::Str("now".to_owned()), 0);
1803                b.emit_op(
1804                    Opcode::PureFunc,
1805                    0,
1806                    arg_reg,
1807                    reg,
1808                    P4::FuncName("DATE".to_owned()),
1809                    1,
1810                );
1811                b.free_temp(arg_reg);
1812                Ok(())
1813            }
1814            Literal::CurrentTimestamp => {
1815                let arg_reg = b.alloc_temp();
1816                b.emit_op(Opcode::String8, 0, arg_reg, 0, P4::Str("now".to_owned()), 0);
1817                b.emit_op(
1818                    Opcode::PureFunc,
1819                    0,
1820                    arg_reg,
1821                    reg,
1822                    P4::FuncName("DATETIME".to_owned()),
1823                    1,
1824                );
1825                b.free_temp(arg_reg);
1826                Ok(())
1827            }
1828        },
1829
1830        // Binary operations: arithmetic, comparison, logical, bitwise.
1831        Expr::BinaryOp {
1832            left, op, right, ..
1833        } => {
1834            if is_comparison_op(*op) {
1835                return emit_comparison_expr(b, left, *op, right, reg);
1836            }
1837            // Arithmetic / logical / bitwise: left→reg, right→tmp, apply op.
1838            let tmp = b.alloc_temp();
1839            emit_expr(b, left, reg)?;
1840            emit_expr(b, right, tmp)?;
1841            let opcode = binary_op_to_opcode(*op);
1842            // VDBE arithmetic: p1=rhs, p2=lhs, p3=dest
1843            b.emit_op(opcode, tmp, reg, reg, P4::None, 0);
1844            b.free_temp(tmp);
1845            Ok(())
1846        }
1847
1848        // Unary operations: negate, plus, NOT, bit-NOT.
1849        Expr::UnaryOp {
1850            op, expr: inner, ..
1851        } => {
1852            emit_expr(b, inner, reg)?;
1853            match op {
1854                AstUnaryOp::Negate => {
1855                    let tmp = b.alloc_temp();
1856                    b.emit_op(Opcode::Integer, -1, tmp, 0, P4::None, 0);
1857                    b.emit_op(Opcode::Multiply, tmp, reg, reg, P4::None, 0);
1858                    b.free_temp(tmp);
1859                }
1860                AstUnaryOp::Plus => {} // no-op
1861                AstUnaryOp::Not => {
1862                    b.emit_op(Opcode::Not, reg, reg, 0, P4::None, 0);
1863                }
1864                AstUnaryOp::BitNot => {
1865                    b.emit_op(Opcode::BitNot, reg, reg, 0, P4::None, 0);
1866                }
1867            }
1868            Ok(())
1869        }
1870
1871        // IS [NOT] NULL.
1872        Expr::IsNull {
1873            expr: inner, not, ..
1874        } => {
1875            emit_expr(b, inner, reg)?;
1876            let true_label = b.emit_label();
1877            let done_label = b.emit_label();
1878            if *not {
1879                b.emit_jump_to_label(Opcode::NotNull, reg, 0, true_label, P4::None, 0);
1880            } else {
1881                b.emit_jump_to_label(Opcode::IsNull, reg, 0, true_label, P4::None, 0);
1882            }
1883            b.emit_op(Opcode::Integer, 0, reg, 0, P4::None, 0);
1884            b.emit_jump_to_label(Opcode::Goto, 0, 0, done_label, P4::None, 0);
1885            b.resolve_label(true_label);
1886            b.emit_op(Opcode::Integer, 1, reg, 0, P4::None, 0);
1887            b.resolve_label(done_label);
1888            Ok(())
1889        }
1890
1891        // CAST(expr AS type).
1892        Expr::Cast {
1893            expr: inner,
1894            type_name,
1895            ..
1896        } => {
1897            emit_expr(b, inner, reg)?;
1898            let affinity = type_to_affinity(&type_name.name);
1899            // Cast opcode: p1 = register, p2 = affinity character code.
1900            // 'A'=65 Blob, 'B'=66 Text, 'C'=67 Numeric, 'D'=68 Integer, 'E'=69 Real
1901            b.emit_op(Opcode::Cast, reg, i32::from(affinity as u8), 0, P4::None, 0);
1902            Ok(())
1903        }
1904
1905        // Scalar function call.
1906        Expr::FunctionCall { name, args, .. } => {
1907            // Function registry stores names in uppercase via canonical_name().
1908            let canon = name.trim().to_ascii_uppercase();
1909            match args {
1910                FunctionArgs::Star => {
1911                    b.emit_op(Opcode::PureFunc, 0, 0, reg, P4::FuncName(canon), 0);
1912                }
1913                FunctionArgs::List(arg_list) => {
1914                    let n_args = arg_list.len();
1915                    if n_args == 0 {
1916                        b.emit_op(Opcode::PureFunc, 0, 0, reg, P4::FuncName(canon), 0);
1917                    } else {
1918                        let first_arg_reg = b.alloc_regs(n_args as i32);
1919                        for (i, arg) in arg_list.iter().enumerate() {
1920                            emit_expr(b, arg, first_arg_reg + i as i32)?;
1921                        }
1922                        b.emit_op(
1923                            Opcode::PureFunc,
1924                            0,
1925                            first_arg_reg,
1926                            reg,
1927                            P4::FuncName(canon),
1928                            n_args as u16,
1929                        );
1930                    }
1931                }
1932            }
1933            Ok(())
1934        }
1935
1936        // CASE [operand] WHEN ... THEN ... [ELSE ...] END
1937        Expr::Case {
1938            operand,
1939            whens,
1940            else_expr,
1941            ..
1942        } => {
1943            let done_label = b.emit_label();
1944            if let Some(base_expr) = operand {
1945                let base_reg = b.alloc_temp();
1946                emit_expr(b, base_expr, base_reg)?;
1947                for (when_val, then_val) in whens {
1948                    let next_label = b.emit_label();
1949                    let when_reg = b.alloc_temp();
1950                    emit_expr(b, when_val, when_reg)?;
1951                    b.emit_jump_to_label(
1952                        Opcode::Ne,
1953                        when_reg,
1954                        base_reg,
1955                        next_label,
1956                        P4::None,
1957                        0x10,
1958                    );
1959                    b.free_temp(when_reg);
1960                    emit_expr(b, then_val, reg)?;
1961                    b.emit_jump_to_label(Opcode::Goto, 0, 0, done_label, P4::None, 0);
1962                    b.resolve_label(next_label);
1963                }
1964                b.free_temp(base_reg);
1965            } else {
1966                for (when_cond, then_val) in whens {
1967                    let next_label = b.emit_label();
1968                    let cond_reg = b.alloc_temp();
1969                    emit_expr(b, when_cond, cond_reg)?;
1970                    b.emit_jump_to_label(Opcode::IfNot, cond_reg, 0, next_label, P4::None, 1);
1971                    b.free_temp(cond_reg);
1972                    emit_expr(b, then_val, reg)?;
1973                    b.emit_jump_to_label(Opcode::Goto, 0, 0, done_label, P4::None, 0);
1974                    b.resolve_label(next_label);
1975                }
1976            }
1977            if let Some(else_val) = else_expr {
1978                emit_expr(b, else_val, reg)?;
1979            } else {
1980                b.emit_op(Opcode::Null, 0, reg, 0, P4::None, 0);
1981            }
1982            b.resolve_label(done_label);
1983            Ok(())
1984        }
1985
1986        // Collate — just evaluate the inner expression.
1987        Expr::Collate { expr: inner, .. } => emit_expr(b, inner, reg),
1988
1989        _ => Err(CodegenError::Unsupported(
1990            "planner expression codegen for this expression type".to_owned(),
1991        )),
1992    }
1993}
1994
1995// ---------------------------------------------------------------------------
1996// Tests
1997// ---------------------------------------------------------------------------
1998
1999#[cfg(test)]
2000mod tests {
2001    use super::*;
2002    use fsqlite_ast::{
2003        Assignment, AssignmentTarget, BinaryOp as AstBinaryOp, ColumnRef, DeleteStatement,
2004        Distinctness, Expr, FromClause, InsertSource, InsertStatement, Literal, PlaceholderType,
2005        QualifiedName, QualifiedTableRef, ResultColumn, SelectBody, SelectCore, SelectStatement,
2006        Span, TableOrSubquery, UpdateStatement,
2007    };
2008    use fsqlite_types::opcode::{Opcode, ProgramBuilder, VdbeProgram};
2009
2010    fn test_schema() -> Vec<TableSchema> {
2011        vec![TableSchema {
2012            name: "t".to_owned(),
2013            root_page: 2,
2014            columns: vec![
2015                ColumnInfo {
2016                    name: "a".to_owned(),
2017                    affinity: 'd',
2018                    default_value: None,
2019                },
2020                ColumnInfo {
2021                    name: "b".to_owned(),
2022                    affinity: 'C',
2023                    default_value: None,
2024                },
2025            ],
2026            indexes: vec![],
2027        }]
2028    }
2029
2030    fn test_schema_with_index() -> Vec<TableSchema> {
2031        vec![TableSchema {
2032            name: "t".to_owned(),
2033            root_page: 2,
2034            columns: vec![
2035                ColumnInfo {
2036                    name: "a".to_owned(),
2037                    affinity: 'd',
2038                    default_value: None,
2039                },
2040                ColumnInfo {
2041                    name: "b".to_owned(),
2042                    affinity: 'C',
2043                    default_value: None,
2044                },
2045            ],
2046            indexes: vec![IndexSchema {
2047                name: "idx_t_b".to_owned(),
2048                root_page: 3,
2049                columns: vec!["b".to_owned()],
2050                is_unique: false,
2051            }],
2052        }]
2053    }
2054
2055    fn from_table(name: &str) -> FromClause {
2056        FromClause {
2057            source: TableOrSubquery::Table {
2058                name: QualifiedName::bare(name),
2059                alias: None,
2060                index_hint: None,
2061                time_travel: None,
2062            },
2063            joins: vec![],
2064        }
2065    }
2066
2067    fn placeholder(n: u32) -> Expr {
2068        Expr::Placeholder(PlaceholderType::Numbered(n), Span::ZERO)
2069    }
2070
2071    #[test]
2072    fn test_table_schema_accessors_affinity_index_and_case_insensitive_lookup() {
2073        let schema = TableSchema {
2074            name: "t".to_owned(),
2075            root_page: 2,
2076            columns: vec![
2077                ColumnInfo {
2078                    name: "id".to_owned(),
2079                    affinity: 'd',
2080                    default_value: None,
2081                },
2082                ColumnInfo {
2083                    name: "name".to_owned(),
2084                    affinity: 'b',
2085                    default_value: None,
2086                },
2087                ColumnInfo {
2088                    name: "age".to_owned(),
2089                    affinity: 'c',
2090                    default_value: None,
2091                },
2092            ],
2093            indexes: vec![IndexSchema {
2094                name: "idx_age_name".to_owned(),
2095                root_page: 3,
2096                columns: vec!["age".to_owned(), "name".to_owned()],
2097                is_unique: false,
2098            }],
2099        };
2100
2101        // affinity_string: one char per column, in declaration order.
2102        assert_eq!(schema.affinity_string(), "dbc");
2103
2104        // column_index: case-insensitive, in declaration order; None for absent.
2105        assert_eq!(schema.column_index("id"), Some(0));
2106        assert_eq!(
2107            schema.column_index("NAME"),
2108            Some(1),
2109            "lookup is case-insensitive"
2110        );
2111        assert_eq!(schema.column_index("Age"), Some(2));
2112        assert_eq!(schema.column_index("missing"), None);
2113
2114        // index_for_column matches the LEFTMOST index column only (case-insensitive).
2115        assert_eq!(
2116            schema.index_for_column("age").map(|i| i.name.as_str()),
2117            Some("idx_age_name")
2118        );
2119        assert_eq!(
2120            schema.index_for_column("AGE").map(|i| i.name.as_str()),
2121            Some("idx_age_name"),
2122            "case-insensitive"
2123        );
2124        assert!(
2125            schema.index_for_column("name").is_none(),
2126            "a non-leftmost index column is not matched"
2127        );
2128        assert!(schema.index_for_column("id").is_none());
2129    }
2130
2131    #[test]
2132    fn test_type_to_affinity_follows_sqlite_rules() {
2133        // SQLite affinity determination is a case-insensitive substring scan in
2134        // precedence order: INT->'D', {CHAR,CLOB,TEXT,VARCHAR}->'B',
2135        // {BLOB, no type}->'A', {REAL,FLOA,DOUB}->'E', else NUMERIC 'C'.
2136        assert_eq!(type_to_affinity("INTEGER"), 'D');
2137        assert_eq!(type_to_affinity("int"), 'D');
2138        assert_eq!(type_to_affinity("BIGINT"), 'D');
2139        assert_eq!(type_to_affinity("TINYINT"), 'D');
2140        // SQLite quirk: anything containing "INT" gets INTEGER affinity ("POINT").
2141        assert_eq!(type_to_affinity("POINT"), 'D');
2142
2143        assert_eq!(type_to_affinity("TEXT"), 'B');
2144        assert_eq!(type_to_affinity("VARCHAR(255)"), 'B');
2145        assert_eq!(type_to_affinity("CHARACTER(20)"), 'B');
2146        assert_eq!(type_to_affinity("CLOB"), 'B');
2147
2148        // BLOB or a missing declared type yields no affinity ('A').
2149        assert_eq!(type_to_affinity("BLOB"), 'A');
2150        assert_eq!(type_to_affinity(""), 'A');
2151
2152        assert_eq!(type_to_affinity("REAL"), 'E');
2153        assert_eq!(type_to_affinity("DOUBLE PRECISION"), 'E');
2154        assert_eq!(type_to_affinity("FLOAT"), 'E');
2155
2156        // Everything else falls through to NUMERIC.
2157        assert_eq!(type_to_affinity("NUMERIC"), 'C');
2158        assert_eq!(type_to_affinity("DECIMAL(10,2)"), 'C');
2159        assert_eq!(type_to_affinity("BOOLEAN"), 'C');
2160        assert_eq!(type_to_affinity("DATETIME"), 'C');
2161    }
2162
2163    #[test]
2164    fn test_rowid_ref_aliases_and_conflict_action_codes() {
2165        // SQLite recognizes three case-insensitive rowid aliases.
2166        for name in ["rowid", "ROWID", "_rowid_", "oid", "OID", "RowId"] {
2167            assert!(
2168                is_rowid_ref(&ColumnRef::bare(name)),
2169                "{name} is a rowid alias"
2170            );
2171        }
2172        for name in ["id", "name", "rowid_", "row_id", "_oid_"] {
2173            assert!(
2174                !is_rowid_ref(&ColumnRef::bare(name)),
2175                "{name} is not a rowid alias"
2176            );
2177        }
2178
2179        // ON CONFLICT actions map to OE_ codes; a missing action defaults to ABORT.
2180        assert_eq!(
2181            conflict_action_to_oe(None),
2182            OE_ABORT,
2183            "default conflict action is ABORT"
2184        );
2185        assert_eq!(
2186            conflict_action_to_oe(Some(&ConflictAction::Abort)),
2187            OE_ABORT
2188        );
2189        assert_eq!(
2190            conflict_action_to_oe(Some(&ConflictAction::Rollback)),
2191            OE_ROLLBACK
2192        );
2193        assert_eq!(conflict_action_to_oe(Some(&ConflictAction::Fail)), OE_FAIL);
2194        assert_eq!(
2195            conflict_action_to_oe(Some(&ConflictAction::Ignore)),
2196            OE_IGNORE
2197        );
2198        assert_eq!(
2199            conflict_action_to_oe(Some(&ConflictAction::Replace)),
2200            OE_REPLACE
2201        );
2202        // The five OE conflict codes are distinct.
2203        let codes: std::collections::HashSet<u16> =
2204            [OE_ROLLBACK, OE_ABORT, OE_FAIL, OE_IGNORE, OE_REPLACE]
2205                .into_iter()
2206                .collect();
2207        assert_eq!(codes.len(), 5, "OE conflict codes must be distinct");
2208    }
2209
2210    #[test]
2211    fn test_emit_comparison_expr_shape_and_is_nulleq_flag() {
2212        // emit_comparison_expr evaluates both operands into temps, then a
2213        // comparison opcode jumps to set dest=1, falling through to dest=0 via a
2214        // Goto. IS / IS NOT additionally set the NULLEQ flag (p5=0x80) so that
2215        // NULL IS NULL compares true. Neither the emitted shape nor the flag was
2216        // directly asserted (binary_op_to_opcode only checks Is -> Eq).
2217        let lit = |n: i64| Expr::Literal(Literal::Integer(n), Span::ZERO);
2218
2219        // `3 < 5`: eval, eval, Lt(jump), Integer 0, Goto, Integer 1.
2220        let mut b = ProgramBuilder::new();
2221        let dest = b.alloc_reg();
2222        emit_comparison_expr(&mut b, &lit(3), AstBinaryOp::Lt, &lit(5), dest).unwrap();
2223        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2224        let prog = b.finish().unwrap();
2225        assert!(has_opcodes(
2226            &prog,
2227            &[
2228                Opcode::Integer, // left operand
2229                Opcode::Integer, // right operand
2230                Opcode::Lt,      // comparison jump
2231                Opcode::Integer, // false branch -> 0
2232                Opcode::Goto,
2233                Opcode::Integer, // true branch -> 1
2234            ]
2235        ));
2236        let lt = prog
2237            .ops()
2238            .iter()
2239            .find(|op| op.opcode == Opcode::Lt)
2240            .expect("Lt present");
2241        assert_eq!(lt.p5, 0, "a plain comparison carries no NULLEQ flag");
2242
2243        // `3 IS 5`: maps to Eq with the NULLEQ flag set (0x80).
2244        let mut b2 = ProgramBuilder::new();
2245        let d2 = b2.alloc_reg();
2246        emit_comparison_expr(&mut b2, &lit(3), AstBinaryOp::Is, &lit(5), d2).unwrap();
2247        b2.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
2248        let prog2 = b2.finish().unwrap();
2249        let eq = prog2
2250            .ops()
2251            .iter()
2252            .find(|op| op.opcode == Opcode::Eq)
2253            .expect("Eq present");
2254        assert_eq!(
2255            eq.p5, 0x80,
2256            "IS uses the NULLEQ flag so NULL IS NULL is true"
2257        );
2258    }
2259
2260    #[test]
2261    fn test_is_rowid_ref_recognizes_aliases_case_insensitively() {
2262        // is_rowid_ref accepts the three rowid aliases (rowid, _rowid_, oid)
2263        // case-insensitively and rejects everything else. It is exercised
2264        // indirectly via SELECT codegen but never asserted as a predicate.
2265        for name in [
2266            "rowid", "ROWID", "RowId", "_rowid_", "_ROWID_", "oid", "OID",
2267        ] {
2268            assert!(
2269                is_rowid_ref(&ColumnRef::bare(name)),
2270                "{name} should be a rowid alias"
2271            );
2272        }
2273        for name in ["id", "row_id", "rowid2", "oids", "_rowid", "rowi", ""] {
2274            assert!(
2275                !is_rowid_ref(&ColumnRef::bare(name)),
2276                "{name} should NOT be a rowid alias"
2277            );
2278        }
2279    }
2280
2281    #[test]
2282    fn test_binary_op_to_opcode_and_is_comparison_classification() {
2283        use AstBinaryOp as B;
2284
2285        // Arithmetic / bitwise / logical / concat map directly...
2286        assert_eq!(binary_op_to_opcode(B::Add), Opcode::Add);
2287        assert_eq!(binary_op_to_opcode(B::Subtract), Opcode::Subtract);
2288        assert_eq!(binary_op_to_opcode(B::Multiply), Opcode::Multiply);
2289        assert_eq!(binary_op_to_opcode(B::Divide), Opcode::Divide);
2290        // ...with one rename: Modulo -> Remainder.
2291        assert_eq!(binary_op_to_opcode(B::Modulo), Opcode::Remainder);
2292        assert_eq!(binary_op_to_opcode(B::Concat), Opcode::Concat);
2293        assert_eq!(binary_op_to_opcode(B::BitAnd), Opcode::BitAnd);
2294        assert_eq!(binary_op_to_opcode(B::BitOr), Opcode::BitOr);
2295        assert_eq!(binary_op_to_opcode(B::ShiftLeft), Opcode::ShiftLeft);
2296        assert_eq!(binary_op_to_opcode(B::ShiftRight), Opcode::ShiftRight);
2297        assert_eq!(binary_op_to_opcode(B::And), Opcode::And);
2298        assert_eq!(binary_op_to_opcode(B::Or), Opcode::Or);
2299
2300        // Comparisons; IS / IS NOT collapse onto Eq / Ne at the opcode level.
2301        assert_eq!(binary_op_to_opcode(B::Eq), Opcode::Eq);
2302        assert_eq!(binary_op_to_opcode(B::Is), Opcode::Eq);
2303        assert_eq!(binary_op_to_opcode(B::Ne), Opcode::Ne);
2304        assert_eq!(binary_op_to_opcode(B::IsNot), Opcode::Ne);
2305        assert_eq!(binary_op_to_opcode(B::Lt), Opcode::Lt);
2306        assert_eq!(binary_op_to_opcode(B::Le), Opcode::Le);
2307        assert_eq!(binary_op_to_opcode(B::Gt), Opcode::Gt);
2308        assert_eq!(binary_op_to_opcode(B::Ge), Opcode::Ge);
2309
2310        // is_comparison_op: the six relational ops plus IS / IS NOT, nothing else.
2311        for op in [B::Eq, B::Ne, B::Lt, B::Le, B::Gt, B::Ge, B::Is, B::IsNot] {
2312            assert!(is_comparison_op(op), "{op:?} is a comparison");
2313        }
2314        for op in [
2315            B::Add,
2316            B::Subtract,
2317            B::Multiply,
2318            B::Divide,
2319            B::Modulo,
2320            B::Concat,
2321            B::BitAnd,
2322            B::BitOr,
2323            B::ShiftLeft,
2324            B::ShiftRight,
2325            B::And,
2326            B::Or,
2327        ] {
2328            assert!(!is_comparison_op(op), "{op:?} is not a comparison");
2329        }
2330    }
2331
2332    #[test]
2333    fn test_extract_column_eq_bind_symmetry_and_rowid_exclusion() {
2334        let bin = |left: Expr, right: Expr| Expr::BinaryOp {
2335            left: Box::new(left),
2336            op: AstBinaryOp::Eq,
2337            right: Box::new(right),
2338            span: Span::ZERO,
2339        };
2340        let col = |name: &str| Expr::Column(ColumnRef::bare(name), Span::ZERO);
2341
2342        // `name = ?2` -> ("name", 2).
2343        let e = bin(col("name"), placeholder(2));
2344        assert_eq!(
2345            extract_column_eq_bind(Some(&e)),
2346            Some(("name".to_owned(), 2))
2347        );
2348
2349        // Symmetric form `?3 = age` -> ("age", 3).
2350        let e = bin(placeholder(3), col("age"));
2351        assert_eq!(
2352            extract_column_eq_bind(Some(&e)),
2353            Some(("age".to_owned(), 3))
2354        );
2355
2356        // rowid columns are excluded (they take a separate seek path) -> None.
2357        let e = bin(col("rowid"), placeholder(1));
2358        assert_eq!(extract_column_eq_bind(Some(&e)), None);
2359
2360        // A non-bind right-hand side (`name = age`) -> None.
2361        let e = bin(col("name"), col("age"));
2362        assert_eq!(extract_column_eq_bind(Some(&e)), None);
2363
2364        // A non-equality operator (`name < ?1`) -> None.
2365        let lt = Expr::BinaryOp {
2366            left: Box::new(col("name")),
2367            op: AstBinaryOp::Lt,
2368            right: Box::new(placeholder(1)),
2369            span: Span::ZERO,
2370        };
2371        assert_eq!(extract_column_eq_bind(Some(&lt)), None);
2372
2373        // No WHERE clause -> None.
2374        assert_eq!(extract_column_eq_bind(None), None);
2375    }
2376
2377    #[test]
2378    fn test_extract_rowid_bind_symmetry_and_form_preservation() {
2379        let eq = |left: Expr, right: Expr| Expr::BinaryOp {
2380            left: Box::new(left),
2381            op: AstBinaryOp::Eq,
2382            right: Box::new(right),
2383            span: Span::ZERO,
2384        };
2385        let col = |name: &str| Expr::Column(ColumnRef::bare(name), Span::ZERO);
2386
2387        // `rowid = ?2` -> Numbered(2); the *_param form collapses to 2.
2388        let e = eq(col("rowid"), placeholder(2));
2389        assert_eq!(
2390            extract_rowid_bind(Some(&e)),
2391            Some(BindParamRef::Numbered(2))
2392        );
2393        assert_eq!(extract_rowid_bind_param(Some(&e)), Some(2));
2394
2395        // Symmetric, and the `oid` alias is recognized: `?3 = oid` -> Numbered(3).
2396        let e = eq(placeholder(3), col("oid"));
2397        assert_eq!(
2398            extract_rowid_bind(Some(&e)),
2399            Some(BindParamRef::Numbered(3))
2400        );
2401
2402        // An anonymous placeholder preserves its form; *_param collapses it to 1.
2403        let e = eq(
2404            col("rowid"),
2405            Expr::Placeholder(PlaceholderType::Anonymous, Span::ZERO),
2406        );
2407        assert_eq!(extract_rowid_bind(Some(&e)), Some(BindParamRef::Anonymous));
2408        assert_eq!(extract_rowid_bind_param(Some(&e)), Some(1));
2409
2410        // A non-rowid column is not extracted here (handled by column-eq path).
2411        let e = eq(col("name"), placeholder(1));
2412        assert_eq!(extract_rowid_bind(Some(&e)), None);
2413
2414        // Non-bind RHS and a missing WHERE -> None.
2415        assert_eq!(extract_rowid_bind(Some(&eq(col("rowid"), col("id")))), None);
2416        assert_eq!(extract_rowid_bind(None), None);
2417    }
2418
2419    #[test]
2420    fn test_result_column_count_expands_stars() {
2421        let table = TableSchema {
2422            name: "t".to_owned(),
2423            root_page: 2,
2424            columns: vec![
2425                ColumnInfo {
2426                    name: "a".to_owned(),
2427                    affinity: 'd',
2428                    default_value: None,
2429                },
2430                ColumnInfo {
2431                    name: "b".to_owned(),
2432                    affinity: 'b',
2433                    default_value: None,
2434                },
2435                ColumnInfo {
2436                    name: "c".to_owned(),
2437                    affinity: 'c',
2438                    default_value: None,
2439                },
2440            ],
2441            indexes: vec![],
2442        };
2443        let expr = || ResultColumn::Expr {
2444            expr: Expr::Literal(Literal::Integer(1), Span::ZERO),
2445            alias: None,
2446        };
2447
2448        assert_eq!(result_column_count(&[], &table), 0);
2449        assert_eq!(result_column_count(&[expr(), expr()], &table), 2);
2450        // `*` expands to the table's column count (3); each Expr adds 1.
2451        assert_eq!(result_column_count(&[ResultColumn::Star], &table), 3);
2452        assert_eq!(
2453            result_column_count(&[ResultColumn::Star, expr()], &table),
2454            4
2455        );
2456        // Each star expands independently.
2457        assert_eq!(
2458            result_column_count(&[ResultColumn::Star, ResultColumn::Star], &table),
2459            6
2460        );
2461
2462        // `t.*` (TableStar) expands exactly like `*` and composes with a plain
2463        // star and with an expr. This variant was not previously constructed by
2464        // the test, even though result_column_count handles it identically.
2465        let table_star = || ResultColumn::TableStar(QualifiedName::bare("t"));
2466        assert_eq!(result_column_count(&[table_star()], &table), 3);
2467        assert_eq!(
2468            result_column_count(&[table_star(), ResultColumn::Star], &table),
2469            6
2470        );
2471        assert_eq!(result_column_count(&[table_star(), expr()], &table), 4);
2472    }
2473
2474    #[test]
2475    fn test_expr_classifiers_column_name_rowid_and_bind_param() {
2476        let col = |name: &str| Expr::Column(ColumnRef::bare(name), Span::ZERO);
2477        let lit = Expr::Literal(Literal::Integer(7), Span::ZERO);
2478
2479        // column_name: a plain column -> its name; rowid aliases / non-columns -> None.
2480        assert_eq!(column_name(&col("name")), Some("name".to_owned()));
2481        assert_eq!(
2482            column_name(&col("rowid")),
2483            None,
2484            "rowid is excluded from column extraction"
2485        );
2486        assert_eq!(column_name(&col("OID")), None);
2487        assert_eq!(column_name(&lit), None);
2488
2489        // is_rowid_expr: true only for a rowid-alias column expression.
2490        assert!(is_rowid_expr(&col("rowid")));
2491        assert!(is_rowid_expr(&col("_rowid_")));
2492        assert!(!is_rowid_expr(&col("name")));
2493        assert!(!is_rowid_expr(&lit));
2494
2495        // bind_param_ref: numbered/anonymous placeholders; non-placeholder -> None.
2496        assert_eq!(
2497            bind_param_ref(&placeholder(5)),
2498            Some(BindParamRef::Numbered(5))
2499        );
2500        assert_eq!(
2501            bind_param_ref(&Expr::Placeholder(PlaceholderType::Anonymous, Span::ZERO)),
2502            Some(BindParamRef::Anonymous)
2503        );
2504        assert_eq!(bind_param_ref(&lit), None);
2505        // bind_param_index collapses anonymous to 1, numbered to its index.
2506        assert_eq!(bind_param_index(&placeholder(5)), Some(5));
2507        assert_eq!(
2508            bind_param_index(&Expr::Placeholder(PlaceholderType::Anonymous, Span::ZERO)),
2509            Some(1)
2510        );
2511    }
2512
2513    #[test]
2514    fn test_default_value_to_expr_handles_missing_valid_and_invalid_defaults() {
2515        let no_default = ColumnInfo {
2516            name: "a".to_owned(),
2517            affinity: 'd',
2518            default_value: None,
2519        };
2520        let with_default = ColumnInfo {
2521            name: "b".to_owned(),
2522            affinity: 'd',
2523            default_value: Some("42".to_owned()),
2524        };
2525        let bad_default = ColumnInfo {
2526            name: "c".to_owned(),
2527            affinity: 'd',
2528            default_value: Some("1 +".to_owned()),
2529        };
2530        let table = TableSchema {
2531            name: "t".to_owned(),
2532            root_page: 2,
2533            columns: vec![no_default.clone(), with_default.clone()],
2534            indexes: vec![],
2535        };
2536
2537        // No DEFAULT -> NULL literal.
2538        assert!(matches!(
2539            default_value_to_expr(&table, &no_default),
2540            Ok(Expr::Literal(Literal::Null, _))
2541        ));
2542        // A valid DEFAULT parses to an expression.
2543        assert!(default_value_to_expr(&table, &with_default).is_ok());
2544        // An unparseable DEFAULT surfaces as a CodegenError::Unsupported.
2545        assert!(matches!(
2546            default_value_to_expr(&table, &bad_default),
2547            Err(CodegenError::Unsupported(_))
2548        ));
2549
2550        // insert_default_exprs produces one expr per column; the no-default
2551        // column yields a NULL literal.
2552        let defaults = insert_default_exprs(&table).expect("defaults compile");
2553        assert_eq!(defaults.len(), 2);
2554        assert!(
2555            matches!(defaults[0], Expr::Literal(Literal::Null, _)),
2556            "column a has no default -> NULL"
2557        );
2558    }
2559
2560    #[test]
2561    fn test_insert_target_indices_resolution_order_and_errors() {
2562        let table = TableSchema {
2563            name: "t".to_owned(),
2564            root_page: 2,
2565            columns: vec![
2566                ColumnInfo {
2567                    name: "a".to_owned(),
2568                    affinity: 'd',
2569                    default_value: None,
2570                },
2571                ColumnInfo {
2572                    name: "b".to_owned(),
2573                    affinity: 'd',
2574                    default_value: None,
2575                },
2576                ColumnInfo {
2577                    name: "c".to_owned(),
2578                    affinity: 'd',
2579                    default_value: None,
2580                },
2581            ],
2582            indexes: vec![],
2583        };
2584
2585        // An empty column list targets all columns in declaration order.
2586        assert_eq!(insert_target_indices(&[], &table).unwrap(), vec![0, 1, 2]);
2587
2588        // An explicit list resolves to positions in INSERT order (not table
2589        // order), case-insensitively.
2590        assert_eq!(
2591            insert_target_indices(&["c".to_owned(), "A".to_owned()], &table).unwrap(),
2592            vec![2, 0]
2593        );
2594
2595        // An unknown column is rejected with ColumnNotFound.
2596        assert!(matches!(
2597            insert_target_indices(&["a".to_owned(), "missing".to_owned()], &table),
2598            Err(CodegenError::ColumnNotFound { ref column, .. }) if column == "missing"
2599        ));
2600    }
2601
2602    #[test]
2603    fn test_expand_insert_values_row_places_values_and_fills_defaults() {
2604        let lit = |n: i64| Expr::Literal(Literal::Integer(n), Span::ZERO);
2605        let table = TableSchema {
2606            name: "t".to_owned(),
2607            root_page: 2,
2608            columns: vec![
2609                ColumnInfo {
2610                    name: "a".to_owned(),
2611                    affinity: 'd',
2612                    default_value: None,
2613                },
2614                ColumnInfo {
2615                    name: "b".to_owned(),
2616                    affinity: 'd',
2617                    default_value: Some("99".to_owned()),
2618                },
2619                ColumnInfo {
2620                    name: "c".to_owned(),
2621                    affinity: 'd',
2622                    default_value: None,
2623                },
2624            ],
2625            indexes: vec![],
2626        };
2627
2628        // No column list -> values pass through unchanged.
2629        let passed = expand_insert_values_row(&[lit(1), lit(2), lit(3)], &[], &table).unwrap();
2630        assert_eq!(passed.len(), 3);
2631        assert!(matches!(passed[0], Expr::Literal(Literal::Integer(1), _)));
2632        assert!(matches!(passed[2], Expr::Literal(Literal::Integer(3), _)));
2633
2634        // INSERT INTO t(c, a) VALUES (10, 20): values land at their TARGET
2635        // positions (a=20, c=10); the omitted column b takes its DEFAULT (99).
2636        let expanded = expand_insert_values_row(
2637            &[lit(10), lit(20)],
2638            &["c".to_owned(), "a".to_owned()],
2639            &table,
2640        )
2641        .unwrap();
2642        assert_eq!(expanded.len(), 3);
2643        assert!(
2644            matches!(expanded[0], Expr::Literal(Literal::Integer(20), _)),
2645            "a = 20"
2646        );
2647        assert!(
2648            matches!(expanded[1], Expr::Literal(Literal::Integer(99), _)),
2649            "b = default 99"
2650        );
2651        assert!(
2652            matches!(expanded[2], Expr::Literal(Literal::Integer(10), _)),
2653            "c = 10"
2654        );
2655
2656        // A value/column count mismatch -> Unsupported.
2657        assert!(matches!(
2658            expand_insert_values_row(&[lit(1)], &["a".to_owned(), "b".to_owned()], &table),
2659            Err(CodegenError::Unsupported(_))
2660        ));
2661    }
2662
2663    #[test]
2664    fn test_single_table_select_source_name_resolves_table_and_rejects_subquery() {
2665        // A bare table source yields its name.
2666        let from = from_table("users");
2667        assert_eq!(
2668            single_table_select_source_name(&from.source).unwrap(),
2669            "users"
2670        );
2671
2672        // A subquery FROM source is not a single-table source -> Unsupported.
2673        let subquery = TableOrSubquery::Subquery {
2674            query: Box::new(star_select("x")),
2675            alias: None,
2676        };
2677        assert!(matches!(
2678            single_table_select_source_name(&subquery),
2679            Err(CodegenError::Unsupported(_))
2680        ));
2681    }
2682
2683    #[test]
2684    fn test_find_table_is_case_insensitive_and_errors_on_missing() {
2685        let schema = vec![
2686            TableSchema {
2687                name: "Users".to_owned(),
2688                root_page: 2,
2689                columns: vec![],
2690                indexes: vec![],
2691            },
2692            TableSchema {
2693                name: "orders".to_owned(),
2694                root_page: 3,
2695                columns: vec![],
2696                indexes: vec![],
2697            },
2698        ];
2699        // Exact and case-insensitive name matches.
2700        assert_eq!(find_table(&schema, "Users").unwrap().name, "Users");
2701        assert_eq!(
2702            find_table(&schema, "users").unwrap().name,
2703            "Users",
2704            "case-insensitive"
2705        );
2706        assert_eq!(find_table(&schema, "ORDERS").unwrap().name, "orders");
2707        // A missing table -> TableNotFound carrying the requested name.
2708        assert!(matches!(
2709            find_table(&schema, "missing"),
2710            Err(CodegenError::TableNotFound(ref n)) if n == "missing"
2711        ));
2712    }
2713
2714    #[test]
2715    fn test_codegen_error_display_messages() {
2716        assert_eq!(
2717            CodegenError::TableNotFound("users".to_owned()).to_string(),
2718            "table not found: users"
2719        );
2720        // The message names the column before the table.
2721        assert_eq!(
2722            CodegenError::ColumnNotFound {
2723                table: "t".to_owned(),
2724                column: "c".to_owned(),
2725            }
2726            .to_string(),
2727            "column c not found in table t"
2728        );
2729        assert_eq!(
2730            CodegenError::Unsupported("DISTINCT".to_owned()).to_string(),
2731            "unsupported: DISTINCT"
2732        );
2733    }
2734
2735    #[test]
2736    fn test_codegen_select_no_from_emits_resultrow_without_cursor() {
2737        let stmt = SelectStatement {
2738            with: None,
2739            body: SelectBody {
2740                select: SelectCore::Select {
2741                    distinct: Distinctness::All,
2742                    columns: vec![ResultColumn::Expr {
2743                        expr: Expr::Literal(Literal::Integer(1), Span::ZERO),
2744                        alias: None,
2745                    }],
2746                    from: None,
2747                    where_clause: None,
2748                    group_by: vec![],
2749                    having: None,
2750                    windows: vec![],
2751                },
2752                compounds: vec![],
2753            },
2754            order_by: vec![],
2755            limit: None,
2756        };
2757        let schema: Vec<TableSchema> = vec![];
2758        let ctx = CodegenContext::default();
2759        let mut b = ProgramBuilder::new();
2760        codegen_select(&mut b, &stmt, &schema, &ctx).unwrap();
2761        let prog = b.finish().unwrap();
2762
2763        // A no-FROM SELECT evaluates the expression and emits ResultRow + Halt...
2764        assert!(has_opcodes(
2765            &prog,
2766            &[Opcode::Integer, Opcode::ResultRow, Opcode::Halt]
2767        ));
2768        // ...and never opens a table cursor.
2769        assert!(
2770            prog.ops().iter().all(|op| op.opcode != Opcode::OpenRead),
2771            "a no-FROM SELECT must not open a read cursor"
2772        );
2773    }
2774
2775    #[test]
2776    fn test_codegen_select_column_eq_emits_filtered_scan() {
2777        // SELECT a FROM t WHERE b = ?1 -> a full scan with an equality filter on
2778        // b (the non-rowid column-eq path), distinct from a plain full scan.
2779        let stmt = simple_select(&["a"], "t", Some(col_eq_param("b", 1)));
2780        let schema = test_schema();
2781        let ctx = CodegenContext::default();
2782        let mut b = ProgramBuilder::new();
2783        codegen_select(&mut b, &stmt, &schema, &ctx).unwrap();
2784        let prog = b.finish().unwrap();
2785
2786        // Loads the bind param, opens the table, reads the filter column and
2787        // skips non-matching rows (Ne), then emits matches and iterates.
2788        assert!(has_opcodes(
2789            &prog,
2790            &[
2791                Opcode::Variable,
2792                Opcode::OpenRead,
2793                Opcode::Rewind,
2794                Opcode::Column,
2795                Opcode::Ne,
2796                Opcode::ResultRow,
2797                Opcode::Next,
2798                Opcode::Halt,
2799            ]
2800        ));
2801    }
2802
2803    #[test]
2804    fn test_codegen_select_values_emits_one_resultrow_per_row() {
2805        let lit = |n: i64| Expr::Literal(Literal::Integer(n), Span::ZERO);
2806        let values = |rows: Vec<Vec<Expr>>| SelectStatement {
2807            with: None,
2808            body: SelectBody {
2809                select: SelectCore::Values(rows),
2810                compounds: vec![],
2811            },
2812            order_by: vec![],
2813            limit: None,
2814        };
2815        let schema: Vec<TableSchema> = vec![];
2816        let ctx = CodegenContext::default();
2817
2818        // VALUES (1,2), (3,4) -> one ResultRow per row, no table cursor.
2819        let stmt = values(vec![vec![lit(1), lit(2)], vec![lit(3), lit(4)]]);
2820        let mut b = ProgramBuilder::new();
2821        codegen_select(&mut b, &stmt, &schema, &ctx).unwrap();
2822        let prog = b.finish().unwrap();
2823        let resultrows = prog
2824            .ops()
2825            .iter()
2826            .filter(|op| op.opcode == Opcode::ResultRow)
2827            .count();
2828        assert_eq!(resultrows, 2, "one ResultRow per VALUES row");
2829        assert!(
2830            prog.ops().iter().all(|op| op.opcode != Opcode::OpenRead),
2831            "VALUES has no table cursor"
2832        );
2833        assert!(has_opcodes(&prog, &[Opcode::ResultRow, Opcode::Halt]));
2834
2835        // Rows with mismatched arity are rejected.
2836        let bad = values(vec![vec![lit(1)], vec![lit(1), lit(2)]]);
2837        let mut b2 = ProgramBuilder::new();
2838        assert!(matches!(
2839            codegen_select(&mut b2, &bad, &schema, &ctx),
2840            Err(CodegenError::Unsupported(_))
2841        ));
2842    }
2843
2844    #[test]
2845    fn test_codegen_insert_threads_conflict_action_into_insert_op() {
2846        let make = |conflict: Option<ConflictAction>| InsertStatement {
2847            with: None,
2848            or_conflict: conflict,
2849            table: QualifiedName::bare("t"),
2850            alias: None,
2851            columns: vec![],
2852            source: InsertSource::Values(vec![vec![placeholder(1), placeholder(2)]]),
2853            upsert: vec![],
2854            returning: vec![],
2855        };
2856        let schema = test_schema();
2857        let ctx = CodegenContext::default();
2858
2859        // The conflict operand (p5) of the emitted Insert op carries the OE_ code.
2860        let oe_of = |conflict: Option<ConflictAction>| -> u16 {
2861            let mut b = ProgramBuilder::new();
2862            codegen_insert(&mut b, &make(conflict), &schema, &ctx).unwrap();
2863            let prog = b.finish().unwrap();
2864            prog.ops()
2865                .iter()
2866                .find(|op| op.opcode == Opcode::Insert)
2867                .expect("Insert op present")
2868                .p5
2869        };
2870
2871        // A plain INSERT defaults to ABORT; OR IGNORE / OR REPLACE thread theirs.
2872        assert_eq!(oe_of(None), OE_ABORT);
2873        assert_eq!(oe_of(Some(ConflictAction::Ignore)), OE_IGNORE);
2874        assert_eq!(oe_of(Some(ConflictAction::Replace)), OE_REPLACE);
2875    }
2876
2877    fn rowid_eq_param() -> Box<Expr> {
2878        Box::new(Expr::BinaryOp {
2879            left: Box::new(Expr::Column(ColumnRef::bare("rowid"), Span::ZERO)),
2880            op: AstBinaryOp::Eq,
2881            right: Box::new(placeholder(1)),
2882            span: Span::ZERO,
2883        })
2884    }
2885
2886    fn col_eq_param(col: &str, n: u32) -> Box<Expr> {
2887        Box::new(Expr::BinaryOp {
2888            left: Box::new(Expr::Column(ColumnRef::bare(col), Span::ZERO)),
2889            op: AstBinaryOp::Eq,
2890            right: Box::new(placeholder(n)),
2891            span: Span::ZERO,
2892        })
2893    }
2894
2895    fn simple_select(
2896        cols: &[&str],
2897        table: &str,
2898        where_clause: Option<Box<Expr>>,
2899    ) -> SelectStatement {
2900        SelectStatement {
2901            with: None,
2902            body: SelectBody {
2903                select: SelectCore::Select {
2904                    distinct: Distinctness::All,
2905                    columns: cols
2906                        .iter()
2907                        .map(|c| ResultColumn::Expr {
2908                            expr: Expr::Column(ColumnRef::bare(*c), Span::ZERO),
2909                            alias: None,
2910                        })
2911                        .collect(),
2912                    from: Some(from_table(table)),
2913                    where_clause,
2914                    group_by: vec![],
2915                    having: None,
2916                    windows: vec![],
2917                },
2918                compounds: vec![],
2919            },
2920            order_by: vec![],
2921            limit: None,
2922        }
2923    }
2924
2925    fn star_select(table: &str) -> SelectStatement {
2926        SelectStatement {
2927            with: None,
2928            body: SelectBody {
2929                select: SelectCore::Select {
2930                    distinct: Distinctness::All,
2931                    columns: vec![ResultColumn::Star],
2932                    from: Some(from_table(table)),
2933                    where_clause: None,
2934                    group_by: vec![],
2935                    having: None,
2936                    windows: vec![],
2937                },
2938                compounds: vec![],
2939            },
2940            order_by: vec![],
2941            limit: None,
2942        }
2943    }
2944
2945    fn opcode_sequence(prog: &VdbeProgram) -> Vec<Opcode> {
2946        prog.ops().iter().map(|op| op.opcode).collect()
2947    }
2948
2949    fn has_opcodes(prog: &VdbeProgram, expected: &[Opcode]) -> bool {
2950        let ops = opcode_sequence(prog);
2951        // Check that expected opcodes appear in order (not necessarily adjacent).
2952        let mut ops_iter = ops.iter();
2953        for expected_op in expected {
2954            if !ops_iter.any(|op| op == expected_op) {
2955                return false;
2956            }
2957        }
2958        true
2959    }
2960
2961    #[test]
2962    fn test_emit_expr_literals() {
2963        let mut b = ProgramBuilder::new();
2964
2965        let reg_real = b.alloc_reg();
2966        emit_expr(
2967            &mut b,
2968            &Expr::Literal(Literal::Float(3.25), Span::ZERO),
2969            reg_real,
2970        )
2971        .unwrap();
2972
2973        let reg_blob = b.alloc_reg();
2974        emit_expr(
2975            &mut b,
2976            &Expr::Literal(Literal::Blob(vec![0, 1, 2, 3]), Span::ZERO),
2977            reg_blob,
2978        )
2979        .unwrap();
2980
2981        let reg_null = b.alloc_reg();
2982        emit_expr(&mut b, &Expr::Literal(Literal::Null, Span::ZERO), reg_null).unwrap();
2983
2984        let reg_true = b.alloc_reg();
2985        emit_expr(&mut b, &Expr::Literal(Literal::True, Span::ZERO), reg_true).unwrap();
2986
2987        let reg_false = b.alloc_reg();
2988        emit_expr(
2989            &mut b,
2990            &Expr::Literal(Literal::False, Span::ZERO),
2991            reg_false,
2992        )
2993        .unwrap();
2994
2995        let reg_current_time = b.alloc_reg();
2996        emit_expr(
2997            &mut b,
2998            &Expr::Literal(Literal::CurrentTime, Span::ZERO),
2999            reg_current_time,
3000        )
3001        .unwrap();
3002
3003        let reg_current_date = b.alloc_reg();
3004        emit_expr(
3005            &mut b,
3006            &Expr::Literal(Literal::CurrentDate, Span::ZERO),
3007            reg_current_date,
3008        )
3009        .unwrap();
3010
3011        let reg_current_timestamp = b.alloc_reg();
3012        emit_expr(
3013            &mut b,
3014            &Expr::Literal(Literal::CurrentTimestamp, Span::ZERO),
3015            reg_current_timestamp,
3016        )
3017        .unwrap();
3018
3019        let prog = b.finish().unwrap();
3020        let ops = prog.ops();
3021        // 5 simple literals + 3 × (String8 + PureFunc) for current time/date/timestamp = 11
3022        assert_eq!(ops.len(), 11);
3023
3024        assert_eq!(ops[0].opcode, Opcode::Real);
3025        assert_eq!(ops[0].p2, reg_real);
3026        assert_eq!(ops[0].p4, P4::Real(3.25));
3027
3028        assert_eq!(ops[1].opcode, Opcode::Blob);
3029        assert_eq!(ops[1].p1, 4);
3030        assert_eq!(ops[1].p2, reg_blob);
3031        assert_eq!(ops[1].p4, P4::Blob(vec![0, 1, 2, 3]));
3032
3033        assert_eq!(ops[2].opcode, Opcode::Null);
3034        assert_eq!(ops[2].p2, reg_null);
3035        assert_eq!(ops[2].p4, P4::None);
3036
3037        assert_eq!(ops[3].opcode, Opcode::Integer);
3038        assert_eq!(ops[3].p1, 1);
3039        assert_eq!(ops[3].p2, reg_true);
3040        assert_eq!(ops[3].p4, P4::None);
3041
3042        assert_eq!(ops[4].opcode, Opcode::Integer);
3043        assert_eq!(ops[4].p1, 0);
3044        assert_eq!(ops[4].p2, reg_false);
3045        assert_eq!(ops[4].p4, P4::None);
3046
3047        // CurrentTime → String8("now") + PureFunc("time")
3048        assert_eq!(ops[5].opcode, Opcode::String8);
3049        assert_eq!(ops[6].opcode, Opcode::PureFunc);
3050        assert_eq!(ops[6].p3, reg_current_time);
3051
3052        // CurrentDate → String8("now") + PureFunc("date")
3053        assert_eq!(ops[7].opcode, Opcode::String8);
3054        assert_eq!(ops[8].opcode, Opcode::PureFunc);
3055        assert_eq!(ops[8].p3, reg_current_date);
3056
3057        // CurrentTimestamp → String8("now") + PureFunc("datetime")
3058        assert_eq!(ops[9].opcode, Opcode::String8);
3059        assert_eq!(ops[10].opcode, Opcode::PureFunc);
3060        assert_eq!(ops[10].p3, reg_current_timestamp);
3061    }
3062
3063    #[test]
3064    fn test_emit_expr_scalar_literal_opcodes() {
3065        // test_emit_expr_large_integer_literal_uses_int64_opcode covers the
3066        // Integer small/large split; this pins the opcode (and p1, for booleans)
3067        // that each remaining scalar literal lowers to.
3068        let emit_first = |lit: Literal| -> (Opcode, i32) {
3069            let mut b = ProgramBuilder::new();
3070            let reg = b.alloc_reg();
3071            emit_expr(&mut b, &Expr::Literal(lit, Span::ZERO), reg).unwrap();
3072            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3073            let prog = b.finish().unwrap();
3074            let op = prog
3075                .ops()
3076                .iter()
3077                .find(|o| o.opcode != Opcode::Halt)
3078                .expect("a literal op");
3079            (op.opcode, op.p1)
3080        };
3081
3082        assert_eq!(emit_first(Literal::Null).0, Opcode::Null);
3083        assert_eq!(emit_first(Literal::Float(1.5)).0, Opcode::Real);
3084        assert_eq!(
3085            emit_first(Literal::String("hi".to_owned())).0,
3086            Opcode::String8
3087        );
3088        assert_eq!(emit_first(Literal::Blob(vec![1, 2, 3])).0, Opcode::Blob);
3089
3090        // Boolean literals both lower to Integer, distinguished by p1 (1 / 0).
3091        assert_eq!(emit_first(Literal::True), (Opcode::Integer, 1));
3092        assert_eq!(emit_first(Literal::False), (Opcode::Integer, 0));
3093    }
3094
3095    #[test]
3096    fn test_emit_expr_arithmetic_and_unary_ops() {
3097        // emit_expr lowers a non-comparison BinaryOp to the matching arithmetic
3098        // opcode (left into dest, right into a temp), and the four unary ops to:
3099        // Negate -> multiply by -1, Plus -> no-op, Not -> Not, BitNot -> BitNot.
3100        let lit = |n: i64| Box::new(Expr::Literal(Literal::Integer(n), Span::ZERO));
3101        let prog_of = |expr: Expr| {
3102            let mut b = ProgramBuilder::new();
3103            let reg = b.alloc_reg();
3104            emit_expr(&mut b, &expr, reg).unwrap();
3105            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3106            b.finish().unwrap()
3107        };
3108
3109        // 3 + 5 -> Integer, Integer, Add.
3110        let add = prog_of(Expr::BinaryOp {
3111            left: lit(3),
3112            op: AstBinaryOp::Add,
3113            right: lit(5),
3114            span: Span::ZERO,
3115        });
3116        assert!(has_opcodes(
3117            &add,
3118            &[Opcode::Integer, Opcode::Integer, Opcode::Add]
3119        ));
3120
3121        // -3 -> operand, Integer(-1), Multiply (negation lowers to x * -1).
3122        let neg = prog_of(Expr::UnaryOp {
3123            op: AstUnaryOp::Negate,
3124            expr: lit(3),
3125            span: Span::ZERO,
3126        });
3127        assert!(has_opcodes(
3128            &neg,
3129            &[Opcode::Integer, Opcode::Integer, Opcode::Multiply]
3130        ));
3131
3132        // NOT x -> Not; ~x -> BitNot.
3133        let not = prog_of(Expr::UnaryOp {
3134            op: AstUnaryOp::Not,
3135            expr: lit(3),
3136            span: Span::ZERO,
3137        });
3138        assert!(not.ops().iter().any(|o| o.opcode == Opcode::Not));
3139        let bitnot = prog_of(Expr::UnaryOp {
3140            op: AstUnaryOp::BitNot,
3141            expr: lit(3),
3142            span: Span::ZERO,
3143        });
3144        assert!(bitnot.ops().iter().any(|o| o.opcode == Opcode::BitNot));
3145
3146        // Unary plus is a no-op: only the operand (one Integer) plus the Halt.
3147        let plus = prog_of(Expr::UnaryOp {
3148            op: AstUnaryOp::Plus,
3149            expr: lit(3),
3150            span: Span::ZERO,
3151        });
3152        let non_halt: Vec<Opcode> = plus
3153            .ops()
3154            .iter()
3155            .map(|o| o.opcode)
3156            .filter(|&o| o != Opcode::Halt)
3157            .collect();
3158        assert_eq!(
3159            non_halt,
3160            vec![Opcode::Integer],
3161            "unary plus emits only the operand"
3162        );
3163    }
3164
3165    #[test]
3166    fn test_emit_expr_cast_threads_affinity_char_into_cast_op_p2() {
3167        // CAST(expr AS type) emits a Cast op whose p2 is the affinity
3168        // character's byte value (type_to_affinity applied to the type name).
3169        // type_to_affinity is unit-tested, but its wire-up into the emitted Cast
3170        // op was not.
3171        let cast_p2 = |type_name: &str| -> i32 {
3172            let mut b = ProgramBuilder::new();
3173            let reg = b.alloc_reg();
3174            let expr = Expr::Cast {
3175                expr: Box::new(Expr::Literal(Literal::Integer(3), Span::ZERO)),
3176                type_name: fsqlite_ast::TypeName {
3177                    name: type_name.to_owned(),
3178                    arg1: None,
3179                    arg2: None,
3180                },
3181                span: Span::ZERO,
3182            };
3183            emit_expr(&mut b, &expr, reg).unwrap();
3184            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3185            let prog = b.finish().unwrap();
3186            prog.ops()
3187                .iter()
3188                .find(|o| o.opcode == Opcode::Cast)
3189                .expect("Cast op present")
3190                .p2
3191        };
3192
3193        // Affinity char codes: A=Blob(65), B=Text(66), D=Integer(68), E=Real(69).
3194        assert_eq!(cast_p2("INTEGER"), i32::from(b'D'));
3195        assert_eq!(cast_p2("TEXT"), i32::from(b'B'));
3196        assert_eq!(cast_p2("REAL"), i32::from(b'E'));
3197        assert_eq!(cast_p2("BLOB"), i32::from(b'A'));
3198    }
3199
3200    #[test]
3201    fn test_emit_expr_is_null_vs_is_not_null_guard_opcode() {
3202        // x IS NULL and x IS NOT NULL both materialize 1/0, but the guard is
3203        // IsNull vs NotNull respectively. emit_expr's IsNull branch was untested.
3204        let prog_of = |not: bool| {
3205            let mut b = ProgramBuilder::new();
3206            let reg = b.alloc_reg();
3207            let expr = Expr::IsNull {
3208                expr: Box::new(Expr::Literal(Literal::Integer(3), Span::ZERO)),
3209                not,
3210                span: Span::ZERO,
3211            };
3212            emit_expr(&mut b, &expr, reg).unwrap();
3213            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3214            b.finish().unwrap()
3215        };
3216
3217        // IS NULL: guarded by IsNull, then the 0 / Goto / 1 materialization.
3218        let is_null = prog_of(false);
3219        assert!(has_opcodes(
3220            &is_null,
3221            &[
3222                Opcode::IsNull,
3223                Opcode::Integer,
3224                Opcode::Goto,
3225                Opcode::Integer
3226            ]
3227        ));
3228        assert!(!is_null.ops().iter().any(|o| o.opcode == Opcode::NotNull));
3229
3230        // IS NOT NULL: guarded by NotNull instead.
3231        let is_not_null = prog_of(true);
3232        assert!(has_opcodes(
3233            &is_not_null,
3234            &[
3235                Opcode::NotNull,
3236                Opcode::Integer,
3237                Opcode::Goto,
3238                Opcode::Integer
3239            ]
3240        ));
3241        assert!(!is_not_null.ops().iter().any(|o| o.opcode == Opcode::IsNull));
3242    }
3243
3244    #[test]
3245    fn test_emit_expr_function_call_canonicalizes_name_and_threads_arg_count() {
3246        // emit_expr lowers a scalar function call to PureFunc, canonicalizing the
3247        // name to uppercase (the registry's canonical form) and threading the
3248        // argument count into p5.
3249        let mut b = ProgramBuilder::new();
3250        let reg = b.alloc_reg();
3251        let expr = Expr::FunctionCall {
3252            name: "abs".to_owned(), // lowercase on purpose
3253            args: FunctionArgs::List(vec![Expr::Literal(Literal::Integer(3), Span::ZERO)]),
3254            distinct: false,
3255            order_by: vec![],
3256            filter: None,
3257            over: None,
3258            span: Span::ZERO,
3259        };
3260        emit_expr(&mut b, &expr, reg).unwrap();
3261        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3262        let prog = b.finish().unwrap();
3263        let func = prog
3264            .ops()
3265            .iter()
3266            .find(|o| o.opcode == Opcode::PureFunc)
3267            .expect("PureFunc op present");
3268        assert!(
3269            matches!(&func.p4, P4::FuncName(n) if n.as_str() == "ABS"),
3270            "lowercase 'abs' must be canonicalized to uppercase in P4"
3271        );
3272        assert_eq!(func.p5, 1, "one argument threaded into p5");
3273    }
3274
3275    #[test]
3276    fn test_emit_expr_collate_is_transparent() {
3277        // COLLATE is a no-op at this codegen layer: emit_expr(x COLLATE C) emits
3278        // exactly what emit_expr(x) would, with no collation-specific opcode.
3279        let mut b = ProgramBuilder::new();
3280        let reg = b.alloc_reg();
3281        let expr = Expr::Collate {
3282            expr: Box::new(Expr::Literal(Literal::Integer(3), Span::ZERO)),
3283            collation: "NOCASE".to_owned(),
3284            span: Span::ZERO,
3285        };
3286        emit_expr(&mut b, &expr, reg).unwrap();
3287        b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3288        let prog = b.finish().unwrap();
3289        let non_halt: Vec<Opcode> = prog
3290            .ops()
3291            .iter()
3292            .map(|o| o.opcode)
3293            .filter(|&o| o != Opcode::Halt)
3294            .collect();
3295        assert_eq!(
3296            non_halt,
3297            vec![Opcode::Integer],
3298            "COLLATE wraps transparently: only the inner literal's opcode is emitted"
3299        );
3300    }
3301
3302    #[test]
3303    fn test_emit_expr_case_searched_and_simple_forms() {
3304        let lit = |n: i64| Expr::Literal(Literal::Integer(n), Span::ZERO);
3305        let prog_of = |expr: Expr| {
3306            let mut b = ProgramBuilder::new();
3307            let reg = b.alloc_reg();
3308            emit_expr(&mut b, &expr, reg).unwrap();
3309            b.emit_op(Opcode::Halt, 0, 0, 0, P4::None, 0);
3310            b.finish().unwrap()
3311        };
3312
3313        // Searched CASE (no operand, no ELSE): each WHEN is guarded by IfNot,
3314        // and the missing ELSE falls through to a Null default.
3315        let searched = prog_of(Expr::Case {
3316            operand: None,
3317            whens: vec![(lit(1), lit(10))],
3318            else_expr: None,
3319            span: Span::ZERO,
3320        });
3321        assert!(
3322            searched.ops().iter().any(|o| o.opcode == Opcode::IfNot),
3323            "searched CASE guards each WHEN with IfNot"
3324        );
3325        assert!(
3326            searched.ops().iter().any(|o| o.opcode == Opcode::Null),
3327            "a CASE with no ELSE falls through to a Null default"
3328        );
3329
3330        // Simple CASE (with operand and an ELSE): WHENs compare against the
3331        // operand with Ne, and the explicit ELSE means no Null default.
3332        let simple = prog_of(Expr::Case {
3333            operand: Some(Box::new(lit(5))),
3334            whens: vec![(lit(5), lit(10))],
3335            else_expr: Some(Box::new(lit(0))),
3336            span: Span::ZERO,
3337        });
3338        assert!(
3339            simple.ops().iter().any(|o| o.opcode == Opcode::Ne),
3340            "simple CASE compares the operand against each WHEN with Ne"
3341        );
3342        assert!(
3343            !simple.ops().iter().any(|o| o.opcode == Opcode::Null),
3344            "an explicit ELSE means no implicit Null default"
3345        );
3346    }
3347
3348    #[test]
3349    fn test_emit_expr_unsupported_expr_returns_error() {
3350        // emit_expr handles literals, placeholders, binary/unary ops, IsNull,
3351        // Cast, FunctionCall, Case, and Collate; everything else hits the
3352        // catch-all and returns Unsupported. A bare column reference is only
3353        // emittable in a cursor/scan context (via emit_column_reads), not as a
3354        // free expression, so emit_expr rejects it.
3355        let mut b = ProgramBuilder::new();
3356        let reg = b.alloc_reg();
3357        let err = emit_expr(&mut b, &Expr::Column(ColumnRef::bare("x"), Span::ZERO), reg)
3358            .expect_err("a free column reference is not emittable by emit_expr");
3359        assert!(matches!(err, CodegenError::Unsupported(_)));
3360    }
3361
3362    #[test]
3363    fn test_emit_expr_large_integer_literal_uses_int64_opcode() {
3364        let mut b = ProgramBuilder::new();
3365        let reg = b.alloc_reg();
3366        let value = 4_102_444_800_000_000_i64;
3367        emit_expr(
3368            &mut b,
3369            &Expr::Literal(Literal::Integer(value), Span::ZERO),
3370            reg,
3371        )
3372        .unwrap();
3373
3374        let prog = b.finish().unwrap();
3375        let ops = prog.ops();
3376        assert_eq!(ops.len(), 1);
3377        assert_eq!(ops[0].opcode, Opcode::Int64);
3378        assert_eq!(ops[0].p2, reg);
3379        assert_eq!(ops[0].p4, P4::Int64(value));
3380    }
3381
3382    // === Test 1: SELECT by rowid ===
3383    #[test]
3384    fn test_codegen_select_by_rowid() {
3385        let stmt = simple_select(&["b"], "t", Some(rowid_eq_param()));
3386        let schema = test_schema();
3387        let ctx = CodegenContext::default();
3388        let mut b = ProgramBuilder::new();
3389        codegen_select(&mut b, &stmt, &schema, &ctx).unwrap();
3390        let prog = b.finish().unwrap();
3391
3392        assert!(has_opcodes(
3393            &prog,
3394            &[
3395                Opcode::Init,
3396                Opcode::Transaction,
3397                Opcode::Variable,
3398                Opcode::OpenRead,
3399                Opcode::SeekRowid,
3400                Opcode::Column,
3401                Opcode::ResultRow,
3402                Opcode::Close,
3403                Opcode::Halt,
3404            ]
3405        ));
3406        // Transaction should be read-only (p2=0).
3407        let txn = prog
3408            .ops()
3409            .iter()
3410            .find(|op| op.opcode == Opcode::Transaction)
3411            .unwrap();
3412        assert_eq!(txn.p2, 0);
3413    }
3414
3415    #[test]
3416    fn test_codegen_select_parenthesized_single_table_source() -> Result<(), String> {
3417        let stmt = SelectStatement {
3418            with: None,
3419            body: SelectBody {
3420                select: SelectCore::Select {
3421                    distinct: Distinctness::All,
3422                    columns: vec![ResultColumn::Expr {
3423                        expr: Expr::Column(ColumnRef::bare("b"), Span::ZERO),
3424                        alias: None,
3425                    }],
3426                    from: Some(FromClause {
3427                        source: TableOrSubquery::ParenJoin(Box::new(from_table("t"))),
3428                        joins: vec![],
3429                    }),
3430                    where_clause: Some(rowid_eq_param()),
3431                    group_by: vec![],
3432                    having: None,
3433                    windows: vec![],
3434                },
3435                compounds: vec![],
3436            },
3437            order_by: vec![],
3438            limit: None,
3439        };
3440        let schema = test_schema();
3441        let ctx = CodegenContext::default();
3442        let mut b = ProgramBuilder::new();
3443        codegen_select(&mut b, &stmt, &schema, &ctx).map_err(|err| format!("{err:?}"))?;
3444        let prog = b.finish().map_err(|err| format!("{err:?}"))?;
3445
3446        if !has_opcodes(
3447            &prog,
3448            &[
3449                Opcode::OpenRead,
3450                Opcode::SeekRowid,
3451                Opcode::Column,
3452                Opcode::ResultRow,
3453            ],
3454        ) {
3455            return Err(format!(
3456                "parenthesized table source should use table SELECT path, got {:?}",
3457                opcode_sequence(&prog)
3458            ));
3459        }
3460
3461        let open_read_root = prog
3462            .ops()
3463            .iter()
3464            .find(|op| op.opcode == Opcode::OpenRead)
3465            .map(|op| op.p2);
3466        if open_read_root != Some(2) {
3467            return Err(format!(
3468                "expected OpenRead root page 2, got {open_read_root:?}"
3469            ));
3470        }
3471
3472        Ok(())
3473    }
3474
3475    #[test]
3476    fn test_codegen_select_values_multirow() -> Result<(), String> {
3477        let stmt = SelectStatement {
3478            with: None,
3479            body: SelectBody {
3480                select: SelectCore::Values(vec![
3481                    vec![
3482                        Expr::Literal(Literal::Integer(1), Span::ZERO),
3483                        Expr::Literal(Literal::String("alpha".to_owned()), Span::ZERO),
3484                    ],
3485                    vec![
3486                        Expr::Literal(Literal::Integer(2), Span::ZERO),
3487                        Expr::Literal(Literal::String("beta".to_owned()), Span::ZERO),
3488                    ],
3489                ]),
3490                compounds: vec![],
3491            },
3492            order_by: vec![],
3493            limit: None,
3494        };
3495        let ctx = CodegenContext::default();
3496        let mut b = ProgramBuilder::new();
3497        codegen_select(&mut b, &stmt, &[], &ctx).map_err(|err| format!("{err:?}"))?;
3498        let prog = b.finish().map_err(|err| format!("{err:?}"))?;
3499
3500        if !has_opcodes(
3501            &prog,
3502            &[
3503                Opcode::Init,
3504                Opcode::Transaction,
3505                Opcode::Integer,
3506                Opcode::String8,
3507                Opcode::ResultRow,
3508                Opcode::Integer,
3509                Opcode::String8,
3510                Opcode::ResultRow,
3511                Opcode::Halt,
3512            ],
3513        ) {
3514            return Err(format!(
3515                "VALUES SELECT should emit one result row per VALUES row, got {:?}",
3516                opcode_sequence(&prog)
3517            ));
3518        }
3519
3520        let result_row_count = prog
3521            .ops()
3522            .iter()
3523            .filter(|op| op.opcode == Opcode::ResultRow && op.p2 == 2)
3524            .count();
3525        if result_row_count != 2 {
3526            return Err(format!(
3527                "VALUES SELECT should emit two two-column ResultRow ops, got {result_row_count}"
3528            ));
3529        }
3530
3531        Ok(())
3532    }
3533
3534    #[test]
3535    fn test_codegen_select_values_rejects_mismatched_arity() -> Result<(), String> {
3536        let stmt = SelectStatement {
3537            with: None,
3538            body: SelectBody {
3539                select: SelectCore::Values(vec![
3540                    vec![Expr::Literal(Literal::Integer(1), Span::ZERO)],
3541                    vec![
3542                        Expr::Literal(Literal::Integer(2), Span::ZERO),
3543                        Expr::Literal(Literal::Integer(3), Span::ZERO),
3544                    ],
3545                ]),
3546                compounds: vec![],
3547            },
3548            order_by: vec![],
3549            limit: None,
3550        };
3551        let ctx = CodegenContext::default();
3552        let mut b = ProgramBuilder::new();
3553
3554        match codegen_select(&mut b, &stmt, &[], &ctx) {
3555            Err(CodegenError::Unsupported(msg)) if msg.contains("same arity") => Ok(()),
3556            other => Err(format!("expected VALUES arity error, got {other:?}")),
3557        }
3558    }
3559
3560    // === Test 2: INSERT VALUES ===
3561    #[test]
3562    fn test_codegen_insert_values() {
3563        let stmt = InsertStatement {
3564            with: None,
3565            or_conflict: None,
3566            table: QualifiedName::bare("t"),
3567            alias: None,
3568            columns: vec![],
3569            source: InsertSource::Values(vec![vec![placeholder(1), placeholder(2)]]),
3570            upsert: vec![],
3571            returning: vec![],
3572        };
3573        let schema = test_schema();
3574        let ctx = CodegenContext::default();
3575        let mut b = ProgramBuilder::new();
3576        codegen_insert(&mut b, &stmt, &schema, &ctx).unwrap();
3577        let prog = b.finish().unwrap();
3578
3579        assert!(has_opcodes(
3580            &prog,
3581            &[
3582                Opcode::Init,
3583                Opcode::Transaction,
3584                Opcode::OpenWrite,
3585                Opcode::NewRowid,
3586                Opcode::Variable,
3587                Opcode::Variable,
3588                Opcode::MakeRecord,
3589                Opcode::Insert,
3590                Opcode::Close,
3591                Opcode::Halt,
3592            ]
3593        ));
3594        // Transaction should be write (p2=1).
3595        let txn = prog
3596            .ops()
3597            .iter()
3598            .find(|op| op.opcode == Opcode::Transaction)
3599            .unwrap();
3600        assert_eq!(txn.p2, 1);
3601    }
3602
3603    #[test]
3604    fn test_codegen_insert_values_uses_declared_defaults_for_omitted_columns() {
3605        let stmt = InsertStatement {
3606            with: None,
3607            or_conflict: None,
3608            table: QualifiedName::bare("t"),
3609            alias: None,
3610            columns: vec!["name".to_owned()],
3611            source: InsertSource::Values(vec![vec![placeholder(1)]]),
3612            upsert: vec![],
3613            returning: vec![],
3614        };
3615        let schema = vec![TableSchema {
3616            name: "t".to_owned(),
3617            root_page: 2,
3618            columns: vec![
3619                ColumnInfo {
3620                    name: "id".to_owned(),
3621                    affinity: 'd',
3622                    default_value: None,
3623                },
3624                ColumnInfo {
3625                    name: "name".to_owned(),
3626                    affinity: 'C',
3627                    default_value: None,
3628                },
3629                ColumnInfo {
3630                    name: "status".to_owned(),
3631                    affinity: 'C',
3632                    default_value: Some("'pending'".to_owned()),
3633                },
3634            ],
3635            indexes: vec![],
3636        }];
3637        let ctx = CodegenContext::default();
3638        let mut b = ProgramBuilder::new();
3639        codegen_insert(&mut b, &stmt, &schema, &ctx).unwrap();
3640        let prog = b.finish().unwrap();
3641
3642        assert!(
3643            prog.ops()
3644                .iter()
3645                .any(|op| op.opcode == Opcode::String8 && op.p4 == P4::Str("pending".to_owned()))
3646        );
3647    }
3648
3649    #[test]
3650    fn test_codegen_insert_allows_duplicate_target_columns() {
3651        let stmt = InsertStatement {
3652            with: None,
3653            or_conflict: None,
3654            table: QualifiedName::bare("t"),
3655            alias: None,
3656            columns: vec!["b".to_owned(), "b".to_owned()],
3657            source: InsertSource::Values(vec![vec![placeholder(1), placeholder(2)]]),
3658            upsert: vec![],
3659            returning: vec![],
3660        };
3661        let schema = test_schema();
3662        let ctx = CodegenContext::default();
3663        let mut b = ProgramBuilder::new();
3664        codegen_insert(&mut b, &stmt, &schema, &ctx).unwrap();
3665    }
3666
3667    #[test]
3668    fn test_codegen_insert_default_values_uses_declared_defaults() {
3669        let stmt = InsertStatement {
3670            with: None,
3671            or_conflict: None,
3672            table: QualifiedName::bare("t"),
3673            alias: None,
3674            columns: vec![],
3675            source: InsertSource::DefaultValues,
3676            upsert: vec![],
3677            returning: vec![],
3678        };
3679        let schema = vec![TableSchema {
3680            name: "t".to_owned(),
3681            root_page: 2,
3682            columns: vec![
3683                ColumnInfo {
3684                    name: "id".to_owned(),
3685                    affinity: 'd',
3686                    default_value: None,
3687                },
3688                ColumnInfo {
3689                    name: "status".to_owned(),
3690                    affinity: 'C',
3691                    default_value: Some("'active'".to_owned()),
3692                },
3693                ColumnInfo {
3694                    name: "count".to_owned(),
3695                    affinity: 'd',
3696                    default_value: Some("42".to_owned()),
3697                },
3698            ],
3699            indexes: vec![],
3700        }];
3701        let ctx = CodegenContext::default();
3702        let mut b = ProgramBuilder::new();
3703        codegen_insert(&mut b, &stmt, &schema, &ctx).unwrap();
3704        let prog = b.finish().unwrap();
3705
3706        assert!(
3707            prog.ops()
3708                .iter()
3709                .any(|op| op.opcode == Opcode::String8 && op.p4 == P4::Str("active".to_owned()))
3710        );
3711        assert!(
3712            prog.ops()
3713                .iter()
3714                .any(|op| op.opcode == Opcode::Integer && op.p1 == 42)
3715        );
3716    }
3717
3718    #[test]
3719    fn test_codegen_insert_default_values_uses_expression_defaults() {
3720        let stmt = InsertStatement {
3721            with: None,
3722            or_conflict: None,
3723            table: QualifiedName::bare("t"),
3724            alias: None,
3725            columns: vec![],
3726            source: InsertSource::DefaultValues,
3727            upsert: vec![],
3728            returning: vec![],
3729        };
3730        let schema = vec![TableSchema {
3731            name: "t".to_owned(),
3732            root_page: 2,
3733            columns: vec![
3734                ColumnInfo {
3735                    name: "id".to_owned(),
3736                    affinity: 'd',
3737                    default_value: None,
3738                },
3739                ColumnInfo {
3740                    name: "total".to_owned(),
3741                    affinity: 'd',
3742                    default_value: Some("(40 + 2)".to_owned()),
3743                },
3744            ],
3745            indexes: vec![],
3746        }];
3747        let ctx = CodegenContext::default();
3748        let mut b = ProgramBuilder::new();
3749        codegen_insert(&mut b, &stmt, &schema, &ctx).unwrap();
3750        let prog = b.finish().unwrap();
3751
3752        assert!(
3753            prog.ops().iter().any(|op| op.opcode == Opcode::Add),
3754            "expression defaults should compile as expressions, not string literals"
3755        );
3756    }
3757
3758    #[test]
3759    fn test_codegen_insert_default_values_rejects_unparseable_defaults() {
3760        let stmt = InsertStatement {
3761            with: None,
3762            or_conflict: None,
3763            table: QualifiedName::bare("t"),
3764            alias: None,
3765            columns: vec![],
3766            source: InsertSource::DefaultValues,
3767            upsert: vec![],
3768            returning: vec![],
3769        };
3770        let schema = vec![TableSchema {
3771            name: "t".to_owned(),
3772            root_page: 2,
3773            columns: vec![ColumnInfo {
3774                name: "broken".to_owned(),
3775                affinity: 'C',
3776                default_value: Some("('unterminated".to_owned()),
3777            }],
3778            indexes: vec![],
3779        }];
3780        let ctx = CodegenContext::default();
3781        let mut b = ProgramBuilder::new();
3782        let err = codegen_insert(&mut b, &stmt, &schema, &ctx).unwrap_err();
3783        assert!(
3784            matches!(err, CodegenError::Unsupported(ref msg) if msg.contains("failed to parse DEFAULT expression")),
3785            "unexpected error: {err:?}"
3786        );
3787    }
3788
3789    // === Test: INSERT ... SELECT ===
3790    #[test]
3791    #[allow(clippy::too_many_lines)]
3792    fn test_codegen_insert_select_values() {
3793        // INSERT INTO t VALUES (1) parsed as InsertSource::Select
3794        let inner_values = SelectStatement {
3795            with: None,
3796            body: SelectBody {
3797                select: SelectCore::Values(vec![vec![placeholder(1)]]),
3798                compounds: vec![],
3799            },
3800            order_by: vec![],
3801            limit: None,
3802        };
3803
3804        let stmt = InsertStatement {
3805            with: None,
3806            or_conflict: None,
3807            table: QualifiedName::bare("t"),
3808            alias: None,
3809            columns: vec![],
3810            source: InsertSource::Select(Box::new(inner_values)),
3811            upsert: vec![],
3812            returning: vec![],
3813        };
3814
3815        let schema = test_schema();
3816        let ctx = CodegenContext::default();
3817        let mut b = ProgramBuilder::new();
3818        codegen_insert(&mut b, &stmt, &schema, &ctx).unwrap();
3819        let prog = b.finish().unwrap();
3820
3821        // Should look like normal INSERT VALUES
3822        assert!(has_opcodes(
3823            &prog,
3824            &[
3825                Opcode::Init,
3826                Opcode::Transaction,
3827                Opcode::OpenWrite,
3828                Opcode::NewRowid,
3829                Opcode::Variable,
3830                Opcode::MakeRecord,
3831                Opcode::Insert,
3832                Opcode::Close,
3833                Opcode::Halt,
3834            ]
3835        ));
3836    }
3837
3838    #[test]
3839    #[allow(clippy::too_many_lines)]
3840    fn test_codegen_insert_select() {
3841        // Schema with source "s" and target "t".
3842        let schema = vec![
3843            TableSchema {
3844                name: "t".to_owned(),
3845                root_page: 2,
3846                columns: vec![
3847                    ColumnInfo {
3848                        name: "a".to_owned(),
3849                        affinity: 'd',
3850                        default_value: None,
3851                    },
3852                    ColumnInfo {
3853                        name: "b".to_owned(),
3854                        affinity: 'C',
3855                        default_value: None,
3856                    },
3857                ],
3858                indexes: vec![],
3859            },
3860            TableSchema {
3861                name: "s".to_owned(),
3862                root_page: 3,
3863                columns: vec![
3864                    ColumnInfo {
3865                        name: "x".to_owned(),
3866                        affinity: 'd',
3867                        default_value: None,
3868                    },
3869                    ColumnInfo {
3870                        name: "y".to_owned(),
3871                        affinity: 'C',
3872                        default_value: None,
3873                    },
3874                ],
3875                indexes: vec![],
3876            },
3877        ];
3878
3879        // INSERT INTO t SELECT * FROM s
3880        let inner_select = SelectStatement {
3881            with: None,
3882            body: SelectBody {
3883                select: SelectCore::Select {
3884                    distinct: Distinctness::All,
3885                    columns: vec![ResultColumn::Star],
3886                    from: Some(FromClause {
3887                        source: TableOrSubquery::Table {
3888                            name: QualifiedName::bare("s"),
3889                            alias: None,
3890                            index_hint: None,
3891                            time_travel: None,
3892                        },
3893                        joins: vec![],
3894                    }),
3895                    where_clause: None,
3896                    group_by: vec![],
3897                    having: None,
3898                    windows: vec![],
3899                },
3900                compounds: vec![],
3901            },
3902            order_by: vec![],
3903            limit: None,
3904        };
3905
3906        let stmt = InsertStatement {
3907            with: None,
3908            or_conflict: None,
3909            table: QualifiedName::bare("t"),
3910            alias: None,
3911            columns: vec![],
3912            source: InsertSource::Select(Box::new(inner_select)),
3913            upsert: vec![],
3914            returning: vec![],
3915        };
3916
3917        let ctx = CodegenContext::default();
3918        let mut b = ProgramBuilder::new();
3919        codegen_insert(&mut b, &stmt, &schema, &ctx).unwrap();
3920        let prog = b.finish().unwrap();
3921
3922        assert!(has_opcodes(
3923            &prog,
3924            &[
3925                Opcode::Init,
3926                Opcode::Transaction,
3927                Opcode::OpenWrite,
3928                Opcode::OpenRead,
3929                Opcode::Rewind,
3930                Opcode::Column,
3931                Opcode::Column,
3932                Opcode::NewRowid,
3933                Opcode::MakeRecord,
3934                Opcode::Insert,
3935                Opcode::Next,
3936                Opcode::Close,
3937                Opcode::Close,
3938                Opcode::Halt,
3939            ]
3940        ));
3941
3942        // Transaction should be write (p2=1).
3943        let txn = prog
3944            .ops()
3945            .iter()
3946            .find(|op| op.opcode == Opcode::Transaction)
3947            .unwrap();
3948        assert_eq!(txn.p2, 1);
3949
3950        // OpenWrite should target table "t" (root_page=2).
3951        let open_write = prog
3952            .ops()
3953            .iter()
3954            .find(|op| op.opcode == Opcode::OpenWrite)
3955            .unwrap();
3956        assert_eq!(open_write.p2, 2);
3957
3958        // OpenRead should target table "s" (root_page=3).
3959        let open_read = prog
3960            .ops()
3961            .iter()
3962            .find(|op| op.opcode == Opcode::OpenRead)
3963            .unwrap();
3964        assert_eq!(open_read.p2, 3);
3965    }
3966
3967    // === Test: INSERT ... SELECT without FROM (expression-only) ===
3968    #[test]
3969    fn test_codegen_insert_select_without_from() {
3970        // INSERT INTO t SELECT 42, 'hello'
3971        let inner_select = SelectStatement {
3972            with: None,
3973            body: SelectBody {
3974                select: SelectCore::Select {
3975                    distinct: Distinctness::All,
3976                    columns: vec![
3977                        ResultColumn::Expr {
3978                            expr: Expr::Literal(Literal::Integer(42), Span::ZERO),
3979                            alias: None,
3980                        },
3981                        ResultColumn::Expr {
3982                            expr: Expr::Literal(Literal::String("hello".to_owned()), Span::ZERO),
3983                            alias: None,
3984                        },
3985                    ],
3986                    from: None,
3987                    where_clause: None,
3988                    group_by: vec![],
3989                    having: None,
3990                    windows: vec![],
3991                },
3992                compounds: vec![],
3993            },
3994            order_by: vec![],
3995            limit: None,
3996        };
3997
3998        let stmt = InsertStatement {
3999            with: None,
4000            or_conflict: None,
4001            table: QualifiedName::bare("t"),
4002            alias: None,
4003            columns: vec![],
4004            source: InsertSource::Select(Box::new(inner_select)),
4005            upsert: vec![],
4006            returning: vec![],
4007        };
4008
4009        let schema = test_schema();
4010        let ctx = CodegenContext::default();
4011        let mut b = ProgramBuilder::new();
4012        codegen_insert(&mut b, &stmt, &schema, &ctx).unwrap();
4013        let prog = b.finish().unwrap();
4014
4015        // Should produce a single-row insert without OpenRead/Rewind/Next.
4016        assert!(has_opcodes(
4017            &prog,
4018            &[
4019                Opcode::Init,
4020                Opcode::Transaction,
4021                Opcode::OpenWrite,
4022                Opcode::Integer, // 42
4023                Opcode::String8, // 'hello'
4024                Opcode::NewRowid,
4025                Opcode::MakeRecord,
4026                Opcode::Insert,
4027                Opcode::Close,
4028                Opcode::Halt,
4029            ]
4030        ));
4031
4032        // No OpenRead — no source table.
4033        assert!(prog.ops().iter().all(|op| op.opcode != Opcode::OpenRead));
4034
4035        // Transaction should be write (p2=1).
4036        let txn = prog
4037            .ops()
4038            .iter()
4039            .find(|op| op.opcode == Opcode::Transaction)
4040            .unwrap();
4041        assert_eq!(txn.p2, 1);
4042    }
4043
4044    // === Test 3: UPDATE by rowid ===
4045    #[test]
4046    fn test_codegen_update_by_rowid() {
4047        let stmt = UpdateStatement {
4048            with: None,
4049            or_conflict: None,
4050            table: QualifiedTableRef {
4051                name: QualifiedName::bare("t"),
4052                alias: None,
4053                index_hint: None,
4054                time_travel: None,
4055            },
4056            assignments: vec![Assignment {
4057                target: AssignmentTarget::Column("b".to_owned()),
4058                value: placeholder(1),
4059            }],
4060            from: None,
4061            where_clause: Some(Expr::BinaryOp {
4062                left: Box::new(Expr::Column(ColumnRef::bare("rowid"), Span::ZERO)),
4063                op: AstBinaryOp::Eq,
4064                right: Box::new(placeholder(2)),
4065                span: Span::ZERO,
4066            }),
4067            returning: vec![],
4068            order_by: vec![],
4069            limit: None,
4070        };
4071        let schema = test_schema();
4072        let ctx = CodegenContext::default();
4073        let mut b = ProgramBuilder::new();
4074        codegen_update(&mut b, &stmt, &schema, &ctx).unwrap();
4075        let prog = b.finish().unwrap();
4076
4077        // Verify: reads ALL columns, then overwrites changed one.
4078        assert!(has_opcodes(
4079            &prog,
4080            &[
4081                Opcode::Init,
4082                Opcode::Transaction,
4083                Opcode::Variable, // new value
4084                Opcode::Variable, // rowid
4085                Opcode::OpenWrite,
4086                Opcode::NotExists,
4087                Opcode::Column,     // read existing col a
4088                Opcode::Column,     // read existing col b
4089                Opcode::Copy,       // overwrite b with new value
4090                Opcode::MakeRecord, // pack ALL columns
4091                Opcode::Insert,     // write back
4092                Opcode::Close,
4093                Opcode::Halt,
4094            ]
4095        ));
4096
4097        // MakeRecord should have 2 columns (the full record).
4098        let mr = prog
4099            .ops()
4100            .iter()
4101            .find(|op| op.opcode == Opcode::MakeRecord)
4102            .unwrap();
4103        assert_eq!(mr.p2, 2); // ALL columns, not just the changed one.
4104    }
4105
4106    #[test]
4107    fn test_codegen_update_notexists_jump_skips_insert_to_close() {
4108        // UPDATE t SET b = ?1 WHERE rowid = ?2 performs a read-modify-write:
4109        // NotExists guards the whole block, so when the rowid is absent the jump
4110        // must skip past the Insert (the REPLACE that writes the row back) and
4111        // land on Close. Otherwise a missing-rowid UPDATE would silently insert a
4112        // phantom row. `test_codegen_update_by_rowid` only checks the opcode
4113        // subsequence + MakeRecord arity; this pins the guard's jump target.
4114        let stmt = UpdateStatement {
4115            with: None,
4116            or_conflict: None,
4117            table: QualifiedTableRef {
4118                name: QualifiedName::bare("t"),
4119                alias: None,
4120                index_hint: None,
4121                time_travel: None,
4122            },
4123            assignments: vec![Assignment {
4124                target: AssignmentTarget::Column("b".to_owned()),
4125                value: placeholder(1),
4126            }],
4127            from: None,
4128            where_clause: Some(Expr::BinaryOp {
4129                left: Box::new(Expr::Column(ColumnRef::bare("rowid"), Span::ZERO)),
4130                op: AstBinaryOp::Eq,
4131                right: Box::new(placeholder(2)),
4132                span: Span::ZERO,
4133            }),
4134            returning: vec![],
4135            order_by: vec![],
4136            limit: None,
4137        };
4138        let schema = test_schema();
4139        let ctx = CodegenContext::default();
4140        let mut b = ProgramBuilder::new();
4141        codegen_update(&mut b, &stmt, &schema, &ctx).unwrap();
4142        let prog = b.finish().unwrap();
4143        let ops = prog.ops();
4144
4145        let notexists = ops
4146            .iter()
4147            .position(|op| op.opcode == Opcode::NotExists)
4148            .expect("NotExists op present");
4149        let insert = ops
4150            .iter()
4151            .position(|op| op.opcode == Opcode::Insert)
4152            .expect("Insert (REPLACE write-back) op present");
4153        let close = ops
4154            .iter()
4155            .position(|op| op.opcode == Opcode::Close)
4156            .expect("Close op present");
4157
4158        // The write-back Insert sits strictly between the guard and Close, so it
4159        // is part of the span the NotExists branch hops over.
4160        assert!(
4161            notexists < insert,
4162            "NotExists must precede the write-back Insert"
4163        );
4164        assert!(insert < close, "the write-back Insert must precede Close");
4165
4166        // NotExists jumps to Close when the rowid is absent, skipping the Insert.
4167        assert_eq!(
4168            usize::try_from(ops[notexists].p2).unwrap(),
4169            close,
4170            "NotExists must jump to Close (skipping the write-back Insert) when the rowid is absent"
4171        );
4172    }
4173
4174    #[test]
4175    fn test_codegen_update_makerecord_carries_table_affinity_string() {
4176        // UPDATE re-packs the full row with MakeRecord, whose P4 must be the
4177        // table's affinity string so each column value is coerced to its declared
4178        // affinity on write-back. affinity_string()/type_to_affinity are unit
4179        // tested in isolation, and test_codegen_update_by_rowid checks the
4180        // MakeRecord column count, but neither pins that the computed affinity
4181        // string actually reaches the emitted MakeRecord op's P4 operand.
4182        let stmt = UpdateStatement {
4183            with: None,
4184            or_conflict: None,
4185            table: QualifiedTableRef {
4186                name: QualifiedName::bare("t"),
4187                alias: None,
4188                index_hint: None,
4189                time_travel: None,
4190            },
4191            assignments: vec![Assignment {
4192                target: AssignmentTarget::Column("b".to_owned()),
4193                value: placeholder(1),
4194            }],
4195            from: None,
4196            where_clause: Some(Expr::BinaryOp {
4197                left: Box::new(Expr::Column(ColumnRef::bare("rowid"), Span::ZERO)),
4198                op: AstBinaryOp::Eq,
4199                right: Box::new(placeholder(2)),
4200                span: Span::ZERO,
4201            }),
4202            returning: vec![],
4203            order_by: vec![],
4204            limit: None,
4205        };
4206        let schema = test_schema();
4207        // Derive the expectation from the schema itself (no brittle literal): for
4208        // test_schema this is the two column affinities "dC".
4209        let expected_affinity = schema[0].affinity_string();
4210        let ctx = CodegenContext::default();
4211        let mut b = ProgramBuilder::new();
4212        codegen_update(&mut b, &stmt, &schema, &ctx).unwrap();
4213        let prog = b.finish().unwrap();
4214
4215        let make_record = prog
4216            .ops()
4217            .iter()
4218            .find(|op| op.opcode == Opcode::MakeRecord)
4219            .expect("MakeRecord op present");
4220        match &make_record.p4 {
4221            P4::Affinity(aff) => assert_eq!(
4222                *aff, expected_affinity,
4223                "MakeRecord P4 affinity must equal the table's affinity_string()"
4224            ),
4225            _ => panic!("MakeRecord P4 must be P4::Affinity (got a different P4 variant)"),
4226        }
4227    }
4228
4229    // === Test 4: DELETE by rowid ===
4230    #[test]
4231    fn test_codegen_delete_by_rowid() {
4232        let stmt = DeleteStatement {
4233            with: None,
4234            table: QualifiedTableRef {
4235                name: QualifiedName::bare("t"),
4236                alias: None,
4237                index_hint: None,
4238                time_travel: None,
4239            },
4240            where_clause: Some(Expr::BinaryOp {
4241                left: Box::new(Expr::Column(ColumnRef::bare("rowid"), Span::ZERO)),
4242                op: AstBinaryOp::Eq,
4243                right: Box::new(placeholder(1)),
4244                span: Span::ZERO,
4245            }),
4246            returning: vec![],
4247            order_by: vec![],
4248            limit: None,
4249        };
4250        let schema = test_schema();
4251        let ctx = CodegenContext::default();
4252        let mut b = ProgramBuilder::new();
4253        codegen_delete(&mut b, &stmt, &schema, &ctx).unwrap();
4254        let prog = b.finish().unwrap();
4255
4256        assert!(has_opcodes(
4257            &prog,
4258            &[
4259                Opcode::Init,
4260                Opcode::Transaction,
4261                Opcode::Variable,
4262                Opcode::OpenWrite,
4263                Opcode::NotExists,
4264                Opcode::Delete,
4265                Opcode::Close,
4266                Opcode::Halt,
4267            ]
4268        ));
4269    }
4270
4271    #[test]
4272    fn test_codegen_delete_notexists_jump_skips_delete_to_close() {
4273        // DELETE FROM t WHERE rowid = ?1 guards the row delete with NotExists:
4274        // when the rowid is absent the NotExists jump must skip past Delete and
4275        // land on Close, so a missing row is a no-op rather than deleting whatever
4276        // the cursor currently points at. The subsequence check in
4277        // `test_codegen_delete_by_rowid` only proves the opcodes are present in
4278        // order; this pins the actual jump target.
4279        let stmt = DeleteStatement {
4280            with: None,
4281            table: QualifiedTableRef {
4282                name: QualifiedName::bare("t"),
4283                alias: None,
4284                index_hint: None,
4285                time_travel: None,
4286            },
4287            where_clause: Some(Expr::BinaryOp {
4288                left: Box::new(Expr::Column(ColumnRef::bare("rowid"), Span::ZERO)),
4289                op: AstBinaryOp::Eq,
4290                right: Box::new(placeholder(1)),
4291                span: Span::ZERO,
4292            }),
4293            returning: vec![],
4294            order_by: vec![],
4295            limit: None,
4296        };
4297        let schema = test_schema();
4298        let ctx = CodegenContext::default();
4299        let mut b = ProgramBuilder::new();
4300        codegen_delete(&mut b, &stmt, &schema, &ctx).unwrap();
4301        let prog = b.finish().unwrap();
4302        let ops = prog.ops();
4303
4304        let notexists = ops
4305            .iter()
4306            .position(|op| op.opcode == Opcode::NotExists)
4307            .expect("NotExists op present");
4308        let delete = ops
4309            .iter()
4310            .position(|op| op.opcode == Opcode::Delete)
4311            .expect("Delete op present");
4312        let close = ops
4313            .iter()
4314            .position(|op| op.opcode == Opcode::Close)
4315            .expect("Close op present");
4316
4317        // Delete sits strictly between the guard and Close, so it is exactly the
4318        // instruction the NotExists branch hops over.
4319        assert!(notexists < delete, "NotExists must precede Delete");
4320        assert!(delete < close, "Delete must precede Close");
4321
4322        // NotExists jumps to the Close instruction when the rowid is absent.
4323        assert_eq!(
4324            usize::try_from(ops[notexists].p2).unwrap(),
4325            close,
4326            "NotExists must jump to Close (skipping Delete) when the rowid is absent"
4327        );
4328    }
4329
4330    // === Test 5: Label resolution ===
4331    #[test]
4332    fn test_codegen_label_resolution() {
4333        let stmt = simple_select(&["a"], "t", Some(rowid_eq_param()));
4334        let schema = test_schema();
4335        let ctx = CodegenContext::default();
4336        let mut b = ProgramBuilder::new();
4337        codegen_select(&mut b, &stmt, &schema, &ctx).unwrap();
4338        let prog = b.finish().unwrap();
4339
4340        // All p2 fields that are jumps should have valid addresses (>= 0).
4341        for op in prog.ops() {
4342            if op.opcode.is_jump() {
4343                assert!(
4344                    op.p2 >= 0,
4345                    "unresolved jump at {:?}: p2 = {}",
4346                    op.opcode,
4347                    op.p2
4348                );
4349                assert!(
4350                    usize::try_from(op.p2).unwrap() <= prog.len(),
4351                    "jump target out of range at {:?}: p2 = {} (prog len = {})",
4352                    op.opcode,
4353                    op.p2,
4354                    prog.len()
4355                );
4356            }
4357        }
4358    }
4359
4360    // === Test 6: Register allocation ===
4361    #[test]
4362    fn test_codegen_register_allocation() {
4363        let stmt = InsertStatement {
4364            with: None,
4365            or_conflict: None,
4366            table: QualifiedName::bare("t"),
4367            alias: None,
4368            columns: vec![],
4369            source: InsertSource::Values(vec![vec![placeholder(1), placeholder(2)]]),
4370            upsert: vec![],
4371            returning: vec![],
4372        };
4373        let schema = test_schema();
4374        let ctx = CodegenContext::default();
4375        let mut b = ProgramBuilder::new();
4376        codegen_insert(&mut b, &stmt, &schema, &ctx).unwrap();
4377        let prog = b.finish().unwrap();
4378
4379        // All register references (p1, p2, p3 where applicable) should be
4380        // within the allocated range.
4381        let max_reg = prog.register_count();
4382        assert!(max_reg > 0);
4383
4384        // Variable instructions: p2 is the target register.
4385        for op in prog.ops() {
4386            if op.opcode == Opcode::Variable {
4387                assert!(
4388                    op.p2 >= 1 && op.p2 <= max_reg,
4389                    "Variable register out of range: p2 = {}, max = {}",
4390                    op.p2,
4391                    max_reg
4392                );
4393            }
4394        }
4395    }
4396
4397    // === Test 7: Concurrent mode NewRowid ===
4398    #[test]
4399    fn test_codegen_concurrent_newrowid() {
4400        let stmt = InsertStatement {
4401            with: None,
4402            or_conflict: None,
4403            table: QualifiedName::bare("t"),
4404            alias: None,
4405            columns: vec![],
4406            source: InsertSource::Values(vec![vec![placeholder(1)]]),
4407            upsert: vec![],
4408            returning: vec![],
4409        };
4410        let schema = test_schema();
4411        let ctx = CodegenContext {
4412            concurrent_mode: true,
4413        };
4414        let mut b = ProgramBuilder::new();
4415        codegen_insert(&mut b, &stmt, &schema, &ctx).unwrap();
4416        let prog = b.finish().unwrap();
4417
4418        // In concurrent mode, NewRowid p3 should be non-zero.
4419        let nr = prog
4420            .ops()
4421            .iter()
4422            .find(|op| op.opcode == Opcode::NewRowid)
4423            .unwrap();
4424        assert_ne!(
4425            nr.p3, 0,
4426            "NewRowid p3 should be non-zero in concurrent mode"
4427        );
4428
4429        // In non-concurrent mode, p3 should be 0.
4430        let ctx_normal = CodegenContext::default();
4431        let mut b2 = ProgramBuilder::new();
4432        codegen_insert(&mut b2, &stmt, &schema, &ctx_normal).unwrap();
4433        let prog2 = b2.finish().unwrap();
4434        let nr2 = prog2
4435            .ops()
4436            .iter()
4437            .find(|op| op.opcode == Opcode::NewRowid)
4438            .unwrap();
4439        assert_eq!(nr2.p3, 0, "NewRowid p3 should be 0 in normal mode");
4440    }
4441
4442    // === Test 8: SELECT full scan ===
4443    #[test]
4444    fn test_codegen_select_full_scan() {
4445        let stmt = star_select("t");
4446        let schema = test_schema();
4447        let ctx = CodegenContext::default();
4448        let mut b = ProgramBuilder::new();
4449        codegen_select(&mut b, &stmt, &schema, &ctx).unwrap();
4450        let prog = b.finish().unwrap();
4451
4452        assert!(has_opcodes(
4453            &prog,
4454            &[
4455                Opcode::Init,
4456                Opcode::Transaction,
4457                Opcode::OpenRead,
4458                Opcode::Rewind,
4459                Opcode::Column,
4460                Opcode::Column,
4461                Opcode::ResultRow,
4462                Opcode::Next,
4463                Opcode::Close,
4464                Opcode::Halt,
4465            ]
4466        ));
4467
4468        // ResultRow should cover 2 columns (a and b).
4469        let rr = prog
4470            .ops()
4471            .iter()
4472            .find(|op| op.opcode == Opcode::ResultRow)
4473            .unwrap();
4474        assert_eq!(rr.p2, 2);
4475    }
4476
4477    // === Test 9: SELECT with index ===
4478    #[test]
4479    fn test_codegen_select_with_index() {
4480        let stmt = simple_select(&["a"], "t", Some(col_eq_param("b", 1)));
4481        let schema = test_schema_with_index();
4482        let ctx = CodegenContext::default();
4483        let mut b = ProgramBuilder::new();
4484        codegen_select(&mut b, &stmt, &schema, &ctx).unwrap();
4485        let prog = b.finish().unwrap();
4486
4487        // Should use OpenRead on both table and index.
4488        let open_reads = prog
4489            .ops()
4490            .iter()
4491            .filter(|op| op.opcode == Opcode::OpenRead)
4492            .count();
4493        assert_eq!(open_reads, 2, "should open both table and index");
4494
4495        // Should have SeekGE + IdxGT + IdxRowid + SeekRowid pattern.
4496        assert!(has_opcodes(
4497            &prog,
4498            &[
4499                Opcode::MakeRecord,
4500                Opcode::OpenRead,
4501                Opcode::OpenRead,
4502                Opcode::SeekGE,
4503                Opcode::IdxGT,
4504                Opcode::IdxRowid,
4505                Opcode::SeekRowid,
4506                Opcode::Column,
4507                Opcode::ResultRow,
4508            ]
4509        ));
4510
4511        let variable = prog
4512            .ops()
4513            .iter()
4514            .find(|op| op.opcode == Opcode::Variable)
4515            .expect("Variable should load index probe parameter");
4516        let make_record = prog
4517            .ops()
4518            .iter()
4519            .find(|op| op.opcode == Opcode::MakeRecord)
4520            .expect("MakeRecord should encode index probe key");
4521        assert_eq!(
4522            make_record.p1, variable.p2,
4523            "MakeRecord source should be Variable destination register"
4524        );
4525        assert_eq!(
4526            make_record.p2, 2,
4527            "probe key should include indexed value and synthetic low rowid"
4528        );
4529        let int64 = prog
4530            .ops()
4531            .iter()
4532            .find(|op| op.opcode == Opcode::Int64)
4533            .expect("Int64 should load i64::MIN for duplicate-range seek lower bound");
4534        assert_eq!(int64.p4, P4::Int64(i64::MIN));
4535        assert_eq!(
4536            make_record.p1 + 1,
4537            int64.p2,
4538            "MakeRecord should consume [param_reg, min_rowid_reg]"
4539        );
4540        let seek_ge = prog
4541            .ops()
4542            .iter()
4543            .find(|op| op.opcode == Opcode::SeekGE)
4544            .expect("SeekGE should be emitted for index probe");
4545        assert_eq!(
4546            seek_ge.p3, make_record.p3,
4547            "SeekGE must read probe key from MakeRecord destination register"
4548        );
4549        let idx_gt = prog
4550            .ops()
4551            .iter()
4552            .find(|op| op.opcode == Opcode::IdxGT)
4553            .expect("IdxGT should bound index equality duplicates");
4554        assert_eq!(
4555            idx_gt.p3, make_record.p3,
4556            "IdxGT must compare against the same probe key as SeekGE"
4557        );
4558        assert_eq!(
4559            idx_gt.p5, 1,
4560            "IdxGT should compare only the indexed value prefix, not the synthetic low rowid"
4561        );
4562
4563        let is_null_count = prog
4564            .ops()
4565            .iter()
4566            .filter(|op| op.opcode == Opcode::IsNull)
4567            .count();
4568        assert!(
4569            is_null_count >= 1,
4570            "indexed equality should guard NULL probe"
4571        );
4572
4573        let seek_rowid = prog
4574            .ops()
4575            .iter()
4576            .find(|op| op.opcode == Opcode::SeekRowid)
4577            .expect("SeekRowid should follow IdxRowid");
4578        assert_ne!(
4579            seek_rowid.p2, 0,
4580            "SeekRowid miss target must not jump to pc=0"
4581        );
4582        let next = prog
4583            .ops()
4584            .iter()
4585            .find(|op| op.opcode == Opcode::Next)
4586            .expect("index equality path must iterate duplicates");
4587        assert_eq!(next.p1, 1, "Next should advance the index cursor");
4588    }
4589
4590    #[test]
4591    fn test_codegen_select_unindexed_column_eq_uses_filtered_scan() {
4592        let stmt = simple_select(&["b"], "t", Some(col_eq_param("a", 2)));
4593        let schema = test_schema();
4594        let ctx = CodegenContext::default();
4595        let mut b = ProgramBuilder::new();
4596        codegen_select(&mut b, &stmt, &schema, &ctx).unwrap();
4597        let prog = b.finish().unwrap();
4598
4599        let open_reads = prog
4600            .ops()
4601            .iter()
4602            .filter(|op| op.opcode == Opcode::OpenRead)
4603            .count();
4604        assert_eq!(
4605            open_reads, 1,
4606            "unindexed equality should scan the table without opening an index"
4607        );
4608        assert!(
4609            !prog
4610                .ops()
4611                .iter()
4612                .any(|op| matches!(op.opcode, Opcode::SeekGE | Opcode::IdxGT | Opcode::IdxRowid)),
4613            "unindexed equality should not emit index-probe opcodes"
4614        );
4615        assert!(has_opcodes(
4616            &prog,
4617            &[
4618                Opcode::Init,
4619                Opcode::Transaction,
4620                Opcode::Variable,
4621                Opcode::OpenRead,
4622                Opcode::Rewind,
4623                Opcode::Column,
4624                Opcode::Ne,
4625                Opcode::Column,
4626                Opcode::ResultRow,
4627                Opcode::Next,
4628                Opcode::Close,
4629                Opcode::Halt,
4630            ]
4631        ));
4632
4633        let variable = prog
4634            .ops()
4635            .iter()
4636            .find(|op| op.opcode == Opcode::Variable)
4637            .expect("Variable should load the equality parameter");
4638        assert_eq!(variable.p1, 2, "numbered placeholder should be preserved");
4639        let ne = prog
4640            .ops()
4641            .iter()
4642            .find(|op| op.opcode == Opcode::Ne)
4643            .expect("filtered scan should skip rows that do not match");
4644        assert_eq!(ne.p1, variable.p2);
4645        assert_ne!(
4646            ne.p5 & 0x10,
4647            0,
4648            "WHERE equality must skip NULL comparisons instead of returning them"
4649        );
4650
4651        let next = prog
4652            .ops()
4653            .iter()
4654            .find(|op| op.opcode == Opcode::Next)
4655            .expect("filtered scan should advance the table cursor");
4656        assert_eq!(next.p1, 0, "Next should advance the table cursor");
4657    }
4658
4659    // === Test 10: INSERT RETURNING ===
4660    #[test]
4661    fn test_codegen_insert_returning() {
4662        let stmt = InsertStatement {
4663            with: None,
4664            or_conflict: None,
4665            table: QualifiedName::bare("t"),
4666            alias: None,
4667            columns: vec![],
4668            source: InsertSource::Values(vec![vec![placeholder(1)]]),
4669            upsert: vec![],
4670            returning: vec![ResultColumn::Expr {
4671                expr: Expr::Column(ColumnRef::bare("rowid"), Span::ZERO),
4672                alias: None,
4673            }],
4674        };
4675        let schema = test_schema();
4676        let ctx = CodegenContext::default();
4677        let mut b = ProgramBuilder::new();
4678        codegen_insert(&mut b, &stmt, &schema, &ctx).unwrap();
4679        let prog = b.finish().unwrap();
4680
4681        // With RETURNING, there should be a ResultRow after Insert.
4682        assert!(has_opcodes(
4683            &prog,
4684            &[Opcode::Insert, Opcode::ResultRow, Opcode::Close,]
4685        ));
4686    }
4687
4688    // ===================================================================
4689    // CodegenError Display / Error trait tests
4690    // ===================================================================
4691
4692    #[test]
4693    fn test_codegen_error_display_table_not_found() {
4694        let err = CodegenError::TableNotFound("users".to_owned());
4695        let msg = err.to_string();
4696        assert!(msg.contains("table not found"), "got: {msg}");
4697        assert!(msg.contains("users"), "got: {msg}");
4698    }
4699
4700    #[test]
4701    fn test_codegen_error_display_column_not_found() {
4702        let err = CodegenError::ColumnNotFound {
4703            table: "users".to_owned(),
4704            column: "email".to_owned(),
4705        };
4706        let msg = err.to_string();
4707        assert!(msg.contains("email"), "got: {msg}");
4708        assert!(msg.contains("users"), "got: {msg}");
4709    }
4710
4711    #[test]
4712    fn test_codegen_error_display_unsupported() {
4713        let err = CodegenError::Unsupported("window functions".to_owned());
4714        let msg = err.to_string();
4715        assert!(msg.contains("unsupported"), "got: {msg}");
4716        assert!(msg.contains("window functions"), "got: {msg}");
4717    }
4718
4719    #[test]
4720    fn test_codegen_error_is_error() {
4721        let err = CodegenError::TableNotFound("t".to_owned());
4722        assert!(std::error::Error::source(&err).is_none());
4723    }
4724
4725    // ===================================================================
4726    // TableSchema method tests
4727    // ===================================================================
4728
4729    #[test]
4730    fn test_table_schema_affinity_string() {
4731        let schema = TableSchema {
4732            name: "t".to_owned(),
4733            root_page: 2,
4734            columns: vec![
4735                ColumnInfo {
4736                    name: "id".to_owned(),
4737                    affinity: 'd',
4738                    default_value: None,
4739                },
4740                ColumnInfo {
4741                    name: "name".to_owned(),
4742                    affinity: 'C',
4743                    default_value: None,
4744                },
4745                ColumnInfo {
4746                    name: "amount".to_owned(),
4747                    affinity: 'e',
4748                    default_value: None,
4749                },
4750            ],
4751            indexes: vec![],
4752        };
4753        assert_eq!(schema.affinity_string(), "dCe");
4754    }
4755
4756    #[test]
4757    fn test_table_schema_column_index() {
4758        let schema = test_schema();
4759        // Case-insensitive lookup.
4760        assert_eq!(schema[0].column_index("a"), Some(0));
4761        assert_eq!(schema[0].column_index("A"), Some(0));
4762        assert_eq!(schema[0].column_index("b"), Some(1));
4763        assert_eq!(schema[0].column_index("z"), None);
4764    }
4765
4766    #[test]
4767    fn test_table_schema_index_for_column() {
4768        let schema = test_schema_with_index();
4769        let table = &schema[0];
4770        // Should find idx_t_b (leftmost column is "b").
4771        let found = table.index_for_column("b");
4772        assert!(found.is_some());
4773        assert_eq!(found.unwrap().name, "idx_t_b");
4774
4775        // Case-insensitive.
4776        let found = table.index_for_column("B");
4777        assert!(found.is_some());
4778
4779        // No index on column "a".
4780        assert!(table.index_for_column("a").is_none());
4781    }
4782
4783    #[test]
4784    fn test_table_schema_affinity_string_empty() {
4785        let schema = TableSchema {
4786            name: "empty".to_owned(),
4787            root_page: 2,
4788            columns: vec![],
4789            indexes: vec![],
4790        };
4791        assert_eq!(schema.affinity_string(), "");
4792    }
4793
4794    // ===================================================================
4795    // CodegenContext tests
4796    // ===================================================================
4797
4798    #[test]
4799    fn test_codegen_context_default() {
4800        let ctx = CodegenContext::default();
4801        assert!(!ctx.concurrent_mode);
4802    }
4803
4804    // ===================================================================
4805    // Codegen error path tests
4806    // ===================================================================
4807
4808    #[test]
4809    fn test_codegen_select_table_not_found() {
4810        let stmt = star_select("nonexistent");
4811        let schema = test_schema();
4812        let ctx = CodegenContext::default();
4813        let mut b = ProgramBuilder::new();
4814        let err = codegen_select(&mut b, &stmt, &schema, &ctx).expect_err("should fail");
4815        assert!(matches!(err, CodegenError::TableNotFound(_)));
4816    }
4817
4818    #[test]
4819    fn test_codegen_insert_table_not_found() {
4820        let stmt = InsertStatement {
4821            with: None,
4822            or_conflict: None,
4823            table: QualifiedName::bare("nonexistent"),
4824            alias: None,
4825            columns: vec![],
4826            source: InsertSource::Values(vec![vec![placeholder(1)]]),
4827            upsert: vec![],
4828            returning: vec![],
4829        };
4830        let schema = test_schema();
4831        let ctx = CodegenContext::default();
4832        let mut b = ProgramBuilder::new();
4833        let err = codegen_insert(&mut b, &stmt, &schema, &ctx).expect_err("should fail");
4834        assert!(matches!(err, CodegenError::TableNotFound(_)));
4835    }
4836
4837    #[test]
4838    fn test_codegen_update_table_not_found() {
4839        let stmt = UpdateStatement {
4840            with: None,
4841            or_conflict: None,
4842            table: QualifiedTableRef {
4843                name: QualifiedName::bare("nonexistent"),
4844                alias: None,
4845                index_hint: None,
4846                time_travel: None,
4847            },
4848            assignments: vec![],
4849            from: None,
4850            where_clause: None,
4851            returning: vec![],
4852            order_by: vec![],
4853            limit: None,
4854        };
4855        let schema = test_schema();
4856        let ctx = CodegenContext::default();
4857        let mut b = ProgramBuilder::new();
4858        let err = codegen_update(&mut b, &stmt, &schema, &ctx).expect_err("should fail");
4859        assert!(matches!(err, CodegenError::TableNotFound(_)));
4860    }
4861
4862    #[test]
4863    fn test_codegen_update_unknown_assignment_column_returns_error() {
4864        let stmt = UpdateStatement {
4865            with: None,
4866            or_conflict: None,
4867            table: QualifiedTableRef {
4868                name: QualifiedName::bare("t"),
4869                alias: None,
4870                index_hint: None,
4871                time_travel: None,
4872            },
4873            assignments: vec![Assignment {
4874                target: AssignmentTarget::Column("no_such_col".to_owned()),
4875                value: placeholder(1),
4876            }],
4877            from: None,
4878            where_clause: Some(*rowid_eq_param()),
4879            returning: vec![],
4880            order_by: vec![],
4881            limit: None,
4882        };
4883        let schema = test_schema();
4884        let ctx = CodegenContext::default();
4885        let mut b = ProgramBuilder::new();
4886        let err = codegen_update(&mut b, &stmt, &schema, &ctx).expect_err("should fail");
4887        assert!(matches!(
4888            err,
4889            CodegenError::ColumnNotFound { ref column, .. } if column == "no_such_col"
4890        ));
4891    }
4892
4893    #[test]
4894    fn test_codegen_update_requires_rowid_predicate() {
4895        let stmt = UpdateStatement {
4896            with: None,
4897            or_conflict: None,
4898            table: QualifiedTableRef {
4899                name: QualifiedName::bare("t"),
4900                alias: None,
4901                index_hint: None,
4902                time_travel: None,
4903            },
4904            assignments: vec![Assignment {
4905                target: AssignmentTarget::Column("b".to_owned()),
4906                value: placeholder(1),
4907            }],
4908            from: None,
4909            where_clause: None,
4910            returning: vec![],
4911            order_by: vec![],
4912            limit: None,
4913        };
4914        let schema = test_schema();
4915        let ctx = CodegenContext::default();
4916        let mut b = ProgramBuilder::new();
4917        let err = codegen_update(&mut b, &stmt, &schema, &ctx).expect_err("should fail");
4918        assert!(matches!(err, CodegenError::Unsupported(_)));
4919    }
4920
4921    #[test]
4922    fn test_codegen_update_rowid_anonymous_bind_is_offset_after_assignments() {
4923        let stmt = UpdateStatement {
4924            with: None,
4925            or_conflict: None,
4926            table: QualifiedTableRef {
4927                name: QualifiedName::bare("t"),
4928                alias: None,
4929                index_hint: None,
4930                time_travel: None,
4931            },
4932            assignments: vec![Assignment {
4933                target: AssignmentTarget::Column("b".to_owned()),
4934                value: placeholder(1),
4935            }],
4936            from: None,
4937            where_clause: Some(Expr::BinaryOp {
4938                left: Box::new(Expr::Column(ColumnRef::bare("rowid"), Span::ZERO)),
4939                op: AstBinaryOp::Eq,
4940                right: Box::new(Expr::Placeholder(PlaceholderType::Anonymous, Span::ZERO)),
4941                span: Span::ZERO,
4942            }),
4943            returning: vec![],
4944            order_by: vec![],
4945            limit: None,
4946        };
4947        let schema = test_schema();
4948        let ctx = CodegenContext::default();
4949        let mut b = ProgramBuilder::new();
4950        codegen_update(&mut b, &stmt, &schema, &ctx).unwrap();
4951        let prog = b.finish().unwrap();
4952        let vars: Vec<_> = prog
4953            .ops()
4954            .iter()
4955            .filter(|op| op.opcode == Opcode::Variable)
4956            .collect();
4957        assert_eq!(vars.len(), 2);
4958        assert_eq!(vars[0].p1, 1, "first bind should be SET assignment");
4959        assert_eq!(vars[1].p1, 2, "rowid bind should follow SET binds");
4960    }
4961
4962    #[test]
4963    fn test_codegen_select_unindexed_filter_projected_column_uses_filtered_scan() {
4964        let stmt = simple_select(&["a"], "t", Some(col_eq_param("a", 1)));
4965        let schema = test_schema();
4966        let ctx = CodegenContext::default();
4967        let mut b = ProgramBuilder::new();
4968        codegen_select(&mut b, &stmt, &schema, &ctx).unwrap();
4969        let prog = b.finish().unwrap();
4970
4971        assert!(has_opcodes(
4972            &prog,
4973            &[
4974                Opcode::Variable,
4975                Opcode::OpenRead,
4976                Opcode::Rewind,
4977                Opcode::Column,
4978                Opcode::Ne,
4979                Opcode::Column,
4980                Opcode::ResultRow,
4981                Opcode::Next,
4982            ]
4983        ));
4984        let column_reads: Vec<_> = prog
4985            .ops()
4986            .iter()
4987            .filter(|op| op.opcode == Opcode::Column)
4988            .collect();
4989        assert_eq!(
4990            column_reads.len(),
4991            2,
4992            "filtering and projecting the same unindexed column should read it for both the predicate and output"
4993        );
4994        assert!(
4995            column_reads.iter().all(|op| op.p2 == 0),
4996            "both reads should target column a"
4997        );
4998        let ne = prog
4999            .ops()
5000            .iter()
5001            .find(|op| op.opcode == Opcode::Ne)
5002            .expect("filtered scan should skip non-matching rows");
5003        assert_ne!(
5004            ne.p5 & 0x10,
5005            0,
5006            "WHERE equality must skip NULL comparisons instead of returning them"
5007        );
5008    }
5009
5010    #[test]
5011    fn test_codegen_select_unsupported_projection_expression_is_error() {
5012        let stmt = SelectStatement {
5013            with: None,
5014            body: SelectBody {
5015                select: SelectCore::Select {
5016                    distinct: Distinctness::All,
5017                    columns: vec![ResultColumn::Expr {
5018                        expr: Expr::Between {
5019                            expr: Box::new(Expr::Literal(Literal::Integer(5), Span::ZERO)),
5020                            low: Box::new(Expr::Literal(Literal::Integer(1), Span::ZERO)),
5021                            high: Box::new(Expr::Literal(Literal::Integer(10), Span::ZERO)),
5022                            not: false,
5023                            span: Span::ZERO,
5024                        },
5025                        alias: None,
5026                    }],
5027                    from: Some(FromClause {
5028                        source: TableOrSubquery::Table {
5029                            name: QualifiedName::bare("t"),
5030                            alias: None,
5031                            index_hint: None,
5032                            time_travel: None,
5033                        },
5034                        joins: vec![],
5035                    }),
5036                    where_clause: None,
5037                    group_by: vec![],
5038                    having: None,
5039                    windows: vec![],
5040                },
5041                compounds: vec![],
5042            },
5043            order_by: vec![],
5044            limit: None,
5045        };
5046        let schema = test_schema();
5047        let ctx = CodegenContext::default();
5048        let mut b = ProgramBuilder::new();
5049        let err = codegen_select(&mut b, &stmt, &schema, &ctx).expect_err("should fail");
5050        assert!(matches!(err, CodegenError::Unsupported(_)));
5051    }
5052
5053    #[test]
5054    fn test_codegen_delete_table_not_found() {
5055        let stmt = DeleteStatement {
5056            with: None,
5057            table: QualifiedTableRef {
5058                name: QualifiedName::bare("nonexistent"),
5059                alias: None,
5060                index_hint: None,
5061                time_travel: None,
5062            },
5063            where_clause: None,
5064            returning: vec![],
5065            order_by: vec![],
5066            limit: None,
5067        };
5068        let schema = test_schema();
5069        let ctx = CodegenContext::default();
5070        let mut b = ProgramBuilder::new();
5071        let err = codegen_delete(&mut b, &stmt, &schema, &ctx).expect_err("should fail");
5072        assert!(matches!(err, CodegenError::TableNotFound(_)));
5073    }
5074
5075    // ===================================================================
5076    // Rowid pseudo-column projection tests (bd-3r24)
5077    // ===================================================================
5078
5079    #[test]
5080    fn test_codegen_select_rowid_projection() {
5081        let stmt = simple_select(&["rowid"], "t", None);
5082        let schema = test_schema();
5083        let ctx = CodegenContext::default();
5084        let mut b = ProgramBuilder::new();
5085        codegen_select(&mut b, &stmt, &schema, &ctx).unwrap();
5086        let prog = b.finish().unwrap();
5087
5088        // Should contain OP_Rowid (not OP_Column) for the rowid reference.
5089        assert!(
5090            has_opcodes(&prog, &[Opcode::Rowid, Opcode::ResultRow]),
5091            "SELECT rowid should emit OP_Rowid"
5092        );
5093    }
5094
5095    #[test]
5096    fn test_codegen_select_rowid_alias_underscore() {
5097        let stmt = simple_select(&["_rowid_"], "t", None);
5098        let schema = test_schema();
5099        let ctx = CodegenContext::default();
5100        let mut b = ProgramBuilder::new();
5101        codegen_select(&mut b, &stmt, &schema, &ctx).unwrap();
5102        let prog = b.finish().unwrap();
5103
5104        assert!(
5105            has_opcodes(&prog, &[Opcode::Rowid, Opcode::ResultRow]),
5106            "SELECT _rowid_ should emit OP_Rowid"
5107        );
5108    }
5109
5110    #[test]
5111    fn test_codegen_select_oid_alias() {
5112        let stmt = simple_select(&["oid"], "t", None);
5113        let schema = test_schema();
5114        let ctx = CodegenContext::default();
5115        let mut b = ProgramBuilder::new();
5116        codegen_select(&mut b, &stmt, &schema, &ctx).unwrap();
5117        let prog = b.finish().unwrap();
5118
5119        assert!(
5120            has_opcodes(&prog, &[Opcode::Rowid, Opcode::ResultRow]),
5121            "SELECT oid should emit OP_Rowid"
5122        );
5123    }
5124
5125    #[test]
5126    fn test_codegen_select_rowid_with_columns() {
5127        // SELECT rowid, a, b FROM t — mixed pseudo-column and real columns.
5128        let stmt = simple_select(&["rowid", "a", "b"], "t", None);
5129        let schema = test_schema();
5130        let ctx = CodegenContext::default();
5131        let mut b = ProgramBuilder::new();
5132        codegen_select(&mut b, &stmt, &schema, &ctx).unwrap();
5133        let prog = b.finish().unwrap();
5134
5135        // Should contain OP_Rowid followed by two OP_Column opcodes.
5136        assert!(
5137            has_opcodes(
5138                &prog,
5139                &[
5140                    Opcode::Rowid,
5141                    Opcode::Column,
5142                    Opcode::Column,
5143                    Opcode::ResultRow
5144                ]
5145            ),
5146            "SELECT rowid, a, b should emit Rowid + Column + Column"
5147        );
5148    }
5149
5150    #[test]
5151    fn test_codegen_select_rowid_case_insensitive() {
5152        // Uppercase ROWID should also be recognized.
5153        let stmt = simple_select(&["ROWID"], "t", None);
5154        let schema = test_schema();
5155        let ctx = CodegenContext::default();
5156        let mut b = ProgramBuilder::new();
5157        codegen_select(&mut b, &stmt, &schema, &ctx).unwrap();
5158        let prog = b.finish().unwrap();
5159
5160        assert!(
5161            has_opcodes(&prog, &[Opcode::Rowid, Opcode::ResultRow]),
5162            "SELECT ROWID should emit OP_Rowid (case-insensitive)"
5163        );
5164    }
5165}