diff --git a/Cargo.lock b/Cargo.lock index 06557527fd44..06b0e507ed2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3633,6 +3633,18 @@ dependencies = [ "tonic-build", ] +[[package]] +name = "databend-common-script" +version = "0.1.0" +dependencies = [ + "databend-common-ast", + "databend-common-exception", + "derive-visitor", + "goldenfile", + "minitrace", + "unindent", +] + [[package]] name = "databend-common-settings" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index d1b55e044de7..b8b1a8ce41a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ members = [ "src/query/pipeline/sinks", "src/query/pipeline/sources", "src/query/pipeline/transforms", + "src/query/script", "src/query/settings", "src/query/sql", "src/query/storages/common/blocks", diff --git a/src/common/exception/src/exception_code.rs b/src/common/exception/src/exception_code.rs index 1d8cd6f8503e..94c9056052a4 100644 --- a/src/common/exception/src/exception_code.rs +++ b/src/common/exception/src/exception_code.rs @@ -352,6 +352,9 @@ build_exceptions! { TenantQuotaUnknown(2902), TenantQuotaExceeded(2903), + // Script error codes. + ScriptSemanticError(3001), + ScriptExecutionError(3002), } // Storage errors [3001, 4000]. diff --git a/src/query/ast/src/ast/query.rs b/src/query/ast/src/ast/query.rs index 75fd194ca2eb..e546d700d373 100644 --- a/src/query/ast/src/ast/query.rs +++ b/src/query/ast/src/ast/query.rs @@ -320,10 +320,10 @@ pub enum SetOperator { #[derive(Debug, Clone, PartialEq, Drive, DriveMut)] pub struct OrderByExpr { pub expr: Expr, - // Optional `ASC` or `DESC` + /// `ASC` or `DESC` #[drive(skip)] pub asc: Option, - // Optional `NULLS FIRST` or `NULLS LAST` + /// `NULLS FIRST` or `NULLS LAST` #[drive(skip)] pub nulls_first: Option, } @@ -352,16 +352,16 @@ impl Display for OrderByExpr { /// One item of the comma-separated list following `SELECT` #[derive(Debug, Clone, PartialEq, Drive, DriveMut)] pub enum SelectTarget { - // Expression with alias, e.g. `SELECT t.a, b AS a, a+1 AS b FROM t` + /// Expression with alias, e.g. `SELECT t.a, b AS a, a+1 AS b FROM t` AliasedExpr { expr: Box, alias: Option, }, - // Qualified star name, e.g. `SELECT t.* exclude a, columns(expr) FROM t`. - // Columns("pattern_str") - // Columns(lambda expression) - // For simplicity, star wildcard is involved. + /// Qualified star name, e.g. `SELECT t.* exclude a, columns(expr) FROM t`. + /// Columns("pattern_str") + /// Columns(lambda expression) + /// For simplicity, star wildcard is involved. StarColumns { qualified: QualifiedName, column_filter: Option, diff --git a/src/query/ast/src/ast/statements/script.rs b/src/query/ast/src/ast/statements/script.rs index 3e4ca5bae7f0..b6684e3821e6 100644 --- a/src/query/ast/src/ast/statements/script.rs +++ b/src/query/ast/src/ast/statements/script.rs @@ -19,7 +19,6 @@ use databend_common_exception::Span; use crate::ast::Expr; use crate::ast::Identifier; -use crate::ast::Query; use crate::ast::Statement; use crate::ast::TypeName; @@ -64,15 +63,15 @@ impl Display for VariableDeclare { } #[derive(Debug, Clone, PartialEq)] -pub struct QueryDeclare { +pub struct StatementDeclare { pub name: Identifier, - pub query: Query, + pub stmt: Statement, } -impl Display for QueryDeclare { +impl Display for StatementDeclare { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let QueryDeclare { name, query } = self; - write!(f, "{name} RESULTSET := {query}") + let StatementDeclare { name, stmt } = self; + write!(f, "{name} RESULTSET := {stmt}") } } @@ -82,9 +81,13 @@ pub enum ScriptStatement { span: Span, declare: VariableDeclare, }, - LetQuery { + LetStatement { span: Span, - declare: QueryDeclare, + declare: StatementDeclare, + }, + RunStatement { + span: Span, + stmt: Statement, }, Assign { span: Span, @@ -149,17 +152,14 @@ pub enum ScriptStatement { results: Vec>, else_result: Option>, }, - SQLStatement { - span: Span, - stmt: Statement, - }, } impl Display for ScriptStatement { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { ScriptStatement::LetVar { declare, .. } => write!(f, "LET {declare}"), - ScriptStatement::LetQuery { declare, .. } => write!(f, "LET {declare}"), + ScriptStatement::LetStatement { declare, .. } => write!(f, "LET {declare}"), + ScriptStatement::RunStatement { stmt, .. } => write!(f, "{stmt}"), ScriptStatement::Assign { name, value, .. } => write!(f, "{name} := {value}"), ScriptStatement::Return { value, .. } => { if let Some(value) = value { @@ -352,7 +352,6 @@ impl Display for ScriptStatement { } write!(f, "END IF") } - ScriptStatement::SQLStatement { stmt, .. } => write!(f, "{stmt}"), } } } diff --git a/src/query/ast/src/parser/expr.rs b/src/query/ast/src/parser/expr.rs index 7e74e304704a..6315b119c198 100644 --- a/src/query/ast/src/parser/expr.rs +++ b/src/query/ast/src/parser/expr.rs @@ -1480,21 +1480,17 @@ pub fn literal_string(i: Input) -> IResult { QuotedString }, |token| { - if token - .text() - .chars() - .next() - .filter(|c| i.dialect.is_string_quote(*c)) - .is_some() - { - let str = &token.text()[1..token.text().len() - 1]; - let unescaped = unescape_string(str, '\'').ok_or(nom::Err::Failure( - ErrorKind::Other("invalid escape or unicode"), - ))?; - Ok(unescaped) - } else { - Err(nom::Err::Error(ErrorKind::ExpectToken(QuotedString))) + if let Some(quote) = token.text().chars().next() { + if i.dialect.is_string_quote(quote) { + let str = &token.text()[1..token.text().len() - 1]; + let unescaped = unescape_string(str, quote).ok_or(nom::Err::Failure( + ErrorKind::Other("invalid escape or unicode"), + ))?; + return Ok(unescaped); + } } + + Err(nom::Err::Error(ErrorKind::ExpectToken(QuotedString))) }, )(i) } diff --git a/src/query/ast/src/parser/script.rs b/src/query/ast/src/parser/script.rs index b3ee7bdbb317..611cbb477aca 100644 --- a/src/query/ast/src/parser/script.rs +++ b/src/query/ast/src/parser/script.rs @@ -19,19 +19,22 @@ use crate::ast::*; use crate::parser::common::*; use crate::parser::expr::*; use crate::parser::input::Input; -use crate::parser::query::*; use crate::parser::statement::*; use crate::parser::token::*; use crate::rule; +pub fn script_stmts(i: Input) -> IResult> { + semicolon_terminated_list1(script_stmt)(i) +} + pub fn script_stmt(i: Input) -> IResult { - let let_query_stmt = map( + let let_stmt_stmt = map( consumed(rule! { - LET ~^#ident ~ RESULTSET ~ ^":=" ~ ^#query + LET ~^#ident ~ RESULTSET ~ ^":=" ~ ^#statement_body }), - |(span, (_, name, _, _, query))| ScriptStatement::LetQuery { + |(span, (_, name, _, _, stmt))| ScriptStatement::LetStatement { span: transform_span(span.tokens), - declare: QueryDeclare { name, query }, + declare: StatementDeclare { name, stmt }, }, ); let let_var_stmt = map( @@ -47,6 +50,15 @@ pub fn script_stmt(i: Input) -> IResult { }, }, ); + let run_stmt = map( + consumed(rule! { + #statement_body + }), + |(span, stmt)| ScriptStatement::RunStatement { + span: transform_span(span.tokens), + stmt, + }, + ); let assign_stmt = map( consumed(rule! { #ident ~ ":=" ~ ^#expr @@ -199,19 +211,11 @@ pub fn script_stmt(i: Input) -> IResult { } }, ); - let sql_stmt = map( - consumed(rule! { - #statement_body - }), - |(span, stmt)| ScriptStatement::SQLStatement { - span: transform_span(span.tokens), - stmt, - }, - ); rule!( - #let_query_stmt + #let_stmt_stmt | #let_var_stmt + | #run_stmt | #assign_stmt | #return_stmt | #for_loop_stmt @@ -223,6 +227,5 @@ pub fn script_stmt(i: Input) -> IResult { | #continue_stmt | #case_stmt | #if_stmt - | #sql_stmt )(i) } diff --git a/src/query/ast/src/parser/unescape.rs b/src/query/ast/src/parser/unescape.rs index 933066aee206..7923991cf89f 100644 --- a/src/query/ast/src/parser/unescape.rs +++ b/src/query/ast/src/parser/unescape.rs @@ -28,6 +28,7 @@ pub fn unescape_string(s: &str, quote: char) -> Option { Some('r') => s.push('\r'), Some('t') => s.push('\t'), Some('\'') => s.push('\''), + Some('"') => s.push('"'), Some('\\') => s.push('\\'), Some('u') => s.push(unescape_unicode(&mut chars)?), Some('x') => s.push(unescape_byte(&mut chars)?), diff --git a/src/query/ast/tests/it/parser.rs b/src/query/ast/tests/it/parser.rs index 5e0905a57776..ae36260986e4 100644 --- a/src/query/ast/tests/it/parser.rs +++ b/src/query/ast/tests/it/parser.rs @@ -782,7 +782,7 @@ fn test_statement_error() { #[test] fn test_raw_insert_stmt() { let mut mint = Mint::new("tests/it/testdata"); - let file = &mut mint.new_goldenfile("raw_insert.txt").unwrap(); + let file = &mut mint.new_goldenfile("raw-insert.txt").unwrap(); let cases = &[ r#"insert into t (c1, c2) values (1, 2), (3, 4);"#, r#"insert into t (c1, c2) values (1, 2);"#, @@ -978,48 +978,72 @@ fn test_expr() { } #[test] -fn test_experimental_expr() { +fn test_expr_error() { let mut mint = Mint::new("tests/it/testdata"); - let file = &mut mint.new_goldenfile("experimental-expr.txt").unwrap(); + let file = &mut mint.new_goldenfile("expr-error.txt").unwrap(); let cases = &[ - r#"a"#, + r#"5 * (a and ) 1"#, + r#"a + +"#, + r#"CAST(col1 AS foo)"#, + r#"1 a"#, + r#"CAST(col1)"#, r#"a.add(b)"#, - r#"a.sub(b).add(e)"#, - r#"a.sub(b).add(e)"#, - r#"1 + {'k1': 4}.k1"#, - r#"'3'.plus(4)"#, - r#"(3).add({'k1': 4 }.k1)"#, r#"[ x * 100 FOR x in [1,2,3] if x % 2 = 0 ]"#, + r#" + G.E.B IS NOT NULL + AND col1 NOT BETWEEN col2 AND + AND 1 + col3 DIV sum(col4) + "#, ]; for case in cases { - run_parser_with_dialect(file, expr, Dialect::Experimental, ParseMode::Default, case); + run_parser(file, expr, case); } } #[test] -fn test_expr_error() { +fn test_dialect() { let mut mint = Mint::new("tests/it/testdata"); - let file = &mut mint.new_goldenfile("expr-error.txt").unwrap(); + let file = &mut mint.new_goldenfile("dialect.txt").unwrap(); let cases = &[ - r#"5 * (a and ) 1"#, - r#"a + +"#, - r#"CAST(col1 AS foo)"#, - r#"1 a"#, - r#"CAST(col1)"#, + r#"'a'"#, + r#""a""#, + r#"`a`"#, + r#"'a''b'"#, + r#"'a""b'"#, + r#"'a\'b'"#, + r#"'a"b'"#, + r#"'a`b'"#, + r#""a''b""#, + r#""a""b""#, + r#""a'b""#, + r#""a\"b""#, + r#""a`b""#, + ]; + + for case in cases { + run_parser_with_dialect(file, expr, Dialect::PostgreSQL, ParseMode::Default, case); + } + + for case in cases { + run_parser_with_dialect(file, expr, Dialect::MySQL, ParseMode::Default, case); + } + + let cases = &[ + r#"a"#, r#"a.add(b)"#, + r#"a.sub(b).add(e)"#, + r#"a.sub(b).add(e)"#, + r#"1 + {'k1': 4}.k1"#, + r#"'3'.plus(4)"#, + r#"(3).add({'k1': 4 }.k1)"#, r#"[ x * 100 FOR x in [1,2,3] if x % 2 = 0 ]"#, - r#" - G.E.B IS NOT NULL AND - col1 NOT BETWEEN col2 AND - AND 1 + col3 DIV sum(col4) - "#, ]; for case in cases { - run_parser(file, expr, case); + run_parser_with_dialect(file, expr, Dialect::Experimental, ParseMode::Default, case); } } diff --git a/src/query/ast/tests/it/testdata/experimental-expr.txt b/src/query/ast/tests/it/testdata/dialect.txt similarity index 72% rename from src/query/ast/tests/it/testdata/experimental-expr.txt rename to src/query/ast/tests/it/testdata/dialect.txt index 98435492add7..5e9f36b89a46 100644 --- a/src/query/ast/tests/it/testdata/experimental-expr.txt +++ b/src/query/ast/tests/it/testdata/dialect.txt @@ -1,3 +1,497 @@ +---------- Input ---------- +'a' +---------- Output --------- +'a' +---------- AST ------------ +Literal { + span: Some( + 0..3, + ), + value: String( + "a", + ), +} + + +---------- Input ---------- +"a" +---------- Output --------- +"a" +---------- AST ------------ +ColumnRef { + span: Some( + 0..3, + ), + column: ColumnRef { + database: None, + table: None, + column: Name( + Identifier { + span: Some( + 0..3, + ), + name: "a", + quote: Some( + '"', + ), + is_hole: false, + }, + ), + }, +} + + +---------- Input ---------- +`a` +---------- Output --------- +`a` +---------- AST ------------ +ColumnRef { + span: Some( + 0..3, + ), + column: ColumnRef { + database: None, + table: None, + column: Name( + Identifier { + span: Some( + 0..3, + ), + name: "a", + quote: Some( + '`', + ), + is_hole: false, + }, + ), + }, +} + + +---------- Input ---------- +'a''b' +---------- Output --------- +'a\'b' +---------- AST ------------ +Literal { + span: Some( + 0..6, + ), + value: String( + "a'b", + ), +} + + +---------- Input ---------- +'a""b' +---------- Output --------- +'a""b' +---------- AST ------------ +Literal { + span: Some( + 0..6, + ), + value: String( + "a\"\"b", + ), +} + + +---------- Input ---------- +'a\'b' +---------- Output --------- +'a\'b' +---------- AST ------------ +Literal { + span: Some( + 0..6, + ), + value: String( + "a'b", + ), +} + + +---------- Input ---------- +'a"b' +---------- Output --------- +'a"b' +---------- AST ------------ +Literal { + span: Some( + 0..5, + ), + value: String( + "a\"b", + ), +} + + +---------- Input ---------- +'a`b' +---------- Output --------- +'a`b' +---------- AST ------------ +Literal { + span: Some( + 0..5, + ), + value: String( + "a`b", + ), +} + + +---------- Input ---------- +"a''b" +---------- Output --------- +"a''b" +---------- AST ------------ +ColumnRef { + span: Some( + 0..6, + ), + column: ColumnRef { + database: None, + table: None, + column: Name( + Identifier { + span: Some( + 0..6, + ), + name: "a''b", + quote: Some( + '"', + ), + is_hole: false, + }, + ), + }, +} + + +---------- Input ---------- +"a""b" +---------- Output --------- +"a""b" +---------- AST ------------ +ColumnRef { + span: Some( + 0..6, + ), + column: ColumnRef { + database: None, + table: None, + column: Name( + Identifier { + span: Some( + 0..6, + ), + name: "a\"b", + quote: Some( + '"', + ), + is_hole: false, + }, + ), + }, +} + + +---------- Input ---------- +"a'b" +---------- Output --------- +"a'b" +---------- AST ------------ +ColumnRef { + span: Some( + 0..5, + ), + column: ColumnRef { + database: None, + table: None, + column: Name( + Identifier { + span: Some( + 0..5, + ), + name: "a'b", + quote: Some( + '"', + ), + is_hole: false, + }, + ), + }, +} + + +---------- Input ---------- +"a\"b" +---------- Output --------- +"a\""b" +---------- AST ------------ +ColumnRef { + span: Some( + 0..6, + ), + column: ColumnRef { + database: None, + table: None, + column: Name( + Identifier { + span: Some( + 0..6, + ), + name: "a\\\"b", + quote: Some( + '"', + ), + is_hole: false, + }, + ), + }, +} + + +---------- Input ---------- +"a`b" +---------- Output --------- +"a`b" +---------- AST ------------ +ColumnRef { + span: Some( + 0..5, + ), + column: ColumnRef { + database: None, + table: None, + column: Name( + Identifier { + span: Some( + 0..5, + ), + name: "a`b", + quote: Some( + '"', + ), + is_hole: false, + }, + ), + }, +} + + +---------- Input ---------- +'a' +---------- Output --------- +'a' +---------- AST ------------ +Literal { + span: Some( + 0..3, + ), + value: String( + "a", + ), +} + + +---------- Input ---------- +"a" +---------- Output --------- +'a' +---------- AST ------------ +Literal { + span: Some( + 0..3, + ), + value: String( + "a", + ), +} + + +---------- Input ---------- +`a` +---------- Output --------- +`a` +---------- AST ------------ +ColumnRef { + span: Some( + 0..3, + ), + column: ColumnRef { + database: None, + table: None, + column: Name( + Identifier { + span: Some( + 0..3, + ), + name: "a", + quote: Some( + '`', + ), + is_hole: false, + }, + ), + }, +} + + +---------- Input ---------- +'a''b' +---------- Output --------- +'a\'b' +---------- AST ------------ +Literal { + span: Some( + 0..6, + ), + value: String( + "a'b", + ), +} + + +---------- Input ---------- +'a""b' +---------- Output --------- +'a""b' +---------- AST ------------ +Literal { + span: Some( + 0..6, + ), + value: String( + "a\"\"b", + ), +} + + +---------- Input ---------- +'a\'b' +---------- Output --------- +'a\'b' +---------- AST ------------ +Literal { + span: Some( + 0..6, + ), + value: String( + "a'b", + ), +} + + +---------- Input ---------- +'a"b' +---------- Output --------- +'a"b' +---------- AST ------------ +Literal { + span: Some( + 0..5, + ), + value: String( + "a\"b", + ), +} + + +---------- Input ---------- +'a`b' +---------- Output --------- +'a`b' +---------- AST ------------ +Literal { + span: Some( + 0..5, + ), + value: String( + "a`b", + ), +} + + +---------- Input ---------- +"a''b" +---------- Output --------- +'a\'\'b' +---------- AST ------------ +Literal { + span: Some( + 0..6, + ), + value: String( + "a''b", + ), +} + + +---------- Input ---------- +"a""b" +---------- Output --------- +'a"b' +---------- AST ------------ +Literal { + span: Some( + 0..6, + ), + value: String( + "a\"b", + ), +} + + +---------- Input ---------- +"a'b" +---------- Output --------- +'a\'b' +---------- AST ------------ +Literal { + span: Some( + 0..5, + ), + value: String( + "a'b", + ), +} + + +---------- Input ---------- +"a\"b" +---------- Output --------- +'a"b' +---------- AST ------------ +Literal { + span: Some( + 0..6, + ), + value: String( + "a\"b", + ), +} + + +---------- Input ---------- +"a`b" +---------- Output --------- +'a`b' +---------- AST ------------ +Literal { + span: Some( + 0..5, + ), + value: String( + "a`b", + ), +} + + ---------- Input ---------- a ---------- Output --------- diff --git a/src/query/ast/tests/it/testdata/expr-error.txt b/src/query/ast/tests/it/testdata/expr-error.txt index 0e7a628d16a1..34bd1ae51a12 100644 --- a/src/query/ast/tests/it/testdata/expr-error.txt +++ b/src/query/ast/tests/it/testdata/expr-error.txt @@ -87,18 +87,18 @@ error: ---------- Input ---------- -G.E.B IS NOT NULL AND - col1 NOT BETWEEN col2 AND - AND 1 + col3 DIV sum(col4) +G.E.B IS NOT NULL +AND col1 NOT BETWEEN col2 AND +AND 1 + col3 DIV sum(col4) ---------- Output --------- error: - --> SQL:3:9 + --> SQL:3:1 | -1 | G.E.B IS NOT NULL AND +1 | G.E.B IS NOT NULL | - while parsing expression -2 | col1 NOT BETWEEN col2 AND +2 | AND col1 NOT BETWEEN col2 AND | --- while parsing `[NOT] BETWEEN ... AND ...` -3 | AND 1 + col3 DIV sum(col4) - | ^^^ expected more tokens for expression +3 | AND 1 + col3 DIV sum(col4) + | ^^^ expected more tokens for expression diff --git a/src/query/ast/tests/it/testdata/raw_insert.txt b/src/query/ast/tests/it/testdata/raw-insert.txt similarity index 100% rename from src/query/ast/tests/it/testdata/raw_insert.txt rename to src/query/ast/tests/it/testdata/raw-insert.txt diff --git a/src/query/ast/tests/it/testdata/script.txt b/src/query/ast/tests/it/testdata/script.txt index 268310a4ad9f..6aa59f355638 100644 --- a/src/query/ast/tests/it/testdata/script.txt +++ b/src/query/ast/tests/it/testdata/script.txt @@ -41,11 +41,11 @@ LET t1 RESULTSET := SELECT * FROM numbers(100) ---------- Output --------- LET t1 RESULTSET := SELECT * FROM numbers(100) ---------- AST ------------ -LetQuery { +LetStatement { span: Some( 0..46, ), - declare: QueryDeclare { + declare: StatementDeclare { name: Identifier { span: Some( 4..6, @@ -54,70 +54,72 @@ LetQuery { quote: None, is_hole: false, }, - query: Query { - span: Some( - 20..46, - ), - with: None, - body: Select( - SelectStmt { - span: Some( - 20..46, - ), - hints: None, - distinct: false, - select_list: [ - StarColumns { - qualified: [ - Star( - Some( - 27..28, + stmt: Query( + Query { + span: Some( + 20..46, + ), + with: None, + body: Select( + SelectStmt { + span: Some( + 20..46, + ), + hints: None, + distinct: false, + select_list: [ + StarColumns { + qualified: [ + Star( + Some( + 27..28, + ), ), - ), - ], - column_filter: None, - }, - ], - from: [ - TableFunction { - span: Some( - 34..46, - ), - lateral: false, - name: Identifier { + ], + column_filter: None, + }, + ], + from: [ + TableFunction { span: Some( - 34..41, + 34..46, ), - name: "numbers", - quote: None, - is_hole: false, - }, - params: [ - Literal { + lateral: false, + name: Identifier { span: Some( - 42..45, - ), - value: UInt64( - 100, + 34..41, ), + name: "numbers", + quote: None, + is_hole: false, }, - ], - named_params: [], - alias: None, - }, - ], - selection: None, - group_by: None, - having: None, - window_list: None, - qualify: None, - }, - ), - order_by: [], - limit: [], - offset: None, - ignore_result: false, - }, + params: [ + Literal { + span: Some( + 42..45, + ), + value: UInt64( + 100, + ), + }, + ], + named_params: [], + alias: None, + }, + ], + selection: None, + group_by: None, + having: None, + window_list: None, + qualify: None, + }, + ), + order_by: [], + limit: [], + offset: None, + ignore_result: false, + }, + ), }, } @@ -1312,7 +1314,7 @@ Loop { 0..52, ), body: [ - SQLStatement { + RunStatement { span: Some( 9..42, ), @@ -1455,7 +1457,7 @@ select :a + 1 ---------- Output --------- SELECT :a + 1 ---------- AST ------------ -SQLStatement { +RunStatement { span: Some( 0..13, ), @@ -1519,7 +1521,7 @@ select IDENTIFIER(:b) ---------- Output --------- SELECT IDENTIFIER(:b) ---------- AST ------------ -SQLStatement { +RunStatement { span: Some( 0..21, ), @@ -1582,7 +1584,7 @@ select a.IDENTIFIER(:b).c + minus(:d) ---------- Output --------- SELECT a.IDENTIFIER(:b).c + minus(:d) ---------- AST ------------ -SQLStatement { +RunStatement { span: Some( 0..37, ), diff --git a/src/query/functions/tests/it/scalars/testdata/string.txt b/src/query/functions/tests/it/scalars/testdata/string.txt index fc612631b73a..870f131681a8 100644 --- a/src/query/functions/tests/it/scalars/testdata/string.txt +++ b/src/query/functions/tests/it/scalars/testdata/string.txt @@ -361,12 +361,12 @@ output : 'a\'b' ast : quote('a\"b') -raw expr : quote('a\"b') -checked expr : quote("a\\\"b") -optimized expr : "a\\\\\\\"b" +raw expr : quote('a"b') +checked expr : quote("a\"b") +optimized expr : "a\\\"b" output type : String -output domain : {"a\\\\\\\"b"..="a\\\\\\\"b"} -output : 'a\\\"b' +output domain : {"a\\\"b"..="a\\\"b"} +output : 'a\"b' ast : quote('a\bb') diff --git a/src/query/script/Cargo.toml b/src/query/script/Cargo.toml new file mode 100644 index 000000000000..17c49ea57592 --- /dev/null +++ b/src/query/script/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "databend-common-script" +version = { workspace = true } +authors = { workspace = true } +license = { workspace = true } +publish = { workspace = true } +edition = { workspace = true } + +[lib] +doctest = false + +[dependencies] # In alphabetical order +# Workspace dependencies +databend-common-ast = { path = "../ast" } +databend-common-exception = { path = "../../common/exception" } + +# Crates.io dependencies +derive-visitor = { workspace = true } +minitrace = { workspace = true } + +[dev-dependencies] +goldenfile = "1.4" +unindent = "0.2.3" diff --git a/src/query/script/src/compiler.rs b/src/query/script/src/compiler.rs new file mode 100644 index 000000000000..2ec39b4f91dc --- /dev/null +++ b/src/query/script/src/compiler.rs @@ -0,0 +1,1093 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; +use std::fmt; +use std::fmt::Display; +use std::vec; + +use databend_common_ast::ast::BinaryOperator; +use databend_common_ast::ast::ColumnID; +use databend_common_ast::ast::ColumnRef; +use databend_common_ast::ast::Expr; +use databend_common_ast::ast::FunctionCall; +use databend_common_ast::ast::Identifier; +use databend_common_ast::ast::Indirection; +use databend_common_ast::ast::Literal; +use databend_common_ast::ast::Query; +use databend_common_ast::ast::ScriptStatement; +use databend_common_ast::ast::SelectStmt; +use databend_common_ast::ast::SelectTarget; +use databend_common_ast::ast::SetExpr; +use databend_common_ast::ast::Statement; +use databend_common_ast::ast::TableReference; +use databend_common_ast::ast::UnaryOperator; +use databend_common_exception::ErrorCode; +use databend_common_exception::Result; +use databend_common_exception::Span; +use derive_visitor::DriveMut; +use derive_visitor::VisitorMut; + +use crate::ir::ColumnAccess; +use crate::ir::IterRef; +use crate::ir::LabelRef; +use crate::ir::RefAllocator; +use crate::ir::ScriptIR; +use crate::ir::SetRef; +use crate::ir::StatementTemplate; +use crate::ir::VarRef; + +#[minitrace::trace] +pub fn compile(code: &[ScriptStatement]) -> Result> { + if code.is_empty() { + return Err(ErrorCode::ScriptSemanticError("empty script".to_string())); + } + + let mut compiler = Compiler::new(); + let result = compiler.compile(code); + + assert!(compiler.scopes.len() == 1 || result.is_err()); + + result +} + +struct Compiler { + ref_allocator: RefAllocator, + scopes: Vec, +} + +impl Compiler { + pub fn new() -> Compiler { + Compiler { + ref_allocator: RefAllocator::default(), + scopes: vec![Scope::default()], + } + } + + pub fn compile(&mut self, code: &[ScriptStatement]) -> Result> { + let mut output = vec![]; + + for line in code { + match line { + ScriptStatement::LetVar { declare, .. } => { + let to_var = self.declare_var(&declare.name)?; + output.append(&mut self.compile_expr(&declare.default, to_var)?); + } + ScriptStatement::LetStatement { span, declare } => { + let to_set = self.declare_set(&declare.name)?; + output.append(&mut self.compile_sql_statement(*span, &declare.stmt, to_set)?); + } + ScriptStatement::RunStatement { span, stmt } => { + let to_set = self.declare_anonymous_set(*span, "unused_result")?; + output.append(&mut self.compile_sql_statement(*span, stmt, to_set)?); + } + ScriptStatement::Assign { name, value, .. } => { + let to_var = self.lookup_var(name)?; + output.append(&mut self.compile_expr(value, to_var)?); + } + ScriptStatement::Return { value: None, .. } => { + output.push(ScriptIR::Return); + } + ScriptStatement::Return { + value: Some(value), .. + } => { + // TODO(andylokandy): support returning table + let to_var = self.declare_anonymous_var(value.span(), "return_val")?; + output.append(&mut self.compile_expr(value, to_var.clone())?); + output.push(ScriptIR::ReturnVar { var: to_var }); + } + ScriptStatement::ForLoop { + span, + variable, + is_reverse, + lower_bound, + upper_bound, + body, + label, + } => { + output.append(&mut self.compile_for_loop( + *span, + variable, + *is_reverse, + lower_bound, + upper_bound, + body, + label, + )?); + } + ScriptStatement::ForIn { + span, + variable, + resultset, + body, + label, + } => { + output + .append(&mut self.compile_for_in(*span, variable, resultset, body, label)?); + } + ScriptStatement::WhileLoop { + span, + condition, + body, + label, + } => { + output.append(&mut self.compile_while_loop(*span, condition, body, label)?); + } + ScriptStatement::RepeatLoop { + span, + body, + until_condition, + label, + } => { + output.append(&mut self.compile_repeat_loop( + *span, + until_condition, + body, + label, + )?); + } + ScriptStatement::Loop { span, body, label } => { + output.append(&mut self.compile_loop(*span, body, label)?); + } + ScriptStatement::Break { + label: Some(label), .. + } => { + let (_, break_label) = self.lookup_loop(label)?; + output.push(ScriptIR::Goto { + to_label: break_label, + }); + } + ScriptStatement::Break { span, label: None } => { + let (_, break_label) = self.current_loop(*span)?; + output.push(ScriptIR::Goto { + to_label: break_label, + }); + } + ScriptStatement::Continue { + label: Some(label), .. + } => { + let (continue_label, _) = self.lookup_loop(label)?; + output.push(ScriptIR::Goto { + to_label: continue_label, + }); + } + ScriptStatement::Continue { span, label: None } => { + let (continue_label, _) = self.current_loop(*span)?; + output.push(ScriptIR::Goto { + to_label: continue_label, + }); + } + ScriptStatement::If { + span, + conditions, + results, + else_result, + } => { + output.append(&mut self.compile_if(*span, conditions, results, else_result)?); + } + ScriptStatement::Case { + span, + operand: None, + conditions, + results, + else_result, + } => { + output.append(&mut self.compile_if(*span, conditions, results, else_result)?); + } + ScriptStatement::Case { + span, + operand: Some(operand), + conditions, + results, + else_result, + } => { + output.append(&mut self.compile_case( + *span, + operand, + conditions, + results, + else_result, + )?); + } + } + } + + Ok(output) + } + + fn compile_expr(&mut self, expr: &Expr, to_var: VarRef) -> Result> { + let mut output = vec![]; + + let (mut lines, expr) = self.quote_expr(expr)?; + output.append(&mut lines); + + // QUERY 'SELECT ', expr_result + let select_stmt = Statement::Query(Box::new(Query { + span: expr.span(), + with: None, + body: SetExpr::Select(Box::new(SelectStmt { + span: expr.span(), + hints: None, + distinct: false, + select_list: vec![SelectTarget::AliasedExpr { + expr: Box::new(expr.clone()), + alias: None, + }], + from: vec![], + selection: None, + group_by: None, + having: None, + window_list: None, + qualify: None, + })), + order_by: vec![], + limit: vec![], + offset: None, + ignore_result: false, + })); + let stmt = StatementTemplate::new(expr.span(), select_stmt); + let set_ref = self.declare_anonymous_set(expr.span(), "expr_result")?; + output.push(ScriptIR::Query { + stmt, + to_set: set_ref.clone(), + }); + + // ITER expr_result, expr_result_iter + let iter_ref = self.declare_anonymous_iter(expr.span(), "expr_result_iter")?; + output.push(ScriptIR::Iter { + set: set_ref, + to_iter: iter_ref.clone(), + }); + + // READ expr_result_iter, $0, to_var + output.push(ScriptIR::Read { + iter: iter_ref, + column: ColumnAccess::Position(0), + to_var, + }); + + Ok(output) + } + + fn compile_for_loop( + &mut self, + span: Span, + variable: &Identifier, + is_reverse: bool, + lower_bound: &Expr, + upper_bound: &Expr, + body: &[ScriptStatement], + label: &Option, + ) -> Result> { + let mut output = vec![]; + + self.push_scope(); + + let (continue_label, break_label) = match label { + Some(label) => self.declare_loop(label)?, + None => self.declare_anonymous_loop(span)?, + }; + + // QUERY 'SELECT * FROM generate_series(, , )', for_index_set + let (mut lines, lower_bound) = self.quote_expr(lower_bound)?; + output.append(&mut lines); + let (mut lines, upper_bound) = self.quote_expr(upper_bound)?; + output.append(&mut lines); + let (start, end, step) = if is_reverse { + (upper_bound, lower_bound, -1) + } else { + (lower_bound, upper_bound, 1) + }; + let select_stmt = Statement::Query(Box::new(Query { + span: variable.span, + with: None, + body: SetExpr::Select(Box::new(SelectStmt { + span: variable.span, + hints: None, + distinct: false, + select_list: vec![SelectTarget::StarColumns { + qualified: vec![Indirection::Star(None)], + column_filter: None, + }], + from: vec![TableReference::TableFunction { + span: variable.span, + lateral: false, + name: Identifier::from_name(variable.span, "generate_series"), + params: vec![start.clone(), end.clone(), Expr::Literal { + span: variable.span, + value: Literal::Decimal256 { + value: step.into(), + precision: 1, + scale: 0, + }, + }], + named_params: vec![], + alias: None, + }], + selection: None, + group_by: None, + having: None, + window_list: None, + qualify: None, + })), + order_by: vec![], + limit: vec![], + offset: None, + ignore_result: false, + })); + let stmt = StatementTemplate::new(variable.span, select_stmt); + let to_set = self.declare_anonymous_set(variable.span, "for_index_set")?; + output.push(ScriptIR::Query { + stmt, + to_set: to_set.clone(), + }); + + // ITER for_index_set, for_index_iter + let iter = self.declare_anonymous_iter(variable.span, "for_index_iter")?; + output.push(ScriptIR::Iter { + set: to_set, + to_iter: iter.clone(), + }); + + // Label LOOP + output.push(ScriptIR::Label { + label: continue_label.clone(), + }); + + // JUMP_IF_ENDED for_index_iter, LOOP_END + output.push(ScriptIR::JumpIfEnded { + iter: iter.clone(), + to_label: break_label.clone(), + }); + + // READ for_index_iter, $0, variable + let variable = self.declare_var(variable)?; + output.push(ScriptIR::Read { + iter: iter.clone(), + column: ColumnAccess::Position(0), + to_var: variable.clone(), + }); + + // + output.append(&mut self.compile(body)?); + + // NEXT for_index_iter + output.push(ScriptIR::Next { iter: iter.clone() }); + + // GOTO LOOP + output.push(ScriptIR::Goto { + to_label: continue_label.clone(), + }); + + // Label LOOP_END + output.push(ScriptIR::Label { label: break_label }); + + self.pop_scope(); + + Ok(output) + } + + fn compile_for_in( + &mut self, + span: Span, + variable: &Identifier, + resultset: &Identifier, + body: &[ScriptStatement], + label: &Option, + ) -> Result> { + let mut output = vec![]; + + self.push_scope(); + + let (continue_label, break_label) = match label { + Some(label) => self.declare_loop(label)?, + None => self.declare_anonymous_loop(span)?, + }; + + // ITER resultset, for_iter + let set = self.lookup_set(resultset)?; + let iter = self.declare_iter(variable.span, variable)?; + output.push(ScriptIR::Iter { + set, + to_iter: iter.clone(), + }); + + // Label LOOP + output.push(ScriptIR::Label { + label: continue_label.clone(), + }); + + // JUMP_IF_ENDED for_iter, LOOP_END + output.push(ScriptIR::JumpIfEnded { + iter: iter.clone(), + to_label: break_label.clone(), + }); + + // + output.append(&mut self.compile(body)?); + + // NEXT for_iter + output.push(ScriptIR::Next { iter: iter.clone() }); + + // GOTO LOOP + output.push(ScriptIR::Goto { + to_label: continue_label.clone(), + }); + + // Label LOOP_END + output.push(ScriptIR::Label { label: break_label }); + + self.pop_scope(); + + Ok(output) + } + + fn compile_while_loop( + &mut self, + span: Span, + condition: &Expr, + body: &[ScriptStatement], + label: &Option, + ) -> Result> { + let mut output = vec![]; + + self.push_scope(); + + let (continue_label, break_label) = match label { + Some(label) => self.declare_loop(label)?, + None => self.declare_anonymous_loop(span)?, + }; + + // Label LOOP + output.push(ScriptIR::Label { + label: continue_label.clone(), + }); + + // )> + // JUMP_IF_TRUE break_condition, LOOP_END + let break_condition = wrap_not(wrap_is_true(condition.clone())); + let break_condition_var = + self.declare_anonymous_var(break_condition.span(), "break_condition")?; + output.append(&mut self.compile_expr(&break_condition, break_condition_var.clone())?); + output.push(ScriptIR::JumpIfTrue { + condition: break_condition_var, + to_label: break_label.clone(), + }); + + // + output.append(&mut self.compile(body)?); + + // GOTO LOOP + output.push(ScriptIR::Goto { + to_label: continue_label.clone(), + }); + + // Label LOOP_END + output.push(ScriptIR::Label { label: break_label }); + + self.pop_scope(); + + Ok(output) + } + + fn compile_repeat_loop( + &mut self, + span: Span, + until_condition: &Expr, + body: &[ScriptStatement], + label: &Option, + ) -> Result> { + let mut output = vec![]; + + self.push_scope(); + + let (continue_label, break_label) = match label { + Some(label) => self.declare_loop(label)?, + None => self.declare_anonymous_loop(span)?, + }; + + // Label LOOP + output.push(ScriptIR::Label { + label: continue_label.clone(), + }); + + // + output.append(&mut self.compile(body)?); + + // + // JUMP_IF_TRUE break_condition, LOOP_END + let break_condition = wrap_is_true(until_condition.clone()); + let break_condition_var = + self.declare_anonymous_var(break_condition.span(), "break_condition")?; + output.append(&mut self.compile_expr(&break_condition, break_condition_var.clone())?); + output.push(ScriptIR::JumpIfTrue { + condition: break_condition_var, + to_label: break_label.clone(), + }); + + // GOTO LOOP + output.push(ScriptIR::Goto { + to_label: continue_label.clone(), + }); + + // Label LOOP_END + output.push(ScriptIR::Label { label: break_label }); + + self.pop_scope(); + + Ok(output) + } + + fn compile_loop( + &mut self, + span: Span, + body: &[ScriptStatement], + label: &Option, + ) -> Result> { + let mut output = vec![]; + + self.push_scope(); + + let (continue_label, break_label) = match label { + Some(label) => self.declare_loop(label)?, + None => self.declare_anonymous_loop(span)?, + }; + + // Label LOOP + output.push(ScriptIR::Label { + label: continue_label.clone(), + }); + + // + output.append(&mut self.compile(body)?); + + // GOTO LOOP + output.push(ScriptIR::Goto { + to_label: continue_label.clone(), + }); + + // Label LOOP_END + output.push(ScriptIR::Label { label: break_label }); + + self.pop_scope(); + + Ok(output) + } + + fn compile_if( + &mut self, + span: Span, + conditions: &[Expr], + results: &[Vec], + else_result: &Option>, + ) -> Result> { + let mut output = vec![]; + + self.push_scope(); + + let then_labels = conditions + .iter() + .map(|condition| { + LabelRef::new_internal(condition.span(), "IF_THEN", &mut self.ref_allocator) + }) + .collect::>(); + let end_label = LabelRef::new_internal(span, "IF_END", &mut self.ref_allocator); + + for (condition, then_label) in conditions.iter().zip(&then_labels) { + // + // JUMP_IF_TRUE condition, IF_THEN + let condition = wrap_is_true(condition.clone()); + let condition_var = self.declare_anonymous_var(condition.span(), "condition")?; + output.append(&mut self.compile_expr(&condition, condition_var.clone())?); + output.push(ScriptIR::JumpIfTrue { + condition: condition_var, + to_label: then_label.clone(), + }); + } + + if let Some(else_result) = else_result { + // + self.push_scope(); + output.append(&mut self.compile(else_result)?); + self.pop_scope(); + } + + // GOTO IF_END + output.push(ScriptIR::Goto { + to_label: end_label.clone(), + }); + + for (result, then_label) in results.iter().zip(&then_labels) { + // Label IF_THEN + output.push(ScriptIR::Label { + label: then_label.clone(), + }); + + // + self.push_scope(); + output.append(&mut self.compile(result)?); + self.pop_scope(); + + // GOTO IF_END + output.push(ScriptIR::Goto { + to_label: end_label.clone(), + }); + } + + // Label IF_END + output.push(ScriptIR::Label { + label: end_label.clone(), + }); + + self.pop_scope(); + + Ok(output) + } + + fn compile_case( + &mut self, + span: Span, + oprand: &Expr, + conditions: &[Expr], + results: &[Vec], + else_result: &Option>, + ) -> Result> { + let conditions = conditions + .iter() + .map(|condition| wrap_eq(condition.span(), oprand.clone(), condition.clone())) + .collect::>(); + self.compile_if(span, &conditions, results, else_result) + } + + fn compile_sql_statement( + &self, + span: Span, + stmt: &Statement, + to_set: SetRef, + ) -> Result> { + #[derive(VisitorMut)] + #[visitor(Expr(enter), Identifier(enter))] + struct QuoteVisitor<'a> { + compiler: &'a Compiler, + error: Option, + } + + impl QuoteVisitor<'_> { + fn enter_expr(&mut self, expr: &mut Expr) { + if let Expr::Hole { span, name } = expr { + let index = self + .compiler + .lookup_var(&Identifier::from_name(*span, name.clone())); + match index { + Ok(index) => { + *expr = Expr::Hole { + span: *span, + name: index.index.to_string(), + }; + } + Err(e) => { + self.error = Some(e.set_span(*span)); + } + } + } + } + + fn enter_identifier(&mut self, ident: &mut Identifier) { + if ident.is_hole { + let index = self.compiler.lookup_var(ident); + match index { + Ok(index) => { + *ident = Identifier::from_name(ident.span, index.to_string()); + ident.is_hole = true; + } + Err(e) => { + self.error = Some(e.set_span(ident.span)); + } + } + } + } + } + + let mut stmt = stmt.clone(); + let mut visitor = QuoteVisitor { + compiler: self, + error: None, + }; + stmt.drive_mut(&mut visitor); + + if let Some(e) = visitor.error { + return Err(e); + } + + // QUERY , to_set + let stmt = StatementTemplate::new(span, stmt); + let output = vec![ScriptIR::Query { stmt, to_set }]; + + Ok(output) + } + + fn push_scope(&mut self) { + self.scopes.push(Scope::default()); + } + + fn pop_scope(&mut self) { + self.scopes.pop().unwrap(); + } + + fn normalize_ident(&self, ident: &Identifier) -> RefName { + // TODO(andylokandy): use NameResolutionCtx + RefName(ident.name.clone()) + } + + fn declare_ref(&mut self, ident: &Identifier, item: RefItem) -> Result<()> { + let name = self.normalize_ident(ident); + for scope in self.scopes.iter().rev() { + if let Some(shadowed) = scope.items.get(&name) { + if !shadowed.is_same_kind(&item) { + return Err(ErrorCode::ScriptSemanticError(format!( + "`{name}` is already defined as a different kind of variable" + )) + .set_span(ident.span)); + } + break; + } + } + self.scopes.last_mut().unwrap().items.insert(name, item); + Ok(()) + } + + fn declare_anonymous_ref(&mut self, item: RefItem) -> Result<()> { + self.scopes.last_mut().unwrap().anonymous_items.push(item); + Ok(()) + } + + fn declare_var(&mut self, ident: &Identifier) -> Result { + let name = self.normalize_ident(ident); + let var = VarRef::new(ident.span, &name.0, &mut self.ref_allocator); + self.declare_ref(ident, RefItem::Var(var.clone()))?; + Ok(var) + } + + fn declare_anonymous_var(&mut self, span: Span, hint: &str) -> Result { + let var = VarRef::new_internal(span, hint, &mut self.ref_allocator); + self.declare_anonymous_ref(RefItem::Var(var.clone()))?; + Ok(var) + } + + fn declare_set(&mut self, ident: &Identifier) -> Result { + let name = self.normalize_ident(ident); + let set = SetRef::new(ident.span, &name.0, &mut self.ref_allocator); + self.declare_ref(ident, RefItem::Set(set.clone()))?; + Ok(set) + } + + fn declare_anonymous_set(&mut self, span: Span, hint: &str) -> Result { + let set = SetRef::new_internal(span, hint, &mut self.ref_allocator); + self.declare_anonymous_ref(RefItem::Set(set.clone()))?; + Ok(set) + } + + fn declare_iter(&mut self, span: Span, ident: &Identifier) -> Result { + let name = self.normalize_ident(ident); + let iter = IterRef::new(span, &name.0, &mut self.ref_allocator); + self.declare_ref(ident, RefItem::Iter(iter.clone()))?; + Ok(iter) + } + + fn declare_anonymous_iter(&mut self, span: Span, hint: &str) -> Result { + let iter = IterRef::new_internal(span, hint, &mut self.ref_allocator); + self.declare_anonymous_ref(RefItem::Iter(iter.clone()))?; + Ok(iter) + } + + fn declare_loop(&mut self, ident: &Identifier) -> Result<(LabelRef, LabelRef)> { + let name = self.normalize_ident(ident); + let continue_label = LabelRef::new( + ident.span, + &format!("{}_LOOP", &name.0), + &mut self.ref_allocator, + ); + let break_label = LabelRef::new( + ident.span, + &format!("{}_LOOP_END", &name.0), + &mut self.ref_allocator, + ); + self.declare_ref(ident, RefItem::Loop { + continue_label: continue_label.clone(), + break_label: break_label.clone(), + })?; + Ok((continue_label, break_label)) + } + + fn declare_anonymous_loop(&mut self, span: Span) -> Result<(LabelRef, LabelRef)> { + let continue_label = LabelRef::new_internal(span, "LOOP", &mut self.ref_allocator); + let break_label = LabelRef::new_internal(span, "LOOP_END", &mut self.ref_allocator); + self.declare_anonymous_ref(RefItem::Loop { + continue_label: continue_label.clone(), + break_label: break_label.clone(), + })?; + Ok((continue_label, break_label)) + } + + fn lookup_ref(&self, ident: &Identifier) -> Result { + let name = self.normalize_ident(ident); + for scope in self.scopes.iter().rev() { + if let Some(item) = scope.items.get(&name) { + return Ok(item.clone()); + } + } + Err(ErrorCode::ScriptSemanticError(format!("`{name}` is not defined")).set_span(ident.span)) + } + + fn lookup_var(&self, ident: &Identifier) -> Result { + let RefItem::Var(var) = self.lookup_ref(ident)? else { + let name = self.normalize_ident(ident); + return Err(ErrorCode::ScriptSemanticError(format!( + "`{name}` is not a scalar variable" + )) + .set_span(ident.span)); + }; + Ok(var) + } + + fn lookup_set(&self, ident: &Identifier) -> Result { + let RefItem::Set(set) = self.lookup_ref(ident)? else { + let name = self.normalize_ident(ident); + return Err( + ErrorCode::ScriptSemanticError(format!("`{name}` is not a set")) + .set_span(ident.span), + ); + }; + Ok(set) + } + + fn lookup_iter(&self, ident: &Identifier) -> Result { + let RefItem::Iter(iter) = self.lookup_ref(ident)? else { + let name = self.normalize_ident(ident); + return Err( + ErrorCode::ScriptSemanticError(format!("`{name}` is not a row variable")) + .set_span(ident.span), + ); + }; + Ok(iter) + } + + fn lookup_loop(&self, ident: &Identifier) -> Result<(LabelRef, LabelRef)> { + let RefItem::Loop { + continue_label, + break_label, + } = self.lookup_ref(ident)? + else { + let name = self.normalize_ident(ident); + return Err( + ErrorCode::ScriptSemanticError(format!("`{name}` is not a loop")) + .set_span(ident.span), + ); + }; + Ok((continue_label, break_label)) + } + + fn current_loop(&self, span: Span) -> Result<(LabelRef, LabelRef)> { + for scope in self.scopes.iter().rev() { + for item in scope.anonymous_items.iter().chain(scope.items.values()) { + if let RefItem::Loop { + continue_label, + break_label, + } = item + { + return Ok((continue_label.clone(), break_label.clone())); + } + } + } + Err(ErrorCode::ScriptSemanticError("not in a loop".to_string()).set_span(span)) + } + + fn quote_expr(&mut self, expr: &Expr) -> Result<(Vec, Expr)> { + #[derive(VisitorMut)] + #[visitor(Expr(enter), Identifier(enter))] + struct QuoteVisitor<'a> { + compiler: &'a mut Compiler, + output: Vec, + error: Option, + } + + impl QuoteVisitor<'_> { + fn enter_expr(&mut self, expr: &mut Expr) { + match expr { + Expr::ColumnRef { + span, + column: + ColumnRef { + database: None, + table: None, + column: ColumnID::Name(column), + }, + } => { + let index = self.compiler.lookup_var(column); + match index { + Ok(index) => { + *expr = Expr::Hole { + span: *span, + name: index.index.to_string(), + }; + } + Err(e) => { + self.error = Some(e.set_span(*span)); + } + } + } + Expr::ColumnRef { + span, + column: + ColumnRef { + database: None, + table: Some(iter), + column: ColumnID::Name(column), + }, + } => { + let res = try { + // READ , , to_var + let to_var = self + .compiler + .declare_anonymous_var(column.span, &format!("{iter}.{column}"))?; + let iter = self.compiler.lookup_iter(iter)?; + let column = + ColumnAccess::Name(self.compiler.normalize_ident(column).0); + self.output.push(ScriptIR::Read { + iter, + column, + to_var: to_var.clone(), + }); + + *expr = Expr::Hole { + span: *span, + name: to_var.index.to_string(), + }; + }; + + if let Err(err) = res { + self.error = Some(err); + } + } + Expr::Hole { span, .. } => { + self.error = Some( + ErrorCode::ScriptSemanticError( + "variable doesn't need to be quoted in this context, \ + try removing the colon" + .to_string(), + ) + .set_span(*span), + ); + } + _ => {} + } + } + + fn enter_identifier(&mut self, ident: &mut Identifier) { + if ident.is_hole { + self.error = Some( + ErrorCode::ScriptSemanticError( + "variable is not allowed in this context".to_string(), + ) + .set_span(ident.span), + ); + } + } + } + + let mut expr = expr.clone(); + let mut visitor = QuoteVisitor { + compiler: self, + output: vec![], + error: None, + }; + expr.drive_mut(&mut visitor); + + if let Some(err) = visitor.error { + return Err(err); + } + + Ok((visitor.output, expr)) + } +} + +#[derive(Debug, Default)] +struct Scope { + items: HashMap, + anonymous_items: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct RefName(String); + +impl Display for RefName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Debug, Clone)] +enum RefItem { + Var(VarRef), + Set(SetRef), + Iter(IterRef), + Loop { + continue_label: LabelRef, + break_label: LabelRef, + }, +} + +impl RefItem { + pub fn is_same_kind(&self, other: &RefItem) -> bool { + #[allow(clippy::match_like_matches_macro)] + match (self, other) { + (RefItem::Var(_), RefItem::Var(_)) => true, + (RefItem::Set(_), RefItem::Set(_)) => true, + (RefItem::Iter(_), RefItem::Iter(_)) => true, + (RefItem::Loop { .. }, RefItem::Loop { .. }) => true, + _ => false, + } + } +} + +fn wrap_eq(span: Span, lhs: Expr, rhs: Expr) -> Expr { + Expr::BinaryOp { + span, + op: BinaryOperator::Eq, + left: Box::new(lhs), + right: Box::new(rhs), + } +} + +fn wrap_not(expr: Expr) -> Expr { + Expr::UnaryOp { + span: expr.span(), + op: UnaryOperator::Not, + expr: Box::new(expr), + } +} + +fn wrap_is_true(expr: Expr) -> Expr { + Expr::FunctionCall { + span: expr.span(), + func: FunctionCall { + distinct: false, + name: Identifier::from_name(expr.span(), "is_true"), + args: vec![expr], + params: vec![], + window: None, + lambda: None, + }, + } +} diff --git a/src/query/script/src/executor.rs b/src/query/script/src/executor.rs new file mode 100644 index 000000000000..09c8de7c2917 --- /dev/null +++ b/src/query/script/src/executor.rs @@ -0,0 +1,225 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; + +use databend_common_ast::ast::Literal; +use databend_common_exception::ErrorCode; +use databend_common_exception::Result; + +use crate::ir::ColumnAccess; +use crate::ir::IterRef; +use crate::ir::LabelRef; +use crate::ir::ScriptIR; +use crate::ir::SetRef; +use crate::ir::VarRef; + +pub trait Client { + type Scalar: Clone; + type DataBlock: Clone; + + fn query(&self, query: &str) -> Result; + fn scalar_to_literal(&self, scalar: &Self::Scalar) -> Literal; + fn read_from_block( + &self, + block: &Self::DataBlock, + row: usize, + col: &ColumnAccess, + ) -> Self::Scalar; + fn block_len(&self, block: &Self::DataBlock) -> usize; + fn is_true(&self, scalar: &Self::Scalar) -> bool; +} + +#[derive(Debug, Clone)] +pub enum ReturnValue { + Var(C::Scalar), + Set(C::DataBlock), +} + +#[derive(Debug)] +struct Cursor { + set: SetRef, + row: usize, + len: usize, +} + +#[derive(Debug)] +pub struct Executor { + client: C, + code: Vec, + max_steps: usize, + vars: HashMap, + sets: HashMap, + iters: HashMap, + label_to_pc: HashMap, + return_value: Option>, + pc: usize, +} + +impl Executor { + pub fn load(client: C, code: Vec, max_steps: usize) -> Self { + assert!(!code.is_empty()); + + let mut label_to_pc = HashMap::new(); + for (pc, line) in code.iter().enumerate() { + if let ScriptIR::Label { label } = line { + label_to_pc.insert(label.clone(), pc); + } + } + + Executor { + client, + code, + max_steps, + vars: HashMap::new(), + sets: HashMap::new(), + iters: HashMap::new(), + label_to_pc, + return_value: None, + pc: 0, + } + } + + pub fn run(&mut self) -> Result>> { + for _ in 0..self.max_steps { + if self.pc >= self.code.len() { + return Ok(self.return_value.take()); + } + self.step()?; + } + + Err(ErrorCode::ScriptExecutionError(format!( + "Execution of script has exceeded the limit of {} steps, \ + which usually means you may have an infinite loop. Otherwise, \ + You can increase the limit with `set script_max_steps = 10000;`.", + self.max_steps + ))) + } + + fn step(&mut self) -> Result<()> { + let line = self + .code + .get(self.pc) + .ok_or_else(|| { + ErrorCode::ScriptExecutionError(format!("pc out of bounds: {}", self.pc)) + })? + .clone(); + match &line { + ScriptIR::Query { stmt, to_set } => { + let sql = stmt + .subst(|var| Ok(self.client.scalar_to_literal(self.get_var(&var)?)))? + .to_string(); + let block = self + .client + .query(&sql) + .map_err(|err| err.set_span(stmt.span))?; + self.sets.insert(to_set.clone(), block); + } + ScriptIR::Iter { set, to_iter } => { + let block = self.get_set(set)?; + let cursor = Cursor { + set: set.clone(), + row: 0, + len: self.client.block_len(block), + }; + self.iters.insert(to_iter.clone(), cursor); + } + ScriptIR::Read { + iter, + column, + to_var, + } => { + let cursor = self.get_iter(iter)?; + let block = self.get_set(&cursor.set)?; + let scalar = self.client.read_from_block(block, cursor.row, column); + self.vars.insert(to_var.clone(), scalar); + } + ScriptIR::Next { iter } => { + let cursor = self.get_iter_mut(iter)?; + assert!(cursor.row < cursor.len); + cursor.row += 1; + } + ScriptIR::Label { .. } => {} + ScriptIR::JumpIfEnded { iter, to_label } => { + let cursor = self.get_iter(iter)?; + if cursor.row >= cursor.len { + self.goto(to_label)?; + } + } + ScriptIR::JumpIfTrue { + condition, + to_label, + } => { + let scalar = self.get_var(condition)?; + if self.client.is_true(scalar) { + self.goto(to_label)?; + } + } + ScriptIR::Goto { to_label } => { + self.goto(to_label)?; + } + ScriptIR::Return => { + self.goto_end(); + } + ScriptIR::ReturnVar { var } => { + self.return_value = Some(ReturnValue::Var(self.get_var(var)?.clone())); + self.goto_end(); + } + ScriptIR::ReturnSet { set } => { + self.return_value = Some(ReturnValue::Set(self.get_set(set)?.clone())); + self.goto_end(); + } + } + + self.pc += 1; + + Ok(()) + } + + fn get_var(&self, var: &VarRef) -> Result<&C::Scalar> { + self.vars + .get(var) + .ok_or_else(|| ErrorCode::ScriptExecutionError(format!("unknown var: {var}"))) + } + + fn get_set(&self, set: &SetRef) -> Result<&C::DataBlock> { + self.sets + .get(set) + .ok_or_else(|| ErrorCode::ScriptExecutionError(format!("unknown set: {set}"))) + } + + fn get_iter(&self, iter: &IterRef) -> Result<&Cursor> { + self.iters + .get(iter) + .ok_or_else(|| ErrorCode::ScriptExecutionError(format!("unknown iter: {iter}"))) + } + + fn get_iter_mut(&mut self, iter: &IterRef) -> Result<&mut Cursor> { + self.iters + .get_mut(iter) + .ok_or_else(|| ErrorCode::ScriptExecutionError(format!("unknown iter: {iter}"))) + } + + fn goto(&mut self, label: &LabelRef) -> Result<()> { + self.pc = *self + .label_to_pc + .get(label) + .ok_or_else(|| ErrorCode::ScriptExecutionError(format!("unknown label: {label}")))?; + Ok(()) + } + + fn goto_end(&mut self) { + self.pc = self.code.len(); + } +} diff --git a/src/query/script/src/ir.rs b/src/query/script/src/ir.rs new file mode 100644 index 000000000000..e4013367dac3 --- /dev/null +++ b/src/query/script/src/ir.rs @@ -0,0 +1,258 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt; +use std::fmt::Display; +use std::hash::Hash; + +use databend_common_ast::ast::Expr; +use databend_common_ast::ast::Identifier; +use databend_common_ast::ast::Literal; +use databend_common_ast::ast::Statement; +use databend_common_exception::ErrorCode; +use databend_common_exception::Result; +use databend_common_exception::Span; +use derive_visitor::DriveMut; +use derive_visitor::VisitorMut; + +pub type VarRef = Ref<0>; +pub type SetRef = Ref<1>; +pub type IterRef = Ref<2>; +pub type LabelRef = Ref<3>; + +#[derive(Debug, Clone)] +pub struct Ref { + pub span: Span, + pub index: usize, + pub display_name: String, +} + +impl PartialEq for Ref { + fn eq(&self, other: &Self) -> bool { + self.index == other.index + } +} + +impl Eq for Ref {} + +impl Hash for Ref { + fn hash(&self, state: &mut H) { + self.index.hash(state); + } +} + +impl Display for Ref { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}({})", self.display_name, self.index) + } +} + +#[derive(Default)] +pub struct RefAllocator { + next_index: usize, +} + +impl Ref { + pub fn new(span: Span, name: &str, allocator: &mut RefAllocator) -> Self { + let index = allocator.next_index; + allocator.next_index += 1; + Ref { + span, + index, + display_name: name.to_string(), + } + } + + pub fn new_internal(span: Span, hint: &str, allocator: &mut RefAllocator) -> Self { + let index = allocator.next_index; + allocator.next_index += 1; + Ref { + span, + index, + display_name: format!("__{hint}{index}"), + } + } + + pub fn placeholder(index: usize) -> Self { + Ref { + span: None, + index, + display_name: format!(":{}", index), + } + } +} + +#[derive(Debug, Clone)] +pub enum ScriptIR { + /// Executes a SQL query and stores the result in a named result set. + Query { + stmt: StatementTemplate, + to_set: SetRef, + }, + /// Initializes an iterator for a given result set. + Iter { set: SetRef, to_iter: IterRef }, + /// Retrieves a cell value in the current row to a variable. + Read { + iter: IterRef, + column: ColumnAccess, + to_var: VarRef, + }, + /// Forward the iterator to the next line. + Next { iter: IterRef }, + /// Defines a label. + Label { label: LabelRef }, + /// Jumps to a specified label if the iterator has reached the end of the result set. + JumpIfEnded { iter: IterRef, to_label: LabelRef }, + /// Jumps to a specified label if the condition is true. + JumpIfTrue { + condition: VarRef, + to_label: LabelRef, + }, + /// Uncoditionally jumps to a specified label. + Goto { to_label: LabelRef }, + /// Returns from the script. + Return, + /// Returns a variable from the script. + ReturnVar { var: VarRef }, + /// Returns a result set from the script. + ReturnSet { set: SetRef }, +} + +impl Display for ScriptIR { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ScriptIR::Query { + stmt: query, + to_set, + } => write!(f, "QUERY {query}, {to_set}")?, + ScriptIR::Iter { set, to_iter } => write!(f, "ITER {set}, {to_iter}")?, + ScriptIR::Read { + iter, + column, + to_var, + } => write!(f, "READ {iter}, {column}, {to_var}")?, + ScriptIR::Next { iter } => { + write!(f, "NEXT {iter}")?; + } + ScriptIR::Label { label } => write!(f, "{label}:")?, + ScriptIR::JumpIfEnded { iter, to_label } => { + write!(f, "JUMP_IF_ENDED {iter}, {to_label}")? + } + ScriptIR::JumpIfTrue { + condition, + to_label, + } => write!(f, "JUMP_IF_TRUE {condition}, {to_label}")?, + ScriptIR::Goto { to_label } => write!(f, "GOTO {to_label}")?, + ScriptIR::Return => write!(f, "RETURN")?, + ScriptIR::ReturnVar { var } => write!(f, "RETURN {var}")?, + ScriptIR::ReturnSet { set } => write!(f, "RETURN {set}")?, + }; + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub enum ColumnAccess { + Position(usize), + Name(String), +} + +impl Display for ColumnAccess { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ColumnAccess::Position(index) => write!(f, "${}", index), + ColumnAccess::Name(name) => write!(f, "\"{}\"", name), + } + } +} + +impl Display for StatementTemplate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.stmt) + } +} + +#[derive(Debug, Clone)] +pub struct StatementTemplate { + pub span: Span, + pub stmt: Statement, +} + +impl StatementTemplate { + pub fn new(span: Span, stmt: Statement) -> Self { + StatementTemplate { span, stmt } + } + + pub fn subst(&self, lookup_var: impl Fn(VarRef) -> Result) -> Result { + #[derive(VisitorMut)] + #[visitor(Expr(enter), Identifier(enter))] + struct SubstVisitor<'a> { + lookup_var: &'a dyn Fn(VarRef) -> Result, + error: Option, + } + + impl SubstVisitor<'_> { + fn enter_expr(&mut self, expr: &mut Expr) { + if let Expr::Hole { span, name } = expr { + let index = name.parse::().unwrap(); + let value = (self.lookup_var)(VarRef::placeholder(index)); + match value { + Ok(value) => { + *expr = Expr::Literal { span: *span, value }; + } + Err(e) => { + self.error = Some(e.set_span(*span)); + } + } + } + } + + fn enter_identifier(&mut self, ident: &mut Identifier) { + if ident.is_hole { + let index = ident.name.parse::().unwrap(); + let value = (self.lookup_var)(VarRef::placeholder(index)); + match value { + Ok(Literal::String(name)) => { + *ident = Identifier::from_name(ident.span, name); + } + Ok(value) => { + self.error = Some( + ErrorCode::ScriptSemanticError(format!( + "expected string literal, got {value}" + )) + .set_span(ident.span), + ); + } + Err(e) => { + self.error = Some(e.set_span(ident.span)); + } + } + } + } + } + + let mut stmt = self.stmt.clone(); + let mut visitor = SubstVisitor { + lookup_var: &lookup_var, + error: None, + }; + stmt.drive_mut(&mut visitor); + + if let Some(e) = visitor.error { + return Err(e); + } + + Ok(stmt) + } +} diff --git a/src/query/script/src/lib.rs b/src/query/script/src/lib.rs new file mode 100644 index 000000000000..281417981b1f --- /dev/null +++ b/src/query/script/src/lib.rs @@ -0,0 +1,25 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![feature(try_blocks)] + +pub mod ir; + +mod compiler; +pub use compiler::compile; + +mod executor; +pub use executor::Client; +pub use executor::Executor; +pub use executor::ReturnValue; diff --git a/src/query/script/tests/it/main.rs b/src/query/script/tests/it/main.rs new file mode 100644 index 000000000000..8070300fed69 --- /dev/null +++ b/src/query/script/tests/it/main.rs @@ -0,0 +1,644 @@ +// Copyright 2022 Datafuse Labs. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![feature(try_blocks)] + +use std::collections::HashMap; +use std::fmt; +use std::fmt::Display; +use std::fmt::Formatter; +use std::io::Write; +use std::sync::Arc; +use std::sync::Mutex; + +use databend_common_ast::ast::Literal; +use databend_common_ast::parser::run_parser; +use databend_common_ast::parser::script::script_stmts; +use databend_common_ast::parser::tokenize_sql; +use databend_common_ast::parser::Dialect; +use databend_common_ast::parser::ParseMode; +use databend_common_exception::Result; +use databend_common_script::compile; +use databend_common_script::ir::ColumnAccess; +use databend_common_script::Client; +use databend_common_script::Executor; +use goldenfile::Mint; + +fn run_script(file: &mut dyn Write, src: &str) { + let src = unindent::unindent(src); + let src = src.trim(); + + let res: Result<_> = try { + let tokens = tokenize_sql(src).unwrap(); + let ast = run_parser( + &tokens, + Dialect::PostgreSQL, + ParseMode::Template, + false, + script_stmts, + )?; + let ir = compile(&ast)?; + let client = mock_client(); + let query_log = client.query_log.clone(); + let mut executor = Executor::load(client, ir.clone(), 1000); + let result = executor.run()?; + + (ir, query_log, result) + }; + + match res { + Ok((ir, query_log, result)) => { + writeln!(file, "---------- Input ----------").unwrap(); + writeln!(file, "{}", src).unwrap(); + writeln!(file, "---------- IR -------------").unwrap(); + for line in ir { + writeln!(file, "{}", line).unwrap(); + } + writeln!(file, "---------- QUERY ---------").unwrap(); + for (query, block) in query_log.lock().unwrap().iter() { + writeln!(file, "QUERY: {}", query).unwrap(); + writeln!(file, "BLOCK: {}", block).unwrap(); + } + writeln!(file, "---------- Output ---------").unwrap(); + writeln!(file, "{:?}", result).unwrap(); + writeln!(file, "\n").unwrap(); + } + Err(err) => { + let report = err.display_with_sql(src).message().trim().to_string(); + writeln!(file, "---------- Input ----------").unwrap(); + writeln!(file, "{}", src).unwrap(); + writeln!(file, "---------- Output ----------").unwrap(); + writeln!(file, "{}", report).unwrap(); + writeln!(file, "\n").unwrap(); + } + } +} + +#[test] +fn test_script() { + let mut mint = Mint::new("tests/it/testdata"); + let file = &mut mint.new_goldenfile("script.txt").unwrap(); + + run_script( + file, + r#" + CREATE TABLE t1 (a INT, b INT, c INT); + INSERT INTO t1 VALUES (1, 2, 3); + DROP TABLE t1; + "#, + ); + run_script( + file, + r#" + LET x := 1; + LET y := x + 1; + LET z RESULTSET := SELECT :y + 1; + "#, + ); + run_script( + file, + r#" + RETURN; + "#, + ); + run_script( + file, + r#" + LET x := 1; + LET sum := 0; + FOR x IN x TO x + 2 DO + sum := sum + x; + END FOR; + RETURN sum; + "#, + ); + run_script( + file, + r#" + LET sum := 0; + FOR x IN REVERSE -1 TO 1 DO + sum := sum + x; + END FOR; + RETURN sum; + "#, + ); + run_script( + file, + r#" + LET x RESULTSET := SELECT * FROM numbers(3); + LET sum := 0; + FOR row IN x DO + sum := sum + row.number; + END FOR; + RETURN sum; + "#, + ); + run_script( + file, + r#" + LET x := 1; + WHILE x < 3 DO + x := x + 1; + END WHILE; + RETURN x; + "#, + ); + run_script( + file, + r#" + LET x := 1; + REPEAT + x := x + 1; + UNTIL x = 3 + END REPEAT; + RETURN x; + "#, + ); + run_script( + file, + r#" + LOOP + LET x := 0; + LOOP + LET y := x; + IF y < 2 THEN + x := x + 1; + CONTINUE; + ELSE + BREAK loop_label; + END IF; + END LOOP; + END LOOP loop_label; + "#, + ); + run_script( + file, + r#" + LET x := 0; + LOOP + LET x := 1; + BREAK; + END LOOP; + RETURN x; + "#, + ); + run_script( + file, + r#" + LET x := 1; + CASE x + WHEN 1 THEN RETURN 'ONE'; + WHEN 2 THEN RETURN 'TWO'; + ELSE RETURN 'OTHER'; + END CASE; + "#, + ); + run_script( + file, + r#" + LET x := 2; + CASE x + WHEN 1 THEN RETURN 'ONE'; + WHEN 2 THEN RETURN 'TWO'; + ELSE RETURN 'OTHER'; + END CASE; + "#, + ); + run_script( + file, + r#" + LET x := 3; + CASE x + WHEN 1 THEN RETURN 'ONE'; + WHEN 2 THEN RETURN 'TWO'; + ELSE RETURN 'OTHER'; + END CASE; + "#, + ); + run_script( + file, + r#" + LET x := 1; + CASE + WHEN x = 1 THEN RETURN 'ONE'; + WHEN x = 2 THEN RETURN 'TWO'; + ELSE RETURN 'OTHER'; + END CASE; + "#, + ); + run_script( + file, + r#" + LET x := 2; + CASE + WHEN x = 1 THEN RETURN 'ONE'; + WHEN x = 2 THEN RETURN 'TWO'; + ELSE RETURN 'OTHER'; + END CASE; + "#, + ); + run_script( + file, + r#" + LET x := 3; + CASE + WHEN x = 1 THEN RETURN 'ONE'; + WHEN x = 2 THEN RETURN 'TWO'; + ELSE RETURN 'OTHER'; + END CASE; + "#, + ); +} + +#[test] +fn test_script_error() { + let mut mint = Mint::new("tests/it/testdata"); + let file = &mut mint.new_goldenfile("script-error.txt").unwrap(); + + run_script( + file, + r#" + LET x := y + 1; + "#, + ); + run_script( + file, + r#" + LET x := 1; + LET x RESULTSET := SELECT 1; + "#, + ); + run_script( + file, + r#" + LET x RESULTSET := SELECT 1; + LET x := 1; + "#, + ); + run_script( + file, + r#" + LET x RESULTSET := SELECT 1; + LET y := x; + "#, + ); + run_script( + file, + r#" + LET x := 1; + LET y := x.a; + "#, + ); + run_script( + file, + r#" + LET x := 'min'; + LET y := IDENTIFIER(:x)([1,2]); + "#, + ); + run_script( + file, + r#" + LET x := 1; + LET y := :x + 1; + "#, + ); + run_script( + file, + r#" + LET x := 1; + FOR row IN x DO + BREAK; + END FOR; + "#, + ); + run_script( + file, + r#" + BREAK; + "#, + ); + run_script( + file, + r#" + CONTINUE; + "#, + ); + run_script( + file, + r#" + LOOP + BREAK foo; + END LOOP bar; + "#, + ); + run_script( + file, + r#" + LOOP + CONTINUE foo; + END LOOP bar; + "#, + ); + run_script( + file, + r#" + LOOP + CONTINUE; + END LOOP; + "#, + ); +} + +fn mock_client() -> MockClient { + MockClient::new() + .response_when( + "SELECT * FROM numbers(3)", + MockBlock::named(vec!["number"], vec![ + vec![Literal::UInt64(0)], + vec![Literal::UInt64(1)], + vec![Literal::UInt64(2)], + ]), + ) + .response_when( + "CREATE TABLE t1 (a Int32, b Int32, c Int32)", + MockBlock::empty(), + ) + .response_when("INSERT INTO t1 VALUES (1, 2, 3)", MockBlock::empty()) + .response_when("DROP TABLE t1", MockBlock::empty()) + .response_when( + "SELECT * FROM generate_series(1, 1 + 2, 1)", + MockBlock::unnamed(vec![ + vec![Literal::UInt64(1)], + vec![Literal::UInt64(2)], + vec![Literal::UInt64(3)], + ]), + ) + .response_when( + "SELECT * FROM generate_series(1, - 1, -1)", + MockBlock::unnamed(vec![ + vec![Literal::UInt64(1)], + vec![Literal::UInt64(0)], + vec![Literal::Decimal256 { + value: (-1).into(), + precision: 1, + scale: 0, + }], + ]), + ) + .response_when( + "SELECT 0", + MockBlock::unnamed(vec![vec![Literal::UInt64(0)]]), + ) + .response_when( + "SELECT 1", + MockBlock::unnamed(vec![vec![Literal::UInt64(1)]]), + ) + .response_when( + "SELECT 2", + MockBlock::unnamed(vec![vec![Literal::UInt64(2)]]), + ) + .response_when( + "SELECT 3", + MockBlock::unnamed(vec![vec![Literal::UInt64(3)]]), + ) + .response_when( + "SELECT 6", + MockBlock::unnamed(vec![vec![Literal::UInt64(6)]]), + ) + .response_when( + "SELECT 'ONE'", + MockBlock::unnamed(vec![vec![Literal::String("ONE".to_string())]]), + ) + .response_when( + "SELECT 'TWO'", + MockBlock::unnamed(vec![vec![Literal::String("TWO".to_string())]]), + ) + .response_when( + "SELECT 'OTHER'", + MockBlock::unnamed(vec![vec![Literal::String("OTHER".to_string())]]), + ) + .response_when( + "SELECT 0 + 0", + MockBlock::unnamed(vec![vec![Literal::UInt64(0)]]), + ) + .response_when( + "SELECT 0 + 1", + MockBlock::unnamed(vec![vec![Literal::UInt64(1)]]), + ) + .response_when( + "SELECT 1 + 0", + MockBlock::unnamed(vec![vec![Literal::UInt64(1)]]), + ) + .response_when( + "SELECT 1 + -1", + MockBlock::unnamed(vec![vec![Literal::UInt64(0)]]), + ) + .response_when( + "SELECT 1 + 1", + MockBlock::unnamed(vec![vec![Literal::UInt64(2)]]), + ) + .response_when( + "SELECT 1 + 2", + MockBlock::unnamed(vec![vec![Literal::UInt64(3)]]), + ) + .response_when( + "SELECT 2 + 1", + MockBlock::unnamed(vec![vec![Literal::UInt64(3)]]), + ) + .response_when( + "SELECT 3 + 3", + MockBlock::unnamed(vec![vec![Literal::UInt64(6)]]), + ) + .response_when( + "SELECT 3 * 3", + MockBlock::unnamed(vec![vec![Literal::UInt64(9)]]), + ) + .response_when( + "SELECT is_true(0 < 2)", + MockBlock::unnamed(vec![vec![Literal::Boolean(true)]]), + ) + .response_when( + "SELECT is_true(1 < 2)", + MockBlock::unnamed(vec![vec![Literal::Boolean(true)]]), + ) + .response_when( + "SELECT is_true(2 < 2)", + MockBlock::unnamed(vec![vec![Literal::Boolean(false)]]), + ) + .response_when( + "SELECT is_true(1 = 1)", + MockBlock::unnamed(vec![vec![Literal::Boolean(true)]]), + ) + .response_when( + "SELECT is_true(2 = 1)", + MockBlock::unnamed(vec![vec![Literal::Boolean(false)]]), + ) + .response_when( + "SELECT is_true(2 = 2)", + MockBlock::unnamed(vec![vec![Literal::Boolean(true)]]), + ) + .response_when( + "SELECT is_true(2 = 3)", + MockBlock::unnamed(vec![vec![Literal::Boolean(false)]]), + ) + .response_when( + "SELECT is_true(3 = 1)", + MockBlock::unnamed(vec![vec![Literal::Boolean(false)]]), + ) + .response_when( + "SELECT is_true(3 = 2)", + MockBlock::unnamed(vec![vec![Literal::Boolean(false)]]), + ) + .response_when( + "SELECT is_true(3 = 3)", + MockBlock::unnamed(vec![vec![Literal::Boolean(true)]]), + ) + .response_when( + "SELECT NOT is_true(1 < 3)", + MockBlock::unnamed(vec![vec![Literal::Boolean(false)]]), + ) + .response_when( + "SELECT NOT is_true(2 < 3)", + MockBlock::unnamed(vec![vec![Literal::Boolean(false)]]), + ) + .response_when( + "SELECT NOT is_true(3 < 3)", + MockBlock::unnamed(vec![vec![Literal::Boolean(true)]]), + ) +} + +#[derive(Debug, Clone)] +struct MockClient { + responses: HashMap, + query_log: Arc>>, +} + +impl MockClient { + pub fn new() -> Self { + MockClient { + responses: HashMap::new(), + query_log: Arc::new(Mutex::new(vec![])), + } + } + + pub fn response_when(mut self, query: &str, block: MockBlock) -> Self { + self.responses.insert(query.to_string(), block); + self + } +} + +impl Client for MockClient { + type Scalar = Literal; + type DataBlock = MockBlock; + + fn query(&self, query: &str) -> Result { + match self.responses.get(query) { + Some(block) => { + self.query_log + .lock() + .unwrap() + .push((query.to_string(), block.clone())); + Ok(block.clone()) + } + None => panic!("response to query is not defined: {query}"), + } + } + + fn scalar_to_literal(&self, scalar: &Self::Scalar) -> Literal { + scalar.clone() + } + + fn read_from_block( + &self, + block: &Self::DataBlock, + row: usize, + col: &ColumnAccess, + ) -> Self::Scalar { + match col { + ColumnAccess::Position(col) => block.data[row][*col].clone(), + ColumnAccess::Name(name) => { + let col = block.column_names.iter().position(|x| x == name).unwrap(); + block.data[row][col].clone() + } + } + } + + fn block_len(&self, block: &Self::DataBlock) -> usize { + block.data.len() + } + + fn is_true(&self, scalar: &Self::Scalar) -> bool { + *scalar == Literal::Boolean(true) + } +} + +#[derive(Debug, Clone)] +struct MockBlock { + column_names: Vec, + data: Vec>, +} + +impl MockBlock { + pub fn empty() -> Self { + MockBlock { + column_names: vec![], + data: vec![], + } + } + + pub fn unnamed(data: Vec>) -> Self { + MockBlock { + column_names: (0..data[0].len()).map(|x| format!("${x}")).collect(), + data, + } + } + + pub fn named( + column_names: impl IntoIterator, + data: Vec>, + ) -> Self { + MockBlock { + column_names: column_names.into_iter().map(|x| x.to_string()).collect(), + data, + } + } +} + +impl Display for MockBlock { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "(")?; + for (i, name) in self.column_names.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", name)?; + } + write!(f, "): ")?; + for (i, row) in self.data.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "(")?; + for (j, cell) in row.iter().enumerate() { + if j > 0 { + write!(f, ", ")?; + } + write!(f, "{}", cell)?; + } + write!(f, ")")?; + } + Ok(()) + } +} diff --git a/src/query/script/tests/it/testdata/script-error.txt b/src/query/script/tests/it/testdata/script-error.txt new file mode 100644 index 000000000000..0afd0cf1e6ee --- /dev/null +++ b/src/query/script/tests/it/testdata/script-error.txt @@ -0,0 +1,150 @@ +---------- Input ---------- +LET x := y + 1; +---------- Output ---------- +error: + --> SQL:1:10 + | +1 | LET x := y + 1; + | ^ `y` is not defined + + +---------- Input ---------- +LET x := 1; +LET x RESULTSET := SELECT 1; +---------- Output ---------- +error: + --> SQL:2:5 + | +1 | LET x := 1; +2 | LET x RESULTSET := SELECT 1; + | ^ `x` is already defined as a different kind of variable + + +---------- Input ---------- +LET x RESULTSET := SELECT 1; +LET x := 1; +---------- Output ---------- +error: + --> SQL:2:5 + | +1 | LET x RESULTSET := SELECT 1; +2 | LET x := 1; + | ^ `x` is already defined as a different kind of variable + + +---------- Input ---------- +LET x RESULTSET := SELECT 1; +LET y := x; +---------- Output ---------- +error: + --> SQL:2:10 + | +1 | LET x RESULTSET := SELECT 1; +2 | LET y := x; + | ^ `x` is not a scalar variable + + +---------- Input ---------- +LET x := 1; +LET y := x.a; +---------- Output ---------- +error: + --> SQL:2:10 + | +1 | LET x := 1; +2 | LET y := x.a; + | ^ `x` is not a row variable + + +---------- Input ---------- +LET x := 'min'; +LET y := IDENTIFIER(:x)([1,2]); +---------- Output ---------- +error: + --> SQL:2:10 + | +1 | LET x := 'min'; +2 | LET y := IDENTIFIER(:x)([1,2]); + | ^^^^^^^^^^^^^^ variable is not allowed in this context + + +---------- Input ---------- +LET x := 1; +LET y := :x + 1; +---------- Output ---------- +error: + --> SQL:2:10 + | +1 | LET x := 1; +2 | LET y := :x + 1; + | ^^ variable doesn't need to be quoted in this context, try removing the colon + + +---------- Input ---------- +LET x := 1; +FOR row IN x DO + BREAK; +END FOR; +---------- Output ---------- +error: + --> SQL:2:12 + | +1 | LET x := 1; +2 | FOR row IN x DO + | ^ `x` is not a set + + +---------- Input ---------- +BREAK; +---------- Output ---------- +error: + --> SQL:1:1 + | +1 | BREAK; + | ^^^^^ not in a loop + + +---------- Input ---------- +CONTINUE; +---------- Output ---------- +error: + --> SQL:1:1 + | +1 | CONTINUE; + | ^^^^^^^^ not in a loop + + +---------- Input ---------- +LOOP + BREAK foo; +END LOOP bar; +---------- Output ---------- +error: + --> SQL:2:11 + | +1 | LOOP +2 | BREAK foo; + | ^^^ `foo` is not defined + + +---------- Input ---------- +LOOP + CONTINUE foo; +END LOOP bar; +---------- Output ---------- +error: + --> SQL:2:14 + | +1 | LOOP +2 | CONTINUE foo; + | ^^^ `foo` is not defined + + +---------- Input ---------- +LOOP + CONTINUE; +END LOOP; +---------- Output ---------- +Execution of script has exceeded the limit of 1000 steps, which usually means you may have an infinite loop. Otherwise, You can increase the limit with `set script_max_steps = 10000;`. + + diff --git a/src/query/script/tests/it/testdata/script.txt b/src/query/script/tests/it/testdata/script.txt new file mode 100644 index 000000000000..d93a46dcc172 --- /dev/null +++ b/src/query/script/tests/it/testdata/script.txt @@ -0,0 +1,660 @@ +---------- Input ---------- +CREATE TABLE t1 (a INT, b INT, c INT); +INSERT INTO t1 VALUES (1, 2, 3); +DROP TABLE t1; +---------- IR ------------- +QUERY CREATE TABLE t1 (a Int32, b Int32, c Int32), __unused_result0(0) +QUERY INSERT INTO t1 VALUES (1, 2, 3), __unused_result1(1) +QUERY DROP TABLE t1, __unused_result2(2) +---------- QUERY --------- +QUERY: CREATE TABLE t1 (a Int32, b Int32, c Int32) +BLOCK: (): +QUERY: INSERT INTO t1 VALUES (1, 2, 3) +BLOCK: (): +QUERY: DROP TABLE t1 +BLOCK: (): +---------- Output --------- +None + + +---------- Input ---------- +LET x := 1; +LET y := x + 1; +LET z RESULTSET := SELECT :y + 1; +---------- IR ------------- +QUERY SELECT 1, __expr_result1(1) +ITER __expr_result1(1), __expr_result_iter2(2) +READ __expr_result_iter2(2), $0, x(0) +QUERY SELECT :0 + 1, __expr_result4(4) +ITER __expr_result4(4), __expr_result_iter5(5) +READ __expr_result_iter5(5), $0, y(3) +QUERY SELECT :3 + 1, z(6) +---------- QUERY --------- +QUERY: SELECT 1 +BLOCK: ($0): (1) +QUERY: SELECT 1 + 1 +BLOCK: ($0): (2) +QUERY: SELECT 2 + 1 +BLOCK: ($0): (3) +---------- Output --------- +None + + +---------- Input ---------- +RETURN; +---------- IR ------------- +RETURN +---------- QUERY --------- +---------- Output --------- +None + + +---------- Input ---------- +LET x := 1; +LET sum := 0; +FOR x IN x TO x + 2 DO + sum := sum + x; +END FOR; +RETURN sum; +---------- IR ------------- +QUERY SELECT 1, __expr_result1(1) +ITER __expr_result1(1), __expr_result_iter2(2) +READ __expr_result_iter2(2), $0, x(0) +QUERY SELECT 0, __expr_result4(4) +ITER __expr_result4(4), __expr_result_iter5(5) +READ __expr_result_iter5(5), $0, sum(3) +QUERY SELECT * FROM generate_series(:0, :0 + 2, 1), __for_index_set8(8) +ITER __for_index_set8(8), __for_index_iter9(9) +__LOOP6(6): +JUMP_IF_ENDED __for_index_iter9(9), __LOOP_END7(7) +READ __for_index_iter9(9), $0, x(10) +QUERY SELECT :3 + :10, __expr_result11(11) +ITER __expr_result11(11), __expr_result_iter12(12) +READ __expr_result_iter12(12), $0, sum(3) +NEXT __for_index_iter9(9) +GOTO __LOOP6(6) +__LOOP_END7(7): +QUERY SELECT :3, __expr_result14(14) +ITER __expr_result14(14), __expr_result_iter15(15) +READ __expr_result_iter15(15), $0, __return_val13(13) +RETURN __return_val13(13) +---------- QUERY --------- +QUERY: SELECT 1 +BLOCK: ($0): (1) +QUERY: SELECT 0 +BLOCK: ($0): (0) +QUERY: SELECT * FROM generate_series(1, 1 + 2, 1) +BLOCK: ($0): (1), (2), (3) +QUERY: SELECT 0 + 1 +BLOCK: ($0): (1) +QUERY: SELECT 1 + 2 +BLOCK: ($0): (3) +QUERY: SELECT 3 + 3 +BLOCK: ($0): (6) +QUERY: SELECT 6 +BLOCK: ($0): (6) +---------- Output --------- +Some(Var(UInt64(6))) + + +---------- Input ---------- +LET sum := 0; +FOR x IN REVERSE -1 TO 1 DO + sum := sum + x; +END FOR; +RETURN sum; +---------- IR ------------- +QUERY SELECT 0, __expr_result1(1) +ITER __expr_result1(1), __expr_result_iter2(2) +READ __expr_result_iter2(2), $0, sum(0) +QUERY SELECT * FROM generate_series(1, - 1, -1), __for_index_set5(5) +ITER __for_index_set5(5), __for_index_iter6(6) +__LOOP3(3): +JUMP_IF_ENDED __for_index_iter6(6), __LOOP_END4(4) +READ __for_index_iter6(6), $0, x(7) +QUERY SELECT :0 + :7, __expr_result8(8) +ITER __expr_result8(8), __expr_result_iter9(9) +READ __expr_result_iter9(9), $0, sum(0) +NEXT __for_index_iter6(6) +GOTO __LOOP3(3) +__LOOP_END4(4): +QUERY SELECT :0, __expr_result11(11) +ITER __expr_result11(11), __expr_result_iter12(12) +READ __expr_result_iter12(12), $0, __return_val10(10) +RETURN __return_val10(10) +---------- QUERY --------- +QUERY: SELECT 0 +BLOCK: ($0): (0) +QUERY: SELECT * FROM generate_series(1, - 1, -1) +BLOCK: ($0): (1), (0), (-1) +QUERY: SELECT 0 + 1 +BLOCK: ($0): (1) +QUERY: SELECT 1 + 0 +BLOCK: ($0): (1) +QUERY: SELECT 1 + -1 +BLOCK: ($0): (0) +QUERY: SELECT 0 +BLOCK: ($0): (0) +---------- Output --------- +Some(Var(UInt64(0))) + + +---------- Input ---------- +LET x RESULTSET := SELECT * FROM numbers(3); +LET sum := 0; +FOR row IN x DO + sum := sum + row.number; +END FOR; +RETURN sum; +---------- IR ------------- +QUERY SELECT * FROM numbers(3), x(0) +QUERY SELECT 0, __expr_result2(2) +ITER __expr_result2(2), __expr_result_iter3(3) +READ __expr_result_iter3(3), $0, sum(1) +ITER x(0), row(6) +__LOOP4(4): +JUMP_IF_ENDED row(6), __LOOP_END5(5) +READ row(6), "number", __row.number7(7) +QUERY SELECT :1 + :7, __expr_result8(8) +ITER __expr_result8(8), __expr_result_iter9(9) +READ __expr_result_iter9(9), $0, sum(1) +NEXT row(6) +GOTO __LOOP4(4) +__LOOP_END5(5): +QUERY SELECT :1, __expr_result11(11) +ITER __expr_result11(11), __expr_result_iter12(12) +READ __expr_result_iter12(12), $0, __return_val10(10) +RETURN __return_val10(10) +---------- QUERY --------- +QUERY: SELECT * FROM numbers(3) +BLOCK: (number): (0), (1), (2) +QUERY: SELECT 0 +BLOCK: ($0): (0) +QUERY: SELECT 0 + 0 +BLOCK: ($0): (0) +QUERY: SELECT 0 + 1 +BLOCK: ($0): (1) +QUERY: SELECT 1 + 2 +BLOCK: ($0): (3) +QUERY: SELECT 3 +BLOCK: ($0): (3) +---------- Output --------- +Some(Var(UInt64(3))) + + +---------- Input ---------- +LET x := 1; +WHILE x < 3 DO + x := x + 1; +END WHILE; +RETURN x; +---------- IR ------------- +QUERY SELECT 1, __expr_result1(1) +ITER __expr_result1(1), __expr_result_iter2(2) +READ __expr_result_iter2(2), $0, x(0) +__LOOP3(3): +QUERY SELECT NOT is_true(:0 < 3), __expr_result6(6) +ITER __expr_result6(6), __expr_result_iter7(7) +READ __expr_result_iter7(7), $0, __break_condition5(5) +JUMP_IF_TRUE __break_condition5(5), __LOOP_END4(4) +QUERY SELECT :0 + 1, __expr_result8(8) +ITER __expr_result8(8), __expr_result_iter9(9) +READ __expr_result_iter9(9), $0, x(0) +GOTO __LOOP3(3) +__LOOP_END4(4): +QUERY SELECT :0, __expr_result11(11) +ITER __expr_result11(11), __expr_result_iter12(12) +READ __expr_result_iter12(12), $0, __return_val10(10) +RETURN __return_val10(10) +---------- QUERY --------- +QUERY: SELECT 1 +BLOCK: ($0): (1) +QUERY: SELECT NOT is_true(1 < 3) +BLOCK: ($0): (FALSE) +QUERY: SELECT 1 + 1 +BLOCK: ($0): (2) +QUERY: SELECT NOT is_true(2 < 3) +BLOCK: ($0): (FALSE) +QUERY: SELECT 2 + 1 +BLOCK: ($0): (3) +QUERY: SELECT NOT is_true(3 < 3) +BLOCK: ($0): (TRUE) +QUERY: SELECT 3 +BLOCK: ($0): (3) +---------- Output --------- +Some(Var(UInt64(3))) + + +---------- Input ---------- +LET x := 1; +REPEAT + x := x + 1; +UNTIL x = 3 +END REPEAT; +RETURN x; +---------- IR ------------- +QUERY SELECT 1, __expr_result1(1) +ITER __expr_result1(1), __expr_result_iter2(2) +READ __expr_result_iter2(2), $0, x(0) +__LOOP3(3): +QUERY SELECT :0 + 1, __expr_result5(5) +ITER __expr_result5(5), __expr_result_iter6(6) +READ __expr_result_iter6(6), $0, x(0) +QUERY SELECT is_true(:0 = 3), __expr_result8(8) +ITER __expr_result8(8), __expr_result_iter9(9) +READ __expr_result_iter9(9), $0, __break_condition7(7) +JUMP_IF_TRUE __break_condition7(7), __LOOP_END4(4) +GOTO __LOOP3(3) +__LOOP_END4(4): +QUERY SELECT :0, __expr_result11(11) +ITER __expr_result11(11), __expr_result_iter12(12) +READ __expr_result_iter12(12), $0, __return_val10(10) +RETURN __return_val10(10) +---------- QUERY --------- +QUERY: SELECT 1 +BLOCK: ($0): (1) +QUERY: SELECT 1 + 1 +BLOCK: ($0): (2) +QUERY: SELECT is_true(2 = 3) +BLOCK: ($0): (FALSE) +QUERY: SELECT 2 + 1 +BLOCK: ($0): (3) +QUERY: SELECT is_true(3 = 3) +BLOCK: ($0): (TRUE) +QUERY: SELECT 3 +BLOCK: ($0): (3) +---------- Output --------- +Some(Var(UInt64(3))) + + +---------- Input ---------- +LOOP + LET x := 0; + LOOP + LET y := x; + IF y < 2 THEN + x := x + 1; + CONTINUE; + ELSE + BREAK loop_label; + END IF; + END LOOP; +END LOOP loop_label; +---------- IR ------------- +loop_label_LOOP(0): +QUERY SELECT 0, __expr_result3(3) +ITER __expr_result3(3), __expr_result_iter4(4) +READ __expr_result_iter4(4), $0, x(2) +__LOOP5(5): +QUERY SELECT :2, __expr_result8(8) +ITER __expr_result8(8), __expr_result_iter9(9) +READ __expr_result_iter9(9), $0, y(7) +QUERY SELECT is_true(:7 < 2), __expr_result13(13) +ITER __expr_result13(13), __expr_result_iter14(14) +READ __expr_result_iter14(14), $0, __condition12(12) +JUMP_IF_TRUE __condition12(12), __IF_THEN10(10) +GOTO loop_label_LOOP_END(1) +GOTO __IF_END11(11) +__IF_THEN10(10): +QUERY SELECT :2 + 1, __expr_result15(15) +ITER __expr_result15(15), __expr_result_iter16(16) +READ __expr_result_iter16(16), $0, x(2) +GOTO __LOOP5(5) +GOTO __IF_END11(11) +__IF_END11(11): +GOTO __LOOP5(5) +__LOOP_END6(6): +GOTO loop_label_LOOP(0) +loop_label_LOOP_END(1): +---------- QUERY --------- +QUERY: SELECT 0 +BLOCK: ($0): (0) +QUERY: SELECT 0 +BLOCK: ($0): (0) +QUERY: SELECT is_true(0 < 2) +BLOCK: ($0): (TRUE) +QUERY: SELECT 0 + 1 +BLOCK: ($0): (1) +QUERY: SELECT 1 +BLOCK: ($0): (1) +QUERY: SELECT is_true(1 < 2) +BLOCK: ($0): (TRUE) +QUERY: SELECT 1 + 1 +BLOCK: ($0): (2) +QUERY: SELECT 2 +BLOCK: ($0): (2) +QUERY: SELECT is_true(2 < 2) +BLOCK: ($0): (FALSE) +---------- Output --------- +None + + +---------- Input ---------- +LET x := 0; +LOOP + LET x := 1; + BREAK; +END LOOP; +RETURN x; +---------- IR ------------- +QUERY SELECT 0, __expr_result1(1) +ITER __expr_result1(1), __expr_result_iter2(2) +READ __expr_result_iter2(2), $0, x(0) +__LOOP3(3): +QUERY SELECT 1, __expr_result6(6) +ITER __expr_result6(6), __expr_result_iter7(7) +READ __expr_result_iter7(7), $0, x(5) +GOTO __LOOP_END4(4) +GOTO __LOOP3(3) +__LOOP_END4(4): +QUERY SELECT :0, __expr_result9(9) +ITER __expr_result9(9), __expr_result_iter10(10) +READ __expr_result_iter10(10), $0, __return_val8(8) +RETURN __return_val8(8) +---------- QUERY --------- +QUERY: SELECT 0 +BLOCK: ($0): (0) +QUERY: SELECT 1 +BLOCK: ($0): (1) +QUERY: SELECT 0 +BLOCK: ($0): (0) +---------- Output --------- +Some(Var(UInt64(0))) + + +---------- Input ---------- +LET x := 1; +CASE x + WHEN 1 THEN RETURN 'ONE'; + WHEN 2 THEN RETURN 'TWO'; + ELSE RETURN 'OTHER'; +END CASE; +---------- IR ------------- +QUERY SELECT 1, __expr_result1(1) +ITER __expr_result1(1), __expr_result_iter2(2) +READ __expr_result_iter2(2), $0, x(0) +QUERY SELECT is_true(:0 = 1), __expr_result7(7) +ITER __expr_result7(7), __expr_result_iter8(8) +READ __expr_result_iter8(8), $0, __condition6(6) +JUMP_IF_TRUE __condition6(6), __IF_THEN3(3) +QUERY SELECT is_true(:0 = 2), __expr_result10(10) +ITER __expr_result10(10), __expr_result_iter11(11) +READ __expr_result_iter11(11), $0, __condition9(9) +JUMP_IF_TRUE __condition9(9), __IF_THEN4(4) +QUERY SELECT 'OTHER', __expr_result13(13) +ITER __expr_result13(13), __expr_result_iter14(14) +READ __expr_result_iter14(14), $0, __return_val12(12) +RETURN __return_val12(12) +GOTO __IF_END5(5) +__IF_THEN3(3): +QUERY SELECT 'ONE', __expr_result16(16) +ITER __expr_result16(16), __expr_result_iter17(17) +READ __expr_result_iter17(17), $0, __return_val15(15) +RETURN __return_val15(15) +GOTO __IF_END5(5) +__IF_THEN4(4): +QUERY SELECT 'TWO', __expr_result19(19) +ITER __expr_result19(19), __expr_result_iter20(20) +READ __expr_result_iter20(20), $0, __return_val18(18) +RETURN __return_val18(18) +GOTO __IF_END5(5) +__IF_END5(5): +---------- QUERY --------- +QUERY: SELECT 1 +BLOCK: ($0): (1) +QUERY: SELECT is_true(1 = 1) +BLOCK: ($0): (TRUE) +QUERY: SELECT 'ONE' +BLOCK: ($0): ('ONE') +---------- Output --------- +Some(Var(String("ONE"))) + + +---------- Input ---------- +LET x := 2; +CASE x + WHEN 1 THEN RETURN 'ONE'; + WHEN 2 THEN RETURN 'TWO'; + ELSE RETURN 'OTHER'; +END CASE; +---------- IR ------------- +QUERY SELECT 2, __expr_result1(1) +ITER __expr_result1(1), __expr_result_iter2(2) +READ __expr_result_iter2(2), $0, x(0) +QUERY SELECT is_true(:0 = 1), __expr_result7(7) +ITER __expr_result7(7), __expr_result_iter8(8) +READ __expr_result_iter8(8), $0, __condition6(6) +JUMP_IF_TRUE __condition6(6), __IF_THEN3(3) +QUERY SELECT is_true(:0 = 2), __expr_result10(10) +ITER __expr_result10(10), __expr_result_iter11(11) +READ __expr_result_iter11(11), $0, __condition9(9) +JUMP_IF_TRUE __condition9(9), __IF_THEN4(4) +QUERY SELECT 'OTHER', __expr_result13(13) +ITER __expr_result13(13), __expr_result_iter14(14) +READ __expr_result_iter14(14), $0, __return_val12(12) +RETURN __return_val12(12) +GOTO __IF_END5(5) +__IF_THEN3(3): +QUERY SELECT 'ONE', __expr_result16(16) +ITER __expr_result16(16), __expr_result_iter17(17) +READ __expr_result_iter17(17), $0, __return_val15(15) +RETURN __return_val15(15) +GOTO __IF_END5(5) +__IF_THEN4(4): +QUERY SELECT 'TWO', __expr_result19(19) +ITER __expr_result19(19), __expr_result_iter20(20) +READ __expr_result_iter20(20), $0, __return_val18(18) +RETURN __return_val18(18) +GOTO __IF_END5(5) +__IF_END5(5): +---------- QUERY --------- +QUERY: SELECT 2 +BLOCK: ($0): (2) +QUERY: SELECT is_true(2 = 1) +BLOCK: ($0): (FALSE) +QUERY: SELECT is_true(2 = 2) +BLOCK: ($0): (TRUE) +QUERY: SELECT 'TWO' +BLOCK: ($0): ('TWO') +---------- Output --------- +Some(Var(String("TWO"))) + + +---------- Input ---------- +LET x := 3; +CASE x + WHEN 1 THEN RETURN 'ONE'; + WHEN 2 THEN RETURN 'TWO'; + ELSE RETURN 'OTHER'; +END CASE; +---------- IR ------------- +QUERY SELECT 3, __expr_result1(1) +ITER __expr_result1(1), __expr_result_iter2(2) +READ __expr_result_iter2(2), $0, x(0) +QUERY SELECT is_true(:0 = 1), __expr_result7(7) +ITER __expr_result7(7), __expr_result_iter8(8) +READ __expr_result_iter8(8), $0, __condition6(6) +JUMP_IF_TRUE __condition6(6), __IF_THEN3(3) +QUERY SELECT is_true(:0 = 2), __expr_result10(10) +ITER __expr_result10(10), __expr_result_iter11(11) +READ __expr_result_iter11(11), $0, __condition9(9) +JUMP_IF_TRUE __condition9(9), __IF_THEN4(4) +QUERY SELECT 'OTHER', __expr_result13(13) +ITER __expr_result13(13), __expr_result_iter14(14) +READ __expr_result_iter14(14), $0, __return_val12(12) +RETURN __return_val12(12) +GOTO __IF_END5(5) +__IF_THEN3(3): +QUERY SELECT 'ONE', __expr_result16(16) +ITER __expr_result16(16), __expr_result_iter17(17) +READ __expr_result_iter17(17), $0, __return_val15(15) +RETURN __return_val15(15) +GOTO __IF_END5(5) +__IF_THEN4(4): +QUERY SELECT 'TWO', __expr_result19(19) +ITER __expr_result19(19), __expr_result_iter20(20) +READ __expr_result_iter20(20), $0, __return_val18(18) +RETURN __return_val18(18) +GOTO __IF_END5(5) +__IF_END5(5): +---------- QUERY --------- +QUERY: SELECT 3 +BLOCK: ($0): (3) +QUERY: SELECT is_true(3 = 1) +BLOCK: ($0): (FALSE) +QUERY: SELECT is_true(3 = 2) +BLOCK: ($0): (FALSE) +QUERY: SELECT 'OTHER' +BLOCK: ($0): ('OTHER') +---------- Output --------- +Some(Var(String("OTHER"))) + + +---------- Input ---------- +LET x := 1; +CASE + WHEN x = 1 THEN RETURN 'ONE'; + WHEN x = 2 THEN RETURN 'TWO'; + ELSE RETURN 'OTHER'; +END CASE; +---------- IR ------------- +QUERY SELECT 1, __expr_result1(1) +ITER __expr_result1(1), __expr_result_iter2(2) +READ __expr_result_iter2(2), $0, x(0) +QUERY SELECT is_true(:0 = 1), __expr_result7(7) +ITER __expr_result7(7), __expr_result_iter8(8) +READ __expr_result_iter8(8), $0, __condition6(6) +JUMP_IF_TRUE __condition6(6), __IF_THEN3(3) +QUERY SELECT is_true(:0 = 2), __expr_result10(10) +ITER __expr_result10(10), __expr_result_iter11(11) +READ __expr_result_iter11(11), $0, __condition9(9) +JUMP_IF_TRUE __condition9(9), __IF_THEN4(4) +QUERY SELECT 'OTHER', __expr_result13(13) +ITER __expr_result13(13), __expr_result_iter14(14) +READ __expr_result_iter14(14), $0, __return_val12(12) +RETURN __return_val12(12) +GOTO __IF_END5(5) +__IF_THEN3(3): +QUERY SELECT 'ONE', __expr_result16(16) +ITER __expr_result16(16), __expr_result_iter17(17) +READ __expr_result_iter17(17), $0, __return_val15(15) +RETURN __return_val15(15) +GOTO __IF_END5(5) +__IF_THEN4(4): +QUERY SELECT 'TWO', __expr_result19(19) +ITER __expr_result19(19), __expr_result_iter20(20) +READ __expr_result_iter20(20), $0, __return_val18(18) +RETURN __return_val18(18) +GOTO __IF_END5(5) +__IF_END5(5): +---------- QUERY --------- +QUERY: SELECT 1 +BLOCK: ($0): (1) +QUERY: SELECT is_true(1 = 1) +BLOCK: ($0): (TRUE) +QUERY: SELECT 'ONE' +BLOCK: ($0): ('ONE') +---------- Output --------- +Some(Var(String("ONE"))) + + +---------- Input ---------- +LET x := 2; +CASE + WHEN x = 1 THEN RETURN 'ONE'; + WHEN x = 2 THEN RETURN 'TWO'; + ELSE RETURN 'OTHER'; +END CASE; +---------- IR ------------- +QUERY SELECT 2, __expr_result1(1) +ITER __expr_result1(1), __expr_result_iter2(2) +READ __expr_result_iter2(2), $0, x(0) +QUERY SELECT is_true(:0 = 1), __expr_result7(7) +ITER __expr_result7(7), __expr_result_iter8(8) +READ __expr_result_iter8(8), $0, __condition6(6) +JUMP_IF_TRUE __condition6(6), __IF_THEN3(3) +QUERY SELECT is_true(:0 = 2), __expr_result10(10) +ITER __expr_result10(10), __expr_result_iter11(11) +READ __expr_result_iter11(11), $0, __condition9(9) +JUMP_IF_TRUE __condition9(9), __IF_THEN4(4) +QUERY SELECT 'OTHER', __expr_result13(13) +ITER __expr_result13(13), __expr_result_iter14(14) +READ __expr_result_iter14(14), $0, __return_val12(12) +RETURN __return_val12(12) +GOTO __IF_END5(5) +__IF_THEN3(3): +QUERY SELECT 'ONE', __expr_result16(16) +ITER __expr_result16(16), __expr_result_iter17(17) +READ __expr_result_iter17(17), $0, __return_val15(15) +RETURN __return_val15(15) +GOTO __IF_END5(5) +__IF_THEN4(4): +QUERY SELECT 'TWO', __expr_result19(19) +ITER __expr_result19(19), __expr_result_iter20(20) +READ __expr_result_iter20(20), $0, __return_val18(18) +RETURN __return_val18(18) +GOTO __IF_END5(5) +__IF_END5(5): +---------- QUERY --------- +QUERY: SELECT 2 +BLOCK: ($0): (2) +QUERY: SELECT is_true(2 = 1) +BLOCK: ($0): (FALSE) +QUERY: SELECT is_true(2 = 2) +BLOCK: ($0): (TRUE) +QUERY: SELECT 'TWO' +BLOCK: ($0): ('TWO') +---------- Output --------- +Some(Var(String("TWO"))) + + +---------- Input ---------- +LET x := 3; +CASE + WHEN x = 1 THEN RETURN 'ONE'; + WHEN x = 2 THEN RETURN 'TWO'; + ELSE RETURN 'OTHER'; +END CASE; +---------- IR ------------- +QUERY SELECT 3, __expr_result1(1) +ITER __expr_result1(1), __expr_result_iter2(2) +READ __expr_result_iter2(2), $0, x(0) +QUERY SELECT is_true(:0 = 1), __expr_result7(7) +ITER __expr_result7(7), __expr_result_iter8(8) +READ __expr_result_iter8(8), $0, __condition6(6) +JUMP_IF_TRUE __condition6(6), __IF_THEN3(3) +QUERY SELECT is_true(:0 = 2), __expr_result10(10) +ITER __expr_result10(10), __expr_result_iter11(11) +READ __expr_result_iter11(11), $0, __condition9(9) +JUMP_IF_TRUE __condition9(9), __IF_THEN4(4) +QUERY SELECT 'OTHER', __expr_result13(13) +ITER __expr_result13(13), __expr_result_iter14(14) +READ __expr_result_iter14(14), $0, __return_val12(12) +RETURN __return_val12(12) +GOTO __IF_END5(5) +__IF_THEN3(3): +QUERY SELECT 'ONE', __expr_result16(16) +ITER __expr_result16(16), __expr_result_iter17(17) +READ __expr_result_iter17(17), $0, __return_val15(15) +RETURN __return_val15(15) +GOTO __IF_END5(5) +__IF_THEN4(4): +QUERY SELECT 'TWO', __expr_result19(19) +ITER __expr_result19(19), __expr_result_iter20(20) +READ __expr_result_iter20(20), $0, __return_val18(18) +RETURN __return_val18(18) +GOTO __IF_END5(5) +__IF_END5(5): +---------- QUERY --------- +QUERY: SELECT 3 +BLOCK: ($0): (3) +QUERY: SELECT is_true(3 = 1) +BLOCK: ($0): (FALSE) +QUERY: SELECT is_true(3 = 2) +BLOCK: ($0): (FALSE) +QUERY: SELECT 'OTHER' +BLOCK: ($0): ('OTHER') +---------- Output --------- +Some(Var(String("OTHER"))) + + diff --git a/tests/sqllogictests/suites/query/02_function/02_0023_function_strings_quote.test b/tests/sqllogictests/suites/query/02_function/02_0023_function_strings_quote.test index 5b7e839b7df6..0e0c15001218 100644 --- a/tests/sqllogictests/suites/query/02_function/02_0023_function_strings_quote.test +++ b/tests/sqllogictests/suites/query/02_function/02_0023_function_strings_quote.test @@ -14,7 +14,7 @@ onlyif mysql query T select quote('a\"b') ---- -a\\\"b +a\"b onlyif mysql query T diff --git a/tests/sqllogictests/suites/query/02_function/02_0048_function_semi_structureds_parse_json.test b/tests/sqllogictests/suites/query/02_function/02_0048_function_semi_structureds_parse_json.test index fee9074c604d..dffb6bc6d9c2 100644 --- a/tests/sqllogictests/suites/query/02_function/02_0048_function_semi_structureds_parse_json.test +++ b/tests/sqllogictests/suites/query/02_function/02_0048_function_semi_structureds_parse_json.test @@ -60,7 +60,7 @@ select parse_json('{ "x" : "abc", "y" : false, "z": 10} ') query T -select parse_json('{ "test" : "\\"abc\"测试⚠️✅❌မြန်မာဘာသာ" } ') +select parse_json('{ "test" : "\\"abc\\"测试⚠️✅❌မြန်မာဘာသာ" } ') ---- {"test":"\"abc\"测试⚠️✅❌မြန်မာဘာသာ"}