Skip to main content

nu_command/database/values/
sqlite.rs

1use super::definitions::{
2    db_column::DbColumn, db_constraint::DbConstraint, db_foreignkey::DbForeignKey,
3    db_index::DbIndex, db_table::DbTable,
4};
5use nu_protocol::{
6    CustomValue, IntoPipelineData, PipelineData, Record, ShellError, Signals, Span, Spanned, Value,
7    ast, casing::Casing, engine::EngineState, shell_error::generic::GenericError,
8    shell_error::io::IoError,
9};
10use rusqlite::{
11    Connection, Error as SqliteError, OpenFlags, Row, Statement, ToSql, types::ValueRef,
12};
13use serde::{Deserialize, Serialize};
14use std::{
15    collections::BTreeMap,
16    fmt::Write,
17    fs::File,
18    io::Read,
19    ops::{Deref, DerefMut},
20    path::{Path, PathBuf},
21    sync::{Mutex, MutexGuard, OnceLock},
22};
23
24const SQLITE_MAGIC_BYTES: &[u8] = "SQLite format 3\0".as_bytes();
25pub const MEMORY_DB: &str = "file:memdb1?mode=memory&cache=shared";
26const DATABASE_NAME: &str = "main";
27
28// A single mutex-guarded connection to the shared in-memory SQLite database.
29//
30// Every `stor` command, `query db`, `schema`, cell-path access, etc. must go
31// through this connection (via `open_sqlite_db` / `OpenedConnection`). Opening
32// additional connections to the same shared-cache URI under concurrency
33// (`par-each`, `job spawn`) produces SQLITE_BUSY and can drop statements.
34// The static connection also acts as the process lifetime anchor for
35// `mode=memory&cache=shared` (the DB lives as long as at least one connection
36// remains open).
37static SHARED_MEM_CONN: OnceLock<Mutex<Connection>> = OnceLock::new();
38
39/// True when `path` is the process-wide in-memory shared-cache database URI.
40pub fn is_memory_db(path: &Path) -> bool {
41    path.to_string_lossy() == MEMORY_DB
42}
43
44fn map_lock_error(err: impl std::fmt::Display) -> ShellError {
45    ShellError::Generic(GenericError::new_internal(
46        "Failed to acquire shared memory DB lock",
47        err.to_string(),
48    ))
49}
50
51/// Returns the process-wide mutex around the shared in-memory connection,
52/// initializing it on first use.
53fn shared_mem_mutex() -> Result<&'static Mutex<Connection>, ShellError> {
54    if let Some(mutex) = SHARED_MEM_CONN.get() {
55        return Ok(mutex);
56    }
57
58    // First open (or race: losers drop their connection; one Mutex remains).
59    let conn = open_connection_in_memory_custom()?;
60    let _ = SHARED_MEM_CONN.set(Mutex::new(conn));
61    SHARED_MEM_CONN.get().ok_or_else(|| {
62        ShellError::Generic(GenericError::new_internal(
63            "Failed to initialize shared memory DB connection",
64            "shared memory connection was not set",
65        ))
66    })
67}
68
69/// Ensures the shared in-memory connection exists without holding the mutex.
70///
71/// Call this early in process startup so the static connection is the lifetime
72/// anchor for the shared-cache memory DB.
73pub fn init_shared_memory_db() -> Result<(), ShellError> {
74    shared_mem_mutex().map(|_| ())
75}
76
77/// Returns a lock guard for the single shared in-memory SQLite connection.
78///
79/// Prefer [`open_sqlite_db`] for path-based access so file and memory DBs share
80/// one call site. Available crate-wide for `stor` commands that always target memdb.
81pub(crate) fn get_shared_mem_conn() -> Result<MutexGuard<'static, Connection>, ShellError> {
82    shared_mem_mutex()?.lock().map_err(map_lock_error)
83}
84
85/// Either the process-wide shared in-memory connection (mutex held for the
86/// lifetime of this value), or an owned file connection.
87///
88/// All SQLite access should go through this type so the memory DB cannot be
89/// opened a second time outside the process-wide mutex.
90pub enum OpenedConnection {
91    Shared(MutexGuard<'static, Connection>),
92    Owned(Connection),
93}
94
95impl Deref for OpenedConnection {
96    type Target = Connection;
97
98    fn deref(&self) -> &Self::Target {
99        match self {
100            Self::Shared(guard) => guard,
101            Self::Owned(conn) => conn,
102        }
103    }
104}
105
106impl DerefMut for OpenedConnection {
107    fn deref_mut(&mut self) -> &mut Self::Target {
108        match self {
109            Self::Shared(guard) => guard,
110            Self::Owned(conn) => conn,
111        }
112    }
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct SQLiteDatabase {
117    // I considered storing a SQLite connection here, but decided against it because
118    // 1) YAGNI, 2) it's not obvious how cloning a connection could work, 3) state
119    // management gets tricky quick. Revisit this approach if we find a compelling use case.
120    pub path: PathBuf,
121    #[serde(skip, default = "Signals::empty")]
122    // this understandably can't be serialized. think that's OK, I'm not aware of a
123    // reason why a CustomValue would be serialized outside of a plugin
124    signals: Signals,
125}
126
127impl SQLiteDatabase {
128    pub fn new(path: &Path, signals: Signals) -> Self {
129        Self {
130            path: PathBuf::from(path),
131            signals,
132        }
133    }
134
135    pub fn try_from_path(path: &Path, span: Span, signals: Signals) -> Result<Self, ShellError> {
136        let mut file = File::open(path).map_err(|e| IoError::new(e, span, PathBuf::from(path)))?;
137
138        let mut buf: [u8; 16] = [0; 16];
139        file.read_exact(&mut buf)
140            .map_err(|e| ShellError::Io(IoError::new(e, span, PathBuf::from(path))))
141            .and_then(|_| {
142                if buf == SQLITE_MAGIC_BYTES {
143                    Ok(SQLiteDatabase::new(path, signals))
144                } else {
145                    Err(ShellError::Generic(GenericError::new(
146                        "Not a SQLite file",
147                        format!("Could not read '{}' as SQLite file", path.display()),
148                        span,
149                    )))
150                }
151            })
152    }
153
154    pub fn try_from_value(value: Value) -> Result<Self, ShellError> {
155        let span = value.span();
156        match value {
157            Value::Custom { val, .. } => match val.as_any().downcast_ref::<Self>() {
158                Some(db) => Ok(Self {
159                    path: db.path.clone(),
160                    signals: db.signals.clone(),
161                }),
162                None => Err(ShellError::CantConvert {
163                    to_type: "database".into(),
164                    from_type: "non-database".into(),
165                    span,
166                    help: None,
167                }),
168            },
169            x => Err(ShellError::CantConvert {
170                to_type: "database".into(),
171                from_type: x.get_type().to_string(),
172                span: x.span(),
173                help: None,
174            }),
175        }
176    }
177
178    pub fn try_from_pipeline(input: PipelineData, span: Span) -> Result<Self, ShellError> {
179        let value = input.into_value(span)?;
180        Self::try_from_value(value)
181    }
182
183    pub fn into_value(self, span: Span) -> Value {
184        let db = Box::new(self);
185        Value::custom(db, span)
186    }
187
188    pub fn query(
189        &self,
190        sql: &Spanned<String>,
191        params: NuSqlParams,
192        call_span: Span,
193    ) -> Result<Value, ShellError> {
194        let conn = open_sqlite_db(&self.path, call_span)?;
195        let stream = run_sql_query(&conn, sql, params, &self.signals, None)
196            .map_err(|e| e.into_shell_error(sql.span, "Failed to query SQLite database"))?;
197        Ok(stream)
198    }
199
200    /// Opens this database for use. Memory DB access holds the process-wide mutex.
201    pub fn open_connection(&self, call_span: Span) -> Result<OpenedConnection, ShellError> {
202        open_sqlite_db(&self.path, call_span)
203    }
204
205    fn sleeper(attempts: i32) -> bool {
206        log::warn!("SQLITE_BUSY, retrying after 250ms (attempt {attempts})");
207        std::thread::sleep(std::time::Duration::from_millis(250));
208        true
209    }
210
211    pub fn get_tables(&self, conn: &Connection) -> Result<Vec<DbTable>, SqliteError> {
212        let mut table_names =
213            conn.prepare("SELECT name FROM sqlite_master WHERE type = 'table'")?;
214        let rows = table_names.query_map([], |row| row.get(0))?;
215        let mut tables = Vec::new();
216
217        for row in rows {
218            let table_name: String = row?;
219            tables.push(DbTable {
220                name: table_name,
221                create_time: None,
222                update_time: None,
223                engine: None,
224                schema: None,
225            })
226        }
227
228        Ok(tables.into_iter().collect())
229    }
230
231    pub fn drop_all_tables(&self, conn: &Connection) -> Result<(), SqliteError> {
232        let tables = self.get_tables(conn)?;
233
234        for table in tables {
235            conn.execute(&format!("DROP TABLE {}", table.name), [])?;
236        }
237
238        Ok(())
239    }
240
241    pub fn export_in_memory_database_to_file(
242        &self,
243        conn: &Connection,
244        filename: String,
245    ) -> Result<(), SqliteError> {
246        //vacuum main into 'c:\\temp\\foo.db'
247        conn.execute(&format!("vacuum main into '{filename}'"), [])?;
248
249        Ok(())
250    }
251
252    pub fn backup_database_to_file(
253        &self,
254        conn: &Connection,
255        filename: String,
256    ) -> Result<(), SqliteError> {
257        conn.backup(DATABASE_NAME, Path::new(&filename), None)?;
258        Ok(())
259    }
260
261    pub fn restore_database_from_file(
262        &self,
263        conn: &mut Connection,
264        filename: String,
265    ) -> Result<(), SqliteError> {
266        conn.restore(
267            DATABASE_NAME,
268            Path::new(&filename),
269            Some(|p: rusqlite::backup::Progress| {
270                let percent = if p.pagecount == 0 {
271                    100
272                } else {
273                    (p.pagecount - p.remaining) * 100 / p.pagecount
274                };
275                if percent % 10 == 0 {
276                    log::trace!("Restoring: {percent} %");
277                }
278            }),
279        )?;
280        Ok(())
281    }
282
283    fn get_column_info(&self, row: &Row) -> Result<DbColumn, SqliteError> {
284        let dbc = DbColumn {
285            cid: row.get("cid")?,
286            name: row.get("name")?,
287            r#type: row.get("type")?,
288            notnull: row.get("notnull")?,
289            default: row.get("dflt_value")?,
290            pk: row.get("pk")?,
291        };
292        Ok(dbc)
293    }
294
295    pub fn get_columns(
296        &self,
297        conn: &Connection,
298        table: &DbTable,
299    ) -> Result<Vec<DbColumn>, SqliteError> {
300        let mut column_names = conn.prepare(&format!(
301            "SELECT * FROM pragma_table_info('{}');",
302            table.name
303        ))?;
304
305        let mut columns: Vec<DbColumn> = Vec::new();
306        let rows = column_names.query_and_then([], |row| self.get_column_info(row))?;
307
308        for row in rows {
309            columns.push(row?);
310        }
311
312        Ok(columns)
313    }
314
315    fn get_constraint_info(&self, row: &Row) -> Result<DbConstraint, SqliteError> {
316        let dbc = DbConstraint {
317            name: row.get("index_name")?,
318            column_name: row.get("column_name")?,
319            origin: row.get("origin")?,
320        };
321        Ok(dbc)
322    }
323
324    pub fn get_constraints(
325        &self,
326        conn: &Connection,
327        table: &DbTable,
328    ) -> Result<Vec<DbConstraint>, SqliteError> {
329        let mut column_names = conn.prepare(&format!(
330            "
331            SELECT
332                p.origin,
333                s.name AS index_name,
334                i.name AS column_name
335            FROM
336                sqlite_master s
337                JOIN pragma_index_list(s.tbl_name) p ON s.name = p.name,
338                pragma_index_info(s.name) i
339            WHERE
340                s.type = 'index'
341                AND tbl_name = '{}'
342                AND NOT p.origin = 'c'
343            ",
344            table.name
345        ))?;
346
347        let mut constraints: Vec<DbConstraint> = Vec::new();
348        let rows = column_names.query_and_then([], |row| self.get_constraint_info(row))?;
349
350        for row in rows {
351            constraints.push(row?);
352        }
353
354        Ok(constraints)
355    }
356
357    fn get_foreign_keys_info(&self, row: &Row) -> Result<DbForeignKey, SqliteError> {
358        let dbc = DbForeignKey {
359            column_name: row.get("from")?,
360            ref_table: row.get("table")?,
361            ref_column: row.get("to")?,
362        };
363        Ok(dbc)
364    }
365
366    pub fn get_foreign_keys(
367        &self,
368        conn: &Connection,
369        table: &DbTable,
370    ) -> Result<Vec<DbForeignKey>, SqliteError> {
371        let mut column_names = conn.prepare(&format!(
372            "SELECT p.`from`, p.`to`, p.`table` FROM pragma_foreign_key_list('{}') p",
373            table.name
374        ))?;
375
376        let mut foreign_keys: Vec<DbForeignKey> = Vec::new();
377        let rows = column_names.query_and_then([], |row| self.get_foreign_keys_info(row))?;
378
379        for row in rows {
380            foreign_keys.push(row?);
381        }
382
383        Ok(foreign_keys)
384    }
385
386    fn get_index_info(&self, row: &Row) -> Result<DbIndex, SqliteError> {
387        let dbc = DbIndex {
388            name: row.get("index_name")?,
389            column_name: row.get("name")?,
390            seqno: row.get("seqno")?,
391        };
392        Ok(dbc)
393    }
394
395    pub fn get_indexes(
396        &self,
397        conn: &Connection,
398        table: &DbTable,
399    ) -> Result<Vec<DbIndex>, SqliteError> {
400        let mut column_names = conn.prepare(&format!(
401            "
402            SELECT
403                m.name AS index_name,
404                p.*
405            FROM
406                sqlite_master m,
407                pragma_index_info(m.name) p
408            WHERE
409                m.type = 'index'
410                AND m.tbl_name = '{}'
411            ",
412            table.name,
413        ))?;
414
415        let mut indexes: Vec<DbIndex> = Vec::new();
416        let rows = column_names.query_and_then([], |row| self.get_index_info(row))?;
417
418        for row in rows {
419            indexes.push(row?);
420        }
421
422        Ok(indexes)
423    }
424}
425
426impl CustomValue for SQLiteDatabase {
427    fn clone_value(&self, span: Span) -> Value {
428        Value::custom(Box::new(self.clone()), span)
429    }
430
431    fn type_name(&self) -> String {
432        self.typetag_name().to_string()
433    }
434
435    fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
436        let db = open_sqlite_db(&self.path, span)?;
437        read_entire_sqlite_db(&db, span, &self.signals)
438            .map_err(|e| e.into_shell_error(span, "Failed to read from SQLite database."))
439    }
440
441    fn as_any(&self) -> &dyn std::any::Any {
442        self
443    }
444
445    fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
446        self
447    }
448
449    fn follow_path_int(
450        &self,
451        _self_span: Span,
452        _index: usize,
453        path_span: Span,
454        _optional: bool,
455    ) -> Result<Value, ShellError> {
456        // In theory we could support this, but tables don't have an especially well-defined order
457        Err(ShellError::IncompatiblePathAccess { type_name: "SQLite databases do not support integer-indexed access. Try specifying a table name instead".into(), span: path_span })
458    }
459
460    fn follow_path_string(
461        &self,
462        _self_span: Span,
463        column_name: String,
464        path_span: Span,
465        _optional: bool,
466        _casing: Casing,
467    ) -> Result<Value, ShellError> {
468        // Return a lazy SQLiteQueryBuilder instead of executing the query immediately
469        let table = SQLiteQueryBuilder::new(self.path.clone(), column_name, self.signals.clone());
470        Ok(Value::custom(Box::new(table), path_span))
471    }
472
473    fn typetag_name(&self) -> &'static str {
474        "SQLiteDatabase"
475    }
476
477    fn typetag_deserialize(&self) {
478        unimplemented!("typetag_deserialize")
479    }
480}
481
482/// Opens a SQLite database for the given path.
483///
484/// - [`MEMORY_DB`]: returns the process-wide shared connection under a mutex.
485/// - File paths: opens an owned connection with a busy handler.
486pub fn open_sqlite_db(path: &Path, call_span: Span) -> Result<OpenedConnection, ShellError> {
487    if is_memory_db(path) {
488        return Ok(OpenedConnection::Shared(get_shared_mem_conn()?));
489    }
490
491    let path = path.to_string_lossy().to_string();
492    let conn = Connection::open(path).map_err(|err| {
493        ShellError::Generic(GenericError::new(
494            "Failed to open SQLite database",
495            err.to_string(),
496            call_span,
497        ))
498    })?;
499    conn.busy_handler(Some(SQLiteDatabase::sleeper))
500        .map_err(|err| {
501            ShellError::Generic(GenericError::new(
502                "Failed to set busy handler for SQLite database",
503                err.to_string(),
504                call_span,
505            ))
506        })?;
507    Ok(OpenedConnection::Owned(conn))
508}
509
510fn run_sql_query(
511    conn: &Connection,
512    sql: &Spanned<String>,
513    params: NuSqlParams,
514    signals: &Signals,
515    column_adapters: Option<&BTreeMap<String, SQLiteColumnAdapter>>,
516) -> Result<Value, SqliteOrShellError> {
517    let stmt = conn.prepare(&sql.item)?;
518    prepared_statement_to_nu_list(stmt, params, sql.span, signals, column_adapters)
519}
520
521// This is taken from to text local_into_string but tweaks it a bit so that certain formatting does not happen
522pub fn value_to_sql(
523    engine_state: &EngineState,
524    value: Value,
525    call_span: Span,
526) -> Result<Box<dyn rusqlite::ToSql>, ShellError> {
527    match value {
528        Value::Bool { val, .. } => Ok(Box::new(val)),
529        Value::Int { val, .. } => Ok(Box::new(val)),
530        Value::Float { val, .. } => Ok(Box::new(val)),
531        Value::Filesize { val, .. } => Ok(Box::new(val.get())),
532        Value::Duration { val, .. } => Ok(Box::new(val)),
533        Value::Date { val, .. } => Ok(Box::new(val)),
534        Value::String { val, .. } => Ok(Box::new(val)),
535        Value::Binary { val, .. } => Ok(Box::new(val.into_owned())),
536        Value::Nothing { .. } => Ok(Box::new(rusqlite::types::Null)),
537        val => {
538            let span = val.span();
539            let ty = val.get_type();
540            let json_value = crate::value_to_json_value(engine_state, val, call_span, false)?;
541            match nu_json::to_string_raw(&json_value) {
542                Ok(s) => Ok(Box::new(s)),
543                Err(err) => Err(ShellError::CantConvert {
544                    to_type: "JSON".into(),
545                    from_type: ty.to_string(),
546                    span,
547                    help: Some(err.to_string()),
548                }),
549            }
550        }
551    }
552}
553
554pub fn values_to_sql(
555    engine_state: &EngineState,
556    values: impl IntoIterator<Item = Value>,
557    call_span: Span,
558) -> Result<Vec<Box<dyn rusqlite::ToSql>>, ShellError> {
559    values
560        .into_iter()
561        .map(|v| value_to_sql(engine_state, v, call_span))
562        .collect::<Result<Vec<_>, _>>()
563}
564
565pub enum NuSqlParams {
566    List(Vec<Box<dyn ToSql>>),
567    Named(Vec<(String, Box<dyn ToSql>)>),
568}
569
570impl Default for NuSqlParams {
571    fn default() -> Self {
572        NuSqlParams::List(Vec::new())
573    }
574}
575
576pub fn nu_value_to_params(
577    engine_state: &EngineState,
578    value: Value,
579    call_span: Span,
580) -> Result<NuSqlParams, ShellError> {
581    match value {
582        Value::Record { val, .. } => {
583            let mut params = Vec::with_capacity(val.len());
584
585            for (mut column, value) in val.into_owned().into_iter() {
586                let sql_type_erased = value_to_sql(engine_state, value, call_span)?;
587
588                if !column.starts_with([':', '@', '$']) {
589                    column.insert(0, ':');
590                }
591
592                params.push((column, sql_type_erased));
593            }
594
595            Ok(NuSqlParams::Named(params))
596        }
597        Value::List { vals, .. } => {
598            let mut params = Vec::with_capacity(vals.len());
599
600            for value in vals.into_iter() {
601                let sql_type_erased = value_to_sql(engine_state, value, call_span)?;
602
603                params.push(sql_type_erased);
604            }
605
606            Ok(NuSqlParams::List(params))
607        }
608
609        // We accept no parameters
610        Value::Nothing { .. } => Ok(NuSqlParams::default()),
611
612        _ => Err(ShellError::TypeMismatch {
613            err_message: "Invalid parameters value: expected record or list".to_string(),
614            span: value.span(),
615        }),
616    }
617}
618
619#[derive(Debug)]
620enum SqliteOrShellError {
621    SqliteError(SqliteError),
622    ShellError(ShellError),
623}
624
625impl From<SqliteError> for SqliteOrShellError {
626    fn from(error: SqliteError) -> Self {
627        Self::SqliteError(error)
628    }
629}
630
631impl From<ShellError> for SqliteOrShellError {
632    fn from(error: ShellError) -> Self {
633        Self::ShellError(error)
634    }
635}
636
637impl SqliteOrShellError {
638    fn into_shell_error(self, span: Span, msg: &str) -> ShellError {
639        match self {
640            Self::SqliteError(err) => {
641                ShellError::Generic(GenericError::new(msg.to_string(), err.to_string(), span))
642            }
643            Self::ShellError(err) => err,
644        }
645    }
646}
647
648/// The SQLite type behind a query column returned as some raw type (e.g. 'text')
649#[derive(Clone, Copy)]
650pub enum DeclType {
651    Json,
652    Jsonb,
653}
654
655impl DeclType {
656    pub fn from_str(s: &str) -> Option<Self> {
657        match s.to_uppercase().as_str() {
658            "JSON" => Some(DeclType::Json),
659            "JSONB" => Some(DeclType::Jsonb),
660            _ => None, // We are only special-casing JSON(B) columns for now
661        }
662    }
663}
664
665/// A column out of an SQLite query, together with its type
666pub struct TypedColumn {
667    pub name: String,
668    pub decl_type: Option<DeclType>,
669}
670
671impl TypedColumn {
672    pub fn from_rusqlite_column(c: &rusqlite::Column) -> Self {
673        Self {
674            name: c.name().to_owned(),
675            decl_type: c.decl_type().and_then(DeclType::from_str),
676        }
677    }
678}
679
680fn prepared_statement_to_nu_list(
681    mut stmt: Statement,
682    params: NuSqlParams,
683    call_span: Span,
684    signals: &Signals,
685    column_adapters: Option<&BTreeMap<String, SQLiteColumnAdapter>>,
686) -> Result<Value, SqliteOrShellError> {
687    let columns: Vec<TypedColumn> = stmt
688        .columns()
689        .iter()
690        .map(TypedColumn::from_rusqlite_column)
691        .collect();
692
693    fn collect_row_values(
694        row_results: impl IntoIterator<Item = Result<Value, SqliteError>>,
695        signals: &Signals,
696        call_span: Span,
697    ) -> Result<Vec<Value>, SqliteOrShellError> {
698        let mut row_values = vec![];
699
700        for row_result in row_results {
701            signals.check(&call_span)?;
702            if let Ok(row_value) = row_result {
703                row_values.push(row_value);
704            }
705        }
706
707        Ok(row_values)
708    }
709
710    // Both parameter styles need separate query_map calls because rusqlite uses
711    // different parameter reference types for positional and named bindings.
712    // Keep the row processing shared through `collect_row_values`.
713    let row_values = match params {
714        NuSqlParams::List(params) => {
715            let refs: Vec<&dyn ToSql> = params.iter().map(|value| &**value).collect();
716
717            let row_results = stmt.query_map(refs.as_slice(), |row| {
718                Ok(convert_sqlite_row_to_nu_value(
719                    row,
720                    call_span,
721                    &columns,
722                    column_adapters,
723                ))
724            })?;
725
726            collect_row_values(row_results, signals, call_span)?
727        }
728        NuSqlParams::Named(pairs) => {
729            let refs: Vec<_> = pairs
730                .iter()
731                .map(|(column, value)| (column.as_str(), &**value))
732                .collect();
733
734            let row_results = stmt.query_map(refs.as_slice(), |row| {
735                Ok(convert_sqlite_row_to_nu_value(
736                    row,
737                    call_span,
738                    &columns,
739                    column_adapters,
740                ))
741            })?;
742
743            collect_row_values(row_results, signals, call_span)?
744        }
745    };
746
747    Ok(Value::list(row_values, call_span))
748}
749
750fn read_entire_sqlite_db(
751    conn: &Connection,
752    call_span: Span,
753    signals: &Signals,
754) -> Result<Value, SqliteOrShellError> {
755    let mut tables = Record::new();
756
757    let mut get_table_names =
758        conn.prepare("SELECT name FROM sqlite_master WHERE type = 'table'")?;
759    let rows = get_table_names.query_map([], |row| row.get(0))?;
760
761    for row in rows {
762        let table_name: String = row?;
763        // TODO: Should use params here?
764        let table_stmt = conn.prepare(&format!("select * from [{table_name}]"))?;
765        let rows = prepared_statement_to_nu_list(
766            table_stmt,
767            NuSqlParams::default(),
768            call_span,
769            signals,
770            None,
771        )?;
772        tables.push(table_name, rows);
773    }
774
775    Ok(Value::record(tables, call_span))
776}
777
778pub fn convert_sqlite_row_to_nu_value(
779    row: &Row,
780    span: Span,
781    columns: &[TypedColumn],
782    column_adapters: Option<&BTreeMap<String, SQLiteColumnAdapter>>,
783) -> Value {
784    let record = columns
785        .iter()
786        .enumerate()
787        .map(|(i, col)| {
788            let adapter = column_adapters
789                .and_then(|adapters| adapters.get(&col.name))
790                .copied();
791            (
792                col.name.clone(),
793                convert_sqlite_value_to_nu_value_with_adapter(
794                    row.get_ref_unwrap(i),
795                    col.decl_type,
796                    adapter,
797                    span,
798                ),
799            )
800        })
801        .collect();
802
803    Value::record(record, span)
804}
805
806#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
807pub enum SQLiteColumnAdapter {
808    /// Convert integer values interpreted as Unix epoch milliseconds into Nu datetimes.
809    UnixMillisToDate,
810    /// Convert integer values interpreted as milliseconds into Nu durations.
811    MillisToDuration,
812}
813
814fn convert_sqlite_value_to_nu_value_with_adapter(
815    value: ValueRef,
816    decl_type: Option<DeclType>,
817    adapter: Option<SQLiteColumnAdapter>,
818    span: Span,
819) -> Value {
820    match adapter {
821        Some(SQLiteColumnAdapter::UnixMillisToDate) => match value {
822            ValueRef::Integer(i) => chrono::DateTime::from_timestamp_millis(i)
823                .map(|datetime| Value::date(datetime.into(), span))
824                .unwrap_or_else(|| Value::int(i, span)),
825            _ => convert_sqlite_value_to_nu_value(value, decl_type, span),
826        },
827        Some(SQLiteColumnAdapter::MillisToDuration) => match value {
828            ValueRef::Integer(i) => i
829                .checked_mul(1_000_000)
830                .map(|nanos| Value::duration(nanos, span))
831                .unwrap_or_else(|| Value::int(i, span)),
832            _ => convert_sqlite_value_to_nu_value(value, decl_type, span),
833        },
834        None => convert_sqlite_value_to_nu_value(value, decl_type, span),
835    }
836}
837
838pub fn convert_sqlite_value_to_nu_value(
839    value: ValueRef,
840    decl_type: Option<DeclType>,
841    span: Span,
842) -> Value {
843    match value {
844        ValueRef::Null => Value::nothing(span),
845        ValueRef::Integer(i) => Value::int(i, span),
846        ValueRef::Real(f) => Value::float(f, span),
847        ValueRef::Text(buf) => match (std::str::from_utf8(buf), decl_type) {
848            (Ok(txt), Some(DeclType::Json | DeclType::Jsonb)) => {
849                match crate::try_json_str_to_value(txt, span, false, &Signals::empty()) {
850                    Ok(val) => val,
851                    Err(err) => Value::error(err, span),
852                }
853            }
854            (Ok(txt), _) => Value::string(txt.to_string(), span),
855            (Err(_), _) => Value::error(ShellError::NonUtf8 { span }, span),
856        },
857        ValueRef::Blob(u) => Value::binary(u.to_vec(), span),
858    }
859}
860
861pub fn open_connection_in_memory_custom() -> Result<Connection, ShellError> {
862    let flags = OpenFlags::default();
863    let conn = Connection::open_with_flags(MEMORY_DB, flags).map_err(|e| {
864        ShellError::Generic(GenericError::new(
865            "Failed to open SQLite custom connection in memory",
866            e.to_string(),
867            Span::test_data(),
868        ))
869    })?;
870    conn.busy_handler(Some(SQLiteDatabase::sleeper))
871        .map_err(|e| {
872            ShellError::Generic(GenericError::new(
873                "Failed to set busy handler for SQLite custom connection in memory",
874                e.to_string(),
875                Span::test_data(),
876            ))
877        })?;
878    Ok(conn)
879}
880
881pub fn open_connection_in_memory() -> Result<Connection, ShellError> {
882    Connection::open_in_memory().map_err(|e| {
883        ShellError::Generic(GenericError::new(
884            "Failed to open SQLite standard connection in memory",
885            e.to_string(),
886            Span::test_data(),
887        ))
888    })
889}
890
891/// A lazy query builder for SQLite tables, allowing SQL pushdown optimizations
892/// for commands like `length`, `select`, `first`, `last`, `skip`, and `uniq`.
893#[derive(Debug, Clone, Serialize, Deserialize)]
894pub struct SQLiteQueryBuilder {
895    pub db_path: PathBuf,
896    pub table_name: String,
897    pub sql_select: Option<String>, // e.g., "column1, column2" or "*" for all
898    pub sql_where: Option<String>,  // e.g., "column = ?"
899    pub sql_params: Vec<String>,    // parameters for the where clause
900    pub sql_order_by: Option<String>, // e.g., "id DESC"
901    pub sql_limit: Option<i64>,
902    pub sql_offset: Option<i64>,
903    pub sql_distinct: bool,
904    #[serde(default)]
905    pub column_adapters: BTreeMap<String, SQLiteColumnAdapter>,
906    #[serde(skip, default = "Signals::empty")]
907    signals: Signals,
908}
909
910impl SQLiteQueryBuilder {
911    pub fn new(db_path: PathBuf, table_name: String, signals: Signals) -> Self {
912        Self {
913            db_path,
914            table_name,
915            sql_select: None,
916            sql_where: None,
917            sql_params: Vec::new(),
918            sql_order_by: None,
919            sql_limit: None,
920            sql_offset: None,
921            sql_distinct: false,
922            column_adapters: BTreeMap::new(),
923            signals,
924        }
925    }
926
927    pub fn with_select(mut self, select: String) -> Self {
928        self.sql_select = Some(select);
929        self
930    }
931
932    pub fn with_where(mut self, where_clause: String, params: Vec<String>) -> Self {
933        self.sql_where = Some(where_clause);
934        self.sql_params = params;
935        self
936    }
937
938    pub fn with_order_by(mut self, order_by: String) -> Self {
939        self.sql_order_by = Some(order_by);
940        self
941    }
942
943    pub fn with_limit(mut self, limit: i64) -> Self {
944        self.sql_limit = Some(limit);
945        self
946    }
947
948    pub fn with_offset(mut self, offset: i64) -> Self {
949        self.sql_offset = Some(offset);
950        self
951    }
952
953    pub fn with_distinct(mut self) -> Self {
954        self.sql_distinct = true;
955        self
956    }
957
958    pub fn with_column_adapter(
959        mut self,
960        column_name: String,
961        adapter: SQLiteColumnAdapter,
962    ) -> Self {
963        self.column_adapters.insert(column_name, adapter);
964        self
965    }
966
967    /// Register a datetime adapter for a column containing Unix epoch milliseconds.
968    pub fn with_unix_millis_datetime_column(self, column_name: String) -> Self {
969        self.with_column_adapter(column_name, SQLiteColumnAdapter::UnixMillisToDate)
970    }
971
972    /// Register a duration adapter for a column containing milliseconds.
973    pub fn with_millis_duration_column(self, column_name: String) -> Self {
974        self.with_column_adapter(column_name, SQLiteColumnAdapter::MillisToDuration)
975    }
976
977    /// Projects a subset of *output* columns from the current SELECT list.
978    ///
979    /// This is used by filter pushdowns (for example, `history | select command`) where
980    /// Nushell refers to post-alias output names, but the underlying SQLite table may have
981    /// different source column names.
982    ///
983    /// Example:
984    /// - current projection: `command_line as command, duration_ms as duration`
985    /// - requested output: `command`
986    /// - rewritten projection: `command_line as command`
987    ///
988    /// If a requested output name cannot be mapped unambiguously to the existing projection,
989    /// this returns `None` so callers can safely fall back to non-pushdown behavior.
990    ///
991    /// This method intentionally does not parse full SQL grammar; it relies on a small,
992    /// conservative parser that is sufficient for projections we generate internally.
993    pub fn project_output_columns(&self, columns: &[String]) -> Option<Self> {
994        if columns.is_empty() {
995            return Some(self.clone());
996        }
997
998        let new_select = if let Some(select) = &self.sql_select {
999            // Parse the current projection into `(output_name, full_expression)` pairs.
1000            // We preserve the full expression so aliases and conversions stay intact.
1001            let current = parse_sql_select_projection(select)?;
1002            let mut projected = Vec::with_capacity(columns.len());
1003
1004            for requested in columns {
1005                // Match by output column name (case-insensitive)
1006                let expression = current.iter().find_map(|(output_name, expression)| {
1007                    output_name
1008                        .eq_ignore_ascii_case(requested)
1009                        .then_some(expression)
1010                })?;
1011                projected.push(expression.clone());
1012            }
1013
1014            projected.join(", ")
1015        } else {
1016            columns.join(", ")
1017        };
1018
1019        Some(self.clone().with_select(new_select))
1020    }
1021
1022    pub fn build_sql(&self) -> String {
1023        let distinct = if self.sql_distinct { "DISTINCT " } else { "" };
1024        let select = self.sql_select.as_deref().unwrap_or("*");
1025        let mut sql = format!("SELECT {distinct}{select} FROM [{}]", self.table_name);
1026
1027        if let Some(where_clause) = &self.sql_where {
1028            write!(sql, " WHERE {}", where_clause).expect("writing to a String is infallible");
1029        }
1030
1031        if let Some(order_by) = &self.sql_order_by {
1032            write!(sql, " ORDER BY {}", order_by).expect("writing to a String is infallible");
1033        }
1034
1035        match (self.sql_limit, self.sql_offset) {
1036            (Some(limit), Some(offset)) => {
1037                write!(sql, " LIMIT {limit} OFFSET {offset}")
1038                    .expect("writing to a String is infallible");
1039            }
1040            (Some(limit), None) => {
1041                write!(sql, " LIMIT {limit}").expect("writing to a String is infallible");
1042            }
1043            (None, Some(offset)) => {
1044                write!(sql, " LIMIT -1 OFFSET {offset}")
1045                    .expect("writing to a String is infallible");
1046            }
1047            (None, None) => {}
1048        }
1049
1050        sql
1051    }
1052
1053    pub fn execute(&self, call_span: Span) -> Result<PipelineData, ShellError> {
1054        let conn = open_sqlite_db(&self.db_path, call_span)?;
1055        let sql = self.build_sql();
1056        let params = NuSqlParams::List(Vec::new()); // FIXME: handle params properly
1057        let query = Spanned {
1058            item: sql,
1059            span: call_span,
1060        };
1061        run_sql_query(
1062            &conn,
1063            &query,
1064            params,
1065            &self.signals,
1066            (!self.column_adapters.is_empty()).then_some(&self.column_adapters),
1067        )
1068        .map(IntoPipelineData::into_pipeline_data)
1069        .map_err(|e| e.into_shell_error(call_span, "Failed to execute query"))
1070    }
1071
1072    pub fn count(&self, call_span: Span) -> Result<i64, ShellError> {
1073        let conn = open_sqlite_db(&self.db_path, call_span)?;
1074        let mut sql = format!("SELECT COUNT(*) FROM [{}]", self.table_name);
1075        if let Some(where_clause) = &self.sql_where {
1076            write!(sql, " WHERE {}", where_clause).expect("writing to a String is infallible");
1077        }
1078        let mut stmt = conn.prepare(&sql).map_err(|e| {
1079            ShellError::Generic(GenericError::new(
1080                "Failed to prepare count query",
1081                e.to_string(),
1082                call_span,
1083            ))
1084        })?;
1085        let params: Vec<Box<dyn ToSql>> = self
1086            .sql_params
1087            .iter()
1088            .map(|s| Box::new(s.clone()) as Box<dyn ToSql>)
1089            .collect();
1090        let count: i64 = stmt
1091            .query_row(rusqlite::params_from_iter(params), |row| row.get(0))
1092            .map_err(|e| {
1093                ShellError::Generic(GenericError::new(
1094                    "Failed to execute count query",
1095                    e.to_string(),
1096                    call_span,
1097                ))
1098            })?;
1099        Ok(count)
1100    }
1101}
1102
1103/// Parses a SELECT projection list into `(output_name, expression)` entries.
1104///
1105/// Input is the text after `SELECT` and before `FROM`, for example:
1106/// `command_line as command, duration_ms as duration`.
1107///
1108/// The returned expression is preserved exactly so it can be re-used in a rewritten
1109/// projection without dropping aliases.
1110///
1111/// Returns `None` for malformed/unsupported entries; callers should then skip pushdown.
1112fn parse_sql_select_projection(select: &str) -> Option<Vec<(String, String)>> {
1113    let projection = split_select_expressions(select)
1114        .into_iter()
1115        .map(|expr| parse_projection_expression(&expr))
1116        .collect::<Option<Vec<_>>>()?;
1117
1118    (!projection.is_empty()).then_some(projection)
1119}
1120
1121/// Splits a SELECT projection list on top-level commas.
1122///
1123/// We only split commas that are outside:
1124/// - single/double quoted strings
1125/// - parenthesized expressions
1126///
1127/// This is intentionally a lightweight splitter rather than a full SQL parser.
1128fn split_select_expressions(select: &str) -> Vec<String> {
1129    let mut expressions = Vec::new();
1130    let mut current = String::new();
1131    let mut depth = 0usize;
1132    let mut quote = None;
1133
1134    for ch in select.chars() {
1135        match ch {
1136            '\'' | '"' => {
1137                // Enter/exit quote mode so commas inside strings are preserved.
1138                if quote == Some(ch) {
1139                    quote = None;
1140                } else if quote.is_none() {
1141                    quote = Some(ch);
1142                }
1143                current.push(ch);
1144            }
1145            '(' if quote.is_none() => {
1146                // Track nesting depth so commas inside function calls do not split.
1147                depth = depth.saturating_add(1);
1148                current.push(ch);
1149            }
1150            ')' if quote.is_none() => {
1151                depth = depth.saturating_sub(1);
1152                current.push(ch);
1153            }
1154            ',' if quote.is_none() && depth == 0 => {
1155                // Top-level separator between projection expressions.
1156                let trimmed = current.trim();
1157                if !trimmed.is_empty() {
1158                    expressions.push(trimmed.to_string());
1159                }
1160                current.clear();
1161            }
1162            _ => current.push(ch),
1163        }
1164    }
1165
1166    let trimmed = current.trim();
1167    if !trimmed.is_empty() {
1168        expressions.push(trimmed.to_string());
1169    }
1170
1171    expressions
1172}
1173
1174/// Parses one projection expression into `(output_name, full_expression)`.
1175///
1176/// Supported forms include:
1177/// - `source_col as alias`
1178/// - `qualified.name`
1179/// - `column`
1180///
1181/// If no explicit alias is present, the output name is derived from the last
1182/// identifier segment (`foo.bar` -> `bar`).
1183fn parse_projection_expression(expr: &str) -> Option<(String, String)> {
1184    let trimmed = expr.trim();
1185    if trimmed.is_empty() {
1186        return None;
1187    }
1188
1189    if let Some((_lhs, rhs)) = split_alias(trimmed) {
1190        // Explicit alias wins and represents the user-visible output column name.
1191        let alias = normalize_identifier(rhs.trim());
1192        if alias.is_empty() {
1193            return None;
1194        }
1195        return Some((alias, trimmed.to_string()));
1196    }
1197
1198    let output_name = normalize_identifier(last_identifier_segment(trimmed));
1199    if output_name.is_empty() {
1200        return None;
1201    }
1202
1203    Some((output_name, trimmed.to_string()))
1204}
1205
1206/// Finds an `AS` alias split in a projection expression.
1207///
1208/// This intentionally requires whitespace around `AS` to avoid false positives in
1209/// identifiers or function names containing `as` as a substring.
1210///
1211/// Returns `(lhs, rhs)` for `lhs AS rhs`.
1212fn split_alias(expr: &str) -> Option<(&str, &str)> {
1213    let bytes = expr.as_bytes();
1214    for idx in 0..bytes.len().saturating_sub(2) {
1215        if idx > 0
1216            && bytes[idx - 1].is_ascii_whitespace()
1217            && bytes[idx + 2].is_ascii_whitespace()
1218            && bytes[idx].eq_ignore_ascii_case(&b'a')
1219            && bytes[idx + 1].eq_ignore_ascii_case(&b's')
1220        {
1221            // Keep the original expression parts intact so rewritten SQL maintains
1222            // the same semantics and formatting as much as possible.
1223            let lhs = expr[..idx].trim_end();
1224            let rhs = expr[idx + 2..].trim_start();
1225            if !lhs.is_empty() && !rhs.is_empty() {
1226                return Some((lhs, rhs));
1227            }
1228        }
1229    }
1230
1231    None
1232}
1233
1234fn last_identifier_segment(expr: &str) -> &str {
1235    expr.rsplit('.').next().unwrap_or(expr)
1236}
1237
1238/// Normalizes an identifier token for matching:
1239/// - trims surrounding whitespace
1240/// - removes a single layer of common SQL identifier wrappers (`"name"`, `` `name` ``, `[name]`)
1241///
1242/// The result is used only for name matching, not for SQL generation.
1243fn normalize_identifier(identifier: &str) -> String {
1244    let trimmed = identifier.trim();
1245    if trimmed.len() >= 2 {
1246        let first = trimmed.as_bytes()[0] as char;
1247        let last = trimmed.as_bytes()[trimmed.len() - 1] as char;
1248        let is_wrapped = matches!((first, last), ('"', '"') | ('`', '`') | ('[', ']'));
1249        if is_wrapped {
1250            return trimmed[1..trimmed.len() - 1].trim().to_string();
1251        }
1252    }
1253
1254    trimmed.to_string()
1255}
1256
1257impl CustomValue for SQLiteQueryBuilder {
1258    fn clone_value(&self, span: Span) -> Value {
1259        Value::custom(Box::new(self.clone()), span)
1260    }
1261
1262    fn type_name(&self) -> String {
1263        "SQLiteQueryBuilder".to_string()
1264    }
1265
1266    fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
1267        self.execute(span).and_then(|pd| pd.into_value(span))
1268    }
1269
1270    fn as_any(&self) -> &dyn std::any::Any {
1271        self
1272    }
1273
1274    fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
1275        self
1276    }
1277
1278    fn follow_path_int(
1279        &self,
1280        _self_span: Span,
1281        index: usize,
1282        path_span: Span,
1283        optional: bool,
1284    ) -> Result<Value, ShellError> {
1285        // Execute and then index - this could be optimized with LIMIT/OFFSET later
1286        let data = self.to_base_value(path_span)?;
1287        data.follow_cell_path(&[ast::PathMember::Int {
1288            val: index,
1289            span: path_span,
1290            optional,
1291        }])
1292        .map(|v| v.into_owned())
1293    }
1294
1295    fn follow_path_string(
1296        &self,
1297        _self_span: Span,
1298        column_name: String,
1299        path_span: Span,
1300        _optional: bool,
1301        _: Casing,
1302    ) -> Result<Value, ShellError> {
1303        // For now, just execute and get the column - this could be optimized later
1304        let data = self.to_base_value(path_span)?;
1305        data.follow_cell_path(&[ast::PathMember::String {
1306            val: column_name,
1307            span: path_span,
1308            optional: false,
1309            casing: Casing::default(),
1310        }])
1311        .map(|v| v.into_owned())
1312    }
1313
1314    fn typetag_name(&self) -> &'static str {
1315        "SQLiteQueryBuilder"
1316    }
1317
1318    fn typetag_deserialize(&self) {
1319        unimplemented!("typetag_deserialize")
1320    }
1321
1322    fn is_iterable(&self) -> bool {
1323        true
1324    }
1325}
1326
1327#[cfg(test)]
1328mod test {
1329    use super::*;
1330    use nu_protocol::record;
1331
1332    #[test]
1333    fn can_read_empty_db() {
1334        let db = open_connection_in_memory().unwrap();
1335        let converted_db =
1336            read_entire_sqlite_db(&db, Span::test_data(), &Signals::empty()).unwrap();
1337
1338        let expected = Value::test_record(Record::new());
1339
1340        assert_eq!(converted_db, expected);
1341    }
1342
1343    #[test]
1344    fn can_read_empty_table() {
1345        let db = open_connection_in_memory().unwrap();
1346
1347        db.execute(
1348            "CREATE TABLE person (
1349                    id     INTEGER PRIMARY KEY,
1350                    name   TEXT NOT NULL,
1351                    data   BLOB
1352                    )",
1353            [],
1354        )
1355        .unwrap();
1356        let converted_db =
1357            read_entire_sqlite_db(&db, Span::test_data(), &Signals::empty()).unwrap();
1358
1359        let expected = Value::test_record(record! {
1360            "person" => Value::test_list(vec![]),
1361        });
1362
1363        assert_eq!(converted_db, expected);
1364    }
1365
1366    #[test]
1367    fn can_read_null_and_non_null_data() {
1368        let span = Span::test_data();
1369        let db = open_connection_in_memory().unwrap();
1370
1371        db.execute(
1372            "CREATE TABLE item (
1373                    id     INTEGER PRIMARY KEY,
1374                    name   TEXT
1375                    )",
1376            [],
1377        )
1378        .unwrap();
1379
1380        db.execute("INSERT INTO item (id, name) VALUES (123, NULL)", [])
1381            .unwrap();
1382
1383        db.execute("INSERT INTO item (id, name) VALUES (456, 'foo bar')", [])
1384            .unwrap();
1385
1386        let converted_db = read_entire_sqlite_db(&db, span, &Signals::empty()).unwrap();
1387
1388        let expected = Value::test_record(record! {
1389            "item" => Value::test_list(
1390                vec![
1391                    Value::test_record(record! {
1392                        "id" =>   Value::test_int(123),
1393                        "name" => Value::nothing(span),
1394                    }),
1395                    Value::test_record(record! {
1396                        "id" =>   Value::test_int(456),
1397                        "name" => Value::test_string("foo bar"),
1398                    }),
1399                ]
1400            ),
1401        });
1402
1403        assert_eq!(converted_db, expected);
1404    }
1405
1406    #[test]
1407    fn sqlite_table_build_sql_combined() {
1408        let table = SQLiteQueryBuilder::new(
1409            PathBuf::from(":memory:"),
1410            "test".to_string(),
1411            Signals::empty(),
1412        )
1413        .with_select("col1".to_string())
1414        .with_where("col2 = ?".to_string(), vec!["val".to_string()])
1415        .with_order_by("col1".to_string())
1416        .with_limit(5);
1417        assert_eq!(
1418            table.build_sql(),
1419            "SELECT col1 FROM [test] WHERE col2 = ? ORDER BY col1 LIMIT 5"
1420        );
1421    }
1422
1423    #[test]
1424    fn sqlite_table_count_integration() {
1425        use tempfile::NamedTempFile;
1426
1427        let temp_file = NamedTempFile::new().unwrap();
1428        let db_path = temp_file.path().to_path_buf();
1429        let signals = Signals::empty();
1430
1431        // Create a test DB with data
1432        {
1433            let conn = Connection::open(&db_path).unwrap();
1434            conn.execute("CREATE TABLE test (id INTEGER, name TEXT)", [])
1435                .unwrap();
1436            for i in 0..10 {
1437                conn.execute(
1438                    "INSERT INTO test (id, name) VALUES (?, ?)",
1439                    rusqlite::params![i, format!("name{}", i)],
1440                )
1441                .unwrap();
1442            }
1443        }
1444
1445        let table = SQLiteQueryBuilder::new(db_path, "test".to_string(), signals);
1446        let count = table.count(Span::test_data()).unwrap();
1447        assert_eq!(count, 10);
1448    }
1449
1450    #[test]
1451    fn sqlite_table_execute_integration() {
1452        use tempfile::NamedTempFile;
1453
1454        let temp_file = NamedTempFile::new().unwrap();
1455        let db_path = temp_file.path().to_path_buf();
1456        let signals = Signals::empty();
1457
1458        // Create a test DB with data
1459        {
1460            let conn = Connection::open(&db_path).unwrap();
1461            conn.execute("CREATE TABLE test (id INTEGER, name TEXT)", [])
1462                .unwrap();
1463            conn.execute("INSERT INTO test (id, name) VALUES (1, 'first')", [])
1464                .unwrap();
1465            conn.execute("INSERT INTO test (id, name) VALUES (2, 'second')", [])
1466                .unwrap();
1467        }
1468
1469        let table = SQLiteQueryBuilder::new(db_path, "test".to_string(), signals);
1470        let result = table.execute(Span::test_data()).unwrap();
1471        let value = result.into_value(Span::test_data()).unwrap();
1472
1473        if let Value::List { vals, .. } = value {
1474            assert_eq!(vals.len(), 2);
1475        } else {
1476            panic!("Expected list");
1477        }
1478    }
1479
1480    #[test]
1481    fn sqlite_table_first_integration() {
1482        use tempfile::NamedTempFile;
1483
1484        let temp_file = NamedTempFile::new().unwrap();
1485        let db_path = temp_file.path().to_path_buf();
1486        let signals = Signals::empty();
1487
1488        // Create a test DB with data
1489        {
1490            let conn = Connection::open(&db_path).unwrap();
1491            conn.execute("CREATE TABLE test (id INTEGER, name TEXT)", [])
1492                .unwrap();
1493            for i in 0..5 {
1494                conn.execute(
1495                    "INSERT INTO test (id, name) VALUES (?, ?)",
1496                    rusqlite::params![i, format!("name{}", i)],
1497                )
1498                .unwrap();
1499            }
1500        }
1501
1502        let table = SQLiteQueryBuilder::new(db_path, "test".to_string(), signals).with_limit(2);
1503        let result = table.execute(Span::test_data()).unwrap();
1504        let value = result.into_value(Span::test_data()).unwrap();
1505
1506        if let Value::List { vals, .. } = value {
1507            assert_eq!(vals.len(), 2);
1508            // Check first two ids
1509            if let Value::Record { val: record, .. } = &vals[0] {
1510                assert_eq!(record.get("id"), Some(&Value::int(0, Span::test_data())));
1511            }
1512        } else {
1513            panic!("Expected list");
1514        }
1515    }
1516
1517    #[test]
1518    fn sqlite_table_last_integration() {
1519        use tempfile::NamedTempFile;
1520
1521        let temp_file = NamedTempFile::new().unwrap();
1522        let db_path = temp_file.path().to_path_buf();
1523        let signals = Signals::empty();
1524
1525        // Create a test DB with data
1526        {
1527            let conn = Connection::open(&db_path).unwrap();
1528            conn.execute("CREATE TABLE test (id INTEGER, name TEXT)", [])
1529                .unwrap();
1530            for i in 0..5 {
1531                conn.execute(
1532                    "INSERT INTO test (id, name) VALUES (?, ?)",
1533                    rusqlite::params![i, format!("name{}", i)],
1534                )
1535                .unwrap();
1536            }
1537        }
1538
1539        let table = SQLiteQueryBuilder::new(db_path, "test".to_string(), signals)
1540            .with_order_by("rowid DESC".to_string())
1541            .with_limit(2);
1542        let result = table.execute(Span::test_data()).unwrap();
1543        let value = result.into_value(Span::test_data()).unwrap();
1544
1545        if let Value::List { vals, .. } = value {
1546            assert_eq!(vals.len(), 2);
1547            // Check last two ids (since DESC, first in result is highest)
1548            if let Value::Record { val: record, .. } = &vals[0] {
1549                assert_eq!(record.get("id"), Some(&Value::int(4, Span::test_data())));
1550            }
1551        } else {
1552            panic!("Expected list");
1553        }
1554    }
1555
1556    #[test]
1557    fn sqlite_table_build_sql_with_select() {
1558        let table = SQLiteQueryBuilder::new(
1559            PathBuf::from(":memory:"),
1560            "test".to_string(),
1561            Signals::empty(),
1562        )
1563        .with_select("col1, col2".to_string());
1564        assert_eq!(table.build_sql(), "SELECT col1, col2 FROM [test]");
1565    }
1566
1567    #[test]
1568    fn sqlite_table_build_sql_with_where() {
1569        let table = SQLiteQueryBuilder::new(
1570            PathBuf::from(":memory:"),
1571            "test".to_string(),
1572            Signals::empty(),
1573        )
1574        .with_where("col = ?".to_string(), vec!["val".to_string()]);
1575        assert_eq!(table.build_sql(), "SELECT * FROM [test] WHERE col = ?");
1576    }
1577
1578    #[test]
1579    fn sqlite_table_build_sql_with_order_by() {
1580        let table = SQLiteQueryBuilder::new(
1581            PathBuf::from(":memory:"),
1582            "test".to_string(),
1583            Signals::empty(),
1584        )
1585        .with_order_by("id DESC".to_string());
1586        assert_eq!(table.build_sql(), "SELECT * FROM [test] ORDER BY id DESC");
1587    }
1588
1589    #[test]
1590    fn sqlite_table_build_sql_with_limit() {
1591        let table = SQLiteQueryBuilder::new(
1592            PathBuf::from(":memory:"),
1593            "test".to_string(),
1594            Signals::empty(),
1595        )
1596        .with_limit(10);
1597        assert_eq!(table.build_sql(), "SELECT * FROM [test] LIMIT 10");
1598    }
1599
1600    #[test]
1601    fn sqlite_table_execute_with_column_adapters() {
1602        use tempfile::NamedTempFile;
1603
1604        let temp_file = NamedTempFile::new().unwrap();
1605        let db_path = temp_file.path().to_path_buf();
1606        let signals = Signals::empty();
1607
1608        {
1609            let conn = Connection::open(&db_path).unwrap();
1610            conn.execute(
1611                "CREATE TABLE history (start_timestamp INTEGER, duration INTEGER)",
1612                [],
1613            )
1614            .unwrap();
1615            conn.execute(
1616                "INSERT INTO history (start_timestamp, duration) VALUES (1736041045123, 30002)",
1617                [],
1618            )
1619            .unwrap();
1620            conn.execute(
1621                "INSERT INTO history (start_timestamp, duration) VALUES (NULL, NULL)",
1622                [],
1623            )
1624            .unwrap();
1625        }
1626
1627        let table = SQLiteQueryBuilder::new(db_path, "history".to_string(), signals)
1628            .with_select("start_timestamp, duration".to_string())
1629            .with_unix_millis_datetime_column("start_timestamp".to_string())
1630            .with_millis_duration_column("duration".to_string());
1631
1632        let result = table.execute(Span::test_data()).unwrap();
1633        let value = result.into_value(Span::test_data()).unwrap();
1634
1635        if let Value::List { vals, .. } = value {
1636            assert_eq!(vals.len(), 2);
1637
1638            if let Value::Record { val: first, .. } = &vals[0] {
1639                assert!(matches!(
1640                    first.get("start_timestamp"),
1641                    Some(Value::Date { .. })
1642                ));
1643                assert!(matches!(
1644                    first.get("duration"),
1645                    Some(Value::Duration { .. })
1646                ));
1647            } else {
1648                panic!("Expected first row to be a record");
1649            }
1650
1651            if let Value::Record { val: second, .. } = &vals[1] {
1652                assert!(matches!(
1653                    second.get("start_timestamp"),
1654                    Some(Value::Nothing { .. })
1655                ));
1656                assert!(matches!(
1657                    second.get("duration"),
1658                    Some(Value::Nothing { .. })
1659                ));
1660            } else {
1661                panic!("Expected second row to be a record");
1662            }
1663        } else {
1664            panic!("Expected list");
1665        }
1666    }
1667
1668    #[test]
1669    fn sqlite_table_project_output_columns_preserves_aliases() {
1670        let table = SQLiteQueryBuilder::new(
1671            PathBuf::from(":memory:"),
1672            "history".to_string(),
1673            Signals::empty(),
1674        )
1675        .with_select(
1676            "start_timestamp, command_line as command, cwd, duration_ms as duration, exit_status"
1677                .to_string(),
1678        );
1679
1680        let projected = table
1681            .project_output_columns(&["command".to_string(), "duration".to_string()])
1682            .expect("projection should succeed");
1683
1684        assert_eq!(
1685            projected.build_sql(),
1686            "SELECT command_line as command, duration_ms as duration FROM [history]"
1687        );
1688    }
1689
1690    #[test]
1691    fn sqlite_table_project_output_columns_returns_none_for_missing_column() {
1692        let table = SQLiteQueryBuilder::new(
1693            PathBuf::from(":memory:"),
1694            "history".to_string(),
1695            Signals::empty(),
1696        )
1697        .with_select("command_line as command".to_string());
1698
1699        assert!(
1700            table
1701                .project_output_columns(&["missing".to_string()])
1702                .is_none()
1703        );
1704    }
1705
1706    /// Regression for concurrent memdb access (#17041 / shared-connection design).
1707    /// Writers and readers all go through the process-wide mutex; no SQLITE_BUSY.
1708    #[test]
1709    fn shared_mem_conn_is_safe_under_concurrency() {
1710        const TABLE: &str = "shared_mem_conn_concurrency";
1711        const WRITERS: usize = 8;
1712        const INSERTS_PER_WRITER: usize = 50;
1713
1714        {
1715            let conn = get_shared_mem_conn().expect("shared conn");
1716            conn.execute(&format!("DROP TABLE IF EXISTS {TABLE}"), [])
1717                .expect("drop");
1718            conn.execute(
1719                &format!("CREATE TABLE {TABLE} (id INTEGER PRIMARY KEY, val INTEGER)"),
1720                [],
1721            )
1722            .expect("create");
1723        }
1724
1725        let mut handles = Vec::with_capacity(WRITERS + 2);
1726        for writer in 0..WRITERS {
1727            handles.push(std::thread::spawn(move || {
1728                for i in 0..INSERTS_PER_WRITER {
1729                    let conn = get_shared_mem_conn().expect("shared conn");
1730                    let val = (writer * INSERTS_PER_WRITER + i) as i64;
1731                    conn.execute(&format!("INSERT INTO {TABLE} (val) VALUES (?1)"), [val])
1732                        .expect("insert under concurrency");
1733                }
1734            }));
1735        }
1736
1737        for _ in 0..2 {
1738            handles.push(std::thread::spawn(|| {
1739                for _ in 0..INSERTS_PER_WRITER {
1740                    let conn = get_shared_mem_conn().expect("shared conn");
1741                    let _: i64 = conn
1742                        .query_row(&format!("SELECT COUNT(*) FROM {TABLE}"), [], |row| {
1743                            row.get(0)
1744                        })
1745                        .expect("select under concurrency");
1746                }
1747            }));
1748        }
1749
1750        for handle in handles {
1751            handle.join().expect("thread panicked");
1752        }
1753
1754        let conn = get_shared_mem_conn().expect("shared conn");
1755        let count: i64 = conn
1756            .query_row(&format!("SELECT COUNT(*) FROM {TABLE}"), [], |row| {
1757                row.get(0)
1758            })
1759            .expect("final count");
1760        assert_eq!(count, (WRITERS * INSERTS_PER_WRITER) as i64);
1761    }
1762
1763    #[test]
1764    fn open_sqlite_db_memory_uses_shared_connection() {
1765        let path = Path::new(MEMORY_DB);
1766        let a = open_sqlite_db(path, Span::test_data()).expect("open a");
1767        drop(a);
1768        let b = open_sqlite_db(path, Span::test_data()).expect("open b");
1769        // Second open after drop should re-lock the same process-wide connection.
1770        let one: i64 = b
1771            .query_row("SELECT 1", [], |row| row.get(0))
1772            .expect("query shared conn");
1773        assert_eq!(one, 1);
1774    }
1775}