From 010335d971c98bcab8b52f1ab08173152eeeff70 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Tue, 14 Apr 2026 12:45:27 +0300 Subject: [PATCH] orm: Table structures support joint primary keys (fixes #25579) --- vlib/orm/README.md | 1 + vlib/orm/orm.v | 54 +++++++++++++++++++++++++++++++--------- vlib/orm/orm_fn_test.v | 44 ++++++++++++++++++++++++++++++++ vlib/orm/orm_null_test.v | 18 ++++++++++++++ 4 files changed, 105 insertions(+), 12 deletions(-) diff --git a/vlib/orm/README.md b/vlib/orm/README.md index f2131e199..48f3d155b 100644 --- a/vlib/orm/README.md +++ b/vlib/orm/README.md @@ -26,6 +26,7 @@ struct Foo { - `[table: 'name']` explicitly sets the name of the table for the struct - `[comment: 'table_comment']` explicitly sets the comment of the table for the struct - `[index: 'f1, f2, f3']` explicitly sets fields of the table (`f1`, `f2`, `f3`) as indexed +- `[unique_key: 'f1, f2, f3']` adds a composite `UNIQUE` constraint for the listed fields ### Fields diff --git a/vlib/orm/orm.v b/vlib/orm/orm.v index 243af2446..965e4664e 100644 --- a/vlib/orm/orm.v +++ b/vlib/orm/orm.v @@ -899,6 +899,26 @@ fn gen_where_clause(where QueryData, q string, qm string, num bool, mut c &int) // fields - See TableField // sql_from_v - Function which maps type indices to sql type names // alternative - Needed for msdb +fn parse_table_attr_fields(table Table, attr VAttribute, valid_sql_field_names []string) ![]string { + if attr.arg == '' || attr.kind != .string { + return error("${attr.name} attribute needs to be in the format [${attr.name}: 'f1, f2, f3']") + } + mut attr_fields := []string{} + for raw_field_name in attr.arg.split(',') { + field_name := raw_field_name.trim_space() + if field_name == '' { + return error("${attr.name} attribute needs to be in the format [${attr.name}: 'f1, f2, f3']") + } + if field_name !in valid_sql_field_names { + return error("table `${table.name}` has no field's name: `${field_name}`") + } + if field_name !in attr_fields { + attr_fields << field_name + } + } + return attr_fields +} + pub fn orm_table_gen(sql_dialect SQLDialect, table Table, q string, defaults bool, def_unique_len int, fields []TableField, sql_from_v fn (int) !string, alternative bool) !string { mut str := 'CREATE TABLE IF NOT EXISTS ${q}${table.name}${q} (' @@ -915,6 +935,7 @@ pub fn orm_table_gen(sql_dialect SQLDialect, table Table, q string, defaults boo mut table_comment := '' mut field_comments := map[string]string{} mut index_fields := []string{} + mut unique_key_fields := [][]string{} valid_sql_field_names := fields.map(sql_field_name(it)) @@ -926,19 +947,21 @@ pub fn orm_table_gen(sql_dialect SQLDialect, table Table, q string, defaults boo } } 'index' { - if attr.arg != '' && attr.kind == .string { - index_strings := attr.arg.split(',') - for i in index_strings { - x := i.trim_space() - if x !in valid_sql_field_names { - return error("table `${table.name}` has no field's name: `${x}`") - } - if x.len > 0 && x !in index_fields { - index_fields << x - } + attr_fields := parse_table_attr_fields(table, attr, valid_sql_field_names) or { + return err + } + for field_name in attr_fields { + if field_name !in index_fields { + index_fields << field_name } - } else { - return error("index attribute needs to be in the format [index: 'f1, f2, f3']") + } + } + 'unique_key' { + attr_fields := parse_table_attr_fields(table, attr, valid_sql_field_names) or { + return err + } + if attr_fields.len > 0 { + unique_key_fields << attr_fields } } else {} @@ -1097,6 +1120,13 @@ pub fn orm_table_gen(sql_dialect SQLDialect, table Table, q string, defaults boo fs << '/* ${k} */UNIQUE(${tmp.join(', ')})' } } + for key_fields in unique_key_fields { + mut tmp := []string{} + for field_name in key_fields { + tmp << '${q}${field_name}${q}' + } + fs << 'UNIQUE(${tmp.join(', ')})' + } if primary != '' { fs << 'PRIMARY KEY(${q}${primary}${q})' diff --git a/vlib/orm/orm_fn_test.v b/vlib/orm/orm_fn_test.v index 1c9001ec7..43fd35fbb 100644 --- a/vlib/orm/orm_fn_test.v +++ b/vlib/orm/orm_fn_test.v @@ -417,6 +417,50 @@ fn test_orm_table_gen() { }, ], sql_type_from_v, false) or { panic(err) } assert mult_unique_query == "CREATE TABLE IF NOT EXISTS 'test_table' ('id' SERIAL DEFAULT 10, 'test' TEXT, 'abc' INT64 DEFAULT 6754, /* test */UNIQUE('test', 'abc'), PRIMARY KEY('id'));" + + table_with_unique := orm.Table{ + name: 'test_table' + attrs: [ + VAttribute{ + name: 'unique_key' + has_arg: true + arg: 'test, abc' + kind: .string + }, + ] + } + table_unique_query := orm.orm_table_gen(.default, table_with_unique, "'", true, 0, + [ + orm.TableField{ + name: 'id' + typ: typeof[int]().idx + nullable: true + default_val: '10' + attrs: [ + VAttribute{ + name: 'primary' + }, + VAttribute{ + name: 'sql' + has_arg: true + arg: 'serial' + kind: .plain + }, + ] + }, + orm.TableField{ + name: 'test' + typ: typeof[string]().idx + nullable: true + }, + orm.TableField{ + name: 'abc' + typ: typeof[i64]().idx + nullable: true + default_val: '6754' + }, + ], sql_type_from_v, false) or { panic(err) } + assert table_unique_query == "CREATE TABLE IF NOT EXISTS 'test_table' ('id' SERIAL DEFAULT 10, 'test' TEXT, 'abc' INT64 DEFAULT 6754, UNIQUE('test', 'abc'), PRIMARY KEY('id'));" } fn test_orm_table_gen_h2() { diff --git a/vlib/orm/orm_null_test.v b/vlib/orm/orm_null_test.v index 60ee97122..aaa9d71a3 100644 --- a/vlib/orm/orm_null_test.v +++ b/vlib/orm/orm_null_test.v @@ -111,6 +111,24 @@ mut: h ?int = 55 } +@[unique_key: 'role_id, api_id, source_type, source_id'] +@[table: 'core_role_api'] +struct CoreRoleApi { + role_id string + api_id string + source_type string + source_id string +} + +fn test_struct_unique_key_attribute() { + db := MockDB.new() + + sql db { + create table CoreRoleApi + }! + assert db.st.last == 'CREATE TABLE IF NOT EXISTS `core_role_api` (`role_id` string-type NOT NULL, `api_id` string-type NOT NULL, `source_type` string-type NOT NULL, `source_id` string-type NOT NULL, UNIQUE(`role_id`, `api_id`, `source_type`, `source_id`));' +} + fn test_option_struct_fields_and_none() { db := MockDB.new() -- 2.39.5