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
28static SHARED_MEM_CONN: OnceLock<Mutex<Connection>> = OnceLock::new();
38
39pub 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
51fn shared_mem_mutex() -> Result<&'static Mutex<Connection>, ShellError> {
54 if let Some(mutex) = SHARED_MEM_CONN.get() {
55 return Ok(mutex);
56 }
57
58 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
69pub fn init_shared_memory_db() -> Result<(), ShellError> {
74 shared_mem_mutex().map(|_| ())
75}
76
77pub(crate) fn get_shared_mem_conn() -> Result<MutexGuard<'static, Connection>, ShellError> {
82 shared_mem_mutex()?.lock().map_err(map_lock_error)
83}
84
85pub 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 pub path: PathBuf,
121 #[serde(skip, default = "Signals::empty")]
122 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 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 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 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 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
482pub 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
521pub 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 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#[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, }
662 }
663}
664
665pub 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 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 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 UnixMillisToDate,
810 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#[derive(Debug, Clone, Serialize, Deserialize)]
894pub struct SQLiteQueryBuilder {
895 pub db_path: PathBuf,
896 pub table_name: String,
897 pub sql_select: Option<String>, pub sql_where: Option<String>, pub sql_params: Vec<String>, pub sql_order_by: Option<String>, 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 pub fn with_unix_millis_datetime_column(self, column_name: String) -> Self {
969 self.with_column_adapter(column_name, SQLiteColumnAdapter::UnixMillisToDate)
970 }
971
972 pub fn with_millis_duration_column(self, column_name: String) -> Self {
974 self.with_column_adapter(column_name, SQLiteColumnAdapter::MillisToDuration)
975 }
976
977 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 let current = parse_sql_select_projection(select)?;
1002 let mut projected = Vec::with_capacity(columns.len());
1003
1004 for requested in columns {
1005 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()); 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
1103fn 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
1121fn 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 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 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 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
1174fn 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 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
1206fn 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 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
1238fn 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 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 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 {
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 {
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 {
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 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 {
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 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 #[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 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}