Implement pragma database_list

And also the CLI option .databases, which is just manipulating that.

This is one step in the road to attach.
This commit is contained in:
Glauber Costa
2025-07-20 13:30:08 -05:00
parent 440ff43a72
commit 0545049d59
8 changed files with 135 additions and 13 deletions

View File

@@ -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 count_changes | Not Needed | deprecated in SQLite |
| PRAGMA data_store_directory | Not Needed | deprecated in SQLite | | PRAGMA data_store_directory | Not Needed | deprecated in SQLite |
| PRAGMA data_version | No | | | PRAGMA data_version | No | |
| PRAGMA database_list | No | | | PRAGMA database_list | Yes | |
| PRAGMA default_cache_size | Not Needed | deprecated in SQLite | | PRAGMA default_cache_size | Not Needed | deprecated in SQLite |
| PRAGMA defer_foreign_keys | No | | | PRAGMA defer_foreign_keys | No | |
| PRAGMA empty_result_callbacks | Not Needed | deprecated in SQLite | | PRAGMA empty_result_callbacks | Not Needed | deprecated in SQLite |

View File

@@ -624,6 +624,11 @@ impl Limbo {
let _ = self.writeln(e.to_string()); let _ = self.writeln(e.to_string());
} }
} }
Command::Databases => {
if let Err(e) = self.display_databases() {
let _ = self.writeln(e.to_string());
}
}
Command::Opcodes(args) => { Command::Opcodes(args) => {
if let Some(opcode) = args.opcode { if let Some(opcode) = args.opcode {
for op in &OPCODE_DESCRIPTIONS { for op in &OPCODE_DESCRIPTIONS {
@@ -1100,6 +1105,75 @@ impl Limbo {
Ok(()) 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) { pub fn handle_remaining_input(&mut self) {
if self.input_buff.is_empty() { if self.input_buff.is_empty() {
return; return;

View File

@@ -62,6 +62,8 @@ pub enum Command {
Echo(EchoArgs), Echo(EchoArgs),
/// Display tables /// Display tables
Tables(TablesArgs), Tables(TablesArgs),
/// Display attached databases
Databases,
/// Import data from FILE into TABLE /// Import data from FILE into TABLE
#[command(name = "import", display_name = ".import")] #[command(name = "import", display_name = ".import")]
Import(ImportArgs), Import(ImportArgs),

View File

@@ -200,39 +200,43 @@ pub const AFTER_HELP_MSG: &str = r#"Usage Examples:
4. To list all tables: 4. To list all tables:
.tables .tables
5. To list all available SQL opcodes: 5. To list all databases:
.databases
6. To list all available SQL opcodes:
.opcodes .opcodes
6. To change the current output mode to 'pretty': 7. To change the current output mode to 'pretty':
.mode pretty .mode pretty
7. Send output to STDOUT if no file is specified: 8. Send output to STDOUT if no file is specified:
.output .output
8. To change the current working directory to '/tmp': 9. To change the current working directory to '/tmp':
.cd /tmp .cd /tmp
9. Show the current values of settings: 10. Show the current values of settings:
.show .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 .import --csv sample.csv csv_table
11. To display the database contents as SQL: 12. To display the database contents as SQL:
.dump .dump
12. To load an extension library: 13. To load an extension library:
.load /target/debug/liblimbo_regexp .load /target/debug/liblimbo_regexp
13. To list all available VFS: 14. To list all available VFS:
.listvfs .listvfs
14. To show names of indexes:
15. To show names of indexes:
.indexes ?TABLE? .indexes ?TABLE?
15. To turn on column headers in list mode: 16. To turn on column headers in list mode:
.headers on .headers on
16. To turn off column headers in list mode: 17. To turn off column headers in list mode:
.headers off .headers off
Note: Note:

View File

@@ -893,6 +893,23 @@ impl Connection {
self.page_size.get() 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. /// Reset the page size for the current connection.
/// ///
/// Specifying a new page size does not change the page size immediately. /// Specifying a new page size does not change the page size immediately.

View File

@@ -46,6 +46,7 @@ pub fn pragma_for(pragma: &PragmaName) -> Pragma {
| PragmaFlags::NoColumns1, | PragmaFlags::NoColumns1,
&["cache_size"], &["cache_size"],
), ),
DatabaseList => Pragma::new(PragmaFlags::Result0, &["seq", "name", "file"]),
JournalMode => Pragma::new( JournalMode => Pragma::new(
PragmaFlags::NeedSchema | PragmaFlags::Result0 | PragmaFlags::SchemaReq, PragmaFlags::NeedSchema | PragmaFlags::Result0 | PragmaFlags::SchemaReq,
&["journal_mode"], &["journal_mode"],

View File

@@ -250,6 +250,7 @@ fn update_pragma(
connection.set_capture_data_changes(opts); connection.set_capture_data_changes(opts);
Ok((program, TransactionMode::Write)) 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()); program.add_pragma_result_column(pragma.to_string());
Ok((program, TransactionMode::None)) 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 => { PragmaName::JournalMode => {
program.emit_string8("wal".into(), register); program.emit_string8("wal".into(), register);
program.emit_result_row(register, 1); program.emit_result_row(register, 1);

View File

@@ -1749,6 +1749,8 @@ pub enum PragmaName {
AutoVacuum, AutoVacuum,
/// `cache_size` pragma /// `cache_size` pragma
CacheSize, CacheSize,
/// List databases
DatabaseList,
/// Run integrity check on the database file /// Run integrity check on the database file
IntegrityCheck, IntegrityCheck,
/// `journal_mode` pragma /// `journal_mode` pragma