From 64391efa4d673a20e656ade9272612fb93704fee Mon Sep 17 00:00:00 2001 From: Louis Schmieder Date: Sat, 10 Apr 2021 16:38:27 +0200 Subject: [PATCH] orm: add mysql support (#9630) * add mysql to orm * fix got to big packet error * format sql.v * format example * custom sql types * add mysql table cration * add documentation * format sql.v * fix markdown * start implementing select_expr for mysql * remove orm.c * format sql.v * finish mysql expr * remove c * remove unessecary files * change to c implementation * remove c * added str interpolation for idents * fix string insert * fix compilation problems * fix gitly compilation * fix typing mistake * add link to orm docs --- cmd/tools/modules/testing/common.v | 2 + doc/docs.md | 20 +- examples/database/orm.v | 52 +- vlib/orm/README.md | 71 +++ vlib/v/gen/c/cgen.v | 5 + vlib/v/gen/c/sql.v | 789 ++++++++++++++++++++++------- 6 files changed, 748 insertions(+), 191 deletions(-) create mode 100644 vlib/orm/README.md diff --git a/cmd/tools/modules/testing/common.v b/cmd/tools/modules/testing/common.v index aed98ff061..5289dcb3c7 100644 --- a/cmd/tools/modules/testing/common.v +++ b/cmd/tools/modules/testing/common.v @@ -118,9 +118,11 @@ pub fn new_test_session(_vargs string) TestSession { } $if macos { skip_files << 'examples/database/mysql.v' + skip_files << 'examples/database/orm.v' } $if windows { skip_files << 'examples/database/mysql.v' + skip_files << 'examples/database/orm.v' skip_files << 'examples/websocket/ping.v' // requires OpenSSL skip_files << 'examples/websocket/client-server/client.v' // requires OpenSSL skip_files << 'examples/websocket/client-server/server.v' // requires OpenSSL diff --git a/doc/docs.md b/doc/docs.md index 3327d39b9e..7a796de748 100644 --- a/doc/docs.md +++ b/doc/docs.md @@ -3147,8 +3147,8 @@ fn test() []int { (This is still in an alpha state) -V has a built-in ORM (object-relational mapping) which supports SQLite, -and will soon support MySQL, Postgres, MS SQL, and Oracle. +V has a built-in ORM (object-relational mapping) which supports SQLite and MySQL, +but soon it will support Postgres, MS SQL, and Oracle. V's ORM provides a number of benefits: @@ -3164,20 +3164,26 @@ import sqlite struct Customer { // struct name has to be the same as the table name (for now) - id int // a field named `id` of integer type must be the first field - name string + id int [primary; sql: serial] // a field named `id` of integer type must be the first field + name string [nonull] nr_orders int - country string + country string [nonull] } db := sqlite.connect('customers.db') ? + +// you can create tables +// CREATE TABLE IF NOT EXISTS `Customer` (`id` INTEGER PRIMARY KEY, `name` TEXT NOT NULL, `nr_orders` INTEGER, `country` TEXT NOT NULL) +sql db { + create table Customer +} + // select count(*) from Customer nr_customers := sql db { select count from Customer } println('number of all customers: $nr_customers') // V syntax can be used to build queries -// db.select returns an array uk_customers := sql db { select from Customer where country == 'uk' && nr_orders > 0 } @@ -3200,7 +3206,7 @@ sql db { } ``` -For more examples, see vlib/orm/orm_test.v. +For more examples and the docs, see vlib/orm. ## Writing Documentation diff --git a/examples/database/orm.v b/examples/database/orm.v index 2a39a82dfb..7cc1fb847f 100644 --- a/examples/database/orm.v +++ b/examples/database/orm.v @@ -1,16 +1,17 @@ import sqlite +import mysql struct Module { - id int + id int [primary; sql: serial] name string - nr_downloads int + nr_downloads int [sql: u64] creator User } struct User { - id int + id int [primary; sql: serial] age int - name string + name string [nonull] is_customer bool skipped_string string [skip] } @@ -18,8 +19,9 @@ struct User { 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);") + sql db { + create table Module + } mod := Module{ name: 'test' @@ -38,7 +40,41 @@ fn main() { select from Module where id == 1 } - println(modul.name) - println(modul.creator.name) + eprintln(modul) + mysql() +} + +fn mysql() { + mut conn := mysql.Connection{ + host: 'localhost' + port: 3306 + username: 'root' + password: 'abc' + dbname: 'test' + } + conn.connect() or { panic(err) } + + sql conn { + create table Module + } + + mod := Module{ + name: 'test' + nr_downloads: 10 + creator: User{ + age: 21 + name: 'VUser' + is_customer: true + } + } + + sql conn { + insert mod into Module + } + + m := sql conn { + select from Module where id == 1 + } + eprintln(m) } diff --git a/vlib/orm/README.md b/vlib/orm/README.md new file mode 100644 index 0000000000..0293676cd2 --- /dev/null +++ b/vlib/orm/README.md @@ -0,0 +1,71 @@ +# ORM + +## Attributes + +### Fields + +- `[primary]` set the field as the primary key +- `[nonull]` field will be `NOT NULL` in table creation +- `[skip]` field will be skipped +- `[sql: type]` sets the type which is used in sql (special type `serial`) + +## Usage + +```v ignore +struct Foo { + id int [primary; sql: serial] + name string [nonull] +} +``` + +### Create + +```v ignore +sql db { + create table Foo +} +``` + +### Insert + +```v ignore +var := Foo{ + name: 'abc' +} + +sql db { + insert var into Foo +} +``` + +### Update + +```v ignore +sql db { + update Foo set name = 'cde' where name == 'abc' +} +``` + +### Delete +```v ignore +sql db { + delete from Foo where id > 10 +} +``` + +### Select +```v ignore +result := sql db { + select from Foo where id == 1 +} +``` +```v ignore +result := sql db { + select from Foo where id > 1 limit 5 +} +``` +```v ignore +result := sql db { + select from Foo where id > 1 order by id +} +``` \ No newline at end of file diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index bd9af70e7e..78a9e59de0 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -116,6 +116,11 @@ mut: cur_generic_types []ast.Type // `int`, `string`, etc in `foo()` sql_i int sql_stmt_name string + sql_bind_name string + sql_idents []string + sql_idents_types []ast.Type + sql_left_type ast.Type + sql_table_name string sql_side SqlExprSide // left or right, to distinguish idents in `name == name` inside_vweb_tmpl bool inside_return bool diff --git a/vlib/v/gen/c/sql.v b/vlib/v/gen/c/sql.v index 02fb4073ab..f147093798 100644 --- a/vlib/v/gen/c/sql.v +++ b/vlib/v/gen/c/sql.v @@ -28,11 +28,15 @@ fn (mut g Gen) sql_stmt(node ast.SqlStmt) { g.sql_create_table(node) return } + g.sql_table_name = g.table.get_type_symbol(node.table_expr.typ).name typ := g.parse_db_type(node.db_expr) match typ { .sqlite3 { g.sqlite3_stmt(node, typ) } + .mysql { + g.mysql_stmt(node, typ) + } else { verror('This database type `$typ` is not implemented yet in orm') // TODO add better error } @@ -45,6 +49,9 @@ fn (mut g Gen) sql_create_table(node ast.SqlStmt) { .sqlite3 { g.sqlite3_create_table(node, typ) } + .mysql { + g.mysql_create_table(node, typ) + } else { verror('This database type `$typ` is not implemented yet in orm') // TODO add better error } @@ -52,43 +59,40 @@ fn (mut g Gen) sql_create_table(node ast.SqlStmt) { } fn (mut g Gen) sql_select_expr(node ast.SqlExpr, sub bool, line string) { + g.sql_table_name = g.table.get_type_symbol(node.table_expr.typ).name typ := g.parse_db_type(node.db_expr) match typ { .sqlite3 { g.sqlite3_select_expr(node, sub, line, typ) } + .mysql { + g.mysql_select_expr(node, sub, line, typ) + } else { verror('This database type `$typ` is not implemented yet in orm') // TODO add better error } } } -fn (mut g Gen) sql_bind_int(val string, typ SqlType) { +fn (mut g Gen) sql_bind(val string, len string, real_type ast.Type, typ SqlType) { match typ { .sqlite3 { - g.sqlite3_bind_int(val) + g.sqlite3_bind(val, len, real_type) } - else { - // add error - } - } -} - -fn (mut g Gen) sql_bind_string(val string, len string, typ SqlType) { - match typ { - .sqlite3 { - g.sqlite3_bind_string(val, len) - } - else { - // add error + .mysql { + g.mysql_bind(val, real_type) } + else {} } } fn (mut g Gen) sql_type_from_v(typ SqlType, v_typ ast.Type) string { match typ { .sqlite3 { - return g.sqlite3_type_from_v(typ, v_typ) + return g.sqlite3_type_from_v(v_typ) + } + .mysql { + return g.mysql_get_table_type(v_typ) } else { // add error @@ -108,51 +112,8 @@ fn (mut g Gen) sqlite3_stmt(node ast.SqlStmt, typ SqlType) { g.expr(node.db_expr) g.writeln(';') g.write('sqlite3_stmt* $g.sql_stmt_name = ${c.dbtype}__DB_init_stmt($db_name, _SLIT("') - table_name := util.strip_mod_name(g.table.get_type_symbol(node.table_expr.typ).name) - if node.kind == .insert { - g.write('INSERT INTO `$table_name` (') - } else if node.kind == .update { - g.write('UPDATE `$table_name` SET ') - } else if node.kind == .delete { - g.write('DELETE FROM `$table_name` ') - } - if node.kind == .insert { - for i, field in node.fields { - if field.name == 'id' { - continue - } - g.write('`$field.name`') - if i < node.fields.len - 1 { - g.write(', ') - } - } - g.write(') values (') - for i, field in node.fields { - if field.name == 'id' { - continue - } - g.write('?${i + 0}') - if i < node.fields.len - 1 { - g.write(', ') - } - } - g.write(')') - } else if node.kind == .update { - for i, col in node.updated_columns { - g.write(' $col = ') - g.expr_to_sql(node.update_exprs[i], typ) - if i < node.updated_columns.len - 1 { - g.write(', ') - } - } - g.write(' WHERE ') - } else if node.kind == .delete { - g.write(' WHERE ') - } - if node.kind == .update || node.kind == .delete { - g.expr_to_sql(node.where_expr, typ) - } - g.writeln('"));') + g.sql_defaults(node, typ) + g.writeln(');') if node.kind == .insert { // build the object now (`x.name = ... x.id == ...`) for i, field in node.fields { @@ -166,8 +127,10 @@ fn (mut g Gen) sqlite3_stmt(node ast.SqlStmt, typ SqlType) { // insert again expr := node.sub_structs[int(field.typ)] tmp_sql_stmt_name := g.sql_stmt_name + tmp_sql_table_name := g.sql_table_name g.sql_stmt(expr) g.sql_stmt_name = tmp_sql_stmt_name + g.sql_table_name = tmp_sql_table_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() @@ -204,24 +167,6 @@ fn (mut g Gen) sqlite3_select_expr(node ast.SqlExpr, sub bool, line string, sql_ 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 { - // `select count(*) from User` - sql_query += 'COUNT(*) FROM `$table_name` ' - } else { - // `select id, name, country from User` - for i, field in node.fields { - sql_query += '`$field.name`' - if i < node.fields.len - 1 { - sql_query += ', ' - } - } - sql_query += ' FROM `$table_name`' - } - if node.has_where { - sql_query += ' WHERE ' - } // g.write('${dbtype}__DB_q_int(*(${dbtype}__DB*)${node.db_var_name}.data, _SLIT("$sql_query') g.sql_stmt_name = g.new_tmp_var() db_name := g.new_tmp_var() @@ -230,33 +175,13 @@ fn (mut g Gen) sqlite3_select_expr(node ast.SqlExpr, sub bool, line string, sql_ g.write('${c.dbtype}__DB $db_name = ') // $node.db_var_name;') g.expr(node.db_expr) g.writeln(';') + stmt_name := g.new_tmp_var() + g.write('string $stmt_name = _SLIT("') + g.write(g.get_base_sql_select_query(node)) + g.sql_expr_defaults(node, sql_typ) + g.writeln('");') // g.write('sqlite3_stmt* $g.sql_stmt_name = ${dbtype}__DB_init_stmt(*(${dbtype}__DB*)${node.db_var_name}.data, _SLIT("$sql_query') - g.write('sqlite3_stmt* $g.sql_stmt_name = ${c.dbtype}__DB_init_stmt($db_name, _SLIT("') - g.write(sql_query) - if node.has_where && node.where_expr is ast.InfixExpr { - g.expr_to_sql(node.where_expr, sql_typ) - } - if node.has_order { - g.write(' ORDER BY ') - g.sql_side = .left - g.expr_to_sql(node.order_expr, sql_typ) - if node.has_desc { - g.write(' DESC ') - } - } else { - g.write(' ORDER BY id ') - } - if node.has_limit { - g.write(' LIMIT ') - g.sql_side = .right - g.expr_to_sql(node.limit_expr, sql_typ) - } - if node.has_offset { - g.write(' OFFSET ') - g.sql_side = .right - g.expr_to_sql(node.offset_expr, sql_typ) - } - g.writeln('"));') + g.write('sqlite3_stmt* $g.sql_stmt_name = ${c.dbtype}__DB_init_stmt($db_name, $stmt_name);') // Dump all sql parameters generated by our custom expr handler binds := g.sql_buf.str() g.sql_buf = strings.new_builder(100) @@ -341,6 +266,7 @@ fn (mut g Gen) sqlite3_select_expr(node ast.SqlExpr, sub bool, line string, sql_ tmp_sql_i := g.sql_i tmp_sql_stmt_name := g.sql_stmt_name tmp_sql_buf := g.sql_buf + tmp_sql_table_name := g.sql_table_name g.sql_select_expr(expr, true, '\t${tmp}.$field.name =') g.writeln('//parse struct end') @@ -348,6 +274,7 @@ fn (mut g Gen) sqlite3_select_expr(node ast.SqlExpr, sub bool, line string, sql_ g.sql_stmt_name = tmp_sql_stmt_name g.sql_buf = tmp_sql_buf g.sql_i = tmp_sql_i + g.sql_table_name = tmp_sql_table_name } else { g.writeln('${tmp}.$field.name = ${func}($g.sql_stmt_name, $i);') } @@ -366,36 +293,525 @@ fn (mut g Gen) sqlite3_select_expr(node ast.SqlExpr, sub bool, line string, sql_ } fn (mut g Gen) sqlite3_create_table(node ast.SqlStmt, typ SqlType) { + g.writeln('// sqlite3 table creator') + create_string := g.table_gen(node, typ) + g.write('sqlite__DB_exec(') + g.expr(node.db_expr) + g.writeln(', _SLIT("$create_string"));') +} + +fn (mut g Gen) sqlite3_bind(val string, len string, typ ast.Type) { + match g.sqlite3_type_from_v(typ) { + 'INTEGER' { + g.sqlite3_bind_int(val) + } + 'TEXT' { + g.sqlite3_bind_string(val, len) + } + else { + verror('bad sql type=$typ ident_name=$val') + } + } +} + +fn (mut g Gen) sqlite3_bind_int(val string) { + g.sql_buf.writeln('sqlite3_bind_int($g.sql_stmt_name, $g.sql_i, $val);') +} + +fn (mut g Gen) sqlite3_bind_string(val string, len string) { + g.sql_buf.writeln('sqlite3_bind_text($g.sql_stmt_name, $g.sql_i, $val, $len, 0);') +} + +fn (mut g Gen) sqlite3_type_from_v(v_typ ast.Type) string { + if v_typ.is_number() || v_typ == ast.bool_type || v_typ == -1 { + return 'INTEGER' + } + if v_typ.is_string() { + return 'TEXT' + } + return '' +} + +// mysql + +fn (mut g Gen) mysql_stmt(node ast.SqlStmt, typ SqlType) { + g.sql_i = 0 + g.writeln('\n\t//mysql insert') + db_name := g.new_tmp_var() + g.sql_stmt_name = g.new_tmp_var() + g.write('mysql__Connection $db_name = ') + g.expr(node.db_expr) + g.writeln(';') + stmt_name := g.new_tmp_var() + g.write('string $stmt_name = _SLIT("') + g.sql_defaults(node, typ) + g.writeln(';') + g.writeln('MYSQL_STMT* $g.sql_stmt_name = mysql_stmt_init(${db_name}.conn);') + g.writeln('mysql_stmt_prepare($g.sql_stmt_name, ${stmt_name}.str, ${stmt_name}.len);') + + bind := g.new_tmp_var() + g.writeln('MYSQL_BIND $bind[$g.sql_i];') + g.writeln('memset($bind, 0, sizeof(MYSQL_BIND)*$g.sql_i);') + if node.kind == .insert { + for i, field in node.fields { + if field.name == 'id' { + continue + } + g.writeln('//$field.name ($field.typ)') + x := '${node.object_var_name}.$field.name' + if g.table.type_symbols[int(field.typ)].kind == .struct_ { + // insert again + expr := node.sub_structs[int(field.typ)] + tmp_sql_stmt_name := g.sql_stmt_name + tmp_sql_table_name := g.sql_table_name + g.sql_stmt(expr) + g.sql_stmt_name = tmp_sql_stmt_name + g.sql_table_name = tmp_sql_table_name + + res := g.new_tmp_var() + g.writeln('int ${res}_err = mysql_real_query(${db_name}.conn, "SELECT LAST_INSERT_ID();", 24);') + g.writeln('if (${res}_err != 0) { puts(mysql_error(${db_name}.conn)); }') + g.writeln('MYSQL_RES* $res = mysql_store_result(${db_name}.conn);') + g.writeln('if (mysql_num_rows($res) != 1) { puts("Something went wrong"); }') + g.writeln('MYSQL_ROW ${res}_row = mysql_fetch_row($res);') + g.writeln('${x}.id = string_int(tos_clone(${res}_row[0]));') + g.writeln('mysql_free_result($res);') + + g.writeln('$bind[${i - 1}].buffer_type = MYSQL_TYPE_LONG;') + g.writeln('$bind[${i - 1}].buffer = &${x}.id;') + g.writeln('$bind[${i - 1}].is_null = 0;') + g.writeln('$bind[${i - 1}].length = 0;') + } else { + t, sym := g.mysql_buffer_typ_from_field(field) + g.writeln('$bind[${i - 1}].buffer_type = $t;') + if sym == 'char' { + g.writeln('$bind[${i - 1}].buffer = ($sym*) ${x}.str;') + } else { + g.writeln('$bind[${i - 1}].buffer = ($sym*) &$x;') + } + if sym == 'char' { + g.writeln('$bind[${i - 1}].buffer_length = ${x}.len;') + } + g.writeln('$bind[${i - 1}].is_null = 0;') + g.writeln('$bind[${i - 1}].length = 0;') + } + } + } + binds := g.sql_buf.str() + g.sql_buf = strings.new_builder(100) + g.writeln(binds) + // g.writeln('mysql_stmt_attr_set($g.sql_stmt_name, STMT_ATTR_ARRAY_SIZE, 1);') + res := g.new_tmp_var() + g.writeln('int $res = mysql_stmt_bind_param($g.sql_stmt_name, $bind);') + g.writeln('if ($res != 0) { puts(mysql_error(${db_name}.conn)); }') + g.writeln('$res = mysql_stmt_execute($g.sql_stmt_name);') + g.writeln('if ($res != 0) { puts(mysql_error(${db_name}.conn)); puts(mysql_stmt_error($g.sql_stmt_name)); }') + g.writeln('mysql_stmt_close($g.sql_stmt_name);') + g.writeln('mysql_stmt_free_result($g.sql_stmt_name);') +} + +fn (mut g Gen) mysql_select_expr(node ast.SqlExpr, sub bool, line string, typ SqlType) { + g.sql_i = 0 + mut cur_line := line + if !sub { + cur_line = g.go_before_stmt(0) + } + g.sql_stmt_name = g.new_tmp_var() + g.sql_bind_name = g.new_tmp_var() + db_name := g.new_tmp_var() + g.writeln('\n\t// sql select') + g.write('mysql__Connection $db_name = ') + g.expr(node.db_expr) + g.writeln(';') + + stmt_name := g.new_tmp_var() + g.sql_idents = []string{} + g.sql_idents_types = []ast.Type{} + g.write('char* ${stmt_name}_raw = "') + g.write(g.get_base_sql_select_query(node)) + g.sql_expr_defaults(node, typ) + g.writeln('";') + g.writeln('string $stmt_name = tos_clone(${stmt_name}_raw);') + if g.sql_idents.len > 0 { + vals := g.new_tmp_var() + g.writeln('Array_string $vals = __new_array_with_default(0, 0, sizeof(string), 0);') + for i, ident in g.sql_idents { + g.writeln('array_push(&$vals, _MOV((string[]){string_clone(_SLIT("%${i + 1}"))}));') + + g.write('array_push(&$vals, _MOV((string[]){string_clone(') + if g.sql_idents_types[i] == ast.string_type { + g.write('_SLIT(') + } else { + sym := g.table.get_type_name(g.sql_idents_types[i]) + g.write('${sym}_str(') + } + g.writeln('$ident))}));') + } + g.writeln('$stmt_name = string_replace_each($stmt_name, $vals);') + } + /* + g.writeln('MYSQL_STMT* $g.sql_stmt_name = mysql_stmt_init(${db_name}.conn);') + g.writeln('mysql_stmt_prepare($g.sql_stmt_name, ${stmt_name}.str, ${stmt_name}.len);') + + g.writeln('MYSQL_BIND $g.sql_bind_name[$g.sql_i];') + g.writeln('memset($g.sql_bind_name, 0, sizeof(MYSQL_BIND)*$g.sql_i);') + + binds := g.sql_buf.str() + g.sql_buf = strings.new_builder(100) + g.writeln(binds) + + res := g.new_tmp_var() + g.writeln('int $res = mysql_stmt_bind_param($g.sql_stmt_name, $g.sql_bind_name);') + g.writeln('if ($res != 0) { puts(mysql_error(${db_name}.conn)); }') + g.writeln('$res = mysql_stmt_execute($g.sql_stmt_name);') + g.writeln('if ($res != 0) { puts(mysql_error(${db_name}.conn)); puts(mysql_stmt_error($g.sql_stmt_name)); }') + */ + query := g.new_tmp_var() + res := g.new_tmp_var() + fields := g.new_tmp_var() + /* + g.writeln('Option_mysql__Result $res = mysql__Connection_real_query(&$db_name, $stmt_name);') + g.writeln('if (${res}.state != 0) { IError err = ${res}.err; _STR("Something went wrong\\000%.*s", 2, IError_str(err)); }') + g.writeln('Array_mysql__Row ${res}_rows = mysql__Result_rows(*(mysql__Result*)${res}.data);')*/ + g.writeln('int $query = mysql_real_query(${db_name}.conn, ${stmt_name}.str, ${stmt_name}.len);') + g.writeln('if ($query != 0) { puts(mysql_error(${db_name}.conn)); }') + g.writeln('MYSQL_RES* $res = mysql_store_result(${db_name}.conn);') + g.writeln('MYSQL_ROW $fields = mysql_fetch_row($res);') + if node.is_count { + g.writeln('$cur_line string_int(tos_clone($fields[0]));') + } else { + tmp := g.new_tmp_var() + styp := g.typ(node.typ) + tmp_i := g.new_tmp_var() + mut elem_type_str := '' + g.writeln('int $tmp_i = 0;') + if node.is_array { + array_sym := g.table.get_type_symbol(node.typ) + array_info := array_sym.info as ast.Array + elem_type_str = g.typ(array_info.elem_type) + g.writeln('$styp ${tmp}_array = __new_array(0, 10, sizeof($elem_type_str));') + g.writeln('for ($tmp_i = 0; $tmp_i < mysql_num_rows($res); $tmp_i++) {') + g.writeln('\t$elem_type_str $tmp = ($elem_type_str) {') + // + sym := g.table.get_type_symbol(array_info.elem_type) + info := sym.info as ast.Struct + for i, field in info.fields { + g.zero_struct_field(field) + if i != info.fields.len - 1 { + g.write(', ') + } + } + g.writeln('};') + } else { + g.writeln('$styp $tmp = ($styp){') + // Zero fields, (only the [skip] ones?) + // If we don't, string values are going to be nil etc for fields that are not returned + // by the db engine. + sym := g.table.get_type_symbol(node.typ) + info := sym.info as ast.Struct + for i, field in info.fields { + g.zero_struct_field(field) + if i != info.fields.len - 1 { + g.write(', ') + } + } + g.writeln('};') + } + + char_ptr := g.new_tmp_var() + g.writeln('char* $char_ptr = "";') + for i, field in node.fields { + g.writeln('$char_ptr = $fields[$i];') + g.writeln('if ($char_ptr == NULL) { $char_ptr = ""; }') + name := g.table.get_type_symbol(field.typ).cname + if g.table.get_type_symbol(field.typ).kind == .struct_ { + /* + id_name := g.new_tmp_var() + g.writeln('//parse struct start') // + //g.writeln('int $id_name = string_int(tos_clone($fields[$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 = '$char_ptr[$i]' + 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 + tmp_sql_table_name := g.sql_table_name + + 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 + g.sql_table_name := tmp_sql_table_name + */ + } else if field.typ == ast.string_type { + g.writeln('${tmp}.$field.name = tos_clone($char_ptr);') + } else if field.typ == ast.byte_type { + g.writeln('${tmp}.$field.name = (byte) string_${name}(tos_clone($char_ptr));') + } else if field.typ == ast.i8_type { + g.writeln('${tmp}.$field.name = (i8) string_${name}(tos_clone($char_ptr));') + } else { + g.writeln('${tmp}.$field.name = string_${name}(tos_clone($char_ptr));') + } + } + if node.is_array { + g.writeln('\t array_push(&${tmp}_array, _MOV(($elem_type_str[]) { $tmp }));') + g.writeln('}') + } + g.writeln('string_free(&$stmt_name);') + g.writeln('mysql_free_result($res);') + if node.is_array { + g.writeln('$cur_line ${tmp}_array; ') + } else { + g.writeln('$cur_line $tmp; ') + } + } +} + +fn (mut g Gen) mysql_create_table(node ast.SqlStmt, typ SqlType) { + g.writeln('// mysql table creator') + create_string := g.table_gen(node, typ) + tmp := g.new_tmp_var() + g.write('Option_mysql__Result $tmp = mysql__Connection_query(&') + g.expr(node.db_expr) + g.writeln(', _SLIT("$create_string"));') + g.writeln('if (${tmp}.state != 0) { IError err = ${tmp}.err; _STR("Something went wrong\\000%.*s", 2, IError_str(err)); }') +} + +fn (mut g Gen) mysql_bind(val string, _ ast.Type) { + /* + t := g.mysql_buffer_typ_from_typ(typ) + mut sym := g.table.get_type_symbol(typ).cname + if typ == ast.string_type { + sym = 'char *' + } + tmp := g.new_tmp_var() + g.sql_buf.writeln('$sym $tmp = $val;') + g.sql_buf.writeln('$g.sql_bind_name[${g.sql_i - 1}].buffer_type = $t;') + g.sql_buf.writeln('$g.sql_bind_name[${g.sql_i - 1}].buffer = ($sym*) &$tmp;') + if sym == 'char *' { + g.sql_buf.writeln('$g.sql_bind_name[${g.sql_i - 1}].buffer_length = ${val}.len;') + } + g.sql_buf.writeln('$g.sql_bind_name[${g.sql_i - 1}].is_null = 0;') + g.sql_buf.writeln('$g.sql_bind_name[${g.sql_i - 1}].length = 0;')*/ + g.write(val) +} + +fn (mut g Gen) mysql_get_table_type(typ ast.Type) string { + mut table_typ := '' + match typ { + ast.i8_type, ast.byte_type, ast.bool_type { + table_typ = 'TINYINT' + } + ast.i16_type, ast.u16_type { + table_typ = 'SMALLINT' + } + ast.int_type, ast.u32_type { + table_typ = 'INT' + } + ast.i64_type, ast.u64_type { + table_typ = 'BIGINT' + } + ast.f32_type { + table_typ = 'BIGINT' + } + ast.f64_type { + table_typ = 'BIGINT' + } + ast.string_type { + table_typ = 'TEXT' + } + -1 { + table_typ = 'SERIAL' + } + else {} + } + return table_typ +} + +fn (mut g Gen) mysql_buffer_typ_from_typ(typ ast.Type) string { + mut buf_typ := '' + match typ { + ast.i8_type, ast.byte_type, ast.bool_type { + buf_typ = 'MYSQL_TYPE_TINY' + } + ast.i16_type, ast.u16_type { + buf_typ = 'MYSQL_TYPE_SHORT' + } + ast.int_type, ast.u32_type { + buf_typ = 'MYSQL_TYPE_LONG' + } + ast.i64_type, ast.u64_type { + buf_typ = 'MYSQL_TYPE_LONGLONG' + } + ast.f32_type { + buf_typ = 'MYSQL_TYPE_FLOAT' + } + ast.f64_type { + buf_typ = 'MYSQL_TYPE_DOUBLE' + } + ast.string_type { + buf_typ = 'MYSQL_TYPE_STRING' + } + else { + buf_typ = 'MYSQL_TYPE_NULL' + } + } + return buf_typ +} + +fn (mut g Gen) mysql_buffer_typ_from_field(field ast.StructField) (string, string) { + mut typ := g.get_sql_field_type(field) + mut sym := g.table.get_type_symbol(typ).cname + buf_typ := g.mysql_buffer_typ_from_typ(typ) + + if typ == ast.string_type { + sym = 'char' + } + + return buf_typ, sym +} + +// utils + +fn (mut g Gen) sql_expr_defaults(node ast.SqlExpr, sql_typ SqlType) { + if node.has_where && node.where_expr is ast.InfixExpr { + g.expr_to_sql(node.where_expr, sql_typ) + } + if node.has_order { + g.write(' ORDER BY ') + g.sql_side = .left + g.expr_to_sql(node.order_expr, sql_typ) + if node.has_desc { + g.write(' DESC ') + } + } else { + g.write(' ORDER BY id ') + } + if node.has_limit { + g.write(' LIMIT ') + g.sql_side = .right + g.expr_to_sql(node.limit_expr, sql_typ) + } + if node.has_offset { + g.write(' OFFSET ') + g.sql_side = .right + g.expr_to_sql(node.offset_expr, sql_typ) + } +} + +fn (mut g Gen) get_base_sql_select_query(node ast.SqlExpr) string { + mut sql_query := 'SELECT ' + table_name := util.strip_mod_name(g.table.get_type_symbol(node.table_expr.typ).name) + if node.is_count { + // `select count(*) from User` + sql_query += 'COUNT(*) FROM `$table_name` ' + } else { + // `select id, name, country from User` + for i, field in node.fields { + sql_query += '`$field.name`' + if i < node.fields.len - 1 { + sql_query += ', ' + } + } + sql_query += ' FROM `$table_name`' + } + if node.has_where { + sql_query += ' WHERE ' + } + return sql_query +} + +fn (mut g Gen) sql_defaults(node ast.SqlStmt, typ SqlType) { + table_name := util.strip_mod_name(g.table.get_type_symbol(node.table_expr.typ).name) + if node.kind == .insert { + g.write('INSERT INTO `$table_name` (') + } else if node.kind == .update { + g.write('UPDATE `$table_name` SET ') + } else if node.kind == .delete { + g.write('DELETE FROM `$table_name` ') + } + if node.kind == .insert { + for i, field in node.fields { + if field.name == 'id' { + continue + } + g.write('`$field.name`') + if i < node.fields.len - 1 { + g.write(', ') + } + } + g.write(') values (') + for i, field in node.fields { + if field.name == 'id' { + continue + } + if typ == .sqlite3 { + g.write('?${i + 0}') + } else if typ == .mysql { + g.write('?') + } + if i < node.fields.len - 1 { + g.write(', ') + } + g.sql_i++ + } + g.write(')') + } else if node.kind == .update { + for i, col in node.updated_columns { + g.write(' $col = ') + g.expr_to_sql(node.update_exprs[i], typ) + if i < node.updated_columns.len - 1 { + g.write(', ') + } + } + g.write(' WHERE ') + } else if node.kind == .delete { + g.write(' WHERE ') + } + if node.kind == .update || node.kind == .delete { + g.expr_to_sql(node.where_expr, typ) + } + g.write('")') +} + +fn (mut g Gen) table_gen(node ast.SqlStmt, typ SqlType) string { typ_sym := g.table.get_type_symbol(node.table_expr.typ) if typ_sym.info !is ast.Struct { verror('Type `$typ_sym.name` has to be a struct') } - g.writeln('// sqlite3 table creator ($typ_sym.name)') struct_data := typ_sym.info as ast.Struct table_name := typ_sym.name.split('.').last() mut create_string := 'CREATE TABLE IF NOT EXISTS `$table_name` (' mut fields := []string{} + mut primary := '' // for mysql + for field in struct_data.fields { mut is_primary := false - mut skip := false + mut no_null := false for attr in field.attrs { match attr.name { - 'skip' { - skip = true - } 'primary' { is_primary = true + primary = field.name + } + 'nonull' { + no_null = true } else {} } } - if skip { // cpp workaround - continue - } mut stmt := '' - mut converted_typ := g.sql_type_from_v(typ, field.typ) + mut converted_typ := g.sql_type_from_v(typ, g.get_sql_field_type(field)) mut name := field.name if converted_typ == '' { if g.table.get_type_symbol(field.typ).kind == .struct_ { @@ -417,56 +833,26 @@ fn (mut g Gen) sqlite3_create_table(node ast.SqlStmt, typ SqlType) { } stmt = '`$name` $converted_typ' - if field.has_default_expr { + if field.has_default_expr && typ != .mysql { stmt += ' DEFAULT ' stmt += field.default_expr.str() } - if is_primary { + if no_null { + stmt += ' NOT NULL' + } + if is_primary && typ == .sqlite3 { stmt += ' PRIMARY KEY' } fields << stmt } + if typ == .mysql { + fields << 'PRIMARY KEY(`$primary`)' + } create_string += fields.join(', ') create_string += ');' - g.write('sqlite__DB_exec(') - g.expr(node.db_expr) - g.writeln(', _SLIT("$create_string"));') + return create_string } -fn (mut g Gen) sqlite3_bind_int(val string) { - g.sql_buf.writeln('sqlite3_bind_int($g.sql_stmt_name, $g.sql_i, $val);') -} - -fn (mut g Gen) sqlite3_bind_string(val string, len string) { - g.sql_buf.writeln('sqlite3_bind_text($g.sql_stmt_name, $g.sql_i, $val, $len, 0);') -} - -fn (mut g Gen) sqlite3_type_from_v(typ SqlType, v_typ ast.Type) string { - if v_typ.is_number() || v_typ == ast.bool_type { - return 'INTEGER' - } - if v_typ.is_string() { - return 'TEXT' - } - return '' -} - -// mysql - -fn (mut g Gen) mysql_stmt(node ast.SqlStmt) { -} - -fn (mut g Gen) mysql_select_expr(node ast.SqlExpr, sub bool, line string) { -} - -fn (mut g Gen) mysql_bind_int(val string) { -} - -fn (mut g Gen) mysql_bind_string(val string, len string) { -} - -// utils - fn (mut g Gen) expr_to_sql(expr ast.Expr, typ SqlType) { // 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() @@ -497,50 +883,55 @@ fn (mut g Gen) expr_to_sql(expr ast.Expr, typ SqlType) { } ast.StringLiteral { // g.write("'$it.val'") - g.inc_sql_i() - g.sql_bind_string('"$expr.val"', expr.val.len.str(), typ) + g.inc_sql_i(typ) + g.sql_bind('"$expr.val"', expr.val.len.str(), g.sql_get_real_type(ast.string_type), + typ) } ast.IntegerLiteral { - g.inc_sql_i() - g.sql_bind_int(expr.val, typ) + g.inc_sql_i(typ) + g.sql_bind(expr.val, '', g.sql_get_real_type(ast.int_type), typ) } ast.BoolLiteral { // true/false literals were added to Sqlite 3.23 (2018-04-02) // but lots of apps/distros use older sqlite (e.g. Ubuntu 18.04 LTS ) - g.inc_sql_i() + g.inc_sql_i(typ) eval := if expr.val { '1' } else { '0' } - g.sql_bind_int(eval, typ) + g.sql_bind(eval, '', g.sql_get_real_type(ast.byte_type), typ) } ast.Ident { // `name == user_name` => `name == ?1` // for left sides just add a string, for right sides, generate the bindings if g.sql_side == .left { // println("sql gen left $expr.name") + eprintln(expr.name) + g.sql_left_type = g.get_struct_field_typ(expr.name) g.write(expr.name) } else { - g.inc_sql_i() + g.inc_sql_i(typ) info := expr.info as ast.IdentVar ityp := info.typ - if ityp == ast.string_type { - g.sql_bind_string('${expr.name}.str', '${expr.name}.len', typ) - } else if ityp == ast.int_type { - g.sql_bind_int(expr.name, typ) + if typ == .sqlite3 { + if ityp == ast.string_type { + g.sql_bind('${expr.name}.str', '${expr.name}.len', g.sql_get_real_type(ityp), + typ) + } else { + g.sql_bind(expr.name, '', g.sql_get_real_type(ityp), typ) + } } else { - verror('bad sql type=$ityp ident_name=$expr.name') + g.sql_bind('%$g.sql_i.str()', '', g.sql_get_real_type(ityp), typ) + g.sql_idents << expr.name + g.sql_idents_types << g.sql_get_real_type(ityp) } } } ast.SelectorExpr { - g.inc_sql_i() - if expr.typ == ast.int_type { - if expr.expr !is ast.Ident { - verror('orm selector not ident') - } - ident := expr.expr as ast.Ident - g.sql_bind_int(ident.name + '.' + expr.field_name, typ) - } else { - verror('bad sql type=$expr.typ selector expr=$expr.field_name') + g.inc_sql_i(typ) + if expr.expr !is ast.Ident { + verror('orm selector not ident') } + ident := expr.expr as ast.Ident + g.sql_bind(ident.name + '.' + expr.field_name, '', g.sql_get_real_type(expr.typ), + typ) } else { g.expr(expr) @@ -554,9 +945,38 @@ fn (mut g Gen) expr_to_sql(expr ast.Expr, typ SqlType) { */ } -fn (mut g Gen) inc_sql_i() { +fn (mut g Gen) get_struct_field_typ(f string) ast.Type { + sym := g.table.get_type_symbol(g.table.type_idxs[g.sql_table_name]) + + mut typ := ast.Type(-1) + + if sym.kind != .struct_ { + str := sym.info as ast.Struct + for field in str.fields { + if field.name != f { + continue + } + typ = g.get_sql_field_type(field) + break + } + } + + return typ +} + +fn (mut g Gen) sql_get_real_type(typ ast.Type) ast.Type { + if typ != g.sql_left_type && g.sql_left_type >= 0 { + return g.sql_left_type + } + return typ +} + +fn (mut g Gen) inc_sql_i(typ SqlType) { g.sql_i++ - g.write('?$g.sql_i') + if typ == .sqlite3 { + g.write('?') + g.write('$g.sql_i') + } } fn (mut g Gen) parse_db_type(expr ast.Expr) SqlType { @@ -581,8 +1001,25 @@ fn (mut g Gen) parse_db_from_type_string(name string) SqlType { 'sqlite.DB' { return .sqlite3 } + 'mysql.Connection' { + return .mysql + } else { return .unknown } } } + +fn (mut g Gen) get_sql_field_type(field ast.StructField) ast.Type { + mut typ := field.typ + for attr in field.attrs { + if attr.name == 'sql' && attr.arg != '' { + if attr.arg.to_lower() == 'serial' { + typ = ast.Type(-1) + break + } + typ = g.table.type_idxs[attr.arg] + } + } + return typ +}