Skip to main content

clt_database/
util.rs

1use crate::alloc::TursoIteratorExt;
2use crate::numeric::StrToF64;
3use crate::schema::ColDef;
4use crate::translate::emitter::TransactionMode;
5use crate::translate::expr::{walk_expr, walk_expr_mut, WalkControl};
6use crate::translate::plan::{BitSet, JoinedTable, TableReferences};
7use crate::translate::planner::{parse_row_id, TableMask};
8use crate::types::IOResult;
9use crate::IO;
10use crate::{
11    schema::{Column, Schema, Table, Type},
12    types::{Value, ValueType},
13    LimboError, OpenFlags, Result, Statement, SymbolTable,
14};
15use either::Either;
16use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
17use std::future::Future;
18use tracing::{instrument, Level};
19use turso_macros::match_ignore_ascii_case;
20use turso_parser::ast::{self, CreateTableBody, Expr, Literal, UnaryOperator};
21use turso_parser::parser::Parser;
22
23#[macro_export]
24macro_rules! io_yield_one {
25    ($c:expr) => {
26        return Ok(IOResult::IO(IOCompletions::Single($c)));
27    };
28}
29
30#[macro_export]
31macro_rules! eq_ignore_ascii_case {
32    ( $var:expr, $value:literal ) => {{
33        match_ignore_ascii_case!(match $var {
34            $value => true,
35            _ => false,
36        })
37    }};
38}
39
40#[macro_export]
41macro_rules! contains_ignore_ascii_case {
42    ( $var:expr, $value:literal ) => {{
43        let compare_to_idx = $var.len().saturating_sub($value.len());
44        if $var.len() < $value.len() {
45            false
46        } else {
47            let mut result = false;
48            for i in 0..=compare_to_idx {
49                if eq_ignore_ascii_case!(&$var[i..i + $value.len()], $value) {
50                    result = true;
51                    break;
52                }
53            }
54
55            result
56        }
57    }};
58}
59
60#[macro_export]
61macro_rules! starts_with_ignore_ascii_case {
62    ( $var:expr, $value:literal ) => {{
63        if $var.len() < $value.len() {
64            false
65        } else {
66            eq_ignore_ascii_case!(&$var[..$value.len()], $value)
67        }
68    }};
69}
70
71#[macro_export]
72macro_rules! ends_with_ignore_ascii_case {
73    ( $var:expr, $value:literal ) => {{
74        if $var.len() < $value.len() {
75            false
76        } else {
77            eq_ignore_ascii_case!(&$var[$var.len() - $value.len()..], $value)
78        }
79    }};
80}
81
82pub trait IOExt {
83    fn block<T>(&self, f: impl FnMut() -> Result<IOResult<T>>) -> Result<T>;
84    fn wait<T, F>(&self, f: F) -> impl Future<Output = Result<T>> + Send
85    where
86        F: FnMut() -> Result<IOResult<T>> + Send,
87        T: Send;
88}
89
90impl<I: ?Sized + IO> IOExt for I {
91    fn block<T>(&self, mut f: impl FnMut() -> Result<IOResult<T>>) -> Result<T> {
92        Ok(loop {
93            match f()? {
94                IOResult::Done(v) => break v,
95                IOResult::IO(io) => io.wait(self)?,
96            }
97        })
98    }
99
100    async fn wait<T, F>(&self, mut f: F) -> Result<T>
101    where
102        F: FnMut() -> Result<IOResult<T>> + Send,
103        T: Send,
104    {
105        Ok(loop {
106            match f()? {
107                IOResult::Done(v) => break v,
108                IOResult::IO(io) => io.wait_async(self).await?,
109            }
110        })
111    }
112}
113
114// https://sqlite.org/lang_keywords.html
115const QUOTE_PAIRS: &[(char, char)] = &[
116    ('"', '"'),
117    ('[', ']'),
118    ('`', '`'),
119    ('\'', '\''), // string sometimes used as identifier quoting
120];
121
122pub fn normalize_ident(identifier: &str) -> String {
123    // quotes normalization already happened in the parser layer (see Name ast node implementation)
124    // so, we only need to apply SQLite's ASCII-only identifier case folding.
125    identifier.to_ascii_lowercase()
126}
127
128/// Escape a SQL string literal payload for safe interpolation inside single quotes.
129pub fn escape_sql_string_literal(literal: &str) -> String {
130    literal.replace('\'', "''")
131}
132
133/// Quote a SQL identifier with double quotes when necessary.
134/// Always safe to call — returns the bare name when no quoting is needed.
135pub fn quote_identifier(name: &str) -> String {
136    let needs_quoting = name.is_empty()
137        || name.as_bytes()[0].is_ascii_digit()
138        || !name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
139        || turso_parser::lexer::is_quotable_keyword(name.as_bytes());
140    if needs_quoting {
141        let escaped = name.replace('"', "\"\"");
142        format!("\"{escaped}\"")
143    } else {
144        name.to_string()
145    }
146}
147
148pub const PRIMARY_KEY_AUTOMATIC_INDEX_NAME_PREFIX: &str = "sqlite_autoindex_";
149
150/// Unparsed index that comes from a sql query, i.e not an automatic index
151///
152/// CREATE INDEX idx ON table_name(sql)
153pub struct UnparsedFromSqlIndex {
154    pub table_name: String,
155    pub root_page: i64,
156    pub sql: String,
157}
158
159/// Carries the in-progress state of [`parse_schema_rows`] across IO yields:
160/// the schema-scan statement plus the accumulators that `handle_schema_row`
161/// fills row-by-row. Without this, a yield mid-scan would lose the partially
162/// accumulated indexes/materialized-view info and re-run the statement from
163/// scratch.
164#[derive(Default)]
165pub struct ParseSchemaRowsState {
166    inner: Option<ParseSchemaRowsInner>,
167}
168
169struct ParseSchemaRowsInner {
170    rows: Statement,
171    from_sql_indexes: crate::alloc::Vec<UnparsedFromSqlIndex>,
172    automatic_indices: HashMap<String, crate::alloc::Vec<(String, i64)>>,
173    dbsp_state_roots: HashMap<String, i64>,
174    dbsp_state_index_roots: HashMap<String, i64>,
175    materialized_view_info: HashMap<String, (String, i64)>,
176}
177
178impl ParseSchemaRowsState {
179    /// Initialize the scan state from a prepared `SELECT * FROM sqlite_schema`
180    /// statement (or equivalent) and the MVCC transaction it should read under.
181    pub fn new(mut rows: Statement, mv_tx: Option<(u64, TransactionMode)>) -> Self {
182        rows.set_mv_tx(mv_tx);
183        Self {
184            inner: Some(ParseSchemaRowsInner {
185                rows,
186                from_sql_indexes: <crate::alloc::Vec<_> as crate::alloc::TursoTryWithCapacityExt>::try_with_capacity_ext(10).expect(crate::alloc::ALLOC_ERR_MSG),
187                automatic_indices: HashMap::with_capacity_and_hasher(10, Default::default()),
188                dbsp_state_roots: HashMap::default(),
189                dbsp_state_index_roots: HashMap::default(),
190                materialized_view_info: HashMap::default(),
191            }),
192        }
193    }
194}
195
196/// Non-blocking schema-row parser: steps the schema-scan statement held in
197/// `state`, feeding each row to `handle_schema_row`, and yields IO instead of
198/// pumping it. Re-invoke after each yielded completion; accumulators persist in
199/// `state`. On completion, populates indices and materialized views.
200#[instrument(skip_all, level = Level::DEBUG)]
201pub fn parse_schema_rows(
202    state: &mut ParseSchemaRowsState,
203    schema: &mut Schema,
204    syms: &SymbolTable,
205    resolve_attached_db: &dyn Fn(&str) -> Option<usize>,
206) -> Result<IOResult<()>> {
207    {
208        let inner = state
209            .inner
210            .as_mut()
211            .expect("ParseSchemaRowsState not initialized");
212        // Destructure so the statement (receiver) and the accumulators (captured
213        // by the closure) are borrowed as disjoint fields.
214        let ParseSchemaRowsInner {
215            rows,
216            from_sql_indexes,
217            automatic_indices,
218            dbsp_state_roots,
219            dbsp_state_index_roots,
220            materialized_view_info,
221        } = inner;
222        crate::return_if_io!(rows.run_with_row_callback_nonblock(|row| {
223            let ty = row.get::<&str>(0)?;
224            let name = row.get::<&str>(1)?;
225            let table_name = row.get::<&str>(2)?;
226            let root_page = row.get::<i64>(3)?;
227            let sql = row.get::<&str>(4).ok();
228            schema.handle_schema_row(
229                ty,
230                name,
231                table_name,
232                root_page,
233                sql,
234                syms,
235                from_sql_indexes,
236                automatic_indices,
237                dbsp_state_roots,
238                dbsp_state_index_roots,
239                materialized_view_info,
240                resolve_attached_db,
241            )
242        }));
243    }
244
245    // Scan complete: finalize. Take ownership of the accumulators.
246    let inner = state
247        .inner
248        .take()
249        .expect("ParseSchemaRowsState not initialized");
250    let has_mv_store = inner.rows.mv_store().is_some();
251    schema.populate_indices(
252        syms,
253        inner.from_sql_indexes,
254        inner.automatic_indices,
255        has_mv_store,
256    )?;
257    schema.populate_materialized_views(
258        inner.materialized_view_info,
259        inner.dbsp_state_roots,
260        inner.dbsp_state_index_roots,
261    )?;
262
263    Ok(IOResult::Done(()))
264}
265
266fn cmp_numeric_strings(num_str: &str, other: &str) -> bool {
267    fn parse(s: &str) -> Option<Either<i64, f64>> {
268        if let Ok(i) = s.parse::<i64>() {
269            Some(Either::Left(i))
270        } else if let Ok(f) = s.parse::<f64>() {
271            Some(Either::Right(f))
272        } else {
273            None
274        }
275    }
276
277    match (parse(num_str), parse(other)) {
278        (Some(Either::Left(i1)), Some(Either::Left(i2))) => i1 == i2,
279        (Some(Either::Right(f1)), Some(Either::Right(f2))) => f1 == f2,
280        // Integer and Float are NOT equivalent even if values match,
281        // because result type of operations depends on operand types
282        (Some(Either::Left(_)), Some(Either::Right(_)))
283        | (Some(Either::Right(_)), Some(Either::Left(_))) => false,
284        _ => num_str == other,
285    }
286}
287
288pub fn check_ident_equivalency(ident1: &str, ident2: &str) -> bool {
289    fn strip_quotes(identifier: &str) -> &str {
290        for &(start, end) in QUOTE_PAIRS {
291            if identifier.starts_with(start) && identifier.ends_with(end) {
292                return &identifier[1..identifier.len() - 1];
293            }
294        }
295        identifier
296    }
297    strip_quotes(ident1).eq_ignore_ascii_case(strip_quotes(ident2))
298}
299
300/// Returns true if `sql` parses as a `CREATE VIRTUAL TABLE` statement.
301///
302/// Like SQLite (whose `sqlite3InitCallback` feeds schema SQL to the real
303/// parser, so a row is a virtual table purely as a byproduct of the
304/// `create_vtab` grammar rule), classification is done by parsing rather than
305/// by substring or token matching, which would misclassify regular tables
306/// whose SQL merely contains the text (e.g. in a DEFAULT literal).
307pub fn sql_is_create_virtual_table(sql: &str) -> bool {
308    matches!(
309        Parser::new(sql.as_bytes()).next_cmd(),
310        Ok(Some(ast::Cmd::Stmt(ast::Stmt::CreateVirtualTable(_))))
311    )
312}
313
314pub fn module_name_from_sql(sql: &str) -> Result<&str> {
315    if let Some(start) = sql.find("USING") {
316        let start = start + 6;
317        // stop at the first space, semicolon, or parenthesis
318        let end = sql[start..]
319            .find(|c: char| c.is_whitespace() || c == ';' || c == '(')
320            .unwrap_or(sql.len() - start)
321            + start;
322        Ok(sql[start..end].trim())
323    } else {
324        Err(LimboError::InvalidArgument(
325            "Expected 'USING' in module name".to_string(),
326        ))
327    }
328}
329
330// CREATE VIRTUAL TABLE table_name USING module_name(arg1, arg2, ...);
331// CREATE VIRTUAL TABLE table_name USING module_name;
332pub fn module_args_from_sql(sql: &str) -> Result<Vec<turso_ext::Value>> {
333    if !sql.contains('(') {
334        return Ok(vec![]);
335    }
336    let start = sql.find('(').ok_or_else(|| {
337        LimboError::InvalidArgument("Expected '(' in module argument list".to_string())
338    })? + 1;
339    let end = sql.rfind(')').ok_or_else(|| {
340        LimboError::InvalidArgument("Expected ')' in module argument list".to_string())
341    })?;
342
343    let mut args = Vec::new();
344    let mut current_arg = String::new();
345    let mut chars = sql[start..end].chars().peekable();
346    let mut in_quotes = false;
347
348    while let Some(c) = chars.next() {
349        match c {
350            '\'' => {
351                if in_quotes {
352                    if chars.peek() == Some(&'\'') {
353                        // Escaped quote
354                        current_arg.push('\'');
355                        chars.next();
356                    } else {
357                        in_quotes = false;
358                        args.push(turso_ext::Value::from_text(current_arg.trim().to_string()));
359                        current_arg.clear();
360                        // Skip until comma or end
361                        while let Some(&nc) = chars.peek() {
362                            if nc == ',' {
363                                chars.next(); // Consume comma
364                                break;
365                            } else if nc.is_whitespace() {
366                                chars.next();
367                            } else {
368                                return Err(LimboError::InvalidArgument(
369                                    "Unexpected characters after quoted argument".to_string(),
370                                ));
371                            }
372                        }
373                    }
374                } else {
375                    in_quotes = true;
376                }
377            }
378            ',' => {
379                if !in_quotes {
380                    if !current_arg.trim().is_empty() {
381                        args.push(turso_ext::Value::from_text(current_arg.trim().to_string()));
382                        current_arg.clear();
383                    }
384                } else {
385                    current_arg.push(c);
386                }
387            }
388            _ => {
389                current_arg.push(c);
390            }
391        }
392    }
393
394    if !current_arg.trim().is_empty() && !in_quotes {
395        args.push(turso_ext::Value::from_text(current_arg.trim().to_string()));
396    }
397
398    if in_quotes {
399        return Err(LimboError::InvalidArgument(
400            "Unterminated string literal in module arguments".to_string(),
401        ));
402    }
403
404    Ok(args)
405}
406
407pub fn check_literal_equivalency(lhs: &Literal, rhs: &Literal) -> bool {
408    match (lhs, rhs) {
409        (Literal::Numeric(n1), Literal::Numeric(n2)) => cmp_numeric_strings(n1, n2),
410        (Literal::String(s1), Literal::String(s2)) => s1 == s2,
411        (Literal::Blob(b1), Literal::Blob(b2)) => b1 == b2,
412        (Literal::Keyword(k1), Literal::Keyword(k2)) => check_ident_equivalency(k1, k2),
413        (Literal::Null, Literal::Null) => true,
414        (Literal::True, Literal::True) => true,
415        (Literal::False, Literal::False) => true,
416        (Literal::CurrentDate, Literal::CurrentDate) => true,
417        (Literal::CurrentTime, Literal::CurrentTime) => true,
418        (Literal::CurrentTimestamp, Literal::CurrentTimestamp) => true,
419        _ => false,
420    }
421}
422
423/// Returns true if every Column/RowId table reference in `expr` is contained
424/// in `allowed`. Constants (no table refs) pass.
425pub(crate) fn expr_tables_subset_of(
426    expr: &Expr,
427    table_references: &TableReferences,
428    allowed: &TableMask,
429) -> bool {
430    let mut ok = true;
431    let _ = walk_expr(expr, &mut |e: &Expr| -> Result<WalkControl> {
432        match e {
433            Expr::Column { table, .. } | Expr::RowId { table, .. } => {
434                if let Some(idx) = table_references
435                    .joined_tables()
436                    .iter()
437                    .position(|t| t.internal_id == *table)
438                {
439                    if !allowed.get(idx) {
440                        ok = false;
441                        return Ok(WalkControl::SkipChildren);
442                    }
443                }
444                // Outer query references are already in scope — allow them.
445            }
446            _ => {}
447        }
448        Ok(WalkControl::Continue)
449    });
450    ok
451}
452
453/// bind AST identifiers to either Column or Rowid if possible
454pub fn simple_bind_expr(
455    joined_table: &JoinedTable,
456    result_columns: &[ast::ResultColumn],
457    expr: &mut ast::Expr,
458) -> Result<()> {
459    let internal_id = joined_table.internal_id;
460    walk_expr_mut(expr, &mut |expr: &mut ast::Expr| -> Result<WalkControl> {
461        #[allow(clippy::single_match)]
462        match expr {
463            Expr::Id(id) => {
464                for result_column in result_columns.iter() {
465                    if let ast::ResultColumn::Expr(result, Some(ast::As::As(alias))) = result_column
466                    {
467                        if alias.as_str().eq_ignore_ascii_case(id.as_str()) {
468                            *expr = *result.clone();
469                            return Ok(WalkControl::Continue);
470                        }
471                    }
472                }
473                let col_idx = joined_table.columns().iter().position(|c| {
474                    c.name
475                        .as_ref()
476                        .is_some_and(|name| name.eq_ignore_ascii_case(id.as_str()))
477                });
478                if let Some(col_idx) = col_idx {
479                    let col = joined_table.table.columns().get(col_idx).unwrap();
480                    *expr = ast::Expr::Column {
481                        database: None,
482                        table: internal_id,
483                        column: col_idx,
484                        is_rowid_alias: col.is_rowid_alias(),
485                    };
486                } else {
487                    // only if we haven't found a match, check for explicit rowid reference
488                    let is_btree_table = matches!(joined_table.table, Table::BTree(_));
489                    if is_btree_table {
490                        if let Some(rowid) =
491                            parse_row_id(&normalize_ident(id.as_str()), internal_id, || false)?
492                        {
493                            *expr = rowid;
494                        }
495                    }
496                }
497            }
498            _ => {}
499        }
500        Ok(WalkControl::Continue)
501    })?;
502    Ok(())
503}
504
505pub fn try_substitute_parameters(
506    pattern: &Expr,
507    parameters: &HashMap<i32, Expr>,
508) -> Option<Box<Expr>> {
509    match pattern {
510        Expr::FunctionCall {
511            name,
512            distinctness,
513            args,
514            order_by,
515            within_group,
516            filter_over,
517        } => {
518            let mut substituted = Vec::new();
519            for arg in args {
520                substituted.push(try_substitute_parameters(arg, parameters)?);
521            }
522            Some(Box::new(Expr::FunctionCall {
523                args: substituted,
524                distinctness: *distinctness,
525                name: name.clone(),
526                order_by: order_by.clone(),
527                within_group: within_group.clone(),
528                filter_over: filter_over.clone(),
529            }))
530        }
531        Expr::Variable(var) => {
532            if var.name.is_some() {
533                return None;
534            }
535            let Ok(var) = i32::try_from(var.index.get()) else {
536                return None;
537            };
538            Some(Box::new(parameters.get(&var)?.clone()))
539        }
540        _ => Some(Box::new(pattern.clone())),
541    }
542}
543
544pub fn try_capture_parameters(pattern: &Expr, query: &Expr) -> Option<HashMap<i32, Expr>> {
545    let mut captured = HashMap::default();
546    match (pattern, query) {
547        (
548            Expr::FunctionCall {
549                name: name1,
550                distinctness: distinct1,
551                args: args1,
552                order_by: order1,
553                within_group: within1,
554                filter_over: filter1,
555            },
556            Expr::FunctionCall {
557                name: name2,
558                distinctness: distinct2,
559                args: args2,
560                order_by: order2,
561                within_group: within2,
562                filter_over: filter2,
563            },
564        ) => {
565            if !name1.as_str().eq_ignore_ascii_case(name2.as_str()) {
566                return None;
567            }
568            if distinct1.is_some() || distinct2.is_some() {
569                return None;
570            }
571            if !order1.is_empty() || !order2.is_empty() {
572                return None;
573            }
574            if !within1.is_empty() || !within2.is_empty() {
575                return None;
576            }
577            if filter1.filter_clause.is_some() || filter1.over_clause.is_some() {
578                return None;
579            }
580            if filter2.filter_clause.is_some() || filter2.over_clause.is_some() {
581                return None;
582            }
583            for (arg1, arg2) in args1.iter().zip(args2.iter()) {
584                let result = try_capture_parameters(arg1, arg2)?;
585                captured.extend(result);
586            }
587            Some(captured)
588        }
589        (Expr::Variable(var), expr) => {
590            if var.name.is_some() {
591                return None;
592            }
593            let Ok(var) = i32::try_from(var.index.get()) else {
594                return None;
595            };
596            captured.insert(var, expr.clone());
597            Some(captured)
598        }
599        (
600            Expr::Id(_) | Expr::Name(_) | Expr::Column { .. },
601            Expr::Id(_) | Expr::Name(_) | Expr::Column { .. },
602        ) => {
603            if pattern == query {
604                Some(captured)
605            } else {
606                None
607            }
608        }
609        (_, _) => None,
610    }
611}
612
613/// Returns the number of column arguments for FTS functions.
614/// FTS functions have column arguments followed by non-column arguments:
615/// - fts_match(col1, col2, ..., query_string) -> columns = args.len() - 1
616/// - fts_score(col1, col2, ..., query_string) -> columns = args.len() - 1
617/// - fts_highlight(col1, col2, ..., before_tag, after_tag, query_string) -> columns = args.len() - 3
618///
619/// Returns 0 for non-FTS functions.
620/// Specific for FTS but cannot gate behind clt_turso_feature = "fts" so it must
621/// live in util.rs :/
622pub fn count_fts_column_args(expr: &Expr) -> usize {
623    match expr {
624        Expr::FunctionCall { name, args, .. } => {
625            let name_lower = name.as_str().to_lowercase();
626            match name_lower.as_str() {
627                "fts_match" | "fts_score" => args.len().saturating_sub(1),
628                "fts_highlight" => args.len().saturating_sub(3),
629                _ => 0,
630            }
631        }
632        _ => 0,
633    }
634}
635
636/// Match FTS function calls where column arguments can appear in any order.
637///
638/// FTS functions like `fts_match(col1, col2, 'query')` should match
639/// `fts_match(col2, col1, 'query')` as long as the same columns are used.
640///
641/// Semi-specific for FTS but cannot gate behind clt_turso_feature = "fts" so it must
642/// live in util.rs :/
643pub fn try_capture_parameters_column_agnostic(
644    pattern: &Expr,         // pattern expression from index definition
645    query: &Expr,           // the actual query expression
646    num_column_args: usize, // number of leading column arguments
647) -> Option<HashMap<i32, Expr>> {
648    // If not a function call or no column args, fall back to standard matching
649    if num_column_args == 0 {
650        return try_capture_parameters(pattern, query);
651    }
652
653    let (
654        Expr::FunctionCall {
655            name: pattern_name,
656            distinctness: pattern_distinct,
657            args: pattern_args,
658            order_by: pattern_order,
659            within_group: pattern_within,
660            filter_over: pattern_filter,
661        },
662        Expr::FunctionCall {
663            name: query_name,
664            distinctness: query_distinct,
665            args: query_args,
666            order_by: query_order,
667            within_group: query_within,
668            filter_over: query_filter,
669        },
670    ) = (pattern, query)
671    else {
672        return try_capture_parameters(pattern, query);
673    };
674    // Function names must match
675    if !pattern_name
676        .as_str()
677        .eq_ignore_ascii_case(query_name.as_str())
678    {
679        return None;
680    }
681
682    // Argument counts must match
683    if pattern_args.len() != query_args.len() {
684        return None;
685    }
686    // Distinctness must match (we don't support it)
687    if pattern_distinct.is_some() || query_distinct.is_some() {
688        return None;
689    }
690    // ORDER BY within function not supported
691    if !pattern_order.is_empty() || !query_order.is_empty() {
692        return None;
693    }
694    // WITHIN GROUP not supported
695    if !pattern_within.is_empty() || !query_within.is_empty() {
696        return None;
697    }
698
699    // Filter/over clause not supported
700    if pattern_filter.filter_clause.is_some() || pattern_filter.over_clause.is_some() {
701        return None;
702    }
703    if query_filter.filter_clause.is_some() || query_filter.over_clause.is_some() {
704        return None;
705    }
706
707    let mut captured = HashMap::default();
708
709    // Split args into column args (reorderable) and remaining args (positional)
710    let pattern_col_args = &pattern_args[..num_column_args];
711    let query_col_args = &query_args[..num_column_args];
712    let pattern_rest = &pattern_args[num_column_args..];
713    let query_rest = &query_args[num_column_args..];
714
715    // For column arguments: check that the same set of columns is used (order-independent)
716    // We use a greedy matching approach: for each query column, find a matching pattern column
717    let mut matched_pattern_indices = BitSet::default();
718
719    for query_col in query_col_args {
720        let mut found_match = false;
721        for (i, pattern_col) in pattern_col_args.iter().enumerate() {
722            if matched_pattern_indices.get(i) {
723                continue;
724            }
725            if exprs_are_equivalent(pattern_col, query_col) {
726                matched_pattern_indices.set(i).expect("TODO: alloc error");
727                found_match = true;
728                break;
729            }
730        }
731        if !found_match {
732            return None;
733        }
734    }
735    // All pattern columns must be matched
736    if matched_pattern_indices.count() != pattern_col_args.len() {
737        return None;
738    }
739    // Remaining args must match positionally (includes the query string parameter)
740    for (pattern_arg, query_arg) in pattern_rest.iter().zip(query_rest.iter()) {
741        let result = try_capture_parameters(pattern_arg, query_arg)?;
742        captured.extend(result);
743    }
744
745    Some(captured)
746}
747
748/// This function is used to determine whether two expressions are logically
749/// equivalent in the context of queries, even if their representations
750/// differ. e.g.: `SUM(x)` and `sum(x)`, `x + y` and `y + x`
751pub fn exprs_are_equivalent(expr1: &Expr, expr2: &Expr) -> bool {
752    match (expr1, expr2) {
753        (
754            Expr::Between {
755                lhs: lhs1,
756                not: not1,
757                start: start1,
758                end: end1,
759            },
760            Expr::Between {
761                lhs: lhs2,
762                not: not2,
763                start: start2,
764                end: end2,
765            },
766        ) => {
767            not1 == not2
768                && exprs_are_equivalent(lhs1, lhs2)
769                && exprs_are_equivalent(start1, start2)
770                && exprs_are_equivalent(end1, end2)
771        }
772        (Expr::Binary(lhs1, op1, rhs1), Expr::Binary(lhs2, op2, rhs2)) => {
773            op1 == op2
774                && ((exprs_are_equivalent(lhs1, lhs2) && exprs_are_equivalent(rhs1, rhs2))
775                    || (op1.is_commutative()
776                        && exprs_are_equivalent(lhs1, rhs2)
777                        && exprs_are_equivalent(rhs1, lhs2)))
778        }
779        (
780            Expr::Case {
781                base: base1,
782                when_then_pairs: pairs1,
783                else_expr: else1,
784            },
785            Expr::Case {
786                base: base2,
787                when_then_pairs: pairs2,
788                else_expr: else2,
789            },
790        ) => {
791            base1 == base2
792                && pairs1.len() == pairs2.len()
793                && pairs1.iter().zip(pairs2).all(|((w1, t1), (w2, t2))| {
794                    exprs_are_equivalent(w1, w2) && exprs_are_equivalent(t1, t2)
795                })
796                && else1 == else2
797        }
798        (
799            Expr::Cast {
800                expr: expr1,
801                type_name: type1,
802            },
803            Expr::Cast {
804                expr: expr2,
805                type_name: type2,
806            },
807        ) => {
808            exprs_are_equivalent(expr1, expr2)
809                && match (type1, type2) {
810                    (Some(t1), Some(t2)) => t1.name.eq_ignore_ascii_case(&t2.name),
811                    _ => false,
812                }
813        }
814        (Expr::Collate(expr1, collation1), Expr::Collate(expr2, collation2)) => {
815            // TODO: check correctness of comparing colation as strings
816            exprs_are_equivalent(expr1, expr2)
817                && collation1
818                    .as_str()
819                    .eq_ignore_ascii_case(collation2.as_str())
820        }
821        (
822            Expr::FunctionCall {
823                name: name1,
824                distinctness: distinct1,
825                args: args1,
826                order_by: order1,
827                within_group: within1,
828                filter_over: filter1,
829            },
830            Expr::FunctionCall {
831                name: name2,
832                distinctness: distinct2,
833                args: args2,
834                order_by: order2,
835                within_group: within2,
836                filter_over: filter2,
837            },
838        ) => {
839            name1.as_str().eq_ignore_ascii_case(name2.as_str())
840                && distinct1 == distinct2
841                && args1 == args2
842                && order1 == order2
843                && within1 == within2
844                && filter1 == filter2
845        }
846        (
847            Expr::FunctionCallStar {
848                name: name1,
849                filter_over: filter1,
850            },
851            Expr::FunctionCallStar {
852                name: name2,
853                filter_over: filter2,
854            },
855        ) => {
856            name1.as_str().eq_ignore_ascii_case(name2.as_str())
857                && match (&filter1.filter_clause, &filter2.filter_clause) {
858                    (Some(expr1), Some(expr2)) => exprs_are_equivalent(expr1, expr2),
859                    (None, None) => true,
860                    _ => false,
861                }
862                && filter1.over_clause == filter2.over_clause
863        }
864        (Expr::NotNull(expr1), Expr::NotNull(expr2)) => exprs_are_equivalent(expr1, expr2),
865        (Expr::IsNull(expr1), Expr::IsNull(expr2)) => exprs_are_equivalent(expr1, expr2),
866        (Expr::Literal(lit1), Expr::Literal(lit2)) => check_literal_equivalency(lit1, lit2),
867        (Expr::Id(id1), Expr::Id(id2)) => check_ident_equivalency(id1.as_str(), id2.as_str()),
868        (Expr::Unary(op1, expr1), Expr::Unary(op2, expr2)) => {
869            op1 == op2 && exprs_are_equivalent(expr1, expr2)
870        }
871        (Expr::Variable(val), Expr::Variable(val2)) => val == val2,
872        (Expr::Parenthesized(exprs1), Expr::Parenthesized(exprs2)) => {
873            exprs1.len() == exprs2.len()
874                && exprs1
875                    .iter()
876                    .zip(exprs2)
877                    .all(|(e1, e2)| exprs_are_equivalent(e1, e2))
878        }
879        (Expr::Parenthesized(exprs1), exprs2) | (exprs2, Expr::Parenthesized(exprs1)) => {
880            exprs1.len() == 1 && exprs_are_equivalent(&exprs1[0], exprs2)
881        }
882        (Expr::Qualified(tn1, cn1), Expr::Qualified(tn2, cn2)) => {
883            check_ident_equivalency(tn1.as_str(), tn2.as_str())
884                && check_ident_equivalency(cn1.as_str(), cn2.as_str())
885        }
886        (Expr::DoublyQualified(sn1, tn1, cn1), Expr::DoublyQualified(sn2, tn2, cn2)) => {
887            check_ident_equivalency(sn1.as_str(), sn2.as_str())
888                && check_ident_equivalency(tn1.as_str(), tn2.as_str())
889                && check_ident_equivalency(cn1.as_str(), cn2.as_str())
890        }
891        (
892            Expr::InList {
893                lhs: lhs1,
894                not: not1,
895                rhs: rhs1,
896            },
897            Expr::InList {
898                lhs: lhs2,
899                not: not2,
900                rhs: rhs2,
901            },
902        ) => {
903            *not1 == *not2
904                && exprs_are_equivalent(lhs1, lhs2)
905                && rhs1.len() == rhs2.len()
906                && rhs1
907                    .iter()
908                    .zip(rhs2.iter())
909                    .all(|(a, b)| exprs_are_equivalent(a, b))
910        }
911        (
912            Expr::Column {
913                database: db1,
914                is_rowid_alias: r1,
915                table: tbl_1,
916                column: col_1,
917            },
918            Expr::Column {
919                database: db2,
920                is_rowid_alias: r2,
921                table: tbl_2,
922                column: col_2,
923            },
924        ) => tbl_1 == tbl_2 && col_1 == col_2 && db1 == db2 && r1 == r2,
925        // fall back to naive equality check
926        _ => expr1 == expr2,
927    }
928}
929
930/// "evaluate" an expression to determine if it contains a poisonous NULL
931/// which will propagate through most expressions and result in it's evaluation
932/// into NULL. This is used to prevent things like the following:
933/// `ALTER TABLE t ADD COLUMN (a NOT NULL DEFAULT (NULL + 5)`
934pub(crate) fn expr_contains_null(expr: &ast::Expr) -> bool {
935    let mut contains_null = false;
936    let _ = walk_expr(expr, &mut |expr: &ast::Expr| -> Result<WalkControl> {
937        if let ast::Expr::Literal(ast::Literal::Null) = expr {
938            contains_null = true;
939            return Ok(WalkControl::SkipChildren);
940        }
941        Ok(WalkControl::Continue)
942    }); // infallible
943    contains_null
944}
945
946// this function returns the affinity type and whether the type name was exactly "INTEGER"
947// https://www.sqlite.org/datatype3.html
948pub(crate) fn type_from_name(type_name: &str) -> (Type, bool) {
949    let type_name = type_name.as_bytes();
950    if type_name.is_empty() {
951        return (Type::Blob, false);
952    }
953
954    if eq_ignore_ascii_case!(type_name, b"INTEGER") {
955        return (Type::Integer, true);
956    }
957
958    if contains_ignore_ascii_case!(type_name, b"INT") {
959        return (Type::Integer, false);
960    }
961
962    if let Some(ty) = type_name.windows(4).find_map(|s| {
963        match_ignore_ascii_case!(match s {
964            b"CHAR" | b"CLOB" | b"TEXT" => Some(Type::Text),
965            b"BLOB" => Some(Type::Blob),
966            b"REAL" | b"FLOA" | b"DOUB" => Some(Type::Real),
967            _ => None,
968        })
969    }) {
970        return (ty, false);
971    }
972
973    (Type::Numeric, false)
974}
975
976pub fn columns_from_create_table_body(
977    body: &turso_parser::ast::CreateTableBody,
978) -> crate::Result<Vec<Column>> {
979    let CreateTableBody::ColumnsAndConstraints { columns, .. } = body else {
980        return Err(crate::LimboError::ParseError(
981            "CREATE TABLE body must contain columns and constraints".to_string(),
982        ));
983    };
984
985    columns
986        .iter()
987        .map(Column::try_from)
988        .collect::<crate::Result<Vec<Column>>>()
989}
990
991#[derive(Debug, Default, PartialEq)]
992pub struct OpenOptions<'a> {
993    /// The authority component of the URI. may be 'localhost' or empty
994    pub authority: Option<&'a str>,
995    /// The normalized path to the database file
996    pub path: String,
997    /// The vfs query parameter causes the database connection to be opened using the VFS called NAME
998    pub vfs: Option<String>,
999    /// read-only, read-write, read-write and created if it does not exist, or pure in-memory database that never interacts with disk
1000    pub mode: OpenMode,
1001    /// Attempt to set the permissions of the new database file to match the existing file "filename".
1002    pub modeof: Option<String>,
1003    /// Specifies Cache mode shared | private
1004    pub cache: CacheMode,
1005    /// immutable=1|0 specifies that the database is stored on read-only media
1006    pub immutable: bool,
1007    // The encryption cipher
1008    pub cipher: Option<String>,
1009    // The encryption key in hex format
1010    pub hexkey: Option<String>,
1011}
1012
1013pub const MEMORY_PATH: &str = ":memory:";
1014
1015#[derive(Clone, Default, Debug, Copy, PartialEq)]
1016pub enum OpenMode {
1017    ReadOnly,
1018    ReadWrite,
1019    Memory,
1020    #[default]
1021    ReadWriteCreate,
1022}
1023
1024#[derive(Debug, Default, Clone, Copy, PartialEq)]
1025pub enum CacheMode {
1026    #[default]
1027    Private,
1028    Shared,
1029}
1030
1031impl From<&str> for CacheMode {
1032    fn from(s: &str) -> Self {
1033        match s {
1034            "private" => CacheMode::Private,
1035            "shared" => CacheMode::Shared,
1036            _ => CacheMode::Private,
1037        }
1038    }
1039}
1040
1041impl OpenMode {
1042    pub fn from_str(s: &str) -> Result<Self> {
1043        let s_bytes = s.trim().as_bytes();
1044        match_ignore_ascii_case!(match s_bytes {
1045            b"ro" => Ok(OpenMode::ReadOnly),
1046            b"rw" => Ok(OpenMode::ReadWrite),
1047            b"memory" => Ok(OpenMode::Memory),
1048            b"rwc" => Ok(OpenMode::ReadWriteCreate),
1049            _ => Err(LimboError::InvalidArgument(format!(
1050                "Invalid mode: '{s}'. Expected one of 'ro', 'rw', 'memory', 'rwc'"
1051            ))),
1052        })
1053    }
1054}
1055
1056fn is_windows_path(path: &str) -> bool {
1057    path.len() >= 3
1058        && path.chars().nth(1) == Some(':')
1059        && (path.chars().nth(2) == Some('/') || path.chars().nth(2) == Some('\\'))
1060}
1061
1062/// converts windows-style paths to forward slashes, per SQLite spec.
1063fn normalize_windows_path(path: &str) -> String {
1064    let mut normalized = path.replace("\\", "/");
1065
1066    // remove duplicate slashes (`//` → `/`)
1067    while normalized.contains("//") {
1068        normalized = normalized.replace("//", "/");
1069    }
1070
1071    // if absolute windows path (`C:/...`), ensure it starts with `/`
1072    if normalized.len() >= 3
1073        && !normalized.starts_with('/')
1074        && normalized.chars().nth(1) == Some(':')
1075        && normalized.chars().nth(2) == Some('/')
1076    {
1077        normalized.insert(0, '/');
1078    }
1079    normalized
1080}
1081
1082impl<'a> OpenOptions<'a> {
1083    /// Parses a SQLite URI, handling Windows and Unix paths separately.
1084    pub fn parse(uri: &'a str) -> Result<OpenOptions<'a>> {
1085        if !uri.starts_with("file:") {
1086            return Ok(OpenOptions {
1087                path: uri.to_string(),
1088                ..Default::default()
1089            });
1090        }
1091
1092        let mut opts = OpenOptions::default();
1093        let without_scheme = &uri[5..];
1094
1095        let (without_fragment, _) = without_scheme
1096            .split_once('#')
1097            .unwrap_or((without_scheme, ""));
1098
1099        let (without_query, query) = without_fragment
1100            .split_once('?')
1101            .unwrap_or((without_fragment, ""));
1102        parse_query_params(query, &mut opts)?;
1103
1104        // handle authority + path separately
1105        if let Some(after_slashes) = without_query.strip_prefix("//") {
1106            let (authority, path) = after_slashes.split_once('/').unwrap_or((after_slashes, ""));
1107
1108            // sqlite allows only `localhost` or empty authority.
1109            if !(authority.is_empty() || authority == "localhost") {
1110                return Err(LimboError::InvalidArgument(format!(
1111                    "Invalid authority '{authority}'. Only '' or 'localhost' allowed."
1112                )));
1113            }
1114            opts.authority = if authority.is_empty() {
1115                None
1116            } else {
1117                Some(authority)
1118            };
1119
1120            if is_windows_path(path) {
1121                opts.path = normalize_windows_path(&decode_percent(path));
1122            } else if !path.is_empty() {
1123                opts.path = format!("/{}", decode_percent(path));
1124            } else {
1125                opts.path = String::new();
1126            }
1127        } else {
1128            // no authority, must be a normal absolute or relative path.
1129            opts.path = decode_percent(without_query);
1130        }
1131
1132        Ok(opts)
1133    }
1134
1135    pub fn get_flags(&self) -> Result<OpenFlags> {
1136        // Only use modeof if we're in a mode that can create files
1137        if self.mode != OpenMode::ReadWriteCreate && self.modeof.is_some() {
1138            return Err(LimboError::InvalidArgument(
1139                "modeof is not applicable without mode=rwc".to_string(),
1140            ));
1141        }
1142        // If modeof is not applicable or file doesn't exist, use default flags
1143        Ok(match self.mode {
1144            OpenMode::ReadWriteCreate => OpenFlags::Create,
1145            OpenMode::ReadOnly => OpenFlags::ReadOnly,
1146            _ => OpenFlags::default(),
1147        })
1148    }
1149}
1150
1151// parses query parameters and updates OpenOptions
1152fn parse_query_params(query: &str, opts: &mut OpenOptions) -> Result<()> {
1153    for param in query.split('&') {
1154        if let Some((key, value)) = param.split_once('=') {
1155            let decoded_value = decode_percent(value);
1156            match key {
1157                "mode" => opts.mode = OpenMode::from_str(value)?,
1158                "modeof" => opts.modeof = Some(decoded_value),
1159                "cache" => opts.cache = decoded_value.as_str().into(),
1160                "immutable" => opts.immutable = decoded_value == "1",
1161                "vfs" => opts.vfs = Some(decoded_value),
1162                "cipher" => opts.cipher = Some(decoded_value),
1163                "hexkey" => opts.hexkey = Some(decoded_value),
1164                _ => {}
1165            }
1166        }
1167    }
1168    Ok(())
1169}
1170
1171/// Decodes percent-encoded characters
1172/// this function was adapted from the 'urlencoding' crate. MIT
1173pub fn decode_percent(uri: &str) -> String {
1174    let from_hex_digit = |digit: u8| -> Option<u8> {
1175        match digit {
1176            b'0'..=b'9' => Some(digit - b'0'),
1177            b'A'..=b'F' => Some(digit - b'A' + 10),
1178            b'a'..=b'f' => Some(digit - b'a' + 10),
1179            _ => None,
1180        }
1181    };
1182
1183    let offset = uri.chars().take_while(|&c| c != '%').count();
1184
1185    if offset >= uri.len() {
1186        return uri.to_string();
1187    }
1188
1189    let mut decoded: Vec<u8> = Vec::with_capacity(uri.len());
1190    let (ascii, mut data) = uri.as_bytes().split_at(offset);
1191    decoded.extend_from_slice(ascii);
1192
1193    loop {
1194        let mut parts = data.splitn(2, |&c| c == b'%');
1195        let non_escaped_part = parts.next().unwrap();
1196        let rest = parts.next();
1197        if rest.is_none() && decoded.is_empty() {
1198            return String::from_utf8_lossy(data).to_string();
1199        }
1200        decoded.extend_from_slice(non_escaped_part);
1201        match rest {
1202            Some(rest) => match rest.get(0..2) {
1203                Some([first, second]) => match from_hex_digit(*first) {
1204                    Some(first_val) => match from_hex_digit(*second) {
1205                        Some(second_val) => {
1206                            decoded.push((first_val << 4) | second_val);
1207                            data = &rest[2..];
1208                        }
1209                        None => {
1210                            decoded.extend_from_slice(&[b'%', *first]);
1211                            data = &rest[1..];
1212                        }
1213                    },
1214                    None => {
1215                        decoded.push(b'%');
1216                        data = rest;
1217                    }
1218                },
1219                _ => {
1220                    decoded.push(b'%');
1221                    decoded.extend_from_slice(rest);
1222                    break;
1223                }
1224            },
1225            None => break,
1226        }
1227    }
1228    String::from_utf8_lossy(&decoded).to_string()
1229}
1230
1231pub fn trim_ascii_whitespace(s: &str) -> &str {
1232    let bytes = s.as_bytes();
1233    let start = bytes
1234        .iter()
1235        .position(|&b| !b.is_ascii_whitespace())
1236        .unwrap_or(bytes.len());
1237    let end = bytes
1238        .iter()
1239        .rposition(|&b| !b.is_ascii_whitespace())
1240        .map(|i| i + 1)
1241        .unwrap_or(0);
1242    if start <= end {
1243        &s[start..end]
1244    } else {
1245        ""
1246    }
1247}
1248
1249/// NUMERIC Casting a TEXT or BLOB value into NUMERIC yields either an INTEGER or a REAL result.
1250/// If the input text looks like an integer (there is no decimal point nor exponent) and the value
1251/// is small enough to fit in a 64-bit signed integer, then the result will be INTEGER.
1252/// Input text that looks like floating point (there is a decimal point and/or an exponent)
1253/// and the text describes a value that can be losslessly converted back and forth between IEEE 754
1254/// 64-bit float and a 51-bit signed integer, then the result is INTEGER. (In the previous sentence,
1255/// a 51-bit integer is specified since that is one bit less than the length of the mantissa of an
1256/// IEEE 754 64-bit float and thus provides a 1-bit of margin for the text-to-float conversion operation.)
1257/// Any text input that describes a value outside the range of a 64-bit signed integer yields a REAL result.
1258/// Casting a REAL or INTEGER value to NUMERIC is a no-op, even if a real value could be losslessly converted to an integer.
1259///
1260/// `lossless`: If `true`, rejects the input if any characters remain after the numeric prefix (strict / exact conversion).
1261pub fn checked_cast_text_to_numeric(text: &str, lossless: bool) -> std::result::Result<Value, ()> {
1262    // sqlite will parse the first N digits of a string to numeric value, then determine
1263    // whether _that_ value is more likely a real or integer value. e.g.
1264    // '-100234-2344.23e14' evaluates to -100234 instead of -100234.0
1265    let original_len = text.trim().len();
1266    let (kind, text) = parse_numeric_str(text)?;
1267
1268    if original_len != text.len() && lossless {
1269        return Err(());
1270    }
1271
1272    match kind {
1273        ValueType::Integer => match text.parse::<i64>() {
1274            Ok(i) => Ok(Value::from_i64(i)),
1275            Err(e) => {
1276                if matches!(
1277                    e.kind(),
1278                    std::num::IntErrorKind::PosOverflow | std::num::IntErrorKind::NegOverflow
1279                ) {
1280                    // if overflow, we return the representation as a real.
1281                    // we have to match sqlite exactly here, so we match sqlite3AtoF
1282                    let value = text.parse::<f64>().unwrap_or_default();
1283                    let factor = 10f64.powi(15 - value.abs().log10().ceil() as i32);
1284                    Ok(Value::from_f64((value * factor).round() / factor))
1285                } else {
1286                    Err(())
1287                }
1288            }
1289        },
1290        ValueType::Float => {
1291            let value = text.parse::<f64>().unwrap_or(0.0);
1292            Ok(real_to_numeric_value(value))
1293        }
1294        _ => unreachable!(),
1295    }
1296}
1297
1298/// Applies the same normalization strategy as SQLite:
1299/// converts a `f64` to an integer when it represents an exact integral value.
1300///
1301/// The conversion is restricted to 51-bit signed integers to ensure the value
1302/// can round-trip through a text representation without losing precision,
1303/// staying below the 52-bit mantissa limit of IEEE 754 `f64`.
1304fn real_to_numeric_value(value: f64) -> Value {
1305    const INT51_MAX: i64 = 1 << 51;
1306    if value == 0.0 {
1307        return Value::from_i64(0);
1308    }
1309    if value.is_finite() {
1310        let i = value as i64;
1311        if (i as f64) == value && (-INT51_MAX..INT51_MAX).contains(&i) {
1312            return Value::from_i64(i);
1313        }
1314    }
1315    Value::from_f64(value)
1316}
1317
1318fn parse_numeric_str(text: &str) -> Result<(ValueType, &str), ()> {
1319    let text = text.trim();
1320    let bytes = text.as_bytes();
1321
1322    if matches!(
1323        bytes,
1324        [] | [b'e', ..] | [b'E', ..] | [b'.', b'e' | b'E', ..]
1325    ) {
1326        return Err(());
1327    }
1328
1329    let mut end = 0;
1330    let mut has_decimal = false;
1331    let mut has_exponent = false;
1332    if bytes[0] == b'-' || bytes[0] == b'+' {
1333        end = 1;
1334    }
1335    while end < bytes.len() {
1336        match bytes[end] {
1337            b'0'..=b'9' => end += 1,
1338            b'.' if !has_decimal && !has_exponent => {
1339                has_decimal = true;
1340                end += 1;
1341            }
1342            b'e' | b'E' if !has_exponent => {
1343                has_exponent = true;
1344                end += 1;
1345                // allow exponent sign
1346                if end < bytes.len() && (bytes[end] == b'+' || bytes[end] == b'-') {
1347                    end += 1;
1348                }
1349            }
1350            _ => break,
1351        }
1352    }
1353    if end == 0 || (end == 1 && (bytes[0] == b'-' || bytes[0] == b'+')) {
1354        return Err(());
1355    }
1356    // edge case: if it ends with exponent, strip and cast valid digits as float
1357    let last = bytes[end - 1];
1358    if last.eq_ignore_ascii_case(&b'e') {
1359        return Ok((ValueType::Float, &text[0..end - 1]));
1360    // edge case: ends with extponent / sign
1361    } else if has_exponent && (last == b'-' || last == b'+') {
1362        return Ok((ValueType::Float, &text[0..end - 2]));
1363    }
1364    Ok((
1365        if !has_decimal && !has_exponent {
1366            ValueType::Integer
1367        } else {
1368            ValueType::Float
1369        },
1370        &text[..end],
1371    ))
1372}
1373
1374// Check if float can be converted to integer for INTEGER PRIMARY KEY columns.
1375// SQLite uses sqlite3VdbeIntegerAffinity which requires:
1376// 1. The float must round-trip correctly (float -> int -> float gives same value)
1377// 2. The integer must be strictly between i64::MIN and i64::MAX (exclusive)
1378//
1379// This matches SQLite's check: ix > SMALLEST_INT64 && ix < LARGEST_INT64
1380pub fn cast_real_to_integer(float: f64) -> std::result::Result<i64, ()> {
1381    // Must be finite and a whole number (no fractional part)
1382    if !float.is_finite() || float.trunc() != float {
1383        return Err(());
1384    }
1385
1386    // Convert to i64, clamping to i64 range if necessary
1387    // Note: Rust's f64 as i64 saturates to i64::MIN/MAX for out-of-range values
1388    let int_val = float as i64;
1389
1390    // SQLite requires the value to be STRICTLY between i64::MIN and i64::MAX
1391    // (i.e., ix > SMALLEST_INT64 && ix < LARGEST_INT64)
1392    if int_val == i64::MIN || int_val == i64::MAX {
1393        return Err(());
1394    }
1395
1396    // Verify round-trip: converting back to f64 must give the same value
1397    // This matches SQLite's check: pMem->u.r == ix
1398    if (int_val as f64) != float {
1399        return Err(());
1400    }
1401
1402    Ok(int_val)
1403}
1404
1405// we don't need to verify the numeric literal here, as it is already verified by the parser
1406pub fn parse_numeric_literal(text: &str) -> Result<Value> {
1407    // a single extra underscore ("_") character can exist between any two digits
1408    let text = if text.contains('_') {
1409        std::borrow::Cow::Owned(text.replace('_', ""))
1410    } else {
1411        std::borrow::Cow::Borrowed(text)
1412    };
1413
1414    if text.starts_with("0x") || text.starts_with("0X") {
1415        let value = u64::from_str_radix(&text[2..], 16)? as i64;
1416        return Ok(Value::from_i64(value));
1417    } else if text.starts_with("-0x") || text.starts_with("-0X") {
1418        let value = u64::from_str_radix(&text[3..], 16)? as i64;
1419        if value == i64::MIN {
1420            return Err(LimboError::IntegerOverflow);
1421        }
1422        return Ok(Value::from_i64(-value));
1423    }
1424
1425    if let Ok(int_value) = text.parse::<i64>() {
1426        return Ok(Value::from_i64(int_value));
1427    }
1428
1429    let Some(StrToF64::Fractional(float) | StrToF64::Decimal(float)) =
1430        crate::numeric::str_to_f64(text)
1431    else {
1432        unreachable!();
1433    };
1434    Ok(Value::Numeric(crate::numeric::Numeric::Float(float)))
1435}
1436
1437pub fn parse_signed_number(expr: &Expr) -> Result<Value> {
1438    match expr {
1439        Expr::Literal(Literal::Numeric(num)) => parse_numeric_literal(num),
1440        Expr::Unary(op, expr) => match (op, expr.as_ref()) {
1441            (UnaryOperator::Negative, Expr::Literal(Literal::Numeric(num))) => {
1442                let data = "-".to_owned() + &num.to_string();
1443                parse_numeric_literal(&data)
1444            }
1445            (UnaryOperator::Positive, Expr::Literal(Literal::Numeric(num))) => {
1446                parse_numeric_literal(num)
1447            }
1448            _ => Err(LimboError::InvalidArgument(
1449                "signed-number must follow the format: ([+|-] numeric-literal)".to_string(),
1450            )),
1451        },
1452        _ => Err(LimboError::InvalidArgument(
1453            "signed-number must follow the format: ([+|-] numeric-literal)".to_string(),
1454        )),
1455    }
1456}
1457
1458pub fn parse_string(expr: &Expr) -> Result<String> {
1459    match expr {
1460        Expr::Name(name) if name.quoted_with('\'') => Ok(name.as_str().to_string()),
1461        _ => Err(LimboError::InvalidArgument(format!(
1462            "string parameter expected, got {expr:?} instead"
1463        ))),
1464    }
1465}
1466
1467#[allow(unused)]
1468pub fn parse_pragma_bool(expr: &Expr) -> Result<bool> {
1469    const TRUE_VALUES: &[&str] = &["yes", "true", "on"];
1470    const FALSE_VALUES: &[&str] = &["no", "false", "off"];
1471    if let Ok(number) = parse_signed_number(expr) {
1472        if let Value::Numeric(crate::numeric::Numeric::Integer(x @ (0 | 1))) = number {
1473            return Ok(x != 0);
1474        }
1475    } else if let Expr::Name(name) = expr {
1476        let ident = normalize_ident(name.as_str());
1477        if TRUE_VALUES.contains(&ident.as_str()) {
1478            return Ok(true);
1479        }
1480        if FALSE_VALUES.contains(&ident.as_str()) {
1481            return Ok(false);
1482        }
1483    }
1484    Err(LimboError::InvalidArgument(
1485        "boolean pragma value must be either 0|1 integer or yes|true|on|no|false|off token"
1486            .to_string(),
1487    ))
1488}
1489
1490/// Extract column name from an expression (e.g., for SELECT clauses)
1491pub fn extract_column_name_from_expr(expr: impl AsRef<ast::Expr>) -> Option<String> {
1492    match expr.as_ref() {
1493        ast::Expr::Id(name) => Some(name.as_str().to_string()),
1494        ast::Expr::DoublyQualified(_, _, name) | ast::Expr::Qualified(_, name) => {
1495            Some(normalize_ident(name.as_str()))
1496        }
1497        _ => None,
1498    }
1499}
1500
1501/// Information about a table referenced in a view
1502#[derive(Debug, Clone)]
1503pub struct ViewTable {
1504    /// Unqualified table name, normalized.
1505    pub name: String,
1506    /// Database qualifier if present, normalized.
1507    pub db_name: Option<String>,
1508    /// Optional alias (e.g., "c" in "FROM customers c")
1509    pub alias: Option<String>,
1510}
1511
1512/// Information about a column in the view's output
1513#[derive(Debug, Clone)]
1514pub struct ViewColumn {
1515    /// Index into ViewColumnSchema.tables indicating which table this column comes from
1516    /// For computed columns or constants, this will be usize::MAX
1517    pub table_index: usize,
1518    /// The actual column definition
1519    pub column: Column,
1520}
1521
1522/// Schema information for a view, tracking which columns come from which tables
1523#[derive(Debug, Clone)]
1524pub struct ViewColumnSchema {
1525    /// All tables referenced by the view (in order of appearance)
1526    pub tables: Vec<ViewTable>,
1527    /// The view's output columns with their table associations
1528    pub columns: Vec<ViewColumn>,
1529}
1530
1531impl ViewColumnSchema {
1532    /// Get all columns as a flat vector (without table association info)
1533    pub fn flat_columns(&self) -> crate::alloc::Vec<Column> {
1534        self.columns
1535            .iter()
1536            .map(|vc| vc.column.clone())
1537            .try_collect()
1538            .expect(crate::alloc::ALLOC_ERR_MSG)
1539    }
1540
1541    /// Get columns that belong to a specific table
1542    pub fn table_columns(&self, table_index: usize) -> Vec<Column> {
1543        self.columns
1544            .iter()
1545            .filter(|vc| vc.table_index == table_index)
1546            .map(|vc| vc.column.clone())
1547            .collect()
1548    }
1549}
1550
1551/// Walk all expressions in a SELECT statement, including subqueries.
1552pub fn walk_select_expressions<F>(select: &ast::Select, func: &mut F) -> Result<()>
1553where
1554    F: FnMut(&ast::Expr) -> Result<WalkControl>,
1555{
1556    walk_select_expressions_inner(select, func)
1557}
1558
1559fn walk_select_expressions_inner<F>(select: &ast::Select, func: &mut F) -> Result<()>
1560where
1561    F: FnMut(&ast::Expr) -> Result<WalkControl>,
1562{
1563    if let Some(with_clause) = &select.with {
1564        for cte in &with_clause.ctes {
1565            walk_select_expressions_inner(&cte.select, func)?;
1566        }
1567    }
1568
1569    walk_one_select_expressions(&select.body.select, func)?;
1570    for compound in &select.body.compounds {
1571        walk_one_select_expressions(&compound.select, func)?;
1572    }
1573
1574    for sorted_col in &select.order_by {
1575        walk_expr_with_subqueries(&sorted_col.expr, func)?;
1576    }
1577
1578    if let Some(limit) = &select.limit {
1579        walk_expr_with_subqueries(&limit.expr, func)?;
1580        if let Some(offset) = &limit.offset {
1581            walk_expr_with_subqueries(offset, func)?;
1582        }
1583    }
1584
1585    Ok(())
1586}
1587
1588fn walk_one_select_expressions<F>(one_select: &ast::OneSelect, func: &mut F) -> Result<()>
1589where
1590    F: FnMut(&ast::Expr) -> Result<WalkControl>,
1591{
1592    match one_select {
1593        ast::OneSelect::Select {
1594            columns,
1595            from,
1596            where_clause,
1597            group_by,
1598            window_clause,
1599            ..
1600        } => {
1601            for col in columns {
1602                if let ast::ResultColumn::Expr(expr, _) = col {
1603                    walk_expr_with_subqueries(expr, func)?;
1604                }
1605            }
1606
1607            if let Some(from_clause) = from {
1608                walk_from_clause_expressions(from_clause, func)?;
1609            }
1610
1611            if let Some(where_expr) = where_clause {
1612                walk_expr_with_subqueries(where_expr, func)?;
1613            }
1614
1615            if let Some(group_by) = group_by {
1616                for expr in &group_by.exprs {
1617                    walk_expr_with_subqueries(expr, func)?;
1618                }
1619                if let Some(having_expr) = &group_by.having {
1620                    walk_expr_with_subqueries(having_expr, func)?;
1621                }
1622            }
1623
1624            for window_def in window_clause {
1625                walk_window_expressions(&window_def.window, func)?;
1626            }
1627        }
1628        ast::OneSelect::Values(values) => {
1629            for row in values {
1630                for expr in row {
1631                    walk_expr_with_subqueries(expr, func)?;
1632                }
1633            }
1634        }
1635    }
1636
1637    Ok(())
1638}
1639
1640fn walk_from_clause_expressions<F>(from_clause: &ast::FromClause, func: &mut F) -> Result<()>
1641where
1642    F: FnMut(&ast::Expr) -> Result<WalkControl>,
1643{
1644    walk_select_table_expressions(&from_clause.select, func)?;
1645
1646    for join in &from_clause.joins {
1647        walk_select_table_expressions(&join.table, func)?;
1648
1649        if let Some(ast::JoinConstraint::On(expr)) = &join.constraint {
1650            walk_expr_with_subqueries(expr, func)?;
1651        }
1652    }
1653
1654    Ok(())
1655}
1656
1657fn walk_select_table_expressions<F>(select_table: &ast::SelectTable, func: &mut F) -> Result<()>
1658where
1659    F: FnMut(&ast::Expr) -> Result<WalkControl>,
1660{
1661    match select_table {
1662        ast::SelectTable::Select(select, _) => walk_select_expressions_inner(select, func),
1663        ast::SelectTable::Sub(from_clause, _) => walk_from_clause_expressions(from_clause, func),
1664        ast::SelectTable::TableCall(_, args, _) => {
1665            for arg in args {
1666                walk_expr_with_subqueries(arg, func)?;
1667            }
1668            Ok(())
1669        }
1670        ast::SelectTable::Table(_, _, _) => Ok(()),
1671    }
1672}
1673
1674fn walk_window_expressions<F>(window: &ast::Window, func: &mut F) -> Result<()>
1675where
1676    F: FnMut(&ast::Expr) -> Result<WalkControl>,
1677{
1678    for expr in &window.partition_by {
1679        walk_expr_with_subqueries(expr, func)?;
1680    }
1681
1682    for sorted_col in &window.order_by {
1683        walk_expr_with_subqueries(&sorted_col.expr, func)?;
1684    }
1685
1686    if let Some(frame_clause) = &window.frame_clause {
1687        walk_frame_bound_expressions(&frame_clause.start, func)?;
1688        if let Some(end_bound) = &frame_clause.end {
1689            walk_frame_bound_expressions(end_bound, func)?;
1690        }
1691    }
1692
1693    Ok(())
1694}
1695
1696fn walk_frame_bound_expressions<F>(bound: &ast::FrameBound, func: &mut F) -> Result<()>
1697where
1698    F: FnMut(&ast::Expr) -> Result<WalkControl>,
1699{
1700    match bound {
1701        ast::FrameBound::Following(expr) | ast::FrameBound::Preceding(expr) => {
1702            walk_expr_with_subqueries(expr, func)
1703        }
1704        ast::FrameBound::CurrentRow
1705        | ast::FrameBound::UnboundedFollowing
1706        | ast::FrameBound::UnboundedPreceding => Ok(()),
1707    }
1708}
1709
1710pub fn walk_expr_with_subqueries<F>(expr: &ast::Expr, func: &mut F) -> Result<()>
1711where
1712    F: FnMut(&ast::Expr) -> Result<WalkControl>,
1713{
1714    walk_expr(expr, &mut |e| {
1715        let control = func(e)?;
1716        if matches!(control, WalkControl::Continue) {
1717            match e {
1718                ast::Expr::Subquery(select) | ast::Expr::Exists(select) => {
1719                    walk_select_expressions_inner(select, func)?;
1720                }
1721                ast::Expr::InSelect { rhs, .. } => {
1722                    walk_select_expressions_inner(rhs, func)?;
1723                }
1724                _ => {}
1725            }
1726        }
1727        Ok(control)
1728    })?;
1729    Ok(())
1730}
1731
1732fn validate_no_cross_db_references(
1733    select_stmt: &ast::Select,
1734    view_db_name: Option<&ast::Name>,
1735) -> Result<()> {
1736    if let Some(with_clause) = &select_stmt.with {
1737        for cte in &with_clause.ctes {
1738            validate_no_cross_db_references(&cte.select, view_db_name)?;
1739        }
1740    }
1741
1742    validate_one_select_no_cross_db(&select_stmt.body.select, view_db_name)?;
1743
1744    for compound in &select_stmt.body.compounds {
1745        validate_one_select_no_cross_db(&compound.select, view_db_name)?;
1746    }
1747
1748    Ok(())
1749}
1750
1751fn validate_one_select_no_cross_db(
1752    one_select: &ast::OneSelect,
1753    view_db_name: Option<&ast::Name>,
1754) -> Result<()> {
1755    match one_select {
1756        ast::OneSelect::Select { from, .. } => {
1757            if let Some(from_clause) = from {
1758                validate_from_clause_no_cross_db(from_clause, view_db_name)?;
1759            }
1760        }
1761        ast::OneSelect::Values(_) => {}
1762    }
1763    Ok(())
1764}
1765
1766fn validate_from_clause_no_cross_db(
1767    from_clause: &ast::FromClause,
1768    view_db_name: Option<&ast::Name>,
1769) -> Result<()> {
1770    validate_select_table_no_cross_db(&from_clause.select, view_db_name)?;
1771    for join in &from_clause.joins {
1772        validate_select_table_no_cross_db(&join.table, view_db_name)?;
1773    }
1774    Ok(())
1775}
1776
1777fn reject_cross_db_qualified_name(
1778    qualified_name: &ast::QualifiedName,
1779    view_db_name: Option<&ast::Name>,
1780) -> Result<()> {
1781    if let Some(table_db_name) = &qualified_name.db_name {
1782        let is_cross_db = match view_db_name {
1783            Some(view_db) => !view_db
1784                .as_str()
1785                .eq_ignore_ascii_case(table_db_name.as_str()),
1786            None => !table_db_name.as_str().eq_ignore_ascii_case("main"),
1787        };
1788        if is_cross_db {
1789            return Err(crate::LimboError::ParseError(format!(
1790                "view cannot reference table in attached database: {qualified_name}"
1791            )));
1792        }
1793    }
1794    Ok(())
1795}
1796
1797fn validate_select_table_no_cross_db(
1798    select_table: &ast::SelectTable,
1799    view_db_name: Option<&ast::Name>,
1800) -> Result<()> {
1801    match select_table {
1802        ast::SelectTable::Table(name, _, _) | ast::SelectTable::TableCall(name, _, _) => {
1803            reject_cross_db_qualified_name(name, view_db_name)?;
1804        }
1805        ast::SelectTable::Select(select, _) => {
1806            validate_no_cross_db_references(select, view_db_name)?;
1807        }
1808        ast::SelectTable::Sub(from_clause, _) => {
1809            validate_from_clause_no_cross_db(from_clause, view_db_name)?;
1810        }
1811    }
1812    Ok(())
1813}
1814
1815pub fn validate_select_for_unsupported_features(select_stmt: &ast::Select) -> Result<()> {
1816    walk_select_expressions(select_stmt, &mut |expr| {
1817        if let ast::Expr::FunctionCall { order_by, .. } = expr {
1818            if !order_by.is_empty() {
1819                crate::bail_parse_error!(
1820                    "ORDER BY clause is not supported yet in aggregate functions"
1821                );
1822            }
1823        }
1824        Ok(WalkControl::Continue)
1825    })
1826}
1827
1828pub fn validate_select_for_views(
1829    select_stmt: &ast::Select,
1830    view_db_name: Option<&ast::Name>,
1831) -> Result<()> {
1832    validate_select_for_unsupported_features(select_stmt)?;
1833
1834    validate_no_cross_db_references(select_stmt, view_db_name)?;
1835
1836    walk_select_expressions(select_stmt, &mut |expr| {
1837        match expr {
1838            ast::Expr::Subquery(subquery_select) | ast::Expr::Exists(subquery_select) => {
1839                validate_no_cross_db_references(subquery_select, view_db_name)?;
1840            }
1841            ast::Expr::InSelect { rhs, .. } => {
1842                validate_no_cross_db_references(rhs, view_db_name)?;
1843            }
1844            _ => {}
1845        }
1846
1847        Ok(WalkControl::Continue)
1848    })?;
1849
1850    Ok(())
1851}
1852
1853/// Extract column information from a SELECT statement for view creation
1854pub fn extract_view_columns(
1855    select_stmt: &ast::Select,
1856    schema: &Schema,
1857) -> Result<ViewColumnSchema> {
1858    let mut tables = Vec::new();
1859    let mut columns = Vec::new();
1860    let mut column_name_counts: HashMap<String, usize> = HashMap::default();
1861
1862    // Navigate to the first SELECT in the statement
1863    if let ast::OneSelect::Select {
1864        ref from,
1865        columns: select_columns,
1866        ..
1867    } = &select_stmt.body.select
1868    {
1869        // First, extract all tables (from FROM clause and JOINs)
1870        if let Some(from) = from {
1871            // Add the main table from FROM clause
1872            match from.select.as_ref() {
1873                ast::SelectTable::Table(qualified_name, alias, _) => {
1874                    let table_name = normalize_ident(qualified_name.name.as_str());
1875                    let db_name = qualified_name
1876                        .db_name
1877                        .as_ref()
1878                        .map(|db| normalize_ident(db.as_str()));
1879                    tables.push(ViewTable {
1880                        name: table_name,
1881                        db_name,
1882                        alias: alias.as_ref().map(|a| normalize_ident(a.name().as_str())),
1883                    });
1884                }
1885                _ => {
1886                    // Handle other types like subqueries if needed
1887                }
1888            }
1889
1890            // Add tables from JOINs
1891            for join in &from.joins {
1892                match join.table.as_ref() {
1893                    ast::SelectTable::Table(qualified_name, alias, _) => {
1894                        let table_name = normalize_ident(qualified_name.name.as_str());
1895                        let db_name = qualified_name
1896                            .db_name
1897                            .as_ref()
1898                            .map(|db| normalize_ident(db.as_str()));
1899                        tables.push(ViewTable {
1900                            name: table_name,
1901                            db_name,
1902                            alias: alias.as_ref().map(|a| normalize_ident(a.name().as_str())),
1903                        });
1904                    }
1905                    _ => {
1906                        // Handle other types like subqueries if needed
1907                    }
1908                }
1909            }
1910        }
1911
1912        // Helper function to find table index by name or alias
1913        let find_table_index = |name: &str| -> Option<usize> {
1914            tables.iter().position(|t| {
1915                t.name.eq_ignore_ascii_case(name)
1916                    || t.alias
1917                        .as_ref()
1918                        .is_some_and(|a| a.eq_ignore_ascii_case(name))
1919            })
1920        };
1921
1922        // Process each column in the SELECT list
1923        for result_col in select_columns.iter() {
1924            match result_col {
1925                ast::ResultColumn::Expr(expr, alias) => {
1926                    // Figure out which table this expression comes from
1927                    let table_index = match expr.as_ref() {
1928                        ast::Expr::Qualified(table_ref, _col_name) => {
1929                            // Column qualified with table name
1930                            find_table_index(table_ref.as_str())
1931                        }
1932                        ast::Expr::Id(_col_name) => {
1933                            // Unqualified column - would need to resolve based on schema
1934                            // For now, assume it's from the first table if there is one
1935                            if !tables.is_empty() {
1936                                Some(0)
1937                            } else {
1938                                None
1939                            }
1940                        }
1941                        _ => None, // Expression, literal, etc.
1942                    };
1943
1944                    let col_name = alias
1945                        .as_ref()
1946                        // ImplicitColumnName is only for display; skip it
1947                        // so we derive the proper column name below.
1948                        .filter(|a| !matches!(a, ast::As::ImplicitColumnName(_)))
1949                        .map(|a| a.name().as_str().to_string())
1950                        .or_else(|| extract_column_name_from_expr(expr))
1951                        .unwrap_or_else(|| {
1952                            // If we can't extract a simple column name, use the expression itself
1953                            expr.to_string()
1954                        });
1955
1956                    columns.push(ViewColumn {
1957                        table_index: table_index.unwrap_or(usize::MAX),
1958                        column: Column::new_default_text(Some(col_name), "TEXT".to_string(), None),
1959                    });
1960                }
1961                ast::ResultColumn::Star => {
1962                    // For SELECT *, expand to all columns from all tables
1963                    for (table_idx, table) in tables.iter().enumerate() {
1964                        if let Some(table_obj) = schema.get_table(&table.name) {
1965                            for table_column in table_obj.columns() {
1966                                let col_name =
1967                                    table_column.name.clone().unwrap_or_else(|| "?".to_string());
1968
1969                                // Handle duplicate column names by adding suffix
1970                                let final_name =
1971                                    if let Some(count) = column_name_counts.get_mut(&col_name) {
1972                                        *count += 1;
1973                                        format!("{}:{}", col_name, *count - 1)
1974                                    } else {
1975                                        column_name_counts.insert(col_name.clone(), 1);
1976                                        col_name.clone()
1977                                    };
1978
1979                                columns.push(ViewColumn {
1980                                    table_index: table_idx,
1981                                    column: Column::new(
1982                                        Some(final_name),
1983                                        table_column.ty_str.clone(),
1984                                        None,
1985                                        None,
1986                                        table_column.ty(),
1987                                        table_column.collation_opt(),
1988                                        ColDef::default(),
1989                                    ),
1990                                });
1991                            }
1992                        }
1993                    }
1994
1995                    // If no tables, create a placeholder
1996                    if tables.is_empty() {
1997                        columns.push(ViewColumn {
1998                            table_index: usize::MAX,
1999                            column: Column::new_default_text(
2000                                Some("*".to_string()),
2001                                "TEXT".to_string(),
2002                                None,
2003                            ),
2004                        });
2005                    }
2006                }
2007                ast::ResultColumn::TableStar(table_ref) => {
2008                    // For table.*, expand to all columns from the specified table
2009                    let table_name_str = normalize_ident(table_ref.as_str());
2010                    if let Some(table_idx) = find_table_index(&table_name_str) {
2011                        if let Some(table) = schema.get_table(&tables[table_idx].name) {
2012                            for table_column in table.columns() {
2013                                let col_name =
2014                                    table_column.name.clone().unwrap_or_else(|| "?".to_string());
2015
2016                                // Handle duplicate column names by adding suffix
2017                                let final_name =
2018                                    if let Some(count) = column_name_counts.get_mut(&col_name) {
2019                                        *count += 1;
2020                                        format!("{}:{}", col_name, *count - 1)
2021                                    } else {
2022                                        column_name_counts.insert(col_name.clone(), 1);
2023                                        col_name.clone()
2024                                    };
2025
2026                                columns.push(ViewColumn {
2027                                    table_index: table_idx,
2028                                    column: Column::new(
2029                                        Some(final_name),
2030                                        table_column.ty_str.clone(),
2031                                        None,
2032                                        None,
2033                                        table_column.ty(),
2034                                        table_column.collation_opt(),
2035                                        ColDef::default(),
2036                                    ),
2037                                });
2038                            }
2039                        } else {
2040                            // Table not found, create placeholder
2041                            columns.push(ViewColumn {
2042                                table_index: usize::MAX,
2043                                column: Column::new_default_text(
2044                                    Some(format!("{table_name_str}.*")),
2045                                    "TEXT".to_string(),
2046                                    None,
2047                                ),
2048                            });
2049                        }
2050                    }
2051                }
2052            }
2053        }
2054    }
2055
2056    Ok(ViewColumnSchema { tables, columns })
2057}
2058
2059pub fn rewrite_fk_parent_cols_if_self_ref(
2060    clause: &mut ast::ForeignKeyClause,
2061    table: &str,
2062    from: &str,
2063    to: &str,
2064) {
2065    if clause.tbl_name.as_str().eq_ignore_ascii_case(table) {
2066        for c in &mut clause.columns {
2067            if c.col_name.as_str().eq_ignore_ascii_case(from) {
2068                c.col_name = ast::Name::exact(to.to_owned());
2069            }
2070        }
2071    }
2072}
2073
2074/// Returns true if the expression tree references a column whose normalized
2075/// name equals `col_name_normalized`.
2076pub fn check_expr_references_column(expr: &ast::Expr, col_name_normalized: &str) -> bool {
2077    let mut found = false;
2078    // The closure is infallible, so walk_expr cannot fail.
2079    let _ = walk_expr(expr, &mut |e| {
2080        if found {
2081            return Ok(WalkControl::SkipChildren);
2082        }
2083        match e {
2084            ast::Expr::Id(name) | ast::Expr::Name(name) => {
2085                if name.as_str().eq_ignore_ascii_case(col_name_normalized) {
2086                    found = true;
2087                    return Ok(WalkControl::SkipChildren);
2088                }
2089            }
2090            ast::Expr::Qualified(_, col) | ast::Expr::DoublyQualified(_, _, col) => {
2091                if col.as_str().eq_ignore_ascii_case(col_name_normalized) {
2092                    found = true;
2093                    return Ok(WalkControl::SkipChildren);
2094                }
2095            }
2096            _ => {}
2097        }
2098        Ok(WalkControl::Continue)
2099    });
2100    found
2101}
2102
2103/// Rewrite column name references; used in e.g. ALTER TABLE RENAME COLUMN
2104/// to rewrite references to the old column name to the new column name.
2105/// Replaces `Id(old)` and `Name(old)` with `Id(new)`, and updates the
2106/// column name in `Qualified(tbl, old)` references.
2107pub fn rename_identifiers(expr: &mut ast::Expr, from: &str, to: &str) {
2108    // The closure is infallible, so walk_expr_mut cannot fail.
2109    let _ = walk_expr_mut(
2110        expr,
2111        &mut |e: &mut ast::Expr| -> crate::Result<WalkControl> {
2112            match e {
2113                ast::Expr::Id(ref name) | ast::Expr::Name(ref name)
2114                    if name.as_str().eq_ignore_ascii_case(from) =>
2115                {
2116                    *e = ast::Expr::Id(ast::Name::exact(to.to_owned()));
2117                }
2118                ast::Expr::Qualified(ref tbl, ref col_name)
2119                    if col_name.as_str().eq_ignore_ascii_case(from) =>
2120                {
2121                    let tbl = tbl.clone();
2122                    *e = ast::Expr::Qualified(tbl, ast::Name::exact(to.to_owned()));
2123                }
2124                _ => {}
2125            }
2126            Ok(WalkControl::Continue)
2127        },
2128    );
2129}
2130
2131/// Like `rename_identifiers` but scope-aware: only renames qualified refs
2132/// (e.g. `t1.b`) when the qualifier matches the target table or is NEW/OLD
2133/// (which always refer to the trigger's owning table). Unqualified refs
2134/// are renamed unconditionally (caller must ensure they're in the right scope).
2135/// Also enters Subquery/Exists/InSelect expressions that walk_expr_mut skips.
2136pub fn rename_identifiers_scoped(
2137    expr: &mut ast::Expr,
2138    target_table: &str,
2139    trigger_table: &str,
2140    from: &str,
2141    to: &str,
2142) {
2143    rename_identifiers_scoped_inner(expr, target_table, trigger_table, from, to, true, None);
2144}
2145
2146/// Rename column references in a trigger WHEN clause.
2147/// Only renames qualified NEW.col / OLD.col references — bare column names
2148/// are invalid in WHEN clauses per SQLite semantics and must not be renamed.
2149pub fn rename_identifiers_scoped_when_clause(
2150    expr: &mut ast::Expr,
2151    target_table: &str,
2152    trigger_table: &str,
2153    from: &str,
2154    to: &str,
2155) {
2156    rename_identifiers_scoped_inner(expr, target_table, trigger_table, from, to, false, None);
2157}
2158
2159/// Inner implementation with `rename_unqualified` flag controlling whether bare `Expr::Id`
2160/// references should be renamed. When `false`, only qualified refs (table.col, NEW.col, OLD.col)
2161/// are renamed — used when the enclosing SELECT's FROM clause does NOT reference the target table.
2162fn rename_identifiers_scoped_inner(
2163    expr: &mut ast::Expr,
2164    target_table: &str,
2165    trigger_table: &str,
2166    from: &str,
2167    to: &str,
2168    rename_unqualified: bool,
2169    target_qualifiers: Option<&[String]>,
2170) {
2171    let is_renaming_trigger_table = target_table.eq_ignore_ascii_case(trigger_table);
2172    let _ = walk_expr_mut(
2173        expr,
2174        &mut |e: &mut ast::Expr| -> crate::Result<WalkControl> {
2175            match e {
2176                ast::Expr::Subquery(select) | ast::Expr::Exists(select) => {
2177                    let mut quals = target_qualifiers.unwrap_or(&[]).to_vec();
2178                    rewrite_select_column_refs_scoped(
2179                        select,
2180                        target_table,
2181                        trigger_table,
2182                        from,
2183                        to,
2184                        &mut quals,
2185                    );
2186                }
2187                ast::Expr::InSelect { rhs, .. } => {
2188                    let mut quals = target_qualifiers.unwrap_or(&[]).to_vec();
2189                    rewrite_select_column_refs_scoped(
2190                        rhs,
2191                        target_table,
2192                        trigger_table,
2193                        from,
2194                        to,
2195                        &mut quals,
2196                    );
2197                    // lhs will be walked by walk_expr_mut
2198                }
2199                ast::Expr::Id(ref name) | ast::Expr::Name(ref name)
2200                    if rename_unqualified && name.as_str().eq_ignore_ascii_case(from) =>
2201                {
2202                    *e = ast::Expr::Id(ast::Name::exact(to.to_owned()));
2203                }
2204                ast::Expr::Qualified(ref tbl, ref col_name)
2205                    if col_name.as_str().eq_ignore_ascii_case(from) =>
2206                {
2207                    let tbl_norm = normalize_ident(tbl.as_str());
2208                    let should_rename = if tbl_norm == "new" || tbl_norm == "old" {
2209                        is_renaming_trigger_table
2210                    } else {
2211                        target_qualifiers.is_some_and(|qualifiers| qualifiers.contains(&tbl_norm))
2212                            || tbl_norm.eq_ignore_ascii_case(target_table)
2213                    };
2214                    if should_rename {
2215                        let tbl = tbl.clone();
2216                        *e = ast::Expr::Qualified(tbl, ast::Name::exact(to.to_owned()));
2217                    }
2218                }
2219                _ => {}
2220            }
2221            Ok(WalkControl::Continue)
2222        },
2223    );
2224}
2225
2226mod rename_column_view {
2227    use super::*;
2228
2229    #[derive(Debug, Clone)]
2230    pub struct RewrittenView {
2231        pub sql: String,
2232        pub select_stmt: ast::Select,
2233        pub columns: crate::alloc::Vec<Column>,
2234    }
2235
2236    pub fn rewrite_view_sql_for_column_rename(
2237        view_sql: &str,
2238        schema: &Schema,
2239        target_table: &str,
2240        target_db_name: &str,
2241        old_column: &str,
2242        new_column: &str,
2243    ) -> Result<Option<RewrittenView>> {
2244        let mut visiting_views = HashSet::default();
2245        rewrite_view_sql_for_column_rename_inner(
2246            view_sql,
2247            schema,
2248            target_table,
2249            target_db_name,
2250            old_column,
2251            new_column,
2252            &mut visiting_views,
2253        )
2254    }
2255
2256    fn rewrite_view_sql_for_column_rename_inner(
2257        view_sql: &str,
2258        schema: &Schema,
2259        target_table: &str,
2260        target_db_name: &str,
2261        old_column: &str,
2262        new_column: &str,
2263        visiting_views: &mut HashSet<String>,
2264    ) -> Result<Option<RewrittenView>> {
2265        let mut parser = Parser::new(view_sql.as_bytes());
2266        let cmd = parser
2267            .next_cmd()
2268            .map_err(|e| LimboError::ParseError(format!("failed to parse view SQL: {e}")))?;
2269        let Some(ast::Cmd::Stmt(ast::Stmt::CreateView {
2270            temporary,
2271            if_not_exists,
2272            view_name,
2273            columns: view_columns,
2274            mut select,
2275        })) = cmd
2276        else {
2277            return Ok(None);
2278        };
2279
2280        let current_view_name = normalize_ident(view_name.name.as_str());
2281        if !visiting_views.insert(current_view_name.clone()) {
2282            return Err(LimboError::ParseError(format!(
2283                "view {current_view_name} is circularly defined"
2284            )));
2285        }
2286
2287        let rewrite_result = (|| -> Result<Option<RewrittenView>> {
2288            let original_select = select.clone();
2289            let original_columns =
2290                view_columns_from_select(&original_select, schema, &view_columns)?;
2291
2292            let ctx =
2293                ViewRewriteCtx::new(schema, target_table, target_db_name, old_column, new_column);
2294            let sql_changed =
2295                rewrite_view_select_for_column_rename(&mut select, &ctx, &[], visiting_views)?;
2296
2297            let view_column_schema = extract_view_columns(&select, schema)?;
2298            let mut final_columns = apply_view_column_rename(view_column_schema, &ctx);
2299
2300            for (i, indexed_col) in view_columns.iter().enumerate() {
2301                if let Some(col) = final_columns.get_mut(i) {
2302                    col.name = Some(indexed_col.col_name.as_str().to_string());
2303                }
2304            }
2305
2306            let columns_changed = !columns_equivalent(&original_columns, &final_columns);
2307
2308            if !sql_changed && !columns_changed {
2309                return Ok(None);
2310            }
2311
2312            let new_sql = if sql_changed {
2313                let new_stmt = ast::Stmt::CreateView {
2314                    temporary,
2315                    if_not_exists,
2316                    view_name,
2317                    columns: view_columns,
2318                    select: select.clone(),
2319                };
2320                new_stmt.to_string()
2321            } else {
2322                view_sql.to_string()
2323            };
2324
2325            Ok(Some(RewrittenView {
2326                sql: new_sql,
2327                select_stmt: select,
2328                columns: final_columns,
2329            }))
2330        })();
2331        visiting_views.remove(&current_view_name);
2332        rewrite_result
2333    }
2334
2335    fn apply_view_column_rename(
2336        view_columns: ViewColumnSchema,
2337        ctx: &ViewRewriteCtx,
2338    ) -> crate::alloc::Vec<Column> {
2339        let target_norm = ctx.target_table_norm.as_str();
2340        let mut columns = view_columns.columns;
2341
2342        for view_column in &mut columns {
2343            if view_column.table_index == usize::MAX {
2344                continue;
2345            }
2346            let table = &view_columns.tables[view_column.table_index];
2347            if table_name_matches_target(
2348                &table.name,
2349                table.db_name.as_deref(),
2350                target_norm,
2351                &ctx.target_db_norm,
2352            ) {
2353                if let Some(ref mut name) = view_column.column.name {
2354                    if name.as_str().eq_ignore_ascii_case(ctx.old_column) {
2355                        *name = ctx.new_column.to_string();
2356                    }
2357                }
2358            }
2359        }
2360
2361        columns
2362            .into_iter()
2363            .map(|vc| vc.column)
2364            .try_collect()
2365            .expect(crate::alloc::ALLOC_ERR_MSG)
2366    }
2367
2368    fn view_columns_from_select(
2369        select: &ast::Select,
2370        schema: &Schema,
2371        explicit: &[ast::IndexedColumn],
2372    ) -> Result<crate::alloc::Vec<Column>> {
2373        let view_column_schema = extract_view_columns(select, schema)?;
2374        let mut columns = view_column_schema.flat_columns();
2375        for (i, indexed_col) in explicit.iter().enumerate() {
2376            if let Some(col) = columns.get_mut(i) {
2377                col.name = Some(indexed_col.col_name.as_str().to_string());
2378            }
2379        }
2380        Ok(columns)
2381    }
2382
2383    fn columns_equivalent(left: &[Column], right: &[Column]) -> bool {
2384        if left.len() != right.len() {
2385            return false;
2386        }
2387        left.iter().zip(right.iter()).all(|(l, r)| {
2388            let l_name = l.name.as_deref().unwrap_or("");
2389            let r_name = r.name.as_deref().unwrap_or("");
2390            l_name.eq_ignore_ascii_case(r_name)
2391        })
2392    }
2393
2394    #[derive(Clone)]
2395    struct ViewSourceInfo {
2396        qualifiers: Vec<String>,
2397        columns_before: HashSet<String>,
2398        rename_map: HashMap<String, String>,
2399        is_target_table: bool,
2400        db_name: Option<String>,
2401    }
2402
2403    impl ViewSourceInfo {
2404        fn matches_qualifier(&self, qualifier: &str) -> bool {
2405            self.qualifiers.iter().any(|q| q == qualifier)
2406        }
2407    }
2408
2409    fn alias_name(alias: &ast::As) -> &str {
2410        alias.name().as_str()
2411    }
2412
2413    #[derive(Clone)]
2414    struct CteInfo {
2415        columns_before: HashSet<String>,
2416        rename_map: HashMap<String, String>,
2417    }
2418
2419    struct ViewRewriteCtx<'a> {
2420        schema: &'a Schema,
2421        target_table: &'a str,
2422        target_table_norm: String,
2423        target_db_norm: String,
2424        old_column: &'a str,
2425        old_column_norm: String,
2426        new_column: &'a str,
2427    }
2428
2429    impl<'a> ViewRewriteCtx<'a> {
2430        fn new(
2431            schema: &'a Schema,
2432            target_table: &'a str,
2433            target_db_name: &'a str,
2434            old_column: &'a str,
2435            new_column: &'a str,
2436        ) -> Self {
2437            Self {
2438                schema,
2439                target_table,
2440                target_table_norm: normalize_ident(target_table),
2441                target_db_norm: normalize_ident(target_db_name),
2442                old_column,
2443                old_column_norm: normalize_ident(old_column),
2444                new_column,
2445            }
2446        }
2447    }
2448
2449    fn rewrite_view_select_for_column_rename(
2450        select: &mut ast::Select,
2451        ctx: &ViewRewriteCtx,
2452        outer_scopes: &[&[ViewSourceInfo]],
2453        visiting_views: &mut HashSet<String>,
2454    ) -> Result<bool> {
2455        let mut changed = false;
2456
2457        let mut ctes: HashMap<String, CteInfo> = HashMap::default();
2458        if let Some(ref mut with_clause) = select.with {
2459            for cte in &mut with_clause.ctes {
2460                let mut before_cols = select_output_columns(&cte.select, ctx, false)?;
2461                apply_explicit_column_names(&mut before_cols, &cte.columns);
2462                let cte_changed = rewrite_view_select_for_column_rename(
2463                    &mut cte.select,
2464                    ctx,
2465                    &[],
2466                    visiting_views,
2467                )?;
2468                changed |= cte_changed;
2469                let mut after_cols = select_output_columns(&cte.select, ctx, true)?;
2470                apply_explicit_column_names(&mut after_cols, &cte.columns);
2471                let rename_map = build_rename_map(&before_cols, &after_cols, &ctx.old_column_norm);
2472                ctes.insert(
2473                    normalize_ident(cte.tbl_name.as_str()),
2474                    CteInfo {
2475                        columns_before: before_cols
2476                            .into_iter()
2477                            .map(|c| normalize_ident(&c))
2478                            .collect(),
2479                        rename_map,
2480                    },
2481                );
2482            }
2483        }
2484
2485        let mut scope_sources = rewrite_one_select_for_column_rename(
2486            &mut select.body.select,
2487            ctx,
2488            &ctes,
2489            outer_scopes,
2490            &mut changed,
2491            visiting_views,
2492        )?;
2493
2494        for compound in &mut select.body.compounds {
2495            let compound_sources = rewrite_one_select_for_column_rename(
2496                &mut compound.select,
2497                ctx,
2498                &ctes,
2499                outer_scopes,
2500                &mut changed,
2501                visiting_views,
2502            )?;
2503            if scope_sources.is_none() {
2504                scope_sources = compound_sources;
2505            }
2506        }
2507
2508        if let Some(ref sources) = scope_sources {
2509            for sorted_col in &mut select.order_by {
2510                rewrite_expr_in_scope(
2511                    &mut sorted_col.expr,
2512                    sources,
2513                    outer_scopes,
2514                    ctx,
2515                    &mut changed,
2516                    visiting_views,
2517                )?;
2518            }
2519            if let Some(ref mut limit) = select.limit {
2520                rewrite_expr_in_scope(
2521                    &mut limit.expr,
2522                    sources,
2523                    outer_scopes,
2524                    ctx,
2525                    &mut changed,
2526                    visiting_views,
2527                )?;
2528                if let Some(ref mut offset) = limit.offset {
2529                    rewrite_expr_in_scope(
2530                        offset,
2531                        sources,
2532                        outer_scopes,
2533                        ctx,
2534                        &mut changed,
2535                        visiting_views,
2536                    )?;
2537                }
2538            }
2539        }
2540
2541        Ok(changed)
2542    }
2543
2544    fn rewrite_one_select_for_column_rename(
2545        one_select: &mut ast::OneSelect,
2546        ctx: &ViewRewriteCtx,
2547        ctes: &HashMap<String, CteInfo>,
2548        outer_scopes: &[&[ViewSourceInfo]],
2549        changed: &mut bool,
2550        visiting_views: &mut HashSet<String>,
2551    ) -> Result<Option<Vec<ViewSourceInfo>>> {
2552        match one_select {
2553            ast::OneSelect::Select {
2554                columns,
2555                from,
2556                where_clause,
2557                group_by,
2558                window_clause,
2559                ..
2560            } => {
2561                let sources = if let Some(ref mut from_clause) = from {
2562                    rewrite_from_clause_for_column_rename(
2563                        from_clause,
2564                        ctx,
2565                        ctes,
2566                        outer_scopes,
2567                        changed,
2568                        visiting_views,
2569                    )?
2570                } else {
2571                    Vec::new()
2572                };
2573
2574                for col in columns {
2575                    if let ast::ResultColumn::Expr(expr, _) = col {
2576                        rewrite_expr_in_scope(
2577                            expr,
2578                            &sources,
2579                            outer_scopes,
2580                            ctx,
2581                            changed,
2582                            visiting_views,
2583                        )?;
2584                    }
2585                }
2586
2587                if let Some(ref mut where_expr) = where_clause {
2588                    rewrite_expr_in_scope(
2589                        where_expr,
2590                        &sources,
2591                        outer_scopes,
2592                        ctx,
2593                        changed,
2594                        visiting_views,
2595                    )?;
2596                }
2597
2598                if let Some(ref mut group_by) = group_by {
2599                    for expr in &mut group_by.exprs {
2600                        rewrite_expr_in_scope(
2601                            expr,
2602                            &sources,
2603                            outer_scopes,
2604                            ctx,
2605                            changed,
2606                            visiting_views,
2607                        )?;
2608                    }
2609                    if let Some(ref mut having_expr) = group_by.having {
2610                        rewrite_expr_in_scope(
2611                            having_expr,
2612                            &sources,
2613                            outer_scopes,
2614                            ctx,
2615                            changed,
2616                            visiting_views,
2617                        )?;
2618                    }
2619                }
2620
2621                for window_def in window_clause {
2622                    for expr in &mut window_def.window.partition_by {
2623                        rewrite_expr_in_scope(
2624                            expr,
2625                            &sources,
2626                            outer_scopes,
2627                            ctx,
2628                            changed,
2629                            visiting_views,
2630                        )?;
2631                    }
2632                    for sorted in &mut window_def.window.order_by {
2633                        rewrite_expr_in_scope(
2634                            &mut sorted.expr,
2635                            &sources,
2636                            outer_scopes,
2637                            ctx,
2638                            changed,
2639                            visiting_views,
2640                        )?;
2641                    }
2642                }
2643
2644                Ok(Some(sources))
2645            }
2646            ast::OneSelect::Values(values) => {
2647                for row in values {
2648                    for expr in row {
2649                        rewrite_expr_in_scope(
2650                            expr,
2651                            &[],
2652                            outer_scopes,
2653                            ctx,
2654                            changed,
2655                            visiting_views,
2656                        )?;
2657                    }
2658                }
2659                Ok(None)
2660            }
2661        }
2662    }
2663
2664    fn rewrite_from_clause_for_column_rename(
2665        from_clause: &mut ast::FromClause,
2666        ctx: &ViewRewriteCtx,
2667        ctes: &HashMap<String, CteInfo>,
2668        outer_scopes: &[&[ViewSourceInfo]],
2669        changed: &mut bool,
2670        visiting_views: &mut HashSet<String>,
2671    ) -> Result<Vec<ViewSourceInfo>> {
2672        let mut sources = Vec::new();
2673        let first_source = rewrite_select_table_for_column_rename(
2674            &mut from_clause.select,
2675            &[],
2676            ctx,
2677            ctes,
2678            outer_scopes,
2679            changed,
2680            visiting_views,
2681        )?;
2682        sources.push(first_source);
2683
2684        for join in &mut from_clause.joins {
2685            let right_source = rewrite_select_table_for_column_rename(
2686                &mut join.table,
2687                &sources,
2688                ctx,
2689                ctes,
2690                outer_scopes,
2691                changed,
2692                visiting_views,
2693            )?;
2694            sources.push(right_source);
2695            let (right_source, left_sources) = sources
2696                .split_last()
2697                .expect("sources should include the right-hand side join source");
2698            if let Some(ref mut constraint) = join.constraint {
2699                match constraint {
2700                    ast::JoinConstraint::On(expr) => {
2701                        rewrite_expr_in_scope(
2702                            expr,
2703                            &sources,
2704                            outer_scopes,
2705                            ctx,
2706                            changed,
2707                            visiting_views,
2708                        )?;
2709                    }
2710                    ast::JoinConstraint::Using(cols) => {
2711                        *changed |= rewrite_using_columns(
2712                            cols,
2713                            left_sources,
2714                            right_source,
2715                            &ctx.old_column_norm,
2716                            ctx.new_column,
2717                        );
2718                    }
2719                }
2720            }
2721        }
2722
2723        Ok(sources)
2724    }
2725
2726    fn rewrite_select_table_for_column_rename(
2727        select_table: &mut ast::SelectTable,
2728        visible_sources: &[ViewSourceInfo],
2729        ctx: &ViewRewriteCtx,
2730        ctes: &HashMap<String, CteInfo>,
2731        outer_scopes: &[&[ViewSourceInfo]],
2732        changed: &mut bool,
2733        visiting_views: &mut HashSet<String>,
2734    ) -> Result<ViewSourceInfo> {
2735        match select_table {
2736            ast::SelectTable::Table(tbl_name, alias, _) => {
2737                let table_name_norm = normalize_ident(tbl_name.name.as_str());
2738                let table_db_norm = tbl_name
2739                    .db_name
2740                    .as_ref()
2741                    .map(|db| normalize_ident(db.as_str()));
2742                let mut qualifiers = Vec::new();
2743                qualifiers.push(table_name_norm.clone());
2744                if let Some(ref alias) = alias {
2745                    qualifiers.push(normalize_ident(alias_name(alias)));
2746                }
2747                if table_db_norm.is_none() {
2748                    if let Some(cte) = ctes.get(&table_name_norm) {
2749                        return Ok(ViewSourceInfo {
2750                            qualifiers,
2751                            columns_before: cte.columns_before.clone(),
2752                            rename_map: cte.rename_map.clone(),
2753                            is_target_table: false,
2754                            db_name: None,
2755                        });
2756                    }
2757                }
2758
2759                let is_local = table_db_norm
2760                    .as_deref()
2761                    .is_none_or(|db| db == ctx.target_db_norm);
2762
2763                if is_local {
2764                    if let Some(view) = ctx.schema.views.get(&table_name_norm) {
2765                        let columns_before = view
2766                            .columns
2767                            .iter()
2768                            .filter_map(|col| col.name.clone())
2769                            .map(|name| normalize_ident(&name))
2770                            .collect();
2771
2772                        let mut rename_map = HashMap::default();
2773                        if let Some(rewritten) = rewrite_view_sql_for_column_rename_inner(
2774                            &view.sql,
2775                            ctx.schema,
2776                            ctx.target_table,
2777                            &ctx.target_db_norm,
2778                            ctx.old_column,
2779                            ctx.new_column,
2780                            visiting_views,
2781                        )? {
2782                            rename_map = build_rename_map_from_columns(
2783                                &view.columns,
2784                                &rewritten.columns,
2785                                &ctx.old_column_norm,
2786                            );
2787                        }
2788
2789                        return Ok(ViewSourceInfo {
2790                            qualifiers,
2791                            columns_before,
2792                            rename_map,
2793                            is_target_table: false,
2794                            db_name: table_db_norm,
2795                        });
2796                    }
2797                }
2798                let is_target = table_name_matches_target(
2799                    &table_name_norm,
2800                    table_db_norm.as_deref(),
2801                    &ctx.target_table_norm,
2802                    &ctx.target_db_norm,
2803                );
2804                let columns_before = if is_local {
2805                    table_source_columns(ctx.schema, &table_name_norm)
2806                        .unwrap_or_default()
2807                        .into_iter()
2808                        .map(|c| normalize_ident(&c))
2809                        .collect()
2810                } else {
2811                    HashSet::default()
2812                };
2813
2814                Ok(ViewSourceInfo {
2815                    qualifiers,
2816                    columns_before,
2817                    rename_map: HashMap::default(),
2818                    is_target_table: is_target,
2819                    db_name: table_db_norm,
2820                })
2821            }
2822            ast::SelectTable::Select(select, alias) => {
2823                let before_cols = select_output_columns(select, ctx, false)?;
2824                *changed |=
2825                    rewrite_view_select_for_column_rename(select, ctx, &[], visiting_views)?;
2826                let after_cols = select_output_columns(select, ctx, true)?;
2827                let rename_map = build_rename_map(&before_cols, &after_cols, &ctx.old_column_norm);
2828                let qualifiers = alias
2829                    .as_ref()
2830                    .map(|alias| vec![normalize_ident(alias_name(alias))])
2831                    .unwrap_or_default();
2832                Ok(ViewSourceInfo {
2833                    qualifiers,
2834                    columns_before: before_cols
2835                        .into_iter()
2836                        .map(|c| normalize_ident(&c))
2837                        .collect(),
2838                    rename_map,
2839                    is_target_table: false,
2840                    db_name: None,
2841                })
2842            }
2843            ast::SelectTable::Sub(from_clause, alias) => {
2844                let before_cols = from_clause_output_columns(from_clause, ctx, false)?;
2845                let _ = rewrite_from_clause_for_column_rename(
2846                    from_clause,
2847                    ctx,
2848                    ctes,
2849                    outer_scopes,
2850                    changed,
2851                    visiting_views,
2852                )?;
2853                let after_cols = from_clause_output_columns(from_clause, ctx, true)?;
2854                let rename_map = build_rename_map(&before_cols, &after_cols, &ctx.old_column_norm);
2855                let qualifiers = alias
2856                    .as_ref()
2857                    .map(|alias| vec![normalize_ident(alias_name(alias))])
2858                    .unwrap_or_default();
2859                Ok(ViewSourceInfo {
2860                    qualifiers,
2861                    columns_before: before_cols
2862                        .into_iter()
2863                        .map(|c| normalize_ident(&c))
2864                        .collect(),
2865                    rename_map,
2866                    is_target_table: false,
2867                    db_name: None,
2868                })
2869            }
2870            ast::SelectTable::TableCall(_, args, alias) => {
2871                for arg in args {
2872                    rewrite_expr_in_scope(
2873                        arg,
2874                        visible_sources,
2875                        outer_scopes,
2876                        ctx,
2877                        changed,
2878                        visiting_views,
2879                    )?;
2880                }
2881                let qualifiers = alias
2882                    .as_ref()
2883                    .map(|alias| vec![normalize_ident(alias_name(alias))])
2884                    .unwrap_or_default();
2885                Ok(ViewSourceInfo {
2886                    qualifiers,
2887                    columns_before: HashSet::default(),
2888                    rename_map: HashMap::default(),
2889                    is_target_table: false,
2890                    db_name: None,
2891                })
2892            }
2893        }
2894    }
2895
2896    fn rewrite_expr_in_scope(
2897        expr: &mut ast::Expr,
2898        sources: &[ViewSourceInfo],
2899        outer_scopes: &[&[ViewSourceInfo]],
2900        ctx: &ViewRewriteCtx,
2901        changed: &mut bool,
2902        visiting_views: &mut HashSet<String>,
2903    ) -> Result<()> {
2904        let mut outer_scopes_for_subqueries: Vec<&[ViewSourceInfo]> =
2905            Vec::with_capacity(outer_scopes.len() + 1);
2906        if !sources.is_empty() {
2907            outer_scopes_for_subqueries.push(sources);
2908        }
2909        outer_scopes_for_subqueries.extend_from_slice(outer_scopes);
2910        walk_expr_mut(expr, &mut |e: &mut ast::Expr| -> Result<WalkControl> {
2911            if rewrite_expr_column_ref_view(
2912                e,
2913                sources,
2914                outer_scopes,
2915                &ctx.target_db_norm,
2916                &ctx.old_column_norm,
2917                ctx.new_column,
2918            ) {
2919                *changed = true;
2920            }
2921            match e {
2922                ast::Expr::Subquery(select) | ast::Expr::Exists(select) => {
2923                    if rewrite_view_select_for_column_rename(
2924                        select,
2925                        ctx,
2926                        outer_scopes_for_subqueries.as_slice(),
2927                        visiting_views,
2928                    )? {
2929                        *changed = true;
2930                    }
2931                }
2932                ast::Expr::InSelect { rhs, .. } => {
2933                    if rewrite_view_select_for_column_rename(
2934                        rhs,
2935                        ctx,
2936                        outer_scopes_for_subqueries.as_slice(),
2937                        visiting_views,
2938                    )? {
2939                        *changed = true;
2940                    }
2941                }
2942                _ => {}
2943            }
2944            Ok(WalkControl::Continue)
2945        })?;
2946        Ok(())
2947    }
2948
2949    fn rewrite_expr_column_ref_view(
2950        expr: &mut ast::Expr,
2951        sources: &[ViewSourceInfo],
2952        outer_scopes: &[&[ViewSourceInfo]],
2953        target_db_norm: &str,
2954        old_column_norm: &str,
2955        new_column: &str,
2956    ) -> bool {
2957        let apply_rename = |source: &ViewSourceInfo, set_name: &mut dyn FnMut(String)| {
2958            if source.is_target_table {
2959                set_name(new_column.to_string());
2960                return true;
2961            }
2962            if let Some(mapped) = source.rename_map.get(old_column_norm) {
2963                set_name(mapped.to_string());
2964                return true;
2965            }
2966            false
2967        };
2968
2969        match expr {
2970            ast::Expr::Qualified(ns, col) => {
2971                let ns_norm = normalize_ident(ns.as_str());
2972                if !col.as_str().eq_ignore_ascii_case(old_column_norm) {
2973                    return false;
2974                }
2975                let (source, local_ambiguous) =
2976                    resolve_qualified(sources, &ns_norm, target_db_norm);
2977                if let Some(source) = source {
2978                    return apply_rename(source, &mut |name| {
2979                        *col = ast::Name::exact(name);
2980                    });
2981                }
2982                if local_ambiguous {
2983                    return false;
2984                }
2985                for scope in outer_scopes {
2986                    let (source, ambiguous) = resolve_qualified(scope, &ns_norm, target_db_norm);
2987                    if let Some(source) = source {
2988                        return apply_rename(source, &mut |name| {
2989                            *col = ast::Name::exact(name);
2990                        });
2991                    }
2992                    if ambiguous {
2993                        return false;
2994                    }
2995                }
2996            }
2997            ast::Expr::DoublyQualified(schema, ns, col) => {
2998                let schema_norm = normalize_ident(schema.as_str());
2999                if schema_norm != target_db_norm {
3000                    return false;
3001                }
3002                let ns_norm = normalize_ident(ns.as_str());
3003                if !col.as_str().eq_ignore_ascii_case(old_column_norm) {
3004                    return false;
3005                }
3006                let (source, local_ambiguous) = resolve_qualified(sources, &ns_norm, &schema_norm);
3007                if let Some(source) = source {
3008                    return apply_rename(source, &mut |name| {
3009                        *col = ast::Name::exact(name);
3010                    });
3011                }
3012                if local_ambiguous {
3013                    return false;
3014                }
3015                for scope in outer_scopes {
3016                    let (source, ambiguous) = resolve_qualified(scope, &ns_norm, &schema_norm);
3017                    if let Some(source) = source {
3018                        return apply_rename(source, &mut |name| {
3019                            *col = ast::Name::exact(name);
3020                        });
3021                    }
3022                    if ambiguous {
3023                        return false;
3024                    }
3025                }
3026            }
3027            ast::Expr::Id(col) | ast::Expr::Name(col) => {
3028                if !col.as_str().eq_ignore_ascii_case(old_column_norm) {
3029                    return false;
3030                }
3031                let col_norm = normalize_ident(col.as_str());
3032                let (source, local_ambiguous) = resolve_unqualified(sources, &col_norm);
3033                if let Some(source) = source {
3034                    return apply_rename(source, &mut |name| {
3035                        *expr = ast::Expr::Id(ast::Name::exact(name));
3036                    });
3037                }
3038                if local_ambiguous {
3039                    return false;
3040                }
3041                for scope in outer_scopes {
3042                    let (source, ambiguous) = resolve_unqualified(scope, &col_norm);
3043                    if let Some(source) = source {
3044                        return apply_rename(source, &mut |name| {
3045                            *expr = ast::Expr::Id(ast::Name::exact(name));
3046                        });
3047                    }
3048                    if ambiguous {
3049                        return false;
3050                    }
3051                }
3052            }
3053            _ => {}
3054        }
3055        false
3056    }
3057
3058    fn resolve_unqualified<'a>(
3059        candidates: &'a [ViewSourceInfo],
3060        old_column_norm: &str,
3061    ) -> (Option<&'a ViewSourceInfo>, bool) {
3062        let mut matches = candidates
3063            .iter()
3064            .filter(|s| s.columns_before.contains(old_column_norm));
3065        let Some(first) = matches.next() else {
3066            return (None, false);
3067        };
3068        if matches.next().is_some() {
3069            return (None, true);
3070        }
3071        (Some(first), false)
3072    }
3073
3074    fn resolve_qualified<'a>(
3075        candidates: &'a [ViewSourceInfo],
3076        qualifier: &str,
3077        target_db_norm: &str,
3078    ) -> (Option<&'a ViewSourceInfo>, bool) {
3079        let mut matches = candidates.iter().filter(|s| {
3080            s.matches_qualifier(qualifier)
3081                && s.db_name.as_deref().is_none_or(|db| db == target_db_norm)
3082        });
3083        let Some(first) = matches.next() else {
3084            return (None, false);
3085        };
3086        if matches.next().is_some() {
3087            return (None, true);
3088        }
3089        (Some(first), false)
3090    }
3091
3092    fn rewrite_using_columns(
3093        cols: &mut [ast::Name],
3094        left_sources: &[ViewSourceInfo],
3095        right: &ViewSourceInfo,
3096        old_column_norm: &str,
3097        new_column: &str,
3098    ) -> bool {
3099        let mut changed = false;
3100        let left_map = left_sources
3101            .iter()
3102            .find_map(|source| source.rename_map.get(old_column_norm));
3103        let left_has_target = left_sources.iter().any(|source| source.is_target_table);
3104        let right_map = right.rename_map.get(old_column_norm);
3105        let should_rename =
3106            left_has_target || right.is_target_table || left_map.is_some() || right_map.is_some();
3107        if !should_rename {
3108            return false;
3109        }
3110        let replacement = left_map
3111            .or(right_map)
3112            .map(|s| s.as_str())
3113            .unwrap_or(new_column);
3114
3115        for col in cols {
3116            if col.as_str().eq_ignore_ascii_case(old_column_norm) {
3117                *col = ast::Name::exact(replacement.to_string());
3118                changed = true;
3119            }
3120        }
3121        changed
3122    }
3123
3124    fn select_output_columns(
3125        select: &ast::Select,
3126        ctx: &ViewRewriteCtx,
3127        apply_rename: bool,
3128    ) -> Result<Vec<String>> {
3129        let view_columns = extract_view_columns(select, ctx.schema)?;
3130        let mut columns = view_columns.columns;
3131        if apply_rename {
3132            let target_norm = ctx.target_table_norm.as_str();
3133            for view_column in &mut columns {
3134                if view_column.table_index == usize::MAX {
3135                    continue;
3136                }
3137                let table = &view_columns.tables[view_column.table_index];
3138                if table_name_matches_target(
3139                    &table.name,
3140                    table.db_name.as_deref(),
3141                    target_norm,
3142                    &ctx.target_db_norm,
3143                ) {
3144                    if let Some(ref mut name) = view_column.column.name {
3145                        if name.as_str().eq_ignore_ascii_case(ctx.old_column) {
3146                            *name = ctx.new_column.to_string();
3147                        }
3148                    }
3149                }
3150            }
3151        }
3152
3153        Ok(columns
3154            .into_iter()
3155            .map(|vc| vc.column.name.unwrap_or_else(|| "?".to_string()))
3156            .collect())
3157    }
3158
3159    fn apply_explicit_column_names(columns: &mut [String], explicit: &[ast::IndexedColumn]) {
3160        for (i, indexed_col) in explicit.iter().enumerate() {
3161            if let Some(col) = columns.get_mut(i) {
3162                *col = indexed_col.col_name.as_str().to_string();
3163            }
3164        }
3165    }
3166
3167    fn from_clause_output_columns(
3168        from_clause: &ast::FromClause,
3169        ctx: &ViewRewriteCtx,
3170        apply_rename: bool,
3171    ) -> Result<Vec<String>> {
3172        let dummy_select = ast::Select {
3173            with: None,
3174            body: ast::SelectBody {
3175                select: ast::OneSelect::Select {
3176                    distinctness: None,
3177                    columns: vec![ast::ResultColumn::Star],
3178                    from: Some(from_clause.clone()),
3179                    where_clause: None,
3180                    group_by: None,
3181                    window_clause: Vec::new(),
3182                },
3183                compounds: Vec::new(),
3184            },
3185            order_by: Vec::new(),
3186            limit: None,
3187        };
3188        select_output_columns(&dummy_select, ctx, apply_rename)
3189    }
3190
3191    fn build_rename_map(
3192        before_cols: &[String],
3193        after_cols: &[String],
3194        old_column_norm: &str,
3195    ) -> HashMap<String, String> {
3196        let mut map = HashMap::default();
3197        for (before, after) in before_cols.iter().zip(after_cols.iter()) {
3198            if before.as_str().eq_ignore_ascii_case(old_column_norm)
3199                && !after.as_str().eq_ignore_ascii_case(before.as_str())
3200            {
3201                map.insert(old_column_norm.to_string(), after.to_string());
3202            }
3203        }
3204        map
3205    }
3206
3207    fn build_rename_map_from_columns(
3208        before_cols: &[Column],
3209        after_cols: &[Column],
3210        old_column_norm: &str,
3211    ) -> HashMap<String, String> {
3212        if before_cols.len() != after_cols.len() {
3213            return HashMap::default();
3214        }
3215        let mut map = HashMap::default();
3216        for (before, after) in before_cols.iter().zip(after_cols.iter()) {
3217            let Some(before_name) = before.name.as_ref() else {
3218                continue;
3219            };
3220            let Some(after_name) = after.name.as_ref() else {
3221                continue;
3222            };
3223            if before_name.as_str().eq_ignore_ascii_case(old_column_norm)
3224                && !after_name
3225                    .as_str()
3226                    .eq_ignore_ascii_case(before_name.as_str())
3227            {
3228                map.insert(old_column_norm.to_string(), after_name.to_string());
3229            }
3230        }
3231        map
3232    }
3233
3234    fn table_name_matches_target(
3235        table_name: &str,
3236        table_db: Option<&str>,
3237        target_table_norm: &str,
3238        target_db_norm: &str,
3239    ) -> bool {
3240        if !table_name.eq_ignore_ascii_case(target_table_norm) {
3241            return false;
3242        }
3243        match table_db {
3244            None => true,
3245            Some(db) => db.eq_ignore_ascii_case(target_db_norm),
3246        }
3247    }
3248
3249    fn table_source_columns(schema: &Schema, table_name: &str) -> Option<Vec<String>> {
3250        if let Some(table) = schema.get_table(table_name) {
3251            return Some(
3252                table
3253                    .columns()
3254                    .iter()
3255                    .filter_map(|col| col.name.clone())
3256                    .collect(),
3257            );
3258        }
3259        let table_norm = normalize_ident(table_name);
3260        if let Some(view) = schema.views.get(&table_norm) {
3261            return Some(
3262                view.columns
3263                    .iter()
3264                    .filter_map(|col| col.name.clone())
3265                    .collect(),
3266            );
3267        }
3268        None
3269    }
3270}
3271
3272pub use rename_column_view::{rewrite_view_sql_for_column_rename, RewrittenView};
3273
3274/// Rewrite table-qualified column references in a CHECK constraint expression,
3275/// replacing the table name from `from` to `to`. For example, `t1.a > 0` becomes
3276/// `t2.a > 0` when renaming t1 to t2. This matches SQLite 3.49.1+ behavior which
3277/// rewrites qualified refs during ALTER TABLE RENAME instead of rejecting them.
3278pub fn rewrite_check_expr_table_refs(expr: &mut ast::Expr, from: &str, to: &str) {
3279    let _ = walk_expr_mut(
3280        expr,
3281        &mut |e: &mut ast::Expr| -> crate::Result<WalkControl> {
3282            match e {
3283                ast::Expr::Qualified(tbl, col) => {
3284                    if tbl.as_str().eq_ignore_ascii_case(from) {
3285                        let col = col.clone();
3286                        *e = ast::Expr::Qualified(ast::Name::exact(to.to_owned()), col);
3287                    }
3288                }
3289                ast::Expr::Exists(select) | ast::Expr::Subquery(select) => {
3290                    rewrite_select_table_refs(select, from, to);
3291                }
3292                ast::Expr::InSelect { rhs, .. } => {
3293                    rewrite_select_table_refs(rhs, from, to);
3294                }
3295                ast::Expr::InTable { rhs, .. } => {
3296                    if rhs.name.as_str().eq_ignore_ascii_case(from) {
3297                        rhs.name = ast::Name::exact(to.to_owned());
3298                    }
3299                }
3300                _ => {}
3301            }
3302            Ok(WalkControl::Continue)
3303        },
3304    );
3305}
3306
3307/// Update a column-level REFERENCES <tbl>(col,...) constraint
3308pub fn rewrite_column_references_if_needed(
3309    col: &mut ast::ColumnDefinition,
3310    table: &str,
3311    from: &str,
3312    to: &str,
3313) -> Result<()> {
3314    for cc in &mut col.constraints {
3315        match &mut cc.constraint {
3316            ast::ColumnConstraint::ForeignKey { clause, .. } => {
3317                rewrite_fk_parent_cols_if_self_ref(clause, table, from, to);
3318            }
3319            ast::ColumnConstraint::Check(expr) => {
3320                rename_identifiers(expr, from, to);
3321            }
3322            ast::ColumnConstraint::Generated { expr, .. } => {
3323                rename_identifiers(expr, from, to);
3324            }
3325            _ => {}
3326        }
3327    }
3328    Ok(())
3329}
3330
3331/// For a column definition like `parent_id REFERENCES parent(old_col)`, update
3332/// the referenced parent column names when another table renames
3333/// `old_col -> new_col`.
3334pub fn rewrite_column_level_fk_parent_columns_if_needed(
3335    col: &mut ast::ColumnDefinition,
3336    table: &str,
3337    from: &str,
3338    to: &str,
3339) {
3340    for cc in &mut col.constraints {
3341        if let ast::ColumnConstraint::ForeignKey { clause, .. } = &mut cc.constraint {
3342            rewrite_fk_parent_cols_if_self_ref(clause, table, from, to);
3343        }
3344    }
3345}
3346
3347/// If a FK REFERENCES targets `old_tbl`, change it to `new_tbl`
3348pub fn rewrite_fk_parent_table_if_needed(
3349    clause: &mut ast::ForeignKeyClause,
3350    old_tbl: &str,
3351    new_tbl: &str,
3352) -> bool {
3353    if clause.tbl_name.as_str().eq_ignore_ascii_case(old_tbl) {
3354        clause.tbl_name = ast::Name::exact(new_tbl.to_owned());
3355        return true;
3356    }
3357    false
3358}
3359
3360/// For inline REFERENCES tbl in a column definition.
3361pub fn rewrite_inline_col_fk_target_if_needed(
3362    col: &mut ast::ColumnDefinition,
3363    old_tbl: &str,
3364    new_tbl: &str,
3365) -> bool {
3366    let mut changed = false;
3367    for cc in &mut col.constraints {
3368        if let ast::NamedColumnConstraint {
3369            constraint: ast::ColumnConstraint::ForeignKey { clause, .. },
3370            ..
3371        } = cc
3372        {
3373            changed |= rewrite_fk_parent_table_if_needed(clause, old_tbl, new_tbl);
3374        }
3375    }
3376    changed
3377}
3378
3379/// Rewrite table name references inside a trigger's body commands for ALTER TABLE RENAME.
3380/// Updates tbl_name fields in INSERT/UPDATE/DELETE commands and table references
3381/// in FROM clauses and qualified expressions throughout the trigger body.
3382pub fn rewrite_trigger_cmd_table_refs(cmd: &mut ast::TriggerCmd, old_tbl: &str, new_tbl: &str) {
3383    match cmd {
3384        ast::TriggerCmd::Update {
3385            tbl_name,
3386            sets,
3387            from,
3388            where_clause,
3389            ..
3390        } => {
3391            if tbl_name.as_str().eq_ignore_ascii_case(old_tbl) {
3392                *tbl_name = ast::Name::exact(new_tbl.to_owned());
3393            }
3394            for set in sets {
3395                rewrite_check_expr_table_refs(&mut set.expr, old_tbl, new_tbl);
3396            }
3397            if let Some(ref mut from) = from {
3398                rewrite_from_clause_table_refs(from, old_tbl, new_tbl);
3399            }
3400            if let Some(ref mut wc) = where_clause {
3401                rewrite_check_expr_table_refs(wc, old_tbl, new_tbl);
3402            }
3403        }
3404        ast::TriggerCmd::Insert {
3405            tbl_name,
3406            select,
3407            upsert,
3408            ..
3409        } => {
3410            if tbl_name.as_str().eq_ignore_ascii_case(old_tbl) {
3411                *tbl_name = ast::Name::exact(new_tbl.to_owned());
3412            }
3413            rewrite_select_table_refs(select, old_tbl, new_tbl);
3414            if let Some(ref mut upsert) = upsert {
3415                rewrite_upsert_table_refs(upsert, old_tbl, new_tbl);
3416            }
3417        }
3418        ast::TriggerCmd::Delete {
3419            tbl_name,
3420            where_clause,
3421        } => {
3422            if tbl_name.as_str().eq_ignore_ascii_case(old_tbl) {
3423                *tbl_name = ast::Name::exact(new_tbl.to_owned());
3424            }
3425            if let Some(ref mut wc) = where_clause {
3426                rewrite_check_expr_table_refs(wc, old_tbl, new_tbl);
3427            }
3428        }
3429        ast::TriggerCmd::Select(select) => {
3430            rewrite_select_table_refs(select, old_tbl, new_tbl);
3431        }
3432    }
3433}
3434
3435/// Collect the names by which each result column of a single SELECT arm can be
3436/// referenced from ORDER BY. Mirrors the set of identifiers SQLite would
3437/// consider when resolving a bare ORDER BY identifier against that arm's
3438/// output column list.
3439pub(crate) fn output_column_aliases(one_select: &ast::OneSelect) -> Vec<String> {
3440    let ast::OneSelect::Select { columns, .. } = one_select else {
3441        return Vec::new();
3442    };
3443    columns
3444        .iter()
3445        .filter_map(|col| match col {
3446            ast::ResultColumn::Expr(_, Some(alias)) => Some(normalize_ident(alias.name().as_str())),
3447            ast::ResultColumn::Expr(expr, None) => match expr.as_ref() {
3448                ast::Expr::Id(name) | ast::Expr::Name(name) => Some(normalize_ident(name.as_str())),
3449                ast::Expr::Qualified(_, col) | ast::Expr::DoublyQualified(_, _, col) => {
3450                    Some(normalize_ident(col.as_str()))
3451                }
3452                _ => None,
3453            },
3454            _ => None,
3455        })
3456        .collect()
3457}
3458
3459/// Returns true when `expr` is a bare identifier that names the renamed column
3460/// and resolves to one of the SELECT's output column aliases (in the body or
3461/// any compound arm). Per SQLite's ORDER BY resolution rules, such a reference
3462/// is to the alias label, not to a FROM-clause column, so a column rename
3463/// must leave it alone.
3464///
3465/// Cheap in the common case: short-circuits before scanning aliases when the
3466/// expr is not a bare identifier or does not match `old_col`.
3467pub(crate) fn is_order_by_alias_ref(
3468    body: &ast::SelectBody,
3469    expr: &ast::Expr,
3470    old_col: &str,
3471) -> bool {
3472    let name = match expr {
3473        ast::Expr::Id(n) | ast::Expr::Name(n) => n.as_str(),
3474        _ => return false,
3475    };
3476    if !name.eq_ignore_ascii_case(old_col) {
3477        return false;
3478    }
3479    one_select_has_explicit_alias(&body.select, name)
3480        || body
3481            .compounds
3482            .iter()
3483            .any(|c| one_select_has_explicit_alias(&c.select, name))
3484}
3485
3486// Only user-provided aliases (`AS x` or elided form) block ORDER BY rewriting.
3487// `As::ImplicitColumnName` is synthesized by the parser from the original
3488// expression text to label unaliased columns and is decoupled from the
3489// underlying expression — it must not be treated as an alias for rename
3490// purposes, since SQLite rewrites such ORDER BY refs along with the column.
3491fn one_select_has_explicit_alias(one_select: &ast::OneSelect, name: &str) -> bool {
3492    let ast::OneSelect::Select { columns, .. } = one_select else {
3493        return false;
3494    };
3495    columns.iter().any(|col| match col {
3496        ast::ResultColumn::Expr(_, Some(alias)) if alias.is_explicit() => {
3497            alias.name().as_str().eq_ignore_ascii_case(name)
3498        }
3499        _ => false,
3500    })
3501}
3502
3503/// Scope-aware version of `rewrite_select_column_refs` that checks table qualifiers.
3504fn rewrite_select_column_refs_scoped(
3505    select: &mut ast::Select,
3506    target_table: &str,
3507    trigger_table: &str,
3508    old_col: &str,
3509    new_col: &str,
3510    target_qualifiers: &mut Vec<String>,
3511) {
3512    if let Some(with_clause) = &mut select.with {
3513        for cte in &mut with_clause.ctes {
3514            rewrite_select_column_refs_scoped(
3515                &mut cte.select,
3516                target_table,
3517                trigger_table,
3518                old_col,
3519                new_col,
3520                target_qualifiers,
3521            );
3522        }
3523    }
3524
3525    rewrite_one_select_column_refs_scoped(
3526        &mut select.body.select,
3527        target_table,
3528        trigger_table,
3529        old_col,
3530        new_col,
3531        target_qualifiers,
3532    );
3533    for compound in &mut select.body.compounds {
3534        rewrite_one_select_column_refs_scoped(
3535            &mut compound.select,
3536            target_table,
3537            trigger_table,
3538            old_col,
3539            new_col,
3540            target_qualifiers,
3541        );
3542    }
3543    // ORDER BY is in the same scope as the body's FROM
3544    let added = match &select.body.select {
3545        ast::OneSelect::Select { from, .. } => {
3546            let local = from_clause_target_qualifiers(from, target_table);
3547            extend_qualifiers_scoped(target_qualifiers, &local)
3548        }
3549        _ => 0,
3550    };
3551    let rename_unqualified =
3552        !target_qualifiers.is_empty() || target_table.eq_ignore_ascii_case(trigger_table);
3553    // Per SQLite's ORDER BY resolution rules, a bare identifier matching an
3554    // output column alias is treated as that alias, not as a FROM-clause
3555    // column reference. Such a reference must not be rewritten — the alias
3556    // label is independent of the renamed table column.
3557    let body = &select.body;
3558    for col in &mut select.order_by {
3559        if is_order_by_alias_ref(body, &col.expr, old_col) {
3560            continue;
3561        }
3562        rename_identifiers_scoped_inner(
3563            &mut col.expr,
3564            target_table,
3565            trigger_table,
3566            old_col,
3567            new_col,
3568            rename_unqualified,
3569            Some(target_qualifiers),
3570        );
3571    }
3572    if let Some(limit) = &mut select.limit {
3573        rename_identifiers_scoped_inner(
3574            &mut limit.expr,
3575            target_table,
3576            trigger_table,
3577            old_col,
3578            new_col,
3579            rename_unqualified,
3580            Some(target_qualifiers),
3581        );
3582        if let Some(offset) = &mut limit.offset {
3583            rename_identifiers_scoped_inner(
3584                offset,
3585                target_table,
3586                trigger_table,
3587                old_col,
3588                new_col,
3589                rename_unqualified,
3590                Some(target_qualifiers),
3591            );
3592        }
3593    }
3594    target_qualifiers.truncate(target_qualifiers.len() - added);
3595}
3596
3597fn from_clause_target_qualifiers(
3598    from: &Option<ast::FromClause>,
3599    target_table: &str,
3600) -> Vec<String> {
3601    match from {
3602        Some(from_clause) => from_clause_target_qualifiers_inner(from_clause, target_table),
3603        None => Vec::new(),
3604    }
3605}
3606
3607fn from_clause_target_qualifiers_inner(
3608    from_clause: &ast::FromClause,
3609    target_table: &str,
3610) -> Vec<String> {
3611    let target_table = normalize_ident(target_table);
3612    let mut qualifiers = Vec::new();
3613    let mut seen = HashSet::default();
3614    collect_target_qualifiers(
3615        &from_clause.select,
3616        &target_table,
3617        &mut qualifiers,
3618        &mut seen,
3619    );
3620    for join in &from_clause.joins {
3621        collect_target_qualifiers(&join.table, &target_table, &mut qualifiers, &mut seen);
3622    }
3623    qualifiers
3624}
3625
3626fn collect_target_qualifiers(
3627    st: &ast::SelectTable,
3628    target_table: &str,
3629    qualifiers: &mut Vec<String>,
3630    seen: &mut HashSet<String>,
3631) {
3632    let ast::SelectTable::Table(name, alias, _) = st else {
3633        return;
3634    };
3635    if !name.name.as_str().eq_ignore_ascii_case(target_table) {
3636        return;
3637    }
3638
3639    if seen.insert(target_table.to_string()) {
3640        qualifiers.push(target_table.to_string());
3641    }
3642    if let Some(alias) = alias {
3643        let alias_norm = normalize_ident(alias.name().as_str());
3644        if seen.insert(alias_norm.clone()) {
3645            qualifiers.push(alias_norm);
3646        }
3647    }
3648}
3649
3650fn rewrite_one_select_column_refs_scoped(
3651    one: &mut ast::OneSelect,
3652    target_table: &str,
3653    trigger_table: &str,
3654    old_col: &str,
3655    new_col: &str,
3656    target_qualifiers: &mut Vec<String>,
3657) {
3658    match one {
3659        ast::OneSelect::Select {
3660            from,
3661            where_clause,
3662            columns,
3663            group_by,
3664            window_clause,
3665            ..
3666        } => {
3667            // Check if FROM clause references the target table to determine
3668            // whether unqualified Expr::Id should be renamed in this scope
3669            let local = from_clause_target_qualifiers(from, target_table);
3670            let added = extend_qualifiers_scoped(target_qualifiers, &local);
3671
3672            let rename_unqualified =
3673                !target_qualifiers.is_empty() || target_table.eq_ignore_ascii_case(trigger_table);
3674
3675            if let Some(ref mut from) = from {
3676                rewrite_from_clause_column_refs_scoped(
3677                    from,
3678                    target_table,
3679                    trigger_table,
3680                    old_col,
3681                    new_col,
3682                    target_qualifiers,
3683                );
3684            }
3685            if let Some(ref mut wc) = where_clause {
3686                rename_identifiers_scoped_inner(
3687                    wc,
3688                    target_table,
3689                    trigger_table,
3690                    old_col,
3691                    new_col,
3692                    rename_unqualified,
3693                    Some(target_qualifiers),
3694                );
3695            }
3696            for col in columns {
3697                if let ast::ResultColumn::Expr(ref mut expr, _) = col {
3698                    rename_result_identifiers_scoped(
3699                        expr,
3700                        target_table,
3701                        trigger_table,
3702                        old_col,
3703                        new_col,
3704                        rename_unqualified,
3705                        Some(target_qualifiers),
3706                    );
3707                }
3708            }
3709            if let Some(ref mut gb) = group_by {
3710                for expr in &mut gb.exprs {
3711                    rename_identifiers_scoped_inner(
3712                        expr,
3713                        target_table,
3714                        trigger_table,
3715                        old_col,
3716                        new_col,
3717                        rename_unqualified,
3718                        Some(target_qualifiers),
3719                    );
3720                }
3721                if let Some(ref mut having) = gb.having {
3722                    rename_identifiers_scoped_inner(
3723                        having,
3724                        target_table,
3725                        trigger_table,
3726                        old_col,
3727                        new_col,
3728                        rename_unqualified,
3729                        Some(target_qualifiers),
3730                    );
3731                }
3732            }
3733            for window_def in window_clause {
3734                rewrite_window_column_refs_scoped(
3735                    &mut window_def.window,
3736                    target_table,
3737                    trigger_table,
3738                    old_col,
3739                    new_col,
3740                    target_qualifiers,
3741                );
3742            }
3743            target_qualifiers.truncate(target_qualifiers.len() - added);
3744        }
3745        ast::OneSelect::Values(rows) => {
3746            for row in rows {
3747                for expr in row {
3748                    rename_identifiers_scoped(expr, target_table, trigger_table, old_col, new_col);
3749                }
3750            }
3751        }
3752    }
3753}
3754
3755fn rename_result_identifiers_scoped(
3756    expr: &mut ast::Expr,
3757    target_table: &str,
3758    trigger_table: &str,
3759    from: &str,
3760    to: &str,
3761    rename_unqualified: bool,
3762    target_qualifiers: Option<&[String]>,
3763) {
3764    let is_renaming_trigger_table = target_table.eq_ignore_ascii_case(trigger_table);
3765
3766    let _ = walk_expr_mut(
3767        expr,
3768        &mut |e: &mut ast::Expr| -> crate::Result<WalkControl> {
3769            match e {
3770                ast::Expr::Exists(_) => return Ok(WalkControl::SkipChildren),
3771                ast::Expr::Subquery(select) => {
3772                    let mut quals = target_qualifiers.unwrap_or(&[]).to_vec();
3773                    rewrite_select_column_refs_scoped(
3774                        select,
3775                        target_table,
3776                        trigger_table,
3777                        from,
3778                        to,
3779                        &mut quals,
3780                    );
3781                }
3782                ast::Expr::InSelect { rhs, .. } => {
3783                    let mut quals = target_qualifiers.unwrap_or(&[]).to_vec();
3784                    rewrite_select_column_refs_scoped(
3785                        rhs,
3786                        target_table,
3787                        trigger_table,
3788                        from,
3789                        to,
3790                        &mut quals,
3791                    );
3792                }
3793                ast::Expr::Id(ref name) | ast::Expr::Name(ref name)
3794                    if rename_unqualified && name.as_str().eq_ignore_ascii_case(from) =>
3795                {
3796                    *e = ast::Expr::Id(ast::Name::exact(to.to_owned()));
3797                }
3798                ast::Expr::Qualified(ref tbl, ref col_name)
3799                    if col_name.as_str().eq_ignore_ascii_case(from) =>
3800                {
3801                    let tbl_norm = normalize_ident(tbl.as_str());
3802                    let should_rename = if tbl_norm == "new" || tbl_norm == "old" {
3803                        is_renaming_trigger_table
3804                    } else {
3805                        target_qualifiers.is_some_and(|qualifiers| qualifiers.contains(&tbl_norm))
3806                            || tbl_norm.eq_ignore_ascii_case(target_table)
3807                    };
3808                    if should_rename {
3809                        let tbl = tbl.clone();
3810                        *e = ast::Expr::Qualified(tbl, ast::Name::exact(to.to_owned()));
3811                    }
3812                }
3813                _ => {}
3814            }
3815            Ok(WalkControl::Continue)
3816        },
3817    );
3818}
3819
3820fn rewrite_window_column_refs_scoped(
3821    window: &mut ast::Window,
3822    target_table: &str,
3823    trigger_table: &str,
3824    old_col: &str,
3825    new_col: &str,
3826    visible_target_qualifiers: &[String],
3827) {
3828    let rename_unqualified =
3829        !visible_target_qualifiers.is_empty() || target_table.eq_ignore_ascii_case(trigger_table);
3830
3831    for expr in &mut window.partition_by {
3832        rename_identifiers_scoped_inner(
3833            expr,
3834            target_table,
3835            trigger_table,
3836            old_col,
3837            new_col,
3838            rename_unqualified,
3839            Some(visible_target_qualifiers),
3840        );
3841    }
3842    for sorted_col in &mut window.order_by {
3843        rename_identifiers_scoped_inner(
3844            &mut sorted_col.expr,
3845            target_table,
3846            trigger_table,
3847            old_col,
3848            new_col,
3849            rename_unqualified,
3850            Some(visible_target_qualifiers),
3851        );
3852    }
3853}
3854
3855fn rewrite_from_clause_column_refs_scoped(
3856    from: &mut ast::FromClause,
3857    target_table: &str,
3858    trigger_table: &str,
3859    old_col: &str,
3860    new_col: &str,
3861    target_qualifiers: &mut Vec<String>,
3862) {
3863    let local = from_clause_target_qualifiers_inner(from, target_table);
3864    let added = extend_qualifiers_scoped(target_qualifiers, &local);
3865
3866    let rename_unqualified =
3867        !target_qualifiers.is_empty() || target_table.eq_ignore_ascii_case(trigger_table);
3868
3869    rewrite_select_table_entry_column_refs_scoped(
3870        &mut from.select,
3871        target_table,
3872        trigger_table,
3873        old_col,
3874        new_col,
3875        target_qualifiers,
3876    );
3877    for join in &mut from.joins {
3878        rewrite_select_table_entry_column_refs_scoped(
3879            &mut join.table,
3880            target_table,
3881            trigger_table,
3882            old_col,
3883            new_col,
3884            target_qualifiers,
3885        );
3886        if let Some(ast::JoinConstraint::On(ref mut expr)) = join.constraint {
3887            rename_identifiers_scoped_inner(
3888                expr,
3889                target_table,
3890                trigger_table,
3891                old_col,
3892                new_col,
3893                rename_unqualified,
3894                Some(target_qualifiers),
3895            );
3896        }
3897    }
3898    target_qualifiers.truncate(target_qualifiers.len() - added);
3899}
3900
3901fn rewrite_select_table_entry_column_refs_scoped(
3902    st: &mut ast::SelectTable,
3903    target_table: &str,
3904    trigger_table: &str,
3905    old_col: &str,
3906    new_col: &str,
3907    target_qualifiers: &mut Vec<String>,
3908) {
3909    match st {
3910        ast::SelectTable::TableCall(_, ref mut args, _) => {
3911            for arg in args {
3912                rename_identifiers_scoped_inner(
3913                    arg,
3914                    target_table,
3915                    trigger_table,
3916                    old_col,
3917                    new_col,
3918                    true,
3919                    Some(target_qualifiers),
3920                );
3921            }
3922        }
3923        ast::SelectTable::Select(ref mut select, _) => {
3924            rewrite_select_column_refs_scoped(
3925                select,
3926                target_table,
3927                trigger_table,
3928                old_col,
3929                new_col,
3930                target_qualifiers,
3931            );
3932        }
3933        ast::SelectTable::Sub(ref mut from, _) => {
3934            rewrite_from_clause_column_refs_scoped(
3935                from,
3936                target_table,
3937                trigger_table,
3938                old_col,
3939                new_col,
3940                target_qualifiers,
3941            );
3942        }
3943        ast::SelectTable::Table(..) => {}
3944    }
3945}
3946
3947/// Push `local` qualifiers that are not already present in `qualifiers`.
3948/// Returns the number of elements added so the caller can truncate afterwards (backtracking).
3949fn extend_qualifiers_scoped(qualifiers: &mut Vec<String>, local: &[String]) -> usize {
3950    let before = qualifiers.len();
3951    for qualifier in local {
3952        if !qualifiers.iter().any(|q| q == qualifier) {
3953            qualifiers.push(qualifier.clone());
3954        }
3955    }
3956    qualifiers.len() - before
3957}
3958
3959fn expr_still_references_renamed_column(
3960    expr: &ast::Expr,
3961    target_table: &str,
3962    trigger_table: &str,
3963    old_col: &str,
3964    rename_unqualified: bool,
3965    visible_target_qualifiers: &[String],
3966) -> bool {
3967    let mut found = false;
3968
3969    let _ = walk_expr(expr, &mut |e: &ast::Expr| -> crate::Result<WalkControl> {
3970        if found {
3971            return Ok(WalkControl::Continue);
3972        }
3973
3974        match e {
3975            ast::Expr::Subquery(select) | ast::Expr::Exists(select) => {
3976                let mut quals = visible_target_qualifiers.to_vec();
3977                found = select_still_references_renamed_column(
3978                    select,
3979                    target_table,
3980                    trigger_table,
3981                    old_col,
3982                    &mut quals,
3983                );
3984            }
3985            ast::Expr::InSelect { rhs, .. } => {
3986                let mut quals = visible_target_qualifiers.to_vec();
3987                found = select_still_references_renamed_column(
3988                    rhs,
3989                    target_table,
3990                    trigger_table,
3991                    old_col,
3992                    &mut quals,
3993                );
3994            }
3995            ast::Expr::Qualified(ns, col) | ast::Expr::DoublyQualified(_, ns, col) => {
3996                if col.as_str().eq_ignore_ascii_case(old_col) {
3997                    let ns_norm = normalize_ident(ns.as_str());
3998                    if ((ns_norm == "new" || ns_norm == "old")
3999                        && target_table.eq_ignore_ascii_case(trigger_table))
4000                        || visible_target_qualifiers.contains(&ns_norm)
4001                        || (target_table.eq_ignore_ascii_case(trigger_table)
4002                            && ns_norm.eq_ignore_ascii_case(trigger_table))
4003                    {
4004                        found = true;
4005                    }
4006                }
4007            }
4008            ast::Expr::Id(name) | ast::Expr::Name(name) => {
4009                if rename_unqualified && name.as_str().eq_ignore_ascii_case(old_col) {
4010                    found = true;
4011                }
4012            }
4013            _ => {}
4014        }
4015        Ok(WalkControl::Continue)
4016    });
4017
4018    found
4019}
4020
4021fn one_select_still_references_renamed_column(
4022    one: &ast::OneSelect,
4023    target_table: &str,
4024    trigger_table: &str,
4025    old_col: &str,
4026    target_qualifiers: &mut Vec<String>,
4027) -> bool {
4028    match one {
4029        ast::OneSelect::Select {
4030            from,
4031            where_clause,
4032            columns,
4033            group_by,
4034            ..
4035        } => {
4036            let local = from_clause_target_qualifiers(from, target_table);
4037            let added = extend_qualifiers_scoped(target_qualifiers, &local);
4038
4039            let rename_unqualified =
4040                !target_qualifiers.is_empty() || target_table.eq_ignore_ascii_case(trigger_table);
4041
4042            let mut found = false;
4043            if let Some(from_clause) = from {
4044                if from_clause_still_references_renamed_column(
4045                    from_clause,
4046                    target_table,
4047                    trigger_table,
4048                    old_col,
4049                    target_qualifiers,
4050                ) {
4051                    found = true;
4052                }
4053            }
4054
4055            if !found {
4056                if let Some(where_expr) = where_clause {
4057                    if expr_still_references_renamed_column(
4058                        where_expr,
4059                        target_table,
4060                        trigger_table,
4061                        old_col,
4062                        rename_unqualified,
4063                        target_qualifiers,
4064                    ) {
4065                        found = true;
4066                    }
4067                }
4068            }
4069
4070            if !found {
4071                for col in columns {
4072                    if let ast::ResultColumn::Expr(expr, _) = col {
4073                        if expr_still_references_renamed_column(
4074                            expr,
4075                            target_table,
4076                            trigger_table,
4077                            old_col,
4078                            rename_unqualified,
4079                            target_qualifiers,
4080                        ) {
4081                            found = true;
4082                            break;
4083                        }
4084                    }
4085                }
4086            }
4087
4088            if !found {
4089                if let Some(group_by) = group_by {
4090                    for expr in &group_by.exprs {
4091                        if expr_still_references_renamed_column(
4092                            expr,
4093                            target_table,
4094                            trigger_table,
4095                            old_col,
4096                            rename_unqualified,
4097                            target_qualifiers,
4098                        ) {
4099                            found = true;
4100                            break;
4101                        }
4102                    }
4103                    if !found {
4104                        if let Some(having) = &group_by.having {
4105                            if expr_still_references_renamed_column(
4106                                having,
4107                                target_table,
4108                                trigger_table,
4109                                old_col,
4110                                rename_unqualified,
4111                                target_qualifiers,
4112                            ) {
4113                                found = true;
4114                            }
4115                        }
4116                    }
4117                }
4118            }
4119
4120            target_qualifiers.truncate(target_qualifiers.len() - added);
4121            found
4122        }
4123        ast::OneSelect::Values(rows) => {
4124            for row in rows {
4125                for expr in row {
4126                    if expr_still_references_renamed_column(
4127                        expr,
4128                        target_table,
4129                        trigger_table,
4130                        old_col,
4131                        true,
4132                        target_qualifiers,
4133                    ) {
4134                        return true;
4135                    }
4136                }
4137            }
4138            false
4139        }
4140    }
4141}
4142
4143fn select_still_references_renamed_column(
4144    select: &ast::Select,
4145    target_table: &str,
4146    trigger_table: &str,
4147    old_col: &str,
4148    target_qualifiers: &mut Vec<String>,
4149) -> bool {
4150    if let Some(with_clause) = &select.with {
4151        for cte in &with_clause.ctes {
4152            if select_still_references_renamed_column(
4153                &cte.select,
4154                target_table,
4155                trigger_table,
4156                old_col,
4157                target_qualifiers,
4158            ) {
4159                return true;
4160            }
4161        }
4162    }
4163
4164    if one_select_still_references_renamed_column(
4165        &select.body.select,
4166        target_table,
4167        trigger_table,
4168        old_col,
4169        target_qualifiers,
4170    ) {
4171        return true;
4172    }
4173
4174    for compound in &select.body.compounds {
4175        if one_select_still_references_renamed_column(
4176            &compound.select,
4177            target_table,
4178            trigger_table,
4179            old_col,
4180            target_qualifiers,
4181        ) {
4182            return true;
4183        }
4184    }
4185
4186    // ORDER BY is in the same scope as the body's FROM
4187    let added = match &select.body.select {
4188        ast::OneSelect::Select { from, .. } => {
4189            let local = from_clause_target_qualifiers(from, target_table);
4190            extend_qualifiers_scoped(target_qualifiers, &local)
4191        }
4192        _ => 0,
4193    };
4194    let rename_unqualified =
4195        !target_qualifiers.is_empty() || target_table.eq_ignore_ascii_case(trigger_table);
4196
4197    // Bare identifiers in ORDER BY that match an output column alias are
4198    // alias references, not FROM-clause column references, so they don't
4199    // count as a stale reference to the renamed column.
4200    let body = &select.body;
4201
4202    let mut found = false;
4203    for sorted_col in &select.order_by {
4204        if is_order_by_alias_ref(body, &sorted_col.expr, old_col) {
4205            continue;
4206        }
4207        if expr_still_references_renamed_column(
4208            &sorted_col.expr,
4209            target_table,
4210            trigger_table,
4211            old_col,
4212            rename_unqualified,
4213            target_qualifiers,
4214        ) {
4215            found = true;
4216            break;
4217        }
4218    }
4219
4220    if !found {
4221        if let Some(limit) = &select.limit {
4222            if expr_still_references_renamed_column(
4223                &limit.expr,
4224                target_table,
4225                trigger_table,
4226                old_col,
4227                rename_unqualified,
4228                target_qualifiers,
4229            ) {
4230                found = true;
4231            }
4232            if !found {
4233                if let Some(offset) = &limit.offset {
4234                    if expr_still_references_renamed_column(
4235                        offset,
4236                        target_table,
4237                        trigger_table,
4238                        old_col,
4239                        rename_unqualified,
4240                        target_qualifiers,
4241                    ) {
4242                        found = true;
4243                    }
4244                }
4245            }
4246        }
4247    }
4248
4249    target_qualifiers.truncate(target_qualifiers.len() - added);
4250    found
4251}
4252
4253fn select_table_still_references_renamed_column(
4254    st: &ast::SelectTable,
4255    target_table: &str,
4256    trigger_table: &str,
4257    old_col: &str,
4258    target_qualifiers: &mut Vec<String>,
4259) -> bool {
4260    match st {
4261        ast::SelectTable::TableCall(_, args, _) => args.iter().any(|arg| {
4262            expr_still_references_renamed_column(
4263                arg,
4264                target_table,
4265                trigger_table,
4266                old_col,
4267                true,
4268                target_qualifiers,
4269            )
4270        }),
4271        ast::SelectTable::Select(select, _) => select_still_references_renamed_column(
4272            select,
4273            target_table,
4274            trigger_table,
4275            old_col,
4276            target_qualifiers,
4277        ),
4278        ast::SelectTable::Sub(from, _) => from_clause_still_references_renamed_column(
4279            from,
4280            target_table,
4281            trigger_table,
4282            old_col,
4283            target_qualifiers,
4284        ),
4285        ast::SelectTable::Table(..) => false,
4286    }
4287}
4288
4289fn from_clause_still_references_renamed_column(
4290    from: &ast::FromClause,
4291    target_table: &str,
4292    trigger_table: &str,
4293    old_col: &str,
4294    target_qualifiers: &mut Vec<String>,
4295) -> bool {
4296    let local = from_clause_target_qualifiers_inner(from, target_table);
4297    let added = extend_qualifiers_scoped(target_qualifiers, &local);
4298
4299    let rename_unqualified =
4300        !target_qualifiers.is_empty() || target_table.eq_ignore_ascii_case(trigger_table);
4301
4302    let mut found = false;
4303    if select_table_still_references_renamed_column(
4304        &from.select,
4305        target_table,
4306        trigger_table,
4307        old_col,
4308        target_qualifiers,
4309    ) {
4310        found = true;
4311    }
4312
4313    if !found {
4314        for join in &from.joins {
4315            if select_table_still_references_renamed_column(
4316                &join.table,
4317                target_table,
4318                trigger_table,
4319                old_col,
4320                target_qualifiers,
4321            ) {
4322                found = true;
4323                break;
4324            }
4325            if let Some(ast::JoinConstraint::On(expr)) = &join.constraint {
4326                if expr_still_references_renamed_column(
4327                    expr,
4328                    target_table,
4329                    trigger_table,
4330                    old_col,
4331                    rename_unqualified,
4332                    target_qualifiers,
4333                ) {
4334                    found = true;
4335                    break;
4336                }
4337            }
4338        }
4339    }
4340
4341    target_qualifiers.truncate(target_qualifiers.len() - added);
4342    found
4343}
4344
4345pub fn trigger_still_references_renamed_column(
4346    trigger: &crate::schema::Trigger,
4347    target_table: &str,
4348    old_col: &str,
4349) -> bool {
4350    if trigger.table_name.eq_ignore_ascii_case(target_table) {
4351        if let ast::TriggerEvent::UpdateOf(cols) = &trigger.event {
4352            if cols
4353                .iter()
4354                .any(|col| col.as_str().eq_ignore_ascii_case(old_col))
4355            {
4356                return true;
4357            }
4358        }
4359    }
4360
4361    if let Some(when_clause) = &trigger.when_clause {
4362        if expr_still_references_renamed_column(
4363            when_clause,
4364            target_table,
4365            &trigger.table_name,
4366            old_col,
4367            false,
4368            &[],
4369        ) {
4370            return true;
4371        }
4372    }
4373
4374    for cmd in &trigger.commands {
4375        match cmd {
4376            ast::TriggerCmd::Update {
4377                tbl_name,
4378                sets,
4379                from,
4380                where_clause,
4381                ..
4382            } => {
4383                let targets_renamed_table = tbl_name.as_str().eq_ignore_ascii_case(target_table);
4384                let visible_target_qualifiers = from_clause_target_qualifiers(from, target_table);
4385
4386                if targets_renamed_table
4387                    && sets.iter().any(|set| {
4388                        set.col_names
4389                            .iter()
4390                            .any(|col_name| col_name.as_str().eq_ignore_ascii_case(old_col))
4391                    })
4392                {
4393                    return true;
4394                }
4395
4396                for set in sets {
4397                    if expr_still_references_renamed_column(
4398                        &set.expr,
4399                        target_table,
4400                        &trigger.table_name,
4401                        old_col,
4402                        targets_renamed_table,
4403                        &visible_target_qualifiers,
4404                    ) {
4405                        return true;
4406                    }
4407                }
4408
4409                if let Some(where_clause) = where_clause {
4410                    if expr_still_references_renamed_column(
4411                        where_clause,
4412                        target_table,
4413                        &trigger.table_name,
4414                        old_col,
4415                        targets_renamed_table,
4416                        &visible_target_qualifiers,
4417                    ) {
4418                        return true;
4419                    }
4420                }
4421
4422                if let Some(from_clause) = from {
4423                    if from_clause_still_references_renamed_column(
4424                        from_clause,
4425                        target_table,
4426                        &trigger.table_name,
4427                        old_col,
4428                        &mut Vec::new(),
4429                    ) {
4430                        return true;
4431                    }
4432                }
4433            }
4434            ast::TriggerCmd::Insert {
4435                tbl_name,
4436                col_names,
4437                select,
4438                upsert,
4439                ..
4440            } => {
4441                if tbl_name.as_str().eq_ignore_ascii_case(target_table)
4442                    && col_names
4443                        .iter()
4444                        .any(|col_name| col_name.as_str().eq_ignore_ascii_case(old_col))
4445                {
4446                    return true;
4447                }
4448
4449                if select_still_references_renamed_column(
4450                    select,
4451                    target_table,
4452                    &trigger.table_name,
4453                    old_col,
4454                    &mut Vec::new(),
4455                ) {
4456                    return true;
4457                }
4458
4459                if let Some(upsert) = upsert {
4460                    if let Some(index) = &upsert.index {
4461                        for target in &index.targets {
4462                            if expr_still_references_renamed_column(
4463                                &target.expr,
4464                                target_table,
4465                                &trigger.table_name,
4466                                old_col,
4467                                tbl_name.as_str().eq_ignore_ascii_case(target_table),
4468                                &[],
4469                            ) {
4470                                return true;
4471                            }
4472                        }
4473                        if let Some(where_clause) = &index.where_clause {
4474                            if expr_still_references_renamed_column(
4475                                where_clause,
4476                                target_table,
4477                                &trigger.table_name,
4478                                old_col,
4479                                tbl_name.as_str().eq_ignore_ascii_case(target_table),
4480                                &[],
4481                            ) {
4482                                return true;
4483                            }
4484                        }
4485                    }
4486                }
4487            }
4488            ast::TriggerCmd::Delete {
4489                tbl_name,
4490                where_clause,
4491            } => {
4492                if let Some(where_clause) = where_clause {
4493                    if expr_still_references_renamed_column(
4494                        where_clause,
4495                        target_table,
4496                        &trigger.table_name,
4497                        old_col,
4498                        tbl_name.as_str().eq_ignore_ascii_case(target_table),
4499                        &[],
4500                    ) {
4501                        return true;
4502                    }
4503                }
4504            }
4505            ast::TriggerCmd::Select(select) => {
4506                if select_still_references_renamed_column(
4507                    select,
4508                    target_table,
4509                    &trigger.table_name,
4510                    old_col,
4511                    &mut Vec::new(),
4512                ) {
4513                    return true;
4514                }
4515            }
4516        }
4517    }
4518
4519    false
4520}
4521
4522fn rename_excluded_column_refs(expr: &mut ast::Expr, old_col: &str, new_col: &str) {
4523    let _ = walk_expr_mut(
4524        expr,
4525        &mut |e: &mut ast::Expr| -> crate::Result<WalkControl> {
4526            if let ast::Expr::Qualified(ns, col) | ast::Expr::DoublyQualified(_, ns, col) = e {
4527                if ns.as_str().eq_ignore_ascii_case("excluded")
4528                    && col.as_str().eq_ignore_ascii_case(old_col)
4529                {
4530                    *col = ast::Name::exact(new_col.to_owned());
4531                }
4532            }
4533            Ok(WalkControl::Continue)
4534        },
4535    );
4536}
4537
4538fn rewrite_upsert_column_refs_scoped(
4539    upsert: &mut ast::Upsert,
4540    table: &str,
4541    trigger_table: &str,
4542    insert_table: &str,
4543    old_col: &str,
4544    new_col: &str,
4545) {
4546    let insert_targets_renamed_table = insert_table.eq_ignore_ascii_case(table);
4547    let rewrite_expr = |expr: &mut ast::Expr| {
4548        if insert_targets_renamed_table {
4549            rename_identifiers_scoped(expr, table, trigger_table, old_col, new_col);
4550            rename_excluded_column_refs(expr, old_col, new_col);
4551        } else {
4552            rename_identifiers_scoped_when_clause(expr, table, trigger_table, old_col, new_col);
4553        }
4554    };
4555
4556    if let Some(ref mut index) = upsert.index {
4557        for target in &mut index.targets {
4558            rewrite_expr(&mut target.expr);
4559        }
4560        if let Some(ref mut wc) = index.where_clause {
4561            rewrite_expr(wc);
4562        }
4563    }
4564    if let ast::UpsertDo::Set {
4565        ref mut sets,
4566        ref mut where_clause,
4567    } = upsert.do_clause
4568    {
4569        for set in sets {
4570            if insert_targets_renamed_table {
4571                for col_name in &mut set.col_names {
4572                    if col_name.as_str().eq_ignore_ascii_case(old_col) {
4573                        *col_name = ast::Name::exact(new_col.to_owned());
4574                    }
4575                }
4576            }
4577            rewrite_expr(&mut set.expr);
4578        }
4579        if let Some(ref mut wc) = where_clause {
4580            rewrite_expr(wc);
4581        }
4582    }
4583    if let Some(ref mut next) = upsert.next {
4584        rewrite_upsert_column_refs_scoped(
4585            next,
4586            table,
4587            trigger_table,
4588            insert_table,
4589            old_col,
4590            new_col,
4591        );
4592    }
4593}
4594
4595/// Rewrite column references inside a trigger's body commands for ALTER TABLE RENAME COLUMN.
4596/// Uses scope-aware renaming: only renames qualified refs when the qualifier matches
4597/// the target table (or NEW/OLD for the trigger's owning table).
4598pub fn rewrite_trigger_cmd_column_refs(
4599    cmd: &mut ast::TriggerCmd,
4600    table: &str,
4601    trigger_table: &str,
4602    old_col: &str,
4603    new_col: &str,
4604) {
4605    match cmd {
4606        ast::TriggerCmd::Update {
4607            tbl_name,
4608            sets,
4609            from,
4610            where_clause,
4611            ..
4612        } => {
4613            let targets_renamed_table = tbl_name.as_str().eq_ignore_ascii_case(table);
4614            if targets_renamed_table {
4615                for set in sets {
4616                    for col_name in &mut set.col_names {
4617                        if col_name.as_str().eq_ignore_ascii_case(old_col) {
4618                            *col_name = ast::Name::exact(new_col.to_owned());
4619                        }
4620                    }
4621                    rename_identifiers_scoped(
4622                        &mut set.expr,
4623                        table,
4624                        trigger_table,
4625                        old_col,
4626                        new_col,
4627                    );
4628                }
4629                if let Some(ref mut wc) = where_clause {
4630                    rename_identifiers_scoped(wc, table, trigger_table, old_col, new_col);
4631                }
4632            } else {
4633                for set in sets {
4634                    rename_identifiers_scoped_when_clause(
4635                        &mut set.expr,
4636                        table,
4637                        trigger_table,
4638                        old_col,
4639                        new_col,
4640                    );
4641                }
4642                if let Some(ref mut wc) = where_clause {
4643                    rename_identifiers_scoped_when_clause(
4644                        wc,
4645                        table,
4646                        trigger_table,
4647                        old_col,
4648                        new_col,
4649                    );
4650                }
4651            }
4652            if let Some(ref mut from) = from {
4653                rewrite_from_clause_column_refs_scoped(
4654                    from,
4655                    table,
4656                    trigger_table,
4657                    old_col,
4658                    new_col,
4659                    &mut Vec::new(),
4660                );
4661            }
4662        }
4663        ast::TriggerCmd::Insert {
4664            tbl_name,
4665            col_names,
4666            select,
4667            upsert,
4668            ..
4669        } => {
4670            let targets_renamed_table = tbl_name.as_str().eq_ignore_ascii_case(table);
4671            if targets_renamed_table {
4672                for col_name in col_names {
4673                    if col_name.as_str().eq_ignore_ascii_case(old_col) {
4674                        *col_name = ast::Name::exact(new_col.to_owned());
4675                    }
4676                }
4677            }
4678            rewrite_select_column_refs_scoped(
4679                select,
4680                table,
4681                trigger_table,
4682                old_col,
4683                new_col,
4684                &mut Vec::new(),
4685            );
4686            if let Some(ref mut upsert) = upsert {
4687                rewrite_upsert_column_refs_scoped(
4688                    upsert,
4689                    table,
4690                    trigger_table,
4691                    tbl_name.as_str(),
4692                    old_col,
4693                    new_col,
4694                );
4695            }
4696        }
4697        ast::TriggerCmd::Delete {
4698            tbl_name,
4699            where_clause,
4700        } => {
4701            let targets_renamed_table = tbl_name.as_str().eq_ignore_ascii_case(table);
4702            if targets_renamed_table {
4703                if let Some(ref mut wc) = where_clause {
4704                    rename_identifiers_scoped(wc, table, trigger_table, old_col, new_col);
4705                }
4706            } else if let Some(ref mut wc) = where_clause {
4707                rename_identifiers_scoped_when_clause(wc, table, trigger_table, old_col, new_col);
4708            }
4709        }
4710        ast::TriggerCmd::Select(select) => {
4711            rewrite_select_column_refs_scoped(
4712                select,
4713                table,
4714                trigger_table,
4715                old_col,
4716                new_col,
4717                &mut Vec::new(),
4718            );
4719        }
4720    }
4721}
4722
4723fn rewrite_select_table_refs(select: &mut ast::Select, old_tbl: &str, new_tbl: &str) {
4724    if let Some(with_clause) = &mut select.with {
4725        for cte in &mut with_clause.ctes {
4726            rewrite_select_table_refs(&mut cte.select, old_tbl, new_tbl);
4727        }
4728    }
4729    rewrite_one_select_table_refs(&mut select.body.select, old_tbl, new_tbl);
4730    for compound in &mut select.body.compounds {
4731        rewrite_one_select_table_refs(&mut compound.select, old_tbl, new_tbl);
4732    }
4733    for col in &mut select.order_by {
4734        rewrite_check_expr_table_refs(&mut col.expr, old_tbl, new_tbl);
4735    }
4736}
4737
4738fn rewrite_one_select_table_refs(one: &mut ast::OneSelect, old_tbl: &str, new_tbl: &str) {
4739    match one {
4740        ast::OneSelect::Select {
4741            from,
4742            where_clause,
4743            columns,
4744            group_by,
4745            ..
4746        } => {
4747            if let Some(ref mut from) = from {
4748                rewrite_from_clause_table_refs(from, old_tbl, new_tbl);
4749            }
4750            if let Some(ref mut wc) = where_clause {
4751                rewrite_check_expr_table_refs(wc, old_tbl, new_tbl);
4752            }
4753            for col in columns {
4754                match col {
4755                    ast::ResultColumn::Expr(ref mut expr, _) => {
4756                        rewrite_check_expr_table_refs(expr, old_tbl, new_tbl);
4757                    }
4758                    ast::ResultColumn::TableStar(ref mut name) => {
4759                        if name.as_str().eq_ignore_ascii_case(old_tbl) {
4760                            *name = ast::Name::exact(new_tbl.to_owned());
4761                        }
4762                    }
4763                    ast::ResultColumn::Star => {}
4764                }
4765            }
4766            if let Some(ref mut gb) = group_by {
4767                for expr in &mut gb.exprs {
4768                    rewrite_check_expr_table_refs(expr, old_tbl, new_tbl);
4769                }
4770                if let Some(ref mut having) = gb.having {
4771                    rewrite_check_expr_table_refs(having, old_tbl, new_tbl);
4772                }
4773            }
4774        }
4775        ast::OneSelect::Values(rows) => {
4776            for row in rows {
4777                for expr in row {
4778                    rewrite_check_expr_table_refs(expr, old_tbl, new_tbl);
4779                }
4780            }
4781        }
4782    }
4783}
4784
4785fn rewrite_from_clause_table_refs(from: &mut ast::FromClause, old_tbl: &str, new_tbl: &str) {
4786    rewrite_select_table_entry_table_refs(&mut from.select, old_tbl, new_tbl);
4787    for join in &mut from.joins {
4788        rewrite_select_table_entry_table_refs(&mut join.table, old_tbl, new_tbl);
4789        if let Some(ast::JoinConstraint::On(ref mut expr)) = join.constraint {
4790            rewrite_check_expr_table_refs(expr, old_tbl, new_tbl);
4791        }
4792    }
4793}
4794
4795fn rewrite_select_table_entry_table_refs(st: &mut ast::SelectTable, old_tbl: &str, new_tbl: &str) {
4796    match st {
4797        ast::SelectTable::Table(ref mut name, _, _) => {
4798            if name.name.as_str().eq_ignore_ascii_case(old_tbl) {
4799                name.name = ast::Name::exact(new_tbl.to_owned());
4800            }
4801        }
4802        ast::SelectTable::TableCall(ref mut name, ref mut args, _) => {
4803            if name.name.as_str().eq_ignore_ascii_case(old_tbl) {
4804                name.name = ast::Name::exact(new_tbl.to_owned());
4805            }
4806            for arg in args {
4807                rewrite_check_expr_table_refs(arg, old_tbl, new_tbl);
4808            }
4809        }
4810        ast::SelectTable::Select(ref mut select, _) => {
4811            rewrite_select_table_refs(select, old_tbl, new_tbl);
4812        }
4813        ast::SelectTable::Sub(ref mut from, _) => {
4814            rewrite_from_clause_table_refs(from, old_tbl, new_tbl);
4815        }
4816    }
4817}
4818
4819fn rewrite_upsert_table_refs(upsert: &mut ast::Upsert, old_tbl: &str, new_tbl: &str) {
4820    if let Some(ref mut index) = upsert.index {
4821        if let Some(ref mut wc) = index.where_clause {
4822            rewrite_check_expr_table_refs(wc, old_tbl, new_tbl);
4823        }
4824    }
4825    if let ast::UpsertDo::Set {
4826        ref mut sets,
4827        ref mut where_clause,
4828    } = upsert.do_clause
4829    {
4830        for set in sets {
4831            rewrite_check_expr_table_refs(&mut set.expr, old_tbl, new_tbl);
4832        }
4833        if let Some(ref mut wc) = where_clause {
4834            rewrite_check_expr_table_refs(wc, old_tbl, new_tbl);
4835        }
4836    }
4837    if let Some(ref mut next) = upsert.next {
4838        rewrite_upsert_table_refs(next, old_tbl, new_tbl);
4839    }
4840}
4841
4842#[cfg(clt_turso_tests)]
4843pub mod tests {
4844    use super::*;
4845    use crate::schema::{BTreeTable, Type as SchemaValueType};
4846    use turso_parser::ast::{self, Expr, FunctionTail, Literal, Name, Operator::*, Type, Variable};
4847    use turso_parser::parser::Parser;
4848
4849    #[test]
4850    fn test_normalize_ident() {
4851        assert_eq!(normalize_ident("foo"), "foo");
4852        assert_eq!(normalize_ident("FOO"), "foo");
4853        // SQLite folds only ASCII; non-ASCII bytes pass through untouched.
4854        assert_eq!(normalize_ident("ὈΔΥΣΣΕΎΣ"), "ὈΔΥΣΣΕΎΣ");
4855        assert_eq!(normalize_ident("Foo_ΔΥΣ"), "foo_ΔΥΣ");
4856    }
4857
4858    fn schema_with_tables(create_table_sqls: &[&str]) -> Schema {
4859        let mut schema = Schema::new();
4860        for (index, create_table_sql) in create_table_sqls.iter().enumerate() {
4861            let root_page = i64::try_from(index).expect("test table index should fit in i64") + 2;
4862            let table = BTreeTable::from_sql(create_table_sql, root_page)
4863                .expect("test CREATE TABLE should parse");
4864            schema
4865                .add_btree_table(std::sync::Arc::new(table))
4866                .expect("test table should be added to schema");
4867        }
4868
4869        schema
4870    }
4871
4872    fn schema_with_table(create_table_sql: &str) -> Schema {
4873        schema_with_tables(&[create_table_sql])
4874    }
4875
4876    fn parse_select_from(sql: &str) -> Option<ast::FromClause> {
4877        let mut parser = Parser::new(sql.as_bytes());
4878        let cmd = parser
4879            .next_cmd()
4880            .expect("test SQL should parse")
4881            .expect("test SQL should contain a statement");
4882        let ast::Cmd::Stmt(ast::Stmt::Select(select)) = cmd else {
4883            panic!("expected SELECT statement");
4884        };
4885        match select.body.select {
4886            ast::OneSelect::Select { from, .. } => from,
4887            _ => panic!("expected simple SELECT"),
4888        }
4889    }
4890
4891    #[test]
4892    fn test_rewrite_view_sql_select_table_branch() {
4893        let schema = schema_with_table("CREATE TABLE t (a, b)");
4894        let view_sql = "CREATE VIEW v AS SELECT s.x FROM (SELECT b AS x FROM t) AS s";
4895
4896        let rewritten =
4897            rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c")
4898                .unwrap()
4899                .expect("view should be rewritten");
4900
4901        assert!(rewritten.sql.contains("SELECT c AS x FROM t"));
4902    }
4903
4904    #[test]
4905    fn test_rewrite_view_sql_sub_branch() {
4906        let schema = schema_with_table("CREATE TABLE t (a, b)");
4907        let view_sql = "CREATE VIEW v AS SELECT s.b FROM (t) AS s";
4908
4909        let rewritten =
4910            rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c")
4911                .unwrap()
4912                .expect("view should be rewritten");
4913
4914        assert!(!rewritten.sql.contains("s.b"), "{}", rewritten.sql);
4915    }
4916
4917    #[test]
4918    fn test_rewrite_view_sql_table_call_branch() {
4919        let schema = schema_with_table("CREATE TABLE t (a, b)");
4920        let view_sql =
4921            "CREATE VIEW v AS SELECT j.value FROM t JOIN json_each(json_array(t.b)) AS j";
4922
4923        let rewritten =
4924            rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c")
4925                .unwrap()
4926                .expect("view should be rewritten");
4927
4928        assert!(!rewritten.sql.contains("t.b"), "{}", rewritten.sql);
4929    }
4930
4931    #[test]
4932    fn test_rewrite_view_sql_compound_branch() {
4933        let schema = schema_with_table("CREATE TABLE t (a, b)");
4934        let view_sql = "CREATE VIEW v AS SELECT b FROM t UNION ALL SELECT b FROM t ORDER BY b";
4935
4936        let rewritten =
4937            rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c")
4938                .unwrap()
4939                .expect("view should be rewritten");
4940
4941        assert_eq!(rewritten.sql.matches("SELECT c FROM t").count(), 2);
4942        assert!(!rewritten.sql.contains("ORDER BY b"), "{}", rewritten.sql);
4943        assert!(rewritten.sql.contains("ORDER BY c"), "{}", rewritten.sql);
4944    }
4945
4946    #[test]
4947    fn test_rewrite_view_sql_cte_branch() {
4948        let schema = schema_with_table("CREATE TABLE t (a, b)");
4949        let view_sql = "CREATE VIEW v AS WITH cte AS (SELECT b FROM t) SELECT b FROM cte";
4950
4951        let rewritten =
4952            rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c")
4953                .unwrap()
4954                .expect("view should be rewritten");
4955
4956        assert!(
4957            rewritten.sql.contains("WITH cte AS (SELECT c FROM t)"),
4958            "{}",
4959            rewritten.sql
4960        );
4961        assert!(
4962            rewritten.sql.contains("SELECT c FROM cte"),
4963            "{}",
4964            rewritten.sql
4965        );
4966    }
4967
4968    #[test]
4969    fn test_rewrite_view_sql_cte_branch_with_explicit_columns() {
4970        let schema = schema_with_table("CREATE TABLE t (a, b)");
4971        let view_sql = "CREATE VIEW v AS WITH cte(x) AS (SELECT b FROM t) SELECT x FROM cte";
4972
4973        let rewritten =
4974            rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c")
4975                .unwrap()
4976                .expect("view should be rewritten");
4977
4978        assert!(
4979            rewritten.sql.contains("WITH cte(x)") || rewritten.sql.contains("WITH cte (x)"),
4980            "{}",
4981            rewritten.sql
4982        );
4983        assert!(
4984            rewritten.sql.contains("AS (SELECT c FROM t)"),
4985            "{}",
4986            rewritten.sql
4987        );
4988        assert!(
4989            rewritten.sql.contains("SELECT x FROM cte"),
4990            "{}",
4991            rewritten.sql
4992        );
4993    }
4994
4995    #[test]
4996    fn test_rewrite_trigger_cmd_table_refs_cte_branch() {
4997        let sql = "CREATE TEMP TRIGGER trg AFTER INSERT ON temp.old BEGIN WITH cte AS (SELECT * FROM temp.old) SELECT * FROM cte; END";
4998        let mut parser = Parser::new(sql.as_bytes());
4999        let cmd = parser
5000            .next_cmd()
5001            .expect("trigger SQL should parse")
5002            .expect("trigger SQL should produce a statement");
5003        let ast::Cmd::Stmt(ast::Stmt::CreateTrigger { commands, .. }) = cmd else {
5004            panic!("expected CREATE TRIGGER statement");
5005        };
5006        let mut commands = commands;
5007        let ast::TriggerCmd::Select(select) = &mut commands[0] else {
5008            panic!("expected SELECT trigger command");
5009        };
5010
5011        rewrite_select_table_refs(select, "old", "new");
5012
5013        let Some(with_clause) = &select.with else {
5014            panic!("expected WITH clause");
5015        };
5016        let ast::OneSelect::Select {
5017            from: Some(from), ..
5018        } = &with_clause.ctes[0].select.body.select
5019        else {
5020            panic!("expected CTE SELECT core");
5021        };
5022        let ast::SelectTable::Table(tbl_name, _, _) = from.select.as_ref() else {
5023            panic!("expected CTE SELECT FROM table");
5024        };
5025
5026        assert_eq!(
5027            tbl_name.db_name.as_ref().map(ast::Name::as_str),
5028            Some("temp")
5029        );
5030        assert_eq!(tbl_name.name.as_str(), "new");
5031    }
5032
5033    #[test]
5034    fn test_from_clause_target_qualifiers_dedups_case_insensitively() {
5035        let from = parse_select_from("SELECT 1 FROM Target AS tgt, target AS TARGET");
5036        assert_eq!(
5037            from_clause_target_qualifiers(&from, "target"),
5038            vec!["target".to_string(), "tgt".to_string()]
5039        );
5040    }
5041
5042    #[test]
5043    fn test_extend_qualifiers_scoped_preserves_first_seen_order() {
5044        let mut qualifiers = vec!["target".to_string(), "outer_alias".to_string()];
5045        let local = vec![
5046            "outer_alias".to_string(),
5047            "local_alias".to_string(),
5048            "target".to_string(),
5049        ];
5050
5051        let added = extend_qualifiers_scoped(&mut qualifiers, &local);
5052        assert_eq!(
5053            qualifiers,
5054            vec![
5055                "target".to_string(),
5056                "outer_alias".to_string(),
5057                "local_alias".to_string(),
5058            ]
5059        );
5060        assert_eq!(added, 1);
5061        qualifiers.truncate(qualifiers.len() - added);
5062        assert_eq!(
5063            qualifiers,
5064            vec!["target".to_string(), "outer_alias".to_string(),]
5065        );
5066    }
5067
5068    #[test]
5069    fn test_rewrite_trigger_cmd_table_refs_expr_subquery_branch() {
5070        let sql = "CREATE TEMP TRIGGER trg AFTER INSERT ON temp.old BEGIN SELECT EXISTS(SELECT 1 FROM temp.old); END";
5071        let mut parser = Parser::new(sql.as_bytes());
5072        let cmd = parser
5073            .next_cmd()
5074            .expect("trigger SQL should parse")
5075            .expect("trigger SQL should produce a statement");
5076        let ast::Cmd::Stmt(ast::Stmt::CreateTrigger { commands, .. }) = cmd else {
5077            panic!("expected CREATE TRIGGER statement");
5078        };
5079        let mut commands = commands;
5080        let ast::TriggerCmd::Select(select) = &mut commands[0] else {
5081            panic!("expected SELECT trigger command");
5082        };
5083
5084        rewrite_select_table_refs(select, "old", "new");
5085
5086        let ast::OneSelect::Select { columns, .. } = &select.body.select else {
5087            panic!("expected SELECT core");
5088        };
5089        let ast::ResultColumn::Expr(expr, _) = &columns[0] else {
5090            panic!("expected expression result column");
5091        };
5092        let ast::Expr::Exists(subquery) = expr.as_ref() else {
5093            panic!("expected EXISTS expression");
5094        };
5095        let ast::OneSelect::Select {
5096            from: Some(from), ..
5097        } = &subquery.body.select
5098        else {
5099            panic!("expected EXISTS subquery FROM clause");
5100        };
5101        let ast::SelectTable::Table(tbl_name, _, _) = from.select.as_ref() else {
5102            panic!("expected EXISTS subquery FROM table");
5103        };
5104
5105        assert_eq!(
5106            tbl_name.db_name.as_ref().map(ast::Name::as_str),
5107            Some("temp")
5108        );
5109        assert_eq!(tbl_name.name.as_str(), "new");
5110    }
5111
5112    #[test]
5113    fn test_rewrite_view_sql_join_on_branch() {
5114        let schema = schema_with_tables(&["CREATE TABLE t (a, b)", "CREATE TABLE u (b)"]);
5115        let view_sql = "CREATE VIEW v AS SELECT t.a FROM t JOIN u ON t.b = u.b";
5116
5117        let rewritten =
5118            rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c")
5119                .unwrap()
5120                .expect("view should be rewritten");
5121
5122        assert!(!rewritten.sql.contains("t.b"), "{}", rewritten.sql);
5123        assert!(rewritten.sql.contains("t.c = u.b"), "{}", rewritten.sql);
5124    }
5125
5126    #[test]
5127    fn test_rewrite_view_sql_join_using_branch() {
5128        let schema = schema_with_tables(&["CREATE TABLE t (a, b)", "CREATE TABLE u (b)"]);
5129        let view_sql = "CREATE VIEW v AS SELECT t.a FROM t JOIN u USING (b)";
5130
5131        let rewritten =
5132            rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c")
5133                .unwrap()
5134                .expect("view should be rewritten");
5135
5136        assert!(
5137            !rewritten.sql.contains("USING (b)") && !rewritten.sql.contains("USING(b)"),
5138            "{}",
5139            rewritten.sql
5140        );
5141        assert!(
5142            rewritten.sql.contains("USING (c)") || rewritten.sql.contains("USING(c)"),
5143            "{}",
5144            rewritten.sql
5145        );
5146    }
5147
5148    #[test]
5149    fn test_rewrite_view_sql_group_by_having_branch() {
5150        let schema = schema_with_table("CREATE TABLE t (a, b)");
5151        let view_sql = "CREATE VIEW v AS SELECT b FROM t GROUP BY b HAVING b > 0";
5152
5153        let rewritten =
5154            rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c")
5155                .unwrap()
5156                .expect("view should be rewritten");
5157
5158        assert!(
5159            rewritten
5160                .sql
5161                .contains("SELECT c FROM t GROUP BY c HAVING c > 0"),
5162            "{}",
5163            rewritten.sql
5164        );
5165    }
5166
5167    #[test]
5168    fn test_rewrite_view_sql_window_clause_branch() {
5169        let schema = schema_with_table("CREATE TABLE t (a, b)");
5170        let view_sql = "CREATE VIEW v AS SELECT sum(a) OVER (PARTITION BY b ORDER BY b) FROM t";
5171
5172        let rewritten =
5173            rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c")
5174                .unwrap()
5175                .expect("view should be rewritten");
5176
5177        assert!(
5178            !rewritten.sql.contains("PARTITION BY b"),
5179            "{}",
5180            rewritten.sql
5181        );
5182        assert!(!rewritten.sql.contains("ORDER BY b"), "{}", rewritten.sql);
5183        assert!(
5184            rewritten.sql.contains("PARTITION BY c"),
5185            "{}",
5186            rewritten.sql
5187        );
5188        assert!(rewritten.sql.contains("ORDER BY c"), "{}", rewritten.sql);
5189    }
5190
5191    #[test]
5192    fn test_rewrite_view_sql_limit_offset_branch() {
5193        let schema = schema_with_table("CREATE TABLE t (a, b)");
5194        let view_sql = "CREATE VIEW v AS SELECT a FROM t LIMIT b OFFSET b";
5195
5196        let rewritten =
5197            rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c")
5198                .unwrap()
5199                .expect("view should be rewritten");
5200
5201        assert!(!rewritten.sql.contains("LIMIT b"), "{}", rewritten.sql);
5202        assert!(!rewritten.sql.contains("OFFSET b"), "{}", rewritten.sql);
5203        assert!(rewritten.sql.contains("LIMIT c"), "{}", rewritten.sql);
5204        assert!(rewritten.sql.contains("OFFSET c"), "{}", rewritten.sql);
5205    }
5206
5207    #[test]
5208    fn test_rewrite_view_sql_values_branch() {
5209        let schema = schema_with_table("CREATE TABLE t (a, b)");
5210        let view_sql = "CREATE VIEW v AS VALUES ((SELECT b FROM t LIMIT 1))";
5211
5212        let rewritten =
5213            rewrite_view_sql_for_column_rename(view_sql, &schema, "t", "main", "b", "c")
5214                .unwrap()
5215                .expect("view should be rewritten");
5216
5217        assert!(
5218            rewritten.sql.contains("VALUES ((SELECT c FROM t LIMIT 1))"),
5219            "{}",
5220            rewritten.sql
5221        );
5222    }
5223
5224    #[test]
5225    fn test_indexed_variable_comparison() {
5226        let expr1 = Expr::Variable(Variable::indexed(1u32.try_into().unwrap()));
5227        let expr2 = Expr::Variable(Variable::indexed(1u32.try_into().unwrap()));
5228        assert!(exprs_are_equivalent(&expr1, &expr2));
5229    }
5230
5231    #[test]
5232    fn test_named_variable_comparison() {
5233        let expr1 = Expr::Variable(Variable::named(":a".to_string(), 1u32.try_into().unwrap()));
5234        let expr2 = Expr::Variable(Variable::named(":a".to_string(), 1u32.try_into().unwrap()));
5235        assert!(exprs_are_equivalent(&expr1, &expr2));
5236
5237        let expr1 = Expr::Variable(Variable::named(":a".to_string(), 1u32.try_into().unwrap()));
5238        let expr2 = Expr::Variable(Variable::named(":b".to_string(), 2u32.try_into().unwrap()));
5239        assert!(!exprs_are_equivalent(&expr1, &expr2));
5240    }
5241
5242    #[test]
5243    fn test_basic_addition_exprs_are_equivalent() {
5244        let expr1 = Expr::Binary(
5245            Box::new(Expr::Literal(Literal::Numeric("826".to_string()))),
5246            Add,
5247            Box::new(Expr::Literal(Literal::Numeric("389".to_string()))),
5248        );
5249        let expr2 = Expr::Binary(
5250            Box::new(Expr::Literal(Literal::Numeric("389".to_string()))),
5251            Add,
5252            Box::new(Expr::Literal(Literal::Numeric("826".to_string()))),
5253        );
5254        assert!(exprs_are_equivalent(&expr1, &expr2));
5255    }
5256
5257    #[test]
5258    fn test_addition_expressions_equivalent_normalized() {
5259        // Same types: 123.0 + 243.0 == 243.0 + 123.0 (commutative)
5260        let expr1 = Expr::Binary(
5261            Box::new(Expr::Literal(Literal::Numeric("123.0".to_string()))),
5262            Add,
5263            Box::new(Expr::Literal(Literal::Numeric("243.0".to_string()))),
5264        );
5265        let expr2 = Expr::Binary(
5266            Box::new(Expr::Literal(Literal::Numeric("243.0".to_string()))),
5267            Add,
5268            Box::new(Expr::Literal(Literal::Numeric("123.0".to_string()))),
5269        );
5270        assert!(exprs_are_equivalent(&expr1, &expr2));
5271
5272        // Mixed types are NOT equivalent (different result types)
5273        let expr3 = Expr::Binary(
5274            Box::new(Expr::Literal(Literal::Numeric("123.0".to_string()))),
5275            Add,
5276            Box::new(Expr::Literal(Literal::Numeric("243".to_string()))),
5277        );
5278        let expr4 = Expr::Binary(
5279            Box::new(Expr::Literal(Literal::Numeric("243.0".to_string()))),
5280            Add,
5281            Box::new(Expr::Literal(Literal::Numeric("123".to_string()))),
5282        );
5283        assert!(!exprs_are_equivalent(&expr3, &expr4));
5284    }
5285
5286    #[test]
5287    fn test_subtraction_expressions_not_equivalent() {
5288        let expr3 = Expr::Binary(
5289            Box::new(Expr::Literal(Literal::Numeric("364".to_string()))),
5290            Subtract,
5291            Box::new(Expr::Literal(Literal::Numeric("22.0".to_string()))),
5292        );
5293        let expr4 = Expr::Binary(
5294            Box::new(Expr::Literal(Literal::Numeric("22.0".to_string()))),
5295            Subtract,
5296            Box::new(Expr::Literal(Literal::Numeric("364".to_string()))),
5297        );
5298        assert!(!exprs_are_equivalent(&expr3, &expr4));
5299    }
5300
5301    #[test]
5302    fn test_subtraction_expressions_normalized() {
5303        // Same types: 66.0 - 22.0 == 66.0 - 22.0
5304        let expr3 = Expr::Binary(
5305            Box::new(Expr::Literal(Literal::Numeric("66.0".to_string()))),
5306            Subtract,
5307            Box::new(Expr::Literal(Literal::Numeric("22.0".to_string()))),
5308        );
5309        let expr4 = Expr::Binary(
5310            Box::new(Expr::Literal(Literal::Numeric("66.0".to_string()))),
5311            Subtract,
5312            Box::new(Expr::Literal(Literal::Numeric("22.0".to_string()))),
5313        );
5314        assert!(exprs_are_equivalent(&expr3, &expr4));
5315
5316        // Mixed types are NOT equivalent
5317        let expr5 = Expr::Binary(
5318            Box::new(Expr::Literal(Literal::Numeric("66.0".to_string()))),
5319            Subtract,
5320            Box::new(Expr::Literal(Literal::Numeric("22".to_string()))),
5321        );
5322        let expr6 = Expr::Binary(
5323            Box::new(Expr::Literal(Literal::Numeric("66".to_string()))),
5324            Subtract,
5325            Box::new(Expr::Literal(Literal::Numeric("22.0".to_string()))),
5326        );
5327        assert!(!exprs_are_equivalent(&expr5, &expr6));
5328    }
5329
5330    #[test]
5331    fn test_expressions_equivalent_case_insensitive_functioncalls() {
5332        let func1 = Expr::FunctionCall {
5333            name: Name::exact("SUM".to_string()),
5334            distinctness: None,
5335            args: vec![Expr::Id(Name::exact("x".to_string())).into()],
5336            order_by: vec![],
5337            within_group: vec![],
5338            filter_over: FunctionTail {
5339                filter_clause: None,
5340                over_clause: None,
5341            },
5342        };
5343        let func2 = Expr::FunctionCall {
5344            name: Name::exact("sum".to_string()),
5345            distinctness: None,
5346            args: vec![Expr::Id(Name::exact("x".to_string())).into()],
5347            order_by: vec![],
5348            within_group: vec![],
5349            filter_over: FunctionTail {
5350                filter_clause: None,
5351                over_clause: None,
5352            },
5353        };
5354        assert!(exprs_are_equivalent(&func1, &func2));
5355
5356        let func3 = Expr::FunctionCall {
5357            name: Name::exact("SUM".to_string()),
5358            distinctness: Some(ast::Distinctness::Distinct),
5359            args: vec![Expr::Id(Name::exact("x".to_string())).into()],
5360            order_by: vec![],
5361            within_group: vec![],
5362            filter_over: FunctionTail {
5363                filter_clause: None,
5364                over_clause: None,
5365            },
5366        };
5367        assert!(!exprs_are_equivalent(&func1, &func3));
5368    }
5369
5370    #[test]
5371    fn test_expressions_equivalent_identical_fn_with_distinct() {
5372        let sum = Expr::FunctionCall {
5373            name: Name::exact("SUM".to_string()),
5374            distinctness: None,
5375            args: vec![Expr::Id(Name::exact("x".to_string())).into()],
5376            order_by: vec![],
5377            within_group: vec![],
5378            filter_over: FunctionTail {
5379                filter_clause: None,
5380                over_clause: None,
5381            },
5382        };
5383        let sum_distinct = Expr::FunctionCall {
5384            name: Name::exact("SUM".to_string()),
5385            distinctness: Some(ast::Distinctness::Distinct),
5386            args: vec![Expr::Id(Name::exact("x".to_string())).into()],
5387            order_by: vec![],
5388            within_group: vec![],
5389            filter_over: FunctionTail {
5390                filter_clause: None,
5391                over_clause: None,
5392            },
5393        };
5394        assert!(!exprs_are_equivalent(&sum, &sum_distinct));
5395    }
5396
5397    #[test]
5398    fn test_expressions_equivalent_multiplication() {
5399        // Same types: 42.0 * 38.0 == 38.0 * 42.0 (commutative)
5400        let expr1 = Expr::Binary(
5401            Box::new(Expr::Literal(Literal::Numeric("42.0".to_string()))),
5402            Multiply,
5403            Box::new(Expr::Literal(Literal::Numeric("38.0".to_string()))),
5404        );
5405        let expr2 = Expr::Binary(
5406            Box::new(Expr::Literal(Literal::Numeric("38.0".to_string()))),
5407            Multiply,
5408            Box::new(Expr::Literal(Literal::Numeric("42.0".to_string()))),
5409        );
5410        assert!(exprs_are_equivalent(&expr1, &expr2));
5411    }
5412
5413    #[test]
5414    fn test_expressions_both_parenthesized_equivalent() {
5415        // Same types: (683 + 799) == 799 + 683 (commutative, integers only)
5416        let expr1 = Expr::Parenthesized(vec![Expr::Binary(
5417            Box::new(Expr::Literal(Literal::Numeric("683".to_string()))),
5418            Add,
5419            Box::new(Expr::Literal(Literal::Numeric("799".to_string()))),
5420        )
5421        .into()]);
5422        let expr2 = Expr::Binary(
5423            Box::new(Expr::Literal(Literal::Numeric("799".to_string()))),
5424            Add,
5425            Box::new(Expr::Literal(Literal::Numeric("683".to_string()))),
5426        );
5427        assert!(exprs_are_equivalent(&expr1, &expr2));
5428    }
5429    #[test]
5430    fn test_expressions_parenthesized_equivalent() {
5431        let expr7 = Expr::Parenthesized(vec![Expr::Binary(
5432            Box::new(Expr::Literal(Literal::Numeric("6".to_string()))),
5433            Add,
5434            Box::new(Expr::Literal(Literal::Numeric("7".to_string()))),
5435        )
5436        .into()]);
5437        let expr8 = Expr::Binary(
5438            Box::new(Expr::Literal(Literal::Numeric("6".to_string()))),
5439            Add,
5440            Box::new(Expr::Literal(Literal::Numeric("7".to_string()))),
5441        );
5442        assert!(exprs_are_equivalent(&expr7, &expr8));
5443    }
5444
5445    #[test]
5446    fn test_like_expressions_equivalent() {
5447        let expr1 = Expr::Like {
5448            lhs: Box::new(Expr::Id(Name::exact("name".to_string()))),
5449            not: false,
5450            op: ast::LikeOperator::Like,
5451            rhs: Box::new(Expr::Literal(Literal::String("%john%".to_string()))),
5452            escape: Some(Box::new(Expr::Literal(Literal::String("\\".to_string())))),
5453        };
5454        let expr2 = Expr::Like {
5455            lhs: Box::new(Expr::Id(Name::exact("name".to_string()))),
5456            not: false,
5457            op: ast::LikeOperator::Like,
5458            rhs: Box::new(Expr::Literal(Literal::String("%john%".to_string()))),
5459            escape: Some(Box::new(Expr::Literal(Literal::String("\\".to_string())))),
5460        };
5461        assert!(exprs_are_equivalent(&expr1, &expr2));
5462    }
5463
5464    #[test]
5465    fn test_expressions_equivalent_like_escaped() {
5466        let expr1 = Expr::Like {
5467            lhs: Box::new(Expr::Id(Name::exact("name".to_string()))),
5468            not: false,
5469            op: ast::LikeOperator::Like,
5470            rhs: Box::new(Expr::Literal(Literal::String("%john%".to_string()))),
5471            escape: Some(Box::new(Expr::Literal(Literal::String("\\".to_string())))),
5472        };
5473        let expr2 = Expr::Like {
5474            lhs: Box::new(Expr::Id(Name::exact("name".to_string()))),
5475            not: false,
5476            op: ast::LikeOperator::Like,
5477            rhs: Box::new(Expr::Literal(Literal::String("%john%".to_string()))),
5478            escape: Some(Box::new(Expr::Literal(Literal::String("#".to_string())))),
5479        };
5480        assert!(!exprs_are_equivalent(&expr1, &expr2));
5481    }
5482    #[test]
5483    fn test_expressions_equivalent_between() {
5484        let expr1 = Expr::Between {
5485            lhs: Box::new(Expr::Id(Name::exact("age".to_string()))),
5486            not: false,
5487            start: Box::new(Expr::Literal(Literal::Numeric("18".to_string()))),
5488            end: Box::new(Expr::Literal(Literal::Numeric("65".to_string()))),
5489        };
5490        let expr2 = Expr::Between {
5491            lhs: Box::new(Expr::Id(Name::exact("age".to_string()))),
5492            not: false,
5493            start: Box::new(Expr::Literal(Literal::Numeric("18".to_string()))),
5494            end: Box::new(Expr::Literal(Literal::Numeric("65".to_string()))),
5495        };
5496        assert!(exprs_are_equivalent(&expr1, &expr2));
5497
5498        // differing BETWEEN bounds
5499        let expr3 = Expr::Between {
5500            lhs: Box::new(Expr::Id(Name::exact("age".to_string()))),
5501            not: false,
5502            start: Box::new(Expr::Literal(Literal::Numeric("20".to_string()))),
5503            end: Box::new(Expr::Literal(Literal::Numeric("65".to_string()))),
5504        };
5505        assert!(!exprs_are_equivalent(&expr1, &expr3));
5506    }
5507    #[test]
5508    fn test_cast_exprs_equivalent() {
5509        let cast1 = Expr::Cast {
5510            expr: Box::new(Expr::Literal(Literal::Numeric("123".to_string()))),
5511            type_name: Some(Type {
5512                name: "INTEGER".to_string(),
5513                size: None,
5514                array_dimensions: 0,
5515            }),
5516        };
5517
5518        let cast2 = Expr::Cast {
5519            expr: Box::new(Expr::Literal(Literal::Numeric("123".to_string()))),
5520            type_name: Some(Type {
5521                name: "integer".to_string(),
5522                size: None,
5523                array_dimensions: 0,
5524            }),
5525        };
5526        assert!(exprs_are_equivalent(&cast1, &cast2));
5527    }
5528
5529    #[test]
5530    fn test_ident_equivalency() {
5531        assert!(check_ident_equivalency("\"foo\"", "foo"));
5532        assert!(check_ident_equivalency("[foo]", "foo"));
5533        assert!(check_ident_equivalency("`FOO`", "foo"));
5534        assert!(check_ident_equivalency("\"foo\"", "`FOO`"));
5535        assert!(!check_ident_equivalency("\"foo\"", "[bar]"));
5536        assert!(!check_ident_equivalency("foo", "\"bar\""));
5537    }
5538
5539    #[test]
5540    fn test_simple_uri() {
5541        let uri = "file:/home/user/db.sqlite";
5542        let opts = OpenOptions::parse(uri).unwrap();
5543        assert_eq!(opts.path, "/home/user/db.sqlite");
5544        assert_eq!(opts.authority, None);
5545    }
5546
5547    #[test]
5548    fn test_uri_with_authority() {
5549        let uri = "file://localhost/home/user/db.sqlite";
5550        let opts = OpenOptions::parse(uri).unwrap();
5551        assert_eq!(opts.path, "/home/user/db.sqlite");
5552        assert_eq!(opts.authority, Some("localhost"));
5553    }
5554
5555    #[test]
5556    fn test_uri_with_invalid_authority() {
5557        let uri = "file://example.com/home/user/db.sqlite";
5558        let result = OpenOptions::parse(uri);
5559        assert!(result.is_err());
5560    }
5561
5562    #[test]
5563    fn test_uri_with_query_params() {
5564        let uri = "file:/home/user/db.sqlite?vfs=unix&mode=ro&immutable=1";
5565        let opts = OpenOptions::parse(uri).unwrap();
5566        assert_eq!(opts.path, "/home/user/db.sqlite");
5567        assert_eq!(opts.vfs, Some("unix".to_string()));
5568        assert_eq!(opts.mode, OpenMode::ReadOnly);
5569        assert!(opts.immutable);
5570    }
5571
5572    #[test]
5573    fn test_uri_with_fragment() {
5574        let uri = "file:/home/user/db.sqlite#section1";
5575        let opts = OpenOptions::parse(uri).unwrap();
5576        assert_eq!(opts.path, "/home/user/db.sqlite");
5577    }
5578
5579    #[test]
5580    fn test_uri_with_percent_encoding() {
5581        let uri = "file:/home/user/db%20with%20spaces.sqlite?vfs=unix";
5582        let opts = OpenOptions::parse(uri).unwrap();
5583        assert_eq!(opts.path, "/home/user/db with spaces.sqlite");
5584        assert_eq!(opts.vfs, Some("unix".to_string()));
5585    }
5586
5587    #[test]
5588    fn test_uri_without_scheme() {
5589        let uri = "/home/user/db.sqlite";
5590        let result = OpenOptions::parse(uri);
5591        assert!(result.is_ok());
5592        assert_eq!(result.unwrap().path, "/home/user/db.sqlite");
5593    }
5594
5595    #[test]
5596    fn test_uri_with_empty_query() {
5597        let uri = "file:/home/user/db.sqlite?";
5598        let opts = OpenOptions::parse(uri).unwrap();
5599        assert_eq!(opts.path, "/home/user/db.sqlite");
5600        assert_eq!(opts.vfs, None);
5601    }
5602
5603    #[test]
5604    fn test_uri_with_partial_query() {
5605        let uri = "file:/home/user/db.sqlite?mode=rw";
5606        let opts = OpenOptions::parse(uri).unwrap();
5607        assert_eq!(opts.path, "/home/user/db.sqlite");
5608        assert_eq!(opts.mode, OpenMode::ReadWrite);
5609        assert_eq!(opts.vfs, None);
5610    }
5611
5612    #[test]
5613    fn test_uri_windows_style_path() {
5614        let uri = "file:///C:/Users/test/db.sqlite";
5615        let opts = OpenOptions::parse(uri).unwrap();
5616        assert_eq!(opts.path, "/C:/Users/test/db.sqlite");
5617    }
5618
5619    #[test]
5620    fn test_uri_with_only_query_params() {
5621        let uri = "file:?mode=memory&cache=shared";
5622        let opts = OpenOptions::parse(uri).unwrap();
5623        assert_eq!(opts.path, "");
5624        assert_eq!(opts.mode, OpenMode::Memory);
5625        assert_eq!(opts.cache, CacheMode::Shared);
5626    }
5627
5628    #[test]
5629    fn test_uri_with_only_fragment() {
5630        let uri = "file:#fragment";
5631        let opts = OpenOptions::parse(uri).unwrap();
5632        assert_eq!(opts.path, "");
5633    }
5634
5635    #[test]
5636    fn test_uri_with_invalid_scheme() {
5637        let uri = "http:/home/user/db.sqlite";
5638        let result = OpenOptions::parse(uri);
5639        assert!(result.is_ok());
5640        assert_eq!(result.unwrap().path, "http:/home/user/db.sqlite");
5641    }
5642
5643    #[test]
5644    fn test_uri_with_multiple_query_params() {
5645        let uri = "file:/home/user/db.sqlite?vfs=unix&mode=rw&cache=private&immutable=0";
5646        let opts = OpenOptions::parse(uri).unwrap();
5647        assert_eq!(opts.path, "/home/user/db.sqlite");
5648        assert_eq!(opts.vfs, Some("unix".to_string()));
5649        assert_eq!(opts.mode, OpenMode::ReadWrite);
5650        assert_eq!(opts.cache, CacheMode::Private);
5651        assert!(!opts.immutable);
5652    }
5653
5654    #[test]
5655    fn test_uri_with_unknown_query_param() {
5656        let uri = "file:/home/user/db.sqlite?unknown=param";
5657        let opts = OpenOptions::parse(uri).unwrap();
5658        assert_eq!(opts.path, "/home/user/db.sqlite");
5659        assert_eq!(opts.vfs, None);
5660    }
5661
5662    #[test]
5663    fn test_uri_with_multiple_equal_signs() {
5664        let uri = "file:/home/user/db.sqlite?vfs=unix=custom";
5665        let opts = OpenOptions::parse(uri).unwrap();
5666        assert_eq!(opts.path, "/home/user/db.sqlite");
5667        assert_eq!(opts.vfs, Some("unix=custom".to_string()));
5668    }
5669
5670    #[test]
5671    fn test_uri_with_trailing_slash() {
5672        let uri = "file:/home/user/db.sqlite/";
5673        let opts = OpenOptions::parse(uri).unwrap();
5674        assert_eq!(opts.path, "/home/user/db.sqlite/");
5675    }
5676
5677    #[test]
5678    fn test_uri_with_encoded_characters_in_query() {
5679        let uri = "file:/home/user/db.sqlite?vfs=unix%20mode";
5680        let opts = OpenOptions::parse(uri).unwrap();
5681        assert_eq!(opts.path, "/home/user/db.sqlite");
5682        assert_eq!(opts.vfs, Some("unix mode".to_string()));
5683    }
5684
5685    #[test]
5686    fn test_uri_windows_network_path() {
5687        let uri = "file://server/share/db.sqlite";
5688        let result = OpenOptions::parse(uri);
5689        assert!(result.is_err()); // non-localhost authority should fail
5690    }
5691
5692    #[test]
5693    fn test_uri_windows_drive_letter_with_slash() {
5694        let uri = "file:///C:/database.sqlite";
5695        let opts = OpenOptions::parse(uri).unwrap();
5696        assert_eq!(opts.path, "/C:/database.sqlite");
5697    }
5698
5699    #[test]
5700    fn test_localhost_with_double_slash_and_no_path() {
5701        let uri = "file://localhost";
5702        let opts = OpenOptions::parse(uri).unwrap();
5703        assert_eq!(opts.path, "");
5704        assert_eq!(opts.authority, Some("localhost"));
5705    }
5706
5707    #[test]
5708    fn test_uri_windows_drive_letter_without_slash() {
5709        let uri = "file:///C:/database.sqlite";
5710        let opts = OpenOptions::parse(uri).unwrap();
5711        assert_eq!(opts.path, "/C:/database.sqlite");
5712    }
5713
5714    #[test]
5715    fn test_improper_mode() {
5716        // any other mode but ro, rwc, rw, memory should fail per sqlite
5717
5718        let uri = "file:data.db?mode=readonly";
5719        let res = OpenOptions::parse(uri);
5720        assert!(res.is_err());
5721        // including empty
5722        let uri = "file:/home/user/db.sqlite?vfs=&mode=";
5723        let res = OpenOptions::parse(uri);
5724        assert!(res.is_err());
5725    }
5726
5727    // Some examples from https://www.sqlite.org/c3ref/open.html#urifilenameexamples
5728    #[test]
5729    fn test_simple_file_current_dir() {
5730        let uri = "file:data.db";
5731        let opts = OpenOptions::parse(uri).unwrap();
5732        assert_eq!(opts.path, "data.db");
5733        assert_eq!(opts.authority, None);
5734        assert_eq!(opts.vfs, None);
5735        assert_eq!(opts.mode, OpenMode::ReadWriteCreate);
5736    }
5737
5738    #[test]
5739    fn test_simple_file_three_slash() {
5740        let uri = "file:///home/data/data.db";
5741        let opts = OpenOptions::parse(uri).unwrap();
5742        assert_eq!(opts.path, "/home/data/data.db");
5743        assert_eq!(opts.authority, None);
5744        assert_eq!(opts.vfs, None);
5745        assert_eq!(opts.mode, OpenMode::ReadWriteCreate);
5746    }
5747
5748    #[test]
5749    fn test_simple_file_two_slash_localhost() {
5750        let uri = "file://localhost/home/fred/data.db";
5751        let opts = OpenOptions::parse(uri).unwrap();
5752        assert_eq!(opts.path, "/home/fred/data.db");
5753        assert_eq!(opts.authority, Some("localhost"));
5754        assert_eq!(opts.vfs, None);
5755    }
5756
5757    #[test]
5758    fn test_windows_double_invalid() {
5759        let uri = "file://C:/home/fred/data.db?mode=ro";
5760        let opts = OpenOptions::parse(uri);
5761        assert!(opts.is_err());
5762    }
5763
5764    #[test]
5765    fn test_simple_file_two_slash() {
5766        let uri = "file:///C:/Documents%20and%20Settings/fred/Desktop/data.db";
5767        let opts = OpenOptions::parse(uri).unwrap();
5768        assert_eq!(opts.path, "/C:/Documents and Settings/fred/Desktop/data.db");
5769        assert_eq!(opts.vfs, None);
5770    }
5771
5772    #[test]
5773    fn test_decode_percent_basic() {
5774        assert_eq!(decode_percent("hello%20world"), "hello world");
5775        assert_eq!(decode_percent("file%3Adata.db"), "file:data.db");
5776        assert_eq!(decode_percent("path%2Fto%2Ffile"), "path/to/file");
5777    }
5778
5779    #[test]
5780    fn test_decode_percent_edge_cases() {
5781        assert_eq!(decode_percent(""), "");
5782        assert_eq!(decode_percent("plain_text"), "plain_text");
5783        assert_eq!(
5784            decode_percent("%2Fhome%2Fuser%2Fdb.sqlite"),
5785            "/home/user/db.sqlite"
5786        );
5787        // multiple percent-encoded characters in sequence
5788        assert_eq!(decode_percent("%41%42%43"), "ABC");
5789        assert_eq!(decode_percent("%61%62%63"), "abc");
5790    }
5791
5792    #[test]
5793    fn test_decode_percent_invalid_sequences() {
5794        // invalid percent encoding (single % without two hex digits)
5795        assert_eq!(decode_percent("hello%"), "hello%");
5796        // only one hex digit after %
5797        assert_eq!(decode_percent("file%2"), "file%2");
5798        // invalid hex digits (not 0-9, A-F, a-f)
5799        assert_eq!(decode_percent("file%2X.db"), "file%2X.db");
5800
5801        // Incomplete sequence at the end, leave untouched
5802        assert_eq!(decode_percent("path%2Fto%2"), "path/to%2");
5803    }
5804
5805    #[test]
5806    fn test_decode_percent_mixed_valid_invalid() {
5807        assert_eq!(decode_percent("hello%20world%"), "hello world%");
5808        assert_eq!(decode_percent("%2Fpath%2Xto%2Ffile"), "/path%2Xto/file");
5809        assert_eq!(decode_percent("file%3Adata.db%2"), "file:data.db%2");
5810    }
5811
5812    #[test]
5813    fn test_decode_percent_special_characters() {
5814        assert_eq!(
5815            decode_percent("%21%40%23%24%25%5E%26%2A%28%29"),
5816            "!@#$%^&*()"
5817        );
5818        assert_eq!(decode_percent("%5B%5D%7B%7D%7C%5C%3A"), "[]{}|\\:");
5819    }
5820
5821    #[test]
5822    fn test_decode_percent_unmodified_valid_text() {
5823        // ensure already valid text remains unchanged
5824        assert_eq!(
5825            decode_percent("C:/Users/Example/Database.sqlite"),
5826            "C:/Users/Example/Database.sqlite"
5827        );
5828        assert_eq!(
5829            decode_percent("/home/user/db.sqlite"),
5830            "/home/user/db.sqlite"
5831        );
5832    }
5833
5834    #[test]
5835    fn test_text_to_integer() {
5836        assert_eq!(
5837            checked_cast_text_to_numeric("1", false).unwrap(),
5838            Value::from_i64(1)
5839        );
5840        assert_eq!(
5841            checked_cast_text_to_numeric("-1", false).unwrap(),
5842            Value::from_i64(-1)
5843        );
5844        assert_eq!(
5845            checked_cast_text_to_numeric("1823400-00000", false).unwrap(),
5846            Value::from_i64(1823400)
5847        );
5848        assert_eq!(
5849            checked_cast_text_to_numeric("-10000000", false).unwrap(),
5850            Value::from_i64(-10000000)
5851        );
5852        assert_eq!(
5853            checked_cast_text_to_numeric("123xxx", false).unwrap(),
5854            Value::from_i64(123)
5855        );
5856        assert_eq!(
5857            checked_cast_text_to_numeric("9223372036854775807", false).unwrap(),
5858            Value::from_i64(i64::MAX)
5859        );
5860        // Overflow becomes Float (different from cast_text_to_integer which returned 0)
5861        assert_eq!(
5862            checked_cast_text_to_numeric("9223372036854775808", false).unwrap(),
5863            Value::from_f64(9.22337203685478e18)
5864        );
5865        assert_eq!(
5866            checked_cast_text_to_numeric("-9223372036854775808", false).unwrap(),
5867            Value::from_i64(i64::MIN)
5868        );
5869        // Overflow becomes Float (different from cast_text_to_integer which returned 0)
5870        assert_eq!(
5871            checked_cast_text_to_numeric("-9223372036854775809", false).unwrap(),
5872            Value::from_f64(-9.22337203685478e18)
5873        );
5874        assert!(checked_cast_text_to_numeric("-", false).is_err());
5875    }
5876
5877    #[test]
5878    fn test_text_to_real() {
5879        assert_eq!(
5880            checked_cast_text_to_numeric("1", false).unwrap(),
5881            Value::from_i64(1)
5882        );
5883        assert_eq!(
5884            checked_cast_text_to_numeric("-1", false).unwrap(),
5885            Value::from_i64(-1)
5886        );
5887        assert_eq!(
5888            checked_cast_text_to_numeric("1.0", false).unwrap(),
5889            Value::from_i64(1)
5890        );
5891        assert_eq!(
5892            checked_cast_text_to_numeric("-1.0", false).unwrap(),
5893            Value::from_i64(-1)
5894        );
5895        assert_eq!(
5896            checked_cast_text_to_numeric("1e10", false).unwrap(),
5897            Value::from_i64(10_000_000_000)
5898        );
5899        assert_eq!(
5900            checked_cast_text_to_numeric("-1e10", false).unwrap(),
5901            Value::from_i64(-10_000_000_000)
5902        );
5903        assert_eq!(
5904            checked_cast_text_to_numeric("1e-10", false).unwrap(),
5905            Value::from_f64(1e-10)
5906        );
5907        assert_eq!(
5908            checked_cast_text_to_numeric("-1e-10", false).unwrap(),
5909            Value::from_f64(-1e-10)
5910        );
5911        assert_eq!(
5912            checked_cast_text_to_numeric("1.123e10", false).unwrap(),
5913            Value::from_i64(11_230_000_000)
5914        );
5915        assert_eq!(
5916            checked_cast_text_to_numeric("-1.123e10", false).unwrap(),
5917            Value::from_i64(-11_230_000_000)
5918        );
5919        assert_eq!(
5920            checked_cast_text_to_numeric("1.123e-10", false).unwrap(),
5921            Value::from_f64(1.123e-10)
5922        );
5923        assert_eq!(
5924            checked_cast_text_to_numeric("-1.123-e-10", false).unwrap(),
5925            Value::from_f64(-1.123)
5926        );
5927        assert_eq!(
5928            checked_cast_text_to_numeric("1-282584294928", false).unwrap(),
5929            Value::from_i64(1)
5930        );
5931        assert_eq!(
5932            checked_cast_text_to_numeric("1.7976931348623157e309", false).unwrap(),
5933            Value::from_f64(f64::INFINITY),
5934        );
5935        assert_eq!(
5936            checked_cast_text_to_numeric("-1.7976931348623157e308", false).unwrap(),
5937            Value::from_f64(f64::MIN),
5938        );
5939        assert_eq!(
5940            checked_cast_text_to_numeric("-1.7976931348623157e309", false).unwrap(),
5941            Value::from_f64(f64::NEG_INFINITY),
5942        );
5943        assert_eq!(
5944            checked_cast_text_to_numeric("1E", false).unwrap(),
5945            Value::from_i64(1)
5946        );
5947        assert_eq!(
5948            checked_cast_text_to_numeric("1EE", false).unwrap(),
5949            Value::from_i64(1)
5950        );
5951        assert_eq!(
5952            checked_cast_text_to_numeric("-1E", false).unwrap(),
5953            Value::from_i64(-1)
5954        );
5955        assert_eq!(
5956            checked_cast_text_to_numeric("1.", false).unwrap(),
5957            Value::from_i64(1)
5958        );
5959        assert_eq!(
5960            checked_cast_text_to_numeric("-1.", false).unwrap(),
5961            Value::from_i64(-1)
5962        );
5963        assert_eq!(
5964            checked_cast_text_to_numeric("1.23E", false).unwrap(),
5965            Value::from_f64(1.23)
5966        );
5967        assert_eq!(
5968            checked_cast_text_to_numeric(".1.23E-", false).unwrap(),
5969            Value::from_f64(0.1)
5970        );
5971        assert_eq!(
5972            checked_cast_text_to_numeric("0", false).unwrap(),
5973            Value::from_i64(0)
5974        );
5975        assert_eq!(
5976            checked_cast_text_to_numeric("-0", false).unwrap(),
5977            Value::from_i64(0)
5978        );
5979        assert_eq!(
5980            checked_cast_text_to_numeric("-0", false).unwrap(),
5981            Value::from_i64(0)
5982        );
5983        assert_eq!(
5984            checked_cast_text_to_numeric("-0.0", false).unwrap(),
5985            Value::from_i64(0)
5986        );
5987        assert_eq!(
5988            checked_cast_text_to_numeric("0.0", false).unwrap(),
5989            Value::from_i64(0)
5990        );
5991        assert!(checked_cast_text_to_numeric("-", false).is_err());
5992    }
5993
5994    #[test]
5995    fn test_text_to_numeric() {
5996        assert_eq!(
5997            checked_cast_text_to_numeric("1", false).unwrap(),
5998            Value::from_i64(1)
5999        );
6000        assert_eq!(
6001            checked_cast_text_to_numeric("-1", false).unwrap(),
6002            Value::from_i64(-1)
6003        );
6004        assert_eq!(
6005            checked_cast_text_to_numeric("1823400-00000", false).unwrap(),
6006            Value::from_i64(1823400)
6007        );
6008        assert_eq!(
6009            checked_cast_text_to_numeric("-10000000", false).unwrap(),
6010            Value::from_i64(-10000000)
6011        );
6012        assert_eq!(
6013            checked_cast_text_to_numeric("123xxx", false).unwrap(),
6014            Value::from_i64(123)
6015        );
6016        assert_eq!(
6017            checked_cast_text_to_numeric("9223372036854775807", false).unwrap(),
6018            Value::from_i64(i64::MAX)
6019        );
6020        assert_eq!(
6021            checked_cast_text_to_numeric("9223372036854775808", false).unwrap(),
6022            Value::from_f64(9.22337203685478e18)
6023        ); // Exceeds i64, becomes float
6024        assert_eq!(
6025            checked_cast_text_to_numeric("-9223372036854775808", false).unwrap(),
6026            Value::from_i64(i64::MIN)
6027        );
6028        assert_eq!(
6029            checked_cast_text_to_numeric("-9223372036854775809", false).unwrap(),
6030            Value::from_f64(-9.22337203685478e18)
6031        ); // Exceeds i64, becomes float
6032
6033        assert_eq!(
6034            checked_cast_text_to_numeric("1.0", false).unwrap(),
6035            Value::from_i64(1)
6036        );
6037        assert_eq!(
6038            checked_cast_text_to_numeric("-1.0", false).unwrap(),
6039            Value::from_i64(-1)
6040        );
6041        assert_eq!(
6042            checked_cast_text_to_numeric("1e10", false).unwrap(),
6043            Value::from_i64(10_000_000_000)
6044        );
6045        assert_eq!(
6046            checked_cast_text_to_numeric("-1e10", false).unwrap(),
6047            Value::from_i64(-10_000_000_000)
6048        );
6049        assert_eq!(
6050            checked_cast_text_to_numeric("1e-10", false).unwrap(),
6051            Value::from_f64(1e-10)
6052        );
6053        assert_eq!(
6054            checked_cast_text_to_numeric("-1e-10", false).unwrap(),
6055            Value::from_f64(-1e-10)
6056        );
6057        assert_eq!(
6058            checked_cast_text_to_numeric("1.123e10", false).unwrap(),
6059            Value::from_i64(11_230_000_000)
6060        );
6061        assert_eq!(
6062            checked_cast_text_to_numeric("-1.123e10", false).unwrap(),
6063            Value::from_i64(-11_230_000_000)
6064        );
6065        assert_eq!(
6066            checked_cast_text_to_numeric("1.123e-10", false).unwrap(),
6067            Value::from_f64(1.123e-10)
6068        );
6069        assert_eq!(
6070            checked_cast_text_to_numeric("-1.123-e-10", false).unwrap(),
6071            Value::from_f64(-1.123)
6072        );
6073        assert_eq!(
6074            checked_cast_text_to_numeric("1-282584294928", false).unwrap(),
6075            Value::from_i64(1)
6076        );
6077        assert!(checked_cast_text_to_numeric("xxx", false).is_err());
6078        assert_eq!(
6079            checked_cast_text_to_numeric("1.7976931348623157e309", false).unwrap(),
6080            Value::from_f64(f64::INFINITY)
6081        );
6082        assert_eq!(
6083            checked_cast_text_to_numeric("-1.7976931348623157e308", false).unwrap(),
6084            Value::from_f64(f64::MIN)
6085        );
6086        assert_eq!(
6087            checked_cast_text_to_numeric("-1.7976931348623157e309", false).unwrap(),
6088            Value::from_f64(f64::NEG_INFINITY)
6089        );
6090
6091        assert_eq!(
6092            checked_cast_text_to_numeric("1E", false).unwrap(),
6093            Value::from_i64(1)
6094        );
6095        assert_eq!(
6096            checked_cast_text_to_numeric("1EE", false).unwrap(),
6097            Value::from_i64(1)
6098        );
6099        assert_eq!(
6100            checked_cast_text_to_numeric("-1E", false).unwrap(),
6101            Value::from_i64(-1)
6102        );
6103        assert_eq!(
6104            checked_cast_text_to_numeric("1.", false).unwrap(),
6105            Value::from_i64(1)
6106        );
6107        assert_eq!(
6108            checked_cast_text_to_numeric("-1.", false).unwrap(),
6109            Value::from_i64(-1)
6110        );
6111        assert_eq!(
6112            checked_cast_text_to_numeric("1.23E", false).unwrap(),
6113            Value::from_f64(1.23)
6114        );
6115        assert_eq!(
6116            checked_cast_text_to_numeric("1.23E-", false).unwrap(),
6117            Value::from_f64(1.23)
6118        );
6119
6120        assert_eq!(
6121            checked_cast_text_to_numeric("0", false).unwrap(),
6122            Value::from_i64(0)
6123        );
6124        assert_eq!(
6125            checked_cast_text_to_numeric("-0", false).unwrap(),
6126            Value::from_i64(0)
6127        );
6128        assert_eq!(
6129            checked_cast_text_to_numeric("-0.0", false).unwrap(),
6130            Value::from_i64(0)
6131        );
6132        assert_eq!(
6133            checked_cast_text_to_numeric("0.0", false).unwrap(),
6134            Value::from_i64(0)
6135        );
6136        assert!(checked_cast_text_to_numeric("-", false).is_err());
6137        assert_eq!(
6138            checked_cast_text_to_numeric("-e", false).unwrap(),
6139            Value::from_f64(0.0)
6140        );
6141        assert_eq!(
6142            checked_cast_text_to_numeric("-E", false).unwrap(),
6143            Value::from_f64(0.0)
6144        );
6145    }
6146
6147    #[test]
6148    fn test_parse_numeric_str_valid_integer() {
6149        assert_eq!(parse_numeric_str("123"), Ok((ValueType::Integer, "123")));
6150        assert_eq!(parse_numeric_str("-456"), Ok((ValueType::Integer, "-456")));
6151        assert_eq!(parse_numeric_str("+789"), Ok((ValueType::Integer, "+789")));
6152        assert_eq!(
6153            parse_numeric_str("000789"),
6154            Ok((ValueType::Integer, "000789"))
6155        );
6156    }
6157
6158    #[test]
6159    fn test_parse_numeric_str_valid_float() {
6160        assert_eq!(
6161            parse_numeric_str("123.456"),
6162            Ok((ValueType::Float, "123.456"))
6163        );
6164        assert_eq!(
6165            parse_numeric_str("-0.789"),
6166            Ok((ValueType::Float, "-0.789"))
6167        );
6168        assert_eq!(
6169            parse_numeric_str("+0.789"),
6170            Ok((ValueType::Float, "+0.789"))
6171        );
6172        assert_eq!(parse_numeric_str("1e10"), Ok((ValueType::Float, "1e10")));
6173        assert_eq!(parse_numeric_str("+1e10"), Ok((ValueType::Float, "+1e10")));
6174        assert_eq!(
6175            parse_numeric_str("-1.23e-4"),
6176            Ok((ValueType::Float, "-1.23e-4"))
6177        );
6178        assert_eq!(
6179            parse_numeric_str("1.23E+4"),
6180            Ok((ValueType::Float, "1.23E+4"))
6181        );
6182        assert_eq!(parse_numeric_str("1.2.3"), Ok((ValueType::Float, "1.2")))
6183    }
6184
6185    #[test]
6186    fn test_parse_numeric_str_edge_cases() {
6187        assert_eq!(parse_numeric_str("1e"), Ok((ValueType::Float, "1")));
6188        assert_eq!(parse_numeric_str("1e-"), Ok((ValueType::Float, "1")));
6189        assert_eq!(parse_numeric_str("1e+"), Ok((ValueType::Float, "1")));
6190        assert_eq!(parse_numeric_str("-1e"), Ok((ValueType::Float, "-1")));
6191        assert_eq!(parse_numeric_str("-1e-"), Ok((ValueType::Float, "-1")));
6192    }
6193
6194    #[test]
6195    fn test_parse_numeric_str_invalid() {
6196        assert_eq!(parse_numeric_str(""), Err(()));
6197        assert_eq!(parse_numeric_str("abc"), Err(()));
6198        assert_eq!(parse_numeric_str("-"), Err(()));
6199        assert_eq!(parse_numeric_str("+"), Err(()));
6200        assert_eq!(parse_numeric_str("e10"), Err(()));
6201        assert_eq!(parse_numeric_str(".e10"), Err(()));
6202    }
6203
6204    #[test]
6205    fn test_parse_numeric_str_with_whitespace() {
6206        assert_eq!(parse_numeric_str("   123"), Ok((ValueType::Integer, "123")));
6207        assert_eq!(
6208            parse_numeric_str("  -456.78  "),
6209            Ok((ValueType::Float, "-456.78"))
6210        );
6211        assert_eq!(
6212            parse_numeric_str("  1.23e4  "),
6213            Ok((ValueType::Float, "1.23e4"))
6214        );
6215    }
6216
6217    #[test]
6218    fn test_parse_numeric_str_leading_zeros() {
6219        assert_eq!(
6220            parse_numeric_str("000123"),
6221            Ok((ValueType::Integer, "000123"))
6222        );
6223        assert_eq!(
6224            parse_numeric_str("000.456"),
6225            Ok((ValueType::Float, "000.456"))
6226        );
6227        assert_eq!(
6228            parse_numeric_str("0001e3"),
6229            Ok((ValueType::Float, "0001e3"))
6230        );
6231    }
6232
6233    #[test]
6234    fn test_parse_numeric_str_trailing_characters() {
6235        assert_eq!(parse_numeric_str("123abc"), Ok((ValueType::Integer, "123")));
6236        assert_eq!(
6237            parse_numeric_str("456.78xyz"),
6238            Ok((ValueType::Float, "456.78"))
6239        );
6240        assert_eq!(
6241            parse_numeric_str("1.23e4extra"),
6242            Ok((ValueType::Float, "1.23e4"))
6243        );
6244    }
6245
6246    #[test]
6247    fn test_sql_is_create_virtual_table() {
6248        assert!(sql_is_create_virtual_table(
6249            "CREATE VIRTUAL TABLE x USING y;"
6250        ));
6251        assert!(sql_is_create_virtual_table(
6252            "create virtual table x using y"
6253        ));
6254        assert!(sql_is_create_virtual_table(
6255            "  \n\tCREATE  VIRTUAL TABLE x USING y"
6256        ));
6257        assert!(sql_is_create_virtual_table(
6258            "-- comment\nCREATE /* c */ VIRTUAL TABLE x USING y"
6259        ));
6260        assert!(sql_is_create_virtual_table(
6261            "CREATE VIRTUAL TABLE IF NOT EXISTS x USING y(a, b)"
6262        ));
6263        // Quoted identifiers must not confuse the classifier either way.
6264        assert!(sql_is_create_virtual_table(
6265            "CREATE VIRTUAL TABLE \"create table\" USING y"
6266        ));
6267        // Regular table whose SQL merely contains the text must not match.
6268        assert!(!sql_is_create_virtual_table(
6269            "CREATE TABLE t(x TEXT DEFAULT 'create virtual')"
6270        ));
6271        assert!(!sql_is_create_virtual_table(
6272            "CREATE TABLE \"create virtual\"(x)"
6273        ));
6274        assert!(!sql_is_create_virtual_table("CREATE TABLE t(x)"));
6275        assert!(!sql_is_create_virtual_table("CREATE VIRTUALX TABLE t(x)"));
6276        assert!(!sql_is_create_virtual_table("CREATE VIRTUAL"));
6277        assert!(!sql_is_create_virtual_table(""));
6278    }
6279
6280    #[test]
6281    fn test_module_name_basic() {
6282        let sql = "CREATE VIRTUAL TABLE x USING y;";
6283        assert_eq!(module_name_from_sql(sql).unwrap(), "y");
6284    }
6285
6286    #[test]
6287    fn test_module_name_with_args() {
6288        let sql = "CREATE VIRTUAL TABLE x USING modname('a', 'b');";
6289        assert_eq!(module_name_from_sql(sql).unwrap(), "modname");
6290    }
6291
6292    #[test]
6293    fn test_module_name_missing_using() {
6294        let sql = "CREATE VIRTUAL TABLE x (a, b);";
6295        assert!(module_name_from_sql(sql).is_err());
6296    }
6297
6298    #[test]
6299    fn test_module_name_no_semicolon() {
6300        let sql = "CREATE VIRTUAL TABLE x USING limbo(a, b)";
6301        assert_eq!(module_name_from_sql(sql).unwrap(), "limbo");
6302    }
6303
6304    #[test]
6305    fn test_module_name_no_semicolon_or_args() {
6306        let sql = "CREATE VIRTUAL TABLE x USING limbo";
6307        assert_eq!(module_name_from_sql(sql).unwrap(), "limbo");
6308    }
6309
6310    #[test]
6311    fn test_module_args_none() {
6312        let sql = "CREATE VIRTUAL TABLE x USING modname;";
6313        let args = module_args_from_sql(sql).unwrap();
6314        assert_eq!(args.len(), 0);
6315    }
6316
6317    #[test]
6318    fn test_module_args_basic() {
6319        let sql = "CREATE VIRTUAL TABLE x USING modname('arg1', 'arg2');";
6320        let args = module_args_from_sql(sql).unwrap();
6321        assert_eq!(args.len(), 2);
6322        assert_eq!("arg1", args[0].to_text().unwrap());
6323        assert_eq!("arg2", args[1].to_text().unwrap());
6324        for arg in args {
6325            unsafe { arg.__free_internal_type() }
6326        }
6327    }
6328
6329    #[test]
6330    fn test_module_args_with_escaped_quote() {
6331        let sql = "CREATE VIRTUAL TABLE x USING modname('a''b', 'c');";
6332        let args = module_args_from_sql(sql).unwrap();
6333        assert_eq!(args.len(), 2);
6334        assert_eq!(args[0].to_text().unwrap(), "a'b");
6335        assert_eq!(args[1].to_text().unwrap(), "c");
6336        for arg in args {
6337            unsafe { arg.__free_internal_type() }
6338        }
6339    }
6340
6341    #[test]
6342    fn test_module_args_unterminated_string() {
6343        let sql = "CREATE VIRTUAL TABLE x USING modname('arg1, 'arg2');";
6344        assert!(module_args_from_sql(sql).is_err());
6345    }
6346
6347    #[test]
6348    fn test_module_args_extra_garbage_after_quote() {
6349        let sql = "CREATE VIRTUAL TABLE x USING modname('arg1'x);";
6350        assert!(module_args_from_sql(sql).is_err());
6351    }
6352
6353    #[test]
6354    fn test_module_args_trailing_comma() {
6355        let sql = "CREATE VIRTUAL TABLE x USING modname('arg1',);";
6356        let args = module_args_from_sql(sql).unwrap();
6357        assert_eq!(args.len(), 1);
6358        assert_eq!("arg1", args[0].to_text().unwrap());
6359        for arg in args {
6360            unsafe { arg.__free_internal_type() }
6361        }
6362    }
6363
6364    #[test]
6365    fn test_parse_numeric_literal_hex() {
6366        assert_eq!(
6367            parse_numeric_literal("0x1234").unwrap(),
6368            Value::from_i64(4660)
6369        );
6370        assert_eq!(
6371            parse_numeric_literal("0xFFFFFFFF").unwrap(),
6372            Value::from_i64(4294967295)
6373        );
6374        assert_eq!(
6375            parse_numeric_literal("0x7FFFFFFF").unwrap(),
6376            Value::from_i64(2147483647)
6377        );
6378        assert_eq!(
6379            parse_numeric_literal("0x7FFFFFFFFFFFFFFF").unwrap(),
6380            Value::from_i64(9223372036854775807)
6381        );
6382        assert_eq!(
6383            parse_numeric_literal("0xFFFFFFFFFFFFFFFF").unwrap(),
6384            Value::from_i64(-1)
6385        );
6386        assert_eq!(
6387            parse_numeric_literal("0x8000000000000000").unwrap(),
6388            Value::from_i64(-9223372036854775808)
6389        );
6390
6391        assert_eq!(
6392            parse_numeric_literal("-0x1234").unwrap(),
6393            Value::from_i64(-4660)
6394        );
6395        // too big hex
6396        assert!(parse_numeric_literal("-0x8000000000000000").is_err());
6397    }
6398
6399    #[test]
6400    fn test_parse_numeric_literal_integer() {
6401        assert_eq!(parse_numeric_literal("123").unwrap(), Value::from_i64(123));
6402        assert_eq!(
6403            parse_numeric_literal("9_223_372_036_854_775_807").unwrap(),
6404            Value::from_i64(9223372036854775807)
6405        );
6406    }
6407
6408    #[test]
6409    fn test_parse_numeric_literal_float() {
6410        assert_eq!(
6411            parse_numeric_literal("123.456").unwrap(),
6412            Value::from_f64(123.456)
6413        );
6414        assert_eq!(
6415            parse_numeric_literal(".123").unwrap(),
6416            Value::from_f64(0.123)
6417        );
6418        assert_eq!(
6419            parse_numeric_literal("1.23e10").unwrap(),
6420            Value::from_f64(1.23e10)
6421        );
6422        assert_eq!(
6423            parse_numeric_literal("1e-10").unwrap(),
6424            Value::from_f64(1e-10)
6425        );
6426        assert_eq!(
6427            parse_numeric_literal("1.23E+10").unwrap(),
6428            Value::from_f64(1.23e10)
6429        );
6430        assert_eq!(
6431            parse_numeric_literal("1.1_1").unwrap(),
6432            Value::from_f64(1.11)
6433        );
6434
6435        // > i64::MAX, convert to float
6436        assert_eq!(
6437            parse_numeric_literal("9223372036854775808").unwrap(),
6438            Value::from_f64(9.223_372_036_854_776e18)
6439        );
6440        // < i64::MIN, convert to float
6441        assert_eq!(
6442            parse_numeric_literal("-9223372036854775809").unwrap(),
6443            Value::from_f64(-9.223_372_036_854_776e18)
6444        );
6445    }
6446
6447    #[test]
6448    fn test_parse_pragma_bool() {
6449        assert!(parse_pragma_bool(&Expr::Literal(Literal::Numeric("1".into()))).unwrap(),);
6450        assert!(parse_pragma_bool(&Expr::Name(Name::exact("true".into()))).unwrap(),);
6451        assert!(parse_pragma_bool(&Expr::Name(Name::exact("on".into()))).unwrap(),);
6452        assert!(parse_pragma_bool(&Expr::Name(Name::exact("yes".into()))).unwrap(),);
6453
6454        assert!(!parse_pragma_bool(&Expr::Literal(Literal::Numeric("0".into()))).unwrap(),);
6455        assert!(!parse_pragma_bool(&Expr::Name(Name::exact("false".into()))).unwrap(),);
6456        assert!(!parse_pragma_bool(&Expr::Name(Name::exact("off".into()))).unwrap(),);
6457        assert!(!parse_pragma_bool(&Expr::Name(Name::exact("no".into()))).unwrap(),);
6458
6459        assert!(parse_pragma_bool(&Expr::Name(Name::exact("nono".into()))).is_err());
6460        assert!(parse_pragma_bool(&Expr::Name(Name::exact("10".into()))).is_err());
6461        assert!(parse_pragma_bool(&Expr::Name(Name::exact("-1".into()))).is_err());
6462    }
6463
6464    #[test]
6465    fn test_type_from_name() {
6466        let tc = vec![
6467            ("", (SchemaValueType::Blob, false)),
6468            ("INTEGER", (SchemaValueType::Integer, true)),
6469            ("INT", (SchemaValueType::Integer, false)),
6470            ("CHAR", (SchemaValueType::Text, false)),
6471            ("CLOB", (SchemaValueType::Text, false)),
6472            ("TEXT", (SchemaValueType::Text, false)),
6473            ("BLOB", (SchemaValueType::Blob, false)),
6474            ("REAL", (SchemaValueType::Real, false)),
6475            ("FLOAT", (SchemaValueType::Real, false)),
6476            ("DOUBLE", (SchemaValueType::Real, false)),
6477            ("U128", (SchemaValueType::Numeric, false)),
6478        ];
6479
6480        for (input, expected) in tc {
6481            let result = type_from_name(input);
6482            assert_eq!(result, expected, "Failed for input: {input}");
6483        }
6484    }
6485
6486    #[test]
6487    fn test_checked_cast_text_to_numeric_lossless_property() {
6488        assert_eq!(checked_cast_text_to_numeric("1.xx", true), Err(()));
6489        assert_eq!(checked_cast_text_to_numeric("abc", true), Err(()));
6490        assert_eq!(checked_cast_text_to_numeric("--5", true), Err(()));
6491        assert_eq!(checked_cast_text_to_numeric("12.34.56", true), Err(()));
6492        assert_eq!(checked_cast_text_to_numeric("", true), Err(()));
6493        assert_eq!(checked_cast_text_to_numeric(" ", true), Err(()));
6494        assert_eq!(
6495            checked_cast_text_to_numeric("0", true),
6496            Ok(Value::from_i64(0))
6497        );
6498        assert_eq!(
6499            checked_cast_text_to_numeric("42", true),
6500            Ok(Value::from_i64(42))
6501        );
6502        assert_eq!(
6503            checked_cast_text_to_numeric("-42", true),
6504            Ok(Value::from_i64(-42))
6505        );
6506        assert_eq!(
6507            checked_cast_text_to_numeric("999999999999", true),
6508            Ok(Value::from_i64(999_999_999_999))
6509        );
6510        assert_eq!(
6511            checked_cast_text_to_numeric("1.0", true),
6512            Ok(Value::from_i64(1))
6513        );
6514        assert_eq!(
6515            checked_cast_text_to_numeric("-3.22", true),
6516            Ok(Value::from_f64(-3.22))
6517        );
6518        assert_eq!(
6519            checked_cast_text_to_numeric("0.001", true),
6520            Ok(Value::from_f64(0.001))
6521        );
6522        assert_eq!(
6523            checked_cast_text_to_numeric("2e3", true),
6524            Ok(Value::from_i64(2000))
6525        );
6526        assert_eq!(
6527            checked_cast_text_to_numeric("-5.5e-2", true),
6528            Ok(Value::from_f64(-0.055))
6529        );
6530        assert_eq!(
6531            checked_cast_text_to_numeric(" 123 ", true),
6532            Ok(Value::from_i64(123))
6533        );
6534        assert_eq!(
6535            checked_cast_text_to_numeric("\t-3.22\n", true),
6536            Ok(Value::from_f64(-3.22))
6537        );
6538    }
6539
6540    #[test]
6541    fn test_trim_ascii_whitespace_helper() {
6542        assert_eq!(trim_ascii_whitespace("  hello  "), "hello");
6543        assert_eq!(trim_ascii_whitespace("\t\nhello\r\n"), "hello");
6544        assert_eq!(trim_ascii_whitespace("hello"), "hello");
6545        assert_eq!(trim_ascii_whitespace("   "), "");
6546        assert_eq!(trim_ascii_whitespace(""), "");
6547
6548        // non-breaking space should NOT be trimmed
6549        assert_eq!(
6550            trim_ascii_whitespace("\u{00A0}hello\u{00A0}"),
6551            "\u{00A0}hello\u{00A0}"
6552        );
6553        assert_eq!(
6554            trim_ascii_whitespace("  \u{00A0}hello\u{00A0}  "),
6555            "\u{00A0}hello\u{00A0}"
6556        );
6557    }
6558
6559    #[test]
6560    fn test_cast_real_to_integer_limits() {
6561        // Values that are exactly representable in f64 and strictly within i64 range
6562        let max_exact = ((1i64 << 51) - 1) as f64;
6563        assert_eq!(cast_real_to_integer(max_exact), Ok((1i64 << 51) - 1));
6564        assert_eq!(cast_real_to_integer(-max_exact), Ok(-((1i64 << 51) - 1)));
6565
6566        // Values beyond 2^51 are valid if they round-trip correctly and are strictly within bounds
6567        assert_eq!(cast_real_to_integer((1i64 << 51) as f64), Ok(1i64 << 51));
6568        assert_eq!(cast_real_to_integer((1i64 << 52) as f64), Ok(1i64 << 52));
6569
6570        // 2^62 round-trips correctly and is strictly between i64::MIN and i64::MAX
6571        assert_eq!(cast_real_to_integer((1i64 << 62) as f64), Ok(1i64 << 62));
6572
6573        // The original bug's value: 426601719749026560 should work
6574        assert_eq!(
6575            cast_real_to_integer(426601719749026560.0),
6576            Ok(426601719749026560)
6577        );
6578
6579        // SQLite rejects boundary values: i64::MIN and i64::MAX exactly
6580        // (ix > SMALLEST_INT64 && ix < LARGEST_INT64 requires STRICT inequality)
6581        assert_eq!(cast_real_to_integer(i64::MIN as f64), Err(()));
6582        assert_eq!(cast_real_to_integer(i64::MAX as f64), Err(()));
6583
6584        // Values at or beyond i64::MAX + 1 (2^63) should fail
6585        assert_eq!(cast_real_to_integer(9223372036854775808.0), Err(()));
6586
6587        // Values below i64::MIN should fail
6588        assert_eq!(cast_real_to_integer(-9223372036854777856.0), Err(()));
6589
6590        // Non-whole numbers should fail
6591        assert_eq!(cast_real_to_integer(1.5), Err(()));
6592        assert_eq!(cast_real_to_integer(-1.5), Err(()));
6593
6594        // Non-finite values should fail
6595        assert_eq!(cast_real_to_integer(f64::INFINITY), Err(()));
6596        assert_eq!(cast_real_to_integer(f64::NEG_INFINITY), Err(()));
6597        assert_eq!(cast_real_to_integer(f64::NAN), Err(()));
6598    }
6599}