mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-23 17:05:36 +01:00
Merge 'Table info' from Glauber Costa
This implements the table_info pragma, allowing us to fetch information about columns present in a table. Closes #837
This commit is contained in:
@@ -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 | |
|
||||
|
||||
159
core/schema.rs
159
core/schema.rs
@@ -176,8 +176,11 @@ impl PseudoTable {
|
||||
self.columns.push(Column {
|
||||
name: normalize_ident(name),
|
||||
ty,
|
||||
ty_str: ty.to_string(),
|
||||
primary_key,
|
||||
is_rowid_alias: false,
|
||||
notnull: false,
|
||||
default: None,
|
||||
});
|
||||
}
|
||||
pub fn get_column(&self, name: &str) -> Option<(usize, &Column)> {
|
||||
@@ -243,47 +246,76 @@ 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 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;
|
||||
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())
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
ty_str,
|
||||
primary_key,
|
||||
is_rowid_alias: typename_exactly_integer && primary_key,
|
||||
notnull,
|
||||
default,
|
||||
});
|
||||
}
|
||||
if options.contains(TableOptions::WITHOUT_ROWID) {
|
||||
@@ -330,8 +362,12 @@ 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,
|
||||
pub default: Option<Expr>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
@@ -347,7 +383,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",
|
||||
@@ -368,32 +404,47 @@ 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,
|
||||
default: None,
|
||||
},
|
||||
Column {
|
||||
name: "name".to_string(),
|
||||
ty: Type::Text,
|
||||
ty_str: "TEXT".to_string(),
|
||||
primary_key: false,
|
||||
is_rowid_alias: false,
|
||||
notnull: false,
|
||||
default: None,
|
||||
},
|
||||
Column {
|
||||
name: "tbl_name".to_string(),
|
||||
ty: Type::Text,
|
||||
ty_str: "TEXT".to_string(),
|
||||
primary_key: false,
|
||||
is_rowid_alias: false,
|
||||
notnull: false,
|
||||
default: None,
|
||||
},
|
||||
Column {
|
||||
name: "rootpage".to_string(),
|
||||
ty: Type::Integer,
|
||||
ty_str: "INT".to_string(),
|
||||
primary_key: false,
|
||||
is_rowid_alias: false,
|
||||
notnull: false,
|
||||
default: None,
|
||||
},
|
||||
Column {
|
||||
name: "sql".to_string(),
|
||||
ty: Type::Text,
|
||||
ty_str: "TEXT".to_string(),
|
||||
primary_key: false,
|
||||
is_rowid_alias: false,
|
||||
notnull: false,
|
||||
default: None,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -711,6 +762,79 @@ 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_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_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 (
|
||||
@@ -783,8 +907,11 @@ 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,
|
||||
default: None,
|
||||
}],
|
||||
};
|
||||
|
||||
|
||||
@@ -165,12 +165,16 @@ pub fn emit_group_by<'a>(
|
||||
.map(|agg| agg.args.len())
|
||||
.sum::<usize>();
|
||||
// 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,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
||||
@@ -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<ast::PragmaBody>,
|
||||
database_header: Rc<RefCell<DatabaseHeader>>,
|
||||
@@ -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<RefCell<DatabaseHeader>>,
|
||||
pager: Rc<Pager>,
|
||||
@@ -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<ast::Expr>,
|
||||
database_header: Rc<RefCell<DatabaseHeader>>,
|
||||
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(())
|
||||
|
||||
@@ -67,12 +67,16 @@ 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,
|
||||
});
|
||||
}
|
||||
for (i, rc) in result_columns.iter().enumerate() {
|
||||
@@ -82,11 +86,15 @@ 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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -273,8 +273,11 @@ 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,
|
||||
default: None,
|
||||
})
|
||||
.collect(),
|
||||
))),
|
||||
|
||||
@@ -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
|
||||
} {}
|
||||
|
||||
@@ -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(()),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user