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:
parent
72b2f22057
commit
6ac09e605e
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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:')!
|
||||||
|
|
||||||
|
@ -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 }
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
Loading…
Reference in New Issue
Block a user