diff --git a/cmd/tools/vtest-self.v b/cmd/tools/vtest-self.v index c7b06f0071..220b684655 100644 --- a/cmd/tools/vtest-self.v +++ b/cmd/tools/vtest-self.v @@ -17,6 +17,7 @@ const ( 'vlib/sqlite/sqlite_test.v', 'vlib/vweb/tests/vweb_test.v', 'vlib/v/tests/unsafe_test.v', + 'vlib/v/tests/orm_sub_struct_test.v', 'vlib/x/websocket/websocket_test.v', 'vlib/net/http/http_httpbin_test.v', ] @@ -66,6 +67,7 @@ const ( 'vlib/net/websocket/ws_test.v', 'vlib/sqlite/sqlite_test.v', 'vlib/orm/orm_test.v', + 'vlib/v/tests/orm_sub_struct_test.v', 'vlib/clipboard/clipboard_test.v', 'vlib/vweb/tests/vweb_test.v', 'vlib/x/websocket/websocket_test.v', @@ -77,6 +79,7 @@ const ( ] skip_on_windows = [ 'vlib/orm/orm_test.v', + 'vlib/v/tests/orm_sub_struct_test.v', 'vlib/net/websocket/ws_test.v', 'vlib/x/websocket/websocket_test.v', 'vlib/vweb/tests/vweb_test.v', diff --git a/examples/database/orm.v b/examples/database/orm.v new file mode 100644 index 0000000000..854a904cfe --- /dev/null +++ b/examples/database/orm.v @@ -0,0 +1,44 @@ +import sqlite + +struct Module { + id int + name string + nr_downloads int + creator User +} + +struct User { + id int + age int + name string + is_customer bool + skipped_string string [skip] +} + +fn main() { + db := sqlite.connect(':memory:') or { panic(err) } + db.exec('drop table if exists User') + db.exec("create table Module (id integer primary key, name text default '', nr_downloads int default 0, creator int default 0);") + db.exec("create table User (id integer primary key, age int default 0, name text default '', is_customer int default 0);") + + mod := Module{ + name: 'test' + nr_downloads: 10 + creator: User{ + age: 21 + name: 'VUser' + is_customer: true + } + } + sql db { + insert mod into Module + } + + modul := sql db { + select from Module where id == 1 + } + + println(modul.name) + println(modul.creator.name) + +} diff --git a/vlib/v/ast/ast.v b/vlib/v/ast/ast.v index ac9b4ad523..3ce8b17505 100644 --- a/vlib/v/ast/ast.v +++ b/vlib/v/ast/ast.v @@ -1173,8 +1173,9 @@ pub: updated_columns []string // for `update set x=y` update_exprs []Expr // for `update` pub mut: - table_expr Type - fields []table.Field + table_expr Type + fields []table.Field + sub_structs map[int]SqlStmt } pub struct SqlExpr { @@ -1182,7 +1183,6 @@ pub: typ table.Type is_count bool db_expr Expr // `db` in `sql db {` - where_expr Expr has_where bool has_offset bool offset_expr Expr @@ -1194,8 +1194,10 @@ pub: has_limit bool limit_expr Expr pub mut: - table_expr Type - fields []table.Field + where_expr Expr + table_expr Type + fields []table.Field + sub_structs map[int]SqlExpr } [inline] diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index f6ce7d1fe4..402daf733f 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -5492,7 +5492,56 @@ fn (mut c Checker) sql_expr(mut node ast.SqlExpr) table.Type { c.cur_orm_ts = sym info := sym.info as table.Struct fields := c.fetch_and_verify_orm_fields(info, node.table_expr.pos, sym.name) + mut sub_structs := map[int]ast.SqlExpr{} + for f in fields.filter(c.table.types[int(it.typ)].kind == .struct_) { + mut n := ast.SqlExpr{ + pos: node.pos + has_where: true + typ: f.typ + db_expr: node.db_expr + table_expr: ast.Type{ + pos: node.table_expr.pos + typ: f.typ + } + } + tmp_inside_sql := c.inside_sql + c.sql_expr(mut n) + c.inside_sql = tmp_inside_sql + n.where_expr = ast.InfixExpr{ + op: .eq + pos: n.pos + left: ast.Ident{ + language: .v + tok_kind: .eq + scope: c.fn_scope + obj: ast.Var{} + mod: 'main' + name: 'id' + is_mut: false + kind: .unresolved + info: ast.IdentVar{} + } + right: ast.Ident{ + language: .c + mod: 'main' + tok_kind: .eq + obj: ast.Var{} + is_mut: false + scope: c.fn_scope + info: ast.IdentVar{ + typ: table.int_type + } + } + left_type: table.int_type + right_type: table.int_type + auto_locked: '' + or_block: ast.OrExpr{} + } + + sub_structs[int(f.typ)] = n + } node.fields = fields + node.sub_structs = sub_structs if node.has_where { c.expr(node.where_expr) } @@ -5526,7 +5575,25 @@ fn (mut c Checker) sql_stmt(mut node ast.SqlStmt) table.Type { info := sym.info as table.Struct table_sym := c.table.get_type_symbol(node.table_expr.typ) fields := c.fetch_and_verify_orm_fields(info, node.table_expr.pos, table_sym.name) + mut sub_structs := map[int]ast.SqlStmt{} + for f in fields.filter(c.table.types[int(it.typ)].kind == .struct_) { + mut n := ast.SqlStmt{ + pos: node.pos + db_expr: node.db_expr + kind: node.kind + table_expr: ast.Type{ + pos: node.table_expr.pos + typ: f.typ + } + object_var_name: '${node.object_var_name}.$f.name' + } + tmp_inside_sql := c.inside_sql + c.sql_stmt(mut n) + c.inside_sql = tmp_inside_sql + sub_structs[int(f.typ)] = n + } node.fields = fields + node.sub_structs = sub_structs c.expr(node.db_expr) if node.kind == .update { for expr in node.update_exprs { @@ -5534,11 +5601,13 @@ fn (mut c Checker) sql_stmt(mut node ast.SqlStmt) table.Type { } } c.expr(node.where_expr) + return table.void_type } fn (mut c Checker) fetch_and_verify_orm_fields(info table.Struct, pos token.Position, table_name string) []table.Field { - fields := info.fields.filter(it.typ in [table.string_type, table.int_type, table.bool_type] + fields := info.fields.filter( + (it.typ in [table.string_type, table.int_type, table.bool_type] || c.table.types[int(it.typ)].kind == .struct_) && !it.attrs.contains('skip')) if fields.len == 0 { c.error('V orm: select: empty fields in `$table_name`', pos) diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index 2983e23427..9412772e8a 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -2807,7 +2807,7 @@ fn (mut g Gen) expr(node ast.Expr) { g.write('/*OffsetOf*/ (u32)(__offsetof(${util.no_dots(styp)}, $node.field))') } ast.SqlExpr { - g.sql_select_expr(node) + g.sql_select_expr(node, false, '') } ast.StringLiteral { g.string_literal(node) diff --git a/vlib/v/gen/c/sql.v b/vlib/v/gen/c/sql.v index c1c4a0ef43..a28a52c4ff 100644 --- a/vlib/v/gen/c/sql.v +++ b/vlib/v/gen/c/sql.v @@ -80,8 +80,19 @@ fn (mut g Gen) sql_stmt(node ast.SqlStmt) { x := '${node.object_var_name}.$field.name' if field.typ == table.string_type { g.writeln('sqlite3_bind_text($g.sql_stmt_name, ${i + 0}, ${x}.str, ${x}.len, 0);') + } else if g.table.types[int(field.typ)].kind == .struct_ { + // insert again + expr := node.sub_structs[int(field.typ)] + tmp_sql_stmt_name := g.sql_stmt_name + g.sql_stmt(expr) + g.sql_stmt_name = tmp_sql_stmt_name + // get last inserted id + g.writeln('array_sqlite__Row rows = sqlite__DB_exec($db_name, _SLIT("SELECT last_insert_rowid()")).arg0;') + id_name := g.new_tmp_var() + g.writeln('int $id_name = string_int((*(string*)array_get((*(sqlite__Row*)array_get(rows, 0)).vals, 0)));') + g.writeln('sqlite3_bind_int($g.sql_stmt_name, ${i + 0} , $id_name); // id') } else { - g.writeln('sqlite3_bind_int($g.sql_stmt_name, ${i + 0}, $x); // stmt') + g.writeln('sqlite3_bind_int($g.sql_stmt_name, ${i + 0} , $x); // stmt') } } } @@ -95,7 +106,7 @@ fn (mut g Gen) sql_stmt(node ast.SqlStmt) { g.writeln('\tsqlite3_finalize($g.sql_stmt_name);') } -fn (mut g Gen) sql_select_expr(node ast.SqlExpr) { +fn (mut g Gen) sql_select_expr(node ast.SqlExpr, sub bool, line string) { g.sql_i = 0 /* `nr_users := sql db { ... }` => @@ -107,7 +118,10 @@ fn (mut g Gen) sql_select_expr(node ast.SqlExpr) { int nr_users = get_int(stmt) ``` */ - cur_line := g.go_before_stmt(0) + mut cur_line := line + if !sub { + cur_line = g.go_before_stmt(0) + } mut sql_query := 'SELECT ' table_name := util.strip_mod_name(g.table.get_type_symbol(node.table_expr.typ).name) if node.is_count { @@ -231,6 +245,27 @@ fn (mut g Gen) sql_select_expr(node ast.SqlExpr) { g.writeln('if ($string_data != NULL) {') g.writeln('\t${tmp}.$field.name = tos_clone($string_data);') g.writeln('}') + } else if g.table.types[int(field.typ)].kind == .struct_ { + id_name := g.new_tmp_var() + g.writeln('//parse struct start') + g.writeln('int $id_name = ${func}($g.sql_stmt_name, $i);') + mut expr := node.sub_structs[int(field.typ)] + mut where_expr := expr.where_expr as ast.InfixExpr + mut ident := where_expr.right as ast.Ident + ident.name = id_name + where_expr.right = ident + expr.where_expr = where_expr + + tmp_sql_i := g.sql_i + tmp_sql_stmt_name := g.sql_stmt_name + tmp_sql_buf := g.sql_buf + + g.sql_select_expr(expr, true, '\t${tmp}.$field.name =') + g.writeln('//parse struct end') + + g.sql_stmt_name = tmp_sql_stmt_name + g.sql_buf = tmp_sql_buf + g.sql_i = tmp_sql_i } else { g.writeln('${tmp}.$field.name = ${func}($g.sql_stmt_name, $i);') } diff --git a/vlib/v/tests/orm_sub_struct_test.v b/vlib/v/tests/orm_sub_struct_test.v new file mode 100644 index 0000000000..2fde0f63f3 --- /dev/null +++ b/vlib/v/tests/orm_sub_struct_test.v @@ -0,0 +1,33 @@ +import sqlite + +struct Upper { + id int + sub SubStruct +} + +struct SubStruct { + id int + name string +} + +fn test_orm_sub_structs() { + db := sqlite.connect(':memory:') or { panic(err) } + db.exec('create table Upper (id integer primary key, sub int default 0)') + db.exec('create table SubStruct (id integer primary key, name string default "")') + + upper_1 := Upper{ + sub: SubStruct{ + name: 'test123' + } + } + + sql db { + insert upper_1 into Upper + } + + upper_s := sql db { + select from Upper where id == 1 + } + + assert upper_s.sub.name == upper_1.sub.name +} \ No newline at end of file