Skip to content

Commit

Permalink
Greatly improved expressions (#18)
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
elliotchance authored Jul 29, 2021
1 parent 9fb4514 commit baa523a
Show file tree
Hide file tree
Showing 18 changed files with 482 additions and 124 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
61 changes: 55 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ no dependencies.
- [Appendix](#appendix)
- [Data Types](#data-types)
- [Keywords](#keywords)
- [Operators](#operators)
- [SQLSTATE (Errors)](#sqlstate-errors)
- [Testing](#testing)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
-------
Expand Down
38 changes: 38 additions & 0 deletions tests/arithmetic.sql
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions tests/comparison.sql
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions tests/concatenation.sql
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions tests/logical.sql
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions tests/null.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 3 additions & 3 deletions tests/reserved-words.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions tests/select-literal.sql
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ Select 'hello';

SELECT 123, 456;
-- COL1: 123 COL2: 456

SELECT 2 + 3 * 5, (2 + 3) * 5;
-- COL1: 17 COL2: 25
6 changes: 3 additions & 3 deletions tests/select-where.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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 '>=';
Expand All @@ -20,7 +20,7 @@ SELECT * FROM foo WHERE num <= 27;
-- msg: INSERT 1
-- COL1: =
-- NUM: 27
-- COL1: !=
-- COL1: <>
-- NUM: 27
-- NUM: 35
-- COL1: >
Expand Down
11 changes: 8 additions & 3 deletions vsql/ast.v
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down
86 changes: 80 additions & 6 deletions vsql/eval.v
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
}
Expand Down Expand Up @@ -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<f64>(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<f64>(left.f64_value, right.f64_value, e.op)
}

if left.typ.uses_string() && right.typ.uses_string() {
return eval_cmp<string>(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<T>(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 }
Expand Down
Loading

0 comments on commit baa523a

Please sign in to comment.