diff --git a/vlib/orm/orm_test.v b/vlib/orm/orm_test.v index b9a0c83f37..a8d5f3527d 100644 --- a/vlib/orm/orm_test.v +++ b/vlib/orm/orm_test.v @@ -1,6 +1,6 @@ -import os -import pg -import term +//import os +//import pg +//import term import sqlite struct Modules { @@ -11,30 +11,48 @@ struct Modules { //nr_downloads int } +struct User { + id int + name string +} + fn test_orm_sqlite() { db := sqlite.connect(':memory:') or { panic(err) } - /* - db.exec("drop table if exists users") - db.exec("create table users (id integer primary key, name text default '');") + db.exec("drop table if exists User") + db.exec("create table User (id integer primary key, name text default '');") - db.exec("insert into users (name) values ('Sam')") - db.exec("insert into users (name) values ('Peter')") - db.exec("insert into users (name) values ('Kate')") - nr_users := sql db { - //select count from modules + name := 'sam' + + db.exec("insert into User (name) values ('Sam')") + db.exec("insert into User (name) values ('Peter')") + db.exec("insert into User (name) values ('Kate')") + nr_all_users := sql db { + select count from User } - assert nr_users == 3 - println('nr_users=') - println(nr_users) - //nr_modules := db.select count from modules - //nr_modules := db.select count from Modules where id == 1 - //nr_modules := db.select count from Modules where - //name == 'Bob' && id == 1 - */ + assert nr_all_users == 3 + println('nr_all_users=$nr_all_users') + // + nr_users1 := sql db { + select count from User where id == 1 + } + assert nr_users1 == 1 + println('nr_users1=$nr_users1') + // + nr_peters := sql db { + select count from User where id == 2 && name == 'Peter' + } + assert nr_peters == 1 + println('nr_peters=$nr_peters') + // + nr_sams := sql db { + select count from User where id == 1 && name == name + } + println('nr_sams=$nr_sams') } fn test_orm_pg() { +/* dbname := os.getenv('VDB_NAME') dbuser := os.getenv('VDB_USER') if dbname == '' || dbuser == '' { @@ -43,8 +61,7 @@ fn test_orm_pg() { } db := pg.connect(dbname: dbname, user: dbuser) or { panic(err) } _ = db -/* - //nr_modules := db.select count from modules + nr_modules := db.select count from modules //nr_modules := db.select count from Modules where id == 1 nr_modules := db.select count from Modules where name == 'Bob' && id == 1 @@ -61,6 +78,7 @@ fn test_orm_pg() { /* mod := db.retrieve(1) + mod := db.select from Module where id = 1 mod := db.update Module set name = name + '!' where id > 10 diff --git a/vlib/sqlite/sqlite.v b/vlib/sqlite/sqlite.v index 4d0619e821..8e9e490b7c 100644 --- a/vlib/sqlite/sqlite.v +++ b/vlib/sqlite/sqlite.v @@ -37,6 +37,21 @@ pub fn connect(path string) ?DB { } } +// Only for V ORM +fn (db DB) init_stmt(query string) &C.sqlite3_stmt { + stmt := &C.sqlite3_stmt(0) + C.sqlite3_prepare_v2(db.conn, query.str, -1, &stmt, 0) + return stmt +} + +// Only for V ORM +fn get_int_from_stmt(stmt &C.sqlite3_stmt) int { + C.sqlite3_step(stmt) + res := C.sqlite3_column_int(stmt, 0) + C.sqlite3_finalize(stmt) + return res +} + // Returns a single cell with value int. pub fn (db DB) q_int(query string) int { stmt := &C.sqlite3_stmt(0) diff --git a/vlib/strings/builder.c.v b/vlib/strings/builder.c.v index 040d87fb11..9dbd08a795 100644 --- a/vlib/strings/builder.c.v +++ b/vlib/strings/builder.c.v @@ -49,6 +49,24 @@ pub fn (mut b Builder) go_back(n int) { b.len -= n } +pub fn (mut b Builder) cut_last(n int) string { + buf := b.buf[b.len-n..] + s := string(buf.clone()) + b.buf.trim(b.buf.len-n) + b.len -= n + return s +} + +/* +pub fn (mut b Builder) cut_to(pos int) string { + buf := b.buf[pos..] + s := string(buf.clone()) + b.buf.trim(pos) + b.len = pos + return s +} +*/ + pub fn (mut b Builder) go_back_to(pos int) { b.buf.trim(pos) b.len = pos diff --git a/vlib/strings/builder_test.v b/vlib/strings/builder_test.v index 2f66fd401b..bf89f4ef9b 100644 --- a/vlib/strings/builder_test.v +++ b/vlib/strings/builder_test.v @@ -8,11 +8,25 @@ fn test_sb() { assert sb.len == 8 assert sb.str() == 'hi!hello' assert sb.len == 0 + /// sb = strings.new_builder(10) sb.write('a') sb.write('b') assert sb.len == 2 assert sb.str() == 'ab' + /// + sb = strings.new_builder(10) + sb.write('123456') + assert sb.cut_last(2) == '56' + assert sb.str() == '1234' + /// + /* + sb = strings.new_builder(10) + sb.write('123456') + x := sb.cut_to(2) + assert x == '456' + assert sb.str() == '123' + */ } const ( diff --git a/vlib/term/term.v b/vlib/term/term.v index 7db9c14336..8e3bf12b2f 100644 --- a/vlib/term/term.v +++ b/vlib/term/term.v @@ -61,6 +61,10 @@ pub fn header(text, divider string) string { } fn supports_escape_sequences(fd int) bool { + //println('TERM=' + os.getenv('TERM')) + if os.getenv('TERM') == 'dumb' { + return false + } vcolors_override := os.getenv('VCOLORS') if vcolors_override == 'always' { return true diff --git a/vlib/v/ast/ast.v b/vlib/v/ast/ast.v index f1a5173c70..0a3a0b94ee 100644 --- a/vlib/v/ast/ast.v +++ b/vlib/v/ast/ast.v @@ -801,7 +801,13 @@ pub: } pub struct SqlExpr { - typ table.Type +pub: + typ table.Type + is_count bool + db_var_name string // `db` in `sql db {` + table_name string + where_expr Expr + has_where bool } [inline] diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 8c1b6f4dd6..a6cd4e3d12 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -556,7 +556,8 @@ pub fn (mut c Checker) infix_expr(mut infix_expr ast.InfixExpr) table.Type { c.error('$infix_expr.op.str(): type `${typ_sym.name}` does not exist', type_expr.pos) } if left.kind != .interface_ && left.kind != .sum_type { - c.error('`$infix_expr.op.str()` can only be used with interfaces and sum types', type_expr.pos) + c.error('`$infix_expr.op.str()` can only be used with interfaces and sum types', + type_expr.pos) } return table.bool_type } @@ -681,9 +682,12 @@ fn (mut c Checker) check_map_and_filter(is_map bool, elem_typ table.Type, call_e ast.AnonFn { if it.decl.args.len > 1 { c.error('function needs exactly 1 argument', call_expr.pos) - } else if is_map && (it.decl.return_type != elem_typ || it.decl.args[0].typ != elem_typ) { - c.error('type mismatch, should use `fn(a $elem_sym.name) $elem_sym.name {...}`', call_expr.pos) - } else if !is_map && (it.decl.return_type != table.bool_type || it.decl.args[0].typ != elem_typ) { + } else if is_map && (it.decl.return_type != elem_typ || it.decl.args[0].typ != + elem_typ) { + c.error('type mismatch, should use `fn(a $elem_sym.name) $elem_sym.name {...}`', + call_expr.pos) + } else if !is_map && (it.decl.return_type != table.bool_type || it.decl.args[0].typ != + elem_typ) { c.error('type mismatch, should use `fn(a $elem_sym.name) bool {...}`', call_expr.pos) } } @@ -696,9 +700,12 @@ fn (mut c Checker) check_map_and_filter(is_map bool, elem_typ table.Type, call_e if func.args.len > 1 { c.error('function needs exactly 1 argument', call_expr.pos) } else if is_map && (func.return_type != elem_typ || func.args[0].typ != elem_typ) { - c.error('type mismatch, should use `fn(a $elem_sym.name) $elem_sym.name {...}`', call_expr.pos) - } else if !is_map && (func.return_type != table.bool_type || func.args[0].typ != elem_typ) { - c.error('type mismatch, should use `fn(a $elem_sym.name) bool {...}`', call_expr.pos) + c.error('type mismatch, should use `fn(a $elem_sym.name) $elem_sym.name {...}`', + call_expr.pos) + } else if !is_map && (func.return_type != table.bool_type || func.args[0].typ != + elem_typ) { + c.error('type mismatch, should use `fn(a $elem_sym.name) bool {...}`', + call_expr.pos) } } } @@ -1948,15 +1955,29 @@ pub fn (mut c Checker) expr(node ast.Expr) table.Type { ast.None { return table.none_type } + ast.OrExpr { + // never happens + return table.void_type + } ast.ParExpr { return c.expr(it.expr) } + ast.RangeExpr { + // never happens + return table.void_type + } ast.SelectorExpr { return c.selector_expr(mut it) } ast.SizeOf { return table.u32_type } + ast.SqlExpr { + if it.has_where { + c.expr(it.where_expr) + } + return it.typ + } ast.StringLiteral { if it.language == .c { return table.byteptr_type @@ -1986,12 +2007,6 @@ pub fn (mut c Checker) expr(node ast.Expr) table.Type { } return table.bool_type } - else { - tnode := typeof(node) - if tnode != 'unknown v.ast.Expr' { - println('checker.expr(): unhandled node with typeof(`${tnode}`)') - } - } } return table.void_type } diff --git a/vlib/v/gen/cgen.v b/vlib/v/gen/cgen.v index 4fbd64b833..af7f840bd2 100644 --- a/vlib/v/gen/cgen.v +++ b/vlib/v/gen/cgen.v @@ -66,6 +66,7 @@ mut: options strings.Builder // `Option_xxxx` types json_forward_decls strings.Builder // json type forward decls enum_typedefs strings.Builder // enum types + sql_buf strings.Builder // for writing exprs to args via `sqlite3_bind_int()` etc file ast.File fn_decl &ast.FnDecl // pointer to the FnDecl we are currently inside otherwise 0 last_fn_c_name string @@ -76,6 +77,7 @@ mut: is_assign_rhs bool // inside right part of assign after `=` (val expr) is_array_set bool is_amp bool // for `&Foo{}` to merge PrefixExpr `&` and StructInit `Foo{}`; also for `&byte(0)` etc + is_sql bool // Inside `sql db{}` statement, generating sql instead of C (e.g. `and` instead of `&&` etc) optionals []string // to avoid duplicates TODO perf, use map inside_ternary int // ?: comma separated statements on a single line ternary_names map[string]string @@ -102,6 +104,8 @@ mut: fn_main &ast.FnDecl // the FnDecl of the main function. Needed in order to generate the main function code *last* cur_fn &ast.FnDecl cur_generic_type table.Type // `int`, `string`, etc in `foo()` + sql_i int + sql_stmt_name string } const ( @@ -131,6 +135,7 @@ pub fn cgen(files []ast.File, table &table.Table, pref &pref.Preferences) string options: strings.new_builder(100) json_forward_decls: strings.new_builder(100) enum_typedefs: strings.new_builder(100) + sql_buf: strings.new_builder(100) table: table pref: pref fn_decl: 0 @@ -1544,7 +1549,7 @@ fn (mut g Gen) expr(node ast.Expr) { } ast.RangeExpr { // Only used in IndexExpr - } + } ast.SizeOf { mut styp := it.type_name if it.type_name == '' { @@ -1566,7 +1571,7 @@ fn (mut g Gen) expr(node ast.Expr) { g.write('sizeof($styp)') } ast.SqlExpr { - g.write('// sql expression') + g.sql_expr(it) } ast.StringLiteral { if it.is_raw { @@ -1630,10 +1635,6 @@ fn (mut g Gen) expr(node ast.Expr) { g.expr(it.expr) g.write(')') } - //else { - // #printf("node=%d\n", node.typ); - //println(term.red('cgen.expr(): bad node ' + typeof(node))) - //} } } @@ -3173,6 +3174,15 @@ fn (mut g Gen) insert_before_stmt(s string) { g.write(cur_line) } +fn (mut g Gen) write_expr_to_string(expr ast.Expr) string { + pos := g.out.buf.len + g.expr(expr) + return g.out.cut_last(g.out.buf.len - pos) +} + +fn (mut g Gen) start_tmp() { +} + // If user is accessing the return value eg. in assigment, pass the variable name. // If the user is not using the optional return value. We need to pass a temp var // to access its fields (`.ok`, `.error` etc) @@ -3263,7 +3273,6 @@ fn (mut g Gen) in_optimization(left ast.Expr, right ast.ArrayInit) { ptr_typ := g.gen_array_equality_fn(right.elem_type) g.write('${ptr_typ}_arr_eq(') } - g.expr(left) if is_str || is_array { g.write(', ') @@ -4208,7 +4217,6 @@ fn (mut g Gen) array_init(it ast.ArrayInit) { if it.exprs.len == 0 { elem_sym := g.table.get_type_symbol(it.elem_type) is_default_array := elem_sym.kind == .array && it.has_default - if is_default_array { g.write('__new_array_with_array_default(') } else { diff --git a/vlib/v/gen/sql.v b/vlib/v/gen/sql.v new file mode 100644 index 0000000000..36a6c683e1 --- /dev/null +++ b/vlib/v/gen/sql.v @@ -0,0 +1,93 @@ +// Copyright (c) 2019-2020 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module gen + +import v.ast +import strings + +// pg,mysql etc +const ( + dbtype = 'sqlite' +) + +fn (mut g Gen) sql_expr(node ast.SqlExpr) { + g.sql_i = 0 + /* + `nr_users := sql db { ... }` => + ``` + sql_init_stmt() + sql_bind_int() + sql_bind_string() + ... + int nr_users = get_int(stmt) + ``` + */ + cur_line := g.go_before_stmt(0) + mut q := 'select ' + if node.is_count { + // select count(*) from User + q += 'count(*) from $node.table_name' + } + if node.has_where { + q += ' where ' + } + // g.write('${dbtype}__DB_q_int(*(${dbtype}__DB*)${node.db_var_name}.data, tos_lit("$q') + g.sql_stmt_name = g.new_tmp_var() + db_name := g.new_tmp_var() + g.writeln('\n\t// sql') + g.write('${dbtype}__DB $db_name = *(${dbtype}__DB*)${node.db_var_name}.data;') + g.write('sqlite3_stmt* $g.sql_stmt_name = ${dbtype}__DB_init_stmt(*(${dbtype}__DB*)${node.db_var_name}.data, tos_lit("$q') + if node.has_where && node.where_expr is ast.InfixExpr { + g.expr_to_sql(node.where_expr) + } + g.writeln('"));') + // Dump all sql parameters generated by our custom expr handler + binds := g.sql_buf.str() + g.sql_buf = strings.new_builder(100) + g.writeln(binds) + g.writeln('puts(sqlite3_errmsg(${db_name}.conn));') + g.writeln('$cur_line ${dbtype}__get_int_from_stmt($g.sql_stmt_name);') +} + +fn (mut g Gen) expr_to_sql(expr ast.Expr) { + // Custom handling for infix exprs (since we need e.g. `and` instead of `&&` in SQL queries), + // strings. Everything else (like numbers, a.b) is handled by g.expr() + // + // TODO `where id = some_column + 1` needs literal generation of `some_column` as a string, + // not a V variable. Need to distinguish column names from V variables. + match expr { + ast.InfixExpr { + g.expr_to_sql(it.left) + match it.op { + .eq { g.write(' = ') } + .and { g.write(' and ') } + else {} + } + g.expr_to_sql(it.right) + } + ast.StringLiteral { + // g.write("'$it.val'") + g.inc_sql_i() + g.sql_buf.writeln('sqlite3_bind_text($g.sql_stmt_name, $g.sql_i, "$it.val", $it.val.len, 0);') + } + ast.IntegerLiteral { + g.inc_sql_i() + g.sql_buf.writeln('sqlite3_bind_int($g.sql_stmt_name, $g.sql_i, $it.val);') + } + else { + g.expr(expr) + } + } + /* + ast.Ident { + g.write('$it.name') + } + else {} + */ +} + +fn (mut g Gen) inc_sql_i() { + g.sql_i++ + g.write('?$g.sql_i') +} diff --git a/vlib/v/parser/sql.v b/vlib/v/parser/sql.v index fff8dc14a0..284f9fa40f 100644 --- a/vlib/v/parser/sql.v +++ b/vlib/v/parser/sql.v @@ -4,7 +4,76 @@ module parser import v.ast +import v.table fn (mut p Parser) sql_expr() ast.SqlExpr { - return ast.SqlExpr{} + // `sql db {` + p.check_name() + db_var_name := p.check_name() + p.check(.lcbr) + // + p.check(.key_select) + n := p.check_name() + is_count := n == 'count' + mut typ := table.void_type + if is_count { + p.check_name() // from + typ = table.int_type + } + table_type := p.parse_type() // `User` + sym := p.table.get_type_symbol(table_type) + table_name := sym.name + mut where_expr := ast.Expr{} + has_where := p.tok.kind == .name && p.tok.lit == 'where' + if has_where { + p.next() + where_expr = p.expr(0) + } + p.check(.rcbr) + // ///////// + // Register this type's fields as variables so they can be used in `where` + // expressions + // fields := typ.fields.filter(typ == 'string' || typ == 'int') + // fields := typ.fields + // get only string and int fields + // mut fields := []Var + info := sym.info as table.Struct + fields := info.fields.filter(it.typ in [table.string_type, table.int_type, table.bool_type]) + /* + for i, field in info.fields { + if !(field.typ in ['string', 'int', 'bool']) { + println('orm: skipping $field.name') + continue + } + if field.attr.contains('skip') { + continue + } + fields << field +} + */ + if fields.len == 0 { + p.error('V orm: select: empty fields in `$table_name`') + } + if fields[0].name != 'id' { + p.error('V orm: `id int` must be the first field in `$table_name`') + } + for field in fields { + // println('registering sql field var $field.name') + p.scope.register(field.name, ast.Var{ + name: field.name + typ: field.typ + is_mut: true + is_used: true + is_changed: true + }) + } + // //////////// + return ast.SqlExpr{ + is_count: is_count + typ: typ + db_var_name: db_var_name + table_name: table_name + where_expr: where_expr + has_where: has_where + } }