From baa523aed3d01c522fd4fcaf000eea4864d65e3f Mon Sep 17 00:00:00 2001 From: Elliot Chance Date: Thu, 29 Jul 2021 10:01:15 -0400 Subject: [PATCH] Greatly improved expressions (#18) - Expressions can now contain artithmetic for binary operations (+, -, * and /). - Expressions now follow correct operator pecedence. - Added SQLSTATE 22012 "division by zero". - Adding grouping expression (parenthesis). - Added concatenation `||`. - Added logical operators for `AND`, `OR` and `NOT`. - Added unary operators `+` and `-`. - Changes not equal operator `!=` to `<>` to correct follow the SQL standard. - Renamed the lexer tokens to be same as the naming conventions in the SQL standard. --- .github/workflows/ci.yml | 2 +- README.md | 61 ++++++++++-- tests/arithmetic.sql | 38 ++++++++ tests/comparison.sql | 35 +++++++ tests/concatenation.sql | 5 + tests/logical.sql | 29 ++++++ tests/null.sql | 6 ++ tests/reserved-words.sql | 6 +- tests/select-literal.sql | 3 + tests/select-where.sql | 6 +- vsql/ast.v | 11 ++- vsql/eval.v | 86 +++++++++++++++-- vsql/lexer.v | 89 +++++++++++++----- vsql/parser.v | 196 ++++++++++++++++++++++++--------------- vsql/row.v | 2 +- vsql/sqlstate.v | 15 ++- vsql/type.v | 7 ++ vsql/value.v | 9 ++ 18 files changed, 482 insertions(+), 124 deletions(-) create mode 100644 tests/arithmetic.sql create mode 100644 tests/comparison.sql create mode 100644 tests/concatenation.sql create mode 100644 tests/logical.sql diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0969a74..db58de2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,4 +20,4 @@ jobs: - name: Verify fmt run: v fmt -verify . - name: Run SQL tests - run: v -stats test vsql + run: v -stats -prod test vsql diff --git a/README.md b/README.md index f0a9a44..da722ee 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ no dependencies. - [Appendix](#appendix) - [Data Types](#data-types) - [Keywords](#keywords) + - [Operators](#operators) - [SQLSTATE (Errors)](#sqlstate-errors) - [Testing](#testing) @@ -259,6 +260,53 @@ There are some types that are not supported yet: Names of entities (such as tables and columns) cannot be a [reserved word](https://github.com/elliotchance/vsql/blob/main/vsql/keywords.v). +### Operators + +For the tables below: + +- `number` is any of the numer types: `FLOAT`, `DOUBLE PRECISION`, etc. +- `text` is any of the character types: `CHARACTER VARYING`, `CHARACTER`, etc. +- `any` is any data type. + +**Binary Operations** + +| Operator | Precedence | Name | +| --------------------- | ---------- | ---- | +| `number * number` | 2 | Multiplication | +| `number / number` | 2 | Division | +| `number + number` | 3 | Addition | +| `number - number` | 3 | Subtraction | +| `text || text` | 3 | Concatenation | +| `any = any` | 4 | Equal | +| `any <> any` | 4 | Not equal | +| `number > number` | 4 | Greater than | +| `text > text` | 4 | Greater than | +| `number < number` | 4 | Less than | +| `text <= text` | 4 | Less than | +| `number >= number` | 4 | Greater than or equal | +| `text >= text` | 4 | Greater than or equal | +| `number <= number` | 4 | Less than or equal | +| `text <= text` | 4 | Less than or equal | +| `boolean AND boolean` | 6 | Logical and | +| `boolean OR boolean` | 7 | Logical or | + +The _Precedence_ dictates the order of operations. For example `2 + 3 * 5` is +evaluated as `2 + (3 * 5)` because `*` has a lower precedence so it happens +first. You can control the order of operations with parenthesis, like +`(2 + 3) * 5`. + +Dividing by zero will result in `SQLSTATE 22012` error. + +**Unary Operations** + +| Operator | Name | +| --------------------- | ---- | +| `+number` | Noop | +| `-number` | Unary negate | +| `NOT boolean` | Logical negate | +| `any IS NULL` | NULL check | +| `any IS NOT NULL` | Not NULL check | + ### SQLSTATE (Errors) The error returned from `query()` will always one of the `SQLState` struct @@ -307,12 +355,13 @@ db.query('SELECT * FROM bar') or { | SQLSTATE | Reason | | ---------- | ------ | -| `23502` | violates non-null constraint | -| `42601` | syntax error | -| `42703` | column does not exist | -| `42804` | data type mismatch | -| `42P01` | table does not exist | -| `42P07` | table already exists | +| `22012` | Divide by zero. | +| `23502` | Violates non-null constraint. | +| `42601` | Syntax error. | +| `42703` | Column does not exist. | +| `42804` | Data type mismatch. | +| `42P01` | Table does not exist. | +| `42P07` | Table already exists. | Testing ------- diff --git a/tests/arithmetic.sql b/tests/arithmetic.sql new file mode 100644 index 0000000..2fded70 --- /dev/null +++ b/tests/arithmetic.sql @@ -0,0 +1,38 @@ +SELECT 1 + 2; +-- COL1: 3 + +SELECT 1 - 2; +-- COL1: -1 + +SELECT 2 * 3; +-- COL1: 6 + +SELECT 6 / 2; +-- COL1: 3 + +SELECT 1.2 + 2.4; +-- COL1: 3.6 + +SELECT 1.7 - 0.5; +-- COL1: 1.2 + +SELECT 2.2 * 3.3; +-- COL1: 7.26 + +SELECT 6 / 2.5; +-- COL1: 2.4 + +SELECT 0 / 2.5; +-- COL1: 0 + +SELECT 2.5 / 0; +-- error 22012: division by zero + +SELECT -123; +-- COL1: -123 + +SELECT +1.23; +-- COL1: 1.23 + +SELECT 1.5 + 2.4 * 7; +-- COL1: 18.3 diff --git a/tests/comparison.sql b/tests/comparison.sql new file mode 100644 index 0000000..0948178 --- /dev/null +++ b/tests/comparison.sql @@ -0,0 +1,35 @@ +SELECT 1 = 2; +-- COL1: FALSE + +SELECT 1 = 1; +-- COL1: TRUE + +SELECT 1 <> 2; +-- COL1: TRUE + +SELECT 1 <> 1; +-- COL1: FALSE + +SELECT 1 > 2; +-- COL1: FALSE + +SELECT 1 > 1; +-- COL1: FALSE + +SELECT 1 >= 2; +-- COL1: FALSE + +SELECT 1 >= 1; +-- COL1: TRUE + +SELECT 1 < 2; +-- COL1: TRUE + +SELECT 1 < 1; +-- COL1: FALSE + +SELECT 1 <= 2; +-- COL1: TRUE + +SELECT 1 <= 1; +-- COL1: TRUE diff --git a/tests/concatenation.sql b/tests/concatenation.sql new file mode 100644 index 0000000..a01d653 --- /dev/null +++ b/tests/concatenation.sql @@ -0,0 +1,5 @@ +SELECT 'foo' || 'bar'; +-- COL1: foobar + +SELECT 123 || 'bar'; +-- error 42804: data type mismatch cannot INTEGER || CHARACTER VARYING: expected another type but got INTEGER and CHARACTER VARYING diff --git a/tests/logical.sql b/tests/logical.sql new file mode 100644 index 0000000..2fd3f30 --- /dev/null +++ b/tests/logical.sql @@ -0,0 +1,29 @@ +SELECT TRUE AND TRUE; +-- COL1: TRUE + +SELECT TRUE AND FALSE; +-- COL1: FALSE + +SELECT FALSE AND TRUE; +-- COL1: FALSE + +SELECT FALSE AND FALSE; +-- COL1: FALSE + +SELECT TRUE OR TRUE; +-- COL1: TRUE + +SELECT TRUE OR FALSE; +-- COL1: TRUE + +SELECT FALSE OR TRUE; +-- COL1: TRUE + +SELECT FALSE OR FALSE; +-- COL1: FALSE + +SELECT NOT TRUE; +-- COL1: FALSE + +SELECT NOT FALSE; +-- COL1: TRUE diff --git a/tests/null.sql b/tests/null.sql index 2d55b40..ea582b3 100644 --- a/tests/null.sql +++ b/tests/null.sql @@ -42,3 +42,9 @@ SELECT * FROM foo WHERE num IS NOT NULL; -- COL1: is not null -- NUM: 13 -- NUM: 35 + +SELECT NULL IS NULL OR NULL IS NOT NULL; +-- COL1: TRUE + +SELECT NULL IS NULL AND NULL IS NOT NULL; +-- COL1: FALSE diff --git a/tests/reserved-words.sql b/tests/reserved-words.sql index 3630e87..b973d4b 100644 --- a/tests/reserved-words.sql +++ b/tests/reserved-words.sql @@ -14,7 +14,7 @@ CREATE TABLE ALTER (a INT); -- error 42601: syntax error: table name cannot be reserved word: ALTER CREATE TABLE AND (a INT); --- error 42601: syntax error: table name cannot be reserved word: AND +-- error 42601: syntax error: expecting literal_identifier but found AND CREATE TABLE ANY (a INT); -- error 42601: syntax error: table name cannot be reserved word: ANY @@ -329,7 +329,7 @@ CREATE TABLE END_PARTITION (a INT); -- error 42601: syntax error: table name cannot be reserved word: END_PARTITION CREATE TABLE END-EXEC (a INT); --- error 42601: syntax error: expecting op_paren_open but found EXEC +-- error 42601: syntax error: expecting left_paren but found - CREATE TABLE EQUALS (a INT); -- error 42601: syntax error: table name cannot be reserved word: EQUALS @@ -677,7 +677,7 @@ CREATE TABLE OPEN (a INT); -- error 42601: syntax error: table name cannot be reserved word: OPEN CREATE TABLE OR (a INT); --- error 42601: syntax error: table name cannot be reserved word: OR +-- error 42601: syntax error: expecting literal_identifier but found OR CREATE TABLE ORDER (a INT); -- error 42601: syntax error: table name cannot be reserved word: ORDER diff --git a/tests/select-literal.sql b/tests/select-literal.sql index 18b77dd..106eca1 100644 --- a/tests/select-literal.sql +++ b/tests/select-literal.sql @@ -12,3 +12,6 @@ Select 'hello'; SELECT 123, 456; -- COL1: 123 COL2: 456 + +SELECT 2 + 3 * 5, (2 + 3) * 5; +-- COL1: 17 COL2: 25 diff --git a/tests/select-where.sql b/tests/select-where.sql index 864d3c0..12dc538 100644 --- a/tests/select-where.sql +++ b/tests/select-where.sql @@ -4,8 +4,8 @@ INSERT INTO foo (num) VALUES (27); INSERT INTO foo (num) VALUES (35); SELECT '='; SELECT * FROM foo WHERE num = 27; -SELECT '!='; -SELECT * FROM foo WHERE num != 13; +SELECT '<>'; +SELECT * FROM foo WHERE num <> 13; SELECT '>'; SELECT * FROM foo WHERE num > 27; SELECT '>='; @@ -20,7 +20,7 @@ SELECT * FROM foo WHERE num <= 27; -- msg: INSERT 1 -- COL1: = -- NUM: 27 --- COL1: != +-- COL1: <> -- NUM: 27 -- NUM: 35 -- COL1: > diff --git a/vsql/ast.v b/vsql/ast.v index f449f18..6db7de4 100644 --- a/vsql/ast.v +++ b/vsql/ast.v @@ -6,7 +6,7 @@ module vsql type Stmt = CreateTableStmt | DeleteStmt | DropTableStmt | InsertStmt | SelectStmt | UpdateStmt // All possible expression entities. -type Expr = BinaryExpr | Identifier | NoExpr | NullExpr | Value +type Expr = BinaryExpr | Identifier | NoExpr | NullExpr | UnaryExpr | Value // CREATE TABLE ... struct CreateTableStmt { @@ -57,10 +57,15 @@ struct Identifier { name string } +struct UnaryExpr { + op string // NOT, -, + + expr Expr +} + struct BinaryExpr { - col string + left Expr op string - value Value + right Expr } // NoExpr is just a placeholder when there is no expression provided. diff --git a/vsql/eval.v b/vsql/eval.v index 99d21c9..452d172 100644 --- a/vsql/eval.v +++ b/vsql/eval.v @@ -8,6 +8,7 @@ fn eval_as_value(data Row, e Expr) ?Value { Identifier { return eval_identifier(data, e) } NullExpr { return eval_null(data, e) } NoExpr { return sqlstate_42601('no expression provided') } + UnaryExpr { return eval_unary(data, e) } Value { return e } } } @@ -40,20 +41,93 @@ fn eval_null(data Row, e NullExpr) ?Value { } fn eval_binary(data Row, e BinaryExpr) ?Value { - col := identifier_name(e.col) + left := eval_as_value(data, e.left) ? + right := eval_as_value(data, e.right) ? - if data.data[col].typ.uses_f64() && e.value.typ.uses_f64() { - return eval_cmp(data.get_f64(col) ?, e.value.f64_value, e.op) + match e.op { + '=', '<>', '>', '<', '>=', '<=' { + if left.typ.uses_f64() && right.typ.uses_f64() { + return eval_cmp(left.f64_value, right.f64_value, e.op) + } + + if left.typ.uses_string() && right.typ.uses_string() { + return eval_cmp(left.string_value, right.string_value, e.op) + } + } + '||' { + if left.typ.uses_string() && right.typ.uses_string() { + return new_varchar_value(left.string_value + right.string_value, 0) + } + } + '+' { + if left.typ.uses_f64() && right.typ.uses_f64() { + return new_float_value(left.f64_value + right.f64_value) + } + } + '-' { + if left.typ.uses_f64() && right.typ.uses_f64() { + return new_float_value(left.f64_value - right.f64_value) + } + } + '*' { + if left.typ.uses_f64() && right.typ.uses_f64() { + return new_float_value(left.f64_value * right.f64_value) + } + } + '/' { + if left.typ.uses_f64() && right.typ.uses_f64() { + if right.f64_value == 0 { + return sqlstate_22012() // division by zero + } + + return new_float_value(left.f64_value / right.f64_value) + } + } + 'AND' { + if left.typ.typ == .is_boolean && right.typ.typ == .is_boolean { + return new_boolean_value((left.f64_value != 0) && (right.f64_value != 0)) + } + } + 'OR' { + if left.typ.typ == .is_boolean && right.typ.typ == .is_boolean { + return new_boolean_value((left.f64_value != 0) || (right.f64_value != 0)) + } + } + else {} + } + + return sqlstate_42804('cannot $left.typ $e.op $right.typ', 'another type', '$left.typ and $right.typ') +} + +fn eval_unary(data Row, e UnaryExpr) ?Value { + value := eval_as_value(data, e.expr) ? + + match e.op { + '-' { + if value.typ.uses_f64() { + return new_float_value(-value.f64_value) + } + } + '+' { + if value.typ.uses_f64() { + return new_float_value(value.f64_value) + } + } + 'NOT' { + if value.typ.typ == .is_boolean { + return new_boolean_value(!(value.f64_value != 0)) + } + } + else {} } - // TODO(elliotchance): Use the correct SQLSTATE error. - return error('cannot $col $e.op $e.value.typ') + return sqlstate_42804('cannot $e.op$value.typ', 'another type', value.typ.str()) } fn eval_cmp(lhs T, rhs T, op string) Value { return new_boolean_value(match op { '=' { lhs == rhs } - '!=' { lhs != rhs } + '<>' { lhs != rhs } '>' { lhs > rhs } '>=' { lhs >= rhs } '<' { lhs < rhs } diff --git a/vsql/lexer.v b/vsql/lexer.v index 913fa7f..e83083e 100644 --- a/vsql/lexer.v +++ b/vsql/lexer.v @@ -3,8 +3,17 @@ module vsql +// Except for the eof and the keywords, the other tokens use the names described +// in the SQL standard. enum TokenKind { eof // End of file + asterisk // ::= * + comma // ::= , + concatenation_operator // ::= || + equals_operator // ::= = + greater_than_operator // ::= > + greater_than_or_equals_operator // ::= >= + keyword_and // AND keyword_bigint // BIGINT keyword_boolean // BOOLEAN keyword_char // CHAR @@ -23,6 +32,7 @@ enum TokenKind { keyword_is // IS keyword_not // NOT keyword_null // NULL + keyword_or // OR keyword_precision // PRECISION keyword_real // REAL keyword_select // SELECT @@ -36,20 +46,18 @@ enum TokenKind { keyword_varchar // VARCHAR keyword_varying // VARYING keyword_where // WHERE + left_paren // ::= ( + less_than_operator // ::= < + less_than_or_equals_operator // ::= <= literal_identifier // foo or "foo" (delimited) literal_number // 123 literal_string // 'hello' - op_comma // , - op_eq // = - op_gt // > - op_gte // >= - op_lt // < - op_lte // <= - op_multiply // * - op_neq // != - op_paren_close // ) - op_paren_open // ( - op_semi_colon // ; + minus_sign // ::= - + not_equals_operator // ::= <> + plus_sign // ::= + + right_paren // ::= ) + semicolon // ::= ; + solidus // ::= / } struct Token { @@ -77,7 +85,7 @@ fn tokenize(sql string) []Token { word += '${cs[i]}' i++ } - tokens << Token{TokenKind.literal_number, word} + tokens << Token{.literal_number, word} continue } @@ -90,7 +98,7 @@ fn tokenize(sql string) []Token { i++ } i++ - tokens << Token{TokenKind.literal_string, word} + tokens << Token{.literal_string, word} continue } @@ -103,15 +111,16 @@ fn tokenize(sql string) []Token { i++ } i++ - tokens << Token{TokenKind.literal_identifier, '"$word"'} + tokens << Token{.literal_identifier, '"$word"'} continue } // operators multi := map{ - '!=': TokenKind.op_neq - '>=': TokenKind.op_gte - '<=': TokenKind.op_lte + '<>': TokenKind.not_equals_operator + '>=': TokenKind.greater_than_or_equals_operator + '<=': TokenKind.less_than_or_equals_operator + '||': TokenKind.concatenation_operator } for op, tk in multi { if cs[i] == op[0] && cs[i + 1] == op[1] { @@ -122,14 +131,17 @@ fn tokenize(sql string) []Token { } single := map{ - `(`: TokenKind.op_paren_open - `)`: TokenKind.op_paren_close - `=`: TokenKind.op_eq - `>`: TokenKind.op_gt - `<`: TokenKind.op_lt - `*`: TokenKind.op_multiply - `,`: TokenKind.op_comma - `;`: TokenKind.op_semi_colon + `(`: TokenKind.left_paren + `)`: TokenKind.right_paren + `*`: TokenKind.asterisk + `+`: TokenKind.plus_sign + `,`: TokenKind.comma + `-`: TokenKind.minus_sign + `/`: TokenKind.solidus + `;`: TokenKind.semicolon + `<`: TokenKind.less_than_operator + `=`: TokenKind.equals_operator + `>`: TokenKind.greater_than_operator } for op, tk in single { if cs[i] == op { @@ -154,6 +166,7 @@ fn tokenize(sql string) []Token { } tokens << match word.to_upper() { + 'AND' { Token{TokenKind.keyword_and, word} } 'BIGINT' { Token{TokenKind.keyword_bigint, word} } 'BOOLEAN' { Token{TokenKind.keyword_boolean, word} } 'CHAR' { Token{TokenKind.keyword_char, word} } @@ -172,6 +185,7 @@ fn tokenize(sql string) []Token { 'IS' { Token{TokenKind.keyword_is, word} } 'NOT' { Token{TokenKind.keyword_not, word} } 'NULL' { Token{TokenKind.keyword_null, word} } + 'OR' { Token{TokenKind.keyword_or, word} } 'PRECISION' { Token{TokenKind.keyword_precision, word} } 'REAL' { Token{TokenKind.keyword_real, word} } 'SELECT' { Token{TokenKind.keyword_select, word} } @@ -204,3 +218,28 @@ fn is_identifier_char(c byte, is_not_first bool) bool { return yes } + +fn precedence(tk TokenKind) int { + return match tk { + .asterisk, .solidus { + 2 + } + .plus_sign, .minus_sign { + 3 + } + .equals_operator, .not_equals_operator, .less_than_operator, .less_than_or_equals_operator, + .greater_than_operator, .greater_than_or_equals_operator { + 4 + } + .keyword_and { + 6 + } + .keyword_or { + 7 + } + else { + panic(tk) + 0 + } + } +} diff --git a/vsql/parser.v b/vsql/parser.v index 681f45d..e8a2b4a 100644 --- a/vsql/parser.v +++ b/vsql/parser.v @@ -39,7 +39,7 @@ fn parse(sql string) ?Stmt { // The ; is optional. However, we do not support multiple queries yet so // make sure we catch that. - if parser.peek(.op_semi_colon).len > 0 { + if parser.peek(.semicolon).len > 0 { parser.pos++ } @@ -71,13 +71,13 @@ fn (mut p Parser) consume_type() ?Type { // incomplete type. types := [ // 5 - [TokenKind.keyword_char, .keyword_varying, .op_paren_open, .literal_number, .op_paren_close], - [.keyword_character, .keyword_varying, .op_paren_open, .literal_number, .op_paren_close], + [TokenKind.keyword_char, .keyword_varying, .left_paren, .literal_number, .right_paren], + [.keyword_character, .keyword_varying, .left_paren, .literal_number, .right_paren], // 4 - [.keyword_char, .op_paren_open, .literal_number, .op_paren_close], - [.keyword_character, .op_paren_open, .literal_number, .op_paren_close], - [.keyword_float, .op_paren_open, .literal_number, .op_paren_close], - [.keyword_varchar, .op_paren_open, .literal_number, .op_paren_close], + [.keyword_char, .left_paren, .literal_number, .right_paren], + [.keyword_character, .left_paren, .literal_number, .right_paren], + [.keyword_float, .left_paren, .literal_number, .right_paren], + [.keyword_varchar, .left_paren, .literal_number, .right_paren], // 2 [.keyword_double, .keyword_precision], // 1 @@ -132,17 +132,17 @@ fn (mut p Parser) consume_create_table() ?CreateTableStmt { table_name := p.consume(.literal_identifier) ? // columns - p.consume(.op_paren_open) ? + p.consume(.left_paren) ? mut columns := []Column{} columns << p.consume_column_def() ? - for p.peek(.op_comma).len > 0 { - p.consume(.op_comma) ? + for p.peek(.comma).len > 0 { + p.consume(.comma) ? columns << p.consume_column_def() ? } - p.consume(.op_paren_close) ? + p.consume(.right_paren) ? return CreateTableStmt{table_name.value, columns} } @@ -182,34 +182,55 @@ fn (mut p Parser) consume_insert() ?InsertStmt { // columns mut cols := []string{} - p.consume(.op_paren_open) ? + p.consume(.left_paren) ? col := p.consume(.literal_identifier) ? cols << col.value - for p.peek(.op_comma).len > 0 { + for p.peek(.comma).len > 0 { p.pos++ next_col := p.consume(.literal_identifier) ? cols << next_col.value } - p.consume(.op_paren_close) ? + p.consume(.right_paren) ? // values mut values := []Value{} p.consume(.keyword_values) ? - p.consume(.op_paren_open) ? + p.consume(.left_paren) ? values << p.consume_value() ? - for p.peek(.op_comma).len > 0 { + for p.peek(.comma).len > 0 { p.pos++ values << p.consume_value() ? } - p.consume(.op_paren_close) ? + p.consume(.right_paren) ? return InsertStmt{table_name.value, cols, values} } +fn (mut p Parser) consume_grouping_expr() ?Expr { + start := p.pos + + p.consume(.left_paren) or { + p.pos = start + return err + } + + expr := p.consume_expr() or { + p.pos = start + return err + } + + p.consume(.right_paren) or { + p.pos = start + return err + } + + return expr +} + fn (mut p Parser) consume_select() ?SelectStmt { // skip SELECT p.pos++ @@ -218,7 +239,7 @@ fn (mut p Parser) consume_select() ?SelectStmt { mut exprs := []Expr{} exprs << p.consume_expr() ? - for p.peek(.op_comma).len > 0 { + for p.peek(.comma).len > 0 { p.pos++ // skip ',' exprs << p.consume_expr() ? } @@ -268,18 +289,73 @@ fn (mut p Parser) consume_delete() ?DeleteStmt { fn (mut p Parser) consume_expr() ?Expr { // TODO(elliotchance): This should not be allowed outside of SELECT // expressions and this returns a dummy value for now. - if p.peek(.op_multiply).len > 0 { + if p.peek(.asterisk).len > 0 { p.pos++ return new_null_value() } - return p.consume_binary_expr() or { - return p.consume_null_expr() or { - // Value (must be last). - value := p.consume_value() ? - return value + allowed_ops := [ + TokenKind.asterisk, + .concatenation_operator, + .equals_operator, + .greater_than_operator, + .greater_than_or_equals_operator, + .keyword_and, + .keyword_or, + .less_than_operator, + .less_than_or_equals_operator, + .minus_sign, + .not_equals_operator, + .plus_sign, + .solidus, + ] + mut parts := [p.consume_simple_expr() ?] + mut operators := []Token{} + for { + if p.peek(.keyword_is, .keyword_null).len > 0 { + p.pos += 2 + parts[parts.len - 1] = NullExpr{parts[parts.len - 1], false} + } else if p.peek(.keyword_is, .keyword_not, .keyword_null).len > 0 { + p.pos += 3 + parts[parts.len - 1] = NullExpr{parts[parts.len - 1], true} + } else { + if p.tokens[p.pos].kind in allowed_ops { + operators << p.tokens[p.pos] + p.pos++ + + parts << p.consume_simple_expr() ? + } else { + break + } + } + } + + return expr_precedence(parts, operators) +} + +fn expr_precedence(parts []Expr, operators []Token) Expr { + if parts.len == 1 { + return parts[0] + } + + if parts.len == 2 { + return BinaryExpr{parts[0], operators[0].value, parts[1]} + } + + mut lowest := precedence(operators[0].kind) + mut at := 0 + mut i := 1 + for i < operators.len { + p := precedence(operators[i].kind) + if p < lowest { + lowest = p + at = i } + i++ } + + return BinaryExpr{expr_precedence(parts[..at], operators[..at - 1]), operators[at - 1].value, expr_precedence(parts[at..], + operators[at..])} } fn (mut p Parser) consume_identifier() ?Identifier { @@ -291,68 +367,38 @@ fn (mut p Parser) consume_identifier() ?Identifier { return sqlstate_42601('expecting identifier but found ${p.tokens[p.pos].value}') } -fn (mut p Parser) consume_value_or_identifier() ?Expr { - return p.consume_identifier() or { - value := p.consume_value() ? - return value - } -} - -fn (mut p Parser) consume_null_expr() ?NullExpr { +fn (mut p Parser) consume_unary_expr() ?Expr { start := p.pos - expr := p.consume_value_or_identifier() or { - p.pos = start - return sqlstate_42601('expecting expr but found ${p.tokens[p.pos].value}') - } - - if p.peek(.keyword_is, .keyword_null).len > 0 { - p.pos += 2 - return NullExpr{expr, false} - } - - if p.peek(.keyword_is, .keyword_not, .keyword_null).len > 0 { - p.pos += 3 - return NullExpr{expr, true} + ops := [TokenKind.minus_sign, .plus_sign, .keyword_not] + for op in ops { + if p.peek(op).len > 0 { + p.pos++ + return UnaryExpr{p.tokens[p.pos - 2].value, p.consume_value() or { + p.pos = start + return err + }} + } } p.pos = start - return sqlstate_42601('expecting null expr but found ${p.tokens[p.pos].value}') + return error('expecting unary') } -fn (mut p Parser) consume_binary_expr() ?BinaryExpr { +fn (mut p Parser) consume_simple_expr() ?Expr { start := p.pos + return p.consume_grouping_expr() or { + return p.consume_unary_expr() or { + return p.consume_identifier() or { + value := p.consume_value() or { + p.pos = start + return err + } - lhs := p.consume(.literal_identifier) or { - p.pos = start - return err - } - mut op := Token{} - - allowed_ops := [ - TokenKind.op_eq, - .op_neq, - .op_gt, - .op_gte, - .op_lt, - .op_lte, - ] - for allowed_op in allowed_ops { - if p.peek(allowed_op).len > 0 { - op = p.consume(allowed_op) or { - p.pos = start - return err + return value } - break } } - - rhs := p.consume_value() or { - p.pos = start - return err - } - - return BinaryExpr{lhs.value, op.value, rhs} } fn (mut p Parser) consume_value() ?Value { @@ -401,7 +447,7 @@ fn (mut p Parser) consume_update() ?UpdateStmt { // SET p.consume(.keyword_set) ? col_name := p.consume(.literal_identifier) ? - p.consume(.op_eq) ? + p.consume(.equals_operator) ? col_value := p.consume_value() ? mut set := map[string]Value{} set[col_name.value] = col_value diff --git a/vsql/row.v b/vsql/row.v index 414f2a5..a3b77eb 100644 --- a/vsql/row.v +++ b/vsql/row.v @@ -37,7 +37,7 @@ pub fn (r Row) get_string(name string) ?string { return match value.typ.typ { .is_null { 'NULL' } .is_boolean { bool_str(r.data[name].f64_value) } - .is_float, .is_real, .is_bigint, .is_integer, .is_smallint { r.data[name].f64_value.str().trim('.') } + .is_float, .is_real, .is_bigint, .is_integer, .is_smallint { f64_string(r.data[name].f64_value) } .is_varchar, .is_character { r.data[name].string_value } } } diff --git a/vsql/sqlstate.v b/vsql/sqlstate.v index f62d7be..b6477cd 100644 --- a/vsql/sqlstate.v +++ b/vsql/sqlstate.v @@ -51,6 +51,19 @@ pub fn sqlstate_from_int(code int) string { return string(b) } +// Divide by zero. +struct SQLState22012 { + msg string + code int +} + +fn sqlstate_22012() IError { + return SQLState22012{ + code: sqlstate_to_int('22012') + msg: 'division by zero' + } +} + // violates non-null constraint struct SQLState23502 { msg string @@ -58,7 +71,7 @@ struct SQLState23502 { } fn sqlstate_23502(msg string) IError { - return SQLState42804{ + return SQLState23502{ code: sqlstate_to_int('23502') msg: 'violates non-null constraint: $msg' } diff --git a/vsql/type.v b/vsql/type.v index 834c9ca..d3bb197 100644 --- a/vsql/type.v +++ b/vsql/type.v @@ -76,3 +76,10 @@ fn (t Type) uses_f64() bool { .is_null, .is_varchar, .is_character { false } } } + +fn (t Type) uses_string() bool { + return match t.typ { + .is_null, .is_boolean, .is_float, .is_bigint, .is_real, .is_smallint, .is_integer { false } + .is_varchar, .is_character { true } + } +} diff --git a/vsql/value.v b/vsql/value.v index d035585..ea69834 100644 --- a/vsql/value.v +++ b/vsql/value.v @@ -73,3 +73,12 @@ fn bool_str(x f64) string { else { 'UNKNOWN' } } } + +fn f64_string(x f64) string { + s := '${x:.6}'.trim('.').split('.') + if s.len == 1 { + return s[0] + } + + return '${s[0]}.${s[1].trim_right('0')}' +}