v/vlib/orm/orm_null_test.v
Tim Marston 756075380b
orm: add null handling and option fields (#19379)
* orm: added is none and !is none handling

* orm: added NullType, support option fields and deprecate [nonull]

Nullable DB fields are now determined by corresponding option struct field.  The
[nonull] attribute is deprecated and fields are all NOT NULL now, unless they
are option fields. New orm primitive, NullType, added to support passing none
values to db backends, which have been updated to support it.  Also, empty
string and 0 numberic values are no longer skipped during insert (since they may
be valid values).

* orm: fix [nonull] deprecation warning

* orm: add null handling to update and select

also, improved formatting for orm cgen, and removed optimised operand handling
of orm `is` and `!is` operators

* sqlite: read/report NULLs using new orm NullType

* postgres: returning data primitives now returns new orm.NullType

* orm: initialise NullType Primitives properly

* orm: do not smart cast operands inside sql

* orm: fix bad setting of option value

* orm: improve orm_null_test.v, adding/fixing selects

* orm: cleanup: rename NullType->Null, use serial const, cgen output

* orm: handle automatically generated fields more explicitly

During insert, fields which are
* [sql: serial]
* [default: whatever]
and where the data is a default value (e.g., 0, ""), those fields are not sent
to the db, so that the db can generate auto-increment or default values.  (This
was previously done only for [primary] fields, and not in all circumstances, but
that is not correct -- primary and serial/auto-increment fields are differnet.)

* orm: udpated README

* orm: select cgen fixes: read from uninit res; fail to init res

* orm: udpated tests

* orm: fix option sub-struct fields

* orm: fixed joins to option structs

Changed orm.write_orm_select() so that you pass to it the name of a resut
variable which it populates with the result (or not) and changed use of it in
sql_select_expr() and calls in write_orm_select() to populate substructs.

* orm: fix pg driver handling of NULL results

* orm: move runtime checks to comptime checker; cache checked tables

* orm: vfmt :(

* orm: markdown formatting

* orm: renamed orm.time_ and orm.enum_; updated db drivers

* checker: updated orm tests

* orm: fix issue setting up ast option values as orm primitives

* checker: ORM use of none/options and operations (added tests)

* orm: fixed tests

* db: clean code

* examples: remove orm nonull attributes

* orm: skip test memory santisation for orm_null_test.v

* orm: make the type-to-primitive converstion fns not public

* orm: mv object var c-code from checker->cgen; fix memory corruption

Code in checker/orm.v used the SqlStmtLine object field name to store c-specific
referenecs to option and array fields (for arrays of children).  I moved this
logic to cgen.  And fixed an issue introduced with option fields, where an array
of children was unpacked into a non-array result which could corrupt memory.

* orm: fixed vast error

* orm: skip 2 tests on ubuntu-musl which require sqlite3.h

* cgen: prevent casting a struct (string)

* v fmt orm_fkey_attribute.vv, orm_multidim_array.vv, orm_table_attributes.vv; run `VAUTOFIX=1 ./v vlib/v/compiler_errors_test.v`
2023-10-05 19:09:03 +03:00

307 lines
6.9 KiB
V

import orm
import db.sqlite
struct MockDBState {
mut:
last string
data []orm.Primitive
where []orm.Primitive
}
struct MockDB {
use_num bool
st &MockDBState = unsafe { nil }
db sqlite.DB
}
fn MockDB.new() &MockDB {
return &MockDB{
st: &MockDBState{}
db: sqlite.connect(':memory:') or { panic(err) }
}
}
fn (db MockDB) @select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive {
mut st := db.st
st.last = orm.orm_select_gen(config, '`', false, '?', 5, where)
st.data = data.data
st.where = where.data
return db.db.@select(config, data, where)
}
fn (db MockDB) insert(table string, data orm.QueryData) ! {
mut st := db.st
last, qdata := orm.orm_stmt_gen(.sqlite, table, '`', .insert, false, '?', 1, data,
orm.QueryData{})
st.last = last
st.data = qdata.data
st.where = []orm.Primitive{}
return db.db.insert(table, data)
}
fn (db MockDB) update(table string, data orm.QueryData, where orm.QueryData) ! {
mut st := db.st
st.last, _ = orm.orm_stmt_gen(.sqlite, table, '`', .update, false, '?', 1, data, where)
st.data = data.data
st.where = where.data
return db.db.update(table, data, where)
}
fn (db MockDB) delete(table string, where orm.QueryData) ! {
mut st := db.st
st.last, _ = orm.orm_stmt_gen(.sqlite, table, '`', .delete, false, '?', 1, orm.QueryData{},
where)
return db.db.delete(table, where)
}
const (
typ_to_typename = {
typeof[i8]().idx: 'i8'
typeof[i16]().idx: 'i16'
typeof[int]().idx: 'int'
typeof[i64]().idx: 'i64'
typeof[u8]().idx: 'u8'
typeof[u16]().idx: 'u16'
typeof[u32]().idx: 'u32'
typeof[u64]().idx: 'u64'
typeof[f32]().idx: 'f32'
typeof[f64]().idx: 'f64'
typeof[string]().idx: 'string'
typeof[bool]().idx: 'bool'
orm.serial: 'serial'
orm.time_: 'time'
orm.enum_: 'enum'
}
)
fn mock_type_from_v(typ int) !string {
return if typ in typ_to_typename {
'${typ_to_typename[typ]}-type'
} else {
error('unknown type ${typ}')
}
}
fn (db MockDB) create(table string, fields []orm.TableField) ! {
mut st := db.st
st.last = orm.orm_table_gen(table, '`', true, 0, fields, mock_type_from_v, false)!
return db.db.create(table, fields)
}
fn (db MockDB) drop(table string) ! {
return db.db.drop(table)
}
fn (db MockDB) last_id() int {
return db.db.last_id()
}
// --
[table: 'foo']
struct Foo {
mut:
id u64 [primary; sql: serial]
a string
// b string [default: '"yes"']
c ?string
d ?string = 'hi'
e int
// f int [default: 33]
g ?int
h ?int = 55
}
fn test_option_struct_fields_and_none() {
db := MockDB.new()
sql db {
create table Foo
}!
assert db.st.last == 'CREATE TABLE IF NOT EXISTS `foo` (`id` serial-type NOT NULL, `a` string-type NOT NULL, `c` string-type, `d` string-type, `e` int-type NOT NULL, `g` int-type, `h` int-type, PRIMARY KEY(`id`));'
_ := sql db {
select from Foo where e > 5 && c is none && c !is none && h == 2
}!
assert db.st.last == 'SELECT `id`, `a`, `c`, `d`, `e`, `g`, `h` FROM `foo` WHERE `e` > ? AND `c` IS ? AND `c` IS NOT ? AND `h` = ?;'
assert db.st.data.len == 0
assert db.st.where.len == 4
assert db.st.where == [orm.Primitive(int(5)), orm.Null{},
orm.Null{}, orm.Primitive(int(2))]
foo := Foo{}
sql db {
insert foo into Foo
}!
assert db.st.last == 'INSERT INTO `foo` (`a`, `c`, `d`, `e`, `g`, `h`) VALUES (?, ?, ?, ?, ?, ?);'
assert db.st.data.len == 6
assert db.st.data == [orm.Primitive(string('')), orm.Null{}, orm.Primitive(string('hi')), int(0),
orm.Null{}, int(55)]
id := db.last_id()
res1 := sql db {
select from Foo where id == id
}!
assert db.st.last == 'SELECT `id`, `a`, `c`, `d`, `e`, `g`, `h` FROM `foo` WHERE `id` = ?;'
assert db.st.data.len == 0
assert db.st.where.len == 1
assert db.st.where == [orm.Primitive(int(id))]
assert res1.len == 1
assert res1[0] == Foo{
id: 1
a: ''
c: none
d: 'hi'
e: 0
g: none
h: 55
}
sql db {
update Foo set c = 'yo', d = none, g = 44, h = none where id == id
}!
assert db.st.last == 'UPDATE `foo` SET `c` = ?, `d` = ?, `g` = ?, `h` = ? WHERE `id` = ?;'
assert db.st.data.len == 4
assert db.st.data == [orm.Primitive(string('yo')), orm.Null{}, int(44), orm.Null{}]
assert db.st.where.len == 1
assert db.st.where == [orm.Primitive(int(id))]
res2 := sql db {
select from Foo where id == id
}!
assert db.st.last == 'SELECT `id`, `a`, `c`, `d`, `e`, `g`, `h` FROM `foo` WHERE `id` = ?;'
assert db.st.data.len == 0
assert db.st.where.len == 1
assert db.st.where == [orm.Primitive(int(id))]
assert res2.len == 1
assert res2[0] == Foo{
id: 1
a: ''
c: 'yo'
d: none
e: 0
g: 44
h: none
}
assert sql db {
select count from Foo where a == 'yo'
}! == 0
assert sql db {
select count from Foo where d == 'yo'
}! == 0
assert sql db {
select count from Foo where c == 'yo'
}! == 1
assert sql db {
select count from Foo where a == ''
}! == 1
assert sql db {
select count from Foo where d == ''
}! == 0
assert sql db {
select count from Foo where c == ''
}! == 0
assert sql db {
select count from Foo where a is none
}! == 0
assert sql db {
select count from Foo where d is none
}! == 1
assert sql db {
select count from Foo where c is none
}! == 0
assert sql db {
select count from Foo where a !is none
}! == 1
assert sql db {
select count from Foo where d !is none
}! == 0
assert sql db {
select count from Foo where c !is none
}! == 1
}
struct Bar {
id u64 [primary; sql: serial]
name ?string
age int
}
fn update_bar1(db MockDB, id u64, name ?string) ! {
foo := 66
sql db {
update Bar set name = name, age = age + 3 + foo where id == id
}!
}
fn update_bar2(db MockDB, name ?string, new_name ?string) ! {
sql db {
update Bar set name = new_name where name == name
}!
}
type NameFn = fn () ?string
fn update_bar3(db MockDB, name_fn NameFn, new_name string) ! {
sql db {
update Bar set name = new_name where name == name_fn()
}!
}
fn test_inserting_passed_optionals() {
db := MockDB.new()
entry1 := Bar{}
entry2 := Bar{
name: 'Alice'
age: 55
}
entry3 := Bar{
name: 'Bob'
age: 66
}
sql db {
create table Bar
insert entry1 into Bar
insert entry2 into Bar
insert entry3 into Bar
}!
update_bar1(db, 2, none)!
update_bar1(db, 1, 'hi')!
res1 := sql db {
select from Bar
}!
assert res1.len == 3
assert res1[0].name or { '' } == 'hi'
assert res1[1].name == none
assert res1[2].name or { '' } == 'Bob'
update_bar2(db, none, 'xxx')! // no effect (select using "is none", not "== none")
update_bar2(db, 'hi', none)!
res2 := sql db {
select from Bar
}!
assert res2.len == 3
assert res2[0].name == none
assert res2[1].name == none
assert res2[2].name or { '' } == 'Bob'
update_bar3(db, fn () ?string {
return none // no effect (select using "is none", not "== none")
}, 'yyy')!
update_bar3(db, fn () ?string {
return 'Bob'
}, 'www')!
res3 := sql db {
select from Bar
}!
assert res3.len == 3
assert res3[0].name == none
assert res3[1].name == none
assert res3[2].name or { '' } == 'www'
}