From 7c20003ef32da0d40f84be5e732cee05fd17b263 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 15 Apr 2026 03:06:38 +0300 Subject: [PATCH] orm: add upsert SQL method (fixes #23957) --- vlib/orm/README.md | 18 +++ vlib/orm/orm.v | 184 ++++++++++++++++++++----- vlib/orm/orm_upsert_test.v | 145 ++++++++++++++++++++ vlib/v/ast/ast.v | 1 + vlib/v/checker/orm.v | 13 +- vlib/v/fmt/fmt.v | 3 + vlib/v/gen/c/orm.v | 271 +++++++++++++++++++++++++++++++++++++ vlib/v/gen/golang/golang.v | 3 + vlib/v/parser/orm.v | 8 +- 9 files changed, 607 insertions(+), 39 deletions(-) create mode 100644 vlib/orm/orm_upsert_test.v diff --git a/vlib/orm/README.md b/vlib/orm/README.md index f206bcb31..79e2818c8 100644 --- a/vlib/orm/README.md +++ b/vlib/orm/README.md @@ -169,6 +169,24 @@ 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. +### Upsert + +`upsert` inserts a row or updates the matching row when one of the table's +primary or unique keys already exists. + +```v ignore +foo := Foo{ + name: 'abc' +} + +sql db { + upsert foo into Foo +}! +``` + +`upsert` currently supports flat ORM rows with primitive, enum, and `time.Time` +fields. + ### Select You can select rows from the database by passing the struct as the table, and diff --git a/vlib/orm/orm.v b/vlib/orm/orm.v index 965e4664e..ce8badd7c 100644 --- a/vlib/orm/orm.v +++ b/vlib/orm/orm.v @@ -637,43 +637,16 @@ pub fn orm_stmt_gen(sql_dialect SQLDialect, table Table, q string, kind StmtKind start_pos int, data QueryData, where QueryData) (string, QueryData) { mut str := '' mut c := start_pos - mut data_fields := []string{} - mut data_data := []Primitive{} + insert_data := prepare_insert_query_data(data) match kind { .insert { mut values := []string{} mut select_fields := []string{} - for i in 0 .. data.fields.len { - column_name := data.fields[i] - is_auto_field := i in data.auto_fields - - if data.data.len > 0 { - // skip fields and allow the database to insert default and - // serial (auto-increment) values where a default (or no) - // value was provided - if is_auto_field { - mut x := data.data[i] - skip_auto_field := match mut x { - Null { true } - 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 - } - } - - data_data << data.data[i] - } + for column_name in insert_data.fields { select_fields << '${q}${column_name}${q}' values << factory_insert_qm_value(num, qm, c) - data_fields << column_name c++ } @@ -744,14 +717,155 @@ pub fn orm_stmt_gen(sql_dialect SQLDialect, table Table, q string, kind StmtKind $if trace_orm ? { eprintln('> orm: ${str}') } + returned_data := if kind == .insert { insert_data } else { data } + + return str, returned_data +} + +fn prepare_insert_query_data(data QueryData) QueryData { + mut prepared := QueryData{ + types: data.types.clone() + kinds: data.kinds.clone() + auto_fields: data.auto_fields.clone() + is_and: data.is_and.clone() + } + for i, column_name in data.fields { + if i >= data.data.len { + prepared.fields << column_name + continue + } + if i in data.auto_fields && should_skip_insert_auto_field(data.data[i]) { + continue + } + prepared.fields << column_name + prepared.data << data.data[i] + } + return prepared +} + +fn should_skip_insert_auto_field(value Primitive) bool { + mut x := value + return match mut x { + Null { true } + 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 } + } +} - return str, QueryData{ - fields: data_fields - data: data_data - types: data.types - kinds: data.kinds - is_and: data.is_and +fn build_upsert_where(data QueryData, conflict_groups [][]string) !QueryData { + mut field_indexes := map[string]int{} + for i, field in data.fields { + field_indexes[field] = i + } + mut where := QueryData{} + for group in conflict_groups { + if group.len == 0 { + continue + } + start := where.fields.len + if start > 0 { + where.is_and << false + } + for i, field_name in group { + idx := field_indexes[field_name] or { + return error('${@FN}(): missing conflict field `${field_name}` in upsert data') + } + if idx >= data.data.len { + return error('${@FN}(): missing conflict value for `${field_name}` in upsert data') + } + where.fields << field_name + where.data << data.data[idx] + where.kinds << .eq + if i > 0 { + where.is_and << true + } + } + if group.len > 1 { + where.parentheses << [start, where.fields.len - 1] + } } + return where +} + +fn upsert_conflict_groups(data QueryData, conflict_groups [][]string) [][]string { + mut present_fields := map[string]bool{} + for field in data.fields { + present_fields[field] = true + } + mut usable := [][]string{} + for group in conflict_groups { + if group.len == 0 { + continue + } + mut ok := true + for field_name in group { + if field_name !in present_fields { + ok = false + break + } + } + if ok { + usable << group + } + } + return usable +} + +pub struct UpsertData { +pub: + valid bool +pub mut: + insert_data QueryData + where QueryData +} + +// prepare_upsert resolves the filtered insert data and the conflict `WHERE` clause for an upsert. +pub fn prepare_upsert(data QueryData, conflict_groups [][]string) UpsertData { + insert_data := prepare_insert_query_data(data) + usable_groups := upsert_conflict_groups(insert_data, conflict_groups) + if usable_groups.len == 0 { + return UpsertData{ + insert_data: insert_data + } + } + where := build_upsert_where(insert_data, usable_groups) or { + return UpsertData{ + insert_data: insert_data + } + } + return UpsertData{ + valid: true + insert_data: insert_data + where: where + } +} + +// upsert_count converts a `select count(*)` ORM result into an integer count. +pub fn upsert_count(result [][]Primitive) int { + if result.len == 0 || result[0].len == 0 { + return 0 + } + count_val := result[0][0] + return match count_val { + int { count_val } + i64 { int(count_val) } + u64 { int(count_val) } + else { 0 } + } +} + +// upsert_missing_conflict_error returns the standard missing-conflict error for SQL upserts. +pub fn upsert_missing_conflict_error(table Table) ! { + return error('upsert(): table `${table.name}` needs at least one primary or unique field with a concrete value') +} + +// upsert_ambiguous_error returns the standard ambiguous-match error for SQL upserts. +pub fn upsert_ambiguous_error(table Table) ! { + return error('upsert(): upsert on table `${table.name}` matched multiple rows') } // Generates an sql select stmt, from universal parameter diff --git a/vlib/orm/orm_upsert_test.v b/vlib/orm/orm_upsert_test.v new file mode 100644 index 000000000..4f27130d0 --- /dev/null +++ b/vlib/orm/orm_upsert_test.v @@ -0,0 +1,145 @@ +// vtest retry: 3 +// vtest build: present_sqlite3? +import db.sqlite + +@[table: 'upsert_users'] +struct UpsertUser { +mut: + id int @[primary; sql: serial] + username string @[unique] + age int +} + +@[table: 'upsert_configs'] +@[unique_key: 'scope, key'] +struct UpsertConfig { +mut: + id int @[primary; sql: serial] + scope string + key string + value string +} + +@[table: 'ambiguous_upsert_users'] +struct AmbiguousUpsertUser { +mut: + id int @[primary] + username string @[unique] + note string +} + +fn test_upsert_updates_existing_row_using_unique_field() { + mut db := sqlite.connect(':memory:') or { panic(err) } + defer { + db.close() or {} + } + + sql db { + create table UpsertUser + }! + + first := UpsertUser{ + username: 'alice' + age: 30 + } + second := UpsertUser{ + username: 'alice' + age: 31 + } + + sql db { + upsert first into UpsertUser + upsert second into UpsertUser + }! + + rows := sql db { + select from UpsertUser where username == 'alice' + }! + + assert rows.len == 1 + assert rows[0].username == 'alice' + assert rows[0].age == 31 + assert rows[0].id == 1 +} + +fn test_upsert_updates_existing_row_using_composite_unique_key() { + mut db := sqlite.connect(':memory:') or { panic(err) } + defer { + db.close() or {} + } + + sql db { + create table UpsertConfig + }! + + first := UpsertConfig{ + scope: 'web' + key: 'theme' + value: 'light' + } + second := UpsertConfig{ + scope: 'web' + key: 'theme' + value: 'dark' + } + + sql db { + upsert first into UpsertConfig + upsert second into UpsertConfig + }! + + rows := sql db { + select from UpsertConfig where scope == 'web' && key == 'theme' + }! + + assert rows.len == 1 + assert rows[0].value == 'dark' + assert rows[0].id == 1 +} + +fn test_upsert_errors_when_conflict_groups_match_multiple_rows() { + mut db := sqlite.connect(':memory:') or { panic(err) } + defer { + db.close() or {} + } + + sql db { + create table AmbiguousUpsertUser + }! + + first := AmbiguousUpsertUser{ + id: 1 + username: 'alice' + note: 'first' + } + second := AmbiguousUpsertUser{ + id: 2 + username: 'bob' + note: 'second' + } + + sql db { + insert first into AmbiguousUpsertUser + insert second into AmbiguousUpsertUser + }! + + ambiguous := AmbiguousUpsertUser{ + id: 1 + username: 'bob' + note: 'updated' + } + + mut got_error := false + sql db { + upsert ambiguous into AmbiguousUpsertUser + } or { got_error = true } + + assert got_error + + rows := sql db { + select from AmbiguousUpsertUser order by id + }! + assert rows.len == 2 + assert rows[0].note == 'first' + assert rows[1].note == 'second' +} diff --git a/vlib/v/ast/ast.v b/vlib/v/ast/ast.v index cdbfce5da..ff80cffd8 100644 --- a/vlib/v/ast/ast.v +++ b/vlib/v/ast/ast.v @@ -2287,6 +2287,7 @@ pub: pub enum SqlStmtKind { insert + upsert update delete create diff --git a/vlib/v/checker/orm.v b/vlib/v/checker/orm.v index 79989f050..b1aae4cb0 100644 --- a/vlib/v/checker/orm.v +++ b/vlib/v/checker/orm.v @@ -527,7 +527,7 @@ fn (mut c Checker) sql_stmt_line(mut node ast.SqlStmtLine) ast.Type { inserting_object_name := node.object_var - if node.kind == .insert && !node.is_generated { + if node.kind in [.insert, .upsert] && !node.is_generated { inserting_object := node.scope.find(inserting_object_name) or { c.error('undefined ident: `${inserting_object_name}`', node.pos) return ast.void_type @@ -618,6 +618,17 @@ fn (mut c Checker) sql_stmt_line(mut node ast.SqlStmtLine) ast.Type { node.fields = fields node.sub_structs = sub_structs.move() + if node.kind == .upsert { + for field in non_primitive_fields { + field_typ, field_sym := c.get_non_array_type(field.typ) + if field_sym.kind == .struct && c.table.sym(field_typ).name == 'time.Time' { + continue + } + c.orm_error('upsert currently supports only primitive, enum, and time.Time fields', + field.pos) + } + } + for i, column in node.updated_columns { updated_fields := node.fields.filter(it.name == column) diff --git a/vlib/v/fmt/fmt.v b/vlib/v/fmt/fmt.v index 5e9c6fc9d..2cedca243 100644 --- a/vlib/v/fmt/fmt.v +++ b/vlib/v/fmt/fmt.v @@ -1676,6 +1676,9 @@ pub fn (mut f Fmt) sql_stmt_line(node ast.SqlStmtLine) { .insert { f.writeln('insert ${node.object_var} into ${table_name}') } + .upsert { + f.writeln('upsert ${node.object_var} into ${table_name}') + } .update { if node.is_dynamic { f.write('dynamic update ${table_name} set ') diff --git a/vlib/v/gen/c/orm.v b/vlib/v/gen/c/orm.v index 598207cb4..d1e243c24 100644 --- a/vlib/v/gen/c/orm.v +++ b/vlib/v/gen/c/orm.v @@ -347,6 +347,8 @@ fn (mut g Gen) sql_stmt_line(stmt_line ast.SqlStmtLine, connection_var_name stri } else if node.kind == .insert { g.write_orm_insert(node, table_name, connection_var_name, result_var_name, or_expr, table_attrs) + } else if node.kind == .upsert { + g.write_orm_upsert(node, table_name, connection_var_name, result_var_name, table_attrs) } else if node.kind == .update { g.write_orm_update(node, table_name, connection_var_name, result_var_name, table_attrs) } else if node.kind == .delete { @@ -524,6 +526,192 @@ fn (mut g Gen) write_orm_insert(node &ast.SqlStmtLine, table_name string, connec result_var_name, '', '', or_expr) } +fn (mut g Gen) write_orm_upsert(node &ast.SqlStmtLine, table_name string, connection_var_name string, result_var_name string, + table_attrs []ast.Attr) { + g.writeln('// sql { upsert into `${table_name}` }') + fields := node.fields + auto_fields := get_auto_field_idxs(fields) + mut inserting_object_type := ast.void_type + 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 = '->' + } + inserting_object_type = inserting_object.typ + } + inserting_object_sym := g.table.sym(inserting_object_type) + data_var_name := g.new_tmp_var() + g.writeln('orm__QueryData ${data_var_name} = (orm__QueryData){') + g.indent++ + if fields.len > 0 { + g.writeln('.fields = builtin__new_array_from_c_array(${fields.len}, ${fields.len}, sizeof(string),') + g.indent++ + g.writeln('_MOV((string[${fields.len}]){') + g.indent++ + for field in fields { + g.writeln('_S("${g.get_orm_column_name_from_struct_field(field)}"),') + } + g.indent-- + g.writeln('})') + g.indent-- + g.writeln('),') + g.writeln('.data = builtin__new_array_from_c_array(${fields.len}, ${fields.len}, sizeof(orm__Primitive),') + g.indent++ + g.writeln('_MOV((orm__Primitive[${fields.len}]){') + g.indent++ + for field in fields { + final_field_typ := g.table.final_type(field.typ) + sym := g.table.sym(final_field_typ) + mut typ := g.orm_primitive_field_name(field.typ) + mut ctyp := sym.cname + typ = vint2int(typ) + var := '${node.object_var}${member_access_type}${orm_field_access_name(field.name)}' + if final_field_typ.has_flag(.option) { + g.writeln('${var}.state == 2 ? _const_orm__null_primitive : orm__${typ}_to_primitive(*(${ctyp}*)(${var}.data)),') + } else if inserting_object_sym.kind == .sum_type { + table_sym := g.table.sym(node.table_expr.typ) + sum_type_var := '(*${node.object_var}._${table_sym.cname})${member_access_type}${orm_field_access_name(field.name)}' + g.writeln('orm__${typ}_to_primitive(${sum_type_var}),') + } else { + g.writeln('orm__${typ}_to_primitive(${var}),') + } + } + g.indent-- + g.writeln('})') + g.indent-- + g.writeln('),') + } else { + g.writeln('.fields = builtin____new_array_with_default_noscan(0, 0, sizeof(string), 0),') + g.writeln('.data = builtin____new_array_with_default_noscan(0, 0, sizeof(orm__Primitive), 0),') + } + g.writeln('.types = builtin____new_array_with_default_noscan(0, 0, sizeof(${ast.int_type_name}), 0),') + if auto_fields.len > 0 { + g.writeln('.auto_fields = builtin__new_array_from_c_array(${auto_fields.len}, ${auto_fields.len}, sizeof(${ast.int_type_name}),') + g.indent++ + g.write('_MOV((${ast.int_type_name}[${auto_fields.len}]){') + for i in auto_fields { + g.write(' ${i},') + } + g.writeln(' })),') + g.indent-- + } else { + g.writeln('.auto_fields = builtin____new_array_with_default_noscan(0, 0, sizeof(${ast.int_type_name}), 0),') + } + g.writeln('.kinds = builtin____new_array_with_default_noscan(0, 0, sizeof(orm__OperationKind), 0),') + g.writeln('.is_and = builtin____new_array_with_default_noscan(0, 0, sizeof(bool), 0),') + g.writeln('.parentheses = builtin____new_array_with_default_noscan(0, 0, sizeof(Array_${ast.int_type_name}), 0),') + g.indent-- + g.writeln('};') + conflict_groups := g.get_orm_upsert_conflict_groups(fields, table_attrs) + conflict_groups_var_name := g.new_tmp_var() + g.write('Array_Array_string ${conflict_groups_var_name} = ') + g.write_orm_upsert_conflict_groups(conflict_groups) + g.writeln(';') + prepared_var_name := g.new_tmp_var() + g.writeln('orm__UpsertData ${prepared_var_name} = orm__prepare_upsert(${data_var_name}, ${conflict_groups_var_name});') + g.writeln('${result_name}_void ${result_var_name};') + g.writeln('if (!${prepared_var_name}.valid) {') + g.indent++ + g.write('${result_var_name} = orm__upsert_missing_conflict_error(') + g.write_orm_table_struct(node.table_expr.typ) + g.writeln(');') + g.indent-- + g.writeln('} else {') + g.indent++ + select_result_var_name := g.new_tmp_var() + g.writeln('${result_name}_Array_Array_orm__Primitive ${select_result_var_name} = orm__Connection_name_table[${connection_var_name}._typ]._method_select(') + g.indent++ + g.writeln('${connection_var_name}._object, // Connection object') + g.writeln('(orm__SelectConfig){') + g.indent++ + g.write('.table = ') + g.write_orm_table_struct(node.table_expr.typ) + g.writeln(',') + g.writeln('.aggregate_kind = orm__AggregateKind__count,') + g.writeln('.aggregate_field = _S(""),') + g.writeln('.has_where = true,') + g.writeln('.has_order = false,') + g.writeln('.order = _S(""),') + g.writeln('.order_type = orm__OrderType__asc,') + g.writeln('.has_limit = false,') + g.writeln('.primary = _S("id"),') + g.writeln('.has_offset = false,') + g.writeln('.has_distinct = false,') + g.writeln('.fields = builtin____new_array_with_default_noscan(0, 0, sizeof(string), 0),') + g.writeln('.select_exprs = builtin____new_array_with_default_noscan(0, 0, sizeof(string), 0),') + g.writeln('.types = builtin__new_array_from_c_array(1, 1, sizeof(${ast.int_type_name}), _MOV((${ast.int_type_name}[1]){ ${ast.int_type.idx()}, })),') + g.writeln('.joins = builtin____new_array_with_default_noscan(0, 0, sizeof(orm__JoinConfig), 0),') + g.indent-- + g.writeln('},') + g.writeln('(orm__QueryData){') + g.indent++ + g.writeln('.fields = builtin____new_array_with_default_noscan(0, 0, sizeof(string), 0),') + g.writeln('.data = builtin____new_array_with_default_noscan(0, 0, sizeof(orm__Primitive), 0),') + g.writeln('.types = builtin____new_array_with_default_noscan(0, 0, sizeof(${ast.int_type_name}), 0),') + g.writeln('.auto_fields = builtin____new_array_with_default_noscan(0, 0, sizeof(${ast.int_type_name}), 0),') + g.writeln('.kinds = builtin____new_array_with_default_noscan(0, 0, sizeof(orm__OperationKind), 0),') + g.writeln('.is_and = builtin____new_array_with_default_noscan(0, 0, sizeof(bool), 0),') + g.writeln('.parentheses = builtin____new_array_with_default_noscan(0, 0, sizeof(Array_${ast.int_type_name}), 0),') + g.indent-- + g.writeln('},') + g.writeln('${prepared_var_name}.where') + g.indent-- + g.writeln(');') + g.writeln('${result_var_name}.is_error = ${select_result_var_name}.is_error;') + g.writeln('${result_var_name}.err = ${select_result_var_name}.err;') + g.writeln('if (!${result_var_name}.is_error) {') + g.indent++ + count_rows_var_name := g.new_tmp_var() + count_var_name := g.new_tmp_var() + g.writeln('Array_Array_orm__Primitive ${count_rows_var_name} = (*(Array_Array_orm__Primitive*)${select_result_var_name}.data);') + g.writeln('${ast.int_type_name} ${count_var_name} = orm__upsert_count(${count_rows_var_name});') + g.writeln('if (${count_var_name} == 0) {') + g.indent++ + g.writeln('${result_var_name} = orm__Connection_name_table[${connection_var_name}._typ]._method_insert(') + g.indent++ + g.writeln('${connection_var_name}._object, // Connection object') + g.write_orm_table_struct(node.table_expr.typ) + g.writeln(',') + g.writeln('${data_var_name}') + g.indent-- + g.writeln(');') + g.indent-- + g.writeln('} else if (${count_var_name} == 1) {') + g.indent++ + g.writeln('if (${prepared_var_name}.insert_data.fields.len == 0) {') + g.indent++ + g.writeln('${result_var_name} = (${result_name}_void){0};') + g.indent-- + g.writeln('} else {') + g.indent++ + g.writeln('${result_var_name} = orm__Connection_name_table[${connection_var_name}._typ]._method_update(') + g.indent++ + g.writeln('${connection_var_name}._object, // Connection object') + g.write_orm_table_struct(node.table_expr.typ) + g.writeln(',') + g.writeln('${prepared_var_name}.insert_data,') + g.writeln('${prepared_var_name}.where') + g.indent-- + g.writeln(');') + g.indent-- + g.writeln('}') + g.indent-- + g.writeln('} else {') + g.indent++ + g.write('${result_var_name} = orm__upsert_ambiguous_error(') + g.write_orm_table_struct(node.table_expr.typ) + g.writeln(');') + g.indent-- + g.writeln('}') + g.indent-- + g.writeln('}') + g.indent-- + g.writeln('}') +} + // write_orm_update writes C code that calls ORM functions for updating rows. fn (mut g Gen) write_orm_update(node &ast.SqlStmtLine, table_name string, connection_var_name string, result_var_name string, _ []ast.Attr) { mut dynamic_update_data_var := '' @@ -1800,6 +1988,89 @@ fn (_ &Gen) get_orm_struct_primary_field(fields []ast.StructField) ?ast.StructFi return none } +fn (g &Gen) get_orm_upsert_conflict_groups(fields []ast.StructField, table_attrs []ast.Attr) [][]string { + mut groups := [][]string{} + mut named_unique_groups := map[string][]string{} + mut named_unique_group_order := []string{} + mut seen := map[string]bool{} + for field in fields { + column_name := g.get_orm_column_name_from_struct_field(field) + for attr in field.attrs { + match attr.name { + 'primary' { + key := column_name + if key !in seen { + groups << [column_name] + seen[key] = true + } + } + 'unique' { + if attr.arg != '' && attr.kind == .string { + if attr.arg !in named_unique_groups { + named_unique_groups[attr.arg] = []string{} + named_unique_group_order << attr.arg + } + named_unique_groups[attr.arg] << column_name + } else { + key := column_name + if key !in seen { + groups << [column_name] + seen[key] = true + } + } + } + else {} + } + } + } + for group_name in named_unique_group_order { + group := named_unique_groups[group_name] + key := group.join(',') + if key !in seen { + groups << group + seen[key] = true + } + } + for attr in table_attrs { + if attr.name != 'unique_key' || attr.arg == '' || attr.kind != .string { + continue + } + mut group := []string{} + for raw_field_name in attr.arg.split(',') { + field_name := raw_field_name.trim_space() + if field_name != '' { + group << field_name + } + } + key := group.join(',') + if group.len > 0 && key !in seen { + groups << group + seen[key] = true + } + } + return groups +} + +fn (mut g Gen) write_orm_upsert_conflict_groups(groups [][]string) { + if groups.len == 0 { + g.write('builtin____new_array_with_default_noscan(0, 0, sizeof(Array_string), 0)') + return + } + g.write('builtin__new_array_from_c_array(${groups.len}, ${groups.len}, sizeof(Array_string), _MOV((Array_string[${groups.len}]){') + for group in groups { + if group.len == 0 { + g.write('builtin____new_array_with_default_noscan(0, 0, sizeof(string), 0),') + continue + } + g.write('builtin__new_array_from_c_array(${group.len}, ${group.len}, sizeof(string), _MOV((string[${group.len}]){') + for field_name in group { + g.write('_S("${field_name}"),') + } + g.write('})),') + } + g.write('}))') +} + // return indexes of any auto-increment fields or fields with default values fn get_auto_field_idxs(fields []ast.StructField) []int { mut ret := []int{} diff --git a/vlib/v/gen/golang/golang.v b/vlib/v/gen/golang/golang.v index fb5887a17..e05cc8a2a 100644 --- a/vlib/v/gen/golang/golang.v +++ b/vlib/v/gen/golang/golang.v @@ -1198,6 +1198,9 @@ pub fn (mut f Gen) sql_stmt_line(node ast.SqlStmtLine) { .insert { f.writeln('insert ${node.object_var} into ${table_name}') } + .upsert { + f.writeln('upsert ${node.object_var} into ${table_name}') + } .update { f.write('update ${table_name} set ') for i, col in node.updated_columns { diff --git a/vlib/v/parser/orm.v b/vlib/v/parser/orm.v index ca1462d0f..b3dff46e9 100644 --- a/vlib/v/parser/orm.v +++ b/vlib/v/parser/orm.v @@ -368,6 +368,8 @@ fn (mut p Parser) parse_sql_stmt_line() ast.SqlStmtLine { mut kind := ast.SqlStmtKind.insert if n == 'delete' { kind = .delete + } else if n == 'upsert' { + kind = .upsert } else if n == 'update' { kind = .update } else if n == 'create' { @@ -426,7 +428,7 @@ fn (mut p Parser) parse_sql_stmt_line() ast.SqlStmtLine { if kind != .delete { if kind == .update { table_type = p.parse_type() - } else if kind == .insert { + } else if kind in [.insert, .upsert] { expr := p.expr(0) if expr is ast.Ident { inserted_var = expr.name @@ -440,7 +442,7 @@ fn (mut p Parser) parse_sql_stmt_line() ast.SqlStmtLine { mut updated_columns := []string{} mut update_exprs := []ast.Expr{cap: 5} mut update_data_expr := ast.empty_expr - if kind == .insert && n != 'into' { + if kind in [.insert, .upsert] && n != 'into' { p.error('expecting `into`') return ast.SqlStmtLine{} } else if kind == .update { @@ -470,7 +472,7 @@ fn (mut p Parser) parse_sql_stmt_line() ast.SqlStmtLine { mut table_pos := p.tok.pos() mut where_expr := ast.empty_expr - if kind == .insert { + if kind in [.insert, .upsert] { table_pos = p.tok.pos() table_type = p.parse_type() } else if kind == .update { -- 2.39.5