From 016b815b59bd75f2930a117c0e4e13b70e87b07a Mon Sep 17 00:00:00 2001 From: Glauber Costa Date: Thu, 30 Jan 2025 15:26:02 -0500 Subject: [PATCH] implement pragma table_info Both () and = variants covered. It is important to make sure that the transaction is a read transaction, so we cannot hide all that logic inside update_pragma, and have to make our decision before that. --- COMPAT.md | 2 +- core/translate/mod.rs | 140 ++++++++++++++++-- testing/pragma.test | 22 +++ vendored/sqlite3-parser/src/parser/ast/mod.rs | 3 + 4 files changed, 154 insertions(+), 13 deletions(-) diff --git a/COMPAT.md b/COMPAT.md index 11167aa91..dccaca900 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -151,7 +151,7 @@ The current status of Limbo is: | PRAGMA soft_heap_limit | No | | | PRAGMA stats | No | Used for testing in SQLite | | PRAGMA synchronous | No | | -| PRAGMA table_info | No | | +| PRAGMA table_info | Yes | | | PRAGMA table_list | No | | | PRAGMA table_xinfo | No | | | PRAGMA temp_store | No | | diff --git a/core/translate/mod.rs b/core/translate/mod.rs index 41c1e3798..1f40c3f02 100644 --- a/core/translate/mod.rs +++ b/core/translate/mod.rs @@ -27,7 +27,7 @@ use crate::storage::pager::Pager; use crate::storage::sqlite3_ondisk::{DatabaseHeader, MIN_PAGE_CACHE_SIZE}; use crate::storage::wal::CheckpointMode; use crate::translate::delete::translate_delete; -use crate::util::PRIMARY_KEY_AUTOMATIC_INDEX_NAME_PREFIX; +use crate::util::{normalize_ident, PRIMARY_KEY_AUTOMATIC_INDEX_NAME_PREFIX}; use crate::vdbe::builder::CursorType; use crate::vdbe::{builder::ProgramBuilder, insn::Insn, Program}; use crate::{bail_parse_error, Connection, LimboError, Result, SymbolTable}; @@ -90,7 +90,14 @@ pub fn translate( ast::Stmt::DropTrigger { .. } => bail_parse_error!("DROP TRIGGER not supported yet"), ast::Stmt::DropView { .. } => bail_parse_error!("DROP VIEW not supported yet"), ast::Stmt::Pragma(name, body) => { - translate_pragma(&mut program, &name, body, database_header.clone(), pager)?; + translate_pragma( + &mut program, + &schema, + &name, + body, + database_header.clone(), + pager, + )?; } ast::Stmt::Reindex { .. } => bail_parse_error!("REINDEX not supported yet"), ast::Stmt::Release(_) => bail_parse_error!("RELEASE not supported yet"), @@ -526,6 +533,7 @@ enum PrimaryKeyDefinitionType<'a> { fn translate_pragma( program: &mut ProgramBuilder, + schema: &Schema, name: &ast::QualifiedName, body: Option, database_header: Rc>, @@ -545,15 +553,44 @@ fn translate_pragma( match body { None => { - query_pragma(pragma, database_header.clone(), program)?; - } - Some(ast::PragmaBody::Equals(value)) => { - write = true; - update_pragma(pragma, value, database_header.clone(), pager, program)?; - } - Some(ast::PragmaBody::Call(_)) => { - todo!() + query_pragma(pragma, schema, None, database_header.clone(), program)?; } + Some(ast::PragmaBody::Equals(value)) => match pragma { + PragmaName::TableInfo => { + query_pragma( + pragma, + schema, + Some(value), + database_header.clone(), + program, + )?; + } + _ => { + write = true; + update_pragma( + pragma, + schema, + value, + database_header.clone(), + pager, + program, + )?; + } + }, + Some(ast::PragmaBody::Call(value)) => match pragma { + PragmaName::TableInfo => { + query_pragma( + pragma, + schema, + Some(value), + database_header.clone(), + program, + )?; + } + _ => { + todo!() + } + }, }; program.emit_insn(Insn::Halt { err_code: 0, @@ -571,6 +608,7 @@ fn translate_pragma( fn update_pragma( pragma: PragmaName, + schema: &Schema, value: ast::Expr, header: Rc>, pager: Rc, @@ -594,18 +632,26 @@ fn update_pragma( Ok(()) } PragmaName::JournalMode => { - query_pragma(PragmaName::JournalMode, header, program)?; + query_pragma(PragmaName::JournalMode, schema, None, header, program)?; Ok(()) } PragmaName::WalCheckpoint => { - query_pragma(PragmaName::WalCheckpoint, header, program)?; + query_pragma(PragmaName::WalCheckpoint, schema, None, header, program)?; Ok(()) } + PragmaName::TableInfo => { + // because we need control over the write parameter for the transaction, + // this should be unreachable. We have to force-call query_pragma before + // getting here + unreachable!(); + } } } fn query_pragma( pragma: PragmaName, + schema: &Schema, + value: Option, database_header: Rc>, program: &mut ProgramBuilder, ) -> Result<()> { @@ -646,6 +692,76 @@ fn query_pragma( count: 3, }); } + PragmaName::TableInfo => { + let table = match value { + Some(ast::Expr::Name(name)) => { + let tbl = normalize_ident(&name.0); + schema.get_table(&tbl) + } + _ => None, + }; + + let base_reg = register; + program.alloc_register(); + program.alloc_register(); + program.alloc_register(); + program.alloc_register(); + program.alloc_register(); + if let Some(table) = table { + for (i, column) in table.columns.iter().enumerate() { + // cid + program.emit_insn(Insn::Integer { + value: i as i64, + dest: base_reg, + }); + + // name + program.emit_insn(Insn::String8 { + value: column.name.clone(), + dest: base_reg + 1, + }); + + // type + program.emit_insn(Insn::String8 { + value: column.ty_str.clone(), + dest: base_reg + 2, + }); + + // notnull + program.emit_insn(Insn::Integer { + value: if column.notnull { 1 } else { 0 }, + dest: base_reg + 3, + }); + + // dflt_value + match &column.default { + None => { + program.emit_insn(Insn::Null { + dest: base_reg + 4, + dest_end: Some(base_reg + 5), + }); + } + Some(expr) => { + program.emit_insn(Insn::String8 { + value: expr.to_string(), + dest: base_reg + 4, + }); + } + } + + // pk + program.emit_insn(Insn::Integer { + value: if column.primary_key { 1 } else { 0 }, + dest: base_reg + 5, + }); + + program.emit_insn(Insn::ResultRow { + start_reg: base_reg, + count: 6, + }); + } + } + } } Ok(()) diff --git a/testing/pragma.test b/testing/pragma.test index ce6b2996c..17c33c1b0 100755 --- a/testing/pragma.test +++ b/testing/pragma.test @@ -10,3 +10,25 @@ do_execsql_test pragma-cache-size { do_execsql_test pragma-update-journal-mode-wal { PRAGMA journal_mode=WAL } {wal} + +do_execsql_test pragma-table-info-equal-syntax { + PRAGMA table_info=sqlite_schema +} {0|type|TEXT|0||0 +1|name|TEXT|0||0 +2|tbl_name|TEXT|0||0 +3|rootpage|INT|0||0 +4|sql|TEXT|0||0 +} + +do_execsql_test pragma-table-info-call-syntax { + PRAGMA table_info(sqlite_schema) +} {0|type|TEXT|0||0 +1|name|TEXT|0||0 +2|tbl_name|TEXT|0||0 +3|rootpage|INT|0||0 +4|sql|TEXT|0||0 +} + +do_execsql_test pragma-table-info-invalid-table { + PRAGMA table_info=pekka +} {} diff --git a/vendored/sqlite3-parser/src/parser/ast/mod.rs b/vendored/sqlite3-parser/src/parser/ast/mod.rs index b81eaa9b5..f46f38e85 100644 --- a/vendored/sqlite3-parser/src/parser/ast/mod.rs +++ b/vendored/sqlite3-parser/src/parser/ast/mod.rs @@ -1593,6 +1593,8 @@ pub enum PragmaName { JournalMode, /// trigger a checkpoint to run on database(s) if WAL is enabled WalCheckpoint, + /// returns information about the columns of a table + TableInfo, } impl FromStr for PragmaName { @@ -1603,6 +1605,7 @@ impl FromStr for PragmaName { "cache_size" => Ok(PragmaName::CacheSize), "wal_checkpoint" => Ok(PragmaName::WalCheckpoint), "journal_mode" => Ok(PragmaName::JournalMode), + "table_info" => Ok(PragmaName::TableInfo), _ => Err(()), } }