Skip to content

Commit

Permalink
Adding NULL values and constraints (#10)
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
elliotchance authored Jul 26, 2021
1 parent 5aed64d commit 27f7c38
Show file tree
Hide file tree
Showing 25 changed files with 454 additions and 138 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
8 changes: 8 additions & 0 deletions tests/boolean.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
SELECT TRUE
-- COL1: TRUE

SELECT FALSE
-- COL1: FALSE

SELECT UNKNOWN
-- COL1: UNKNOWN
3 changes: 3 additions & 0 deletions tests/create-table.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
40 changes: 40 additions & 0 deletions tests/insert.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
44 changes: 44 additions & 0 deletions tests/null.sql
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions tests/reserved-words.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions tests/select-literal.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ select 789

Select 'hello'
-- COL1: hello

SELECT 123, 456
-- COL1: 123 COL2: 456
16 changes: 16 additions & 0 deletions tests/update.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
29 changes: 24 additions & 5 deletions vsql/ast.v
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,7 +17,7 @@ struct CreateTableStmt {
// DELETE ...
struct DeleteStmt {
table_name string
where BinaryExpr
where Expr
}

// DROP TABLE ...
Expand All @@ -31,20 +34,36 @@ 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 {
col string
op string
value Value
}

// NoExpr is just a placeholder when there is no expression provided.
struct NoExpr {
dummy int // empty struct not allowed
}
4 changes: 4 additions & 0 deletions vsql/cast.v
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
2 changes: 1 addition & 1 deletion vsql/create_table.v
Original file line number Diff line number Diff line change
Expand Up @@ -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) ?
Expand Down
4 changes: 2 additions & 2 deletions vsql/delete.v
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
46 changes: 40 additions & 6 deletions vsql/eval.v
Original file line number Diff line number Diff line change
Expand Up @@ -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<f64>(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<T>(lhs T, rhs T, op string) bool {
return match op {
fn eval_cmp<T>(lhs T, rhs T, op string) Value {
return new_boolean_value(match op {
'=' { lhs == rhs }
'!=' { lhs != rhs }
'>' { lhs > rhs }
Expand All @@ -27,5 +61,5 @@ fn eval_cmp<T>(lhs T, rhs T, op string) bool {
// This should not be possible because the parser has already verified
// this.
else { false }
}
})
}
18 changes: 18 additions & 0 deletions vsql/insert.v
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
6 changes: 6 additions & 0 deletions vsql/lexer.v
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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} }
Expand Down
Loading

0 comments on commit 27f7c38

Please sign in to comment.