From 42f93e9bea64b164c2a5540eb0bf95df89a89fe0 Mon Sep 17 00:00:00 2001 From: Glauber Costa Date: Thu, 30 Jan 2025 16:45:36 -0500 Subject: [PATCH 1/5] add default type to Column definition --- core/schema.rs | 41 ++++++++++++++++++++++++++++++++------ core/translate/group_by.rs | 1 + core/translate/order_by.rs | 2 ++ core/translate/plan.rs | 1 + 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/core/schema.rs b/core/schema.rs index fda6c12ba..32dc631c5 100644 --- a/core/schema.rs +++ b/core/schema.rs @@ -178,6 +178,7 @@ impl PseudoTable { ty, primary_key, is_rowid_alias: false, + default: None, }); } pub fn get_column(&self, name: &str) -> Option<(usize, &Column)> { @@ -268,22 +269,33 @@ fn create_table( } None => Type::Null, }; - let mut primary_key = col_def.constraints.iter().any(|c| { - matches!( - c.constraint, - sqlite3_parser::ast::ColumnConstraint::PrimaryKey { .. } - ) - }); + + let mut default = None; + let mut primary_key = false; + for c_def in &col_def.constraints { + match &c_def.constraint { + sqlite3_parser::ast::ColumnConstraint::PrimaryKey { .. } => { + primary_key = true; + } + sqlite3_parser::ast::ColumnConstraint::Default(expr) => { + default = Some(expr.clone()) + } + _ => {} + } + } + if primary_key { primary_key_column_names.push(name.clone()); } else if primary_key_column_names.contains(&name) { primary_key = true; } + cols.push(Column { name: normalize_ident(&name), ty, primary_key, is_rowid_alias: typename_exactly_integer && primary_key, + default, }); } if options.contains(TableOptions::WITHOUT_ROWID) { @@ -332,6 +344,7 @@ pub struct Column { pub ty: Type, pub primary_key: bool, pub is_rowid_alias: bool, + pub default: Option, } #[derive(Debug, Clone, Copy, PartialEq)] @@ -370,30 +383,35 @@ pub fn sqlite_schema_table() -> BTreeTable { ty: Type::Text, primary_key: false, is_rowid_alias: false, + default: None, }, Column { name: "name".to_string(), ty: Type::Text, primary_key: false, is_rowid_alias: false, + default: None, }, Column { name: "tbl_name".to_string(), ty: Type::Text, primary_key: false, is_rowid_alias: false, + default: None, }, Column { name: "rootpage".to_string(), ty: Type::Integer, primary_key: false, is_rowid_alias: false, + default: None, }, Column { name: "sql".to_string(), ty: Type::Text, primary_key: false, is_rowid_alias: false, + default: None, }, ], } @@ -711,6 +729,16 @@ mod tests { Ok(()) } + #[test] + pub fn test_default_value() -> Result<()> { + let sql = r#"CREATE TABLE t1 (a INTEGER DEFAULT 23);"#; + let table = BTreeTable::from_sql(sql, 0)?; + let column = table.get_column("a").unwrap().1; + let default = column.default.clone().unwrap(); + assert_eq!(default.to_string(), "23"); + Ok(()) + } + #[test] pub fn test_sqlite_schema() { let expected = r#"CREATE TABLE sqlite_schema ( @@ -785,6 +813,7 @@ mod tests { ty: Type::Integer, primary_key: false, is_rowid_alias: false, + default: None, }], }; diff --git a/core/translate/group_by.rs b/core/translate/group_by.rs index 1e52a98b5..4c593643c 100644 --- a/core/translate/group_by.rs +++ b/core/translate/group_by.rs @@ -171,6 +171,7 @@ pub fn emit_group_by<'a>( primary_key: false, ty: crate::schema::Type::Null, is_rowid_alias: false, + default: None, }) .collect::>(); diff --git a/core/translate/order_by.rs b/core/translate/order_by.rs index db6ada538..04e29035b 100644 --- a/core/translate/order_by.rs +++ b/core/translate/order_by.rs @@ -73,6 +73,7 @@ pub fn emit_order_by( primary_key: false, ty: crate::schema::Type::Null, is_rowid_alias: false, + default: None, }); } for (i, rc) in result_columns.iter().enumerate() { @@ -87,6 +88,7 @@ pub fn emit_order_by( primary_key: false, ty: crate::schema::Type::Null, is_rowid_alias: false, + default: None, }); } diff --git a/core/translate/plan.rs b/core/translate/plan.rs index 3c1fe54f0..e151ea9e4 100644 --- a/core/translate/plan.rs +++ b/core/translate/plan.rs @@ -275,6 +275,7 @@ impl TableReference { ty: Type::Text, // FIXME: infer proper type is_rowid_alias: false, primary_key: false, + default: None, }) .collect(), ))), From 69d3fbc7974d5fed15b3e9d71ceb9dd9bc3f9e76 Mon Sep 17 00:00:00 2001 From: Glauber Costa Date: Thu, 30 Jan 2025 17:03:49 -0500 Subject: [PATCH 2/5] keep track of notnull constraint on column creation --- core/schema.rs | 31 +++++++++++++++++++++++++++++++ core/translate/group_by.rs | 1 + core/translate/order_by.rs | 2 ++ core/translate/plan.rs | 1 + 4 files changed, 35 insertions(+) diff --git a/core/schema.rs b/core/schema.rs index 32dc631c5..f83e27cf1 100644 --- a/core/schema.rs +++ b/core/schema.rs @@ -178,6 +178,7 @@ impl PseudoTable { ty, primary_key, is_rowid_alias: false, + notnull: false, default: None, }); } @@ -272,11 +273,15 @@ fn create_table( let mut default = None; let mut primary_key = false; + let mut notnull = false; for c_def in &col_def.constraints { match &c_def.constraint { sqlite3_parser::ast::ColumnConstraint::PrimaryKey { .. } => { primary_key = true; } + sqlite3_parser::ast::ColumnConstraint::NotNull { .. } => { + notnull = true; + } sqlite3_parser::ast::ColumnConstraint::Default(expr) => { default = Some(expr.clone()) } @@ -295,6 +300,7 @@ fn create_table( ty, primary_key, is_rowid_alias: typename_exactly_integer && primary_key, + notnull, default, }); } @@ -344,6 +350,7 @@ pub struct Column { pub ty: Type, pub primary_key: bool, pub is_rowid_alias: bool, + pub notnull: bool, pub default: Option, } @@ -383,6 +390,7 @@ pub fn sqlite_schema_table() -> BTreeTable { ty: Type::Text, primary_key: false, is_rowid_alias: false, + notnull: false, default: None, }, Column { @@ -390,6 +398,7 @@ pub fn sqlite_schema_table() -> BTreeTable { ty: Type::Text, primary_key: false, is_rowid_alias: false, + notnull: false, default: None, }, Column { @@ -397,6 +406,7 @@ pub fn sqlite_schema_table() -> BTreeTable { ty: Type::Text, primary_key: false, is_rowid_alias: false, + notnull: false, default: None, }, Column { @@ -404,6 +414,7 @@ pub fn sqlite_schema_table() -> BTreeTable { ty: Type::Integer, primary_key: false, is_rowid_alias: false, + notnull: false, default: None, }, Column { @@ -411,6 +422,7 @@ pub fn sqlite_schema_table() -> BTreeTable { ty: Type::Text, primary_key: false, is_rowid_alias: false, + notnull: false, default: None, }, ], @@ -739,6 +751,24 @@ mod tests { Ok(()) } + #[test] + pub fn test_col_notnull() -> Result<()> { + let sql = r#"CREATE TABLE t1 (a INTEGER NOT NULL);"#; + let table = BTreeTable::from_sql(sql, 0)?; + let column = table.get_column("a").unwrap().1; + assert_eq!(column.notnull, true); + Ok(()) + } + + #[test] + pub fn test_col_notnull_negative() -> Result<()> { + let sql = r#"CREATE TABLE t1 (a INTEGER);"#; + let table = BTreeTable::from_sql(sql, 0)?; + let column = table.get_column("a").unwrap().1; + assert_eq!(column.notnull, false); + Ok(()) + } + #[test] pub fn test_sqlite_schema() { let expected = r#"CREATE TABLE sqlite_schema ( @@ -813,6 +843,7 @@ mod tests { ty: Type::Integer, primary_key: false, is_rowid_alias: false, + notnull: false, default: None, }], }; diff --git a/core/translate/group_by.rs b/core/translate/group_by.rs index 4c593643c..5f7aa7aae 100644 --- a/core/translate/group_by.rs +++ b/core/translate/group_by.rs @@ -171,6 +171,7 @@ pub fn emit_group_by<'a>( primary_key: false, ty: crate::schema::Type::Null, is_rowid_alias: false, + notnull: false, default: None, }) .collect::>(); diff --git a/core/translate/order_by.rs b/core/translate/order_by.rs index 04e29035b..1435510d3 100644 --- a/core/translate/order_by.rs +++ b/core/translate/order_by.rs @@ -73,6 +73,7 @@ pub fn emit_order_by( primary_key: false, ty: crate::schema::Type::Null, is_rowid_alias: false, + notnull: false, default: None, }); } @@ -88,6 +89,7 @@ pub fn emit_order_by( primary_key: false, ty: crate::schema::Type::Null, is_rowid_alias: false, + notnull: false, default: None, }); } diff --git a/core/translate/plan.rs b/core/translate/plan.rs index e151ea9e4..76d0a334c 100644 --- a/core/translate/plan.rs +++ b/core/translate/plan.rs @@ -275,6 +275,7 @@ impl TableReference { ty: Type::Text, // FIXME: infer proper type is_rowid_alias: false, primary_key: false, + notnull: false, default: None, }) .collect(), From f1df43633a13aee13085b5390721fb90b0b93fb1 Mon Sep 17 00:00:00 2001 From: Glauber Costa Date: Thu, 30 Jan 2025 19:24:28 -0500 Subject: [PATCH 3/5] change type Display implementation to not show null This is the behavior that things like pragma table_info seem to expect. --- core/schema.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/schema.rs b/core/schema.rs index f83e27cf1..379a69acc 100644 --- a/core/schema.rs +++ b/core/schema.rs @@ -367,7 +367,7 @@ pub enum Type { impl fmt::Display for Type { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { - Self::Null => "NULL", + Self::Null => "", Self::Text => "TEXT", Self::Numeric => "NUMERIC", Self::Integer => "INTEGER", From 249a8cf8d2c2bbc71b653b7406ac28c58cb9019f Mon Sep 17 00:00:00 2001 From: Glauber Costa Date: Thu, 30 Jan 2025 19:51:37 -0500 Subject: [PATCH 4/5] keep type information as a string in column metadata SQLite holds on to it deeply, for example: sqlite> create table a(a int); sqlite> create table b(b integer); sqlite> create table c(c glauber); sqlite> pragma table_info=a; 0|a|INT|0||0 sqlite> pragma table_info=b; 0|b|INTEGER|0||0 sqlite> pragma table_info=c; 0|c|glauber|0||0 So we'll keep it as well so we can produce the same responses. --- core/schema.rs | 85 ++++++++++++++++++++++++++++++++++---- core/translate/group_by.rs | 4 +- core/translate/order_by.rs | 8 +++- core/translate/plan.rs | 1 + 4 files changed, 86 insertions(+), 12 deletions(-) diff --git a/core/schema.rs b/core/schema.rs index 379a69acc..17d18f74c 100644 --- a/core/schema.rs +++ b/core/schema.rs @@ -176,6 +176,7 @@ impl PseudoTable { self.columns.push(Column { name: normalize_ident(name), ty, + ty_str: ty.to_string(), primary_key, is_rowid_alias: false, notnull: false, @@ -245,30 +246,42 @@ fn create_table( // and the value of this column are the same. // https://www.sqlite.org/lang_createtable.html#rowids_and_the_integer_primary_key let mut typename_exactly_integer = false; - let ty = match col_def.col_type { + let (ty, ty_str) = match col_def.col_type { Some(data_type) => { + let s = data_type.name.as_str(); + let ty_str = if matches!( + s.to_uppercase().as_str(), + "TEXT" | "INT" | "INTEGER" | "BLOB" | "REAL" + ) { + s.to_uppercase().to_string() + } else { + s.to_string() + }; + // https://www.sqlite.org/datatype3.html - let type_name = data_type.name.as_str().to_uppercase(); + let type_name = ty_str.to_uppercase(); if type_name.contains("INT") { typename_exactly_integer = type_name == "INTEGER"; - Type::Integer + (Type::Integer, ty_str) } else if type_name.contains("CHAR") || type_name.contains("CLOB") || type_name.contains("TEXT") { - Type::Text - } else if type_name.contains("BLOB") || type_name.is_empty() { - Type::Blob + (Type::Text, ty_str) + } else if type_name.contains("BLOB") { + (Type::Blob, ty_str) + } else if type_name.is_empty() { + (Type::Blob, "".to_string()) } else if type_name.contains("REAL") || type_name.contains("FLOA") || type_name.contains("DOUB") { - Type::Real + (Type::Real, ty_str) } else { - Type::Numeric + (Type::Numeric, ty_str) } } - None => Type::Null, + None => (Type::Null, "".to_string()), }; let mut default = None; @@ -298,6 +311,7 @@ fn create_table( cols.push(Column { name: normalize_ident(&name), ty, + ty_str, primary_key, is_rowid_alias: typename_exactly_integer && primary_key, notnull, @@ -348,6 +362,8 @@ pub fn _build_pseudo_table(columns: &[ResultColumn]) -> PseudoTable { pub struct Column { pub name: String, pub ty: Type, + // many sqlite operations like table_info retain the original string + pub ty_str: String, pub primary_key: bool, pub is_rowid_alias: bool, pub notnull: bool, @@ -388,6 +404,7 @@ pub fn sqlite_schema_table() -> BTreeTable { Column { name: "type".to_string(), ty: Type::Text, + ty_str: "TEXT".to_string(), primary_key: false, is_rowid_alias: false, notnull: false, @@ -396,6 +413,7 @@ pub fn sqlite_schema_table() -> BTreeTable { Column { name: "name".to_string(), ty: Type::Text, + ty_str: "TEXT".to_string(), primary_key: false, is_rowid_alias: false, notnull: false, @@ -404,6 +422,7 @@ pub fn sqlite_schema_table() -> BTreeTable { Column { name: "tbl_name".to_string(), ty: Type::Text, + ty_str: "TEXT".to_string(), primary_key: false, is_rowid_alias: false, notnull: false, @@ -412,6 +431,7 @@ pub fn sqlite_schema_table() -> BTreeTable { Column { name: "rootpage".to_string(), ty: Type::Integer, + ty_str: "INT".to_string(), primary_key: false, is_rowid_alias: false, notnull: false, @@ -420,6 +440,7 @@ pub fn sqlite_schema_table() -> BTreeTable { Column { name: "sql".to_string(), ty: Type::Text, + ty_str: "TEXT".to_string(), primary_key: false, is_rowid_alias: false, notnull: false, @@ -769,6 +790,51 @@ mod tests { Ok(()) } + #[test] + pub fn test_col_type_string_integer() -> Result<()> { + let sql = r#"CREATE TABLE t1 (a InTeGeR);"#; + let table = BTreeTable::from_sql(sql, 0)?; + let column = table.get_column("a").unwrap().1; + assert_eq!(column.ty_str, "INTEGER"); + Ok(()) + } + + #[test] + pub fn test_col_type_string_int() -> Result<()> { + let sql = r#"CREATE TABLE t1 (a InT);"#; + let table = BTreeTable::from_sql(sql, 0)?; + let column = table.get_column("a").unwrap().1; + assert_eq!(column.ty_str, "INT"); + Ok(()) + } + + #[test] + pub fn test_col_type_string_blob() -> Result<()> { + let sql = r#"CREATE TABLE t1 (a bLoB);"#; + let table = BTreeTable::from_sql(sql, 0)?; + let column = table.get_column("a").unwrap().1; + assert_eq!(column.ty_str, "BLOB"); + Ok(()) + } + + #[test] + pub fn test_col_type_string_empty() -> Result<()> { + let sql = r#"CREATE TABLE t1 (a);"#; + let table = BTreeTable::from_sql(sql, 0)?; + let column = table.get_column("a").unwrap().1; + assert_eq!(column.ty_str, ""); + Ok(()) + } + + #[test] + pub fn test_col_type_string_some_nonsense() -> Result<()> { + let sql = r#"CREATE TABLE t1 (a someNonsenseName);"#; + let table = BTreeTable::from_sql(sql, 0)?; + let column = table.get_column("a").unwrap().1; + assert_eq!(column.ty_str, "someNonsenseName"); + Ok(()) + } + #[test] pub fn test_sqlite_schema() { let expected = r#"CREATE TABLE sqlite_schema ( @@ -841,6 +907,7 @@ mod tests { columns: vec![Column { name: "a".to_string(), ty: Type::Integer, + ty_str: "INT".to_string(), primary_key: false, is_rowid_alias: false, notnull: false, diff --git a/core/translate/group_by.rs b/core/translate/group_by.rs index 5f7aa7aae..284f9e478 100644 --- a/core/translate/group_by.rs +++ b/core/translate/group_by.rs @@ -165,11 +165,13 @@ pub fn emit_group_by<'a>( .map(|agg| agg.args.len()) .sum::(); // sorter column names do not matter + let ty = crate::schema::Type::Null; let pseudo_columns = (0..sorter_column_count) .map(|i| Column { name: i.to_string(), primary_key: false, - ty: crate::schema::Type::Null, + ty, + ty_str: ty.to_string(), is_rowid_alias: false, notnull: false, default: None, diff --git a/core/translate/order_by.rs b/core/translate/order_by.rs index 1435510d3..6e6341239 100644 --- a/core/translate/order_by.rs +++ b/core/translate/order_by.rs @@ -67,11 +67,13 @@ pub fn emit_order_by( let sort_loop_end_label = program.allocate_label(); let mut pseudo_columns = vec![]; for (i, _) in order_by.iter().enumerate() { + let ty = crate::schema::Type::Null; pseudo_columns.push(Column { // Names don't matter. We are tracking which result column is in which position in the ORDER BY clause in m.result_column_indexes_in_orderby_sorter. name: format!("sort_key_{}", i), primary_key: false, - ty: crate::schema::Type::Null, + ty, + ty_str: ty.to_string(), is_rowid_alias: false, notnull: false, default: None, @@ -84,10 +86,12 @@ pub fn emit_order_by( continue; } } + let ty = crate::schema::Type::Null; pseudo_columns.push(Column { name: rc.expr.to_string(), primary_key: false, - ty: crate::schema::Type::Null, + ty, + ty_str: ty.to_string(), is_rowid_alias: false, notnull: false, default: None, diff --git a/core/translate/plan.rs b/core/translate/plan.rs index 76d0a334c..075e6f93f 100644 --- a/core/translate/plan.rs +++ b/core/translate/plan.rs @@ -273,6 +273,7 @@ impl TableReference { .map(|rc| Column { name: rc.name.clone(), ty: Type::Text, // FIXME: infer proper type + ty_str: "TEXT".to_string(), is_rowid_alias: false, primary_key: false, notnull: false, From 016b815b59bd75f2930a117c0e4e13b70e87b07a Mon Sep 17 00:00:00 2001 From: Glauber Costa Date: Thu, 30 Jan 2025 15:26:02 -0500 Subject: [PATCH 5/5] 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(()), } }