diff --git a/COMPAT.md b/COMPAT.md index c9f254556..567efd536 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -119,7 +119,7 @@ Turso aims to be fully compatible with SQLite, with opt-in features not supporte | PRAGMA count_changes | Not Needed | deprecated in SQLite | | PRAGMA data_store_directory | Not Needed | deprecated in SQLite | | PRAGMA data_version | No | | -| PRAGMA database_list | No | | +| PRAGMA database_list | Yes | | | PRAGMA default_cache_size | Not Needed | deprecated in SQLite | | PRAGMA defer_foreign_keys | No | | | PRAGMA empty_result_callbacks | Not Needed | deprecated in SQLite | diff --git a/cli/app.rs b/cli/app.rs index a6e574f30..121cd0c4e 100644 --- a/cli/app.rs +++ b/cli/app.rs @@ -624,6 +624,11 @@ impl Limbo { let _ = self.writeln(e.to_string()); } } + Command::Databases => { + if let Err(e) = self.display_databases() { + let _ = self.writeln(e.to_string()); + } + } Command::Opcodes(args) => { if let Some(opcode) = args.opcode { for op in &OPCODE_DESCRIPTIONS { @@ -1100,6 +1105,75 @@ impl Limbo { Ok(()) } + fn display_databases(&mut self) -> anyhow::Result<()> { + let sql = "PRAGMA database_list"; + + match self.conn.query(sql) { + Ok(Some(ref mut rows)) => { + loop { + match rows.step()? { + StepResult::Row => { + let row = rows.row().unwrap(); + if let ( + Ok(Value::Integer(_seq)), + Ok(Value::Text(name)), + Ok(file_value), + ) = ( + row.get::<&Value>(0), + row.get::<&Value>(1), + row.get::<&Value>(2), + ) { + let file = match file_value { + Value::Text(path) => path.as_str(), + Value::Null => "", + _ => "", + }; + + // Format like SQLite: "main: /path/to/file r/w" + let file_display = if file.is_empty() { + "\"\"".to_string() + } else { + file.to_string() + }; + + // Detect readonly mode from connection + let mode = if self.conn.is_readonly() { + "r/o" + } else { + "r/w" + }; + + let _ = self.writeln(format!( + "{}: {} {}", + name.as_str(), + file_display, + mode + )); + } + } + StepResult::IO => { + rows.run_once()?; + } + StepResult::Interrupt => break, + StepResult::Done => break, + StepResult::Busy => { + let _ = self.writeln("database is busy"); + break; + } + } + } + } + Ok(None) => { + let _ = self.writeln("No results returned from the query."); + } + Err(err) => { + return Err(anyhow::anyhow!("Error querying database list: {}", err)); + } + } + + Ok(()) + } + pub fn handle_remaining_input(&mut self) { if self.input_buff.is_empty() { return; diff --git a/cli/commands/mod.rs b/cli/commands/mod.rs index 77f40c1c9..8c2eec10c 100644 --- a/cli/commands/mod.rs +++ b/cli/commands/mod.rs @@ -62,6 +62,8 @@ pub enum Command { Echo(EchoArgs), /// Display tables Tables(TablesArgs), + /// Display attached databases + Databases, /// Import data from FILE into TABLE #[command(name = "import", display_name = ".import")] Import(ImportArgs), diff --git a/cli/input.rs b/cli/input.rs index 447c741f0..247798021 100644 --- a/cli/input.rs +++ b/cli/input.rs @@ -200,39 +200,43 @@ pub const AFTER_HELP_MSG: &str = r#"Usage Examples: 4. To list all tables: .tables -5. To list all available SQL opcodes: +5. To list all databases: + .databases + +6. To list all available SQL opcodes: .opcodes -6. To change the current output mode to 'pretty': +7. To change the current output mode to 'pretty': .mode pretty -7. Send output to STDOUT if no file is specified: +8. Send output to STDOUT if no file is specified: .output -8. To change the current working directory to '/tmp': +9. To change the current working directory to '/tmp': .cd /tmp -9. Show the current values of settings: +10. Show the current values of settings: .show -10. To import csv file 'sample.csv' into 'csv_table' table: +11. To import csv file 'sample.csv' into 'csv_table' table: .import --csv sample.csv csv_table -11. To display the database contents as SQL: +12. To display the database contents as SQL: .dump -12. To load an extension library: +13. To load an extension library: .load /target/debug/liblimbo_regexp -13. To list all available VFS: +14. To list all available VFS: .listvfs -14. To show names of indexes: + +15. To show names of indexes: .indexes ?TABLE? -15. To turn on column headers in list mode: +16. To turn on column headers in list mode: .headers on -16. To turn off column headers in list mode: +17. To turn off column headers in list mode: .headers off Note: diff --git a/core/lib.rs b/core/lib.rs index 2c38bfe41..2403f85f9 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -893,6 +893,23 @@ impl Connection { self.page_size.get() } + pub fn get_database_canonical_path(&self) -> String { + if self._db.path == ":memory:" { + // For in-memory databases, SQLite shows empty string + String::new() + } else { + // For file databases, try show the full absolute path if that doesn't fail + match std::fs::canonicalize(&self._db.path) { + Ok(abs_path) => abs_path.to_string_lossy().to_string(), + Err(_) => self._db.path.to_string(), + } + } + } + + pub fn is_readonly(&self) -> bool { + self._db.open_flags.contains(OpenFlags::ReadOnly) + } + /// Reset the page size for the current connection. /// /// Specifying a new page size does not change the page size immediately. diff --git a/core/pragma.rs b/core/pragma.rs index 7c45eefcb..eedd04368 100644 --- a/core/pragma.rs +++ b/core/pragma.rs @@ -46,6 +46,7 @@ pub fn pragma_for(pragma: &PragmaName) -> Pragma { | PragmaFlags::NoColumns1, &["cache_size"], ), + DatabaseList => Pragma::new(PragmaFlags::Result0, &["seq", "name", "file"]), JournalMode => Pragma::new( PragmaFlags::NeedSchema | PragmaFlags::Result0 | PragmaFlags::SchemaReq, &["journal_mode"], diff --git a/core/translate/pragma.rs b/core/translate/pragma.rs index cb06347be..163319ac9 100644 --- a/core/translate/pragma.rs +++ b/core/translate/pragma.rs @@ -250,6 +250,7 @@ fn update_pragma( connection.set_capture_data_changes(opts); Ok((program, TransactionMode::Write)) } + PragmaName::DatabaseList => unreachable!("database_list cannot be set"), } } @@ -279,6 +280,27 @@ fn query_pragma( program.add_pragma_result_column(pragma.to_string()); Ok((program, TransactionMode::None)) } + PragmaName::DatabaseList => { + let base_reg = register; + program.alloc_registers(2); + + // For now, we only show the main database (seq=0) + // seq (sequence number) + program.emit_int(0, base_reg); + + // name + program.emit_string8("main".into(), base_reg + 1); + + let file_path = connection.get_database_canonical_path(); + program.emit_string8(file_path, base_reg + 2); + + program.emit_result_row(base_reg, 3); + let pragma = pragma_for(&pragma); + for col_name in pragma.columns.iter() { + program.add_pragma_result_column(col_name.to_string()); + } + Ok((program, TransactionMode::None)) + } PragmaName::JournalMode => { program.emit_string8("wal".into(), register); program.emit_result_row(register, 1); diff --git a/vendored/sqlite3-parser/src/parser/ast/mod.rs b/vendored/sqlite3-parser/src/parser/ast/mod.rs index 096149c72..a44266572 100644 --- a/vendored/sqlite3-parser/src/parser/ast/mod.rs +++ b/vendored/sqlite3-parser/src/parser/ast/mod.rs @@ -1749,6 +1749,8 @@ pub enum PragmaName { AutoVacuum, /// `cache_size` pragma CacheSize, + /// List databases + DatabaseList, /// Run integrity check on the database file IntegrityCheck, /// `journal_mode` pragma