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`
This commit is contained in:
Tim Marston 2023-10-05 17:09:03 +01:00 committed by GitHub
parent 32bb8cf86d
commit 756075380b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 1327 additions and 585 deletions

View file

@ -1770,7 +1770,7 @@ fn (t Tree) sql_stmt_line(node ast.SqlStmtLine) &Node {
obj.add_terse('ast_type', t.string_node('SqlStmtLine')) obj.add_terse('ast_type', t.string_node('SqlStmtLine'))
obj.add_terse('kind', t.enum_node(node.kind)) obj.add_terse('kind', t.enum_node(node.kind))
obj.add_terse('table_expr', t.type_expr(node.table_expr)) obj.add_terse('table_expr', t.type_expr(node.table_expr))
obj.add_terse('object_var_name', t.string_node(node.object_var_name)) obj.add_terse('object_var', t.string_node(node.object_var))
obj.add_terse('where_expr', t.expr(node.where_expr)) obj.add_terse('where_expr', t.expr(node.where_expr))
obj.add_terse('fields', t.array_node_struct_field(node.fields)) obj.add_terse('fields', t.array_node_struct_field(node.fields))
obj.add_terse('updated_columns', t.array_node_string(node.updated_columns)) obj.add_terse('updated_columns', t.array_node_string(node.updated_columns))

View file

@ -132,6 +132,7 @@ const (
'vlib/orm/orm_string_interpolation_in_where_test.v', 'vlib/orm/orm_string_interpolation_in_where_test.v',
'vlib/orm/orm_interface_test.v', 'vlib/orm/orm_interface_test.v',
'vlib/orm/orm_mut_db_test.v', 'vlib/orm/orm_mut_db_test.v',
'vlib/orm/orm_null_test.v',
'vlib/orm/orm_result_test.v', 'vlib/orm/orm_result_test.v',
'vlib/orm/orm_custom_operators_test.v', 'vlib/orm/orm_custom_operators_test.v',
'vlib/orm/orm_fk_test.v', 'vlib/orm/orm_fk_test.v',
@ -214,6 +215,7 @@ const (
'vlib/orm/orm_insert_test.v', 'vlib/orm/orm_insert_test.v',
'vlib/orm/orm_insert_reserved_name_test.v', 'vlib/orm/orm_insert_reserved_name_test.v',
'vlib/orm/orm_fn_calls_test.v', 'vlib/orm/orm_fn_calls_test.v',
'vlib/orm/orm_null_test.v',
'vlib/orm/orm_last_id_test.v', 'vlib/orm/orm_last_id_test.v',
'vlib/orm/orm_string_interpolation_in_where_test.v', 'vlib/orm/orm_string_interpolation_in_where_test.v',
'vlib/orm/orm_interface_test.v', 'vlib/orm/orm_interface_test.v',

View file

@ -4,6 +4,6 @@ module main
struct Product { struct Product {
id int [primary; sql: serial] id int [primary; sql: serial]
user_id int user_id int
name string [nonull; sql_type: 'TEXT'] name string [sql_type: 'TEXT']
created_at string [default: 'CURRENT_TIMESTAMP'] created_at string [default: 'CURRENT_TIMESTAMP']
} }

View file

@ -4,8 +4,8 @@ module main
pub struct User { pub struct User {
mut: mut:
id int [primary; sql: serial] id int [primary; sql: serial]
username string [nonull; sql_type: 'TEXT'; unique] username string [sql_type: 'TEXT'; unique]
password string [nonull; sql_type: 'TEXT'] password string [sql_type: 'TEXT']
active bool active bool
products []Product [fkey: 'user_id'] products []Product [fkey: 'user_id']
} }

View file

@ -75,7 +75,7 @@ pub fn (db DB) @select(config orm.SelectConfig, data orm.QueryData, where orm.Qu
orm.type_string { orm.type_string {
string_binds_map[i] = mysql_bind string_binds_map[i] = mysql_bind
} }
orm.time { orm.time_ {
match field_type { match field_type {
.type_long { .type_long {
mysql_bind.buffer_type = C.MYSQL_TYPE_LONG mysql_bind.buffer_type = C.MYSQL_TYPE_LONG
@ -250,6 +250,9 @@ fn stmt_bind_primitive(mut stmt Stmt, data orm.Primitive) {
orm.InfixType { orm.InfixType {
stmt_bind_primitive(mut stmt, data.right) stmt_bind_primitive(mut stmt, data.right)
} }
orm.Null {
stmt.bind_null()
}
} }
} }
@ -297,7 +300,7 @@ fn data_pointers_to_primitives(data_pointers []&u8, types []int, field_types []F
orm.type_string { orm.type_string {
primitive = unsafe { cstring_to_vstring(&char(data)) } primitive = unsafe { cstring_to_vstring(&char(data)) }
} }
orm.time { orm.time_ {
match field_types[i] { match field_types[i] {
.type_long { .type_long {
timestamp := *(unsafe { &int(data) }) timestamp := *(unsafe { &int(data) })
@ -310,6 +313,9 @@ fn data_pointers_to_primitives(data_pointers []&u8, types []int, field_types []F
else {} else {}
} }
} }
orm.enum_ {
primitive = *(unsafe { &i64(data) })
}
else { else {
return error('Unknown type ${types[i]}') return error('Unknown type ${types[i]}')
} }
@ -329,10 +335,10 @@ fn mysql_type_from_v(typ int) !string {
orm.type_idx['i16'], orm.type_idx['u16'] { orm.type_idx['i16'], orm.type_idx['u16'] {
'SMALLINT' 'SMALLINT'
} }
orm.type_idx['int'], orm.type_idx['u32'], orm.time { orm.type_idx['int'], orm.type_idx['u32'], orm.time_ {
'INT' 'INT'
} }
orm.type_idx['i64'], orm.type_idx['u64'] { orm.type_idx['i64'], orm.type_idx['u64'], orm.enum_ {
'BIGINT' 'BIGINT'
} }
orm.type_idx['f32'] { orm.type_idx['f32'] {

View file

@ -247,6 +247,14 @@ pub fn (mut stmt Stmt) bind_text(b string) {
stmt.bind(mysql.mysql_type_string, b.str, u32(b.len)) stmt.bind(mysql.mysql_type_string, b.str, u32(b.len))
} }
// bind_null binds a single NULL value to the statement `stmt`
pub fn (mut stmt Stmt) bind_null() {
stmt.binds << C.MYSQL_BIND{
buffer_type: mysql.mysql_type_null
length: 0
}
}
// bind binds a single value pointed by `buffer`, to the statement `stmt`. The buffer length must be passed as well in `buf_len`. // bind binds a single value pointed by `buffer`, to the statement `stmt`. The buffer length must be passed as well in `buf_len`.
// Note: it is more convenient to use one of the other bind_XYZ methods. // Note: it is more convenient to use one of the other bind_XYZ methods.
pub fn (mut stmt Stmt) bind(typ int, buffer voidptr, buf_len u32) { pub fn (mut stmt Stmt) bind(typ int, buffer voidptr, buf_len u32) {

View file

@ -10,18 +10,17 @@ import net.conv
pub fn (db DB) @select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive { pub fn (db DB) @select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive {
query := orm.orm_select_gen(config, '"', true, '$', 1, where) query := orm.orm_select_gen(config, '"', true, '$', 1, where)
res := pg_stmt_worker(db, query, where, data)! rows := pg_stmt_worker(db, query, where, data)!
mut ret := [][]orm.Primitive{} mut ret := [][]orm.Primitive{}
if config.is_count { if config.is_count {
} }
for row in res { for row in rows {
mut row_data := []orm.Primitive{} mut row_data := []orm.Primitive{}
for i, val in row.vals { for i, val in row.vals {
field := str_to_primitive(val, config.types[i])! row_data << val_to_primitive(val, config.types[i])!
row_data << field
} }
ret << row_data ret << row_data
} }
@ -189,6 +188,12 @@ fn pg_stmt_match(mut types []u32, mut vals []&char, mut lens []int, mut formats
orm.InfixType { orm.InfixType {
pg_stmt_match(mut types, mut vals, mut lens, mut formats, data.right) pg_stmt_match(mut types, mut vals, mut lens, mut formats, data.right)
} }
orm.Null {
types << u32(0) // we do not know col type, let server infer
vals << &char(0) // NULL pointer indicates NULL
lens << int(0) // ignored
formats << 0 // ignored
}
} }
} }
@ -203,9 +208,12 @@ fn pg_type_from_v(typ int) !string {
orm.type_idx['int'], orm.type_idx['u32'] { orm.type_idx['int'], orm.type_idx['u32'] {
'INT' 'INT'
} }
orm.time { orm.time_ {
'TIMESTAMP' 'TIMESTAMP'
} }
orm.enum_ {
'BIGINT'
}
orm.type_idx['i64'], orm.type_idx['u64'] { orm.type_idx['i64'], orm.type_idx['u64'] {
'BIGINT' 'BIGINT'
} }
@ -231,7 +239,8 @@ fn pg_type_from_v(typ int) !string {
return str return str
} }
fn str_to_primitive(str string, typ int) !orm.Primitive { fn val_to_primitive(val ?string, typ int) !orm.Primitive {
if str := val {
match typ { match typ {
// bool // bool
orm.type_idx['bool'] { orm.type_idx['bool'] {
@ -284,7 +293,7 @@ fn str_to_primitive(str string, typ int) !orm.Primitive {
orm.type_string { orm.type_string {
return orm.Primitive(str) return orm.Primitive(str)
} }
orm.time { orm.time_ {
if str.contains_any(' /:-') { if str.contains_any(' /:-') {
date_time_str := time.parse(str)! date_time_str := time.parse(str)!
return orm.Primitive(date_time_str) return orm.Primitive(date_time_str)
@ -293,7 +302,13 @@ fn str_to_primitive(str string, typ int) !orm.Primitive {
timestamp := str.int() timestamp := str.int()
return orm.Primitive(time.unix(timestamp)) return orm.Primitive(time.unix(timestamp))
} }
orm.enum_ {
return orm.Primitive(str.i64())
}
else {} else {}
} }
return error('Unknown field type ${typ}') return error('Unknown field type ${typ}')
} else {
return orm.Null{}
}
} }

View file

@ -43,7 +43,7 @@ mut:
pub struct Row { pub struct Row {
pub mut: pub mut:
vals []string vals []?string
} }
pub struct Config { pub struct Config {
@ -110,6 +110,8 @@ fn C.PQexec(res &C.PGconn, const_query &char) &C.PGresult
// //
fn C.PQgetisnull(const_res &C.PGresult, int, int) int
fn C.PQgetvalue(const_res &C.PGresult, int, int) &char fn C.PQgetvalue(const_res &C.PGresult, int, int) &char
fn C.PQresultStatus(const_res &C.PGresult) int fn C.PQresultStatus(const_res &C.PGresult) int
@ -179,10 +181,14 @@ fn res_to_rows(res voidptr) []Row {
for i in 0 .. nr_rows { for i in 0 .. nr_rows {
mut row := Row{} mut row := Row{}
for j in 0 .. nr_cols { for j in 0 .. nr_cols {
if C.PQgetisnull(res, i, j) != 0 {
row.vals << none
} else {
val := C.PQgetvalue(res, i, j) val := C.PQgetvalue(res, i, j)
sval := unsafe { val.vstring() } sval := unsafe { val.vstring() }
row.vals << sval row.vals << sval
} }
}
rows << row rows << row
} }
@ -209,7 +215,7 @@ pub fn (db DB) q_int(query string) !int {
return 0 return 0
} }
val := row.vals[0] val := row.vals[0]
return val.int() return val or { '0' }.int()
} }
// q_string submit a command to the database server and // q_string submit a command to the database server and
@ -226,7 +232,7 @@ pub fn (db DB) q_string(query string) !string {
return '' return ''
} }
val := row.vals[0] val := row.vals[0]
return val return val or { '' }
} }
// q_strings submit a command to the database server and // q_strings submit a command to the database server and

View file

@ -145,35 +145,43 @@ fn bind(stmt Stmt, c &int, data orm.Primitive) int {
orm.InfixType { orm.InfixType {
err = bind(stmt, c, data.right) err = bind(stmt, c, data.right)
} }
orm.Null {
err = stmt.bind_null(c)
}
} }
return err return err
} }
// Selects column in result and converts it to an orm.Primitive // Selects column in result and converts it to an orm.Primitive
fn (stmt Stmt) sqlite_select_column(idx int, typ int) !orm.Primitive { fn (stmt Stmt) sqlite_select_column(idx int, typ int) !orm.Primitive {
mut primitive := orm.Primitive(0)
if typ in orm.nums || typ == -1 { if typ in orm.nums || typ == -1 {
primitive = stmt.get_int(idx) return stmt.get_int(idx) or { return orm.Null{} }
} else if typ in orm.num64 { } else if typ in orm.num64 {
primitive = stmt.get_i64(idx) return stmt.get_i64(idx) or { return orm.Null{} }
} else if typ in orm.float { } else if typ in orm.float {
primitive = stmt.get_f64(idx) return stmt.get_f64(idx) or { return orm.Null{} }
} else if typ == orm.type_string { } else if typ == orm.type_string {
primitive = stmt.get_text(idx).clone() if v := stmt.get_text(idx) {
} else if typ == orm.time { return v.clone()
d := stmt.get_int(idx) } else {
primitive = time.unix(d) return orm.Null{}
}
} else if typ == orm.enum_ {
return stmt.get_i64(idx) or { return orm.Null{} }
} else if typ == orm.time_ {
if v := stmt.get_int(idx) {
return time.unix(v)
} else {
return orm.Null{}
}
} else { } else {
return error('Unknown type ${typ}') return error('Unknown type ${typ}')
} }
return primitive
} }
// Convert type int to sql type string // Convert type int to sql type string
fn sqlite_type_from_v(typ int) !string { fn sqlite_type_from_v(typ int) !string {
return if typ in orm.nums || typ < 0 || typ in orm.num64 || typ == orm.time { return if typ in orm.nums || typ in orm.num64 || typ in [orm.serial, orm.time_, orm.enum_] {
'INTEGER' 'INTEGER'
} else if typ in orm.float { } else if typ in orm.float {
'REAL' 'REAL'

View file

@ -116,6 +116,8 @@ fn C.sqlite3_column_double(&C.sqlite3_stmt, int) f64
fn C.sqlite3_column_count(&C.sqlite3_stmt) int fn C.sqlite3_column_count(&C.sqlite3_stmt) int
fn C.sqlite3_column_type(&C.sqlite3_stmt, int) int
// //
fn C.sqlite3_errstr(int) &char fn C.sqlite3_errstr(int) &char

View file

@ -16,9 +16,9 @@ struct TestCustomSqlType {
struct TestDefaultAtribute { struct TestDefaultAtribute {
id string [primary; sql: serial] id string [primary; sql: serial]
name string name string
created_at string [default: 'CURRENT_TIME'] created_at ?string [default: 'CURRENT_TIME']
created_at1 string [default: 'CURRENT_DATE'] created_at1 ?string [default: 'CURRENT_DATE']
created_at2 string [default: 'CURRENT_TIMESTAMP'] created_at2 ?string [default: 'CURRENT_TIMESTAMP']
} }
struct EntityToTest { struct EntityToTest {
@ -153,12 +153,12 @@ fn test_sqlite_orm() {
result_test_default_atribute := test_default_atributes.first() result_test_default_atribute := test_default_atributes.first()
assert result_test_default_atribute.name == 'Hitalo' assert result_test_default_atribute.name == 'Hitalo'
assert test_default_atribute.created_at.len == 0 assert test_default_atribute.created_at or { '' } == ''
assert test_default_atribute.created_at1.len == 0 assert test_default_atribute.created_at1 or { '' } == ''
assert test_default_atribute.created_at2.len == 0 assert test_default_atribute.created_at2 or { '' } == ''
assert result_test_default_atribute.created_at.len == 8 // HH:MM:SS assert result_test_default_atribute.created_at or { '' }.len == 8 // HH:MM:SS
assert result_test_default_atribute.created_at1.len == 10 // YYYY-MM-DD assert result_test_default_atribute.created_at1 or { '' }.len == 10 // YYYY-MM-DD
assert result_test_default_atribute.created_at2.len == 19 // YYYY-MM-DD HH:MM:SS assert result_test_default_atribute.created_at2 or { '' }.len == 19 // YYYY-MM-DD HH:MM:SS
sql db { sql db {
drop table TestDefaultAtribute drop table TestDefaultAtribute

View file

@ -1,5 +1,6 @@
module sqlite module sqlite
fn C.sqlite3_bind_null(&C.sqlite3_stmt, int) int
fn C.sqlite3_bind_double(&C.sqlite3_stmt, int, f64) int fn C.sqlite3_bind_double(&C.sqlite3_stmt, int, f64) int
fn C.sqlite3_bind_int(&C.sqlite3_stmt, int, int) int fn C.sqlite3_bind_int(&C.sqlite3_stmt, int, int) int
fn C.sqlite3_bind_int64(&C.sqlite3_stmt, int, i64) int fn C.sqlite3_bind_int64(&C.sqlite3_stmt, int, i64) int
@ -21,6 +22,10 @@ fn (db &DB) new_init_stmt(query string) !Stmt {
return Stmt{stmt, db} return Stmt{stmt, db}
} }
fn (stmt &Stmt) bind_null(idx int) int {
return C.sqlite3_bind_null(stmt.stmt, idx)
}
fn (stmt &Stmt) bind_int(idx int, v int) int { fn (stmt &Stmt) bind_int(idx int, v int) int {
return C.sqlite3_bind_int(stmt.stmt, idx, v) return C.sqlite3_bind_int(stmt.stmt, idx, v)
} }
@ -37,25 +42,40 @@ fn (stmt &Stmt) bind_text(idx int, s string) int {
return C.sqlite3_bind_text(stmt.stmt, idx, voidptr(s.str), s.len, 0) return C.sqlite3_bind_text(stmt.stmt, idx, voidptr(s.str), s.len, 0)
} }
fn (stmt &Stmt) get_int(idx int) int { fn (stmt &Stmt) get_int(idx int) ?int {
if C.sqlite3_column_type(stmt.stmt, idx) == C.SQLITE_NULL {
return none
} else {
return C.sqlite3_column_int(stmt.stmt, idx) return C.sqlite3_column_int(stmt.stmt, idx)
}
} }
fn (stmt &Stmt) get_i64(idx int) i64 { fn (stmt &Stmt) get_i64(idx int) ?i64 {
if C.sqlite3_column_type(stmt.stmt, idx) == C.SQLITE_NULL {
return none
} else {
return C.sqlite3_column_int64(stmt.stmt, idx) return C.sqlite3_column_int64(stmt.stmt, idx)
}
} }
fn (stmt &Stmt) get_f64(idx int) f64 { fn (stmt &Stmt) get_f64(idx int) ?f64 {
if C.sqlite3_column_type(stmt.stmt, idx) == C.SQLITE_NULL {
return none
} else {
return C.sqlite3_column_double(stmt.stmt, idx) return C.sqlite3_column_double(stmt.stmt, idx)
}
} }
fn (stmt &Stmt) get_text(idx int) string { fn (stmt &Stmt) get_text(idx int) ?string {
if C.sqlite3_column_type(stmt.stmt, idx) == C.SQLITE_NULL {
return none
} else {
b := &char(C.sqlite3_column_text(stmt.stmt, idx)) b := &char(C.sqlite3_column_text(stmt.stmt, idx))
if b == &char(0) { if b == &char(0) {
return '' return ''
} }
return unsafe { b.vstring() } return unsafe { b.vstring() }
}
} }
fn (stmt &Stmt) get_count() int { fn (stmt &Stmt) get_count() int {

View file

@ -1,5 +1,10 @@
# ORM # ORM
## Null
Use option fields in V structs for fields which can be NULL. Regular,
non-option fields are defied as NOT NULL when creating tables.
## Attributes ## Attributes
### Structs ### Structs
@ -11,12 +16,13 @@
- `[primary]` sets the field as the primary key - `[primary]` sets the field as the primary key
- `[unique]` sets the field as unique - `[unique]` sets the field as unique
- `[unique: 'foo']` adds the field to a unique group - `[unique: 'foo']` adds the field to a unique group
- `[nonull]` set the field as not null
- `[skip]` or `[sql: '-']` field will be skipped - `[skip]` or `[sql: '-']` field will be skipped
- `[sql: type]` where `type` is a V type such as `int` or `f64`, or special type `serial` - `[sql: type]` where `type` is a V type such as `int` or `f64`
- `[sql: serial]` lets the DB backend choose a column type for a auto-increment field
- `[sql: 'name']` sets a custom column name for the field - `[sql: 'name']` sets a custom column name for the field
- `[sql_type: 'SQL TYPE']` sets the sql type which is used in sql - `[sql_type: 'SQL TYPE']` sets the sql type which is used in sql
- `[default: 'sql defaults']` sets the default value or function when create a new table - `[default: 'raw_sql]` inserts `raw_sql` verbatim in a "DEFAULT" clause when
create a new table, allowing for values like `CURRENT_TIME`
- `[fkey: 'parent_id']` sets foreign key for an field which holds an array - `[fkey: 'parent_id']` sets foreign key for an field which holds an array
## Usage ## Usage
@ -24,12 +30,13 @@
```v ignore ```v ignore
import time import time
[table: 'foos']
struct Foo { struct Foo {
id int [primary; sql: serial] id int [primary; sql: serial]
name string [nonull] name string
created_at time.Time [sql_type: 'DATETIME'] created_at time.Time [default: 'CURRENT_TIME]
updated_at string [sql_type: 'DATETIME'] updated_at ?string [sql_type: 'TIMESTAMP]
deleted_at time.Time deleted_at ?time.Time
children []Child [fkey: 'parent_id'] children []Child [fkey: 'parent_id']
} }
@ -62,8 +69,8 @@ sql db {
foo := Foo{ foo := Foo{
name: 'abc' name: 'abc'
created_at: time.now() created_at: time.now()
updated_at: time.now() // updated_at defaults to none
deleted_at: time.now() // deleted_at defaults to none
children: [ children: [
Child{ Child{
name: 'abc' name: 'abc'
@ -79,14 +86,22 @@ sql db {
}! }!
``` ```
When inserting, `[sql: serial]` fields, and fields with a `[default: 'raw_sql']`
attribute are not sent to the database when the value being sent is the default
for the V struct field (e.g., 0 int, or an empty string). This allows the
database to insert default values for auto-increment fields and where you have
specified a default.
### Update ### Update
```v ignore ```v ignore
sql db { sql db {
update Foo set name = 'cde', updated_at = time.now() where name == 'abc' update Foo set updated_at = time.now() where name == 'abc' && updated_at is none
}! }!
``` ```
Note that `is none` and `!is none` can be used to select for NULL fields.
### Delete ### Delete
```v ignore ```v ignore
sql db { sql db {

View file

@ -18,8 +18,9 @@ pub const (
typeof[f64]().idx, typeof[f64]().idx,
] ]
type_string = typeof[string]().idx type_string = typeof[string]().idx
time = -2
serial = -1 serial = -1
time_ = -2
enum_ = -3
type_idx = { type_idx = {
'i8': typeof[i8]().idx 'i8': typeof[i8]().idx
'i16': typeof[i16]().idx 'i16': typeof[i16]().idx
@ -35,9 +36,11 @@ pub const (
'string': typeof[string]().idx 'string': typeof[string]().idx
} }
string_max_len = 2048 string_max_len = 2048
null_primitive = Primitive(Null{})
) )
pub type Primitive = InfixType pub type Primitive = InfixType
| Null
| bool | bool
| f32 | f32
| f64 | f64
@ -52,6 +55,8 @@ pub type Primitive = InfixType
| u64 | u64
| u8 | u8
pub struct Null {}
pub enum OperationKind { pub enum OperationKind {
neq // != neq // !=
eq // == eq // ==
@ -60,6 +65,8 @@ pub enum OperationKind {
ge // >= ge // >=
le // <= le // <=
orm_like // LIKE orm_like // LIKE
@is // is
is_not // !is
} }
pub enum MathOperationKind { pub enum MathOperationKind {
@ -94,6 +101,8 @@ fn (kind OperationKind) to_str() string {
.ge { '>=' } .ge { '>=' }
.le { '<=' } .le { '<=' }
.orm_like { 'LIKE' } .orm_like { 'LIKE' }
.@is { 'IS' }
.is_not { 'IS NOT' }
} }
return str return str
} }
@ -114,6 +123,7 @@ fn (kind OrderType) to_str() string {
// Every field, data, type & kind of operation in the expr share the same index in the arrays // Every field, data, type & kind of operation in the expr share the same index in the arrays
// is_and defines how they're addicted to each other either and or or // is_and defines how they're addicted to each other either and or or
// parentheses defines which fields will be inside () // parentheses defines which fields will be inside ()
// auto_fields are indexes of fields where db should generate a value when absent in an insert
pub struct QueryData { pub struct QueryData {
pub: pub:
fields []string fields []string
@ -121,7 +131,7 @@ pub:
types []int types []int
parentheses [][]int parentheses [][]int
kinds []OperationKind kinds []OperationKind
primary_column_name string auto_fields []int
is_and []bool is_and []bool
} }
@ -136,11 +146,10 @@ pub struct TableField {
pub: pub:
name string name string
typ int typ int
is_time bool nullable bool
default_val string default_val string
is_arr bool
is_enum bool
attrs []StructAttribute attrs []StructAttribute
is_arr bool
} }
// table - Table name // table - Table name
@ -190,7 +199,7 @@ pub interface Connection {
// Generates an sql stmt, from universal parameter // Generates an sql stmt, from universal parameter
// q - The quotes character, which can be different in every type, so it's variable // q - The quotes character, which can be different in every type, so it's variable
// num - Stmt uses nums at prepared statements (? or ?1) // num - Stmt uses nums at prepared statements (? or ?1)
// qm - Character for prepared statement, qm because of quotation mark like in sqlite // qm - Character for prepared statement (qm for question mark, as in sqlite)
// start_pos - When num is true, it's the start position of the counter // start_pos - When num is true, it's the start position of the counter
pub fn orm_stmt_gen(sql_dialect SQLDialect, table string, q string, kind StmtKind, num bool, qm string, start_pos int, data QueryData, where QueryData) (string, QueryData) { pub fn orm_stmt_gen(sql_dialect SQLDialect, table string, q string, kind StmtKind, num bool, qm string, start_pos int, data QueryData, where QueryData) (string, QueryData) {
mut str := '' mut str := ''
@ -205,37 +214,28 @@ pub fn orm_stmt_gen(sql_dialect SQLDialect, table string, q string, kind StmtKin
for i in 0 .. data.fields.len { for i in 0 .. data.fields.len {
column_name := data.fields[i] column_name := data.fields[i]
is_primary_column := column_name == data.primary_column_name is_auto_field := i in data.auto_fields
if data.data.len > 0 { if data.data.len > 0 {
// Allow the database to insert an automatically generated primary key // skip fields and allow the database to insert default and
// under the hood if it is not passed by the user. // serial (auto-increment) values where a default (or no)
tidx := data.data[i].type_idx() // value was provided
if is_primary_column && (tidx in orm.nums || tidx in orm.num64) { if is_auto_field {
x := data.data[i] mut x := data.data[i]
match x { skip_auto_field := match mut x {
i8, i16, int, i64, u8, u16, u32, u64 { Null { true }
if i64(x) == 0 { string { x == '' }
i8, i16, int, i64, u8, u16, u32, u64 { u64(x) == 0 }
f32, f64 { f64(x) == 0 }
time.Time { x == time.Time{} }
bool { !x }
else { false }
}
if skip_auto_field {
continue continue
} }
} }
else {}
}
}
match data.data[i].type_name() {
'string' {
if (data.data[i] as string).len == 0 {
continue
}
}
'time.Time' {
if (data.data[i] as time.Time).unix == 0 {
continue
}
}
else {}
}
data_data << data.data[i] data_data << data.data[i]
} }
select_fields << '${q}${column_name}${q}' select_fields << '${q}${column_name}${q}'
@ -300,35 +300,9 @@ pub fn orm_stmt_gen(sql_dialect SQLDialect, table string, q string, kind StmtKin
str += 'DELETE FROM ${q}${table}${q} WHERE ' str += 'DELETE FROM ${q}${table}${q} WHERE '
} }
} }
// where
if kind == .update || kind == .delete { if kind == .update || kind == .delete {
for i, field in where.fields { str += gen_where_clause(where, q, qm, num, mut &c)
mut pre_par := false
mut post_par := false
for par in where.parentheses {
if i in par {
pre_par = par[0] == i
post_par = par[1] == i
}
}
if pre_par {
str += '('
}
str += '${q}${field}${q} ${where.kinds[i].to_str()} ${qm}'
if num {
str += '${c}'
c++
}
if post_par {
str += ')'
}
if i < where.fields.len - 1 {
if where.is_and[i] {
str += ' AND '
} else {
str += ' OR '
}
}
}
} }
str += ';' str += ';'
$if trace_orm_stmt ? { $if trace_orm_stmt ? {
@ -371,34 +345,7 @@ pub fn orm_select_gen(cfg SelectConfig, q string, num bool, qm string, start_pos
if cfg.has_where { if cfg.has_where {
str += ' WHERE ' str += ' WHERE '
for i, field in where.fields { str += gen_where_clause(where, q, qm, num, mut &c)
mut pre_par := false
mut post_par := false
for par in where.parentheses {
if i in par {
pre_par = par[0] == i
post_par = par[1] == i
}
}
if pre_par {
str += '('
}
str += '${q}${field}${q} ${where.kinds[i].to_str()} ${qm}'
if num {
str += '${c}'
c++
}
if post_par {
str += ')'
}
if i < where.fields.len - 1 {
if where.is_and[i] {
str += ' AND '
} else {
str += ' OR '
}
}
}
} }
// Note: do not order, if the user did not want it explicitly, // Note: do not order, if the user did not want it explicitly,
@ -435,6 +382,39 @@ pub fn orm_select_gen(cfg SelectConfig, q string, num bool, qm string, start_pos
return str return str
} }
fn gen_where_clause(where QueryData, q string, qm string, num bool, mut c &int) string {
mut str := ''
for i, field in where.fields {
mut pre_par := false
mut post_par := false
for par in where.parentheses {
if i in par {
pre_par = par[0] == i
post_par = par[1] == i
}
}
if pre_par {
str += '('
}
str += '${q}${field}${q} ${where.kinds[i].to_str()} ${qm}'
if num {
str += '${c}'
c++
}
if post_par {
str += ')'
}
if i < where.fields.len - 1 {
if where.is_and[i] {
str += ' AND '
} else {
str += ' OR '
}
}
}
return str
}
// Generates an sql table stmt, from universal parameter // Generates an sql table stmt, from universal parameter
// table - Table name // table - Table name
// q - see orm_stmt_gen // q - see orm_stmt_gen
@ -461,14 +441,14 @@ pub fn orm_table_gen(table string, q string, defaults bool, def_unique_len int,
continue continue
} }
mut default_val := field.default_val mut default_val := field.default_val
mut no_null := false mut nullable := field.nullable
mut is_unique := false mut is_unique := false
mut is_skip := false mut is_skip := false
mut unique_len := 0 mut unique_len := 0
mut references_table := '' mut references_table := ''
mut references_field := '' mut references_field := ''
mut field_name := sql_field_name(field) mut field_name := sql_field_name(field)
mut ctyp := sql_from_v(sql_field_type(field)) or { mut col_typ := sql_from_v(sql_field_type(field)) or {
field_name = '${field_name}_id' field_name = '${field_name}_id'
sql_from_v(primary_typ)! sql_from_v(primary_typ)!
} }
@ -497,24 +477,15 @@ pub fn orm_table_gen(table string, q string, defaults bool, def_unique_len int,
} }
is_unique = true is_unique = true
} }
'nonull' {
no_null = true
}
'skip' { 'skip' {
is_skip = true is_skip = true
} }
'sql_type' { 'sql_type' {
if attr.kind != .string { col_typ = attr.arg.str()
return error("sql_type attribute needs to be string. Try [sql_type: '${attr.arg}'] instead of [sql_type: ${attr.arg}]")
}
ctyp = attr.arg
} }
'default' { 'default' {
if attr.kind != .string {
return error("default attribute needs to be string. Try [default: '${attr.arg}'] instead of [default: ${attr.arg}]")
}
if default_val == '' { if default_val == '' {
default_val = attr.arg default_val = attr.arg.str()
} }
} }
'references' { 'references' {
@ -549,19 +520,19 @@ pub fn orm_table_gen(table string, q string, defaults bool, def_unique_len int,
continue continue
} }
mut stmt := '' mut stmt := ''
if ctyp == '' { if col_typ == '' {
return error('Unknown type (${field.typ}) for field ${field.name} in struct ${table}') return error('Unknown type (${field.typ}) for field ${field.name} in struct ${table}')
} }
stmt = '${q}${field_name}${q} ${ctyp}' stmt = '${q}${field_name}${q} ${col_typ}'
if defaults && default_val != '' { if defaults && default_val != '' {
stmt += ' DEFAULT ${default_val}' stmt += ' DEFAULT ${default_val}'
} }
if no_null { if !nullable {
stmt += ' NOT NULL' stmt += ' NOT NULL'
} }
if is_unique { if is_unique {
mut f := 'UNIQUE(${q}${field_name}${q}' mut f := 'UNIQUE(${q}${field_name}${q}'
if ctyp == 'TEXT' && def_unique_len > 0 { if col_typ == 'TEXT' && def_unique_len > 0 {
if unique_len > 0 { if unique_len > 0 {
f += '(${unique_len})' f += '(${unique_len})'
} else { } else {
@ -607,15 +578,10 @@ pub fn orm_table_gen(table string, q string, defaults bool, def_unique_len int,
// Get's the sql field type // Get's the sql field type
fn sql_field_type(field TableField) int { fn sql_field_type(field TableField) int {
mut typ := field.typ mut typ := field.typ
if field.is_time {
return -2
} else if field.is_enum {
return typeof[i64]().idx
}
for attr in field.attrs { for attr in field.attrs {
if attr.kind == .plain && attr.name == 'sql' && attr.arg != '' { if attr.kind == .plain && attr.name == 'sql' && attr.arg != '' {
if attr.arg.to_lower() == 'serial' { if attr.arg.to_lower() == 'serial' {
typ = -1 typ = orm.serial
break break
} }
typ = orm.type_idx[attr.arg] typ = orm.type_idx[attr.arg]
@ -639,69 +605,129 @@ fn sql_field_name(field TableField) string {
// needed for backend functions // needed for backend functions
pub fn bool_to_primitive(b bool) Primitive { fn bool_to_primitive(b bool) Primitive {
return Primitive(b) return Primitive(b)
} }
pub fn f32_to_primitive(b f32) Primitive { fn option_bool_to_primitive(b ?bool) Primitive {
return if b_ := b { Primitive(b_) } else { orm.null_primitive }
}
fn f32_to_primitive(b f32) Primitive {
return Primitive(b) return Primitive(b)
} }
pub fn f64_to_primitive(b f64) Primitive { fn option_f32_to_primitive(b ?f32) Primitive {
return if b_ := b { Primitive(b_) } else { orm.null_primitive }
}
fn f64_to_primitive(b f64) Primitive {
return Primitive(b) return Primitive(b)
} }
pub fn i8_to_primitive(b i8) Primitive { fn option_f64_to_primitive(b ?f64) Primitive {
return if b_ := b { Primitive(b_) } else { orm.null_primitive }
}
fn i8_to_primitive(b i8) Primitive {
return Primitive(b) return Primitive(b)
} }
pub fn i16_to_primitive(b i16) Primitive { fn option_i8_to_primitive(b ?i8) Primitive {
return if b_ := b { Primitive(b_) } else { orm.null_primitive }
}
fn i16_to_primitive(b i16) Primitive {
return Primitive(b) return Primitive(b)
} }
pub fn int_to_primitive(b int) Primitive { fn option_i16_to_primitive(b ?i16) Primitive {
return if b_ := b { Primitive(b_) } else { orm.null_primitive }
}
fn int_to_primitive(b int) Primitive {
return Primitive(b) return Primitive(b)
} }
fn option_int_to_primitive(b ?int) Primitive {
return if b_ := b { Primitive(b_) } else { orm.null_primitive }
}
// int_literal_to_primitive handles int literal value // int_literal_to_primitive handles int literal value
pub fn int_literal_to_primitive(b int) Primitive { fn int_literal_to_primitive(b int) Primitive {
return Primitive(b) return Primitive(b)
} }
fn option_int_literal_to_primitive(b ?int) Primitive {
return if b_ := b { Primitive(b_) } else { orm.null_primitive }
}
// float_literal_to_primitive handles float literal value // float_literal_to_primitive handles float literal value
pub fn float_literal_to_primitive(b f64) Primitive { fn float_literal_to_primitive(b f64) Primitive {
return Primitive(b) return Primitive(b)
} }
pub fn i64_to_primitive(b i64) Primitive { fn option_float_literal_to_primitive(b ?f64) Primitive {
return if b_ := b { Primitive(b_) } else { orm.null_primitive }
}
fn i64_to_primitive(b i64) Primitive {
return Primitive(b) return Primitive(b)
} }
pub fn u8_to_primitive(b u8) Primitive { fn option_i64_to_primitive(b ?i64) Primitive {
return if b_ := b { Primitive(b_) } else { orm.null_primitive }
}
fn u8_to_primitive(b u8) Primitive {
return Primitive(b) return Primitive(b)
} }
pub fn u16_to_primitive(b u16) Primitive { fn option_u8_to_primitive(b ?u8) Primitive {
return if b_ := b { Primitive(b_) } else { orm.null_primitive }
}
fn u16_to_primitive(b u16) Primitive {
return Primitive(b) return Primitive(b)
} }
pub fn u32_to_primitive(b u32) Primitive { fn option_u16_to_primitive(b ?u16) Primitive {
return if b_ := b { Primitive(b_) } else { orm.null_primitive }
}
fn u32_to_primitive(b u32) Primitive {
return Primitive(b) return Primitive(b)
} }
pub fn u64_to_primitive(b u64) Primitive { fn option_u32_to_primitive(b ?u32) Primitive {
return if b_ := b { Primitive(b_) } else { orm.null_primitive }
}
fn u64_to_primitive(b u64) Primitive {
return Primitive(b) return Primitive(b)
} }
pub fn string_to_primitive(b string) Primitive { fn option_u64_to_primitive(b ?u64) Primitive {
return if b_ := b { Primitive(b_) } else { orm.null_primitive }
}
fn string_to_primitive(b string) Primitive {
return Primitive(b) return Primitive(b)
} }
pub fn time_to_primitive(b time.Time) Primitive { fn option_string_to_primitive(b ?string) Primitive {
return if b_ := b { Primitive(b_) } else { orm.null_primitive }
}
fn time_to_primitive(b time.Time) Primitive {
return Primitive(b) return Primitive(b)
} }
pub fn infix_to_primitive(b InfixType) Primitive { fn option_time_to_primitive(b ?time.Time) Primitive {
return if b_ := b { Primitive(b_) } else { orm.null_primitive }
}
fn infix_to_primitive(b InfixType) Primitive {
return Primitive(b) return Primitive(b)
} }

View file

@ -152,6 +152,7 @@ fn test_orm_table_gen() {
name: 'id' name: 'id'
typ: typeof[int]().idx typ: typeof[int]().idx
default_val: '10' default_val: '10'
nullable: true
attrs: [ attrs: [
StructAttribute{ StructAttribute{
name: 'primary' name: 'primary'
@ -167,10 +168,12 @@ fn test_orm_table_gen() {
orm.TableField{ orm.TableField{
name: 'test' name: 'test'
typ: typeof[string]().idx typ: typeof[string]().idx
nullable: true
}, },
orm.TableField{ orm.TableField{
name: 'abc' name: 'abc'
typ: typeof[i64]().idx typ: typeof[i64]().idx
nullable: true
default_val: '6754' default_val: '6754'
}, },
], sql_type_from_v, false) or { panic(err) } ], sql_type_from_v, false) or { panic(err) }
@ -180,6 +183,7 @@ fn test_orm_table_gen() {
orm.TableField{ orm.TableField{
name: 'id' name: 'id'
typ: typeof[int]().idx typ: typeof[int]().idx
nullable: true
default_val: '10' default_val: '10'
attrs: [ attrs: [
StructAttribute{ StructAttribute{
@ -196,10 +200,12 @@ fn test_orm_table_gen() {
orm.TableField{ orm.TableField{
name: 'test' name: 'test'
typ: typeof[string]().idx typ: typeof[string]().idx
nullable: true
}, },
orm.TableField{ orm.TableField{
name: 'abc' name: 'abc'
typ: typeof[i64]().idx typ: typeof[i64]().idx
nullable: true
default_val: '6754' default_val: '6754'
}, },
], sql_type_from_v, true) or { panic(err) } ], sql_type_from_v, true) or { panic(err) }
@ -209,6 +215,7 @@ fn test_orm_table_gen() {
orm.TableField{ orm.TableField{
name: 'id' name: 'id'
typ: typeof[int]().idx typ: typeof[int]().idx
nullable: true
default_val: '10' default_val: '10'
attrs: [ attrs: [
StructAttribute{ StructAttribute{
@ -237,12 +244,13 @@ fn test_orm_table_gen() {
default_val: '6754' default_val: '6754'
}, },
], sql_type_from_v, false) or { panic(err) } ], sql_type_from_v, false) or { panic(err) }
assert unique_query == "CREATE TABLE IF NOT EXISTS 'test_table' ('id' SERIAL DEFAULT 10, 'test' TEXT, 'abc' INT64 DEFAULT 6754, PRIMARY KEY('id'), UNIQUE('test'));" assert unique_query == "CREATE TABLE IF NOT EXISTS 'test_table' ('id' SERIAL DEFAULT 10, 'test' TEXT NOT NULL, 'abc' INT64 DEFAULT 6754 NOT NULL, PRIMARY KEY('id'), UNIQUE('test'));"
mult_unique_query := orm.orm_table_gen('test_table', "'", true, 0, [ mult_unique_query := orm.orm_table_gen('test_table', "'", true, 0, [
orm.TableField{ orm.TableField{
name: 'id' name: 'id'
typ: typeof[int]().idx typ: typeof[int]().idx
nullable: true
default_val: '10' default_val: '10'
attrs: [ attrs: [
StructAttribute{ StructAttribute{
@ -259,6 +267,7 @@ fn test_orm_table_gen() {
orm.TableField{ orm.TableField{
name: 'test' name: 'test'
typ: typeof[string]().idx typ: typeof[string]().idx
nullable: true
attrs: [ attrs: [
StructAttribute{ StructAttribute{
name: 'unique' name: 'unique'
@ -271,6 +280,7 @@ fn test_orm_table_gen() {
orm.TableField{ orm.TableField{
name: 'abc' name: 'abc'
typ: typeof[i64]().idx typ: typeof[i64]().idx
nullable: true
default_val: '6754' default_val: '6754'
attrs: [ attrs: [
StructAttribute{ StructAttribute{
@ -294,7 +304,7 @@ fn sql_type_from_v(typ int) !string {
'DOUBLE' 'DOUBLE'
} else if typ == orm.type_string { } else if typ == orm.type_string {
'TEXT' 'TEXT'
} else if typ == -1 { } else if typ == orm.serial {
'SERIAL' 'SERIAL'
} else { } else {
error('Unknown type ${typ}') error('Unknown type ${typ}')

View file

@ -29,7 +29,13 @@ struct Account {
struct Package { struct Package {
id int [primary; sql: serial] id int [primary; sql: serial]
name string [unique] name string [unique]
author User [fkey: 'id'] author User [fkey: 'id'] // mandatory user
}
struct Delivery {
id int [primary; sql: serial]
name string [unique]
author ?User [fkey: 'id'] // optional user
} }
struct User { struct User {
@ -138,7 +144,7 @@ fn test_float_primary_key() {
assert entities.first() == entity assert entities.first() == entity
} }
fn test_does_not_insert_uninitialized_field() { fn test_does_not_insert_uninitialized_mandatory_field() {
db := sqlite.connect(':memory:')! db := sqlite.connect(':memory:')!
sql db { sql db {
@ -151,9 +157,13 @@ fn test_does_not_insert_uninitialized_field() {
// author // author
} }
mut query_successful := true
sql db { sql db {
insert package into Package insert package into Package
}! } or { query_successful = false }
assert !query_successful
users := sql db { users := sql db {
select from User select from User
@ -163,7 +173,7 @@ fn test_does_not_insert_uninitialized_field() {
assert users.len == 0 assert users.len == 0
} }
fn test_insert_empty_field() { fn test_insert_empty_mandatory_field() {
db := sqlite.connect(':memory:')! db := sqlite.connect(':memory:')!
sql db { sql db {
@ -187,6 +197,54 @@ fn test_insert_empty_field() {
assert users.len == 1 assert users.len == 1
} }
fn test_does_insert_uninitialized_optional_field() {
db := sqlite.connect(':memory:')!
sql db {
create table User
create table Delivery
}!
package := Delivery{
name: 'wow'
// author
}
sql db {
insert package into Delivery
}!
users := sql db {
select from User
}!
assert users.len == 0 // no user added
}
fn test_insert_empty_optional_field() {
db := sqlite.connect(':memory:')!
sql db {
create table User
create table Delivery
}!
package := Delivery{
name: 'bob'
author: User{}
}
sql db {
insert package into Delivery
}!
users := sql db {
select from User
}!
assert users.len == 1 // user was added
}
fn test_insert_empty_object() { fn test_insert_empty_object() {
db := sqlite.connect(':memory:')! db := sqlite.connect(':memory:')!

307
vlib/orm/orm_null_test.v Normal file
View file

@ -0,0 +1,307 @@
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'
}

View file

@ -12,7 +12,7 @@ struct Module {
name string name string
nr_downloads int nr_downloads int
test_id u64 test_id u64
user User user ?User
created time.Time created time.Time
} }

View file

@ -1919,7 +1919,7 @@ pub:
is_generated bool is_generated bool
scope &Scope = unsafe { nil } scope &Scope = unsafe { nil }
pub mut: pub mut:
object_var_name string // `user` object_var string // `user`
updated_columns []string // for `update set x=y` updated_columns []string // for `update set x=y`
table_expr TypeNode table_expr TypeNode
fields []StructField fields []StructField

View file

@ -133,7 +133,8 @@ mut:
goto_labels map[string]ast.GotoLabel // to check for unused goto labels goto_labels map[string]ast.GotoLabel // to check for unused goto labels
enum_data_type ast.Type enum_data_type ast.Type
fn_return_type ast.Type fn_return_type ast.Type
orm_table_fields map[string][]ast.StructField // known table structs
//
v_current_commit_hash string // same as V_CURRENT_COMMIT_HASH v_current_commit_hash string // same as V_CURRENT_COMMIT_HASH
} }

View file

@ -14,7 +14,7 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type {
c.expected_type = left_type c.expected_type = left_type
// `if n is ast.Ident && n.is_mut { ... }` // `if n is ast.Ident && n.is_mut { ... }`
if node.op == .and { if !c.inside_sql && node.op == .and {
mut left_node := node.left mut left_node := node.left
for mut left_node is ast.InfixExpr { for mut left_node is ast.InfixExpr {
if left_node.op == .and && mut left_node.right is ast.InfixExpr { if left_node.op == .and && mut left_node.right is ast.InfixExpr {
@ -654,9 +654,13 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type {
parent_left_type := left_sym.info.sum_type parent_left_type := left_sym.info.sum_type
left_sym = c.table.sym(parent_left_type) left_sym = c.table.sym(parent_left_type)
} }
if left_sym.kind !in [.interface_, .sum_type] { if c.inside_sql {
if typ != ast.none_type_idx {
c.error('`${op}` can only be used to test for none in sql', node.pos)
}
} else if left_sym.kind !in [.interface_, .sum_type] {
c.error('`${op}` can only be used with interfaces and sum types', c.error('`${op}` can only be used with interfaces and sum types',
node.pos) node.pos) // can be used in sql too, but keep err simple
} else if mut left_sym.info is ast.SumType { } else if mut left_sym.info is ast.SumType {
if typ !in left_sym.info.variants { if typ !in left_sym.info.variants {
c.error('`${left_sym.name}` has no variant `${right_sym.name}`', c.error('`${left_sym.name}` has no variant `${right_sym.name}`',
@ -749,7 +753,8 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type {
if left_is_option || right_is_option { if left_is_option || right_is_option {
opt_infix_pos := if left_is_option { left_pos } else { right_pos } opt_infix_pos := if left_is_option { left_pos } else { right_pos }
if (node.left !in [ast.Ident, ast.SelectorExpr, ast.ComptimeSelector] if (node.left !in [ast.Ident, ast.SelectorExpr, ast.ComptimeSelector]
|| node.op in [.eq, .ne, .lt, .gt, .le, .ge]) && right_sym.kind != .none_ { || node.op in [.eq, .ne, .lt, .gt, .le, .ge]) && right_sym.kind != .none_
&& !c.inside_sql {
c.error('unwrapped Option cannot be used in an infix expression', opt_infix_pos) c.error('unwrapped Option cannot be used in an infix expression', opt_infix_pos)
} }
} }

View file

@ -6,13 +6,6 @@ import v.ast
import v.token import v.token
import v.util import v.util
const (
v_orm_prefix = 'V ORM'
fkey_attr_name = 'fkey'
pkey_attr_name = 'primary'
connection_interface_name = 'orm.Connection'
)
type ORMExpr = ast.SqlExpr | ast.SqlStmt type ORMExpr = ast.SqlExpr | ast.SqlStmt
fn (mut c Checker) sql_expr(mut node ast.SqlExpr) ast.Type { fn (mut c Checker) sql_expr(mut node ast.SqlExpr) ast.Type {
@ -43,44 +36,43 @@ fn (mut c Checker) sql_expr(mut node ast.SqlExpr) ast.Type {
} }
info := table_sym.info as ast.Struct info := table_sym.info as ast.Struct
mut fields := c.fetch_and_verify_orm_fields(info, node.table_expr.pos, table_sym.name, mut fields := c.fetch_and_check_orm_fields(info, node.table_expr.pos, table_sym.name,
node) node)
non_primitive_fields := c.get_orm_non_primitive_fields(fields) non_primitive_fields := c.get_orm_non_primitive_fields(fields)
mut sub_structs := map[int]ast.SqlExpr{} mut sub_structs := map[int]ast.SqlExpr{}
mut has_pkey_attr := false mut has_primary := false
mut pkey_field := ast.StructField{} mut primary_field := ast.StructField{}
for field in fields { for field in fields {
for attr in field.attrs { if field.attrs.contains('primary') {
if attr.name == checker.pkey_attr_name { if has_primary {
if has_pkey_attr {
c.orm_error('a struct can only have one primary key', field.pos) c.orm_error('a struct can only have one primary key', field.pos)
} }
has_pkey_attr = true has_primary = true
pkey_field = field primary_field = field
}
} }
} }
for field in non_primitive_fields { for field in non_primitive_fields {
if c.table.sym(field.typ).kind == .array && !has_pkey_attr { if c.table.sym(field.typ).kind == .array && !has_primary {
c.orm_error('a struct that has a field that holds an array must have a primary key', c.orm_error('a struct that has a field that holds an array must have a primary key',
field.pos) field.pos)
} }
c.check_orm_struct_field_attributes(field) c.check_orm_non_primitive_struct_field_attrs(field)
typ := c.get_type_of_field_with_related_table(field) foreign_typ := c.get_field_foreign_table_type(field)
mut subquery_expr := ast.SqlExpr{ mut subquery_expr := ast.SqlExpr{
pos: node.pos pos: node.pos
has_where: true has_where: true
where_expr: ast.None{} where_expr: ast.None{}
typ: typ.set_flag(.result) typ: field.typ.clear_flag(.option).set_flag(.result)
db_expr: node.db_expr db_expr: node.db_expr
table_expr: ast.TypeNode{ table_expr: ast.TypeNode{
pos: node.table_expr.pos pos: node.table_expr.pos
typ: typ typ: foreign_typ
} }
is_generated: true is_generated: true
} }
@ -123,25 +115,25 @@ fn (mut c Checker) sql_expr(mut node ast.SqlExpr) ast.Type {
if c.table.sym(field.typ).kind == .array { if c.table.sym(field.typ).kind == .array {
mut where_expr := subquery_expr.where_expr mut where_expr := subquery_expr.where_expr
if mut where_expr is ast.InfixExpr { if mut where_expr is ast.InfixExpr {
where_expr.left_type = pkey_field.typ where_expr.left_type = primary_field.typ
where_expr.right_type = pkey_field.typ where_expr.right_type = primary_field.typ
mut left := where_expr.left mut left := where_expr.left
if mut left is ast.Ident { if mut left is ast.Ident {
left.name = pkey_field.name left.name = primary_field.name
} }
mut right := where_expr.right mut right := where_expr.right
if mut right is ast.Ident { if mut right is ast.Ident {
mut right_info := right.info mut right_info := right.info
if mut right_info is ast.IdentVar { if mut right_info is ast.IdentVar {
right_info.typ = pkey_field.typ right_info.typ = primary_field.typ
} }
} }
} }
} }
sub_structs[int(typ)] = subquery_expr sub_structs[int(field.typ)] = subquery_expr
} }
if node.is_count { if node.is_count {
@ -235,7 +227,7 @@ fn (mut c Checker) sql_stmt_line(mut node ast.SqlStmtLine) ast.Type {
c.cur_orm_ts = old_ts c.cur_orm_ts = old_ts
} }
inserting_object_name := node.object_var_name inserting_object_name := node.object_var
if node.kind == .insert && !node.is_generated { if node.kind == .insert && !node.is_generated {
inserting_object := node.scope.find(inserting_object_name) or { inserting_object := node.scope.find(inserting_object_name) or {
@ -263,8 +255,13 @@ fn (mut c Checker) sql_stmt_line(mut node ast.SqlStmtLine) ast.Type {
} }
info := table_sym.info as ast.Struct info := table_sym.info as ast.Struct
mut fields := c.fetch_and_verify_orm_fields(info, node.table_expr.pos, table_sym.name, mut fields := c.fetch_and_check_orm_fields(info, node.table_expr.pos, table_sym.name,
ast.SqlExpr{}) ast.SqlExpr{})
for field in fields {
c.check_orm_struct_field_attrs(node, field)
}
mut sub_structs := map[int]ast.SqlStmtLine{} mut sub_structs := map[int]ast.SqlStmtLine{}
non_primitive_fields := c.get_orm_non_primitive_fields(fields) non_primitive_fields := c.get_orm_non_primitive_fields(fields)
@ -276,30 +273,25 @@ fn (mut c Checker) sql_stmt_line(mut node ast.SqlStmtLine) ast.Type {
continue continue
} }
c.check_orm_struct_field_attributes(field) c.check_orm_non_primitive_struct_field_attrs(field)
typ := c.get_type_of_field_with_related_table(field) foreign_typ := c.get_field_foreign_table_type(field)
mut object_var_name := '${node.object_var_name}.${field.name}'
if typ != field.typ {
object_var_name = node.object_var_name
}
mut subquery_expr := ast.SqlStmtLine{ mut subquery_expr := ast.SqlStmtLine{
pos: node.pos pos: node.pos
kind: node.kind kind: node.kind
table_expr: ast.TypeNode{ table_expr: ast.TypeNode{
pos: node.table_expr.pos pos: node.table_expr.pos
typ: typ typ: foreign_typ
} }
object_var_name: object_var_name object_var: field.name
is_generated: true is_generated: true
} }
tmp_inside_sql := c.inside_sql tmp_inside_sql := c.inside_sql
c.sql_stmt_line(mut subquery_expr) c.sql_stmt_line(mut subquery_expr)
c.inside_sql = tmp_inside_sql c.inside_sql = tmp_inside_sql
sub_structs[typ] = subquery_expr sub_structs[field.typ] = subquery_expr
} }
node.fields = fields node.fields = fields
@ -336,27 +328,29 @@ fn (mut c Checker) sql_stmt_line(mut node ast.SqlStmtLine) ast.Type {
return ast.void_type return ast.void_type
} }
fn (mut c Checker) check_orm_struct_field_attributes(field ast.StructField) { fn (mut c Checker) check_orm_struct_field_attrs(node ast.SqlStmtLine, field ast.StructField) {
for attr in field.attrs {
if attr.name == 'nonull' {
c.warn('`nonull` attribute is deprecated; non-optional fields are always "NOT NULL", use Option fields where they can be NULL',
node.pos)
}
}
}
fn (mut c Checker) check_orm_non_primitive_struct_field_attrs(field ast.StructField) {
field_type := c.table.sym(field.typ) field_type := c.table.sym(field.typ)
mut has_fkey_attr := false mut has_fkey_attr := false
for attr in field.attrs { for attr in field.attrs {
if attr.name == checker.fkey_attr_name { if attr.name == 'fkey' {
if field_type.kind != .array && field_type.kind != .struct_ { if field_type.kind != .array && field_type.kind != .struct_ {
c.orm_error('the `${checker.fkey_attr_name}` attribute must be used only with arrays and structures', c.orm_error('the `fkey` attribute must be used only with arrays and structures',
attr.pos) attr.pos)
return return
} }
if !attr.has_arg { if !attr.has_arg {
c.orm_error('the `${checker.fkey_attr_name}` attribute must have an argument', c.orm_error('the `fkey` attribute must have an argument', attr.pos)
attr.pos)
return
}
if attr.kind != .string {
c.orm_error("`${checker.fkey_attr_name}` attribute must be string. Try [${checker.fkey_attr_name}: '${attr.arg}'] instead of [${checker.fkey_attr_name}: ${attr.arg}]",
attr.pos)
return return
} }
@ -377,44 +371,60 @@ fn (mut c Checker) check_orm_struct_field_attributes(field ast.StructField) {
} }
if field_type.kind == .array && !has_fkey_attr { if field_type.kind == .array && !has_fkey_attr {
c.orm_error('a field that holds an array must be defined with the `${checker.fkey_attr_name}` attribute', c.orm_error('a field that holds an array must be defined with the `fkey` attribute',
field.pos) field.pos)
} }
} }
fn (mut c Checker) fetch_and_verify_orm_fields(info ast.Struct, pos token.Pos, table_name string, sql_expr ast.SqlExpr) []ast.StructField { fn (mut c Checker) fetch_and_check_orm_fields(info ast.Struct, pos token.Pos, table_name string, sql_expr ast.SqlExpr) []ast.StructField {
field_pos := c.orm_get_field_pos(sql_expr.where_expr) if cache := c.orm_table_fields[table_name] {
return cache
}
mut fields := []ast.StructField{} mut fields := []ast.StructField{}
for field in info.fields { for field in info.fields {
is_primitive := field.typ.is_string() || field.typ.is_bool() || field.typ.is_number() if field.attrs.contains('skip') || field.attrs.contains_arg('sql', '-') {
fsym := c.table.sym(field.typ)
is_struct := fsym.kind == .struct_
is_array := fsym.kind == .array
is_enum := fsym.kind == .enum_
elem_sym := if is_array {
c.table.sym(fsym.array_info().elem_type)
} else {
ast.invalid_type_symbol
}
is_array_with_struct_elements := is_array && elem_sym.kind == .struct_
has_skip_attr := field.attrs.contains('skip') || field.attrs.contains_arg('sql', '-')
if has_skip_attr {
continue continue
} }
if is_primitive || is_struct || is_enum || is_array_with_struct_elements { field_sym := c.table.sym(field.typ)
fields << field is_primitive := field.typ.is_string() || field.typ.is_bool() || field.typ.is_number()
is_struct := field_sym.kind == .struct_
is_array := field_sym.kind == .array
is_enum := field_sym.kind == .enum_
mut is_array_of_structs := false
if is_array {
array_info := field_sym.array_info()
elem_sym := c.table.sym(array_info.elem_type)
is_array_of_structs = elem_sym.kind == .struct_
if attr := field.attrs.find_first('fkey') {
if attr.arg == '' {
c.orm_error('fkey attribute must have an argument', attr.pos)
} }
if is_array && elem_sym.is_primitive() { } else {
c.add_error_detail('') c.orm_error('array fields must have an fkey attribute', field.pos)
c.add_error_detail(' field name: `${field.name}`') }
c.add_error_detail(' data type: `${c.table.type_to_str(field.typ)}`') if array_info.nr_dims > 1 || elem_sym.kind == .array {
c.orm_error('does not support array of primitive types', field_pos) c.orm_error('multi-dimension array fields are not supported', field.pos)
return []ast.StructField{} }
}
if attr := field.attrs.find_first('sql') {
if attr.arg == '' {
c.orm_error('sql attribute must have an argument', attr.pos)
}
}
if is_primitive || is_struct || is_enum || is_array_of_structs {
fields << field
} }
} }
if fields.len == 0 { if fields.len == 0 {
c.orm_error('select: empty fields in `${table_name}`', pos) c.orm_error('select: empty fields in `${table_name}`', pos)
} }
if attr := info.attrs.find_first('table') {
if attr.arg == '' {
c.orm_error('table attribute must have an argument', attr.pos)
}
}
c.orm_table_fields[table_name] = fields
return fields return fields
} }
@ -465,7 +475,7 @@ fn (mut c Checker) check_sql_expr_type_is_int(expr &ast.Expr, sql_keyword string
} }
fn (mut c Checker) orm_error(message string, pos token.Pos) { fn (mut c Checker) orm_error(message string, pos token.Pos) {
c.error('${checker.v_orm_prefix}: ${message}', pos) c.error('ORM: ${message}', pos)
} }
// check_expr_has_no_fn_calls_with_non_orm_return_type checks that an expression has no function calls // check_expr_has_no_fn_calls_with_non_orm_return_type checks that an expression has no function calls
@ -499,6 +509,13 @@ fn (mut c Checker) check_expr_has_no_fn_calls_with_non_orm_return_type(expr &ast
} else if expr is ast.InfixExpr { } else if expr is ast.InfixExpr {
c.check_expr_has_no_fn_calls_with_non_orm_return_type(expr.left) c.check_expr_has_no_fn_calls_with_non_orm_return_type(expr.left)
c.check_expr_has_no_fn_calls_with_non_orm_return_type(expr.right) c.check_expr_has_no_fn_calls_with_non_orm_return_type(expr.right)
if expr.right_type.has_flag(.option) && expr.op !in [.key_is, .not_is] {
c.warn('comparison with Option value probably isn\'t intended; use "is none" and "!is none" to select by NULL',
expr.pos)
} else if expr.right_type == ast.none_type && expr.op !in [.key_is, .not_is] {
c.warn('comparison with none probably isn\'t intended; use "is none" and "!is none" to select by NULL',
expr.pos)
}
} }
} }
@ -572,10 +589,10 @@ fn (mut c Checker) check_orm_or_expr(mut expr ORMExpr) {
if expr.or_expr.kind == .absent { if expr.or_expr.kind == .absent {
if c.inside_defer { if c.inside_defer {
c.error('V ORM returns a result, so it should have an `or {}` block at the end', c.error('ORM returns a result, so it should have an `or {}` block at the end',
expr.pos) expr.pos)
} else { } else {
c.error('V ORM returns a result, so it should have either an `or {}` block, or `!` at the end', c.error('ORM returns a result, so it should have either an `or {}` block, or `!` at the end',
expr.pos) expr.pos)
} }
} else { } else {
@ -595,15 +612,14 @@ fn (mut c Checker) check_orm_or_expr(mut expr ORMExpr) {
// check_db_expr checks the `db_expr` implements `orm.Connection` and has no `option` flag. // check_db_expr checks the `db_expr` implements `orm.Connection` and has no `option` flag.
fn (mut c Checker) check_db_expr(mut db_expr ast.Expr) bool { fn (mut c Checker) check_db_expr(mut db_expr ast.Expr) bool {
connection_type_index := c.table.find_type_idx(checker.connection_interface_name) connection_type_index := c.table.find_type_idx('orm.Connection')
connection_typ := ast.Type(connection_type_index) connection_typ := ast.Type(connection_type_index)
db_expr_type := c.expr(mut db_expr) db_expr_type := c.expr(mut db_expr)
// If we didn't find `orm.Connection`, we don't have any imported modules // If we didn't find `orm.Connection`, we don't have any imported modules
// that depend on `orm` and implement the `orm.Connection` interface. // that depend on `orm` and implement the `orm.Connection` interface.
if connection_type_index == 0 { if connection_type_index == 0 {
c.error('expected a type that implements the `${checker.connection_interface_name}` interface', c.error('expected a type that implements the `orm.Connection` interface', db_expr.pos())
db_expr.pos())
return false return false
} }
@ -630,10 +646,10 @@ fn (mut c Checker) check_orm_table_expr_type(type_node &ast.TypeNode) bool {
return true return true
} }
// get_type_of_field_with_related_table gets the type of table in which // get_field_foreign_table_type gets the type of table in which the primary key
// the primary key is used as the foreign key in the current table. // is referred to by the provided field. For example, the `[]Child` field
// For example, if you are using `[]Child`, the related table type would be `Child`. // refers to the foreign table `Child`.
fn (c &Checker) get_type_of_field_with_related_table(table_field &ast.StructField) ast.Type { fn (c &Checker) get_field_foreign_table_type(table_field &ast.StructField) ast.Type {
if c.table.sym(table_field.typ).kind == .struct_ { if c.table.sym(table_field.typ).kind == .struct_ {
return table_field.typ return table_field.typ
} else if c.table.sym(table_field.typ).kind == .array { } else if c.table.sym(table_field.typ).kind == .array {
@ -673,7 +689,7 @@ fn (c &Checker) get_orm_non_primitive_fields(fields []ast.StructField) []ast.Str
// ``` // ```
// TODO: rewrite it, move to runtime. // TODO: rewrite it, move to runtime.
fn (_ &Checker) check_field_of_inserting_struct_is_uninitialized(node &ast.SqlStmtLine, field_name string) bool { fn (_ &Checker) check_field_of_inserting_struct_is_uninitialized(node &ast.SqlStmtLine, field_name string) bool {
struct_scope := node.scope.find_var(node.object_var_name) or { return false } struct_scope := node.scope.find_var(node.object_var) or { return false }
if struct_scope.expr is ast.StructInit { if struct_scope.expr is ast.StructInit {
return struct_scope.expr.init_fields.filter(it.name == field_name).len == 0 return struct_scope.expr.init_fields.filter(it.name == field_name).len == 0

View file

@ -19,7 +19,7 @@ vlib/v/checker/tests/like_operator_with_non_string_type_error.vv:16:34: error: t
| ~~~~ | ~~~~
17 | }!) 17 | }!)
18 | } 18 | }
vlib/v/checker/tests/like_operator_with_non_string_type_error.vv:16:26: error: V ORM: left side of the `like` expression must be one of the `User`'s fields vlib/v/checker/tests/like_operator_with_non_string_type_error.vv:16:26: error: ORM: left side of the `like` expression must be one of the `User`'s fields
14 | 14 |
15 | println(sql db { 15 | println(sql db {
16 | select from User where 10 like true 16 | select from User where 10 like true

View file

@ -1,4 +1,4 @@
vlib/v/checker/tests/orm_empty_struct.vv:9:15: error: V ORM: select: empty fields in `Person` vlib/v/checker/tests/orm_empty_struct.vv:9:15: error: ORM: select: empty fields in `Person`
7 | db := sqlite.connect(':memory:')! 7 | db := sqlite.connect(':memory:')!
8 | _ := sql db { 8 | _ := sql db {
9 | select from Person 9 | select from Person

View file

@ -0,0 +1,7 @@
vlib/v/checker/tests/orm_fkey_attribute.vv:11:14: error: ORM: the `fkey` attribute must have an argument
9 | id int [primary; sql: serial]
10 | name string
11 | user User [fkey]
| ~~~~~~
12 | }
13 |

View file

@ -0,0 +1,19 @@
import db.sqlite
struct User {
id int [primary; sql: serial]
}
[table: foo]
struct Foo {
id int [primary; sql: serial]
name string
user User [fkey]
}
fn main() {
db := sqlite.connect(':memory:')!
sql db {
create table Foo
}!
}

View file

@ -1,4 +1,4 @@
vlib/v/checker/tests/orm_fkey_has_pkey.vv:5:2: error: V ORM: a struct that has a field that holds an array must have a primary key vlib/v/checker/tests/orm_fkey_has_pkey.vv:5:2: error: ORM: a struct that has a field that holds an array must have a primary key
3 | struct Person { 3 | struct Person {
4 | id int 4 | id int
5 | child []Child [fkey: 'person_id'] 5 | child []Child [fkey: 'person_id']

View file

@ -1,4 +1,4 @@
vlib/v/checker/tests/orm_fn_call_with_wrong_return_type.vv:52:37: error: V ORM: function calls must return only primitive types and time.Time, but `get_second_child` returns `Child` vlib/v/checker/tests/orm_fn_call_with_wrong_return_type.vv:52:37: error: ORM: function calls must return only primitive types and time.Time, but `get_second_child` returns `Child`
50 | 50 |
51 | steve := sql db { 51 | steve := sql db {
52 | select from Parent where child == get_second_child() 52 | select from Parent where child == get_second_child()

View file

@ -1,4 +1,4 @@
vlib/v/checker/tests/orm_left_side_expr_in_infix_expr_has_no_struct_field_error.vv:18:26: error: V ORM: left side of the `==` expression must be one of the `User`'s fields. vlib/v/checker/tests/orm_left_side_expr_in_infix_expr_has_no_struct_field_error.vv:18:26: error: ORM: left side of the `==` expression must be one of the `User`'s fields.
1 possibility: `id`. 1 possibility: `id`.
16 | 16 |
17 | users := sql db { 17 | users := sql db {

View file

@ -1,4 +1,4 @@
vlib/v/checker/tests/orm_limit_less_than_zero_error.vv:18:26: error: V ORM: `limit` must be greater than or equal to zero vlib/v/checker/tests/orm_limit_less_than_zero_error.vv:18:26: error: ORM: `limit` must be greater than or equal to zero
16 | 16 |
17 | users := sql db { 17 | users := sql db {
18 | select from User limit user_limit 18 | select from User limit user_limit

View file

@ -0,0 +1,7 @@
vlib/v/checker/tests/orm_multidim_array.vv:12:2: error: ORM: multi-dimension array fields are not supported
10 | id int [primary; sql: serial]
11 | users []User [fkey: id]
12 | bad [][]User [fkey: id]
| ~~~~~~~~~~~~~~
13 | }
14 |

View file

@ -0,0 +1,20 @@
import db.sqlite
[table: users]
struct User {
id int [pimary]
}
[table: foos]
struct Foo {
id int [primary; sql: serial]
users []User [fkey: id]
bad [][]User [fkey: id]
}
fn main() {
db := sqlite.connect(':memory:')!
sql db {
create table Foo
}!
}

View file

@ -1,4 +1,4 @@
vlib/v/checker/tests/orm_multiple_pkeys.vv:5:2: error: V ORM: a struct can only have one primary key vlib/v/checker/tests/orm_multiple_pkeys.vv:5:2: error: ORM: a struct can only have one primary key
3 | struct Person { 3 | struct Person {
4 | id int [primary] 4 | id int [primary]
5 | name string [primary] 5 | name string [primary]

View file

@ -1,4 +1,4 @@
vlib/v/checker/tests/orm_not_a_struct.vv:10:15: error: V ORM: the table symbol `Person` has to be a struct vlib/v/checker/tests/orm_not_a_struct.vv:10:15: error: ORM: the table symbol `Person` has to be a struct
8 | db := sqlite.connect(':memory:')! 8 | db := sqlite.connect(':memory:')!
9 | _ := sql db { 9 | _ := sql db {
10 | select from Person 10 | select from Person

View file

@ -0,0 +1,49 @@
vlib/v/checker/tests/orm_op_with_option_and_none.vv:11:30: warning: comparison with Option value probably isn't intended; use "is none" and "!is none" to select by NULL
9 | db := sqlite.connect(':memory:')!
10 | _ := sql db {
11 | select from Foo where name == val
| ~~
12 | }!
13 | }
vlib/v/checker/tests/orm_op_with_option_and_none.vv:25:30: warning: comparison with none probably isn't intended; use "is none" and "!is none" to select by NULL
23 |
24 | _ := sql db {
25 | select from Foo where name == none
| ~~
26 | }!
27 |
vlib/v/checker/tests/orm_op_with_option_and_none.vv:29:30: warning: comparison with none probably isn't intended; use "is none" and "!is none" to select by NULL
27 |
28 | _ := sql db {
29 | select from Foo where name != none
| ~~
30 | }!
31 |
vlib/v/checker/tests/orm_op_with_option_and_none.vv:33:30: warning: comparison with none probably isn't intended; use "is none" and "!is none" to select by NULL
31 |
32 | _ := sql db {
33 | select from Foo where name < none
| ^
34 | }!
35 |
vlib/v/checker/tests/orm_op_with_option_and_none.vv:37:30: warning: comparison with none probably isn't intended; use "is none" and "!is none" to select by NULL
35 |
36 | _ := sql db {
37 | select from Foo where name > none
| ^
38 | }!
39 |
vlib/v/checker/tests/orm_op_with_option_and_none.vv:41:30: warning: comparison with none probably isn't intended; use "is none" and "!is none" to select by NULL
39 |
40 | _ := sql db {
41 | select from Foo where name <= none
| ~~
42 | }!
43 |
vlib/v/checker/tests/orm_op_with_option_and_none.vv:45:30: warning: comparison with none probably isn't intended; use "is none" and "!is none" to select by NULL
43 |
44 | _ := sql db {
45 | select from Foo where name >= none
| ~~
46 | }!
47 |

View file

@ -0,0 +1,55 @@
import db.sqlite
struct Foo {
id u64 [primary; sql: serial]
name ?string
}
fn bar(val ?string) ! {
db := sqlite.connect(':memory:')!
_ := sql db {
select from Foo where name == val
}!
}
fn main() {
db := sqlite.connect(':memory:')!
sql db {
create table Foo
}!
bar(none) or {}
_ := sql db {
select from Foo where name == none
}!
_ := sql db {
select from Foo where name != none
}!
_ := sql db {
select from Foo where name < none
}!
_ := sql db {
select from Foo where name > none
}!
_ := sql db {
select from Foo where name <= none
}!
_ := sql db {
select from Foo where name >= none
}!
_ := sql db {
select from Foo where name is none
}!
_ := sql db {
select from Foo where name !is none
}!
}

View file

@ -0,0 +1,7 @@
vlib/v/checker/tests/orm_table_attributes.vv:3:1: error: ORM: table attribute must have an argument
1 | import db.sqlite
2 |
3 | [table]
| ~~~~~~~
4 | struct Foo {
5 | id int [primary; sql: serial]

View file

@ -0,0 +1,13 @@
import db.sqlite
[table]
struct Foo {
id int [primary; sql: serial]
}
fn main() {
db := sqlite.connect(':memory:')!
sql db {
create table Foo
}!
}

View file

@ -1,4 +1,4 @@
vlib/v/checker/tests/orm_using_non_struct_field_in_order_by_error.vv:14:29: error: V ORM: `User` structure has no field with name `database`. vlib/v/checker/tests/orm_using_non_struct_field_in_order_by_error.vv:14:29: error: ORM: `User` structure has no field with name `database`.
2 possibilities: `id`, `username`. 2 possibilities: `id`, `username`.
12 | 12 |
13 | users := sql db { 13 | users := sql db {

View file

@ -1,20 +1,7 @@
vlib/v/checker/tests/orm_where_clause_unsupported_field_types_err.vv:15:29: error: V ORM: does not support array of primitive types vlib/v/checker/tests/orm_where_clause_unsupported_field_types_err.vv:8:2: error: ORM: array fields must have an fkey attribute
13 | bytes := [u8(0)] 6 | pub struct Example {
14 | e := sql db { 7 | id int [primary; sql: serial]
15 | select from Example where example == bytes 8 | example []u8 [sql_type: 'bytea'; unique]
| ~~~~~~~ | ~~~~~~~~~~~~
16 | }! 9 | }
17 | f := sql db { 10 |
Details:
field name: `example`
data type: `[]u8`
vlib/v/checker/tests/orm_where_clause_unsupported_field_types_err.vv:18:30: error: V ORM: does not support array of primitive types
16 | }!
17 | f := sql db {
18 | select from Example where (example == bytes)
| ~~~~~~~
19 | }!
20 | print(e)
Details:
field name: `example`
data type: `[]u8`

View file

@ -1,4 +1,4 @@
vlib/v/checker/tests/orm_wrong_where_expr_error.vv:15:26: error: V ORM: `where` expression must have at least one comparison for filtering rows vlib/v/checker/tests/orm_wrong_where_expr_error.vv:15:26: error: ORM: `where` expression must have at least one comparison for filtering rows
13 | 13 |
14 | users := sql db { 14 | users := sql db {
15 | select from User where 3 15 | select from User where 3

View file

@ -19,6 +19,7 @@ const skip_on_cstrict = [
const skip_on_ubuntu_musl = [ const skip_on_ubuntu_musl = [
'vlib/v/checker/tests/vweb_tmpl_used_var.vv', 'vlib/v/checker/tests/vweb_tmpl_used_var.vv',
'vlib/v/checker/tests/vweb_routing_checks.vv', 'vlib/v/checker/tests/vweb_routing_checks.vv',
'vlib/v/checker/tests/orm_op_with_option_and_none.vv',
'vlib/v/tests/skip_unused/gg_code.vv', 'vlib/v/tests/skip_unused/gg_code.vv',
] ]

View file

@ -1478,7 +1478,7 @@ pub fn (mut f Fmt) sql_stmt_line(node ast.SqlStmtLine) {
f.write('\t') f.write('\t')
match node.kind { match node.kind {
.insert { .insert {
f.writeln('insert ${node.object_var_name} into ${table_name}') f.writeln('insert ${node.object_var} into ${table_name}')
} }
.update { .update {
f.write('update ${table_name} set ') f.write('update ${table_name} set ')

View file

@ -4582,8 +4582,12 @@ fn (mut g Gen) cast_expr(node ast.CastExpr) {
if sym.kind != .alias if sym.kind != .alias
|| (!(sym.info as ast.Alias).parent_type.has_flag(.option) || (!(sym.info as ast.Alias).parent_type.has_flag(.option)
&& (sym.info as ast.Alias).parent_type !in [expr_type, ast.string_type]) { && (sym.info as ast.Alias).parent_type !in [expr_type, ast.string_type]) {
if sym.kind == .string && !node.typ.is_ptr() {
cast_label = '*(string*)&'
} else {
cast_label = '(${styp})' cast_label = '(${styp})'
} }
}
if node.typ.has_flag(.option) && node.expr is ast.None { if node.typ.has_flag(.option) && node.expr is ast.None {
g.gen_option_error(node.typ, node.expr) g.gen_option_error(node.typ, node.expr)
} else if node.typ.has_flag(.option) { } else if node.typ.has_flag(.option) {

View file

@ -34,7 +34,12 @@ fn (mut g Gen) sql_select_expr(node ast.SqlExpr) {
g.writeln('') g.writeln('')
g.write_orm_connection_init(connection_var_name, &node.db_expr) g.write_orm_connection_init(connection_var_name, &node.db_expr)
g.write_orm_select(node, connection_var_name, left, node.or_expr) result_var := g.new_tmp_var()
result_c_typ := g.typ(node.typ)
g.writeln('${result_c_typ} ${result_var};')
g.write_orm_select(node, connection_var_name, result_var)
unwrapped_c_typ := g.typ(node.typ.clear_flags(.result))
g.write('${left} *(${unwrapped_c_typ}*)${result_var}.data')
} }
// sql_stmt writes C code that calls ORM functions for // sql_stmt writes C code that calls ORM functions for
@ -91,16 +96,14 @@ fn (mut g Gen) sql_stmt_line(stmt_line ast.SqlStmtLine, connection_var_name stri
// write_orm_connection_init writes C code that saves the database connection // write_orm_connection_init writes C code that saves the database connection
// into a variable for later use in ORM queries. // into a variable for later use in ORM queries.
fn (mut g Gen) write_orm_connection_init(connection_var_name string, db_expr &ast.Expr) { fn (mut g Gen) write_orm_connection_init(connection_var_name string, db_expr &ast.Expr) {
db_expr_type := g.get_db_expr_type(db_expr) or { db_expr_type := g.get_db_expr_type(db_expr) or { verror('ORM: unknown db type for ${db_expr}') }
verror('V ORM: unknown db type for ${db_expr}')
}
mut db_ctype_name := g.typ(db_expr_type) mut db_ctype_name := g.typ(db_expr_type)
is_pointer := db_ctype_name.ends_with('*') is_pointer := db_ctype_name.ends_with('*')
reference_sign := if is_pointer { '' } else { '&' } reference_sign := if is_pointer { '' } else { '&' }
db_ctype_name = db_ctype_name.trim_right('*') db_ctype_name = db_ctype_name.trim_right('*')
g.writeln('// V ORM') g.writeln('// ORM')
g.write('orm__Connection ${connection_var_name} = ') g.write('orm__Connection ${connection_var_name} = ')
if db_ctype_name == 'orm__Connection' { if db_ctype_name == 'orm__Connection' {
@ -130,17 +133,17 @@ fn (mut g Gen) write_orm_create_table(node ast.SqlStmtLine, table_name string, c
for field in node.fields { for field in node.fields {
g.writeln('// `${table_name}`.`${field.name}`') g.writeln('// `${table_name}`.`${field.name}`')
sym := g.table.sym(field.typ) sym := g.table.sym(field.typ)
typ := match true {
sym.name == 'time.Time' { '_const_orm__time_' }
sym.kind == .enum_ { '_const_orm__enum_' }
else { field.typ.idx().str() }
}
g.writeln('(orm__TableField){') g.writeln('(orm__TableField){')
g.indent++ g.indent++
g.writeln('.name = _SLIT("${field.name}"),') g.writeln('.name = _SLIT("${field.name}"),')
mut typ := int(field.typ)
if sym.name == 'time.Time' {
typ = -2
}
g.writeln('.typ = ${typ}, // `${sym.name}`') g.writeln('.typ = ${typ}, // `${sym.name}`')
g.writeln('.is_arr = ${sym.kind == .array}, ') g.writeln('.is_arr = ${sym.kind == .array}, ')
g.writeln('.is_enum = ${sym.kind == .enum_}, ') g.writeln('.nullable = ${field.typ.has_flag(.option)},')
g.writeln('.is_time = ${g.table.get_type_name(field.typ) == 'time__Time'},')
g.writeln('.default_val = (string){ .str = (byteptr) "${field.default_val}", .is_lit = 1 },') g.writeln('.default_val = (string){ .str = (byteptr) "${field.default_val}", .is_lit = 1 },')
g.writeln('.attrs = new_array_from_c_array(${field.attrs.len}, ${field.attrs.len}, sizeof(StructAttribute),') g.writeln('.attrs = new_array_from_c_array(${field.attrs.len}, ${field.attrs.len}, sizeof(StructAttribute),')
g.indent++ g.indent++
@ -221,7 +224,7 @@ fn (mut g Gen) write_orm_update(node &ast.SqlStmtLine, table_name string, connec
g.writeln('_MOV((string[${node.updated_columns.len}]){') g.writeln('_MOV((string[${node.updated_columns.len}]){')
g.indent++ g.indent++
for field in node.updated_columns { for field in node.updated_columns {
g.writeln(' _SLIT("${field}"),') g.writeln('_SLIT("${field}"),')
} }
g.indent-- g.indent--
g.writeln('})') g.writeln('})')
@ -241,8 +244,8 @@ fn (mut g Gen) write_orm_update(node &ast.SqlStmtLine, table_name string, connec
g.write_orm_expr_to_primitive(e) g.write_orm_expr_to_primitive(e)
} }
g.indent-- g.indent--
g.indent--
g.writeln('})') g.writeln('})')
g.indent--
} }
g.writeln('),') g.writeln('),')
@ -269,6 +272,7 @@ fn (mut g Gen) write_orm_delete(node &ast.SqlStmtLine, table_name string, connec
// inserting a struct into a table, saving inserted `id` into a passed variable. // inserting a struct into a table, saving inserted `id` into a passed variable.
fn (mut g Gen) write_orm_insert_with_last_ids(node ast.SqlStmtLine, connection_var_name string, table_name string, last_ids_arr string, res string, pid string, fkey string, or_expr ast.OrExpr) { fn (mut g Gen) write_orm_insert_with_last_ids(node ast.SqlStmtLine, connection_var_name string, table_name string, last_ids_arr string, res string, pid string, fkey string, or_expr ast.OrExpr) {
mut subs := []ast.SqlStmtLine{} mut subs := []ast.SqlStmtLine{}
mut subs_unwrapped_c_typ := []string{}
mut arrs := []ast.SqlStmtLine{} mut arrs := []ast.SqlStmtLine{}
mut fkeys := []string{} mut fkeys := []string{}
mut field_names := []string{} mut field_names := []string{}
@ -277,45 +281,55 @@ fn (mut g Gen) write_orm_insert_with_last_ids(node ast.SqlStmtLine, connection_v
sym := g.table.sym(field.typ) sym := g.table.sym(field.typ)
if sym.kind == .struct_ && sym.name != 'time.Time' { if sym.kind == .struct_ && sym.name != 'time.Time' {
subs << node.sub_structs[int(field.typ)] subs << node.sub_structs[int(field.typ)]
unwrapped_c_typ := g.typ(field.typ.clear_flag(.option))
subs_unwrapped_c_typ << if field.typ.has_flag(.option) { unwrapped_c_typ } else { '' }
} else if sym.kind == .array { } else if sym.kind == .array {
mut f_key := '' if attr := field.attrs.find_first('fkey') {
for attr in field.attrs { fkeys << attr.arg
if attr.name == 'fkey' && attr.has_arg {
if attr.kind == .string {
f_key = attr.arg
} else { } else {
verror("`fkey` attribute need be string. Try [fkey: '${attr.arg}'] instead of [fkey: ${attr.arg}]") verror('missing fkey attribute')
} }
} arrs << node.sub_structs[int(field.typ)]
}
if f_key == '' {
verror('a field which holds an array, needs a `fkey` defined ("${sym.name}")')
}
fkeys << f_key
info := sym.array_info()
if info.nr_dims == 1 {
arrs << node.sub_structs[int(info.elem_type)]
field_names << field.name field_names << field.name
} else {
verror('V ORM only supports 1 dimensional arrays')
}
} }
} }
fields := node.fields.filter(g.table.sym(it.typ).kind != .array) fields := node.fields.filter(g.table.sym(it.typ).kind != .array)
auto_fields := get_auto_field_idxs(fields)
primary_field := g.get_orm_struct_primary_field(fields) or { ast.StructField{} } primary_field := g.get_orm_struct_primary_field(fields) or { ast.StructField{} }
mut is_serial := false is_serial := primary_field.attrs.contains_arg('sql', 'serial')
for attr in primary_field.attrs { && primary_field.typ == ast.int_type
if attr.kind == .plain && attr.name == 'sql' && attr.arg.to_lower() == 'serial' {
is_serial = true
}
}
is_serial = is_serial && primary_field.typ == ast.int_type
for sub in subs { mut member_access_type := '.'
if node.scope != unsafe { nil } {
inserting_object := node.scope.find(node.object_var) or {
verror('`${node.object_var}` is not found in scope')
}
if inserting_object.typ.is_ptr() {
member_access_type = '->'
}
}
for i, mut sub in subs {
if subs_unwrapped_c_typ[i].len > 0 {
var := '${node.object_var}${member_access_type}${sub.object_var}'
g.writeln('if(${var}.state == 0) {')
g.indent++
sub.object_var = '(*(${subs_unwrapped_c_typ[i]}*)${node.object_var}${member_access_type}${sub.object_var}.data)'
} else {
sub.object_var = '${node.object_var}${member_access_type}${sub.object_var}'
}
g.sql_stmt_line(sub, connection_var_name, or_expr) g.sql_stmt_line(sub, connection_var_name, or_expr)
g.writeln('array_push(&${last_ids_arr}, _MOV((orm__Primitive[]){orm__int_to_primitive(orm__Connection_name_table[${connection_var_name}._typ]._method_last_id(${connection_var_name}._object))}));') g.writeln('array_push(&${last_ids_arr}, _MOV((orm__Primitive[1]){')
g.writeln('\torm__int_to_primitive(orm__Connection_name_table[${connection_var_name}._typ]._method_last_id(${connection_var_name}._object))}));')
if subs_unwrapped_c_typ[i].len > 0 {
g.indent--
g.writeln('} else {')
g.writeln('\tarray_push(&${last_ids_arr}, _MOV((orm__Primitive[1]){ _const_orm__null_primitive }));')
g.writeln('}')
}
} }
g.writeln('// sql { insert into `${table_name}` }') g.writeln('// sql { insert into `${table_name}` }')
@ -329,10 +343,12 @@ fn (mut g Gen) write_orm_insert_with_last_ids(node ast.SqlStmtLine, connection_v
g.indent++ g.indent++
if fields.len > 0 { if fields.len > 0 {
g.write('_MOV((string[${fields.len}]){ ') g.writeln('_MOV((string[${fields.len}]){ ')
g.indent++
for f in fields { for f in fields {
g.write('_SLIT("${g.get_orm_column_name_from_struct_field(f)}"), ') g.writeln('_SLIT("${g.get_orm_column_name_from_struct_field(f)}"),')
} }
g.indent--
g.writeln('})') g.writeln('})')
} else { } else {
g.writeln('NULL') g.writeln('NULL')
@ -340,45 +356,39 @@ fn (mut g Gen) write_orm_insert_with_last_ids(node ast.SqlStmtLine, connection_v
g.indent-- g.indent--
g.writeln('),') g.writeln('),')
mut member_access_type := '.'
if node.scope != unsafe { nil } {
inserting_object := node.scope.find(node.object_var_name) or {
verror('`${node.object_var_name}` is not found in scope')
}
if inserting_object.typ.is_ptr() {
member_access_type = '->'
}
}
g.writeln('.data = new_array_from_c_array(${fields.len}, ${fields.len}, sizeof(orm__Primitive),') g.writeln('.data = new_array_from_c_array(${fields.len}, ${fields.len}, sizeof(orm__Primitive),')
g.indent++ g.indent++
if fields.len > 0 { if fields.len > 0 {
g.write('_MOV((orm__Primitive[${fields.len}]){ ') g.writeln('_MOV((orm__Primitive[${fields.len}]){')
g.indent++
mut structs := 0 mut structs := 0
for field in fields { for field in fields {
if field.name == fkey { if field.name == fkey {
g.write('${pid}, ') g.writeln('${pid}, ')
continue continue
} }
mut sym := g.table.sym(field.typ) mut sym := g.table.sym(field.typ)
mut typ := sym.cname mut typ := sym.cname
if sym.kind == .struct_ && typ != 'time__Time' { if sym.kind == .struct_ && typ != 'time__Time' {
g.write('(*(orm__Primitive*) array_get(${last_ids_arr}, ${structs})),') g.writeln('(*(orm__Primitive*) array_get(${last_ids_arr}, ${structs})),')
structs++ structs++
continue continue
} else if sym.kind == .enum_ {
g.write('orm__i64_to_primitive(${node.object_var_name}${member_access_type}${c_name(field.name)}), ')
continue
} }
// fields processed hereafter can be NULL...
if typ == 'time__Time' { if typ == 'time__Time' {
typ = 'time' typ = 'time'
} else if sym.kind == .enum_ {
typ = 'i64'
} }
var := '${node.object_var}${member_access_type}${c_name(field.name)}'
g.write('orm__${typ}_to_primitive(${node.object_var_name}${member_access_type}${c_name(field.name)}), ') if field.typ.has_flag(.option) {
g.writeln('${var}.state == 2? _const_orm__null_primitive : orm__${typ}_to_primitive(*(${typ}*)(${var}.data)),')
} else {
g.writeln('orm__${typ}_to_primitive(${var}),')
} }
}
g.indent--
g.writeln('})') g.writeln('})')
} else { } else {
g.write('NULL') g.write('NULL')
@ -386,7 +396,18 @@ fn (mut g Gen) write_orm_insert_with_last_ids(node ast.SqlStmtLine, connection_v
g.indent-- g.indent--
g.writeln('),') g.writeln('),')
g.writeln('.types = __new_array_with_default_noscan(0, 0, sizeof(int), 0),') g.writeln('.types = __new_array_with_default_noscan(0, 0, sizeof(int), 0),')
g.writeln('.primary_column_name = _SLIT("${primary_field.name}"),') if auto_fields.len > 0 {
g.writeln('.auto_fields = new_array_from_c_array(${auto_fields.len}, ${auto_fields.len}, sizeof(int),')
g.indent++
g.write('_MOV((int[${auto_fields.len}]){')
for i in auto_fields {
g.write(' ${i},')
}
g.writeln(' })),')
g.indent--
} else {
g.writeln('.auto_fields = __new_array_with_default_noscan(0, 0, sizeof(int), 0),')
}
g.writeln('.kinds = __new_array_with_default_noscan(0, 0, sizeof(orm__OperationKind), 0),') g.writeln('.kinds = __new_array_with_default_noscan(0, 0, sizeof(orm__OperationKind), 0),')
g.writeln('.is_and = __new_array_with_default_noscan(0, 0, sizeof(bool), 0),') g.writeln('.is_and = __new_array_with_default_noscan(0, 0, sizeof(bool), 0),')
g.indent-- g.indent--
@ -406,19 +427,18 @@ fn (mut g Gen) write_orm_insert_with_last_ids(node ast.SqlStmtLine, connection_v
if typ == 'time__Time' { if typ == 'time__Time' {
typ = 'time' typ = 'time'
} }
g.writeln('orm__Primitive ${id_name} = orm__${typ}_to_primitive(${node.object_var_name}${member_access_type}${c_name(primary_field.name)});') g.writeln('orm__Primitive ${id_name} = orm__${typ}_to_primitive(${node.object_var}${member_access_type}${c_name(primary_field.name)});')
} }
for i, mut arr in arrs { for i, mut arr in arrs {
c_field_name := c_name(field_names[i])
idx := g.new_tmp_var() idx := g.new_tmp_var()
g.writeln('for (int ${idx} = 0; ${idx} < ${arr.object_var_name}${member_access_type}${c_field_name}.len; ${idx}++) {') g.writeln('for (int ${idx} = 0; ${idx} < ${node.object_var}${member_access_type}${arr.object_var}.len; ${idx}++) {')
g.indent++
last_ids := g.new_tmp_var() last_ids := g.new_tmp_var()
res_ := g.new_tmp_var() res_ := g.new_tmp_var()
tmp_var := g.new_tmp_var() tmp_var := g.new_tmp_var()
ctyp := g.typ(arr.table_expr.typ) ctyp := g.typ(arr.table_expr.typ)
g.writeln('${ctyp} ${tmp_var} = (*(${ctyp}*)array_get(${arr.object_var_name}${member_access_type}${c_field_name}, ${idx}));') g.writeln('${ctyp} ${tmp_var} = (*(${ctyp}*)array_get(${node.object_var}${member_access_type}${arr.object_var}, ${idx}));')
arr.object_var_name = tmp_var arr.object_var = tmp_var
mut fff := []ast.StructField{} mut fff := []ast.StructField{}
for f in arr.fields { for f in arr.fields {
mut skip := false mut skip := false
@ -438,6 +458,7 @@ fn (mut g Gen) write_orm_insert_with_last_ids(node ast.SqlStmtLine, connection_v
unsafe { fff.free() } unsafe { fff.free() }
g.write_orm_insert_with_last_ids(arr, connection_var_name, g.get_table_name_by_struct_type(arr.table_expr.typ), g.write_orm_insert_with_last_ids(arr, connection_var_name, g.get_table_name_by_struct_type(arr.table_expr.typ),
last_ids, res_, id_name, fkeys[i], or_expr) last_ids, res_, id_name, fkeys[i], or_expr)
g.indent--
g.writeln('}') g.writeln('}')
} }
} }
@ -475,9 +496,12 @@ fn (mut g Gen) write_orm_expr_to_primitive(expr ast.Expr) {
ast.CallExpr { ast.CallExpr {
g.write_orm_primitive(expr.return_type, expr) g.write_orm_primitive(expr.return_type, expr)
} }
ast.None {
g.write_orm_primitive(ast.none_type, expr)
}
else { else {
eprintln(expr) eprintln(expr)
verror('V ORM: ${expr.type_name()} is not supported') verror('ORM: ${expr.type_name()} is not supported')
} }
} }
} }
@ -489,20 +513,18 @@ fn (mut g Gen) write_orm_primitive(t ast.Type, expr ast.Expr) {
mut typ := sym.cname mut typ := sym.cname
if typ == 'orm__Primitive' { if typ == 'orm__Primitive' {
g.expr(expr) g.expr(expr)
g.write(',') g.writeln(',')
return return
} }
if typ == 'time__Time' { if typ == 'none' {
typ = 'time' g.writeln('_const_orm__null_primitive,')
} return
if typ == 'orm__InfixType' {
typ = 'infix'
} }
g.write('orm__${typ}_to_primitive(')
if expr is ast.InfixExpr { if expr is ast.InfixExpr {
g.write('(orm__InfixType){') g.writeln('orm__infix_to_primitive((orm__InfixType){')
g.write(' .name = _SLIT("${expr.left}"),') g.indent++
g.write('.name = _SLIT("${expr.left}"),')
mut kind := match expr.op { mut kind := match expr.op {
.plus { .plus {
'orm__MathOperationKind__add' 'orm__MathOperationKind__add'
@ -520,16 +542,28 @@ fn (mut g Gen) write_orm_primitive(t ast.Type, expr ast.Expr) {
'' ''
} }
} }
g.write(' .operator = ${kind},') g.writeln(' .operator = ${kind},')
g.write(' .right = ') g.write(' .right = ')
g.write_orm_expr_to_primitive(expr.right) g.write_orm_expr_to_primitive(expr.right)
g.write(' }') g.indent--
} else if expr is ast.CallExpr { g.writeln(' }),')
} else {
if typ == 'time__Time' {
typ = 'time'
}
if t.has_flag(.option) {
typ = 'option_${typ}'
}
g.write('orm__${typ}_to_primitive(')
if expr is ast.CallExpr {
g.call_expr(expr) g.call_expr(expr)
} else { } else {
g.left_is_opt = true
g.expr(expr) g.expr(expr)
} }
g.write('),') g.writeln('),')
}
} }
// write_orm_where writes C code that generates // write_orm_where writes C code that generates
@ -541,7 +575,8 @@ fn (mut g Gen) write_orm_where(where_expr ast.Expr) {
mut data := []ast.Expr{} mut data := []ast.Expr{}
mut is_ands := []bool{} mut is_ands := []bool{}
g.writeln('// V ORM where') g.writeln('// ORM where')
g.writeln('(orm__QueryData){') g.writeln('(orm__QueryData){')
g.indent++ g.indent++
g.write_orm_where_expr(where_expr, mut fields, mut parentheses, mut kinds, mut data, mut g.write_orm_where_expr(where_expr, mut fields, mut parentheses, mut kinds, mut data, mut
@ -550,11 +585,13 @@ fn (mut g Gen) write_orm_where(where_expr ast.Expr) {
if fields.len > 0 { if fields.len > 0 {
g.writeln('.fields = new_array_from_c_array(${fields.len}, ${fields.len}, sizeof(string),') g.writeln('.fields = new_array_from_c_array(${fields.len}, ${fields.len}, sizeof(string),')
g.indent++ g.indent++
g.write('_MOV((string[${fields.len}]){') g.writeln('_MOV((string[${fields.len}]){')
g.indent++
for field in fields { for field in fields {
g.write(' _SLIT("${field}"),') g.writeln('_SLIT("${field}"),')
} }
g.writeln(' })') g.indent--
g.writeln('})')
g.indent-- g.indent--
} else { } else {
g.writeln('.fields = __new_array_with_default_noscan(${fields.len}, ${fields.len}, sizeof(string), 0') g.writeln('.fields = __new_array_with_default_noscan(${fields.len}, ${fields.len}, sizeof(string), 0')
@ -564,12 +601,15 @@ fn (mut g Gen) write_orm_where(where_expr ast.Expr) {
g.writeln('.data = new_array_from_c_array(${data.len}, ${data.len}, sizeof(orm__Primitive),') g.writeln('.data = new_array_from_c_array(${data.len}, ${data.len}, sizeof(orm__Primitive),')
g.indent++ g.indent++
if data.len > 0 { if data.len > 0 {
g.write('_MOV((orm__Primitive[${data.len}]){ ') g.writeln('_MOV((orm__Primitive[${data.len}]){')
g.indent++
for e in data { for e in data {
g.write_orm_expr_to_primitive(e) g.write_orm_expr_to_primitive(e)
g.write(' ')
} }
g.write('})') g.indent--
g.writeln('})')
} else {
g.writeln('0')
} }
g.indent-- g.indent--
g.writeln('),') g.writeln('),')
@ -603,7 +643,7 @@ fn (mut g Gen) write_orm_where(where_expr ast.Expr) {
g.writeln('${k},') g.writeln('${k},')
} }
g.indent-- g.indent--
g.write('})') g.writeln('})')
g.indent-- g.indent--
} else { } else {
g.write('.kinds = __new_array_with_default_noscan(${kinds.len}, ${kinds.len}, sizeof(orm__OperationKind), 0') g.write('.kinds = __new_array_with_default_noscan(${kinds.len}, ${kinds.len}, sizeof(orm__OperationKind), 0')
@ -612,11 +652,15 @@ fn (mut g Gen) write_orm_where(where_expr ast.Expr) {
if is_ands.len > 0 { if is_ands.len > 0 {
g.write('.is_and = new_array_from_c_array(${is_ands.len}, ${is_ands.len}, sizeof(bool),') g.write('.is_and = new_array_from_c_array(${is_ands.len}, ${is_ands.len}, sizeof(bool),')
g.write(' _MOV((bool[${is_ands.len}]){') g.indent++
g.writeln('_MOV((bool[${is_ands.len}]){')
g.indent++
for is_and in is_ands { for is_and in is_ands {
g.write(' ${is_and}, ') g.writeln('${is_and},')
} }
g.indent--
g.write('})') g.write('})')
g.indent--
} else { } else {
g.write('.is_and = __new_array_with_default_noscan(${is_ands.len}, ${is_ands.len}, sizeof(bool), 0') g.write('.is_and = __new_array_with_default_noscan(${is_ands.len}, ${is_ands.len}, sizeof(bool), 0')
} }
@ -654,6 +698,12 @@ fn (mut g Gen) write_orm_where_expr(expr ast.Expr, mut fields []string, mut pare
.key_like { .key_like {
'orm__OperationKind__orm_like' 'orm__OperationKind__orm_like'
} }
.key_is {
'orm__OperationKind__is'
}
.not_is {
'orm__OperationKind__is_not'
}
else { else {
'' ''
} }
@ -712,12 +762,16 @@ fn (mut g Gen) write_orm_where_expr(expr ast.Expr, mut fields []string, mut pare
ast.CallExpr { ast.CallExpr {
data << expr data << expr
} }
ast.None {
data << expr
}
else {} else {}
} }
} }
// write_orm_select writes C code that calls ORM functions for selecting rows. // write_orm_select writes C code that calls ORM functions for selecting rows,
fn (mut g Gen) write_orm_select(node ast.SqlExpr, connection_var_name string, left_expr_string string, or_expr ast.OrExpr) { // storing the result in the provided variable name
fn (mut g Gen) write_orm_select(node ast.SqlExpr, connection_var_name string, result_var string) {
mut fields := []ast.StructField{} mut fields := []ast.StructField{}
mut primary_field := g.get_orm_struct_primary_field(node.fields) or { ast.StructField{} } mut primary_field := g.get_orm_struct_primary_field(node.fields) or { ast.StructField{} }
@ -772,26 +826,28 @@ fn (mut g Gen) write_orm_select(node ast.SqlExpr, connection_var_name string, le
select_fields := fields.filter(g.table.sym(it.typ).kind != .array) select_fields := fields.filter(g.table.sym(it.typ).kind != .array)
g.writeln('.fields = new_array_from_c_array(${select_fields.len}, ${select_fields.len}, sizeof(string),') g.writeln('.fields = new_array_from_c_array(${select_fields.len}, ${select_fields.len}, sizeof(string),')
g.indent++ g.indent++
mut types := []int{} mut types := []string{}
if select_fields.len > 0 { if select_fields.len > 0 {
g.write('_MOV((string[${select_fields.len}]){') g.writeln('_MOV((string[${select_fields.len}]){')
g.indent++
for field in select_fields { for field in select_fields {
g.write(' _SLIT("${g.get_orm_column_name_from_struct_field(field)}"),') g.writeln('_SLIT("${g.get_orm_column_name_from_struct_field(field)}"),')
sym := g.table.sym(field.typ) sym := g.table.sym(field.typ)
if sym.name == 'time.Time' { if sym.name == 'time.Time' {
types << -2 types << '_const_orm__time_'
continue continue
} }
if sym.kind == .struct_ { if sym.kind == .struct_ {
types << int(ast.int_type) types << int(ast.int_type).str()
continue continue
} else if sym.kind == .enum_ { } else if sym.kind == .enum_ {
types << int(ast.i64_type) types << '_const_orm__enum_'
continue continue
} }
types << int(field.typ) types << field.typ.idx().str()
} }
g.writeln(' })') g.indent--
g.writeln('})')
} else { } else {
g.writeln('NULL') g.writeln('NULL')
} }
@ -857,27 +913,22 @@ fn (mut g Gen) write_orm_select(node ast.SqlExpr, connection_var_name string, le
g.indent-- g.indent--
g.writeln(');') g.writeln(');')
unwrapped_typ := node.typ.clear_flag(.result) g.writeln('${result_var}.is_error = ${select_result_var_name}.is_error;')
unwrapped_c_typ := g.typ(unwrapped_typ) g.writeln('${result_var}.err = ${select_result_var_name}.err;')
c_typ := g.typ(node.typ) g.or_block(result_var, node.or_expr, node.typ)
mut non_orm_result_var_name := g.new_tmp_var() // or_block could have ended in return (longjump) or could have
g.writeln('${c_typ} ${non_orm_result_var_name};') // yielded another value, so we must test for on non-error result
g.writeln('${non_orm_result_var_name}.is_error = ${select_result_var_name}.is_error;') g.writeln('if (!${result_var}.is_error) {')
g.writeln('${non_orm_result_var_name}.err = ${select_result_var_name}.err;')
g.or_block(non_orm_result_var_name, node.or_expr, node.typ)
g.writeln('else {')
g.indent++ g.indent++
unwrapped_c_typ := g.typ(node.typ.clear_flag(.result))
select_unwrapped_result_var_name := g.new_tmp_var() select_unwrapped_result_var_name := g.new_tmp_var()
g.writeln('Array_Array_orm__Primitive ${select_unwrapped_result_var_name} = (*(Array_Array_orm__Primitive*)${select_result_var_name}.data);') g.writeln('Array_Array_orm__Primitive ${select_unwrapped_result_var_name} = (*(Array_Array_orm__Primitive*)${select_result_var_name}.data);')
if node.is_count { if node.is_count {
g.writeln('*(${unwrapped_c_typ}*) ${non_orm_result_var_name}.data = *((*(orm__Primitive*) array_get((*(Array_orm__Primitive*)array_get(${select_unwrapped_result_var_name}, 0)), 0))._int);') g.writeln('*(${unwrapped_c_typ}*) ${result_var}.data = *((*(orm__Primitive*) array_get((*(Array_orm__Primitive*)array_get(${select_unwrapped_result_var_name}, 0)), 0))._int);')
g.indent--
g.writeln('}')
} else { } else {
tmp := g.new_tmp_var() tmp := g.new_tmp_var()
idx := g.new_tmp_var() idx := g.new_tmp_var()
@ -913,10 +964,12 @@ fn (mut g Gen) write_orm_select(node ast.SqlExpr, connection_var_name string, le
g.writeln('if (${select_unwrapped_result_var_name}.len > 0) {') g.writeln('if (${select_unwrapped_result_var_name}.len > 0) {')
g.indent++ g.indent++
mut selected_fields_idx := 0 mut fields_idx := 0
for field in fields { for field in fields {
array_get_call_code := '(*(orm__Primitive*) array_get((*(Array_orm__Primitive*) array_get(${select_unwrapped_result_var_name}, ${idx})), ${selected_fields_idx}))' array_get_call_code := '(*(orm__Primitive*) array_get((*(Array_orm__Primitive*) array_get(${select_unwrapped_result_var_name}, ${idx})), ${fields_idx}))'
sym := g.table.sym(field.typ) sym := g.table.sym(field.typ)
field_var := '${tmp}.${c_name(field.name)}'
field_c_typ := g.typ(field.typ)
if sym.kind == .struct_ && sym.name != 'time.Time' { if sym.kind == .struct_ && sym.name != 'time.Time' {
mut sub := node.sub_structs[int(field.typ)] mut sub := node.sub_structs[int(field.typ)]
mut where_expr := sub.where_expr as ast.InfixExpr mut where_expr := sub.where_expr as ast.InfixExpr
@ -933,28 +986,30 @@ fn (mut g Gen) write_orm_select(node ast.SqlExpr, connection_var_name string, le
where_expr.right = ident where_expr.right = ident
sub.where_expr = where_expr sub.where_expr = where_expr
g.write_orm_select(sub, connection_var_name, '${tmp}.${c_name(field.name)} = ', sub_result_var := g.new_tmp_var()
or_expr) sub_result_c_typ := g.typ(sub.typ)
selected_fields_idx++ g.writeln('${sub_result_c_typ} ${sub_result_var};')
g.write_orm_select(sub, connection_var_name, sub_result_var)
if field.typ.has_flag(.option) {
unwrapped_field_c_typ := g.typ(field.typ.clear_flag(.option))
g.writeln('if (!${sub_result_var}.is_error)')
g.writeln('\t_option_ok(${sub_result_var}.data, (_option *)&${field_var}, sizeof(${unwrapped_field_c_typ}));')
g.writeln('else')
g.writeln('\t${field_var} = (${field_c_typ}){ .state = 2, .err = _const_none__, .data = {EMPTY_STRUCT_INITIALIZATION} };')
} else {
g.writeln('if (!${sub_result_var}.is_error)')
g.writeln('\t${field_var} = *(${field_c_typ}*)${sub_result_var}.data;')
}
fields_idx++
} else if sym.kind == .array { } else if sym.kind == .array {
mut fkey := '' mut fkey := ''
// TODO: move to the ORM checker if attr := field.attrs.find_first('fkey') {
for attr in field.attrs {
if attr.name == 'fkey' && attr.has_arg {
if attr.kind == .string {
fkey = attr.arg fkey = attr.arg
} else { } else {
verror("`fkey` attribute need be string. Try [fkey: '${attr.arg}'] instead of [fkey: ${attr.arg}]") verror('missing fkey attribute')
} }
} sub := node.sub_structs[field.typ]
}
// TODO: move to the ORM checker
if fkey == '' {
verror('a field which holds an array, needs a `fkey` defined ("${sym.name}")')
}
info := sym.array_info()
arr_typ := info.elem_type
sub := node.sub_structs[int(arr_typ)]
mut where_expr := sub.where_expr as ast.InfixExpr mut where_expr := sub.where_expr as ast.InfixExpr
mut left_where_expr := where_expr.left as ast.Ident mut left_where_expr := where_expr.left as ast.Ident
mut right_where_expr := where_expr.right as ast.Ident mut right_where_expr := where_expr.right as ast.Ident
@ -990,20 +1045,30 @@ fn (mut g Gen) write_orm_select(node ast.SqlExpr, connection_var_name string, le
where_expr: where_expr where_expr: where_expr
} }
g.write_orm_select(sql_expr_select_array, connection_var_name, '${tmp}.${c_name(field.name)} = ', sub_result_var := g.new_tmp_var()
or_expr) sub_result_c_typ := g.typ(sub.typ)
g.writeln('${sub_result_c_typ} ${sub_result_var};')
g.write_orm_select(sql_expr_select_array, connection_var_name, sub_result_var)
g.writeln('if (!${sub_result_var}.is_error)')
g.writeln('\t${field_var} = *(${unwrapped_c_typ}*)${sub_result_var}.data;')
} else if field.typ.has_flag(.option) {
prim_var := g.new_tmp_var()
g.writeln('orm__Primitive *${prim_var} = &${array_get_call_code};')
g.writeln('if (${prim_var}->_typ == ${g.table.find_type_idx('orm.Null')})')
g.writeln('\t${field_var} = (${field_c_typ}){ .state = 2, .err = _const_none__, .data = {EMPTY_STRUCT_INITIALIZATION} };')
g.writeln('else')
g.writeln('\t_option_ok(${prim_var}->_${sym.cname}, (_option *)&${field_var}, sizeof(${sym.cname}));')
fields_idx++
} else if sym.kind == .enum_ { } else if sym.kind == .enum_ {
mut typ := sym.cname mut typ := sym.cname
g.writeln('${tmp}.${c_name(field.name)} = (${typ}) (*(${array_get_call_code}._i64));') g.writeln('${tmp}.${c_name(field.name)} = (${typ}) (*(${array_get_call_code}._i64));')
selected_fields_idx++ fields_idx++
} else { } else {
mut typ := sym.cname g.writeln('${field_var} = *(${array_get_call_code}._${sym.cname});')
g.writeln('${tmp}.${c_name(field.name)} = *(${array_get_call_code}._${typ});') fields_idx++
selected_fields_idx++
} }
} }
g.indent--
g.writeln('}')
if node.is_array { if node.is_array {
g.writeln('array_push(&${tmp}_array, _MOV((${typ_str}[]){ ${tmp} }));') g.writeln('array_push(&${tmp}_array, _MOV((${typ_str}[]){ ${tmp} }));')
@ -1011,45 +1076,37 @@ fn (mut g Gen) write_orm_select(node ast.SqlExpr, connection_var_name string, le
g.writeln('}') g.writeln('}')
} }
g.write('*(${unwrapped_c_typ}*) ${non_orm_result_var_name}.data = ${tmp}') g.indent--
g.writeln('}')
g.write('*(${unwrapped_c_typ}*) ${result_var}.data = ${tmp}')
if node.is_array { if node.is_array {
g.write('_array') g.write('_array')
} }
g.writeln(';') g.writeln(';')
}
g.indent-- g.indent--
g.writeln('}') g.writeln('}')
}
g.write('${left_expr_string.trim_space()} *(${unwrapped_c_typ}*) ${non_orm_result_var_name}.data')
if node.is_generated { if node.is_generated {
g.write(';') g.writeln(';')
} }
} }
// filter_struct_fields_by_orm_attrs filters struct fields taking into its attributes. // filter_struct_fields_by_orm_attrs filters struct fields taking into its attributes.
// Used by non-create queries for skipping fields. // Used by non-create queries for skipping fields.
fn (_ &Gen) filter_struct_fields_by_orm_attrs(fields []ast.StructField) []ast.StructField { fn (_ &Gen) filter_struct_fields_by_orm_attrs(fields []ast.StructField) []ast.StructField {
mut result := []ast.StructField{} mut ret := []ast.StructField{}
for field in fields { for field in fields {
mut skip := false if field.attrs.contains('skip') || field.attrs.contains_arg('sql', '-') {
continue
for attr in field.attrs {
if attr.name == 'skip' {
skip = true
}
if attr.name == 'sql' && attr.arg == '-' {
skip = true
} }
ret << field
} }
if !skip { return ret
result << field
}
}
return result
} }
// get_db_expr_type returns the database type from the database expression. // get_db_expr_type returns the database type from the database expression.
@ -1067,13 +1124,12 @@ fn (g &Gen) get_db_expr_type(expr ast.Expr) ?ast.Type {
// get_table_name_by_struct_type converts the struct type to a table name. // get_table_name_by_struct_type converts the struct type to a table name.
fn (g &Gen) get_table_name_by_struct_type(typ ast.Type) string { fn (g &Gen) get_table_name_by_struct_type(typ ast.Type) string {
info := g.table.sym(typ).struct_info() sym := g.table.sym(typ)
mut table_name := util.strip_mod_name(g.table.sym(typ).name) info := sym.struct_info()
mut table_name := util.strip_mod_name(sym.name)
for attr in info.attrs { if attr := info.attrs.find_first('table') {
if attr.kind == .string && attr.name == 'table' && attr.arg != '' { table_name = attr.arg
return attr.arg
}
} }
return table_name return table_name
@ -1096,10 +1152,10 @@ fn (g &Gen) get_orm_current_table_field(name string) ?ast.StructField {
fn (g &Gen) get_orm_column_name_from_struct_field(field ast.StructField) string { fn (g &Gen) get_orm_column_name_from_struct_field(field ast.StructField) string {
mut name := field.name mut name := field.name
for attr in field.attrs { if attr := field.attrs.find_first('sql') {
if attr.kind == .string && attr.name == 'sql' && attr.arg != '' { if attr.arg !in ['serial', 'i8', 'i16', 'int', 'i64', 'u8', 'u16', 'u32', 'u64', 'f32',
'f64', 'bool', 'string'] {
name = attr.arg name = attr.arg
break
} }
} }
@ -1114,12 +1170,24 @@ fn (g &Gen) get_orm_column_name_from_struct_field(field ast.StructField) string
// get_orm_struct_primary_field returns the table's primary column field. // get_orm_struct_primary_field returns the table's primary column field.
fn (_ &Gen) get_orm_struct_primary_field(fields []ast.StructField) ?ast.StructField { fn (_ &Gen) get_orm_struct_primary_field(fields []ast.StructField) ?ast.StructField {
for field in fields { for field in fields {
for attr in field.attrs { if _ := field.attrs.find_first('primary') {
if attr.name == 'primary' {
return field return field
} }
} }
}
return none return none
} }
// return indexes of any auto-increment fields or fields with default values
fn get_auto_field_idxs(fields []ast.StructField) []int {
mut ret := []int{}
for i, field in fields {
for attr in field.attrs {
if attr.name == 'default' {
ret << i
} else if attr.name == 'sql' && attr.arg == 'serial' {
ret << i
}
}
}
return ret
}

View file

@ -1186,7 +1186,7 @@ pub fn (mut f Gen) sql_stmt_line(node ast.SqlStmtLine) {
f.write('\t') f.write('\t')
match node.kind { match node.kind {
.insert { .insert {
f.writeln('insert ${node.object_var_name} into ${table_name}') f.writeln('insert ${node.object_var} into ${table_name}')
} }
.update { .update {
f.write('update ${table_name} set ') f.write('update ${table_name} set ')

View file

@ -216,7 +216,7 @@ fn (mut p Parser) parse_sql_stmt_line() ast.SqlStmtLine {
scope: p.scope scope: p.scope
} }
} }
mut inserted_var_name := '' mut inserted_var := ''
mut table_type := ast.Type(0) mut table_type := ast.Type(0)
if kind != .delete { if kind != .delete {
if kind == .update { if kind == .update {
@ -224,7 +224,7 @@ fn (mut p Parser) parse_sql_stmt_line() ast.SqlStmtLine {
} else if kind == .insert { } else if kind == .insert {
expr := p.expr(0) expr := p.expr(0)
if expr is ast.Ident { if expr is ast.Ident {
inserted_var_name = expr.name inserted_var = expr.name
} else { } else {
p.error('can only insert variables') p.error('can only insert variables')
return ast.SqlStmtLine{} return ast.SqlStmtLine{}
@ -277,7 +277,7 @@ fn (mut p Parser) parse_sql_stmt_line() ast.SqlStmtLine {
typ: table_type typ: table_type
pos: table_pos pos: table_pos
} }
object_var_name: inserted_var_name object_var: inserted_var
pos: pos pos: pos
updated_columns: updated_columns updated_columns: updated_columns
update_exprs: update_exprs update_exprs: update_exprs
@ -290,7 +290,7 @@ fn (mut p Parser) parse_sql_stmt_line() ast.SqlStmtLine {
fn (mut p Parser) check_sql_keyword(name string) ?bool { fn (mut p Parser) check_sql_keyword(name string) ?bool {
if p.check_name() != name { if p.check_name() != name {
p.error('V ORM: expecting `${name}`') p.error('ORM: expecting `${name}`')
return none return none
} }
return true return true

View file

@ -1,4 +1,4 @@
vlib/v/parser/tests/orm_no_error_handler.vv:10:7: error: V ORM returns a result, so it should have either an `or {}` block, or `!` at the end vlib/v/parser/tests/orm_no_error_handler.vv:10:7: error: ORM returns a result, so it should have either an `or {}` block, or `!` at the end
8 | db := sqlite.connect(':memory:')! 8 | db := sqlite.connect(':memory:')!
9 | 9 |
10 | _ := sql db { 10 | _ := sql db {