v orm: select

pull/5401/head
Alexander Medvednikov 2020-06-17 00:59:33 +02:00
parent 23993d2264
commit ed58192e4c
10 changed files with 304 additions and 44 deletions

View File

@ -1,6 +1,6 @@
import os //import os
import pg //import pg
import term //import term
import sqlite import sqlite
struct Modules { struct Modules {
@ -11,30 +11,48 @@ struct Modules {
//nr_downloads int //nr_downloads int
} }
struct User {
id int
name string
}
fn test_orm_sqlite() { fn test_orm_sqlite() {
db := sqlite.connect(':memory:') or { panic(err) } db := sqlite.connect(':memory:') or { panic(err) }
/* db.exec("drop table if exists User")
db.exec("drop table if exists users") db.exec("create table User (id integer primary key, name text default '');")
db.exec("create table users (id integer primary key, name text default '');")
db.exec("insert into users (name) values ('Sam')") name := 'sam'
db.exec("insert into users (name) values ('Peter')")
db.exec("insert into users (name) values ('Kate')") db.exec("insert into User (name) values ('Sam')")
nr_users := sql db { db.exec("insert into User (name) values ('Peter')")
//select count from modules db.exec("insert into User (name) values ('Kate')")
nr_all_users := sql db {
select count from User
} }
assert nr_users == 3 assert nr_all_users == 3
println('nr_users=') println('nr_all_users=$nr_all_users')
println(nr_users) //
//nr_modules := db.select count from modules nr_users1 := sql db {
//nr_modules := db.select count from Modules where id == 1 select count from User where id == 1
//nr_modules := db.select count from Modules where }
//name == 'Bob' && 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() { fn test_orm_pg() {
/*
dbname := os.getenv('VDB_NAME') dbname := os.getenv('VDB_NAME')
dbuser := os.getenv('VDB_USER') dbuser := os.getenv('VDB_USER')
if dbname == '' || dbuser == '' { if dbname == '' || dbuser == '' {
@ -43,8 +61,7 @@ fn test_orm_pg() {
} }
db := pg.connect(dbname: dbname, user: dbuser) or { panic(err) } db := pg.connect(dbname: dbname, user: dbuser) or { panic(err) }
_ = db _ = 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 id == 1
nr_modules := db.select count from Modules where nr_modules := db.select count from Modules where
name == 'Bob' && id == 1 name == 'Bob' && id == 1
@ -61,6 +78,7 @@ fn test_orm_pg() {
/* /*
mod := db.retrieve<Module>(1) mod := db.retrieve<Module>(1)
mod := db.select from Module where id = 1
mod := db.update Module set name = name + '!' where id > 10 mod := db.update Module set name = name + '!' where id > 10

View File

@ -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. // Returns a single cell with value int.
pub fn (db DB) q_int(query string) int { pub fn (db DB) q_int(query string) int {
stmt := &C.sqlite3_stmt(0) stmt := &C.sqlite3_stmt(0)

View File

@ -49,6 +49,24 @@ pub fn (mut b Builder) go_back(n int) {
b.len -= n 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) { pub fn (mut b Builder) go_back_to(pos int) {
b.buf.trim(pos) b.buf.trim(pos)
b.len = pos b.len = pos

View File

@ -8,11 +8,25 @@ fn test_sb() {
assert sb.len == 8 assert sb.len == 8
assert sb.str() == 'hi!hello' assert sb.str() == 'hi!hello'
assert sb.len == 0 assert sb.len == 0
///
sb = strings.new_builder(10) sb = strings.new_builder(10)
sb.write('a') sb.write('a')
sb.write('b') sb.write('b')
assert sb.len == 2 assert sb.len == 2
assert sb.str() == 'ab' 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 ( const (

View File

@ -61,6 +61,10 @@ pub fn header(text, divider string) string {
} }
fn supports_escape_sequences(fd int) bool { fn supports_escape_sequences(fd int) bool {
//println('TERM=' + os.getenv('TERM'))
if os.getenv('TERM') == 'dumb' {
return false
}
vcolors_override := os.getenv('VCOLORS') vcolors_override := os.getenv('VCOLORS')
if vcolors_override == 'always' { if vcolors_override == 'always' {
return true return true

View File

@ -801,7 +801,13 @@ pub:
} }
pub struct SqlExpr { pub struct SqlExpr {
pub:
typ table.Type typ table.Type
is_count bool
db_var_name string // `db` in `sql db {`
table_name string
where_expr Expr
has_where bool
} }
[inline] [inline]

View File

@ -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) c.error('$infix_expr.op.str(): type `${typ_sym.name}` does not exist', type_expr.pos)
} }
if left.kind != .interface_ && left.kind != .sum_type { 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 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 { ast.AnonFn {
if it.decl.args.len > 1 { if it.decl.args.len > 1 {
c.error('function needs exactly 1 argument', call_expr.pos) 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) { } else if is_map && (it.decl.return_type != elem_typ || it.decl.args[0].typ !=
c.error('type mismatch, should use `fn(a $elem_sym.name) $elem_sym.name {...}`', call_expr.pos) elem_typ) {
} 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) $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) 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 { if func.args.len > 1 {
c.error('function needs exactly 1 argument', call_expr.pos) 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) { } 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) c.error('type mismatch, should use `fn(a $elem_sym.name) $elem_sym.name {...}`',
} else if !is_map && (func.return_type != table.bool_type || func.args[0].typ != elem_typ) { call_expr.pos)
c.error('type mismatch, should use `fn(a $elem_sym.name) bool {...}`', 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 { ast.None {
return table.none_type return table.none_type
} }
ast.OrExpr {
// never happens
return table.void_type
}
ast.ParExpr { ast.ParExpr {
return c.expr(it.expr) return c.expr(it.expr)
} }
ast.RangeExpr {
// never happens
return table.void_type
}
ast.SelectorExpr { ast.SelectorExpr {
return c.selector_expr(mut it) return c.selector_expr(mut it)
} }
ast.SizeOf { ast.SizeOf {
return table.u32_type return table.u32_type
} }
ast.SqlExpr {
if it.has_where {
c.expr(it.where_expr)
}
return it.typ
}
ast.StringLiteral { ast.StringLiteral {
if it.language == .c { if it.language == .c {
return table.byteptr_type return table.byteptr_type
@ -1986,12 +2007,6 @@ pub fn (mut c Checker) expr(node ast.Expr) table.Type {
} }
return table.bool_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 return table.void_type
} }

View File

@ -66,6 +66,7 @@ mut:
options strings.Builder // `Option_xxxx` types options strings.Builder // `Option_xxxx` types
json_forward_decls strings.Builder // json type forward decls json_forward_decls strings.Builder // json type forward decls
enum_typedefs strings.Builder // enum types enum_typedefs strings.Builder // enum types
sql_buf strings.Builder // for writing exprs to args via `sqlite3_bind_int()` etc
file ast.File file ast.File
fn_decl &ast.FnDecl // pointer to the FnDecl we are currently inside otherwise 0 fn_decl &ast.FnDecl // pointer to the FnDecl we are currently inside otherwise 0
last_fn_c_name string last_fn_c_name string
@ -76,6 +77,7 @@ mut:
is_assign_rhs bool // inside right part of assign after `=` (val expr) is_assign_rhs bool // inside right part of assign after `=` (val expr)
is_array_set bool is_array_set bool
is_amp bool // for `&Foo{}` to merge PrefixExpr `&` and StructInit `Foo{}`; also for `&byte(0)` etc 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 optionals []string // to avoid duplicates TODO perf, use map
inside_ternary int // ?: comma separated statements on a single line inside_ternary int // ?: comma separated statements on a single line
ternary_names map[string]string 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* 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_fn &ast.FnDecl
cur_generic_type table.Type // `int`, `string`, etc in `foo<T>()` cur_generic_type table.Type // `int`, `string`, etc in `foo<T>()`
sql_i int
sql_stmt_name string
} }
const ( const (
@ -131,6 +135,7 @@ pub fn cgen(files []ast.File, table &table.Table, pref &pref.Preferences) string
options: strings.new_builder(100) options: strings.new_builder(100)
json_forward_decls: strings.new_builder(100) json_forward_decls: strings.new_builder(100)
enum_typedefs: strings.new_builder(100) enum_typedefs: strings.new_builder(100)
sql_buf: strings.new_builder(100)
table: table table: table
pref: pref pref: pref
fn_decl: 0 fn_decl: 0
@ -1566,7 +1571,7 @@ fn (mut g Gen) expr(node ast.Expr) {
g.write('sizeof($styp)') g.write('sizeof($styp)')
} }
ast.SqlExpr { ast.SqlExpr {
g.write('// sql expression') g.sql_expr(it)
} }
ast.StringLiteral { ast.StringLiteral {
if it.is_raw { if it.is_raw {
@ -1630,10 +1635,6 @@ fn (mut g Gen) expr(node ast.Expr) {
g.expr(it.expr) g.expr(it.expr)
g.write(')') 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) 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 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 // If the user is not using the optional return value. We need to pass a temp var
// to access its fields (`.ok`, `.error` etc) // 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) ptr_typ := g.gen_array_equality_fn(right.elem_type)
g.write('${ptr_typ}_arr_eq(') g.write('${ptr_typ}_arr_eq(')
} }
g.expr(left) g.expr(left)
if is_str || is_array { if is_str || is_array {
g.write(', ') g.write(', ')
@ -4208,7 +4217,6 @@ fn (mut g Gen) array_init(it ast.ArrayInit) {
if it.exprs.len == 0 { if it.exprs.len == 0 {
elem_sym := g.table.get_type_symbol(it.elem_type) elem_sym := g.table.get_type_symbol(it.elem_type)
is_default_array := elem_sym.kind == .array && it.has_default is_default_array := elem_sym.kind == .array && it.has_default
if is_default_array { if is_default_array {
g.write('__new_array_with_array_default(') g.write('__new_array_with_array_default(')
} else { } else {

93
vlib/v/gen/sql.v 100644
View File

@ -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')
}

View File

@ -4,7 +4,76 @@
module parser module parser
import v.ast import v.ast
import v.table
fn (mut p Parser) sql_expr() ast.SqlExpr { 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
}
} }