From 27f7c38c0cb9f13e53443e5f6b40aacb1695011e Mon Sep 17 00:00:00 2001 From: Elliot Chance Date: Mon, 26 Jul 2021 14:38:51 -0400 Subject: [PATCH] Adding NULL values and constraints (#10) - NULL can now be used as a value. - Specifying a `NOT NULL` constraint on table columns will ensure that NULL values will not be added to the table. `NULL` can also be explicitely provided to allow for `NULL`s. - New expressions in the form of `X IS NULL` and `X IS NOT NULL` can be used in both `SELECT` expressions and all `WHERE` clauses. - A new SQLSTATE 23502 violates non-null constraint has been added. As a consequence, also: - TRUE, FALSE and UNKNOWN (boolean values) can now be used in expressions. - SELECT statements support multiple expressions (not just a single value or `*`). However, the `*` is still not actually implemented. - The version stored in the databse file will now start to increment and be enforced for compatibility when opening a database. --- README.md | 1 + tests/boolean.sql | 8 ++ tests/create-table.sql | 3 + tests/insert.sql | 40 ++++++ tests/null.sql | 44 +++++++ tests/reserved-words.sql | 6 +- tests/select-literal.sql | 3 + tests/update.sql | 16 +++ vsql/ast.v | 29 ++++- vsql/cast.v | 4 + vsql/create_table.v | 2 +- vsql/delete.v | 4 +- vsql/eval.v | 46 ++++++- vsql/insert.v | 18 +++ vsql/lexer.v | 6 + vsql/parser.v | 275 +++++++++++++++++++++++++-------------- vsql/row.v | 1 + vsql/select.v | 21 ++- vsql/sqlstate.v | 12 ++ vsql/storage.v | 18 ++- vsql/table.v | 5 +- vsql/type.v | 4 +- vsql/update.v | 10 +- vsql/value.v | 12 +- vsql/where.v | 4 +- 25 files changed, 454 insertions(+), 138 deletions(-) create mode 100644 tests/boolean.sql create mode 100644 tests/null.sql diff --git a/README.md b/README.md index eb81201..b3edeed 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,7 @@ struct definitions. | SQLSTATE | Reason | | ---------- | ------ | +| `23502` | violates non-null constraint | | `42601` | syntax error | | `42703` | column does not exist | | `42804` | data type mismatch | diff --git a/tests/boolean.sql b/tests/boolean.sql new file mode 100644 index 0000000..1d37008 --- /dev/null +++ b/tests/boolean.sql @@ -0,0 +1,8 @@ +SELECT TRUE +-- COL1: TRUE + +SELECT FALSE +-- COL1: FALSE + +SELECT UNKNOWN +-- COL1: UNKNOWN diff --git a/tests/create-table.sql b/tests/create-table.sql index f6f61a7..fa233de 100644 --- a/tests/create-table.sql +++ b/tests/create-table.sql @@ -63,3 +63,6 @@ CREATE TABLE "foo" (a FLOAT) CREATE TABLE "Foo" (baz CHARACTER VARYING(10)) -- msg: CREATE TABLE 1 -- msg: CREATE TABLE 1 + +CREATE TABLE t1 (f1 CHARACTER VARYING(10) NULL, f2 FLOAT NOT NULL) +-- msg: CREATE TABLE 1 diff --git a/tests/insert.sql b/tests/insert.sql index e9b000e..b21dfef 100644 --- a/tests/insert.sql +++ b/tests/insert.sql @@ -48,3 +48,43 @@ CREATE TABLE foo (b BOOLEAN) INSERT INTO foo (b) VALUES (123) -- msg: CREATE TABLE 1 -- error: vsql.SQLState42804: data type mismatch for column B: expected BOOLEAN but got INTEGER + +CREATE TABLE t1 (f1 CHARACTER VARYING(10) NULL, f2 FLOAT NOT NULL) +INSERT INTO t1 (f1, f2) VALUES ('a', 1.23) +SELECT * FROM t1 +-- msg: CREATE TABLE 1 +-- msg: INSERT 1 +-- F1: a F2: 1.23 + +CREATE TABLE t1 (f1 CHARACTER VARYING(10) NULL, f2 FLOAT NOT NULL) +INSERT INTO t1 (f1, f2) VALUES ('a', NULL) +SELECT * FROM t1 +-- msg: CREATE TABLE 1 +-- error: vsql.SQLState42804: violates non-null constraint: column F2 + +CREATE TABLE t1 (f1 CHARACTER VARYING(10) NULL, f2 FLOAT NOT NULL) +INSERT INTO t1 (f1, f2) VALUES (NULL, 1.23) +SELECT * FROM t1 +-- msg: CREATE TABLE 1 +-- msg: INSERT 1 +-- F1: NULL F2: 1.23 + +CREATE TABLE t1 (f1 CHARACTER VARYING(10), f2 FLOAT) +INSERT INTO t1 (f1, f2) VALUES (NULL, NULL) +SELECT * FROM t1 +-- msg: CREATE TABLE 1 +-- msg: INSERT 1 +-- F1: NULL F2: NULL + +CREATE TABLE t1 (f1 VARCHAR(10) NULL, f2 FLOAT NOT NULL) +INSERT INTO t1 (f2) VALUES (1.23) +SELECT * FROM t1 +-- msg: CREATE TABLE 1 +-- msg: INSERT 1 +-- F1: NULL F2: 1.23 + +CREATE TABLE t1 (f1 CHARACTER VARYING(10) NULL, f2 FLOAT NOT NULL) +INSERT INTO t1 (f1) VALUES ('a') +SELECT * FROM t1 +-- msg: CREATE TABLE 1 +-- error: vsql.SQLState42804: violates non-null constraint: column F2 diff --git a/tests/null.sql b/tests/null.sql new file mode 100644 index 0000000..5070627 --- /dev/null +++ b/tests/null.sql @@ -0,0 +1,44 @@ +SELECT NULL +-- COL1: NULL + +SELECT 'a' IS NULL +-- COL1: FALSE + +SELECT 'a' IS NOT NULL +-- COL1: TRUE + +SELECT 1.23 IS NULL +-- COL1: FALSE + +SELECT 1.23 IS NOT NULL +-- COL1: TRUE + +SELECT 123 IS NULL +-- COL1: FALSE + +SELECT 123 IS NOT NULL +-- COL1: TRUE + +SELECT NULL IS NULL +-- COL1: TRUE + +SELECT NULL IS NOT NULL +-- COL1: FALSE + +CREATE TABLE foo (num FLOAT) +INSERT INTO foo (num) VALUES (13) +INSERT INTO foo (num) VALUES (NULL) +INSERT INTO foo (num) VALUES (35) +SELECT 'is null' +SELECT * FROM foo WHERE num IS NULL +SELECT 'is not null' +SELECT * FROM foo WHERE num IS NOT NULL +-- msg: CREATE TABLE 1 +-- msg: INSERT 1 +-- msg: INSERT 1 +-- msg: INSERT 1 +-- COL1: is null +-- NUM: NULL +-- COL1: is not null +-- NUM: 13 +-- NUM: 35 diff --git a/tests/reserved-words.sql b/tests/reserved-words.sql index 3ac4a53..2a6695b 100644 --- a/tests/reserved-words.sql +++ b/tests/reserved-words.sql @@ -473,7 +473,7 @@ CREATE TABLE INTO (a INT) -- error: vsql.SQLState42601: syntax error: expecting literal_identifier but found INTO CREATE TABLE IS (a INT) --- error: vsql.SQLState42601: syntax error: table name cannot be reserved word: IS +-- error: vsql.SQLState42601: syntax error: expecting literal_identifier but found IS CREATE TABLE JOIN (a INT) -- error: vsql.SQLState42601: syntax error: table name cannot be reserved word: JOIN @@ -629,7 +629,7 @@ CREATE TABLE NORMALIZE (a INT) -- error: vsql.SQLState42601: syntax error: table name cannot be reserved word: NORMALIZE CREATE TABLE NOT (a INT) --- error: vsql.SQLState42601: syntax error: table name cannot be reserved word: NOT +-- error: vsql.SQLState42601: syntax error: expecting literal_identifier but found NOT CREATE TABLE NTH_VALUE (a INT) -- error: vsql.SQLState42601: syntax error: table name cannot be reserved word: NTH_VALUE @@ -638,7 +638,7 @@ CREATE TABLE NTILE (a INT) -- error: vsql.SQLState42601: syntax error: table name cannot be reserved word: NTILE CREATE TABLE NULL (a INT) --- error: vsql.SQLState42601: syntax error: table name cannot be reserved word: NULL +-- error: vsql.SQLState42601: syntax error: expecting literal_identifier but found NULL CREATE TABLE NULLIF (a INT) -- error: vsql.SQLState42601: syntax error: table name cannot be reserved word: NULLIF diff --git a/tests/select-literal.sql b/tests/select-literal.sql index 8efb57d..f9b307f 100644 --- a/tests/select-literal.sql +++ b/tests/select-literal.sql @@ -9,3 +9,6 @@ select 789 Select 'hello' -- COL1: hello + +SELECT 123, 456 +-- COL1: 123 COL2: 456 diff --git a/tests/update.sql b/tests/update.sql index 92fa539..25667dc 100644 --- a/tests/update.sql +++ b/tests/update.sql @@ -33,3 +33,19 @@ CREATE TABLE foo (baz FLOAT) UPDATE foo SET baz = true -- msg: CREATE TABLE 1 -- error: vsql.SQLState42804: data type mismatch for column BAZ: expected FLOAT but got BOOLEAN + +CREATE TABLE foo (baz FLOAT) +INSERT INTO foo (baz) VALUES (123) +UPDATE foo SET baz = NULL +SELECT * FROM foo +-- msg: CREATE TABLE 1 +-- msg: INSERT 1 +-- msg: UPDATE 1 +-- BAZ: NULL + +CREATE TABLE foo (baz FLOAT NOT NULL) +INSERT INTO foo (baz) VALUES (123) +UPDATE foo SET baz = NULL +-- msg: CREATE TABLE 1 +-- msg: INSERT 1 +-- error: vsql.SQLState42804: violates non-null constraint: column BAZ diff --git a/vsql/ast.v b/vsql/ast.v index 478571f..f449f18 100644 --- a/vsql/ast.v +++ b/vsql/ast.v @@ -2,9 +2,12 @@ module vsql -// All possible root statments +// All possible root statments. type Stmt = CreateTableStmt | DeleteStmt | DropTableStmt | InsertStmt | SelectStmt | UpdateStmt +// All possible expression entities. +type Expr = BinaryExpr | Identifier | NoExpr | NullExpr | Value + // CREATE TABLE ... struct CreateTableStmt { table_name string @@ -14,7 +17,7 @@ struct CreateTableStmt { // DELETE ... struct DeleteStmt { table_name string - where BinaryExpr + where Expr } // DROP TABLE ... @@ -31,16 +34,27 @@ struct InsertStmt { // SELECT ... struct SelectStmt { - value Value + exprs []Expr from string - where BinaryExpr + where Expr } // UPDATE ... struct UpdateStmt { table_name string set map[string]Value - where BinaryExpr + where Expr +} + +// NullExpr for "IS NULL" and "IS NOT NULL". +struct NullExpr { + expr Expr + not bool +} + +// Identifier is foo or "Foo" +struct Identifier { + name string } struct BinaryExpr { @@ -48,3 +62,8 @@ struct BinaryExpr { op string value Value } + +// NoExpr is just a placeholder when there is no expression provided. +struct NoExpr { + dummy int // empty struct not allowed +} diff --git a/vsql/cast.v b/vsql/cast.v index 4c8b669..617dd9a 100644 --- a/vsql/cast.v +++ b/vsql/cast.v @@ -7,6 +7,10 @@ module vsql fn cast(msg string, v Value, to Type) ?Value { match v.typ.typ { + .is_null { + // A NULL can be any type so it's always castable. + return v + } .is_boolean { match to.typ { .is_boolean { return v } diff --git a/vsql/create_table.v b/vsql/create_table.v index fcbadb6..ff5492d 100644 --- a/vsql/create_table.v +++ b/vsql/create_table.v @@ -23,7 +23,7 @@ fn (mut c Connection) create_table(stmt CreateTableStmt) ?Result { return sqlstate_42601('column name cannot be reserved word: $column_name') } - columns << Column{column_name, column.typ} + columns << Column{column_name, column.typ, column.not_null} } c.storage.create_table(table_name, columns) ? diff --git a/vsql/delete.v b/vsql/delete.v index 07e6268..3578e9b 100644 --- a/vsql/delete.v +++ b/vsql/delete.v @@ -15,8 +15,8 @@ fn (mut c Connection) delete(stmt DeleteStmt) ?Result { mut deleted := 0 for row in rows { mut ok := true - if stmt.where.op != '' { - ok = eval(row, stmt.where) ? + if stmt.where !is NoExpr { + ok = eval_as_bool(row, stmt.where) ? } if ok { diff --git a/vsql/eval.v b/vsql/eval.v index b602077..fa2707a 100644 --- a/vsql/eval.v +++ b/vsql/eval.v @@ -2,22 +2,56 @@ module vsql -fn eval(data Row, e BinaryExpr) ?bool { - return eval_binary(data, e) +fn eval_as_value(data Row, e Expr) ?Value { + match e { + BinaryExpr { return eval_binary(data, e) } + Identifier { return eval_identifier(data, e) } + NullExpr { return eval_null(data, e) } + NoExpr { return sqlstate_42601('no expression provided') } + Value { return e } + } +} + +fn eval_as_bool(data Row, e Expr) ?bool { + v := eval_as_value(data, e) ? + + if v.typ.typ == .is_boolean { + return v.f64_value != 0 + } + + return sqlstate_42804('in expression', 'BOOLEAN', v.typ.str()) } -fn eval_binary(data Row, e BinaryExpr) ?bool { +fn eval_identifier(data Row, e Identifier) ?Value { + col := identifier_name(e.name) + value := data.data[col] or { panic(col) } + + return value +} + +fn eval_null(data Row, e NullExpr) ?Value { + value := eval_as_value(data, e.expr) ? + + if e.not { + return new_boolean_value(value.typ.typ != .is_null) + } + + return new_boolean_value(value.typ.typ == .is_null) +} + +fn eval_binary(data Row, e BinaryExpr) ?Value { col := identifier_name(e.col) 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) } + // TODO(elliotchance): Use the correct SQLSTATE error. return error('cannot $col $e.op $e.value.typ') } -fn eval_cmp(lhs T, rhs T, op string) bool { - return match op { +fn eval_cmp(lhs T, rhs T, op string) Value { + return new_boolean_value(match op { '=' { lhs == rhs } '!=' { lhs != rhs } '>' { lhs > rhs } @@ -27,5 +61,5 @@ fn eval_cmp(lhs T, rhs T, op string) bool { // This should not be possible because the parser has already verified // this. else { false } - } + }) } diff --git a/vsql/insert.v b/vsql/insert.v index 79ef4d8..69e794d 100644 --- a/vsql/insert.v +++ b/vsql/insert.v @@ -25,9 +25,27 @@ fn (mut c Connection) insert(stmt InsertStmt) ?Result { column_name := identifier_name(column) table_column := table.column(column_name) ? value := cast('for column $column_name', stmt.values[i], table_column.typ) ? + + if value.typ.typ == .is_null && table_column.not_null { + return sqlstate_23502('column $column_name') + } + row.data[column_name] = value } + // Fill in unspecified columns with NULL + for col in table.columns { + if col.name in row.data { + continue + } + + if col.not_null { + return sqlstate_23502('column $col.name') + } + + row.data[col.name] = new_null_value() + } + c.storage.write_row(row, table) ? return new_result_msg('INSERT 1') diff --git a/vsql/lexer.v b/vsql/lexer.v index ab75065..01811e5 100644 --- a/vsql/lexer.v +++ b/vsql/lexer.v @@ -20,6 +20,9 @@ enum TokenKind { keyword_int // INT keyword_integer // INTEGER keyword_into // INTO + keyword_is // IS + keyword_not // NOT + keyword_null // NULL keyword_precision // PRECISION keyword_real // REAL keyword_select // SELECT @@ -164,6 +167,9 @@ fn tokenize(sql string) []Token { 'INT' { Token{TokenKind.keyword_int, word} } 'INTEGER' { Token{TokenKind.keyword_integer, word} } 'INTO' { Token{TokenKind.keyword_into, word} } + 'IS' { Token{TokenKind.keyword_is, word} } + 'NOT' { Token{TokenKind.keyword_not, word} } + 'NULL' { Token{TokenKind.keyword_null, word} } 'PRECISION' { Token{TokenKind.keyword_precision, word} } 'REAL' { Token{TokenKind.keyword_real, word} } 'SELECT' { Token{TokenKind.keyword_select, word} } diff --git a/vsql/parser.v b/vsql/parser.v index f41ce8f..e1f9870 100644 --- a/vsql/parser.v +++ b/vsql/parser.v @@ -14,7 +14,7 @@ fn parse(sql string) ?Stmt { match tokens[0].kind { .keyword_create { - return parser.consume_create() or { return err } + return parser.consume_create_table() or { return err } } .keyword_delete { return parser.consume_delete() or { return err } @@ -58,35 +58,25 @@ fn (mut p Parser) consume_type() ?Type { // incomplete type. types := [ // 5 - [TokenKind.keyword_char, TokenKind.keyword_varying, TokenKind.op_paren_open, - TokenKind.literal_number, TokenKind.op_paren_close], - [TokenKind.keyword_character, TokenKind.keyword_varying, TokenKind.op_paren_open, - TokenKind.literal_number, TokenKind.op_paren_close], + [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], // 4 - [TokenKind.keyword_char, TokenKind.op_paren_open, TokenKind.literal_number, - TokenKind.op_paren_close, - ], - [TokenKind.keyword_character, TokenKind.op_paren_open, TokenKind.literal_number, - TokenKind.op_paren_close, - ], - [TokenKind.keyword_float, TokenKind.op_paren_open, TokenKind.literal_number, - TokenKind.op_paren_close, - ], - [TokenKind.keyword_varchar, TokenKind.op_paren_open, TokenKind.literal_number, - TokenKind.op_paren_close, - ], + [.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], // 2 - [TokenKind.keyword_double, TokenKind.keyword_precision], + [.keyword_double, .keyword_precision], // 1 - [TokenKind.keyword_bigint], - [TokenKind.keyword_boolean], - [TokenKind.keyword_character], - [TokenKind.keyword_char], - [TokenKind.keyword_float], - [TokenKind.keyword_integer], - [TokenKind.keyword_int], - [TokenKind.keyword_real], - [TokenKind.keyword_smallint], + [.keyword_bigint], + [.keyword_boolean], + [.keyword_character], + [.keyword_char], + [.keyword_float], + [.keyword_integer], + [.keyword_int], + [.keyword_real], + [.keyword_smallint], ] for typ in types { peek := p.peek(...typ) @@ -122,32 +112,43 @@ fn (mut p Parser) consume_type() ?Type { return sqlstate_42601('expecting type but found ${p.tokens[p.pos].value}') } -fn (mut p Parser) consume_create() ?CreateTableStmt { +fn (mut p Parser) consume_create_table() ?CreateTableStmt { // CREATE TABLE - p.consume(TokenKind.keyword_create) ? - p.consume(TokenKind.keyword_table) ? - table_name := p.consume(TokenKind.literal_identifier) ? + p.consume(.keyword_create) ? + p.consume(.keyword_table) ? + table_name := p.consume(.literal_identifier) ? // columns - p.consume(TokenKind.op_paren_open) ? + p.consume(.op_paren_open) ? mut columns := []Column{} - col_name := p.consume(TokenKind.literal_identifier) ? - col_type := p.consume_type() ? - columns << Column{col_name.value, col_type} + columns << p.consume_column_def() ? - for p.peek(TokenKind.op_comma).len > 0 { - p.consume(TokenKind.op_comma) ? - next_col_name := p.consume(TokenKind.literal_identifier) ? - next_col_type := p.consume_type() ? - columns << Column{next_col_name.value, next_col_type} + for p.peek(.op_comma).len > 0 { + p.consume(.op_comma) ? + columns << p.consume_column_def() ? } - p.consume(TokenKind.op_paren_close) ? + p.consume(.op_paren_close) ? return CreateTableStmt{table_name.value, columns} } +fn (mut p Parser) consume_column_def() ?Column { + col_name := p.consume(.literal_identifier) ? + col_type := p.consume_type() ? + + mut not_null := false + if p.peek(.keyword_not, .keyword_null).len > 0 { + p.pos += 2 + not_null = true + } else if p.peek(.keyword_null).len > 0 { + p.pos++ + } + + return Column{col_name.value, col_type, not_null} +} + fn (mut p Parser) consume(tk TokenKind) ?Token { if p.tokens[p.pos].kind == tk { defer { @@ -162,36 +163,36 @@ fn (mut p Parser) consume(tk TokenKind) ?Token { fn (mut p Parser) consume_insert() ?InsertStmt { // INSERT INTO - p.consume(TokenKind.keyword_insert) ? - p.consume(TokenKind.keyword_into) ? - table_name := p.consume(TokenKind.literal_identifier) ? + p.consume(.keyword_insert) ? + p.consume(.keyword_into) ? + table_name := p.consume(.literal_identifier) ? // columns mut cols := []string{} - p.consume(TokenKind.op_paren_open) ? - col := p.consume(TokenKind.literal_identifier) ? + p.consume(.op_paren_open) ? + col := p.consume(.literal_identifier) ? cols << col.value - for p.peek(TokenKind.op_comma).len > 0 { + for p.peek(.op_comma).len > 0 { p.pos++ - next_col := p.consume(TokenKind.literal_identifier) ? + next_col := p.consume(.literal_identifier) ? cols << next_col.value } - p.consume(TokenKind.op_paren_close) ? + p.consume(.op_paren_close) ? // values mut values := []Value{} - p.consume(TokenKind.keyword_values) ? - p.consume(TokenKind.op_paren_open) ? + p.consume(.keyword_values) ? + p.consume(.op_paren_open) ? values << p.consume_value() ? - for p.peek(TokenKind.op_comma).len > 0 { + for p.peek(.op_comma).len > 0 { p.pos++ values << p.consume_value() ? } - p.consume(TokenKind.op_paren_close) ? + p.consume(.op_paren_close) ? return InsertStmt{table_name.value, cols, values} } @@ -200,97 +201,170 @@ fn (mut p Parser) consume_select() ?SelectStmt { // skip SELECT p.pos++ - // fields - mut fields := new_varchar_value(p.tokens[p.pos].value, 0) - if p.tokens[p.pos].kind == TokenKind.literal_number { - fields = new_float_value(p.tokens[p.pos].value.f64()) + // expressions + mut exprs := []Expr{} + exprs << p.consume_expr() ? + + for p.peek(.op_comma).len > 0 { + p.pos++ // skip ',' + exprs << p.consume_expr() ? } - p.pos++ // FROM mut from := '' - if p.tokens[p.pos].kind == TokenKind.keyword_from { + if p.tokens[p.pos].kind == .keyword_from { from = p.tokens[p.pos + 1].value p.pos += 2 } // WHERE - mut expr := BinaryExpr{} - if p.peek(TokenKind.keyword_where).len > 0 { - expr = p.consume_where() ? + mut where := Expr(NoExpr{}) + if p.peek(.keyword_where).len > 0 { + p.pos++ // skip WHERE + where = p.consume_expr() ? } - return SelectStmt{fields, from, expr} + return SelectStmt{exprs, from, where} } fn (mut p Parser) consume_drop_table() ?DropTableStmt { // DROP TABLE - p.consume(TokenKind.keyword_drop) ? - p.consume(TokenKind.keyword_table) ? - table_name := p.consume(TokenKind.literal_identifier) ? + p.consume(.keyword_drop) ? + p.consume(.keyword_table) ? + table_name := p.consume(.literal_identifier) ? return DropTableStmt{table_name.value} } fn (mut p Parser) consume_delete() ?DeleteStmt { // DELETE FROM - p.consume(TokenKind.keyword_delete) ? - p.consume(TokenKind.keyword_from) ? - table_name := p.consume(TokenKind.literal_identifier) ? + p.consume(.keyword_delete) ? + p.consume(.keyword_from) ? + table_name := p.consume(.literal_identifier) ? // WHERE - mut expr := BinaryExpr{} - if p.peek(TokenKind.keyword_where).len > 0 { - expr = p.consume_where() ? + mut expr := Expr(NoExpr{}) + if p.peek(.keyword_where).len > 0 { + p.pos++ // skip WHERE + expr = p.consume_expr() ? } return DeleteStmt{table_name.value, expr} } -fn (mut p Parser) consume_where() ?BinaryExpr { - p.consume(TokenKind.keyword_where) ? +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 { + 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 + } + } +} + +fn (mut p Parser) consume_identifier() ?Identifier { + if p.peek(.literal_identifier).len > 0 { + p.pos++ + return Identifier{p.tokens[p.pos - 1].value} + } + + 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 { + start := p.pos - lhs := p.consume(TokenKind.literal_identifier) ? + 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} + } + + p.pos = start + return sqlstate_42601('expecting null expr but found ${p.tokens[p.pos].value}') +} + +fn (mut p Parser) consume_binary_expr() ?BinaryExpr { + start := p.pos + + lhs := p.consume(.literal_identifier) or { + p.pos = start + return err + } mut op := Token{} allowed_ops := [ TokenKind.op_eq, - TokenKind.op_neq, - TokenKind.op_gt, - TokenKind.op_gte, - TokenKind.op_lt, - TokenKind.op_lte, + .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) ? + op = p.consume(allowed_op) or { + p.pos = start + return err + } break } } - rhs := p.consume_value() ? + rhs := p.consume_value() or { + p.pos = start + return err + } return BinaryExpr{lhs.value, op.value, rhs} } fn (mut p Parser) consume_value() ?Value { - if p.peek(TokenKind.keyword_true).len > 0 { + if p.peek(.keyword_null).len > 0 { + p.pos++ + return new_null_value() + } + + if p.peek(.keyword_true).len > 0 { p.pos++ - return new_true_value() + return new_boolean_value(true) } - if p.peek(TokenKind.keyword_false).len > 0 { + if p.peek(.keyword_false).len > 0 { p.pos++ - return new_false_value() + return new_boolean_value(false) } - if p.peek(TokenKind.keyword_unknown).len > 0 { + if p.peek(.keyword_unknown).len > 0 { p.pos++ return new_unknown_value() } - if p.peek(TokenKind.literal_number).len > 0 { - t := p.consume(TokenKind.literal_number) ? + if p.peek(.literal_number).len > 0 { + t := p.consume(.literal_number) ? if t.value.contains('.') { return new_float_value(t.value.f64()) } @@ -298,31 +372,32 @@ fn (mut p Parser) consume_value() ?Value { return new_integer_value(t.value.int()) } - if p.peek(TokenKind.literal_string).len > 0 { - t := p.consume(TokenKind.literal_string) ? + if p.peek(.literal_string).len > 0 { + t := p.consume(.literal_string) ? return new_varchar_value(t.value, 0) } - return sqlstate_42601('expecting value but found ${p.tokens[p.pos]}') + return sqlstate_42601('expecting value but found ${p.tokens[p.pos].value}') } fn (mut p Parser) consume_update() ?UpdateStmt { // UPDATE - p.consume(TokenKind.keyword_update) ? - table_name := p.consume(TokenKind.literal_identifier) ? + p.consume(.keyword_update) ? + table_name := p.consume(.literal_identifier) ? // SET - p.consume(TokenKind.keyword_set) ? - col_name := p.consume(TokenKind.literal_identifier) ? - p.consume(TokenKind.op_eq) ? + p.consume(.keyword_set) ? + col_name := p.consume(.literal_identifier) ? + p.consume(.op_eq) ? col_value := p.consume_value() ? mut set := map[string]Value{} set[col_name.value] = col_value // WHERE - mut expr := BinaryExpr{} - if p.peek(TokenKind.keyword_where).len > 0 { - expr = p.consume_where() ? + mut expr := Expr(NoExpr{}) + if p.peek(.keyword_where).len > 0 { + p.pos++ // skip WHERE + expr = p.consume_expr() ? } return UpdateStmt{table_name.value, set, expr} diff --git a/vsql/row.v b/vsql/row.v index 248d092..f612e85 100644 --- a/vsql/row.v +++ b/vsql/row.v @@ -15,6 +15,7 @@ pub fn (r Row) get_f64(name string) f64 { pub fn (r Row) get_string(name string) string { return match r.data[name].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_varchar, .is_character { r.data[name].string_value } diff --git a/vsql/select.v b/vsql/select.v index 77d6e41..accc2e1 100644 --- a/vsql/select.v +++ b/vsql/select.v @@ -10,7 +10,7 @@ fn (mut c Connection) query_select(stmt SelectStmt) ?Result { table := c.storage.tables[table_name] mut rows := c.storage.read_rows(table.index) ? - if stmt.where.op != '' { + if stmt.where !is NoExpr { rows = where(rows, false, stmt.where) ? } @@ -20,11 +20,22 @@ fn (mut c Connection) query_select(stmt SelectStmt) ?Result { return sqlstate_42p01(table_name) } - return new_result(['COL1'], [ + mut data := map[string]Value{} + mut col_num := 1 + mut cols := []string{cap: stmt.exprs.len} + empty_row := Row{ + data: map[string]Value{} + } + for expr in stmt.exprs { + column_name := 'COL$col_num' + data[column_name] = eval_as_value(empty_row, expr) ? + cols << column_name + col_num++ + } + + return new_result(cols, [ Row{ - data: map{ - 'COL1': stmt.value - } + data: data }, ]) } diff --git a/vsql/sqlstate.v b/vsql/sqlstate.v index 94185df..e2fb3a2 100644 --- a/vsql/sqlstate.v +++ b/vsql/sqlstate.v @@ -7,6 +7,18 @@ module vsql +// violates non-null constraint +struct SQLState23502 { + msg string + code int +} + +fn sqlstate_23502(msg string) IError { + return SQLState42804{ + msg: 'violates non-null constraint: $msg' + } +} + // syntax error struct SQLState42601 { msg string diff --git a/vsql/storage.v b/vsql/storage.v index 74614e0..24e2f40 100644 --- a/vsql/storage.v +++ b/vsql/storage.v @@ -12,7 +12,7 @@ import os struct FileStorage { path string mut: - version i8 // should be = 1 + version i8 f os.File tables map[string]Table pos u32 @@ -28,11 +28,15 @@ struct FileStorageNextObject { } fn new_file_storage(path string) ?FileStorage { + // This is a rudimentary way to ensure that small changes to storage.v are + // compatible as things change so rapidly. Sorry if you had a database in a + // previous version, you'll need to recreate it. + current_version := i8(2) + // If the file doesn't exist we initialize it and reopen it. if !os.exists(path) { mut tmpf := os.create(path) ? - file_version := i8(1) - tmpf.write_raw(file_version) ? + tmpf.write_raw(current_version) ? tmpf.close() } @@ -44,6 +48,9 @@ fn new_file_storage(path string) ?FileStorage { } f.version = f.read() ? + if f.version != current_version { + return error('need version $current_version but database is $f.version') + } for { next := f.read_object() ? @@ -87,6 +94,7 @@ fn (mut f FileStorage) write_value(v Value) ? { f.write(v.typ.typ) ? match v.typ.typ { + .is_null {} .is_boolean, .is_float, .is_bigint, .is_integer, .is_real, .is_smallint { f.write(v.f64_value) ? } @@ -103,6 +111,9 @@ fn (mut f FileStorage) read_value() ?Value { typ := f.read() ? return match typ { + .is_null { + new_null_value() + } .is_boolean, .is_bigint, .is_float, .is_real, .is_smallint, .is_integer { Value{ typ: Type{typ, 0} @@ -125,6 +136,7 @@ fn (mut f FileStorage) read_value() ?Value { fn sizeof_value(value Value) int { return int(sizeof(SQLType) + match value.typ.typ { + .is_null { 0 } .is_boolean, .is_float, .is_integer, .is_bigint, .is_smallint, .is_real { sizeof(f64) } .is_varchar, .is_character { sizeof(int) + u32(value.string_value.len) } }) diff --git a/vsql/table.v b/vsql/table.v index fe21df5..32078a9 100644 --- a/vsql/table.v +++ b/vsql/table.v @@ -3,8 +3,9 @@ module vsql struct Column { - name string - typ Type + name string + typ Type + not_null bool } struct Table { diff --git a/vsql/type.v b/vsql/type.v index 5996866..834c9ca 100644 --- a/vsql/type.v +++ b/vsql/type.v @@ -8,6 +8,7 @@ struct Type { } enum SQLType { + is_null // NULL is_bigint // BIGINT is_boolean // BOOLEAN is_character // CHARACTER and CHAR @@ -20,6 +21,7 @@ enum SQLType { fn (t SQLType) str() string { return match t { + .is_null { 'NULL' } .is_bigint { 'BIGINT' } .is_boolean { 'BOOLEAN' } .is_character { 'CHARACTER' } @@ -71,6 +73,6 @@ fn (t Type) str() string { fn (t Type) uses_f64() bool { return match t.typ { .is_boolean, .is_float, .is_bigint, .is_real, .is_smallint, .is_integer { true } - .is_varchar, .is_character { false } + .is_null, .is_varchar, .is_character { false } } } diff --git a/vsql/update.v b/vsql/update.v index 48e1844..a09367f 100644 --- a/vsql/update.v +++ b/vsql/update.v @@ -15,7 +15,11 @@ fn (mut c Connection) update(stmt UpdateStmt) ?Result { for k, v in stmt.set { column_name := identifier_name(k) table_column := table.column(column_name) ? - cast('for column $column_name', v, table_column.typ) ? + value := cast('for column $column_name', v, table_column.typ) ? + + if table_column.not_null && value.typ.typ == .is_null { + return sqlstate_23502('column $column_name') + } } mut delete_rows := []Row{} @@ -23,8 +27,8 @@ fn (mut c Connection) update(stmt UpdateStmt) ?Result { for mut row in c.storage.read_rows(table.index) ? { // Missing WHERE matches all records mut ok := true - if stmt.where.op != '' { - ok = eval(row, stmt.where) ? + if stmt.where !is NoExpr { + ok = eval_as_bool(row, stmt.where) ? } if ok { diff --git a/vsql/value.v b/vsql/value.v index 89572a0..2e2d617 100644 --- a/vsql/value.v +++ b/vsql/value.v @@ -11,17 +11,16 @@ pub: string_value string // char and varchar } -fn new_true_value() Value { +fn new_null_value() Value { return Value{ - typ: Type{.is_boolean, 0} - f64_value: 1 + typ: Type{.is_null, 0} } } -fn new_false_value() Value { +fn new_boolean_value(b bool) Value { return Value{ typ: Type{.is_boolean, 0} - f64_value: 0 + f64_value: if b { 1 } else { 0 } } } @@ -55,6 +54,9 @@ fn new_varchar_value(x string, size int) Value { fn (v Value) == (v2 Value) bool { return match v.typ.typ { + .is_null { + false + } .is_boolean, .is_bigint, .is_integer, .is_smallint, .is_float, .is_real { v2.typ.typ == v.typ.typ && v.f64_value == v2.f64_value } diff --git a/vsql/where.v b/vsql/where.v index f6b0430..b9cf40f 100644 --- a/vsql/where.v +++ b/vsql/where.v @@ -2,10 +2,10 @@ module vsql -fn where(rows []Row, inverse bool, expr BinaryExpr) ?[]Row { +fn where(rows []Row, inverse bool, expr Expr) ?[]Row { mut new_rows := []Row{} for row in rows { - mut ok := eval(row, expr) ? + mut ok := eval_as_bool(row, expr) ? if inverse { ok = !ok }