1
0
mirror of https://github.com/vlang/v.git synced 2023-08-10 21:13:21 +03:00

orm: allow structs without the id field, more flexible primary keys (#18140)

This commit is contained in:
Mark aka walkingdevel 2023-05-08 21:21:42 +00:00 committed by GitHub
parent 72b2f22057
commit 6ac09e605e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 199 additions and 40 deletions

View File

@ -116,12 +116,13 @@ fn (kind OrderType) to_str() string {
// parentheses defines which fields will be inside () // parentheses defines which fields will be inside ()
pub struct QueryData { pub struct QueryData {
pub: pub:
fields []string fields []string
data []Primitive data []Primitive
types []int types []int
parentheses [][]int parentheses [][]int
kinds []OperationKind kinds []OperationKind
is_and []bool primary_column_name string
is_and []bool
} }
pub struct InfixType { pub struct InfixType {
@ -202,7 +203,18 @@ pub fn orm_stmt_gen(sql_dialect SQLDialect, table string, q string, kind StmtKin
mut select_fields := []string{} mut select_fields := []string{}
for i in 0 .. data.fields.len { for i in 0 .. data.fields.len {
column_name := data.fields[i]
is_primary_column := column_name == data.primary_column_name
if data.data.len > 0 { if data.data.len > 0 {
// Allow the database to insert an automatically generated primary key
// under the hood if it is not passed by the user.
if is_primary_column && data.data[i].type_idx() in orm.nums {
if (data.data[i] as int) == 0 {
continue
}
}
match data.data[i].type_name() { match data.data[i].type_name() {
'string' { 'string' {
if (data.data[i] as string).len == 0 { if (data.data[i] as string).len == 0 {
@ -218,9 +230,9 @@ pub fn orm_stmt_gen(sql_dialect SQLDialect, table string, q string, kind StmtKin
} }
data_data << data.data[i] data_data << data.data[i]
} }
select_fields << '${q}${data.fields[i]}${q}' select_fields << '${q}${column_name}${q}'
values << factory_insert_qm_value(num, qm, c) values << factory_insert_qm_value(num, qm, c)
data_fields << data.fields[i] data_fields << column_name
c++ c++
} }
@ -313,6 +325,7 @@ pub fn orm_stmt_gen(sql_dialect SQLDialect, table string, q string, kind StmtKin
$if trace_orm ? { $if trace_orm ? {
eprintln('> orm: ${str}') eprintln('> orm: ${str}')
} }
return str, QueryData{ return str, QueryData{
fields: data_fields fields: data_fields
data: data_data data: data_data
@ -519,9 +532,7 @@ pub fn orm_table_gen(table string, q string, defaults bool, def_unique_len int,
} }
fs << stmt fs << stmt
} }
if primary == '' {
return error('A primary key is required for ${table}')
}
if unique.len > 0 { if unique.len > 0 {
for k, v in unique { for k, v in unique {
mut tmp := []string{} mut tmp := []string{}
@ -531,7 +542,11 @@ pub fn orm_table_gen(table string, q string, defaults bool, def_unique_len int,
fs << '/* ${k} */UNIQUE(${tmp.join(', ')})' fs << '/* ${k} */UNIQUE(${tmp.join(', ')})'
} }
} }
fs << 'PRIMARY KEY(${q}${primary}${q})'
if primary != '' {
fs << 'PRIMARY KEY(${q}${primary}${q})'
}
fs << unique_fields fs << unique_fields
str += fs.join(', ') str += fs.join(', ')
str += ');' str += ');'
@ -541,6 +556,7 @@ pub fn orm_table_gen(table string, q string, defaults bool, def_unique_len int,
$if trace_orm ? { $if trace_orm ? {
eprintln('> orm: ${str}') eprintln('> orm: ${str}')
} }
return str return str
} }

View File

@ -18,8 +18,53 @@ mut:
owner_id int owner_id int
} }
struct Entity {
name string [primary]
description string
}
fn test_create_without_id_field() {
db := sqlite.connect(':memory:')!
sql db {
create table Entity
}!
first := Entity{
name: 'First'
description: 'Such wow! No `id` field'
}
second := Entity{
name: 'Second'
description: 'Such wow! No `id` field again'
}
sql db {
insert first into Entity
insert second into Entity
}!
entities := sql db {
select from Entity
}!
assert entities.len == 2
first_entity := sql db {
select from Entity where name == 'First'
}!
assert first_entity.first().name == 'First'
second_entity := sql db {
select from Entity where name == 'Second'
}!
assert second_entity.first().name == 'Second'
}
fn test_create_only_one_table() { fn test_create_only_one_table() {
mut db := sqlite.connect(':memory:') or { panic(err) } mut db := sqlite.connect(':memory:')!
sql db { sql db {
create table Parent create table Parent
@ -47,7 +92,7 @@ fn test_create_only_one_table() {
} }
fn test_drop_only_one_table() { fn test_drop_only_one_table() {
mut db := sqlite.connect(':memory:') or { panic(err) } mut db := sqlite.connect(':memory:')!
sql db { sql db {
create table Parent create table Parent

View File

@ -1,4 +1,5 @@
import db.sqlite import db.sqlite
import rand
struct Parent { struct Parent {
id int [primary; sql: serial] id int [primary; sql: serial]
@ -37,12 +38,106 @@ pub mut:
username string [unique] username string [unique]
} }
struct Entity {
uuid string [primary]
description string
}
struct EntityWithFloatPrimary {
id f64 [primary]
name string
}
pub fn insert_parent(db sqlite.DB, mut parent Parent) ! { pub fn insert_parent(db sqlite.DB, mut parent Parent) ! {
sql db { sql db {
insert parent into Parent insert parent into Parent
}! }!
} }
fn test_set_primary_value() {
// The primary key is an constraint that ensures each record in a table is unique.
// Primary keys must contain unique values and cannot contain `NULL` values.
// However, this statement does not imply that a value cannot be inserted by the user.
// Therefore, let's allow this.
db := sqlite.connect(':memory:')!
sql db {
create table Child
}!
child := Child{
id: 10
parent_id: 20
}
sql db {
insert child into Child
}!
children := sql db {
select from Child
}!
assert children.first() == child
}
fn test_uuid_primary_key() {
db := sqlite.connect(':memory:')!
uuid := rand.uuid_v4()
sql db {
create table Entity
}!
entity := Entity{
uuid: uuid
description: 'Test'
}
sql db {
insert entity into Entity
}!
entities := sql db {
select from Entity where uuid == uuid
}!
mut is_duplicate_inserted := true
sql db {
insert entity into Entity
} or { is_duplicate_inserted = false }
assert entities.len == 1
assert entities.first() == entity
assert is_duplicate_inserted == false
}
fn test_float_primary_key() {
db := sqlite.connect(':memory:')!
id := 3.14
sql db {
create table EntityWithFloatPrimary
}!
entity := EntityWithFloatPrimary{
id: id
name: 'Test'
}
sql db {
insert entity into EntityWithFloatPrimary
}!
entities := sql db {
select from EntityWithFloatPrimary where id == id
}!
assert entities.len == 1
assert entities.first() == entity
}
fn test_does_not_insert_uninitialized_field() { fn test_does_not_insert_uninitialized_field() {
db := sqlite.connect(':memory:')! db := sqlite.connect(':memory:')!

View File

@ -212,6 +212,7 @@ fn (mut c Checker) sql_stmt_line(mut node ast.SqlStmtLine) ast.Type {
info := table_sym.info as ast.Struct info := table_sym.info as ast.Struct
mut fields := c.fetch_and_verify_orm_fields(info, node.table_expr.pos, table_sym.name) mut fields := c.fetch_and_verify_orm_fields(info, node.table_expr.pos, table_sym.name)
mut sub_structs := map[int]ast.SqlStmtLine{} mut sub_structs := map[int]ast.SqlStmtLine{}
for f in fields.filter((c.table.type_symbols[int(it.typ)].kind == .struct_ for f in fields.filter((c.table.type_symbols[int(it.typ)].kind == .struct_
|| (c.table.sym(it.typ).kind == .array || (c.table.sym(it.typ).kind == .array
&& c.table.sym(c.table.sym(it.typ).array_info().elem_type).kind == .struct_)) && c.table.sym(c.table.sym(it.typ).array_info().elem_type).kind == .struct_))
@ -332,9 +333,7 @@ fn (mut c Checker) fetch_and_verify_orm_fields(info ast.Struct, pos token.Pos, t
c.orm_error('select: empty fields in `${table_name}`', pos) c.orm_error('select: empty fields in `${table_name}`', pos)
return []ast.StructField{} return []ast.StructField{}
} }
if fields[0].name != 'id' {
c.orm_error('`id int` must be the first field in `${table_name}`', pos)
}
return fields return fields
} }
@ -537,6 +536,20 @@ fn (mut c Checker) check_db_expr(db_expr &ast.Expr) bool {
return true return true
} }
// walkingdevel: Now I don't think it's a good solution
// because it only checks structure initialization,
// but structure fields may be updated later before inserting.
// For example,
// ```v
// mut package := Package{
// name: 'xml'
// }
//
// package.author = User{
// username: 'walkingdevel'
// }
// ```
// TODO: rewrite it, move to runtime.
fn (_ &Checker) check_field_of_inserting_struct_is_uninitialized(node &ast.SqlStmtLine, field_name string) bool { fn (_ &Checker) check_field_of_inserting_struct_is_uninitialized(node &ast.SqlStmtLine, field_name string) bool {
struct_scope := node.scope.find_var(node.object_var_name) or { return false } struct_scope := node.scope.find_var(node.object_var_name) or { return false }

View File

@ -302,6 +302,7 @@ fn (mut g Gen) write_orm_insert_with_last_ids(node ast.SqlStmtLine, connection_v
} }
fields := node.fields.filter(g.table.sym(it.typ).kind != .array) fields := node.fields.filter(g.table.sym(it.typ).kind != .array)
primary_field_name := g.get_orm_struct_primary_field_name(fields) or { '' }
for sub in subs { for sub in subs {
g.sql_stmt_line(sub, connection_var_name, or_expr) g.sql_stmt_line(sub, connection_var_name, or_expr)
@ -373,6 +374,7 @@ fn (mut g Gen) write_orm_insert_with_last_ids(node ast.SqlStmtLine, connection_v
g.indent-- g.indent--
g.writeln('),') g.writeln('),')
g.writeln('.types = __new_array_with_default_noscan(0, 0, sizeof(int), 0),') g.writeln('.types = __new_array_with_default_noscan(0, 0, sizeof(int), 0),')
g.writeln('.primary_column_name = _SLIT("${primary_field_name}"),')
g.writeln('.kinds = __new_array_with_default_noscan(0, 0, sizeof(orm__OperationKind), 0),') g.writeln('.kinds = __new_array_with_default_noscan(0, 0, sizeof(orm__OperationKind), 0),')
g.writeln('.is_and = __new_array_with_default_noscan(0, 0, sizeof(bool), 0),') g.writeln('.is_and = __new_array_with_default_noscan(0, 0, sizeof(bool), 0),')
g.indent-- g.indent--
@ -396,11 +398,7 @@ fn (mut g Gen) write_orm_insert_with_last_ids(node ast.SqlStmtLine, connection_v
mut fff := []ast.StructField{} mut fff := []ast.StructField{}
for f in arr.fields { for f in arr.fields {
mut skip := false mut skip := false
mut primary := false
for attr in f.attrs { for attr in f.attrs {
if attr.name == 'primary' {
primary = true
}
if attr.name == 'skip' { if attr.name == 'skip' {
skip = true skip = true
} }
@ -408,7 +406,7 @@ fn (mut g Gen) write_orm_insert_with_last_ids(node ast.SqlStmtLine, connection_v
skip = true skip = true
} }
} }
if !skip && !primary { if !skip {
fff << f fff << f
} }
} }
@ -986,19 +984,14 @@ fn (mut g Gen) write_orm_select(node ast.SqlExpr, connection_var_name string, le
} }
// filter_struct_fields_by_orm_attrs filters struct fields taking into its attributes. // filter_struct_fields_by_orm_attrs filters struct fields taking into its attributes.
// Used by non-create queries for skipping fields // Used by non-create queries for skipping fields.
// if it has a `skip` attribute or `primary` that inserts automatically.
fn (_ &Gen) filter_struct_fields_by_orm_attrs(fields []ast.StructField) []ast.StructField { fn (_ &Gen) filter_struct_fields_by_orm_attrs(fields []ast.StructField) []ast.StructField {
mut result := []ast.StructField{} mut result := []ast.StructField{}
for field in fields { for field in fields {
mut skip := false mut skip := false
mut primary := false
for attr in field.attrs { for attr in field.attrs {
if attr.name == 'primary' {
primary = true
}
if attr.name == 'skip' { if attr.name == 'skip' {
skip = true skip = true
} }
@ -1007,7 +1000,7 @@ fn (_ &Gen) filter_struct_fields_by_orm_attrs(fields []ast.StructField) []ast.St
} }
} }
if !skip && !primary { if !skip {
result << field result << field
} }
} }

View File

@ -22,7 +22,7 @@ fn test_orm_array() {
create table Child create table Child
}! }!
par := Parent{ new_parent := Parent{
name: 'test' name: 'test'
children: [ children: [
Child{ Child{
@ -35,7 +35,7 @@ fn test_orm_array() {
} }
sql db { sql db {
insert par into Parent insert new_parent into Parent
}! }!
parents := sql db { parents := sql db {
@ -47,8 +47,8 @@ fn test_orm_array() {
}! }!
parent := parents.first() parent := parents.first()
assert parent.name == par.name assert parent.name == new_parent.name
assert parent.children.len == par.children.len assert parent.children.len == new_parent.children.len
assert parent.children[0].name == 'abc' assert parent.children[0].name == 'abc'
assert parent.children[1].name == 'def' assert parent.children[1].name == 'def'
} }
@ -57,8 +57,6 @@ fn test_orm_relationship() {
mut db := sqlite.connect(':memory:') or { panic(err) } mut db := sqlite.connect(':memory:') or { panic(err) }
sql db { sql db {
create table Parent create table Parent
}!
sql db {
create table Child create table Child
}! }!
@ -66,13 +64,12 @@ fn test_orm_relationship() {
name: 'abc' name: 'abc'
} }
par := Parent{ new_parent := Parent{
name: 'test' name: 'test'
children: [] children: []
} }
sql db { sql db {
insert par into Parent insert new_parent into Parent
}! }!
mut parents := sql db { mut parents := sql db {
@ -93,7 +90,7 @@ fn test_orm_relationship() {
insert child into Child insert child into Child
}! }!
assert parent.name == par.name assert parent.name == new_parent.name
assert parent.children.len == 0 assert parent.children.len == 0
parents = sql db { parents = sql db {
@ -101,7 +98,7 @@ fn test_orm_relationship() {
}! }!
parent = parents.first() parent = parents.first()
assert parent.name == par.name assert parent.name == new_parent.name
assert parent.children.len == 2 assert parent.children.len == 2
assert parent.children[0].name == 'atum' assert parent.children[0].name == 'atum'
assert parent.children[1].name == 'bacon' assert parent.children[1].name == 'bacon'