diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 7ff0a348a..6c9ae53bb 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -2,9 +2,9 @@ name: Coverage on: push: - branches: [main] + branches: [main, release-*] pull_request: - branches: [main] + branches: [main, release-*] env: CARGO_TERM_COLOR: always diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ff63f9683..0d3d66e9b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,9 +2,9 @@ name: Rust on: push: - branches: [main] + branches: [main, release-*] pull_request: - branches: [main] + branches: [main, release-*] env: CARGO_TERM_COLOR: always diff --git a/.gitignore b/.gitignore index c57a3cdc9..a35c33cd5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # GlueSQL project files /data/ /pkg/rust/data/ +/cli/tmp/ /storages/**/data/ /storages/**/tmp/ /reports/ diff --git a/README.md b/README.md index 5a25d5c61..9d05ce926 100644 --- a/README.md +++ b/README.md @@ -32,12 +32,41 @@ GlueSQL provides three reference storage options. gluesql = "0.13" ``` -- CLI application +- Install CLI -``` +```sh $ cargo install gluesql ``` +- Run CLI + +```sh +$ gluesql [--path ~/data_path] [--execute ~/sql_path] +``` + +### Migration using CLI + +#### Dump whole schemas and data by generating SQL using `--dump {PATH}` option + +```sh +$ gluesql --path ~/glue_data --dump ./dump.sql +``` + +```sql +-- dump.sql +CREATE TABLE User (id INT, name TEXT); +CREATE INDEX User_id ON User (id); +.. +INSERT INTO User VALUES (1, 'Foo'), (2, 'Bar') .. +.. +``` + +#### Import database + +```sh +$ gluesql --path ~/new_data --execute ./dump.sql +``` + ### Usage ```rust diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 62986431e..be94a1dce 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gluesql-cli" -version = "0.13.0" +version = "0.13.1" edition = "2021" authors = ["Taehoon Moon "] description = "GlueSQL - Open source SQL database engine fully written in Rust with pure functional execution layer, easily swappable storage and web assembly support!" @@ -9,7 +9,7 @@ repository = "https://github.com/gluesql/gluesql" documentation = "https://docs.rs/gluesql/" [dependencies] -gluesql-core = { path = "../core", version = "0.13.0", features = ["alter-table"] } +gluesql-core = { path = "../core", version = "0.13.1", features = ["alter-table"] } gluesql_sled_storage = { path = "../storages/sled-storage", version = "0.13.0" } gluesql_memory_storage = { path = "../storages/memory-storage", version = "0.13.0" } @@ -19,3 +19,10 @@ rustyline-derive = "0.6" tabled ="0.8" thiserror = "1.0" edit = "0.1.4" +futures = "0.3" +anyhow = "1.0" +itertools = "0.10" + +[dev-dependencies] +tokio = { version = "1", features = ["rt", "macros"] } + diff --git a/cli/src/lib.rs b/cli/src/lib.rs index ce1d8dac0..71445a725 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -7,11 +7,19 @@ mod print; use { crate::cli::Cli, + anyhow::{Error, Result}, clap::Parser, - gluesql_core::store::{GStore, GStoreMut}, + futures::executor::block_on, + gluesql_core::{ + ast::{Expr, SetExpr, Statement, ToSql, Values}, + prelude::Row, + store::Transaction, + store::{GStore, GStoreMut, Store}, + }, gluesql_memory_storage::MemoryStorage, gluesql_sled_storage::SledStorage, - std::{fmt::Debug, path::PathBuf}, + itertools::Itertools, + std::{fmt::Debug, fs::File, io::Write, path::PathBuf}, }; #[derive(Parser, Debug)] @@ -24,14 +32,25 @@ struct Args { /// SQL file to execute #[clap(short, long, value_parser)] execute: Option, + + /// PATH to dump whole database + #[clap(short, long, value_parser)] + dump: Option, } -pub fn run() { +pub fn run() -> Result<()> { let args = Args::parse(); if let Some(path) = args.path { let path = path.as_path().to_str().expect("wrong path"); + if let Some(dump_path) = args.dump { + let storage = SledStorage::new(path).expect("failed to load sled-storage"); + dump_database(storage, dump_path)?; + + return Ok::<_, Error>(()); + } + println!("[sled-storage] connected to {}", path); run( SledStorage::new(path).expect("failed to load sled-storage"), @@ -56,4 +75,55 @@ pub fn run() { eprintln!("{}", e); } } + + Ok(()) +} + +pub fn dump_database(storage: SledStorage, dump_path: PathBuf) -> Result { + let file = File::create(dump_path)?; + + block_on(async { + let (storage, _) = storage.begin(true).await.map_err(|(_, error)| error)?; + let schemas = storage.fetch_all_schemas().await?; + for schema in schemas { + writeln!(&file, "{}", schema.clone().to_ddl())?; + + let rows_list = storage + .scan_data(&schema.table_name) + .await? + .map_ok(|(_, row)| row) + .chunks(100); + + for rows in &rows_list { + let exprs_list = rows + .map(|result| { + result.map(|Row(values)| { + values + .into_iter() + .map(|value| Ok(Expr::try_from(value)?)) + .collect::>>() + })? + }) + .collect::, _>>()?; + + let insert_statement = Statement::Insert { + table_name: schema.table_name.clone(), + columns: Vec::new(), + source: gluesql_core::ast::Query { + body: SetExpr::Values(Values(exprs_list)), + order_by: Vec::new(), + limit: None, + offset: None, + }, + } + .to_sql(); + + writeln!(&file, "{}", insert_statement)?; + } + + writeln!(&file)?; + } + + Ok(storage) + }) } diff --git a/cli/src/main.rs b/cli/src/main.rs index 752ec4a8a..bd332ed1a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,3 +1,3 @@ fn main() { - gluesql_cli::run(); + gluesql_cli::run().unwrap(); } diff --git a/cli/tests/dump.rs b/cli/tests/dump.rs new file mode 100644 index 000000000..c4732e6eb --- /dev/null +++ b/cli/tests/dump.rs @@ -0,0 +1,93 @@ +use { + gluesql_cli::dump_database, + gluesql_core::prelude::Glue, + gluesql_sled_storage::{sled, SledStorage}, + std::{fs::File, io::Read, path::PathBuf}, +}; + +#[tokio::test] +async fn dump_and_import() { + let data_path = "tmp/src"; + let dump_path = PathBuf::from("tmp/dump.sql"); + + let config = sled::Config::default().path(data_path).temporary(true); + let source_storage = SledStorage::try_from(config).unwrap(); + let mut source_glue = Glue::new(source_storage); + + let sqls = vec![ + "CREATE TABLE Foo ( + boolean BOOLEAN, + int8 INT8, + int16 INT16, + int32 INT32, + int INT, + int128 INT128, + uinti8 UINT8, + text TEXT, + bytea BYTEA, + date DATE, + timestamp TIMESTAMP, + time TIME, + interval INTERVAL, + uuid UUID, + map MAP, + list LIST, + );", + r#"INSERT INTO Foo + VALUES ( + true, + 1, + 2, + 3, + 4, + 5, + 6, + 'a', + X'123456', + DATE '2022-11-01', + TIMESTAMP '2022-11-02', + TIME '23:59:59', + INTERVAL '1' DAY, + '550e8400-e29b-41d4-a716-446655440000', + '{"a": {"red": "apple", "blue": 1}, "b": 10}', + '[{ "foo": 100, "bar": [true, 0, [10.5, false] ] }, 10, 20]' + );"#, + "CREATE INDEX Foo_int ON Foo (int);", + "CREATE TABLE Bar AS SELECT N FROM SERIES(101);", + ]; + + for sql in sqls { + source_glue.execute(sql).unwrap(); + } + + let source_storage = dump_database(source_glue.storage.unwrap(), dump_path.clone()).unwrap(); + + let data_path = "tmp/target"; + let config = sled::Config::default().path(data_path).temporary(true); + let target_storage = SledStorage::try_from(config).unwrap(); + let mut target_glue = Glue::new(target_storage); + + let mut sqls = String::new(); + File::open(dump_path) + .unwrap() + .read_to_string(&mut sqls) + .unwrap(); + + for sql in sqls.split(';').filter(|sql| !sql.trim().is_empty()) { + target_glue.execute(sql).unwrap(); + } + + let mut source_glue = Glue::new(source_storage); + + // schemas should be identical + let sql = "SELECT OBJECT_TYPE, OBJECT_NAME FROM GLUE_OBJECTS"; + let source_data = source_glue.execute(sql).unwrap(); + let target_data = target_glue.execute(sql).unwrap(); + assert_eq!(source_data, target_data); + + // data should be identical + let sql = "SELECT * FROM Foo JOIN Bar;"; + let source_data = source_glue.execute(sql).unwrap(); + let target_data = target_glue.execute(sql).unwrap(); + assert_eq!(source_data, target_data); +} diff --git a/core/Cargo.toml b/core/Cargo.toml index 5e1a978e5..1a83dfac6 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gluesql-core" -version = "0.13.0" +version = "0.13.1" edition = "2021" authors = ["Taehoon Moon "] description = "GlueSQL - Open source SQL database engine fully written in Rust with pure functional execution layer, easily swappable storage and web assembly support!" diff --git a/core/src/ast/ast_literal.rs b/core/src/ast/ast_literal.rs index ba5d43efb..87f0e1240 100644 --- a/core/src/ast/ast_literal.rs +++ b/core/src/ast/ast_literal.rs @@ -19,8 +19,8 @@ impl ToSql for AstLiteral { match self { AstLiteral::Boolean(b) => b.to_string().to_uppercase(), AstLiteral::Number(n) => n.to_string(), - AstLiteral::QuotedString(qs) => format!(r#""{qs}""#), - AstLiteral::HexString(hs) => format!(r#""{hs}""#), + AstLiteral::QuotedString(qs) => format!("'{qs}'"), + AstLiteral::HexString(hs) => format!("'{hs}'"), AstLiteral::Null => "NULL".to_owned(), } } @@ -57,7 +57,7 @@ mod tests { assert_eq!("TRUE", AstLiteral::Boolean(true).to_sql()); assert_eq!("123", AstLiteral::Number(BigDecimal::from(123)).to_sql()); assert_eq!( - r#""hello""#, + "'hello'", AstLiteral::QuotedString("hello".to_owned()).to_sql() ); assert_eq!("NULL", AstLiteral::Null.to_sql()); diff --git a/core/src/ast/ddl.rs b/core/src/ast/ddl.rs index 92e068533..594f50a62 100644 --- a/core/src/ast/ddl.rs +++ b/core/src/ast/ddl.rs @@ -78,6 +78,7 @@ impl ToSql for ColumnDef { .map(|option| option.to_sql()) .collect::>() .join(" "); + format!("{name} {data_type} {options}") .trim_end() .to_owned() diff --git a/core/src/ast/expr.rs b/core/src/ast/expr.rs index 9105eab61..34550502a 100644 --- a/core/src/ast/expr.rs +++ b/core/src/ast/expr.rs @@ -153,7 +153,7 @@ impl ToSql for Expr { }, Expr::Nested(expr) => format!("({})", expr.to_sql()), Expr::Literal(s) => s.to_sql(), - Expr::TypedString { data_type, value } => format!("{data_type} \"{value}\""), + Expr::TypedString { data_type, value } => format!("{data_type} '{value}'"), Expr::Case { operand, when_then, @@ -274,7 +274,7 @@ mod tests { assert_eq!("id IS NOT NULL", Expr::IsNotNull(id_expr).to_sql()); assert_eq!( - r#"INT "1""#, + "INT '1'", Expr::TypedString { data_type: DataType::Int, value: "1".to_owned() @@ -310,7 +310,7 @@ mod tests { ); assert_eq!( - r#"id LIKE "%abc""#, + "id LIKE '%abc'", Expr::Like { expr: Box::new(Expr::Identifier("id".to_owned())), negated: false, @@ -319,7 +319,7 @@ mod tests { .to_sql() ); assert_eq!( - r#"id NOT LIKE "%abc""#, + "id NOT LIKE '%abc'", Expr::Like { expr: Box::new(Expr::Identifier("id".to_owned())), negated: true, @@ -329,7 +329,7 @@ mod tests { ); assert_eq!( - r#"id ILIKE "%abc_""#, + "id ILIKE '%abc_'", Expr::ILike { expr: Box::new(Expr::Identifier("id".to_owned())), negated: false, @@ -338,7 +338,7 @@ mod tests { .to_sql() ); assert_eq!( - r#"id NOT ILIKE "%abc_""#, + "id NOT ILIKE '%abc_'", Expr::ILike { expr: Box::new(Expr::Identifier("id".to_owned())), negated: true, @@ -348,7 +348,7 @@ mod tests { ); assert_eq!( - r#"id IN ("a", "b", "c")"#, + "id IN ('a', 'b', 'c')", Expr::InList { expr: Box::new(Expr::Identifier("id".to_owned())), list: vec![ @@ -362,7 +362,7 @@ mod tests { ); assert_eq!( - r#"id NOT IN ("a", "b", "c")"#, + "id NOT IN ('a', 'b', 'c')", Expr::InList { expr: Box::new(Expr::Identifier("id".to_owned())), list: vec![ @@ -511,13 +511,11 @@ mod tests { assert_eq!( trim( - r#" - CASE id - WHEN 1 THEN "a" - WHEN 2 THEN "b" - ELSE "c" - END - "#, + "CASE id + WHEN 1 THEN 'a' + WHEN 2 THEN 'b' + ELSE 'c' + END", ), Expr::Case { operand: Some(Box::new(Expr::Identifier("id".to_owned()))), @@ -564,7 +562,7 @@ mod tests { .to_sql() ); assert_eq!( - r#"INTERVAL "3-5" HOUR TO MINUTE"#, + "INTERVAL '3-5' HOUR TO MINUTE", &Expr::Interval { expr: Box::new(Expr::Literal(AstLiteral::QuotedString("3-5".to_owned()))), leading_field: Some(DateTimeField::Hour), diff --git a/core/src/ast/function.rs b/core/src/ast/function.rs index 185eff449..63d3a073c 100644 --- a/core/src/ast/function.rs +++ b/core/src/ast/function.rs @@ -286,7 +286,7 @@ impl ToSql for Function { sub_expr, } => format!("POSITION({} IN {})", sub_expr.to_sql(), from_expr.to_sql()), Function::Extract { field, expr } => { - format!(r#"EXTRACT({field} FROM "{}")"#, expr.to_sql()) + format!("EXTRACT({field} FROM '{}')", expr.to_sql()) } Function::Ascii(e) => format!("ASCII({})", e.to_sql()), Function::Chr(e) => format!("CHR({})", e.to_sql()), @@ -353,7 +353,7 @@ mod tests { ); assert_eq!( - r#"LOWER("Bye")"#, + "LOWER('Bye')", &Expr::Function(Box::new(Function::Lower(Expr::Literal( AstLiteral::QuotedString("Bye".to_owned()) )))) @@ -361,7 +361,7 @@ mod tests { ); assert_eq!( - r#"UPPER("Hi")"#, + "UPPER('Hi')", &Expr::Function(Box::new(Function::Upper(Expr::Literal( AstLiteral::QuotedString("Hi".to_owned()) )))) @@ -369,7 +369,7 @@ mod tests { ); assert_eq!( - r#"LEFT("GlueSQL", 2)"#, + "LEFT('GlueSQL', 2)", &Expr::Function(Box::new(Function::Left { expr: Expr::Literal(AstLiteral::QuotedString("GlueSQL".to_owned())), size: Expr::Literal(AstLiteral::Number(BigDecimal::from_str("2").unwrap())) @@ -378,7 +378,7 @@ mod tests { ); assert_eq!( - r#"RIGHT("GlueSQL", 3)"#, + "RIGHT('GlueSQL', 3)", &Expr::Function(Box::new(Function::Right { expr: Expr::Literal(AstLiteral::QuotedString("GlueSQL".to_owned())), size: Expr::Literal(AstLiteral::Number(BigDecimal::from_str("3").unwrap())) @@ -411,7 +411,7 @@ mod tests { ); assert_eq!( - r#"LPAD("GlueSQL", 2)"#, + "LPAD('GlueSQL', 2)", &Expr::Function(Box::new(Function::Lpad { expr: Expr::Literal(AstLiteral::QuotedString("GlueSQL".to_owned())), size: Expr::Literal(AstLiteral::Number(BigDecimal::from_str("2").unwrap())), @@ -421,7 +421,7 @@ mod tests { ); assert_eq!( - r#"LPAD("GlueSQL", 10, "Go")"#, + "LPAD('GlueSQL', 10, 'Go')", &Expr::Function(Box::new(Function::Lpad { expr: Expr::Literal(AstLiteral::QuotedString("GlueSQL".to_owned())), size: Expr::Literal(AstLiteral::Number(BigDecimal::from_str("10").unwrap())), @@ -431,7 +431,7 @@ mod tests { ); assert_eq!( - r#"RPAD("GlueSQL", 10)"#, + "RPAD('GlueSQL', 10)", &Expr::Function(Box::new(Function::Rpad { expr: Expr::Literal(AstLiteral::QuotedString("GlueSQL".to_owned())), size: Expr::Literal(AstLiteral::Number(BigDecimal::from_str("10").unwrap())), @@ -441,7 +441,7 @@ mod tests { ); assert_eq!( - r#"RPAD("GlueSQL", 10, "Go")"#, + "RPAD('GlueSQL', 10, 'Go')", &Expr::Function(Box::new(Function::Rpad { expr: Expr::Literal(AstLiteral::QuotedString("GlueSQL".to_owned())), size: Expr::Literal(AstLiteral::Number(BigDecimal::from_str("10").unwrap())), @@ -523,7 +523,7 @@ mod tests { ); assert_eq!( - r#"TRIM("*" FROM name)"#, + "TRIM('*' FROM name)", &Expr::Function(Box::new(Function::Trim { expr: Expr::Identifier("name".to_owned()), filter_chars: Some(Expr::Literal(AstLiteral::QuotedString("*".to_owned()))), @@ -533,7 +533,7 @@ mod tests { ); assert_eq!( - r#"TRIM(BOTH "*" FROM name)"#, + "TRIM(BOTH '*' FROM name)", &Expr::Function(Box::new(Function::Trim { expr: Expr::Identifier("name".to_owned()), filter_chars: Some(Expr::Literal(AstLiteral::QuotedString("*".to_owned()))), @@ -543,7 +543,7 @@ mod tests { ); assert_eq!( - r#"TRIM(LEADING "*" FROM name)"#, + "TRIM(LEADING '*' FROM name)", &Expr::Function(Box::new(Function::Trim { expr: Expr::Identifier("name".to_owned()), filter_chars: Some(Expr::Literal(AstLiteral::QuotedString("*".to_owned()))), @@ -698,7 +698,7 @@ mod tests { assert_eq!("PI()", &Expr::Function(Box::new(Function::Pi())).to_sql()); assert_eq!( - r#"LTRIM(" HI ")"#, + "LTRIM(' HI ')", &Expr::Function(Box::new(Function::Ltrim { expr: Expr::Literal(AstLiteral::QuotedString(" HI ".to_owned())), chars: None @@ -707,7 +707,7 @@ mod tests { ); assert_eq!( - r#"LTRIM("*IMPORTANT", "*")"#, + "LTRIM('*IMPORTANT', '*')", &Expr::Function(Box::new(Function::Ltrim { expr: Expr::Literal(AstLiteral::QuotedString("*IMPORTANT".to_owned())), chars: Some(Expr::Literal(AstLiteral::QuotedString("*".to_owned()))), @@ -716,7 +716,7 @@ mod tests { ); assert_eq!( - r#"RTRIM(" HI ")"#, + "RTRIM(' HI ')", &Expr::Function(Box::new(Function::Rtrim { expr: Expr::Literal(AstLiteral::QuotedString(" HI ".to_owned())), chars: None @@ -725,7 +725,7 @@ mod tests { ); assert_eq!( - r#"RTRIM("IMPORTANT*", "*")"#, + "RTRIM('IMPORTANT*', '*')", &Expr::Function(Box::new(Function::Rtrim { expr: Expr::Literal(AstLiteral::QuotedString("IMPORTANT*".to_owned())), chars: Some(Expr::Literal(AstLiteral::QuotedString("*".to_owned()))), @@ -742,7 +742,7 @@ mod tests { ); assert_eq!( - r#"REPEAT("Ha", 8)"#, + "REPEAT('Ha', 8)", &Expr::Function(Box::new(Function::Repeat { expr: Expr::Literal(AstLiteral::QuotedString("Ha".to_owned())), num: Expr::Literal(AstLiteral::Number(BigDecimal::from_str("8").unwrap())) @@ -759,7 +759,7 @@ mod tests { ); assert_eq!( - r#"SUBSTR("GlueSQL", 2)"#, + "SUBSTR('GlueSQL', 2)", &Expr::Function(Box::new(Function::Substr { expr: Expr::Literal(AstLiteral::QuotedString("GlueSQL".to_owned())), start: Expr::Literal(AstLiteral::Number(BigDecimal::from_str("2").unwrap())), @@ -769,7 +769,7 @@ mod tests { ); assert_eq!( - r#"SUBSTR("GlueSQL", 1, 3)"#, + "SUBSTR('GlueSQL', 1, 3)", &Expr::Function(Box::new(Function::Substr { expr: Expr::Literal(AstLiteral::QuotedString("GlueSQL".to_owned())), start: Expr::Literal(AstLiteral::Number(BigDecimal::from_str("1").unwrap())), @@ -781,7 +781,7 @@ mod tests { ); assert_eq!( - r#"UNWRAP(nested, "a.foo")"#, + "UNWRAP(nested, 'a.foo')", &Expr::Function(Box::new(Function::Unwrap { expr: Expr::Identifier("nested".to_owned()), selector: Expr::Literal(AstLiteral::QuotedString("a.foo".to_owned())) @@ -795,7 +795,7 @@ mod tests { ); assert_eq!( - r#"FORMAT(DATE "2022-10-12", "%Y-%m")"#, + "FORMAT(DATE '2022-10-12', '%Y-%m')", &Expr::Function(Box::new(Function::Format { expr: Expr::TypedString { data_type: DataType::Date, @@ -807,7 +807,7 @@ mod tests { ); assert_eq!( - r#"TO_DATE("2022-10-12", "%Y-%m-%d")"#, + "TO_DATE('2022-10-12', '%Y-%m-%d')", &Expr::Function(Box::new(Function::ToDate { expr: Expr::Literal(AstLiteral::QuotedString("2022-10-12".to_owned())), format: Expr::Literal(AstLiteral::QuotedString("%Y-%m-%d".to_owned())) @@ -816,7 +816,7 @@ mod tests { ); assert_eq!( - r#"TO_TIMESTAMP("2022-10-12 00:34:23", "%Y-%m-%d %H:%M:%S")"#, + "TO_TIMESTAMP('2022-10-12 00:34:23', '%Y-%m-%d %H:%M:%S')", &Expr::Function(Box::new(Function::ToTimestamp { expr: Expr::Literal(AstLiteral::QuotedString("2022-10-12 00:34:23".to_owned())), format: Expr::Literal(AstLiteral::QuotedString("%Y-%m-%d %H:%M:%S".to_owned())) @@ -825,7 +825,7 @@ mod tests { ); assert_eq!( - r#"TO_TIME("00:34:23", "%H:%M:%S")"#, + "TO_TIME('00:34:23', '%H:%M:%S')", &Expr::Function(Box::new(Function::ToTime { expr: Expr::Literal(AstLiteral::QuotedString("00:34:23".to_owned())), format: Expr::Literal(AstLiteral::QuotedString("%H:%M:%S".to_owned())) @@ -834,7 +834,7 @@ mod tests { ); assert_eq!( - r#"POSITION("cup" IN "cupcake")"#, + "POSITION('cup' IN 'cupcake')", &Expr::Function(Box::new(Function::Position { from_expr: Expr::Literal(AstLiteral::QuotedString("cupcake".to_owned())), sub_expr: Expr::Literal(AstLiteral::QuotedString("cup".to_owned())), @@ -843,7 +843,7 @@ mod tests { ); assert_eq!( - r#"ASCII("H")"#, + "ASCII('H')", &Expr::Function(Box::new(Function::Ascii(Expr::Literal( AstLiteral::QuotedString("H".to_owned()) )))) @@ -859,7 +859,7 @@ mod tests { ); assert_eq!( - r#"EXTRACT(MINUTE FROM "2022-05-05 01:02:03")"#, + "EXTRACT(MINUTE FROM '2022-05-05 01:02:03')", &Expr::Function(Box::new(Function::Extract { field: DateTimeField::Minute, expr: Expr::Identifier("2022-05-05 01:02:03".to_owned()) diff --git a/core/src/ast/mod.rs b/core/src/ast/mod.rs index 3ae13fc71..7dc7dd46b 100644 --- a/core/src/ast/mod.rs +++ b/core/src/ast/mod.rs @@ -119,15 +119,19 @@ impl ToSql for Statement { fn to_sql(&self) -> String { match self { Statement::ShowColumns { table_name } => { - format!("SHOW COLUMNS FROM {table_name}") + format!("SHOW COLUMNS FROM {table_name};") } Statement::Insert { table_name, columns, source, } => { - let columns = columns.join(", "); - format!("INSERT INTO {table_name} ({columns}) {}", source.to_sql()) + let columns = match columns.is_empty() { + true => "".to_owned(), + false => format!("({}) ", columns.join(", ")), + }; + + format!("INSERT INTO {table_name} {columns}{};", source.to_sql()) } Statement::Update { table_name, @@ -142,19 +146,19 @@ impl ToSql for Statement { match selection { Some(expr) => { format!( - "UPDATE {table_name} SET {assignments} WHERE {}", + "UPDATE {table_name} SET {assignments} WHERE {};", expr.to_sql() ) } - None => format!("UPDATE {table_name} SET {assignments}"), + None => format!("UPDATE {table_name} SET {assignments};"), } } Statement::Delete { table_name, selection, } => match selection { - Some(expr) => format!("DELETE FROM {table_name} WHERE {}", expr.to_sql()), - None => format!("DELETE FROM {table_name}"), + Some(expr) => format!("DELETE FROM {table_name} WHERE {};", expr.to_sql()), + None => format!("DELETE FROM {table_name};"), }, Statement::CreateTable { if_not_exists, @@ -163,8 +167,8 @@ impl ToSql for Statement { source, } => match source { Some(query) => match if_not_exists { - true => format!("CREATE TABLE IF NOT EXISTS {name} AS {}", query.to_sql()), - false => format!("CREATE TABLE {name} AS {}", query.to_sql()), + true => format!("CREATE TABLE IF NOT EXISTS {name} AS {};", query.to_sql()), + false => format!("CREATE TABLE {name} AS {};", query.to_sql()), }, None => { let columns = columns @@ -173,20 +177,20 @@ impl ToSql for Statement { .collect::>() .join(", "); match if_not_exists { - true => format!("CREATE TABLE IF NOT EXISTS {name} ({columns})"), - false => format!("CREATE TABLE {name} ({columns})"), + true => format!("CREATE TABLE IF NOT EXISTS {name} ({columns});"), + false => format!("CREATE TABLE {name} ({columns});"), } } }, #[cfg(feature = "alter-table")] Statement::AlterTable { name, operation } => { - format!("ALTER TABLE {name} {}", operation.to_sql()) + format!("ALTER TABLE {name} {};", operation.to_sql()) } Statement::DropTable { if_exists, names } => { let names = names.join(", "); match if_exists { - true => format!("DROP TABLE IF EXISTS {}", names), - false => format!("DROP TABLE {}", names), + true => format!("DROP TABLE IF EXISTS {};", names), + false => format!("DROP TABLE {};", names), } } #[cfg(feature = "index")] @@ -195,25 +199,25 @@ impl ToSql for Statement { table_name, column, } => { - format!("CREATE INDEX {name} ON {table_name} {}", column.to_sql()) + format!("CREATE INDEX {name} ON {table_name} {};", column.to_sql()) } #[cfg(feature = "index")] Statement::DropIndex { name, table_name } => { - format!("DROP INDEX {table_name}.{name}") + format!("DROP INDEX {table_name}.{name};") } #[cfg(feature = "transaction")] - Statement::StartTransaction => "START TRANSACTION".to_owned(), + Statement::StartTransaction => "START TRANSACTION;".to_owned(), #[cfg(feature = "transaction")] - Statement::Commit => "COMMIT".to_owned(), + Statement::Commit => "COMMIT;".to_owned(), #[cfg(feature = "transaction")] - Statement::Rollback => "ROLLBACK".to_owned(), + Statement::Rollback => "ROLLBACK;".to_owned(), Statement::ShowVariable(variable) => match variable { - Variable::Tables => "SHOW TABLES".to_owned(), - Variable::Version => "SHOW VERSIONS".to_owned(), + Variable::Tables => "SHOW TABLES;".to_owned(), + Variable::Version => "SHOW VERSIONS;".to_owned(), }, #[cfg(feature = "index")] Statement::ShowIndexes(object_name) => { - format!("SHOW INDEXES FROM {object_name}") + format!("SHOW INDEXES FROM {object_name};") } _ => "(..statement..)".to_owned(), } @@ -247,7 +251,7 @@ mod tests { #[test] fn to_sql_show_columns() { assert_eq!( - "SHOW COLUMNS FROM Bar", + "SHOW COLUMNS FROM Bar;", Statement::ShowColumns { table_name: "Bar".into() } @@ -258,7 +262,7 @@ mod tests { #[test] fn to_sql_insert() { assert_eq!( - r#"INSERT INTO Test (id, num, name) VALUES (1, 2, "Hello")"#, + "INSERT INTO Test (id, num, name) VALUES (1, 2, 'Hello');", Statement::Insert { table_name: "Test".into(), columns: vec!["id".to_owned(), "num".to_owned(), "name".to_owned()], @@ -280,7 +284,7 @@ mod tests { #[test] fn to_sql_update() { assert_eq!( - r#"UPDATE Foo SET id = 4, color = "blue""#, + "UPDATE Foo SET id = 4, color = 'blue';", Statement::Update { table_name: "Foo".into(), assignments: vec![ @@ -301,7 +305,7 @@ mod tests { ); assert_eq!( - r#"UPDATE Foo SET name = "first" WHERE a > b"#, + "UPDATE Foo SET name = 'first' WHERE a > b;", Statement::Update { table_name: "Foo".into(), assignments: vec![Assignment { @@ -321,7 +325,7 @@ mod tests { #[test] fn to_sql_delete() { assert_eq!( - "DELETE FROM Foo", + "DELETE FROM Foo;", Statement::Delete { table_name: "Foo".into(), selection: None @@ -330,7 +334,7 @@ mod tests { ); assert_eq!( - r#"DELETE FROM Foo WHERE item = "glue""#, + "DELETE FROM Foo WHERE item = 'glue';", Statement::Delete { table_name: "Foo".into(), selection: Some(Expr::BinaryOp { @@ -346,7 +350,7 @@ mod tests { #[test] fn to_sql_create_table() { assert_eq!( - "CREATE TABLE IF NOT EXISTS Foo ()", + "CREATE TABLE IF NOT EXISTS Foo ();", Statement::CreateTable { if_not_exists: true, name: "Foo".into(), @@ -357,7 +361,7 @@ mod tests { ); assert_eq!( - "CREATE TABLE Foo (id INT, num INT NULL, name TEXT)", + "CREATE TABLE Foo (id INT, num INT NULL, name TEXT);", Statement::CreateTable { if_not_exists: false, name: "Foo".into(), @@ -387,7 +391,7 @@ mod tests { #[test] fn to_sql_create_table_as() { assert_eq!( - "CREATE TABLE Foo AS SELECT id, count FROM Bar", + "CREATE TABLE Foo AS SELECT id, count FROM Bar;", Statement::CreateTable { if_not_exists: false, name: "Foo".into(), @@ -425,7 +429,7 @@ mod tests { ); assert_eq!( - "CREATE TABLE IF NOT EXISTS Foo AS VALUES (TRUE)", + "CREATE TABLE IF NOT EXISTS Foo AS VALUES (TRUE);", Statement::CreateTable { if_not_exists: true, name: "Foo".into(), @@ -447,7 +451,7 @@ mod tests { #[cfg(feature = "alter-table")] fn to_sql_alter_table() { assert_eq!( - "ALTER TABLE Foo ADD COLUMN amount INT DEFAULT 10", + "ALTER TABLE Foo ADD COLUMN amount INT DEFAULT 10;", Statement::AlterTable { name: "Foo".into(), operation: AlterTableOperation::AddColumn { @@ -464,7 +468,7 @@ mod tests { ); assert_eq!( - "ALTER TABLE Foo DROP COLUMN something", + "ALTER TABLE Foo DROP COLUMN something;", Statement::AlterTable { name: "Foo".into(), operation: AlterTableOperation::DropColumn { @@ -476,7 +480,7 @@ mod tests { ); assert_eq!( - "ALTER TABLE Foo DROP COLUMN IF EXISTS something", + "ALTER TABLE Foo DROP COLUMN IF EXISTS something;", Statement::AlterTable { name: "Foo".into(), operation: AlterTableOperation::DropColumn { @@ -488,7 +492,7 @@ mod tests { ); assert_eq!( - "ALTER TABLE Bar RENAME COLUMN id TO new_id", + "ALTER TABLE Bar RENAME COLUMN id TO new_id;", Statement::AlterTable { name: "Bar".into(), operation: AlterTableOperation::RenameColumn { @@ -500,7 +504,7 @@ mod tests { ); assert_eq!( - "ALTER TABLE Foo RENAME TO Bar", + "ALTER TABLE Foo RENAME TO Bar;", Statement::AlterTable { name: "Foo".to_owned(), operation: AlterTableOperation::RenameTable { @@ -514,7 +518,7 @@ mod tests { #[test] fn to_sql_drop_table() { assert_eq!( - "DROP TABLE Test", + "DROP TABLE Test;", Statement::DropTable { if_exists: false, names: vec!["Test".into()] @@ -523,7 +527,7 @@ mod tests { ); assert_eq!( - "DROP TABLE IF EXISTS Test", + "DROP TABLE IF EXISTS Test;", Statement::DropTable { if_exists: true, names: vec!["Test".into()] @@ -532,7 +536,7 @@ mod tests { ); assert_eq!( - "DROP TABLE Foo, Bar", + "DROP TABLE Foo, Bar;", Statement::DropTable { if_exists: false, names: vec!["Foo".into(), "Bar".into(),] @@ -545,7 +549,7 @@ mod tests { #[cfg(feature = "index")] fn to_sql_create_index() { assert_eq!( - "CREATE INDEX idx_name ON Test LastName", + "CREATE INDEX idx_name ON Test LastName;", Statement::CreateIndex { name: "idx_name".into(), table_name: "Test".into(), @@ -562,7 +566,7 @@ mod tests { #[cfg(feature = "index")] fn to_sql_drop_index() { assert_eq!( - "DROP INDEX Test.idx_id", + "DROP INDEX Test.idx_id;", Statement::DropIndex { name: "idx_id".into(), table_name: "Test".into(), @@ -574,19 +578,19 @@ mod tests { #[test] #[cfg(feature = "transaction")] fn to_sql_transaction() { - assert_eq!("START TRANSACTION", Statement::StartTransaction.to_sql()); - assert_eq!("COMMIT", Statement::Commit.to_sql()); - assert_eq!("ROLLBACK", Statement::Rollback.to_sql()); + assert_eq!("START TRANSACTION;", Statement::StartTransaction.to_sql()); + assert_eq!("COMMIT;", Statement::Commit.to_sql()); + assert_eq!("ROLLBACK;", Statement::Rollback.to_sql()); } #[test] fn to_sql_show_variable() { assert_eq!( - "SHOW TABLES", + "SHOW TABLES;", Statement::ShowVariable(Variable::Tables).to_sql() ); assert_eq!( - "SHOW VERSIONS", + "SHOW VERSIONS;", Statement::ShowVariable(Variable::Version).to_sql() ); } @@ -595,7 +599,7 @@ mod tests { #[cfg(feature = "index")] fn to_sql_show_indexes() { assert_eq!( - "SHOW INDEXES FROM Test", + "SHOW INDEXES FROM Test;", Statement::ShowIndexes("Test".into()).to_sql() ); } diff --git a/core/src/ast/query.rs b/core/src/ast/query.rs index 811fc6b74..7f5fb709d 100644 --- a/core/src/ast/query.rs +++ b/core/src/ast/query.rs @@ -461,7 +461,7 @@ mod tests { .to_sql(); assert_eq!(actual, expected); - let actual = r#"VALUES (1, "glue", 3), (2, "sql", 2)"#.to_owned(); + let actual = "VALUES (1, 'glue', 3), (2, 'sql', 2)".to_owned(); let expected = SetExpr::Values(Values(vec![ vec![ Expr::Literal(AstLiteral::Number(BigDecimal::from_str("1").unwrap())), @@ -480,7 +480,7 @@ mod tests { #[test] fn to_sql_select() { - let actual = r#"SELECT * FROM FOO AS F GROUP BY name HAVING name = "glue""#.to_owned(); + let actual = "SELECT * FROM FOO AS F GROUP BY name HAVING name = 'glue'".to_owned(); let expected = Select { projection: vec![SelectItem::Wildcard], from: TableWithJoins { @@ -505,7 +505,7 @@ mod tests { .to_sql(); assert_eq!(actual, expected); - let actual = r#"SELECT * FROM FOO WHERE name = "glue""#.to_owned(); + let actual = "SELECT * FROM FOO WHERE name = 'glue'".to_owned(); let expected = Select { projection: vec![SelectItem::Wildcard], from: TableWithJoins { diff --git a/core/src/data/interval/string.rs b/core/src/data/interval/string.rs index f1b2da1fd..84fa5863b 100644 --- a/core/src/data/interval/string.rs +++ b/core/src/data/interval/string.rs @@ -44,9 +44,9 @@ impl From<&Interval> for String { let month = v % 12; match (year, month) { - (_, 0) if year != 0 => format!(r#""{}{}" YEAR"#, sign, year), - (0, _) => format!(r#""{}{}" MONTH"#, sign, month), - _ => format!(r#""{}{}-{}" YEAR TO MONTH"#, sign, year, month), + (_, 0) if year != 0 => format!("'{}{}' YEAR", sign, year), + (0, _) => format!("'{}{}' MONTH", sign, month), + _ => format!("'{}{}-{}' YEAR TO MONTH", sign, year, month), } } Interval::Microsecond(v) => { @@ -67,7 +67,7 @@ impl From<&Interval> for String { macro_rules! f { ($template: literal; $( $value: expr )*; $from_to: literal) => { - format!(r#""{}{}" {}"#, sign, format!($template, $( $value ),*), $from_to) + format!("'{}{}' {}", sign, format!($template, $( $value ),*), $from_to) }; (DAY $template: literal) => { @@ -162,7 +162,7 @@ mod tests { macro_rules! test { ($( $value: literal $duration: ident ),* => $result: literal $from_to: tt) => { let interval = interval!($( $value $duration ),*); - let interval_str = format!(r#""{}" {}"#, $result, stringify!($from_to)); + let interval_str = format!("'{}' {}", $result, stringify!($from_to)); assert_eq!(Ok(interval), Interval::try_from(interval_str.as_str())); assert_eq!(String::from(interval), interval_str); @@ -170,7 +170,7 @@ mod tests { ($( $value: literal $duration: ident ),* => $result: literal $from: tt TO $to: tt) => { let interval = interval!($( $value $duration ),*); let interval_str = format!( - r#""{}" {} TO {}"#, + "'{}' {} TO {}", $result, stringify!($from), stringify!($to), diff --git a/core/src/data/schema.rs b/core/src/data/schema.rs index 29fd6df3b..8da3c2c36 100644 --- a/core/src/data/schema.rs +++ b/core/src/data/schema.rs @@ -1,8 +1,8 @@ use { - crate::ast::{ColumnDef, ColumnOption, Expr}, + crate::ast::{ColumnDef, ColumnOption, Expr, Statement, ToSql}, chrono::NaiveDateTime, serde::{Deserialize, Serialize}, - std::fmt::Debug, + std::{fmt::Debug, iter}, strum_macros::Display, }; @@ -22,7 +22,7 @@ pub struct SchemaIndex { pub created: NaiveDateTime, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Schema { pub table_name: String, pub column_defs: Vec, @@ -30,6 +30,37 @@ pub struct Schema { pub created: NaiveDateTime, } +impl Schema { + pub fn to_ddl(self) -> String { + let Schema { + table_name, + column_defs: columns, + indexes, + .. + } = self; + + let create_table = Statement::CreateTable { + if_not_exists: false, + name: table_name.clone(), + columns, + source: None, + } + .to_sql(); + + let create_indexes = indexes.iter().map(|SchemaIndex { name, expr, .. }| { + let expr = expr.to_sql(); + let table_name = &table_name; + + format!("CREATE INDEX {name} ON {table_name} ({expr});") + }); + + iter::once(create_table) + .chain(create_indexes) + .collect::>() + .join("\n") + } +} + impl ColumnDef { pub fn is_nullable(&self) -> bool { self.options @@ -44,3 +75,102 @@ impl ColumnDef { }) } } + +#[cfg(test)] +mod tests { + + use crate::{ + ast::{AstLiteral, ColumnDef, ColumnOption, Expr}, + chrono::Utc, + data::{Schema, SchemaIndex, SchemaIndexOrd}, + prelude::DataType, + }; + + #[test] + fn table_basic() { + let schema = Schema { + table_name: "User".to_owned(), + column_defs: vec![ + ColumnDef { + name: "id".to_owned(), + data_type: DataType::Int, + options: Vec::new(), + }, + ColumnDef { + name: "name".to_owned(), + data_type: DataType::Text, + options: vec![ + ColumnOption::Null, + ColumnOption::Default(Expr::Literal(AstLiteral::QuotedString( + "glue".to_owned(), + ))), + ], + }, + ], + indexes: Vec::new(), + created: Utc::now().naive_utc(), + }; + + assert_eq!( + schema.to_ddl(), + "CREATE TABLE User (id INT, name TEXT NULL DEFAULT 'glue');" + ) + } + + #[test] + fn table_primary() { + let schema = Schema { + table_name: "User".to_owned(), + column_defs: vec![ColumnDef { + name: "id".to_owned(), + data_type: DataType::Int, + options: vec![ColumnOption::Unique { is_primary: true }], + }], + indexes: Vec::new(), + created: Utc::now().naive_utc(), + }; + + assert_eq!(schema.to_ddl(), "CREATE TABLE User (id INT PRIMARY KEY);"); + } + + #[test] + fn table_with_index() { + let schema = Schema { + table_name: "User".to_owned(), + column_defs: vec![ + ColumnDef { + name: "id".to_owned(), + data_type: DataType::Int, + options: Vec::new(), + }, + ColumnDef { + name: "name".to_owned(), + data_type: DataType::Text, + options: Vec::new(), + }, + ], + indexes: vec![ + SchemaIndex { + name: "User_id".to_owned(), + expr: Expr::Identifier("id".to_owned()), + order: SchemaIndexOrd::Both, + created: Utc::now().naive_utc(), + }, + SchemaIndex { + name: "User_name".to_owned(), + expr: Expr::Identifier("name".to_owned()), + order: SchemaIndexOrd::Both, + created: Utc::now().naive_utc(), + }, + ], + created: Utc::now().naive_utc(), + }; + + assert_eq!( + schema.to_ddl(), + "CREATE TABLE User (id INT, name TEXT); +CREATE INDEX User_id ON User (id); +CREATE INDEX User_name ON User (name);" + ); + } +} diff --git a/core/src/data/value/error.rs b/core/src/data/value/error.rs index d12adbbf3..6c691bf20 100644 --- a/core/src/data/value/error.rs +++ b/core/src/data/value/error.rs @@ -176,6 +176,9 @@ pub enum ValueError { #[error("unsupported value by position function: from_str(from_str:?), sub_str(sub_str:?)")] UnSupportedValueByPositionFunction { from_str: Value, sub_str: Value }, + + #[error("failed to convert Value to Expr")] + ValueToExprConversionFailure, } #[derive(Debug, PartialEq, Eq, Serialize, Display)] diff --git a/core/src/data/value/expr.rs b/core/src/data/value/expr.rs new file mode 100644 index 000000000..94b6980bd --- /dev/null +++ b/core/src/data/value/expr.rs @@ -0,0 +1,250 @@ +use { + super::ValueError::ValueToExprConversionFailure, + crate::{ + ast::AstLiteral, + ast::{DateTimeField, Expr}, + chrono::{DateTime, Utc}, + data::Interval, + prelude::DataType, + prelude::Value, + result::Error, + result::Result, + }, + bigdecimal::{BigDecimal, FromPrimitive}, + serde_json::{Map as JsonMap, Value as JsonValue}, + uuid::Uuid, +}; + +impl TryFrom for Expr { + type Error = Error; + + fn try_from(value: Value) -> Result { + const SECOND: i64 = 1_000_000; + + let expr = match value { + Value::Bool(v) => Expr::Literal(AstLiteral::Boolean(v)), + Value::I8(v) => Expr::Literal(AstLiteral::Number( + BigDecimal::from_i8(v).ok_or(ValueToExprConversionFailure)?, + )), + Value::I16(v) => Expr::Literal(AstLiteral::Number( + BigDecimal::from_i16(v).ok_or(ValueToExprConversionFailure)?, + )), + Value::I32(v) => Expr::Literal(AstLiteral::Number( + BigDecimal::from_i32(v).ok_or(ValueToExprConversionFailure)?, + )), + Value::I64(v) => Expr::Literal(AstLiteral::Number( + BigDecimal::from_i64(v).ok_or(ValueToExprConversionFailure)?, + )), + Value::I128(v) => Expr::Literal(AstLiteral::Number( + BigDecimal::from_i128(v).ok_or(ValueToExprConversionFailure)?, + )), + Value::U8(v) => Expr::Literal(AstLiteral::Number( + BigDecimal::from_u8(v).ok_or(ValueToExprConversionFailure)?, + )), + Value::U16(v) => Expr::Literal(AstLiteral::Number( + BigDecimal::from_u16(v).ok_or(ValueToExprConversionFailure)?, + )), + + Value::F64(v) => Expr::Literal(AstLiteral::Number( + BigDecimal::from_f64(v).ok_or(ValueToExprConversionFailure)?, + )), + Value::Decimal(v) => Expr::Literal(AstLiteral::Number( + BigDecimal::from_f64(v.try_into().map_err(|_| ValueToExprConversionFailure)?) + .ok_or(ValueToExprConversionFailure)?, + )), + Value::Str(v) => Expr::Literal(AstLiteral::QuotedString(v)), + Value::Bytea(v) => Expr::Literal(AstLiteral::HexString(hex::encode(v))), + Value::Date(v) => Expr::TypedString { + data_type: DataType::Date, + value: v.to_string(), + }, + Value::Timestamp(v) => Expr::TypedString { + data_type: DataType::Timestamp, + value: DateTime::::from_utc(v, Utc).to_string(), + }, + Value::Time(v) => Expr::TypedString { + data_type: DataType::Time, + value: v.to_string(), + }, + Value::Interval(v) => match v { + Interval::Month(v) => Expr::Interval { + expr: Box::new(Expr::Literal(AstLiteral::Number( + BigDecimal::from_i32(v).ok_or(ValueToExprConversionFailure)?, + ))), + leading_field: Some(DateTimeField::Month), + last_field: None, + }, + Interval::Microsecond(v) => Expr::Interval { + expr: Box::new(Expr::Literal(AstLiteral::Number( + BigDecimal::from_i64(v / SECOND).ok_or(ValueToExprConversionFailure)?, + ))), + leading_field: Some(DateTimeField::Second), + last_field: None, + }, + }, + Value::Uuid(v) => Expr::Literal(AstLiteral::QuotedString( + Uuid::from_u128(v).hyphenated().to_string(), + )), + Value::Map(v) => { + let json: JsonValue = v + .into_iter() + .map(|(key, value)| value.try_into().map(|value| (key, value))) + .collect::>>() + .map(|v| JsonMap::from_iter(v).into()) + .map_err(|_| ValueToExprConversionFailure)?; + + Expr::Literal(AstLiteral::QuotedString(json.to_string())) + } + Value::List(v) => { + let json: JsonValue = v + .into_iter() + .map(|value| value.try_into()) + .collect::>>() + .map(|v| v.into()) + .map_err(|_| ValueToExprConversionFailure)?; + + Expr::Literal(AstLiteral::QuotedString(json.to_string())) + } + Value::Null => Expr::Literal(AstLiteral::Null), + }; + + Ok(expr) + } +} + +#[cfg(test)] +mod tests { + use { + crate::{ + ast::{AstLiteral, DateTimeField, Expr}, + data::Interval, + prelude::{DataType, Value}, + }, + bigdecimal::BigDecimal, + bigdecimal::FromPrimitive, + chrono::{NaiveDate, NaiveTime}, + rust_decimal::Decimal, + std::collections::HashMap, + }; + + #[test] + fn value_to_expr() { + assert_eq!( + Value::Bool(true).try_into(), + Ok(Expr::Literal(AstLiteral::Boolean(true))) + ); + + assert_eq!( + Value::I8(127).try_into(), + Ok(Expr::Literal(AstLiteral::Number( + BigDecimal::from_i8(127).unwrap() + ))) + ); + assert_eq!( + Value::I16(32767).try_into(), + Ok(Expr::Literal(AstLiteral::Number( + BigDecimal::from_i16(32767).unwrap() + ))) + ); + assert_eq!( + Value::I32(2147483647).try_into(), + Ok(Expr::Literal(AstLiteral::Number( + BigDecimal::from_i32(2147483647).unwrap() + ))) + ); + assert_eq!( + Value::I64(64).try_into(), + Ok(Expr::Literal(AstLiteral::Number( + BigDecimal::from_i64(64).unwrap() + ))) + ); + assert_eq!( + Value::I128(128).try_into(), + Ok(Expr::Literal(AstLiteral::Number( + BigDecimal::from_i128(128).unwrap() + ))) + ); + assert_eq!( + Value::U8(8).try_into(), + Ok(Expr::Literal(AstLiteral::Number( + BigDecimal::from_u8(8).unwrap() + ))) + ); + assert_eq!( + Value::F64(64.4).try_into(), + Ok(Expr::Literal(AstLiteral::Number( + BigDecimal::from_f64(64.4).unwrap() + ))) + ); + assert_eq!( + Value::Decimal(Decimal::new(315, 2)).try_into(), + Ok(Expr::Literal(AstLiteral::Number( + BigDecimal::from_f64(3.15).unwrap() + ))) + ); + assert_eq!( + Value::Str("data".to_owned()).try_into(), + Ok(Expr::Literal(AstLiteral::QuotedString("data".to_owned()))) + ); + assert_eq!( + Value::Bytea(hex::decode("1234").unwrap()).try_into(), + Ok(Expr::Literal(AstLiteral::HexString("1234".to_owned()))) + ); + assert_eq!( + Value::Date(NaiveDate::from_ymd(2022, 11, 3)).try_into(), + Ok(Expr::TypedString { + data_type: DataType::Date, + value: "2022-11-03".to_owned(), + }) + ); + assert_eq!( + Value::Timestamp(NaiveDate::from_ymd(2022, 11, 3).and_hms_milli(8, 5, 30, 900)) + .try_into(), + Ok(Expr::TypedString { + data_type: DataType::Timestamp, + value: "2022-11-03 08:05:30.900 UTC".to_owned(), + }), + ); + assert_eq!( + Value::Time(NaiveTime::from_hms(20, 11, 59)).try_into(), + Ok(Expr::TypedString { + data_type: DataType::Time, + value: "20:11:59".to_owned() + }), + ); + assert_eq!( + Value::Interval(Interval::Month(1)).try_into(), + Ok(Expr::Interval { + expr: Box::new(Expr::Literal(AstLiteral::Number( + BigDecimal::from_i64(1).unwrap() + ))), + leading_field: Some(DateTimeField::Month), + last_field: None + }) + ); + assert_eq!( + Value::Uuid(195965723427462096757863453463987888808).try_into(), + Ok(Expr::Literal(AstLiteral::QuotedString( + "936da01f-9abd-4d9d-80c7-02af85c822a8".to_owned() + ))) + ); + assert_eq!( + Value::Map(HashMap::from([("a".to_owned(), Value::Bool(true))])).try_into(), + Ok(Expr::Literal(AstLiteral::QuotedString( + "{\"a\":true}".to_owned() + ))) + ); + assert_eq!( + Value::List(vec![ + Value::I64(1), + Value::Bool(true), + Value::Str("a".to_owned()) + ]) + .try_into(), + Ok(Expr::Literal(AstLiteral::QuotedString( + "[1,true,\"a\"]".to_owned() + ))) + ); + assert_eq!(Value::Null.try_into(), Ok(Expr::Literal(AstLiteral::Null))); + } +} diff --git a/core/src/data/value/json.rs b/core/src/data/value/json.rs index 544296c6f..aa661724f 100644 --- a/core/src/data/value/json.rs +++ b/core/src/data/value/json.rs @@ -180,7 +180,7 @@ mod tests { ); assert_eq!( Value::Interval(Interval::Month(17)).try_into(), - Ok(JsonValue::String(r#""1-5" YEAR TO MONTH"#.to_owned())) + Ok(JsonValue::String("'1-5' YEAR TO MONTH".to_owned())) ); let uuid = "43185717-59af-4e2b-9cd3-3264bf3691a4"; diff --git a/core/src/data/value/mod.rs b/core/src/data/value/mod.rs index 0b3968431..4f0717596 100644 --- a/core/src/data/value/mod.rs +++ b/core/src/data/value/mod.rs @@ -13,13 +13,13 @@ mod binary_op; mod convert; mod date; mod error; +mod expr; mod json; mod literal; mod selector; mod uuid; -pub use error::NumericBinaryOperator; -pub use error::ValueError; +pub use error::{NumericBinaryOperator, ValueError}; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Value { diff --git a/pkg/javascript/web/Cargo.toml b/pkg/javascript/web/Cargo.toml index 262f464db..71b6c3733 100644 --- a/pkg/javascript/web/Cargo.toml +++ b/pkg/javascript/web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gluesql-js" -version = "0.13.0" +version = "0.13.1" edition = "2021" authors = ["Taehoon Moon "] description = "GlueSQL - Open source SQL database engine fully written in Rust with pure functional execution layer, easily swappable storage and web assembly support!" @@ -28,7 +28,7 @@ serde_json = "1" # code size when deploying. console_error_panic_hook = { version = "0.1.6", optional = true } -gluesql-core = { path = "../../../core", version = "0.13.0" } +gluesql-core = { path = "../../../core", version = "0.13.1" } memory-storage = { package = "gluesql_memory_storage", path = "../../../storages/memory-storage", version = "0.13.0" } [dev-dependencies] @@ -37,5 +37,5 @@ wasm-bindgen-test = "0.3.13" [dev-dependencies.test-suite] package = "gluesql-test-suite" path = "../../../test-suite" -version = "0.13.0" +version = "0.13.1" features = ["alter-table"] diff --git a/pkg/rust/Cargo.toml b/pkg/rust/Cargo.toml index e328768e0..09967516b 100644 --- a/pkg/rust/Cargo.toml +++ b/pkg/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gluesql" -version = "0.13.0" +version = "0.13.1" edition = "2021" authors = ["Taehoon Moon "] default-run = "gluesql" @@ -12,8 +12,7 @@ readme = "../../README.md" keywords = [ "sql-database", "sql", - "functional", - "no-mut-in-the-middle", + "websql", "webassembly", ] @@ -21,9 +20,9 @@ keywords = [ all-features = true [dependencies] -gluesql-core = { path = "../../core", version = "0.13.0" } -cli = { package = "gluesql-cli", path = "../../cli", version = "0.13.0", optional = true } -test-suite = { package = "gluesql-test-suite", path = "../../test-suite", version = "0.13.0", optional = true } +gluesql-core = { path = "../../core", version = "0.13.1" } +cli = { package = "gluesql-cli", path = "../../cli", version = "0.13.1", optional = true } +test-suite = { package = "gluesql-test-suite", path = "../../test-suite", version = "0.13.1", optional = true } memory-storage = { package = "gluesql_memory_storage", path = "../../storages/memory-storage", version = "0.13.0", optional = true } shared-memory-storage = { package = "gluesql-shared-memory-storage", path = "../../storages/shared-memory-storage", version = "0.13.0", optional = true } sled-storage = { package = "gluesql_sled_storage", path = "../../storages/sled-storage", version = "0.13.0", optional = true } diff --git a/pkg/rust/src/main.rs b/pkg/rust/src/main.rs index c2be60fa9..96f591ebe 100644 --- a/pkg/rust/src/main.rs +++ b/pkg/rust/src/main.rs @@ -1,4 +1,4 @@ fn main() { #[cfg(feature = "cli")] - cli::run(); + cli::run().unwrap(); } diff --git a/test-suite/Cargo.toml b/test-suite/Cargo.toml index 63e96d737..6081dd959 100644 --- a/test-suite/Cargo.toml +++ b/test-suite/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gluesql-test-suite" -version = "0.13.0" +version = "0.13.1" edition = "2021" authors = ["Taehoon Moon "] description = "GlueSQL - Open source SQL database engine fully written in Rust with pure functional execution layer, easily swappable storage and web assembly support!" @@ -9,7 +9,7 @@ repository = "https://github.com/gluesql/gluesql" documentation = "https://docs.rs/gluesql/" [dependencies] -gluesql-core = { path = "../core", version = "0.13.0" } +gluesql-core = { path = "../core", version = "0.13.1" } async-trait = "0.1" bigdecimal = "0.3" chrono = "0.4" diff --git a/test-suite/src/dictionary_index.rs b/test-suite/src/dictionary_index.rs index eb3ef6461..7b2d9208a 100644 --- a/test-suite/src/dictionary_index.rs +++ b/test-suite/src/dictionary_index.rs @@ -25,7 +25,7 @@ test_case!(ditionary_index, async move { TABLE_NAME | INDEX_NAME | ORDER | EXPRESSION | UNIQUENESS; Str | Str | Str | Str | Bool; "Bar".to_owned() "PRIMARY".to_owned() "BOTH".to_owned() "id".to_owned() true; - "Bar".to_owned() "Bar_name_concat".to_owned() "BOTH".to_owned() "name + \"_\"".to_owned() false; + "Bar".to_owned() "Bar_name_concat".to_owned() "BOTH".to_owned() "name + '_'".to_owned() false; "Foo".to_owned() "Foo_id".to_owned() "BOTH".to_owned() "id".to_owned() false; "Foo".to_owned() "Foo_id_2".to_owned() "BOTH".to_owned() "id + 2".to_owned() false ))