From 416f314390691a255488b831419f4812b83f8f44 Mon Sep 17 00:00:00 2001 From: wanna Date: Mon, 13 Oct 2025 18:16:09 +0800 Subject: [PATCH] fix(contrib/drivers/pgsql): Merge duplicated fields, especially for key constraints. (#4465) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pgsql 执行TableFields 或者字段信息时需要合并key信息 --- contrib/drivers/pgsql/pgsql_table_fields.go | 17 ++-- contrib/drivers/pgsql/pgsql_z_unit_db_test.go | 89 ++++++++++++++++++- .../drivers/pgsql/pgsql_z_unit_init_test.go | 4 +- 3 files changed, 101 insertions(+), 9 deletions(-) diff --git a/contrib/drivers/pgsql/pgsql_table_fields.go b/contrib/drivers/pgsql/pgsql_table_fields.go index d53667c08..d69bd46b2 100644 --- a/contrib/drivers/pgsql/pgsql_table_fields.go +++ b/contrib/drivers/pgsql/pgsql_table_fields.go @@ -57,14 +57,21 @@ func (d *Driver) TableFields(ctx context.Context, table string, schema ...string } fields = make(map[string]*gdb.TableField) var ( - index = 0 - name string - ok bool + index = 0 + name string + ok bool + existingField *gdb.TableField ) for _, m := range result { name = m["field"].String() - // Filter duplicated fields. - if _, ok = fields[name]; ok { + // Merge duplicated fields, especially for key constraints. + // Priority: pri > uni > others + if existingField, ok = fields[name]; ok { + currentKey := m["key"].String() + // Merge key information with priority: pri > uni + if currentKey == "pri" || (currentKey == "uni" && existingField.Key != "pri") { + existingField.Key = currentKey + } continue } fields[name] = &gdb.TableField{ diff --git a/contrib/drivers/pgsql/pgsql_z_unit_db_test.go b/contrib/drivers/pgsql/pgsql_z_unit_db_test.go index 3dd264c9c..79f722ee8 100644 --- a/contrib/drivers/pgsql/pgsql_z_unit_db_test.go +++ b/contrib/drivers/pgsql/pgsql_z_unit_db_test.go @@ -302,8 +302,8 @@ func Test_DB_TableFields(t *testing.T) { defer dropTable(table) var expect = map[string][]any{ - //[]string: Index Type Null Key Default Comment - //id is bigserial so the default is a pgsql function + // []string: Index Type Null Key Default Comment + // id is bigserial so the default is a pgsql function "id": {0, "int8", false, "pri", fmt.Sprintf("nextval('%s_id_seq'::regclass)", table), ""}, "passport": {1, "varchar", false, "", nil, ""}, "password": {2, "varchar", false, "", nil, ""}, @@ -384,6 +384,91 @@ int_col INT);` } +func Test_DB_TableFields_DuplicateConstraints(t *testing.T) { + // Test for the fix of duplicate field results with multiple constraints + // This test verifies that when a field has multiple constraints (e.g., both primary key and unique), + // the TableFields method correctly merges the results with proper priority (pri > uni > others) + gtest.C(t, func(t *gtest.T) { + tableName := "test_multi_constraint" + createSql := fmt.Sprintf(` + CREATE TABLE %s ( + id bigserial NOT NULL PRIMARY KEY, + email varchar(100) NOT NULL UNIQUE, + username varchar(50) NOT NULL, + status int NOT NULL DEFAULT 1 + )`, tableName) + + _, err := db.Exec(ctx, createSql) + t.AssertNil(err) + defer dropTable(tableName) + + // Get table fields + fields, err := db.TableFields(ctx, tableName) + t.AssertNil(err) + + // Verify id field has primary key constraint + t.AssertNE(fields["id"], nil) + t.Assert(fields["id"].Key, "pri") + t.Assert(fields["id"].Name, "id") + t.Assert(fields["id"].Type, "int8") + + // Verify email field has unique constraint + t.AssertNE(fields["email"], nil) + t.Assert(fields["email"].Key, "uni") + t.Assert(fields["email"].Name, "email") + t.Assert(fields["email"].Type, "varchar") + + // Verify username field has no constraint + t.AssertNE(fields["username"], nil) + t.Assert(fields["username"].Key, "") + t.Assert(fields["username"].Name, "username") + + // Verify status field has no constraint and has default value + t.AssertNE(fields["status"], nil) + t.Assert(fields["status"].Key, "") + t.Assert(fields["status"].Name, "status") + t.Assert(fields["status"].Default, 1) + + // Verify field count is correct (no duplicates) + t.Assert(len(fields), 4) + }) + + // Test table with composite constraints + gtest.C(t, func(t *gtest.T) { + tableName := "test_composite_constraint" + createSql := fmt.Sprintf(` + CREATE TABLE %s ( + user_id bigint NOT NULL, + project_id bigint NOT NULL, + role varchar(50) NOT NULL, + PRIMARY KEY (user_id, project_id) + )`, tableName) + + _, err := db.Exec(ctx, createSql) + t.AssertNil(err) + defer dropTable(tableName) + + // Get table fields + fields, err := db.TableFields(ctx, tableName) + t.AssertNil(err) + + // In PostgreSQL, composite primary keys may appear in query results + // The first field in the composite key should be marked as 'pri' + t.AssertNE(fields["user_id"], nil) + t.Assert(fields["user_id"].Name, "user_id") + + t.AssertNE(fields["project_id"], nil) + t.Assert(fields["project_id"].Name, "project_id") + + t.AssertNE(fields["role"], nil) + t.Assert(fields["role"].Name, "role") + t.Assert(fields["role"].Key, "") + + // Verify field count is correct (no duplicates) + t.Assert(len(fields), 3) + }) +} + func Test_DB_InsertIgnore(t *testing.T) { table := createTable() defer dropTable(table) diff --git a/contrib/drivers/pgsql/pgsql_z_unit_init_test.go b/contrib/drivers/pgsql/pgsql_z_unit_init_test.go index e317d3000..1c71042c2 100644 --- a/contrib/drivers/pgsql/pgsql_z_unit_init_test.go +++ b/contrib/drivers/pgsql/pgsql_z_unit_init_test.go @@ -37,8 +37,8 @@ func init() { Link: `pgsql:postgres:12345678@tcp(127.0.0.1:5432)`, } - //pgsql only permit to connect to the designation database. - //so you need to create the pgsql database before you use orm + // pgsql only permit to connect to the designation database. + // so you need to create the pgsql database before you use orm gdb.AddConfigNode(gdb.DefaultGroupName, configNode) if r, err := gdb.New(configNode); err != nil { gtest.Fatal(err)