Skip to content

Commit d353bf0

Browse files
ivothglehailazhouseme
authored
feat(contrib/drivers/pgsql): more field types converting support (gogf#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>
1 parent baf30a0 commit d353bf0

19 files changed

Lines changed: 2759 additions & 75 deletions

container/gvar/gvar_slice.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ package gvar
88

99
import "github.com/gogf/gf/v2/util/gconv"
1010

11+
// Bools converts and returns `v` as []bool.
12+
func (v *Var) Bools() []bool {
13+
return gconv.Bools(v.Val())
14+
}
15+
1116
// Ints converts and returns `v` as []int.
1217
func (v *Var) Ints() []int {
1318
return gconv.Ints(v.Val())

container/gvar/gvar_z_unit_slice_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,24 @@ func TestVar_Ints(t *testing.T) {
2121
})
2222
}
2323

24+
func TestVar_Bools(t *testing.T) {
25+
gtest.C(t, func(t *gtest.T) {
26+
var arr = []bool{true, false, true, false}
27+
objOne := gvar.New(arr, true)
28+
t.AssertEQ(objOne.Bools(), arr)
29+
})
30+
gtest.C(t, func(t *gtest.T) {
31+
var arr = []int{1, 0, 1, 0}
32+
objOne := gvar.New(arr, true)
33+
t.AssertEQ(objOne.Bools(), []bool{true, false, true, false})
34+
})
35+
gtest.C(t, func(t *gtest.T) {
36+
var arr = []string{"true", "false", "1", "0"}
37+
objOne := gvar.New(arr, true)
38+
t.AssertEQ(objOne.Bools(), []bool{true, false, true, false})
39+
})
40+
}
41+
2442
func TestVar_Uints(t *testing.T) {
2543
gtest.C(t, func(t *gtest.T) {
2644
var arr = []int{1, 2, 3, 4, 5}

contrib/drivers/pgsql/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.23.0
44

55
require (
66
github.com/gogf/gf/v2 v2.9.6
7+
github.com/google/uuid v1.6.0
78
github.com/lib/pq v1.10.9
89
)
910

@@ -15,7 +16,6 @@ require (
1516
github.com/fsnotify/fsnotify v1.9.0 // indirect
1617
github.com/go-logr/logr v1.4.3 // indirect
1718
github.com/go-logr/stdr v1.2.2 // indirect
18-
github.com/google/uuid v1.6.0 // indirect
1919
github.com/gorilla/websocket v1.5.3 // indirect
2020
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
2121
github.com/magiconair/properties v1.8.10 // indirect

contrib/drivers/pgsql/pgsql_convert.go

Lines changed: 164 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"reflect"
1212
"strings"
1313

14+
"github.com/google/uuid"
1415
"github.com/lib/pq"
1516

1617
"github.com/gogf/gf/v2/database/gdb"
@@ -43,6 +44,26 @@ func (d *Driver) ConvertValueForField(ctx context.Context, fieldType string, fie
4344
}
4445

4546
// CheckLocalTypeForField checks and returns corresponding local golang type for given db type.
47+
// The parameter `fieldType` is in lower case, like:
48+
// `int2`, `int4`, `int8`, `_int2`, `_int4`, `_int8`, `_float4`, `_float8`, etc.
49+
//
50+
// PostgreSQL type mapping:
51+
//
52+
// | PostgreSQL Type | Local Go Type |
53+
// |------------------------------|---------------|
54+
// | int2, int4 | int |
55+
// | int8 | int64 |
56+
// | uuid | uuid.UUID |
57+
// | _int2, _int4 | []int32 | // Note: pq package does not provide Int16Array; int32 is used for compatibility
58+
// | _int8 | []int64 |
59+
// | _float4 | []float32 |
60+
// | _float8 | []float64 |
61+
// | _bool | []bool |
62+
// | _varchar, _text | []string |
63+
// | _char, _bpchar | []string |
64+
// | _numeric, _decimal, _money | []float64 |
65+
// | _bytea | [][]byte |
66+
// | _uuid | []uuid.UUID |
4667
func (d *Driver) CheckLocalTypeForField(ctx context.Context, fieldType string, fieldValue any) (gdb.LocalType, error) {
4768
var typeName string
4869
match, _ := gregex.MatchString(`(.+?)\((.+)\)`, fieldType)
@@ -53,92 +74,183 @@ func (d *Driver) CheckLocalTypeForField(ctx context.Context, fieldType string, f
5374
}
5475
typeName = strings.ToLower(typeName)
5576
switch typeName {
56-
case
57-
// For pgsql, int2 = smallint.
58-
"int2",
59-
// For pgsql, int4 = integer
60-
"int4":
77+
case "int2", "int4":
6178
return gdb.LocalTypeInt, nil
6279

63-
case
64-
// For pgsql, int8 = bigint
65-
"int8":
80+
case "int8":
6681
return gdb.LocalTypeInt64, nil
6782

68-
case
69-
"_int2",
70-
"_int4":
71-
return gdb.LocalTypeIntSlice, nil
83+
case "uuid":
84+
return gdb.LocalTypeUUID, nil
85+
86+
case "_int2", "_int4":
87+
return gdb.LocalTypeInt32Slice, nil
7288

73-
case
74-
"_int8":
89+
case "_int8":
7590
return gdb.LocalTypeInt64Slice, nil
7691

77-
case
78-
"_varchar", "_text":
92+
case "_float4":
93+
return gdb.LocalTypeFloat32Slice, nil
94+
95+
case "_float8":
96+
return gdb.LocalTypeFloat64Slice, nil
97+
98+
case "_bool":
99+
return gdb.LocalTypeBoolSlice, nil
100+
101+
case "_varchar", "_text", "_char", "_bpchar":
79102
return gdb.LocalTypeStringSlice, nil
80-
case "_numeric", "_decimal":
103+
104+
case "_uuid":
105+
return gdb.LocalTypeUUIDSlice, nil
106+
107+
case "_numeric", "_decimal", "_money":
81108
return gdb.LocalTypeFloat64Slice, nil
82109

110+
case "_bytea":
111+
return gdb.LocalTypeBytesSlice, nil
112+
83113
default:
84114
return d.Core.CheckLocalTypeForField(ctx, fieldType, fieldValue)
85115
}
86116
}
87117

88118
// ConvertValueForLocal converts value to local Golang type of value according field type name from database.
89119
// The parameter `fieldType` is in lower case, like:
90-
// `float(5,2)`, `unsigned double(5,2)`, `decimal(10,2)`, `char(45)`, `varchar(100)`, etc.
120+
// `int2`, `int4`, `int8`, `_int2`, `_int4`, `_int8`, `uuid`, `_uuid`, etc.
121+
//
122+
// See: https://www.postgresql.org/docs/current/datatype.html
123+
//
124+
// PostgreSQL type mapping:
125+
//
126+
// | PostgreSQL Type | SQL Type | pq Type | Go Type |
127+
// |-----------------|--------------------------------|-----------------|-------------|
128+
// | int2 | int2, smallint | - | int |
129+
// | int4 | int4, integer | - | int |
130+
// | int8 | int8, bigint, bigserial | - | int64 |
131+
// | uuid | uuid | - | uuid.UUID |
132+
// | _int2 | int2[], smallint[] | pq.Int32Array | []int32 |
133+
// | _int4 | int4[], integer[] | pq.Int32Array | []int32 |
134+
// | _int8 | int8[], bigint[] | pq.Int64Array | []int64 |
135+
// | _float4 | float4[], real[] | pq.Float32Array | []float32 |
136+
// | _float8 | float8[], double precision[] | pq.Float64Array | []float64 |
137+
// | _bool | boolean[], bool[] | pq.BoolArray | []bool |
138+
// | _varchar | varchar[], character varying[] | pq.StringArray | []string |
139+
// | _text | text[] | pq.StringArray | []string |
140+
// | _char, _bpchar | char[], character[] | pq.StringArray | []string |
141+
// | _numeric | numeric[] | pq.Float64Array | []float64 |
142+
// | _decimal | decimal[] | pq.Float64Array | []float64 |
143+
// | _money | money[] | pq.Float64Array | []float64 |
144+
// | _bytea | bytea[] | pq.ByteaArray | [][]byte |
145+
// | _uuid | uuid[] | pq.StringArray | []uuid.UUID |
146+
//
147+
// Note: PostgreSQL also supports these array types but they are not yet mapped:
148+
// - _date (date[]), _timestamp (timestamp[]), _timestamptz (timestamptz[])
149+
// - _jsonb (jsonb[]), _json (json[])
91150
func (d *Driver) ConvertValueForLocal(ctx context.Context, fieldType string, fieldValue any) (any, error) {
92151
typeName, _ := gregex.ReplaceString(`\(.+\)`, "", fieldType)
93152
typeName = strings.ToLower(typeName)
153+
154+
// Basic types are mostly handled by Core layer, only handle array types here
94155
switch typeName {
95-
// For pgsql, int2 = smallint and int4 = integer.
96-
case "int2", "int4":
97-
return gconv.Int(gconv.String(fieldValue)), nil
98156

99-
// For pgsql, int8 = bigint.
100-
case "int8":
101-
return gconv.Int64(gconv.String(fieldValue)), nil
157+
// []int32
158+
case "_int2", "_int4":
159+
var result pq.Int32Array
160+
if err := result.Scan(fieldValue); err != nil {
161+
return nil, err
162+
}
163+
return []int32(result), nil
102164

103-
// Int32 slice.
104-
case
105-
"_int2", "_int4":
106-
return gconv.Ints(
107-
gstr.ReplaceByMap(gconv.String(fieldValue),
108-
map[string]string{
109-
"{": "[",
110-
"}": "]",
111-
},
112-
),
113-
), nil
114-
115-
// Int64 slice.
116-
case
117-
"_int8":
118-
return gconv.Int64s(
119-
gstr.ReplaceByMap(gconv.String(fieldValue),
120-
map[string]string{
121-
"{": "[",
122-
"}": "]",
123-
},
124-
),
125-
), nil
165+
// []int64
166+
case "_int8":
167+
var result pq.Int64Array
168+
if err := result.Scan(fieldValue); err != nil {
169+
return nil, err
170+
}
171+
return []int64(result), nil
172+
173+
// []float32
174+
case "_float4":
175+
var result pq.Float32Array
176+
if err := result.Scan(fieldValue); err != nil {
177+
return nil, err
178+
}
179+
return []float32(result), nil
180+
181+
// []float64
182+
case "_float8":
183+
var result pq.Float64Array
184+
if err := result.Scan(fieldValue); err != nil {
185+
return nil, err
186+
}
187+
return []float64(result), nil
188+
189+
// []bool
190+
case "_bool":
191+
var result pq.BoolArray
192+
if err := result.Scan(fieldValue); err != nil {
193+
return nil, err
194+
}
195+
return []bool(result), nil
126196

127-
// String slice.
128-
case "_varchar", "_text":
129-
var result = make(pq.StringArray, 0)
197+
// []string
198+
case "_varchar", "_text", "_char", "_bpchar":
199+
var result pq.StringArray
130200
if err := result.Scan(fieldValue); err != nil {
131201
return nil, err
132202
}
133203
return []string(result), nil
134204

135-
// Float64 slice.
136-
case "_numeric", "_decimal":
205+
// uuid.UUID
206+
case "uuid":
207+
var uuidStr string
208+
switch v := fieldValue.(type) {
209+
case []byte:
210+
uuidStr = string(v)
211+
case string:
212+
uuidStr = v
213+
default:
214+
uuidStr = gconv.String(fieldValue)
215+
}
216+
result, err := uuid.Parse(uuidStr)
217+
if err != nil {
218+
return nil, err
219+
}
220+
return result, nil
221+
222+
// []uuid.UUID
223+
case "_uuid":
224+
var strArray pq.StringArray
225+
if err := strArray.Scan(fieldValue); err != nil {
226+
return nil, err
227+
}
228+
result := make([]uuid.UUID, len(strArray))
229+
for i, s := range strArray {
230+
parsed, err := uuid.Parse(s)
231+
if err != nil {
232+
return nil, err
233+
}
234+
result[i] = parsed
235+
}
236+
return result, nil
237+
238+
// []float64
239+
case "_numeric", "_decimal", "_money":
137240
var result pq.Float64Array
138241
if err := result.Scan(fieldValue); err != nil {
139242
return nil, err
140243
}
141244
return []float64(result), nil
245+
246+
// [][]byte
247+
case "_bytea":
248+
var result pq.ByteaArray
249+
if err := result.Scan(fieldValue); err != nil {
250+
return nil, err
251+
}
252+
return [][]byte(result), nil
253+
142254
default:
143255
return d.Core.ConvertValueForLocal(ctx, fieldType, fieldValue)
144256
}

contrib/drivers/pgsql/pgsql_format_upsert.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ func (d *Driver) FormatUpsert(columns []string, list gdb.List, option gdb.DoInse
5252
if columnVal < 0 {
5353
operator, columnVal = "-", -columnVal
5454
}
55+
// Note: In PostgreSQL ON CONFLICT DO UPDATE, we use EXCLUDED to reference
56+
// the value that was proposed for insertion. This differs from MySQL's
57+
// ON DUPLICATE KEY UPDATE behavior where the column name without prefix
58+
// references the current row's value.
5559
onDuplicateStr += fmt.Sprintf(
5660
"%s=EXCLUDED.%s%s%s",
5761
d.QuoteWord(k),

contrib/drivers/pgsql/pgsql_table_fields.go

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,24 @@ import (
1616

1717
var (
1818
tableFieldsSqlTmp = `
19-
SELECT a.attname AS field, t.typname AS type,a.attnotnull as null,
20-
(case when d.contype = 'p' then 'pri' when d.contype = 'u' then 'uni' else '' end) as key
21-
,ic.column_default as default_value,b.description as comment
22-
,coalesce(character_maximum_length, numeric_precision, -1) as length
23-
,numeric_scale as scale
19+
SELECT
20+
a.attname AS field,
21+
t.typname AS type,
22+
a.attnotnull AS null,
23+
(CASE WHEN d.contype = 'p' THEN 'pri' WHEN d.contype = 'u' THEN 'uni' ELSE '' END) AS key,
24+
ic.column_default AS default_value,
25+
b.description AS comment,
26+
COALESCE(character_maximum_length, numeric_precision, -1) AS length,
27+
numeric_scale AS scale
2428
FROM pg_attribute a
25-
left join pg_class c on a.attrelid = c.oid
26-
left join pg_constraint d on d.conrelid = c.oid and a.attnum = d.conkey[1]
27-
left join pg_description b ON a.attrelid=b.objoid AND a.attnum = b.objsubid
28-
left join pg_type t ON a.atttypid = t.oid
29-
left join information_schema.columns ic on ic.column_name = a.attname and ic.table_name = c.relname
30-
WHERE c.oid = '%s'::regclass and a.attisdropped is false and a.attnum > 0
29+
LEFT JOIN pg_class c ON a.attrelid = c.oid
30+
LEFT JOIN pg_constraint d ON d.conrelid = c.oid AND a.attnum = d.conkey[1]
31+
LEFT JOIN pg_description b ON a.attrelid = b.objoid AND a.attnum = b.objsubid
32+
LEFT JOIN pg_type t ON a.atttypid = t.oid
33+
LEFT JOIN information_schema.columns ic ON ic.column_name = a.attname AND ic.table_name = c.relname
34+
WHERE c.oid = '%s'::regclass
35+
AND a.attisdropped IS FALSE
36+
AND a.attnum > 0
3137
ORDER BY a.attnum`
3238
)
3339

0 commit comments

Comments
 (0)