feat(contrib/drivers/pgsql): more field types converting support (#3737)
This pull request significantly improves PostgreSQL array type handling
and conversion in the `pgsql` driver, providing more accurate type
mapping and conversion logic, especially for array types. It introduces
comprehensive documentation, refactors conversion logic to use the `pq`
package for array types, and adds extensive unit tests to ensure
correctness and error handling. Additionally, minor enhancements and
clarifications are made to upsert formatting and table field queries.
### PostgreSQL Array Type Handling and Conversion
* Refactored `CheckLocalTypeForField` and `ConvertValueForLocal` methods
in `contrib/drivers/pgsql/pgsql_convert.go` to accurately map PostgreSQL
array types (such as `_int2`, `_int4`, `_int8`, `_float4`, `_float8`,
`_bool`, `_varchar`, `_text`, `_char`, `_bpchar`, `_numeric`,
`_decimal`, `_money`, `_bytea`) to their corresponding Go types, using
the `pq` package for conversion. Added detailed documentation and
mapping tables for supported types.
[[1]](diffhunk://#diff-a3b1e68bfa29fbcfda7c703bbe875fa82e958f6c3ad942ef82193a9dd8ad67e2R46-R63)
[[2]](diffhunk://#diff-a3b1e68bfa29fbcfda7c703bbe875fa82e958f6c3ad942ef82193a9dd8ad67e2L56-R103)
[[3]](diffhunk://#diff-a3b1e68bfa29fbcfda7c703bbe875fa82e958f6c3ad942ef82193a9dd8ad67e2R112-R209)
* Added comprehensive unit tests in
`contrib/drivers/pgsql/pgsql_z_unit_convert_test.go` to verify type
mapping and conversion for all supported array types, including error
cases for invalid input.
### Utility and API Improvements
* Added a new `Bools()` method to the `gvar.Var` type in
`container/gvar/gvar_slice.go` for converting values to `[]bool`, with
corresponding unit tests in `container/gvar/gvar_z_unit_slice_test.go`.
[[1]](diffhunk://#diff-32e887e540e0170f785508d105cb794e4d54d854b53b6950973c80022973c490R11-R15)
[[2]](diffhunk://#diff-01453eca4d4b3e35d07ca105cb924c6441d0cd9df6cbcc337a89832c8d53057fR24-R41)
### SQL Formatting and Documentation
* Improved documentation and formatting in the upsert logic of
`contrib/drivers/pgsql/pgsql_format_upsert.go` to clarify the use of
`EXCLUDED` in PostgreSQL's `ON CONFLICT DO UPDATE`.
* Enhanced readability of the table field query in
`contrib/drivers/pgsql/pgsql_table_fields.go` by reformatting SQL and
clarifying field extraction.
---------
Co-authored-by: hailaz <739476267@qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
2025-12-08 11:18:45 +08:00
|
|
|
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
|
|
|
|
//
|
|
|
|
|
// This Source Code Form is subject to the terms of the MIT License.
|
|
|
|
|
// If a copy of the MIT was not distributed with this file,
|
|
|
|
|
// You can obtain one at https://github.com/gogf/gf.
|
|
|
|
|
|
|
|
|
|
package pgsql_test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"github.com/gogf/gf/v2/database/gdb"
|
|
|
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
|
|
|
"github.com/gogf/gf/v2/test/gtest"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Test_FormatUpsert_WithOnDuplicateStr tests FormatUpsert with OnDuplicateStr
|
|
|
|
|
func Test_FormatUpsert_WithOnDuplicateStr(t *testing.T) {
|
|
|
|
|
table := createTable()
|
|
|
|
|
defer dropTable(table)
|
|
|
|
|
|
|
|
|
|
gtest.C(t, func(t *gtest.T) {
|
|
|
|
|
// Insert initial data
|
|
|
|
|
_, err := db.Model(table).Data(g.Map{
|
|
|
|
|
"passport": "user1",
|
|
|
|
|
"password": "pwd",
|
|
|
|
|
"nickname": "nick1",
|
|
|
|
|
"create_time": CreateTime,
|
|
|
|
|
}).Insert()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
|
|
|
|
|
// Test Save with OnConflict (upsert)
|
|
|
|
|
_, err = db.Model(table).Data(g.Map{
|
|
|
|
|
"id": 1,
|
|
|
|
|
"passport": "user1",
|
|
|
|
|
"password": "newpwd",
|
|
|
|
|
"nickname": "newnick",
|
|
|
|
|
"create_time": CreateTime,
|
|
|
|
|
}).OnConflict("id").Save()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
|
|
|
|
|
// Verify the update
|
|
|
|
|
one, err := db.Model(table).Where("id", 1).One()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
t.Assert(one["password"].String(), "newpwd")
|
|
|
|
|
t.Assert(one["nickname"].String(), "newnick")
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test_FormatUpsert_WithOnDuplicateMap tests FormatUpsert with OnDuplicateMap
|
|
|
|
|
func Test_FormatUpsert_WithOnDuplicateMap(t *testing.T) {
|
|
|
|
|
table := createTable()
|
|
|
|
|
defer dropTable(table)
|
|
|
|
|
|
|
|
|
|
gtest.C(t, func(t *gtest.T) {
|
|
|
|
|
// Insert initial data
|
|
|
|
|
_, err := db.Model(table).Data(g.Map{
|
|
|
|
|
"passport": "user2",
|
|
|
|
|
"password": "pwd",
|
|
|
|
|
"nickname": "nick2",
|
|
|
|
|
"create_time": CreateTime,
|
|
|
|
|
}).Insert()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
|
|
|
|
|
// Test OnDuplicate with map - values should be column names to use EXCLUDED.column
|
|
|
|
|
_, err = db.Model(table).Data(g.Map{
|
|
|
|
|
"id": 1,
|
|
|
|
|
"passport": "user2",
|
|
|
|
|
"password": "newpwd2",
|
|
|
|
|
"nickname": "newnick2",
|
|
|
|
|
"create_time": CreateTime,
|
|
|
|
|
}).OnConflict("id").OnDuplicate(g.Map{
|
|
|
|
|
"password": "password",
|
|
|
|
|
"nickname": "nickname",
|
|
|
|
|
}).Save()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
|
|
|
|
|
// Verify - values should be from the inserted data
|
|
|
|
|
one, err := db.Model(table).Where("id", 1).One()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
t.Assert(one["password"].String(), "newpwd2")
|
|
|
|
|
t.Assert(one["nickname"].String(), "newnick2")
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test_FormatUpsert_WithCounter tests FormatUpsert with Counter type on numeric column.
|
|
|
|
|
// Note: In PostgreSQL, Counter uses EXCLUDED.column which references the NEW value being inserted,
|
|
|
|
|
// not the current table value. This differs from MySQL's ON DUPLICATE KEY UPDATE behavior.
|
|
|
|
|
func Test_FormatUpsert_WithCounter(t *testing.T) {
|
|
|
|
|
// Create a special table with numeric id for counter test
|
|
|
|
|
tableName := "t_counter_test"
|
|
|
|
|
dropTable(tableName)
|
|
|
|
|
_, err := db.Exec(ctx, `
|
|
|
|
|
CREATE TABLE `+tableName+` (
|
|
|
|
|
id bigserial PRIMARY KEY,
|
|
|
|
|
counter_value int NOT NULL DEFAULT 0,
|
|
|
|
|
name varchar(45)
|
|
|
|
|
)
|
|
|
|
|
`)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Error(err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer dropTable(tableName)
|
|
|
|
|
|
|
|
|
|
gtest.C(t, func(t *gtest.T) {
|
|
|
|
|
// Insert initial data
|
|
|
|
|
_, err := db.Model(tableName).Data(g.Map{
|
|
|
|
|
"counter_value": 10,
|
|
|
|
|
"name": "counter_test",
|
|
|
|
|
}).Insert()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
|
|
|
|
|
// Get initial ID
|
|
|
|
|
one, err := db.Model(tableName).Where("name", "counter_test").One()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
initialId := one["id"].Int64()
|
|
|
|
|
|
|
|
|
|
// Test OnDuplicate with Counter
|
|
|
|
|
// In PostgreSQL: counter_value = EXCLUDED.counter_value + 5
|
|
|
|
|
// EXCLUDED.counter_value is the value we're trying to insert (20)
|
|
|
|
|
// So result = 20 + 5 = 25
|
|
|
|
|
_, err = db.Model(tableName).Data(g.Map{
|
|
|
|
|
"id": initialId,
|
|
|
|
|
"counter_value": 20, // This is the EXCLUDED value
|
|
|
|
|
"name": "counter_test",
|
|
|
|
|
}).OnConflict("id").OnDuplicate(g.Map{
|
|
|
|
|
"counter_value": &gdb.Counter{
|
|
|
|
|
Field: "counter_value",
|
|
|
|
|
Value: 5,
|
|
|
|
|
},
|
|
|
|
|
}).Save()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
|
|
|
|
|
// Verify: EXCLUDED.counter_value(20) + 5 = 25
|
|
|
|
|
one, err = db.Model(tableName).Where("id", initialId).One()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
t.Assert(one["counter_value"].Int(), 25)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
gtest.C(t, func(t *gtest.T) {
|
|
|
|
|
// Test Counter with negative value (decrement)
|
|
|
|
|
one, err := db.Model(tableName).Where("name", "counter_test").One()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
initialId := one["id"].Int64()
|
|
|
|
|
|
|
|
|
|
// In PostgreSQL: counter_value = EXCLUDED.counter_value - 3
|
|
|
|
|
// EXCLUDED.counter_value is 100, so result = 100 - 3 = 97
|
|
|
|
|
_, err = db.Model(tableName).Data(g.Map{
|
|
|
|
|
"id": initialId,
|
|
|
|
|
"counter_value": 100, // This is the EXCLUDED value
|
|
|
|
|
"name": "counter_test",
|
|
|
|
|
}).OnConflict("id").OnDuplicate(g.Map{
|
|
|
|
|
"counter_value": &gdb.Counter{
|
|
|
|
|
Field: "counter_value",
|
|
|
|
|
Value: -3,
|
|
|
|
|
},
|
|
|
|
|
}).Save()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
|
|
|
|
|
// Verify: EXCLUDED.counter_value(100) - 3 = 97
|
|
|
|
|
one, err = db.Model(tableName).Where("id", initialId).One()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
t.Assert(one["counter_value"].Int(), 97)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test_FormatUpsert_WithRaw tests FormatUpsert with Raw type
|
|
|
|
|
func Test_FormatUpsert_WithRaw(t *testing.T) {
|
|
|
|
|
table := createTable()
|
|
|
|
|
defer dropTable(table)
|
|
|
|
|
|
|
|
|
|
gtest.C(t, func(t *gtest.T) {
|
|
|
|
|
// Insert initial data
|
|
|
|
|
_, err := db.Model(table).Data(g.Map{
|
|
|
|
|
"passport": "raw_user",
|
|
|
|
|
"password": "pwd",
|
|
|
|
|
"nickname": "nick",
|
|
|
|
|
"create_time": CreateTime,
|
|
|
|
|
}).Insert()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
|
|
|
|
|
// Get initial ID
|
|
|
|
|
one, err := db.Model(table).Where("passport", "raw_user").One()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
initialId := one["id"].Int64()
|
|
|
|
|
|
|
|
|
|
// Test OnDuplicate with Raw SQL
|
|
|
|
|
_, err = db.Model(table).Data(g.Map{
|
|
|
|
|
"id": initialId,
|
|
|
|
|
"passport": "raw_user",
|
|
|
|
|
"password": "pwd",
|
|
|
|
|
"nickname": "nick",
|
|
|
|
|
"create_time": CreateTime,
|
|
|
|
|
}).OnConflict("id").OnDuplicate(g.Map{
|
|
|
|
|
"password": gdb.Raw("'raw_password'"),
|
|
|
|
|
}).Save()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
|
|
|
|
|
// Verify
|
|
|
|
|
one, err = db.Model(table).Where("id", initialId).One()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
t.Assert(one["password"].String(), "raw_password")
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test_FormatUpsert_NoOnConflict tests FormatUpsert without OnConflict (should fail)
|
|
|
|
|
func Test_FormatUpsert_NoOnConflict(t *testing.T) {
|
|
|
|
|
table := createTable()
|
|
|
|
|
defer dropTable(table)
|
|
|
|
|
|
|
|
|
|
gtest.C(t, func(t *gtest.T) {
|
|
|
|
|
// Insert initial data
|
|
|
|
|
_, err := db.Model(table).Data(g.Map{
|
|
|
|
|
"passport": "no_conflict_user",
|
|
|
|
|
"password": "pwd",
|
|
|
|
|
"nickname": "nick",
|
|
|
|
|
"create_time": CreateTime,
|
|
|
|
|
}).Insert()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
|
2025-12-08 14:54:48 +08:00
|
|
|
// Try Save without OnConflict and without primary key in data - should fail
|
|
|
|
|
// because driver cannot auto-detect conflict columns when primary key is missing
|
feat(contrib/drivers/pgsql): more field types converting support (#3737)
This pull request significantly improves PostgreSQL array type handling
and conversion in the `pgsql` driver, providing more accurate type
mapping and conversion logic, especially for array types. It introduces
comprehensive documentation, refactors conversion logic to use the `pq`
package for array types, and adds extensive unit tests to ensure
correctness and error handling. Additionally, minor enhancements and
clarifications are made to upsert formatting and table field queries.
### PostgreSQL Array Type Handling and Conversion
* Refactored `CheckLocalTypeForField` and `ConvertValueForLocal` methods
in `contrib/drivers/pgsql/pgsql_convert.go` to accurately map PostgreSQL
array types (such as `_int2`, `_int4`, `_int8`, `_float4`, `_float8`,
`_bool`, `_varchar`, `_text`, `_char`, `_bpchar`, `_numeric`,
`_decimal`, `_money`, `_bytea`) to their corresponding Go types, using
the `pq` package for conversion. Added detailed documentation and
mapping tables for supported types.
[[1]](diffhunk://#diff-a3b1e68bfa29fbcfda7c703bbe875fa82e958f6c3ad942ef82193a9dd8ad67e2R46-R63)
[[2]](diffhunk://#diff-a3b1e68bfa29fbcfda7c703bbe875fa82e958f6c3ad942ef82193a9dd8ad67e2L56-R103)
[[3]](diffhunk://#diff-a3b1e68bfa29fbcfda7c703bbe875fa82e958f6c3ad942ef82193a9dd8ad67e2R112-R209)
* Added comprehensive unit tests in
`contrib/drivers/pgsql/pgsql_z_unit_convert_test.go` to verify type
mapping and conversion for all supported array types, including error
cases for invalid input.
### Utility and API Improvements
* Added a new `Bools()` method to the `gvar.Var` type in
`container/gvar/gvar_slice.go` for converting values to `[]bool`, with
corresponding unit tests in `container/gvar/gvar_z_unit_slice_test.go`.
[[1]](diffhunk://#diff-32e887e540e0170f785508d105cb794e4d54d854b53b6950973c80022973c490R11-R15)
[[2]](diffhunk://#diff-01453eca4d4b3e35d07ca105cb924c6441d0cd9df6cbcc337a89832c8d53057fR24-R41)
### SQL Formatting and Documentation
* Improved documentation and formatting in the upsert logic of
`contrib/drivers/pgsql/pgsql_format_upsert.go` to clarify the use of
`EXCLUDED` in PostgreSQL's `ON CONFLICT DO UPDATE`.
* Enhanced readability of the table field query in
`contrib/drivers/pgsql/pgsql_table_fields.go` by reformatting SQL and
clarifying field extraction.
---------
Co-authored-by: hailaz <739476267@qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
2025-12-08 11:18:45 +08:00
|
|
|
_, err = db.Model(table).Data(g.Map{
|
2025-12-08 14:37:35 +08:00
|
|
|
// "id": 1,
|
feat(contrib/drivers/pgsql): more field types converting support (#3737)
This pull request significantly improves PostgreSQL array type handling
and conversion in the `pgsql` driver, providing more accurate type
mapping and conversion logic, especially for array types. It introduces
comprehensive documentation, refactors conversion logic to use the `pq`
package for array types, and adds extensive unit tests to ensure
correctness and error handling. Additionally, minor enhancements and
clarifications are made to upsert formatting and table field queries.
### PostgreSQL Array Type Handling and Conversion
* Refactored `CheckLocalTypeForField` and `ConvertValueForLocal` methods
in `contrib/drivers/pgsql/pgsql_convert.go` to accurately map PostgreSQL
array types (such as `_int2`, `_int4`, `_int8`, `_float4`, `_float8`,
`_bool`, `_varchar`, `_text`, `_char`, `_bpchar`, `_numeric`,
`_decimal`, `_money`, `_bytea`) to their corresponding Go types, using
the `pq` package for conversion. Added detailed documentation and
mapping tables for supported types.
[[1]](diffhunk://#diff-a3b1e68bfa29fbcfda7c703bbe875fa82e958f6c3ad942ef82193a9dd8ad67e2R46-R63)
[[2]](diffhunk://#diff-a3b1e68bfa29fbcfda7c703bbe875fa82e958f6c3ad942ef82193a9dd8ad67e2L56-R103)
[[3]](diffhunk://#diff-a3b1e68bfa29fbcfda7c703bbe875fa82e958f6c3ad942ef82193a9dd8ad67e2R112-R209)
* Added comprehensive unit tests in
`contrib/drivers/pgsql/pgsql_z_unit_convert_test.go` to verify type
mapping and conversion for all supported array types, including error
cases for invalid input.
### Utility and API Improvements
* Added a new `Bools()` method to the `gvar.Var` type in
`container/gvar/gvar_slice.go` for converting values to `[]bool`, with
corresponding unit tests in `container/gvar/gvar_z_unit_slice_test.go`.
[[1]](diffhunk://#diff-32e887e540e0170f785508d105cb794e4d54d854b53b6950973c80022973c490R11-R15)
[[2]](diffhunk://#diff-01453eca4d4b3e35d07ca105cb924c6441d0cd9df6cbcc337a89832c8d53057fR24-R41)
### SQL Formatting and Documentation
* Improved documentation and formatting in the upsert logic of
`contrib/drivers/pgsql/pgsql_format_upsert.go` to clarify the use of
`EXCLUDED` in PostgreSQL's `ON CONFLICT DO UPDATE`.
* Enhanced readability of the table field query in
`contrib/drivers/pgsql/pgsql_table_fields.go` by reformatting SQL and
clarifying field extraction.
---------
Co-authored-by: hailaz <739476267@qq.com>
Co-authored-by: houseme <housemecn@gmail.com>
2025-12-08 11:18:45 +08:00
|
|
|
"passport": "no_conflict_user",
|
|
|
|
|
"password": "newpwd",
|
|
|
|
|
"nickname": "newnick",
|
|
|
|
|
"create_time": CreateTime,
|
|
|
|
|
}).Save()
|
|
|
|
|
t.AssertNE(err, nil)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test_FormatUpsert_MultipleConflictKeys tests FormatUpsert with multiple conflict keys
|
|
|
|
|
func Test_FormatUpsert_MultipleConflictKeys(t *testing.T) {
|
|
|
|
|
table := createTable()
|
|
|
|
|
defer dropTable(table)
|
|
|
|
|
|
|
|
|
|
gtest.C(t, func(t *gtest.T) {
|
|
|
|
|
// Insert initial data
|
|
|
|
|
_, err := db.Model(table).Data(g.Map{
|
|
|
|
|
"passport": "multi_key_user",
|
|
|
|
|
"password": "pwd",
|
|
|
|
|
"nickname": "nick",
|
|
|
|
|
"create_time": CreateTime,
|
|
|
|
|
}).Insert()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
|
|
|
|
|
// Test with multiple conflict keys using only "id" which has a unique constraint
|
|
|
|
|
// Note: Using multiple keys requires a composite unique constraint to exist
|
|
|
|
|
_, err = db.Model(table).Data(g.Map{
|
|
|
|
|
"id": 1,
|
|
|
|
|
"passport": "multi_key_user",
|
|
|
|
|
"password": "newpwd",
|
|
|
|
|
"nickname": "newnick",
|
|
|
|
|
"create_time": CreateTime,
|
|
|
|
|
}).OnConflict("id").Save()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
|
|
|
|
|
// Verify the update
|
|
|
|
|
one, err := db.Model(table).Where("id", 1).One()
|
|
|
|
|
t.AssertNil(err)
|
|
|
|
|
t.Assert(one["password"].String(), "newpwd")
|
|
|
|
|
t.Assert(one["nickname"].String(), "newnick")
|
|
|
|
|
})
|
|
|
|
|
}
|