From d353bf0fbc7b9fd0f71b7c25093a838bf34db90f Mon Sep 17 00:00:00 2001 From: ivothgle <24858557+ivothgle@users.noreply.github.com> Date: Mon, 8 Dec 2025 11:18:45 +0800 Subject: [PATCH] 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 --- container/gvar/gvar_slice.go | 5 + container/gvar/gvar_z_unit_slice_test.go | 18 + contrib/drivers/pgsql/go.mod | 2 +- contrib/drivers/pgsql/pgsql_convert.go | 224 +++- contrib/drivers/pgsql/pgsql_format_upsert.go | 4 + contrib/drivers/pgsql/pgsql_table_fields.go | 28 +- .../pgsql/pgsql_z_unit_convert_test.go | 409 ++++++++ .../drivers/pgsql/pgsql_z_unit_field_test.go | 954 ++++++++++++++++++ .../drivers/pgsql/pgsql_z_unit_filter_test.go | 274 +++++ .../drivers/pgsql/pgsql_z_unit_init_test.go | 215 ++++ .../drivers/pgsql/pgsql_z_unit_open_test.go | 179 ++++ .../drivers/pgsql/pgsql_z_unit_upsert_test.go | 267 +++++ database/gdb/gdb.go | 12 +- database/gdb/gdb_core_underlying.go | 9 +- database/gdb/gdb_func.go | 19 +- util/gconv/gconv_slice_bool.go | 20 + util/gconv/gconv_z_unit_bool_test.go | 23 + .../internal/converter/converter_bool.go | 7 + .../converter/converter_slice_bool.go | 173 ++++ 19 files changed, 2763 insertions(+), 79 deletions(-) create mode 100644 contrib/drivers/pgsql/pgsql_z_unit_convert_test.go create mode 100644 contrib/drivers/pgsql/pgsql_z_unit_field_test.go create mode 100644 contrib/drivers/pgsql/pgsql_z_unit_filter_test.go create mode 100644 contrib/drivers/pgsql/pgsql_z_unit_open_test.go create mode 100644 contrib/drivers/pgsql/pgsql_z_unit_upsert_test.go create mode 100644 util/gconv/gconv_slice_bool.go create mode 100644 util/gconv/internal/converter/converter_slice_bool.go diff --git a/container/gvar/gvar_slice.go b/container/gvar/gvar_slice.go index 629249c4f..fc3e10457 100644 --- a/container/gvar/gvar_slice.go +++ b/container/gvar/gvar_slice.go @@ -8,6 +8,11 @@ package gvar import "github.com/gogf/gf/v2/util/gconv" +// Bools converts and returns `v` as []bool. +func (v *Var) Bools() []bool { + return gconv.Bools(v.Val()) +} + // Ints converts and returns `v` as []int. func (v *Var) Ints() []int { return gconv.Ints(v.Val()) diff --git a/container/gvar/gvar_z_unit_slice_test.go b/container/gvar/gvar_z_unit_slice_test.go index 46531f036..26c218e96 100644 --- a/container/gvar/gvar_z_unit_slice_test.go +++ b/container/gvar/gvar_z_unit_slice_test.go @@ -21,6 +21,24 @@ func TestVar_Ints(t *testing.T) { }) } +func TestVar_Bools(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var arr = []bool{true, false, true, false} + objOne := gvar.New(arr, true) + t.AssertEQ(objOne.Bools(), arr) + }) + gtest.C(t, func(t *gtest.T) { + var arr = []int{1, 0, 1, 0} + objOne := gvar.New(arr, true) + t.AssertEQ(objOne.Bools(), []bool{true, false, true, false}) + }) + gtest.C(t, func(t *gtest.T) { + var arr = []string{"true", "false", "1", "0"} + objOne := gvar.New(arr, true) + t.AssertEQ(objOne.Bools(), []bool{true, false, true, false}) + }) +} + func TestVar_Uints(t *testing.T) { gtest.C(t, func(t *gtest.T) { var arr = []int{1, 2, 3, 4, 5} diff --git a/contrib/drivers/pgsql/go.mod b/contrib/drivers/pgsql/go.mod index 5676c2842..8101dba76 100644 --- a/contrib/drivers/pgsql/go.mod +++ b/contrib/drivers/pgsql/go.mod @@ -4,6 +4,7 @@ go 1.23.0 require ( github.com/gogf/gf/v2 v2.9.6 + github.com/google/uuid v1.6.0 github.com/lib/pq v1.10.9 ) @@ -15,7 +16,6 @@ require ( github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/grokify/html-strip-tags-go v0.1.0 // indirect github.com/magiconair/properties v1.8.10 // indirect diff --git a/contrib/drivers/pgsql/pgsql_convert.go b/contrib/drivers/pgsql/pgsql_convert.go index f308d95df..55c310b65 100644 --- a/contrib/drivers/pgsql/pgsql_convert.go +++ b/contrib/drivers/pgsql/pgsql_convert.go @@ -11,6 +11,7 @@ import ( "reflect" "strings" + "github.com/google/uuid" "github.com/lib/pq" "github.com/gogf/gf/v2/database/gdb" @@ -43,6 +44,26 @@ func (d *Driver) ConvertValueForField(ctx context.Context, fieldType string, fie } // CheckLocalTypeForField checks and returns corresponding local golang type for given db type. +// The parameter `fieldType` is in lower case, like: +// `int2`, `int4`, `int8`, `_int2`, `_int4`, `_int8`, `_float4`, `_float8`, etc. +// +// PostgreSQL type mapping: +// +// | PostgreSQL Type | Local Go Type | +// |------------------------------|---------------| +// | int2, int4 | int | +// | int8 | int64 | +// | uuid | uuid.UUID | +// | _int2, _int4 | []int32 | // Note: pq package does not provide Int16Array; int32 is used for compatibility +// | _int8 | []int64 | +// | _float4 | []float32 | +// | _float8 | []float64 | +// | _bool | []bool | +// | _varchar, _text | []string | +// | _char, _bpchar | []string | +// | _numeric, _decimal, _money | []float64 | +// | _bytea | [][]byte | +// | _uuid | []uuid.UUID | func (d *Driver) CheckLocalTypeForField(ctx context.Context, fieldType string, fieldValue any) (gdb.LocalType, error) { var typeName string match, _ := gregex.MatchString(`(.+?)\((.+)\)`, fieldType) @@ -53,33 +74,42 @@ func (d *Driver) CheckLocalTypeForField(ctx context.Context, fieldType string, f } typeName = strings.ToLower(typeName) switch typeName { - case - // For pgsql, int2 = smallint. - "int2", - // For pgsql, int4 = integer - "int4": + case "int2", "int4": return gdb.LocalTypeInt, nil - case - // For pgsql, int8 = bigint - "int8": + case "int8": return gdb.LocalTypeInt64, nil - case - "_int2", - "_int4": - return gdb.LocalTypeIntSlice, nil + case "uuid": + return gdb.LocalTypeUUID, nil - case - "_int8": + case "_int2", "_int4": + return gdb.LocalTypeInt32Slice, nil + + case "_int8": return gdb.LocalTypeInt64Slice, nil - case - "_varchar", "_text": - return gdb.LocalTypeStringSlice, nil - case "_numeric", "_decimal": + case "_float4": + return gdb.LocalTypeFloat32Slice, nil + + case "_float8": return gdb.LocalTypeFloat64Slice, nil + case "_bool": + return gdb.LocalTypeBoolSlice, nil + + case "_varchar", "_text", "_char", "_bpchar": + return gdb.LocalTypeStringSlice, nil + + case "_uuid": + return gdb.LocalTypeUUIDSlice, nil + + case "_numeric", "_decimal", "_money": + return gdb.LocalTypeFloat64Slice, nil + + case "_bytea": + return gdb.LocalTypeBytesSlice, nil + default: return d.Core.CheckLocalTypeForField(ctx, fieldType, fieldValue) } @@ -87,58 +117,140 @@ func (d *Driver) CheckLocalTypeForField(ctx context.Context, fieldType string, f // ConvertValueForLocal converts value to local Golang type of value according field type name from database. // The parameter `fieldType` is in lower case, like: -// `float(5,2)`, `unsigned double(5,2)`, `decimal(10,2)`, `char(45)`, `varchar(100)`, etc. +// `int2`, `int4`, `int8`, `_int2`, `_int4`, `_int8`, `uuid`, `_uuid`, etc. +// +// See: https://www.postgresql.org/docs/current/datatype.html +// +// PostgreSQL type mapping: +// +// | PostgreSQL Type | SQL Type | pq Type | Go Type | +// |-----------------|--------------------------------|-----------------|-------------| +// | int2 | int2, smallint | - | int | +// | int4 | int4, integer | - | int | +// | int8 | int8, bigint, bigserial | - | int64 | +// | uuid | uuid | - | uuid.UUID | +// | _int2 | int2[], smallint[] | pq.Int32Array | []int32 | +// | _int4 | int4[], integer[] | pq.Int32Array | []int32 | +// | _int8 | int8[], bigint[] | pq.Int64Array | []int64 | +// | _float4 | float4[], real[] | pq.Float32Array | []float32 | +// | _float8 | float8[], double precision[] | pq.Float64Array | []float64 | +// | _bool | boolean[], bool[] | pq.BoolArray | []bool | +// | _varchar | varchar[], character varying[] | pq.StringArray | []string | +// | _text | text[] | pq.StringArray | []string | +// | _char, _bpchar | char[], character[] | pq.StringArray | []string | +// | _numeric | numeric[] | pq.Float64Array | []float64 | +// | _decimal | decimal[] | pq.Float64Array | []float64 | +// | _money | money[] | pq.Float64Array | []float64 | +// | _bytea | bytea[] | pq.ByteaArray | [][]byte | +// | _uuid | uuid[] | pq.StringArray | []uuid.UUID | +// +// Note: PostgreSQL also supports these array types but they are not yet mapped: +// - _date (date[]), _timestamp (timestamp[]), _timestamptz (timestamptz[]) +// - _jsonb (jsonb[]), _json (json[]) func (d *Driver) ConvertValueForLocal(ctx context.Context, fieldType string, fieldValue any) (any, error) { typeName, _ := gregex.ReplaceString(`\(.+\)`, "", fieldType) typeName = strings.ToLower(typeName) + + // Basic types are mostly handled by Core layer, only handle array types here switch typeName { - // For pgsql, int2 = smallint and int4 = integer. - case "int2", "int4": - return gconv.Int(gconv.String(fieldValue)), nil - // For pgsql, int8 = bigint. - case "int8": - return gconv.Int64(gconv.String(fieldValue)), nil - - // Int32 slice. - case - "_int2", "_int4": - return gconv.Ints( - gstr.ReplaceByMap(gconv.String(fieldValue), - map[string]string{ - "{": "[", - "}": "]", - }, - ), - ), nil - - // Int64 slice. - case - "_int8": - return gconv.Int64s( - gstr.ReplaceByMap(gconv.String(fieldValue), - map[string]string{ - "{": "[", - "}": "]", - }, - ), - ), nil - - // String slice. - case "_varchar", "_text": - var result = make(pq.StringArray, 0) + // []int32 + case "_int2", "_int4": + var result pq.Int32Array if err := result.Scan(fieldValue); err != nil { return nil, err } - return []string(result), nil + return []int32(result), nil - // Float64 slice. - case "_numeric", "_decimal": + // []int64 + case "_int8": + var result pq.Int64Array + if err := result.Scan(fieldValue); err != nil { + return nil, err + } + return []int64(result), nil + + // []float32 + case "_float4": + var result pq.Float32Array + if err := result.Scan(fieldValue); err != nil { + return nil, err + } + return []float32(result), nil + + // []float64 + case "_float8": var result pq.Float64Array if err := result.Scan(fieldValue); err != nil { return nil, err } return []float64(result), nil + + // []bool + case "_bool": + var result pq.BoolArray + if err := result.Scan(fieldValue); err != nil { + return nil, err + } + return []bool(result), nil + + // []string + case "_varchar", "_text", "_char", "_bpchar": + var result pq.StringArray + if err := result.Scan(fieldValue); err != nil { + return nil, err + } + return []string(result), nil + + // uuid.UUID + case "uuid": + var uuidStr string + switch v := fieldValue.(type) { + case []byte: + uuidStr = string(v) + case string: + uuidStr = v + default: + uuidStr = gconv.String(fieldValue) + } + result, err := uuid.Parse(uuidStr) + if err != nil { + return nil, err + } + return result, nil + + // []uuid.UUID + case "_uuid": + var strArray pq.StringArray + if err := strArray.Scan(fieldValue); err != nil { + return nil, err + } + result := make([]uuid.UUID, len(strArray)) + for i, s := range strArray { + parsed, err := uuid.Parse(s) + if err != nil { + return nil, err + } + result[i] = parsed + } + return result, nil + + // []float64 + case "_numeric", "_decimal", "_money": + var result pq.Float64Array + if err := result.Scan(fieldValue); err != nil { + return nil, err + } + return []float64(result), nil + + // [][]byte + case "_bytea": + var result pq.ByteaArray + if err := result.Scan(fieldValue); err != nil { + return nil, err + } + return [][]byte(result), nil + default: return d.Core.ConvertValueForLocal(ctx, fieldType, fieldValue) } diff --git a/contrib/drivers/pgsql/pgsql_format_upsert.go b/contrib/drivers/pgsql/pgsql_format_upsert.go index fc003cb4c..81f989700 100644 --- a/contrib/drivers/pgsql/pgsql_format_upsert.go +++ b/contrib/drivers/pgsql/pgsql_format_upsert.go @@ -52,6 +52,10 @@ func (d *Driver) FormatUpsert(columns []string, list gdb.List, option gdb.DoInse if columnVal < 0 { operator, columnVal = "-", -columnVal } + // Note: In PostgreSQL ON CONFLICT DO UPDATE, we use EXCLUDED to reference + // the value that was proposed for insertion. This differs from MySQL's + // ON DUPLICATE KEY UPDATE behavior where the column name without prefix + // references the current row's value. onDuplicateStr += fmt.Sprintf( "%s=EXCLUDED.%s%s%s", d.QuoteWord(k), diff --git a/contrib/drivers/pgsql/pgsql_table_fields.go b/contrib/drivers/pgsql/pgsql_table_fields.go index 64e0ee790..07f3a4e43 100644 --- a/contrib/drivers/pgsql/pgsql_table_fields.go +++ b/contrib/drivers/pgsql/pgsql_table_fields.go @@ -16,18 +16,24 @@ import ( var ( tableFieldsSqlTmp = ` -SELECT a.attname AS field, t.typname AS type,a.attnotnull as null, - (case when d.contype = 'p' then 'pri' when d.contype = 'u' then 'uni' else '' end) as key - ,ic.column_default as default_value,b.description as comment - ,coalesce(character_maximum_length, numeric_precision, -1) as length - ,numeric_scale as scale +SELECT + a.attname AS field, + t.typname AS type, + a.attnotnull AS null, + (CASE WHEN d.contype = 'p' THEN 'pri' WHEN d.contype = 'u' THEN 'uni' ELSE '' END) AS key, + ic.column_default AS default_value, + b.description AS comment, + COALESCE(character_maximum_length, numeric_precision, -1) AS length, + numeric_scale AS scale FROM pg_attribute a - left join pg_class c on a.attrelid = c.oid - left join pg_constraint d on d.conrelid = c.oid and a.attnum = d.conkey[1] - left join pg_description b ON a.attrelid=b.objoid AND a.attnum = b.objsubid - left join pg_type t ON a.atttypid = t.oid - left join information_schema.columns ic on ic.column_name = a.attname and ic.table_name = c.relname -WHERE c.oid = '%s'::regclass and a.attisdropped is false and a.attnum > 0 + LEFT JOIN pg_class c ON a.attrelid = c.oid + LEFT JOIN pg_constraint d ON d.conrelid = c.oid AND a.attnum = d.conkey[1] + LEFT JOIN pg_description b ON a.attrelid = b.objoid AND a.attnum = b.objsubid + LEFT JOIN pg_type t ON a.atttypid = t.oid + LEFT JOIN information_schema.columns ic ON ic.column_name = a.attname AND ic.table_name = c.relname +WHERE c.oid = '%s'::regclass + AND a.attisdropped IS FALSE + AND a.attnum > 0 ORDER BY a.attnum` ) diff --git a/contrib/drivers/pgsql/pgsql_z_unit_convert_test.go b/contrib/drivers/pgsql/pgsql_z_unit_convert_test.go new file mode 100644 index 000000000..62bf92e6e --- /dev/null +++ b/contrib/drivers/pgsql/pgsql_z_unit_convert_test.go @@ -0,0 +1,409 @@ +// 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 ( + "context" + "testing" + + "github.com/google/uuid" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/test/gtest" + + "github.com/gogf/gf/contrib/drivers/pgsql/v2" +) + +// Test_CheckLocalTypeForField tests the CheckLocalTypeForField method +// for various PostgreSQL types +func Test_CheckLocalTypeForField(t *testing.T) { + var ( + ctx = context.Background() + driver = pgsql.Driver{} + ) + + gtest.C(t, func(t *gtest.T) { + // Test basic integer types + localType, err := driver.CheckLocalTypeForField(ctx, "int2", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeInt) + + localType, err = driver.CheckLocalTypeForField(ctx, "int4", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeInt) + + localType, err = driver.CheckLocalTypeForField(ctx, "int8", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeInt64) + }) + + gtest.C(t, func(t *gtest.T) { + // Test integer array types + localType, err := driver.CheckLocalTypeForField(ctx, "_int2", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeInt32Slice) + + localType, err = driver.CheckLocalTypeForField(ctx, "_int4", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeInt32Slice) + + localType, err = driver.CheckLocalTypeForField(ctx, "_int8", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeInt64Slice) + }) + + gtest.C(t, func(t *gtest.T) { + // Test float array types + localType, err := driver.CheckLocalTypeForField(ctx, "_float4", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeFloat32Slice) + + localType, err = driver.CheckLocalTypeForField(ctx, "_float8", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeFloat64Slice) + }) + + gtest.C(t, func(t *gtest.T) { + // Test boolean array type + localType, err := driver.CheckLocalTypeForField(ctx, "_bool", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeBoolSlice) + }) + + gtest.C(t, func(t *gtest.T) { + // Test string array types + localType, err := driver.CheckLocalTypeForField(ctx, "_varchar", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeStringSlice) + + localType, err = driver.CheckLocalTypeForField(ctx, "_text", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeStringSlice) + + localType, err = driver.CheckLocalTypeForField(ctx, "_char", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeStringSlice) + + localType, err = driver.CheckLocalTypeForField(ctx, "_bpchar", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeStringSlice) + }) + + gtest.C(t, func(t *gtest.T) { + // Test numeric array types + localType, err := driver.CheckLocalTypeForField(ctx, "_numeric", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeFloat64Slice) + + localType, err = driver.CheckLocalTypeForField(ctx, "_decimal", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeFloat64Slice) + + localType, err = driver.CheckLocalTypeForField(ctx, "_money", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeFloat64Slice) + }) + + gtest.C(t, func(t *gtest.T) { + // Test bytea array type + localType, err := driver.CheckLocalTypeForField(ctx, "_bytea", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeBytesSlice) + }) + + gtest.C(t, func(t *gtest.T) { + // Test uuid type + localType, err := driver.CheckLocalTypeForField(ctx, "uuid", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeUUID) + }) + + gtest.C(t, func(t *gtest.T) { + // Test uuid array type + localType, err := driver.CheckLocalTypeForField(ctx, "_uuid", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeUUIDSlice) + }) + + gtest.C(t, func(t *gtest.T) { + // Test type with precision, e.g., "numeric(10,2)" + localType, err := driver.CheckLocalTypeForField(ctx, "int2(5)", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeInt) + + localType, err = driver.CheckLocalTypeForField(ctx, "int4(10)", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeInt) + + localType, err = driver.CheckLocalTypeForField(ctx, "INT8(20)", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeInt64) + }) + + gtest.C(t, func(t *gtest.T) { + // Test uppercase type names + localType, err := driver.CheckLocalTypeForField(ctx, "INT2", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeInt) + + localType, err = driver.CheckLocalTypeForField(ctx, "_INT4", nil) + t.AssertNil(err) + t.Assert(localType, gdb.LocalTypeInt32Slice) + }) +} + +// Test_ConvertValueForLocal tests the ConvertValueForLocal method +func Test_ConvertValueForLocal(t *testing.T) { + var ( + ctx = context.Background() + driver = pgsql.Driver{} + ) + + gtest.C(t, func(t *gtest.T) { + // Test _int2 array conversion + result, err := driver.ConvertValueForLocal(ctx, "_int2", []byte(`{1,2,3}`)) + t.AssertNil(err) + t.Assert(result, []int32{1, 2, 3}) + }) + + gtest.C(t, func(t *gtest.T) { + // Test _int4 array conversion + result, err := driver.ConvertValueForLocal(ctx, "_int4", []byte(`{10,20,30}`)) + t.AssertNil(err) + t.Assert(result, []int32{10, 20, 30}) + }) + + gtest.C(t, func(t *gtest.T) { + // Test _int8 array conversion + result, err := driver.ConvertValueForLocal(ctx, "_int8", []byte(`{100,200,300}`)) + t.AssertNil(err) + t.Assert(result, []int64{100, 200, 300}) + }) + + gtest.C(t, func(t *gtest.T) { + // Test _float4 array conversion + result, err := driver.ConvertValueForLocal(ctx, "_float4", []byte(`{1.1,2.2,3.3}`)) + t.AssertNil(err) + resultArr := result.([]float32) + t.Assert(len(resultArr), 3) + t.Assert(resultArr[0] > 1.0 && resultArr[0] < 1.2, true) + t.Assert(resultArr[1] > 2.1 && resultArr[1] < 2.3, true) + t.Assert(resultArr[2] > 3.2 && resultArr[2] < 3.4, true) + }) + + gtest.C(t, func(t *gtest.T) { + // Test _float8 array conversion + result, err := driver.ConvertValueForLocal(ctx, "_float8", []byte(`{1.11,2.22,3.33}`)) + t.AssertNil(err) + resultArr := result.([]float64) + t.Assert(len(resultArr), 3) + t.Assert(resultArr[0] > 1.1 && resultArr[0] < 1.12, true) + t.Assert(resultArr[1] > 2.21 && resultArr[1] < 2.23, true) + t.Assert(resultArr[2] > 3.32 && resultArr[2] < 3.34, true) + }) + + gtest.C(t, func(t *gtest.T) { + // Test _bool array conversion + result, err := driver.ConvertValueForLocal(ctx, "_bool", []byte(`{t,f,t}`)) + t.AssertNil(err) + t.Assert(result, []bool{true, false, true}) + }) + + gtest.C(t, func(t *gtest.T) { + // Test _varchar array conversion + result, err := driver.ConvertValueForLocal(ctx, "_varchar", []byte(`{a,b,c}`)) + t.AssertNil(err) + t.Assert(result, []string{"a", "b", "c"}) + }) + + gtest.C(t, func(t *gtest.T) { + // Test _text array conversion + result, err := driver.ConvertValueForLocal(ctx, "_text", []byte(`{hello,world}`)) + t.AssertNil(err) + t.Assert(result, []string{"hello", "world"}) + }) + + gtest.C(t, func(t *gtest.T) { + // Test _char array conversion + result, err := driver.ConvertValueForLocal(ctx, "_char", []byte(`{x,y,z}`)) + t.AssertNil(err) + t.Assert(result, []string{"x", "y", "z"}) + }) + + gtest.C(t, func(t *gtest.T) { + // Test _bpchar array conversion + result, err := driver.ConvertValueForLocal(ctx, "_bpchar", []byte(`{a,b}`)) + t.AssertNil(err) + t.Assert(result, []string{"a", "b"}) + }) + + gtest.C(t, func(t *gtest.T) { + // Test _numeric array conversion + result, err := driver.ConvertValueForLocal(ctx, "_numeric", []byte(`{1.11,2.22}`)) + t.AssertNil(err) + resultArr := result.([]float64) + t.Assert(len(resultArr), 2) + }) + + gtest.C(t, func(t *gtest.T) { + // Test _decimal array conversion + result, err := driver.ConvertValueForLocal(ctx, "_decimal", []byte(`{3.33,4.44}`)) + t.AssertNil(err) + resultArr := result.([]float64) + t.Assert(len(resultArr), 2) + }) + + gtest.C(t, func(t *gtest.T) { + // Test _money array conversion + result, err := driver.ConvertValueForLocal(ctx, "_money", []byte(`{5.55,6.66}`)) + t.AssertNil(err) + resultArr := result.([]float64) + t.Assert(len(resultArr), 2) + }) + + gtest.C(t, func(t *gtest.T) { + // Test _bytea array conversion + result, err := driver.ConvertValueForLocal(ctx, "_bytea", []byte(`{"\\x68656c6c6f","\\x776f726c64"}`)) + t.AssertNil(err) + resultArr := result.([][]byte) + t.Assert(len(resultArr), 2) + }) + + gtest.C(t, func(t *gtest.T) { + // Test uuid conversion from []byte + result, err := driver.ConvertValueForLocal(ctx, "uuid", []byte(`550e8400-e29b-41d4-a716-446655440000`)) + t.AssertNil(err) + t.Assert(result.(uuid.UUID).String(), "550e8400-e29b-41d4-a716-446655440000") + }) + + gtest.C(t, func(t *gtest.T) { + // Test uuid conversion from string + result, err := driver.ConvertValueForLocal(ctx, "uuid", "550e8400-e29b-41d4-a716-446655440000") + t.AssertNil(err) + t.Assert(result.(uuid.UUID).String(), "550e8400-e29b-41d4-a716-446655440000") + }) + + gtest.C(t, func(t *gtest.T) { + // Test uuid conversion error case with invalid uuid + _, err := driver.ConvertValueForLocal(ctx, "uuid", "invalid-uuid") + t.AssertNE(err, nil) + }) + + gtest.C(t, func(t *gtest.T) { + // Test _uuid array conversion + result, err := driver.ConvertValueForLocal(ctx, "_uuid", []byte(`{550e8400-e29b-41d4-a716-446655440000,6ba7b810-9dad-11d1-80b4-00c04fd430c8}`)) + t.AssertNil(err) + resultArr := result.([]uuid.UUID) + t.Assert(len(resultArr), 2) + t.Assert(resultArr[0].String(), "550e8400-e29b-41d4-a716-446655440000") + t.Assert(resultArr[1].String(), "6ba7b810-9dad-11d1-80b4-00c04fd430c8") + }) + + gtest.C(t, func(t *gtest.T) { + // Test _uuid array conversion error case + _, err := driver.ConvertValueForLocal(ctx, "_uuid", []byte(`{invalid-uuid}`)) + t.AssertNE(err, nil) + }) + + gtest.C(t, func(t *gtest.T) { + // Test error case with invalid data for _int2 + _, err := driver.ConvertValueForLocal(ctx, "_int2", "invalid") + t.AssertNE(err, nil) + }) + + gtest.C(t, func(t *gtest.T) { + // Test error case with invalid data for _int4 + _, err := driver.ConvertValueForLocal(ctx, "_int4", "invalid") + t.AssertNE(err, nil) + }) + + gtest.C(t, func(t *gtest.T) { + // Test error case with invalid data for _int8 + _, err := driver.ConvertValueForLocal(ctx, "_int8", "invalid") + t.AssertNE(err, nil) + }) + + gtest.C(t, func(t *gtest.T) { + // Test error case with invalid data for _float4 + _, err := driver.ConvertValueForLocal(ctx, "_float4", "invalid") + t.AssertNE(err, nil) + }) + + gtest.C(t, func(t *gtest.T) { + // Test error case with invalid data for _float8 + _, err := driver.ConvertValueForLocal(ctx, "_float8", "invalid") + t.AssertNE(err, nil) + }) + + gtest.C(t, func(t *gtest.T) { + // Test error case with invalid data for _bool + _, err := driver.ConvertValueForLocal(ctx, "_bool", "invalid") + t.AssertNE(err, nil) + }) + + gtest.C(t, func(t *gtest.T) { + // Test error case with invalid data for _varchar + _, err := driver.ConvertValueForLocal(ctx, "_varchar", 12345) + t.AssertNE(err, nil) + }) + + gtest.C(t, func(t *gtest.T) { + // Test error case with invalid data for _numeric + _, err := driver.ConvertValueForLocal(ctx, "_numeric", "invalid") + t.AssertNE(err, nil) + }) + + gtest.C(t, func(t *gtest.T) { + // Test error case with invalid data for _bytea + _, err := driver.ConvertValueForLocal(ctx, "_bytea", "invalid") + t.AssertNE(err, nil) + }) +} + +// Test_ConvertValueForField tests the ConvertValueForField method +func Test_ConvertValueForField(t *testing.T) { + var ( + ctx = context.Background() + driver = pgsql.Driver{} + ) + + gtest.C(t, func(t *gtest.T) { + // Test nil value + result, err := driver.ConvertValueForField(ctx, "varchar", nil) + t.AssertNil(err) + t.Assert(result, nil) + }) + + gtest.C(t, func(t *gtest.T) { + // Test slice value for non-json type (should convert [] to {}) + result, err := driver.ConvertValueForField(ctx, "int4[]", []int{1, 2, 3}) + t.AssertNil(err) + t.Assert(result, "{1,2,3}") + }) + + gtest.C(t, func(t *gtest.T) { + // Test slice value for non-json type with strings + // Note: gconv.String for []string{"a","b","c"} produces ["a","b","c"] which then gets converted to {"a","b","c"} + result, err := driver.ConvertValueForField(ctx, "varchar[]", []string{"a", "b", "c"}) + t.AssertNil(err) + t.Assert(result, `{"a","b","c"}`) + }) + + gtest.C(t, func(t *gtest.T) { + // Test slice value for json type (should keep [] as is) + result, err := driver.ConvertValueForField(ctx, "json", []int{1, 2, 3}) + t.AssertNil(err) + t.Assert(result, "[1,2,3]") + }) + + gtest.C(t, func(t *gtest.T) { + // Test slice value for jsonb type (should keep [] as is) + result, err := driver.ConvertValueForField(ctx, "jsonb", []string{"a", "b"}) + t.AssertNil(err) + t.Assert(result, `["a","b"]`) + }) +} diff --git a/contrib/drivers/pgsql/pgsql_z_unit_field_test.go b/contrib/drivers/pgsql/pgsql_z_unit_field_test.go new file mode 100644 index 000000000..7c3df4ab0 --- /dev/null +++ b/contrib/drivers/pgsql/pgsql_z_unit_field_test.go @@ -0,0 +1,954 @@ +// 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 ( + "fmt" + "testing" + + "github.com/google/uuid" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/test/gtest" +) + +// Test_TableFields tests the TableFields method for retrieving table field information +func Test_TableFields(t *testing.T) { + table := createAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + fields, err := db.TableFields(ctx, table) + t.AssertNil(err) + t.Assert(len(fields) > 0, true) + + // Test primary key field + t.Assert(fields["id"].Name, "id") + t.Assert(fields["id"].Key, "pri") + + // Test integer types + t.Assert(fields["col_int2"].Name, "col_int2") + t.Assert(fields["col_int4"].Name, "col_int4") + t.Assert(fields["col_int8"].Name, "col_int8") + + // Test float types + t.Assert(fields["col_float4"].Name, "col_float4") + t.Assert(fields["col_float8"].Name, "col_float8") + t.Assert(fields["col_numeric"].Name, "col_numeric") + + // Test character types + t.Assert(fields["col_char"].Name, "col_char") + t.Assert(fields["col_varchar"].Name, "col_varchar") + t.Assert(fields["col_text"].Name, "col_text") + + // Test boolean type + t.Assert(fields["col_bool"].Name, "col_bool") + + // Test date/time types + t.Assert(fields["col_date"].Name, "col_date") + t.Assert(fields["col_timestamp"].Name, "col_timestamp") + + // Test JSON types + t.Assert(fields["col_json"].Name, "col_json") + t.Assert(fields["col_jsonb"].Name, "col_jsonb") + + // Test array types + t.Assert(fields["col_int2_arr"].Name, "col_int2_arr") + t.Assert(fields["col_int4_arr"].Name, "col_int4_arr") + t.Assert(fields["col_varchar_arr"].Name, "col_varchar_arr") + }) +} + +// Test_TableFields_Types tests field type information +func Test_TableFields_Types(t *testing.T) { + table := createAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + fields, err := db.TableFields(ctx, table) + t.AssertNil(err) + + // Test integer type names + t.Assert(fields["col_int2"].Type, "int2") + t.Assert(fields["col_int4"].Type, "int4") + t.Assert(fields["col_int8"].Type, "int8") + + // Test float type names + t.Assert(fields["col_float4"].Type, "float4") + t.Assert(fields["col_float8"].Type, "float8") + t.Assert(fields["col_numeric"].Type, "numeric") + + // Test character type names + t.Assert(fields["col_char"].Type, "bpchar") + t.Assert(fields["col_varchar"].Type, "varchar") + t.Assert(fields["col_text"].Type, "text") + + // Test boolean type name + t.Assert(fields["col_bool"].Type, "bool") + + // Test date/time type names + t.Assert(fields["col_date"].Type, "date") + t.Assert(fields["col_timestamp"].Type, "timestamp") + t.Assert(fields["col_timestamptz"].Type, "timestamptz") + + // Test JSON type names + t.Assert(fields["col_json"].Type, "json") + t.Assert(fields["col_jsonb"].Type, "jsonb") + + // Test array type names (PostgreSQL uses _ prefix for array types) + t.Assert(fields["col_int2_arr"].Type, "_int2") + t.Assert(fields["col_int4_arr"].Type, "_int4") + t.Assert(fields["col_int8_arr"].Type, "_int8") + t.Assert(fields["col_float4_arr"].Type, "_float4") + t.Assert(fields["col_float8_arr"].Type, "_float8") + t.Assert(fields["col_numeric_arr"].Type, "_numeric") + t.Assert(fields["col_varchar_arr"].Type, "_varchar") + t.Assert(fields["col_text_arr"].Type, "_text") + t.Assert(fields["col_bool_arr"].Type, "_bool") + }) +} + +// Test_TableFields_Nullable tests field nullable information +func Test_TableFields_Nullable(t *testing.T) { + table := createAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + fields, err := db.TableFields(ctx, table) + t.AssertNil(err) + + // NOT NULL fields should have Null = false + t.Assert(fields["col_int2"].Null, false) + t.Assert(fields["col_int4"].Null, false) + t.Assert(fields["col_numeric"].Null, false) + t.Assert(fields["col_varchar"].Null, false) + t.Assert(fields["col_bool"].Null, false) + t.Assert(fields["col_varchar_arr"].Null, false) + + // Nullable fields should have Null = true + t.Assert(fields["col_int8"].Null, true) + t.Assert(fields["col_text"].Null, true) + t.Assert(fields["col_json"].Null, true) + }) +} + +// Test_TableFields_Comments tests field comment information +func Test_TableFields_Comments(t *testing.T) { + table := createAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + fields, err := db.TableFields(ctx, table) + t.AssertNil(err) + + // Test fields with comments + t.Assert(fields["id"].Comment, "Primary key ID") + t.Assert(fields["col_int2"].Comment, "int2 type (smallint)") + t.Assert(fields["col_int4"].Comment, "int4 type (integer)") + t.Assert(fields["col_int8"].Comment, "int8 type (bigint)") + t.Assert(fields["col_numeric"].Comment, "numeric type with precision") + t.Assert(fields["col_varchar"].Comment, "varchar type") + t.Assert(fields["col_bool"].Comment, "boolean type") + t.Assert(fields["col_timestamp"].Comment, "timestamp type") + t.Assert(fields["col_json"].Comment, "json type") + t.Assert(fields["col_jsonb"].Comment, "jsonb type") + + // Test array field comments + t.Assert(fields["col_int2_arr"].Comment, "int2 array type (_int2)") + t.Assert(fields["col_int4_arr"].Comment, "int4 array type (_int4)") + t.Assert(fields["col_int8_arr"].Comment, "int8 array type (_int8)") + t.Assert(fields["col_numeric_arr"].Comment, "numeric array type (_numeric)") + t.Assert(fields["col_varchar_arr"].Comment, "varchar array type (_varchar)") + t.Assert(fields["col_text_arr"].Comment, "text array type (_text)") + }) +} + +// Test_Field_Type_Conversion tests type conversion for various PostgreSQL types +func Test_Field_Type_Conversion(t *testing.T) { + table := createInitAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Query a single record + one, err := db.Model(table).Where("id", 1).One() + t.AssertNil(err) + t.Assert(one.IsEmpty(), false) + + // Test integer type conversions + t.Assert(one["col_int2"].Int(), 1) + t.Assert(one["col_int4"].Int(), 10) + t.Assert(one["col_int8"].Int64(), int64(100)) + + // Test float type conversions + t.Assert(one["col_float4"].Float32() > 0, true) + t.Assert(one["col_float8"].Float64() > 0, true) + + // Test string type conversions + t.AssertNE(one["col_varchar"].String(), "") + t.AssertNE(one["col_text"].String(), "") + + // Test boolean type conversion + t.Assert(one["col_bool"].Bool(), false) // i=1, 1%2==0 is false + }) +} + +// Test_Field_Array_Type_Conversion tests array type conversion +func Test_Field_Array_Type_Conversion(t *testing.T) { + table := createInitAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Query a single record + one, err := db.Model(table).Where("id", 1).One() + t.AssertNil(err) + t.Assert(one.IsEmpty(), false) + + // Test integer array type conversions + int2Arr := one["col_int2_arr"].Ints() + t.Assert(len(int2Arr), 3) + t.Assert(int2Arr[0], 1) + t.Assert(int2Arr[1], 2) + t.Assert(int2Arr[2], 1) + + int4Arr := one["col_int4_arr"].Ints() + t.Assert(len(int4Arr), 3) + t.Assert(int4Arr[0], 10) + t.Assert(int4Arr[1], 20) + t.Assert(int4Arr[2], 1) + + int8Arr := one["col_int8_arr"].Int64s() + t.Assert(len(int8Arr), 3) + t.Assert(int8Arr[0], int64(100)) + t.Assert(int8Arr[1], int64(200)) + t.Assert(int8Arr[2], int64(1)) + + // Test string array type conversions + varcharArr := one["col_varchar_arr"].Strings() + t.Assert(len(varcharArr), 3) + t.Assert(varcharArr[0], "a") + t.Assert(varcharArr[1], "b") + t.Assert(varcharArr[2], "c1") + + textArr := one["col_text_arr"].Strings() + t.Assert(len(textArr), 3) + t.Assert(textArr[0], "x") + t.Assert(textArr[1], "y") + t.Assert(textArr[2], "z1") + + // Test boolean array type conversions + // col_bool_arr is '{true, false, %t}' where %t = i%2==0, for i=1 it's false + boolArr := one["col_bool_arr"].Bools() + t.Assert(len(boolArr), 3) + t.Assert(boolArr[0], true) // literal true + t.Assert(boolArr[1], false) // literal false + t.Assert(boolArr[2], false) // i=1, 1%2==0 is false + }) +} + +// Test_Field_Array_Insert tests inserting array data +func Test_Field_Array_Insert(t *testing.T) { + table := createAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert with array values + _, err := db.Model(table).Data(g.Map{ + "col_int2": 1, + "col_int4": 10, + "col_numeric": 99.99, + "col_varchar": "test", + "col_bool": true, + "col_int2_arr": []int{1, 2, 3}, + "col_int4_arr": []int{10, 20, 30}, + "col_varchar_arr": []string{"a", "b", "c"}, + }).Insert() + t.AssertNil(err) + + // Query and verify + one, err := db.Model(table).OrderDesc("id").One() + t.AssertNil(err) + + t.Assert(one["col_int2"].Int(), 1) + t.Assert(one["col_varchar"].String(), "test") + t.Assert(one["col_bool"].Bool(), true) + + int2Arr := one["col_int2_arr"].Ints() + t.Assert(len(int2Arr), 3) + t.Assert(int2Arr[0], 1) + t.Assert(int2Arr[1], 2) + t.Assert(int2Arr[2], 3) + + varcharArr := one["col_varchar_arr"].Strings() + t.Assert(len(varcharArr), 3) + t.Assert(varcharArr[0], "a") + t.Assert(varcharArr[1], "b") + t.Assert(varcharArr[2], "c") + }) +} + +// Test_Field_Array_Update tests updating array data +func Test_Field_Array_Update(t *testing.T) { + table := createInitAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Update array values + _, err := db.Model(table).Where("id", 1).Data(g.Map{ + "col_int2_arr": []int{100, 200, 300}, + "col_varchar_arr": []string{"x", "y", "z"}, + }).Update() + t.AssertNil(err) + + // Query and verify + one, err := db.Model(table).Where("id", 1).One() + t.AssertNil(err) + + int2Arr := one["col_int2_arr"].Ints() + t.Assert(len(int2Arr), 3) + t.Assert(int2Arr[0], 100) + t.Assert(int2Arr[1], 200) + t.Assert(int2Arr[2], 300) + + varcharArr := one["col_varchar_arr"].Strings() + t.Assert(len(varcharArr), 3) + t.Assert(varcharArr[0], "x") + t.Assert(varcharArr[1], "y") + t.Assert(varcharArr[2], "z") + }) +} + +// Test_Field_JSON_Type tests JSON/JSONB type handling +func Test_Field_JSON_Type(t *testing.T) { + table := createAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert with JSON values + testData := g.Map{ + "name": "test", + "value": 123, + "items": []string{"a", "b", "c"}, + } + _, err := db.Model(table).Data(g.Map{ + "col_int2": 1, + "col_int4": 10, + "col_numeric": 99.99, + "col_varchar": "test", + "col_bool": true, + "col_json": testData, + "col_jsonb": testData, + }).Insert() + t.AssertNil(err) + + // Query and verify + one, err := db.Model(table).OrderDesc("id").One() + t.AssertNil(err) + + // Test JSON field + jsonMap := one["col_json"].Map() + t.Assert(jsonMap["name"], "test") + t.Assert(jsonMap["value"], 123) + + // Test JSONB field + jsonbMap := one["col_jsonb"].Map() + t.Assert(jsonbMap["name"], "test") + t.Assert(jsonbMap["value"], 123) + }) +} + +// Test_Field_Scan_To_Struct tests scanning results to struct +func Test_Field_Scan_To_Struct(t *testing.T) { + table := createInitAllTypesTable() + defer dropTable(table) + + type TestRecord struct { + Id int64 `json:"id"` + ColInt2 int16 `json:"col_int2"` + ColInt4 int32 `json:"col_int4"` + ColInt8 int64 `json:"col_int8"` + ColVarchar string `json:"col_varchar"` + ColBool bool `json:"col_bool"` + ColInt2Arr []int `json:"col_int2_arr"` + ColInt4Arr []int `json:"col_int4_arr"` + ColInt8Arr []int64 `json:"col_int8_arr"` + ColTextArr []string `json:"col_text_arr"` + } + + gtest.C(t, func(t *gtest.T) { + var record TestRecord + err := db.Model(table).Where("id", 1).Scan(&record) + t.AssertNil(err) + + t.Assert(record.Id, int64(1)) + t.Assert(record.ColInt2, int16(1)) + t.Assert(record.ColInt4, int32(10)) + t.Assert(record.ColInt8, int64(100)) + t.AssertNE(record.ColVarchar, "") + t.Assert(record.ColBool, false) + + // Test array fields scanned to struct + t.Assert(len(record.ColInt2Arr), 3) + t.Assert(record.ColInt2Arr[0], 1) + t.Assert(record.ColInt2Arr[1], 2) + t.Assert(record.ColInt2Arr[2], 1) + + t.Assert(len(record.ColTextArr), 3) + t.Assert(record.ColTextArr[0], "x") + t.Assert(record.ColTextArr[1], "y") + t.Assert(record.ColTextArr[2], "z1") + }) +} + +// Test_Field_Scan_To_Struct_Slice tests scanning multiple results to struct slice +func Test_Field_Scan_To_Struct_Slice(t *testing.T) { + table := createInitAllTypesTable() + defer dropTable(table) + + type TestRecord struct { + Id int64 `json:"id"` + ColInt2 int16 `json:"col_int2"` + ColVarchar string `json:"col_varchar"` + ColInt2Arr []int `json:"col_int2_arr"` + ColTextArr []string `json:"col_text_arr"` + } + + gtest.C(t, func(t *gtest.T) { + var records []TestRecord + err := db.Model(table).OrderAsc("id").Limit(5).Scan(&records) + t.AssertNil(err) + + t.Assert(len(records), 5) + + // Verify first record + t.Assert(records[0].Id, int64(1)) + t.Assert(records[0].ColInt2, int16(1)) + t.Assert(len(records[0].ColInt2Arr), 3) + + // Verify last record + t.Assert(records[4].Id, int64(5)) + t.Assert(records[4].ColInt2, int16(5)) + }) +} + +// Test_Field_Empty_Array tests handling empty arrays +func Test_Field_Empty_Array(t *testing.T) { + table := createAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert with empty array values (using default) + _, err := db.Model(table).Data(g.Map{ + "col_int2": 1, + "col_int4": 10, + "col_numeric": 99.99, + "col_varchar": "test", + "col_bool": true, + }).Insert() + t.AssertNil(err) + + // Query and verify empty arrays + one, err := db.Model(table).OrderDesc("id").One() + t.AssertNil(err) + + // Default empty arrays + int2Arr := one["col_int2_arr"].Ints() + t.Assert(len(int2Arr), 0) + + varcharArr := one["col_varchar_arr"].Strings() + t.Assert(len(varcharArr), 0) + }) +} + +// Test_Field_Null_Values tests handling NULL values +func Test_Field_Null_Values(t *testing.T) { + table := createAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert minimal required fields, leaving nullable fields as NULL + _, err := db.Model(table).Data(g.Map{ + "col_int2": 1, + "col_int4": 10, + "col_numeric": 99.99, + "col_varchar": "test", + "col_bool": true, + "col_varchar_arr": []string{}, + }).Insert() + t.AssertNil(err) + + // Query and verify NULL handling + one, err := db.Model(table).OrderDesc("id").One() + t.AssertNil(err) + + // Nullable fields should return appropriate zero values + t.Assert(one["col_text"].IsNil() || one["col_text"].IsEmpty(), true) + t.Assert(one["col_int8_arr"].IsNil() || one["col_int8_arr"].IsEmpty(), true) + }) +} + +// Test_Field_Float_Array_Type_Conversion tests float array type conversion (_float4, _float8) +func Test_Field_Float_Array_Type_Conversion(t *testing.T) { + table := createInitAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Query a single record + one, err := db.Model(table).Where("id", 1).One() + t.AssertNil(err) + t.Assert(one.IsEmpty(), false) + + // Test float4 array type conversions + float4Arr := one["col_float4_arr"].Float32s() + t.Assert(len(float4Arr), 3) + t.Assert(float4Arr[0] > 0, true) + t.Assert(float4Arr[1] > 0, true) + + // Test float8 array type conversions + float8Arr := one["col_float8_arr"].Float64s() + t.Assert(len(float8Arr), 3) + t.Assert(float8Arr[0] > 0, true) + t.Assert(float8Arr[1] > 0, true) + }) +} + +// Test_Field_Numeric_Array_Type_Conversion tests numeric/decimal array type conversion +func Test_Field_Numeric_Array_Type_Conversion(t *testing.T) { + table := createInitAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Query a single record + one, err := db.Model(table).Where("id", 1).One() + t.AssertNil(err) + t.Assert(one.IsEmpty(), false) + + // Test numeric array type conversions + numericArr := one["col_numeric_arr"].Float64s() + t.Assert(len(numericArr), 3) + t.Assert(numericArr[0] > 0, true) + t.Assert(numericArr[1] > 0, true) + + // Test decimal array type conversions + decimalArr := one["col_decimal_arr"].Float64s() + if !one["col_decimal_arr"].IsNil() { + t.Assert(len(decimalArr) > 0, true) + } + }) +} + +// Test_Field_Bool_Array_Type_Conversion tests bool array type conversion more thoroughly +func Test_Field_Bool_Array_Type_Conversion(t *testing.T) { + table := createAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert with specific bool array values + _, err := db.Model(table).Data(g.Map{ + "col_int2": 1, + "col_int4": 10, + "col_numeric": 99.99, + "col_varchar": "test", + "col_bool": true, + "col_bool_arr": []bool{true, false, true}, + }).Insert() + t.AssertNil(err) + + // Query and verify + one, err := db.Model(table).OrderDesc("id").One() + t.AssertNil(err) + + // Test bool array + boolArr := one["col_bool_arr"].Bools() + t.Assert(len(boolArr), 3) + t.Assert(boolArr[0], true) + t.Assert(boolArr[1], false) + t.Assert(boolArr[2], true) + }) +} + +// Test_Field_Char_Array_Type tests char array type (_char) +func Test_Field_Char_Array_Type(t *testing.T) { + table := createAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert with char array values + _, err := db.Model(table).Data(g.Map{ + "col_int2": 1, + "col_int4": 10, + "col_numeric": 99.99, + "col_varchar": "test", + "col_bool": true, + "col_char_arr": []string{"a", "b", "c"}, + "col_varchar_arr": []string{}, + }).Insert() + t.AssertNil(err) + + // Query and verify + one, err := db.Model(table).OrderDesc("id").One() + t.AssertNil(err) + + // Test char array + charArr := one["col_char_arr"].Strings() + t.Assert(len(charArr), 3) + }) +} + +// Test_Field_Bytea_Type tests bytea (binary) type conversion +func Test_Field_Bytea_Type(t *testing.T) { + table := createAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert with binary data + binaryData := []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f} // "Hello" in hex + _, err := db.Model(table).Data(g.Map{ + "col_int2": 1, + "col_int4": 10, + "col_numeric": 99.99, + "col_varchar": "test", + "col_bool": true, + "col_bytea": binaryData, + "col_varchar_arr": []string{}, + }).Insert() + t.AssertNil(err) + + // Query and verify + one, err := db.Model(table).OrderDesc("id").One() + t.AssertNil(err) + + // Test bytea field + result := one["col_bytea"].Bytes() + t.Assert(len(result), 5) + t.Assert(result[0], 0x48) // 'H' + }) +} + +// Test_Field_Bytea_Array_Type tests bytea array type (_bytea) +func Test_Field_Bytea_Array_Type(t *testing.T) { + table := createAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert with bytea array values using raw SQL + // PostgreSQL bytea array literal format: ARRAY[E'\\x010203', E'\\x040506']::bytea[] + _, err := db.Exec(ctx, fmt.Sprintf(` + INSERT INTO %s (col_int2, col_int4, col_numeric, col_varchar, col_bool, col_varchar_arr, col_bytea_arr) + VALUES (1, 10, 99.99, 'test', true, '{}', ARRAY[E'\\x010203', E'\\x040506']::bytea[]) + `, table)) + t.AssertNil(err) + + // Query and verify bytea array + one, err := db.Model(table).OrderDesc("id").One() + t.AssertNil(err) + + // Test bytea array field - should be converted to [][]byte + byteaArrVal := one["col_bytea_arr"] + t.Assert(byteaArrVal.IsNil(), false) + + // Verify the array contains the expected data + byteaArr := byteaArrVal.Interfaces() + t.Assert(len(byteaArr), 2) + }) +} + +// Test_Field_Date_Array_Type tests date array type (_date) +func Test_Field_Date_Array_Type(t *testing.T) { + table := createAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Note: PostgreSQL _date array is not yet mapped in the driver + // This test documents the limitation but can be extended when support is added + + _, err := db.Model(table).Data(g.Map{ + "col_int2": 1, + "col_int4": 10, + "col_numeric": 99.99, + "col_varchar": "test", + "col_bool": true, + "col_varchar_arr": []string{}, + }).Insert() + t.AssertNil(err) + + // Query and verify NULL date array is handled gracefully + one, err := db.Model(table).OrderDesc("id").One() + t.AssertNil(err) + // date array should be nil or empty + t.Assert(one["col_date_arr"].IsNil() || one["col_date_arr"].IsEmpty(), true) + }) +} + +// Test_Field_Timestamp_Array_Type tests timestamp array type (_timestamp) +func Test_Field_Timestamp_Array_Type(t *testing.T) { + table := createAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Note: PostgreSQL _timestamp array is not yet mapped in the driver + // This test documents the limitation but can be extended when support is added + + _, err := db.Model(table).Data(g.Map{ + "col_int2": 1, + "col_int4": 10, + "col_numeric": 99.99, + "col_varchar": "test", + "col_bool": true, + "col_varchar_arr": []string{}, + }).Insert() + t.AssertNil(err) + + // Query and verify NULL timestamp array is handled gracefully + one, err := db.Model(table).OrderDesc("id").One() + t.AssertNil(err) + // timestamp array should be nil or empty + t.Assert(one["col_timestamp_arr"].IsNil() || one["col_timestamp_arr"].IsEmpty(), true) + }) +} + +// Test_Field_JSONB_Array_Type tests JSONB array type (_jsonb) +func Test_Field_JSONB_Array_Type(t *testing.T) { + table := createAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Note: PostgreSQL _jsonb array is not yet mapped in the driver + // This test documents the limitation but can be extended when support is added + + _, err := db.Model(table).Data(g.Map{ + "col_int2": 1, + "col_int4": 10, + "col_numeric": 99.99, + "col_varchar": "test", + "col_bool": true, + "col_varchar_arr": []string{}, + }).Insert() + t.AssertNil(err) + + // Query and verify NULL jsonb array is handled gracefully + one, err := db.Model(table).OrderDesc("id").One() + t.AssertNil(err) + // jsonb array should be nil or empty + t.Assert(one["col_jsonb_arr"].IsNil() || one["col_jsonb_arr"].IsEmpty(), true) + }) +} + +// Test_Field_UUID_Array_Type tests UUID array type (_uuid) +func Test_Field_UUID_Array_Type(t *testing.T) { + table := createAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Insert with UUID array values using raw SQL + // PostgreSQL uuid array literal format: ARRAY['uuid1', 'uuid2']::uuid[] + uuid1 := "550e8400-e29b-41d4-a716-446655440000" + uuid2 := "6ba7b810-9dad-11d1-80b4-00c04fd430c8" + uuid3 := "6ba7b811-9dad-11d1-80b4-00c04fd430c8" + _, err := db.Exec(ctx, fmt.Sprintf(` + INSERT INTO %s (col_int2, col_int4, col_numeric, col_varchar, col_bool, col_varchar_arr, col_uuid_arr) + VALUES (1, 10, 99.99, 'test', true, '{}', ARRAY['%s', '%s', '%s']::uuid[]) + `, table, uuid1, uuid2, uuid3)) + t.AssertNil(err) + + // Query and verify UUID array + one, err := db.Model(table).OrderDesc("id").One() + t.AssertNil(err) + + // Test UUID array field - should be converted to []uuid.UUID + uuidArrVal := one["col_uuid_arr"] + t.Assert(uuidArrVal.IsNil(), false) + + // Verify the array contains the expected data as []uuid.UUID + uuidArr := uuidArrVal.Interfaces() + t.Assert(len(uuidArr), 3) + + // Verify each element is uuid.UUID type + u1, ok := uuidArr[0].(uuid.UUID) + t.Assert(ok, true) + t.Assert(u1.String(), uuid1) + + u2, ok := uuidArr[1].(uuid.UUID) + t.Assert(ok, true) + t.Assert(u2.String(), uuid2) + + u3, ok := uuidArr[2].(uuid.UUID) + t.Assert(ok, true) + t.Assert(u3.String(), uuid3) + }) +} + +// Test_Field_UUID_Type tests UUID type +func Test_Field_UUID_Type(t *testing.T) { + table := createInitAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Query and verify UUID field + one, err := db.Model(table).OrderAsc("id").One() + t.AssertNil(err) + + // Test UUID field - should be converted to uuid.UUID + uuidVal := one["col_uuid"] + t.Assert(uuidVal.IsNil(), false) + + // Verify the value is uuid.UUID type + uuidObj, ok := uuidVal.Val().(uuid.UUID) + t.Assert(ok, true) + + // Verify the UUID format + uuidStr := uuidObj.String() + t.Assert(len(uuidStr) > 0, true) + // UUID should contain the pattern from insert: 550e8400-e29b-41d4-a716-44665544000X + t.Assert(uuidStr, "550e8400-e29b-41d4-a716-446655440001") + + // Also verify we can still get string representation via .String() + t.Assert(uuidVal.String(), "550e8400-e29b-41d4-a716-446655440001") + }) +} + +// Test_Field_Bytea_Array_Type_Scan tests bytea array type and scanning +func Test_Field_Bytea_Array_Type_Scan(t *testing.T) { + table := createInitAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Query and verify bytea array field + one, err := db.Model(table).OrderAsc("id").One() + t.AssertNil(err) + + // Test bytea array field + byteaArrVal := one["col_bytea_arr"] + // bytea array should not be nil since we inserted data + t.Assert(byteaArrVal.IsNil(), false) + }) +} + +// Test_Field_Date_Array_Type_Scan tests date array type and scanning +func Test_Field_Date_Array_Type_Scan(t *testing.T) { + table := createInitAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Query and verify date array field + one, err := db.Model(table).OrderAsc("id").One() + t.AssertNil(err) + + // Test date array field + dateArrVal := one["col_date_arr"] + t.Assert(dateArrVal.IsNil(), false) + + // Verify the array contains the expected data + dateArr := dateArrVal.Strings() + t.Assert(len(dateArr) > 0, true) + }) +} + +// Test_Field_Timestamp_Array_Type_Scan tests timestamp array type and scanning +func Test_Field_Timestamp_Array_Type_Scan(t *testing.T) { + table := createInitAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Query and verify timestamp array field + one, err := db.Model(table).OrderAsc("id").One() + t.AssertNil(err) + + // Test timestamp array field + timestampArrVal := one["col_timestamp_arr"] + t.Assert(timestampArrVal.IsNil(), false) + + // Verify the array contains the expected data + timestampArr := timestampArrVal.Strings() + t.Assert(len(timestampArr) > 0, true) + }) +} + +// Test_Field_JSONB_Array_Type_Scan tests JSONB array type and scanning +func Test_Field_JSONB_Array_Type_Scan(t *testing.T) { + table := createInitAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Query and verify JSONB array field + one, err := db.Model(table).OrderAsc("id").One() + t.AssertNil(err) + + // Test JSONB array field + jsonbArrVal := one["col_jsonb_arr"] + t.Assert(jsonbArrVal.IsNil(), false) + }) +} + +// Test_Field_UUID_Query tests querying by UUID field +func Test_Field_UUID_Query(t *testing.T) { + table := createInitAllTypesTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Test 1: Query by UUID string + uuidStr := "550e8400-e29b-41d4-a716-446655440001" + one, err := db.Model(table).Where("col_uuid", uuidStr).One() + t.AssertNil(err) + t.Assert(one.IsEmpty(), false) + t.Assert(one["id"].Int(), 1) + + // Verify the returned UUID is correct + uuidObj, ok := one["col_uuid"].Val().(uuid.UUID) + t.Assert(ok, true) + t.Assert(uuidObj.String(), uuidStr) + + // Test 2: Query by uuid.UUID type directly + uuidVal, err := uuid.Parse("550e8400-e29b-41d4-a716-446655440002") + t.AssertNil(err) + one, err = db.Model(table).Where("col_uuid", uuidVal).One() + t.AssertNil(err) + t.Assert(one.IsEmpty(), false) + t.Assert(one["id"].Int(), 2) + + // Test 3: Query by UUID string using g.Map + one, err = db.Model(table).Where(g.Map{ + "col_uuid": "550e8400-e29b-41d4-a716-446655440003", + }).One() + t.AssertNil(err) + t.Assert(one.IsEmpty(), false) + t.Assert(one["id"].Int(), 3) + + // Test 4: Query by uuid.UUID type using g.Map + uuidVal, err = uuid.Parse("550e8400-e29b-41d4-a716-446655440004") + t.AssertNil(err) + one, err = db.Model(table).Where(g.Map{ + "col_uuid": uuidVal, + }).One() + t.AssertNil(err) + t.Assert(one.IsEmpty(), false) + t.Assert(one["id"].Int(), 4) + + // Test 5: Query non-existent UUID + one, err = db.Model(table).Where("col_uuid", "00000000-0000-0000-0000-000000000000").One() + t.AssertNil(err) + t.Assert(one.IsEmpty(), true) + + // Test 6: Query multiple records by UUID IN clause with strings + all, err := db.Model(table).WhereIn("col_uuid", g.Slice{ + "550e8400-e29b-41d4-a716-446655440001", + "550e8400-e29b-41d4-a716-446655440002", + }).OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(all), 2) + t.Assert(all[0]["id"].Int(), 1) + t.Assert(all[1]["id"].Int(), 2) + + // Test 7: Query multiple records by UUID IN clause with uuid.UUID types + uuid1, _ := uuid.Parse("550e8400-e29b-41d4-a716-446655440003") + uuid2, _ := uuid.Parse("550e8400-e29b-41d4-a716-446655440004") + all, err = db.Model(table).WhereIn("col_uuid", g.Slice{uuid1, uuid2}).OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(all), 2) + t.Assert(all[0]["id"].Int(), 3) + t.Assert(all[1]["id"].Int(), 4) + }) +} diff --git a/contrib/drivers/pgsql/pgsql_z_unit_filter_test.go b/contrib/drivers/pgsql/pgsql_z_unit_filter_test.go new file mode 100644 index 000000000..28cf17d06 --- /dev/null +++ b/contrib/drivers/pgsql/pgsql_z_unit_filter_test.go @@ -0,0 +1,274 @@ +// 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/frame/g" + "github.com/gogf/gf/v2/os/gctx" + "github.com/gogf/gf/v2/test/gtest" + + "github.com/gogf/gf/contrib/drivers/pgsql/v2" +) + +// Test_DoFilter_LimitOffset tests LIMIT OFFSET conversion +func Test_DoFilter_LimitOffset(t *testing.T) { + var ( + ctx = gctx.New() + driver = pgsql.Driver{} + ) + + gtest.C(t, func(t *gtest.T) { + // Test MySQL style LIMIT x,y to PostgreSQL style LIMIT y OFFSET x + sql := "SELECT * FROM users LIMIT 10, 20" + newSql, _, err := driver.DoFilter(ctx, nil, sql, nil) + t.AssertNil(err) + t.Assert(newSql, "SELECT * FROM users LIMIT 20 OFFSET 10") + }) + + gtest.C(t, func(t *gtest.T) { + // Test with different numbers + sql := "SELECT * FROM users LIMIT 0, 100" + newSql, _, err := driver.DoFilter(ctx, nil, sql, nil) + t.AssertNil(err) + t.Assert(newSql, "SELECT * FROM users LIMIT 100 OFFSET 0") + }) + + gtest.C(t, func(t *gtest.T) { + // Test no conversion needed + sql := "SELECT * FROM users LIMIT 50" + newSql, _, err := driver.DoFilter(ctx, nil, sql, nil) + t.AssertNil(err) + t.Assert(newSql, "SELECT * FROM users LIMIT 50") + }) +} + +// Test_DoFilter_InsertIgnore tests INSERT IGNORE conversion +func Test_DoFilter_InsertIgnore(t *testing.T) { + var ( + ctx = gctx.New() + driver = pgsql.Driver{} + ) + + gtest.C(t, func(t *gtest.T) { + // Test INSERT IGNORE conversion + sql := "INSERT IGNORE INTO users (name) VALUES ($1)" + newSql, _, err := driver.DoFilter(ctx, nil, sql, nil) + t.AssertNil(err) + t.Assert(newSql, "INSERT INTO users (name) VALUES ($1) ON CONFLICT DO NOTHING") + }) +} + +// Test_DoFilter_PlaceholderConversion tests placeholder conversion +func Test_DoFilter_PlaceholderConversion(t *testing.T) { + var ( + ctx = gctx.New() + driver = pgsql.Driver{} + ) + + gtest.C(t, func(t *gtest.T) { + // Test ? placeholder conversion to $n + sql := "SELECT * FROM users WHERE id = ? AND name = ?" + newSql, _, err := driver.DoFilter(ctx, nil, sql, nil) + t.AssertNil(err) + t.Assert(newSql, "SELECT * FROM users WHERE id = $1 AND name = $2") + }) + + gtest.C(t, func(t *gtest.T) { + // Test multiple placeholders + sql := "INSERT INTO users (a, b, c, d, e) VALUES (?, ?, ?, ?, ?)" + newSql, _, err := driver.DoFilter(ctx, nil, sql, nil) + t.AssertNil(err) + t.Assert(newSql, "INSERT INTO users (a, b, c, d, e) VALUES ($1, $2, $3, $4, $5)") + }) +} + +// Test_DoFilter_JsonbOperator tests JSONB operator handling +func Test_DoFilter_JsonbOperator(t *testing.T) { + var ( + ctx = gctx.New() + driver = pgsql.Driver{} + ) + + gtest.C(t, func(t *gtest.T) { + // Test jsonb ?| operator + // The jsonb ? is first converted to $1, then restored to ? + // So the next placeholder becomes $2 + sql := "SELECT * FROM users WHERE (data)::jsonb ?| ?" + newSql, _, err := driver.DoFilter(ctx, nil, sql, nil) + t.AssertNil(err) + // After placeholder conversion, the ? in jsonb should be preserved + t.Assert(newSql, "SELECT * FROM users WHERE (data)::jsonb ?| $2") + }) + + gtest.C(t, func(t *gtest.T) { + // Test jsonb ?& operator + sql := "SELECT * FROM users WHERE (data)::jsonb &? ?" + newSql, _, err := driver.DoFilter(ctx, nil, sql, nil) + t.AssertNil(err) + t.Assert(newSql, "SELECT * FROM users WHERE (data)::jsonb &? $2") + }) + + gtest.C(t, func(t *gtest.T) { + // Test jsonb ? operator + sql := "SELECT * FROM users WHERE (data)::jsonb ? ?" + newSql, _, err := driver.DoFilter(ctx, nil, sql, nil) + t.AssertNil(err) + t.Assert(newSql, "SELECT * FROM users WHERE (data)::jsonb ? $2") + }) + + gtest.C(t, func(t *gtest.T) { + // Test combination of jsonb and regular placeholders + sql := "SELECT * FROM users WHERE id = ? AND (data)::jsonb ?| ?" + newSql, _, err := driver.DoFilter(ctx, nil, sql, nil) + t.AssertNil(err) + t.Assert(newSql, "SELECT * FROM users WHERE id = $1 AND (data)::jsonb ?| $3") + }) +} + +// Test_DoFilter_ComplexQuery tests complex queries with multiple features +func Test_DoFilter_ComplexQuery(t *testing.T) { + var ( + ctx = gctx.New() + driver = pgsql.Driver{} + ) + + gtest.C(t, func(t *gtest.T) { + // Test complex query with LIMIT and placeholders + sql := "SELECT * FROM users WHERE status = ? AND age > ? LIMIT 5, 10" + newSql, _, err := driver.DoFilter(ctx, nil, sql, nil) + t.AssertNil(err) + t.Assert(newSql, "SELECT * FROM users WHERE status = $1 AND age > $2 LIMIT 10 OFFSET 5") + }) +} + +// Test_Tables tests the Tables method +func Test_Tables_Method(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + tables, err := db.Tables(ctx) + t.AssertNil(err) + t.Assert(len(tables) >= 0, true) + }) + + gtest.C(t, func(t *gtest.T) { + // Test with specific schema - use the test schema + tables, err := db.Tables(ctx, "test") + t.AssertNil(err) + t.Assert(len(tables) >= 0, true) + }) +} + +// Test_OrderRandomFunction tests the OrderRandomFunction method +func Test_OrderRandomFunction(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Test ORDER BY RANDOM() + all, err := db.Model(table).OrderRandom().All() + t.AssertNil(err) + t.Assert(len(all), TableSize) + }) +} + +// Test_GetChars tests the GetChars method +func Test_GetChars(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + driver := pgsql.Driver{} + left, right := driver.GetChars() + t.Assert(left, `"`) + t.Assert(right, `"`) + }) +} + +// Test_New tests the New method +func Test_New(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + driver := pgsql.New() + t.AssertNE(driver, nil) + }) +} + +// Test_DoExec_NonIntPrimaryKey tests DoExec with non-integer primary key +func Test_DoExec_NonIntPrimaryKey(t *testing.T) { + // Create a table with UUID primary key + tableName := "t_uuid_pk_test" + _, err := db.Exec(ctx, ` + CREATE TABLE IF NOT EXISTS `+tableName+` ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name varchar(100) + ) + `) + if err != nil { + // If gen_random_uuid is not available, skip this test + t.Log("Skipping UUID test:", err) + return + } + defer db.Exec(ctx, "DROP TABLE IF EXISTS "+tableName) + + gtest.C(t, func(t *gtest.T) { + // Insert with UUID primary key + result, err := db.Model(tableName).Data(g.Map{ + "name": "test_user", + }).Insert() + t.AssertNil(err) + + // LastInsertId should return error for non-integer primary key + _, err = result.LastInsertId() + // For UUID, LastInsertId is not supported + t.AssertNE(err, nil) + + // RowsAffected should still work + affected, err := result.RowsAffected() + t.AssertNil(err) + t.Assert(affected, int64(1)) + }) +} + +// Test_TableFields_WithSchema tests TableFields with specific schema +func Test_TableFields_WithSchema(t *testing.T) { + table := createTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + // Test with schema parameter + fields, err := db.TableFields(ctx, table, "test") + t.AssertNil(err) + t.Assert(len(fields) > 0, true) + }) +} + +// Test_TableFields_UniqueKey tests TableFields with unique key constraint +func Test_TableFields_UniqueKey(t *testing.T) { + tableName := "t_unique_test" + + // Create table with unique constraint + _, err := db.Exec(ctx, ` + CREATE TABLE IF NOT EXISTS `+tableName+` ( + id bigserial PRIMARY KEY, + email varchar(100) UNIQUE NOT NULL, + name varchar(100) + ) + `) + if err != nil { + t.Error(err) + return + } + defer db.Exec(ctx, "DROP TABLE IF EXISTS "+tableName) + + gtest.C(t, func(t *gtest.T) { + fields, err := db.TableFields(ctx, tableName) + t.AssertNil(err) + + // Check primary key + t.Assert(fields["id"].Key, "pri") + + // Check unique key + t.Assert(fields["email"].Key, "uni") + }) +} diff --git a/contrib/drivers/pgsql/pgsql_z_unit_init_test.go b/contrib/drivers/pgsql/pgsql_z_unit_init_test.go index f1a0034f3..c65b20d2c 100644 --- a/contrib/drivers/pgsql/pgsql_z_unit_init_test.go +++ b/contrib/drivers/pgsql/pgsql_z_unit_init_test.go @@ -9,6 +9,7 @@ package pgsql_test import ( "context" "fmt" + "strings" _ "github.com/gogf/gf/contrib/drivers/pgsql/v2" @@ -126,3 +127,217 @@ func dropTableWithDb(db gdb.DB, table string) { gtest.Error(err) } } + +// createAllTypesTable creates a table with all common PostgreSQL types for testing +func createAllTypesTable(table ...string) string { + return createAllTypesTableWithDb(db, table...) +} + +func createAllTypesTableWithDb(db gdb.DB, table ...string) (name string) { + if len(table) > 0 { + name = table[0] + } else { + name = fmt.Sprintf(`%s_%d`, TablePrefix+"all_types", gtime.TimestampNano()) + } + + dropTableWithDb(db, name) + + if _, err := db.Exec(ctx, fmt.Sprintf(` + CREATE TABLE %s ( + -- Basic integer types + id bigserial PRIMARY KEY, + col_int2 int2 NOT NULL DEFAULT 0, + col_int4 int4 NOT NULL DEFAULT 0, + col_int8 int8 DEFAULT 0, + col_smallint smallint, + col_integer integer, + col_bigint bigint, + + -- Float types + col_float4 float4 DEFAULT 0.0, + col_float8 float8 DEFAULT 0.0, + col_real real, + col_double double precision, + col_numeric numeric(10,2) NOT NULL DEFAULT 0.00, + col_decimal decimal(10,2), + + -- Character types + col_char char(10) DEFAULT '', + col_varchar varchar(100) NOT NULL DEFAULT '', + col_text text, + + -- Boolean type + col_bool boolean NOT NULL DEFAULT false, + + -- Date/Time types + col_date date DEFAULT CURRENT_DATE, + col_time time, + col_timetz timetz, + col_timestamp timestamp DEFAULT CURRENT_TIMESTAMP, + col_timestamptz timestamptz, + col_interval interval, + + -- Binary type + col_bytea bytea, + + -- JSON types + col_json json DEFAULT '{}', + col_jsonb jsonb DEFAULT '{}', + + -- UUID type + col_uuid uuid, + + -- Network types + col_inet inet, + col_cidr cidr, + col_macaddr macaddr, + + -- Array types - integers + col_int2_arr int2[] DEFAULT '{}', + col_int4_arr int4[] DEFAULT '{}', + col_int8_arr int8[], + + -- Array types - floats + col_float4_arr float4[], + col_float8_arr float8[], + col_numeric_arr numeric[] DEFAULT '{}', + col_decimal_arr decimal[], + + -- Array types - characters + col_varchar_arr varchar[] NOT NULL DEFAULT '{}', + col_text_arr text[], + col_char_arr char(10)[], + + -- Array types - boolean + col_bool_arr boolean[], + + -- Array types - bytea + col_bytea_arr bytea[], + + -- Array types - date/time + col_date_arr date[], + col_timestamp_arr timestamp[], + + -- Array types - JSON + col_jsonb_arr jsonb[], + + -- Array types - UUID + col_uuid_arr uuid[] + ); + + -- Add comments for columns + COMMENT ON TABLE %s IS 'Test table with all PostgreSQL types'; + COMMENT ON COLUMN %s.id IS 'Primary key ID'; + COMMENT ON COLUMN %s.col_int2 IS 'int2 type (smallint)'; + COMMENT ON COLUMN %s.col_int4 IS 'int4 type (integer)'; + COMMENT ON COLUMN %s.col_int8 IS 'int8 type (bigint)'; + COMMENT ON COLUMN %s.col_numeric IS 'numeric type with precision'; + COMMENT ON COLUMN %s.col_varchar IS 'varchar type'; + COMMENT ON COLUMN %s.col_bool IS 'boolean type'; + COMMENT ON COLUMN %s.col_timestamp IS 'timestamp type'; + COMMENT ON COLUMN %s.col_json IS 'json type'; + COMMENT ON COLUMN %s.col_jsonb IS 'jsonb type'; + COMMENT ON COLUMN %s.col_int2_arr IS 'int2 array type (_int2)'; + COMMENT ON COLUMN %s.col_int4_arr IS 'int4 array type (_int4)'; + COMMENT ON COLUMN %s.col_int8_arr IS 'int8 array type (_int8)'; + COMMENT ON COLUMN %s.col_numeric_arr IS 'numeric array type (_numeric)'; + COMMENT ON COLUMN %s.col_varchar_arr IS 'varchar array type (_varchar)'; + COMMENT ON COLUMN %s.col_text_arr IS 'text array type (_text)'; + `, name, + name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name, name)); err != nil { + gtest.Fatal(err) + } + return +} + +// createInitAllTypesTable creates and initializes a table with all common PostgreSQL types +func createInitAllTypesTable(table ...string) string { + return createInitAllTypesTableWithDb(db, table...) +} + +func createInitAllTypesTableWithDb(db gdb.DB, table ...string) (name string) { + name = createAllTypesTableWithDb(db, table...) + + // Insert test data + for i := 1; i <= TableSize; i++ { + var sql strings.Builder + + // Write INSERT statement header + sql.WriteString(fmt.Sprintf(`INSERT INTO %s ( + col_int2, col_int4, col_int8, col_smallint, col_integer, col_bigint, + col_float4, col_float8, col_real, col_double, col_numeric, col_decimal, + col_char, col_varchar, col_text, col_bool, + col_date, col_time, col_timestamp, + col_json, col_jsonb, + col_bytea, + col_uuid, + col_int2_arr, col_int4_arr, col_int8_arr, + col_float4_arr, col_float8_arr, col_numeric_arr, col_decimal_arr, + col_varchar_arr, col_text_arr, col_bool_arr, col_bytea_arr, col_date_arr, col_timestamp_arr, col_jsonb_arr, col_uuid_arr + ) VALUES (`, name)) + + // Integer types: col_int2, col_int4, col_int8, col_smallint, col_integer, col_bigint + sql.WriteString(fmt.Sprintf("%d, %d, %d, %d, %d, %d, ", + i, i*10, i*100, i, i*10, i*100)) + + // Float types: col_float4, col_float8, col_real, col_double, col_numeric, col_decimal + sql.WriteString(fmt.Sprintf("%d.5, %d.5, %d.5, %d.5, %d.99, %d.99, ", + i, i, i, i, i, i)) + + // Character types: col_char, col_varchar, col_text, col_bool + sql.WriteString(fmt.Sprintf("'char_%d', 'varchar_%d', 'text_%d', %t, ", + i, i, i, i%2 == 0)) + + // Date/Time types: col_date, col_time, col_timestamp + // Calculate day as integer in range 1-28; %02d in fmt.Sprintf ensures two-digit zero-padded format + dayOfMonth := (i-1)%28 + 1 + sql.WriteString(fmt.Sprintf("'2024-01-%02d', '10:00:%02d', '2024-01-%02d 10:00:00', ", + dayOfMonth, (i-1)%60, dayOfMonth)) + + // JSON types: col_json, col_jsonb + sql.WriteString(fmt.Sprintf(`'{"key": "value%d"}', '{"key": "value%d"}', `, i, i)) + + // Bytea type: col_bytea + sql.WriteString(`E'\\xDEADBEEF', `) + + // UUID type: col_uuid (use %x for hex representation, padded to ensure valid UUID) + sql.WriteString(fmt.Sprintf("'550e8400-e29b-41d4-a716-4466554400%02x', ", i)) + + // Integer array types: col_int2_arr, col_int4_arr, col_int8_arr + sql.WriteString(fmt.Sprintf("'{1, 2, %d}', '{10, 20, %d}', '{100, 200, %d}', ", + i, i, i)) + + // Float array types: col_float4_arr, col_float8_arr, col_numeric_arr, col_decimal_arr + sql.WriteString(fmt.Sprintf("'{1.1, 2.2, %d.3}', '{1.1, 2.2, %d.3}', '{1.11, 2.22, %d.33}', '{1.11, 2.22, %d.33}', ", + i, i, i, i)) + + // Character array types: col_varchar_arr, col_text_arr + sql.WriteString(fmt.Sprintf(`'{"a", "b", "c%d"}', '{"x", "y", "z%d"}', `, i, i)) + + // Boolean array type: col_bool_arr + sql.WriteString(fmt.Sprintf("'{true, false, %t}', ", i%2 == 0)) + + // Bytea array type: col_bytea_arr (use ARRAY syntax for bytea) + sql.WriteString(`ARRAY[E'\\xDEADBEEF', E'\\xCAFEBABE']::bytea[], `) + + // Date array type: col_date_arr + sql.WriteString(fmt.Sprintf(`'{"2024-01-%02d", "2024-01-%02d"}', `, dayOfMonth, (dayOfMonth%28)+1)) + + // Timestamp array type: col_timestamp_arr + sql.WriteString(fmt.Sprintf(`'{"2024-01-%02d 10:00:00", "2024-01-%02d 11:00:00"}', `, dayOfMonth, dayOfMonth)) + + // JSONB array type: col_jsonb_arr (store as text array first, then cast to jsonb array) + sql.WriteString(`ARRAY['{"key": "value1"}', '{"key": "value2"}']::jsonb[], `) + + // UUID array type: col_uuid_arr + sql.WriteString(fmt.Sprintf("ARRAY['550e8400-e29b-41d4-a716-4466554400%02x'::uuid, '6ba7b810-9dad-11d1-80b4-00c04fd430c8'::uuid]", i)) + + // Close VALUES + sql.WriteString(")") + + if _, err := db.Exec(ctx, sql.String()); err != nil { + gtest.Fatal(err) + } + } + return +} diff --git a/contrib/drivers/pgsql/pgsql_z_unit_open_test.go b/contrib/drivers/pgsql/pgsql_z_unit_open_test.go new file mode 100644 index 000000000..66b7313d9 --- /dev/null +++ b/contrib/drivers/pgsql/pgsql_z_unit_open_test.go @@ -0,0 +1,179 @@ +// 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/test/gtest" + + "github.com/gogf/gf/contrib/drivers/pgsql/v2" +) + +// Test_Open tests the Open method with various configurations +func Test_Open_WithNamespace(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + driver := pgsql.Driver{} + config := &gdb.ConfigNode{ + User: "postgres", + Pass: "12345678", + Host: "127.0.0.1", + Port: "5432", + Name: "test", + Namespace: "public", + } + db, err := driver.Open(config) + t.AssertNil(err) + t.AssertNE(db, nil) + if db != nil { + db.Close() + } + }) +} + +// Test_Open_WithTimezone tests Open with timezone configuration +func Test_Open_WithTimezone(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + driver := pgsql.Driver{} + config := &gdb.ConfigNode{ + User: "postgres", + Pass: "12345678", + Host: "127.0.0.1", + Port: "5432", + Name: "test", + Timezone: "Asia/Shanghai", + } + db, err := driver.Open(config) + t.AssertNil(err) + t.AssertNE(db, nil) + if db != nil { + db.Close() + } + }) +} + +// Test_Open_WithExtra tests Open with extra configuration +func Test_Open_WithExtra(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + driver := pgsql.Driver{} + config := &gdb.ConfigNode{ + User: "postgres", + Pass: "12345678", + Host: "127.0.0.1", + Port: "5432", + Name: "test", + Extra: "connect_timeout=10", + } + db, err := driver.Open(config) + t.AssertNil(err) + t.AssertNE(db, nil) + if db != nil { + db.Close() + } + }) +} + +// Test_Open_WithInvalidExtra tests Open with invalid extra configuration +func Test_Open_WithInvalidExtra(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + driver := pgsql.Driver{} + config := &gdb.ConfigNode{ + User: "postgres", + Pass: "12345678", + Host: "127.0.0.1", + Port: "5432", + Name: "test", + // Invalid extra format with invalid URL encoding that will cause parse error + Extra: "%Q=%Q&b", + } + _, err := driver.Open(config) + t.AssertNE(err, nil) + }) +} + +// Test_Open_WithFullConfig tests Open with all configuration options +func Test_Open_WithFullConfig(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + driver := pgsql.Driver{} + config := &gdb.ConfigNode{ + User: "postgres", + Pass: "12345678", + Host: "127.0.0.1", + Port: "5432", + Name: "test", + Namespace: "public", + Timezone: "UTC", + Extra: "connect_timeout=10", + } + db, err := driver.Open(config) + t.AssertNil(err) + t.AssertNE(db, nil) + if db != nil { + db.Close() + } + }) +} + +// Test_Open_WithoutPort tests Open without port +func Test_Open_WithoutPort(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + driver := pgsql.Driver{} + config := &gdb.ConfigNode{ + User: "postgres", + Pass: "12345678", + Host: "127.0.0.1", + Name: "test", + } + db, err := driver.Open(config) + t.AssertNil(err) + t.AssertNE(db, nil) + if db != nil { + db.Close() + } + }) +} + +// Test_Open_WithoutName tests Open without database name +func Test_Open_WithoutName(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + driver := pgsql.Driver{} + config := &gdb.ConfigNode{ + User: "postgres", + Pass: "12345678", + Host: "127.0.0.1", + Port: "5432", + } + db, err := driver.Open(config) + t.AssertNil(err) + t.AssertNE(db, nil) + if db != nil { + db.Close() + } + }) +} + +// Test_Open_InvalidHost tests Open with invalid host +func Test_Open_InvalidHost(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + driver := pgsql.Driver{} + config := &gdb.ConfigNode{ + User: "postgres", + Pass: "12345678", + Host: "invalid_host_that_does_not_exist", + Port: "5432", + Name: "test", + } + // Note: sql.Open doesn't actually connect, so no error here + // The error would occur when actually using the connection + db, err := driver.Open(config) + t.AssertNil(err) + if db != nil { + db.Close() + } + }) +} diff --git a/contrib/drivers/pgsql/pgsql_z_unit_upsert_test.go b/contrib/drivers/pgsql/pgsql_z_unit_upsert_test.go new file mode 100644 index 000000000..a93017e97 --- /dev/null +++ b/contrib/drivers/pgsql/pgsql_z_unit_upsert_test.go @@ -0,0 +1,267 @@ +// 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) + + // Try Save without OnConflict - should fail for pgsql + // PostgreSQL requires OnConflict() for Save() operations, unlike MySQL + _, err = db.Model(table).Data(g.Map{ + "id": 1, + "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") + }) +} diff --git a/database/gdb/gdb.go b/database/gdb/gdb.go index f4260b052..f21b5f307 100644 --- a/database/gdb/gdb.go +++ b/database/gdb/gdb.go @@ -794,22 +794,32 @@ const ( LocalTypeDatetime LocalType = "datetime" LocalTypeInt LocalType = "int" LocalTypeUint LocalType = "uint" + LocalTypeInt32 LocalType = "int32" + LocalTypeUint32 LocalType = "uint32" LocalTypeInt64 LocalType = "int64" LocalTypeUint64 LocalType = "uint64" LocalTypeBigInt LocalType = "bigint" LocalTypeIntSlice LocalType = "[]int" + LocalTypeUintSlice LocalType = "[]uint" + LocalTypeInt32Slice LocalType = "[]int32" + LocalTypeUint32Slice LocalType = "[]uint32" LocalTypeInt64Slice LocalType = "[]int64" LocalTypeUint64Slice LocalType = "[]uint64" LocalTypeStringSlice LocalType = "[]string" - LocalTypeFloat64Slice LocalType = "[]float64" LocalTypeInt64Bytes LocalType = "int64-bytes" LocalTypeUint64Bytes LocalType = "uint64-bytes" LocalTypeFloat32 LocalType = "float32" LocalTypeFloat64 LocalType = "float64" + LocalTypeFloat32Slice LocalType = "[]float32" + LocalTypeFloat64Slice LocalType = "[]float64" LocalTypeBytes LocalType = "[]byte" + LocalTypeBytesSlice LocalType = "[][]byte" LocalTypeBool LocalType = "bool" + LocalTypeBoolSlice LocalType = "[]bool" LocalTypeJson LocalType = "json" LocalTypeJsonb LocalType = "jsonb" + LocalTypeUUID LocalType = "uuid.UUID" + LocalTypeUUIDSlice LocalType = "[]uuid.UUID" ) const ( diff --git a/database/gdb/gdb_core_underlying.go b/database/gdb/gdb_core_underlying.go index 7a3623c62..0f78e25f6 100644 --- a/database/gdb/gdb_core_underlying.go +++ b/database/gdb/gdb_core_underlying.go @@ -501,9 +501,7 @@ func (c *Core) OrderRandomFunction() string { return "RAND()" } -func (c *Core) columnValueToLocalValue( - ctx context.Context, value any, columnType *sql.ColumnType, -) (any, error) { +func (c *Core) columnValueToLocalValue(ctx context.Context, value any, columnType *sql.ColumnType) (any, error) { var scanType = columnType.ScanType() if scanType != nil { // Common basic builtin types. @@ -513,10 +511,7 @@ func (c *Core) columnValueToLocalValue( reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: - return gconv.Convert( - gconv.String(value), - columnType.ScanType().String(), - ), nil + return gconv.Convert(gconv.String(value), scanType.String()), nil default: } } diff --git a/database/gdb/gdb_func.go b/database/gdb/gdb_func.go index ba1418609..66f5dc0fb 100644 --- a/database/gdb/gdb_func.go +++ b/database/gdb/gdb_func.go @@ -719,6 +719,14 @@ func formatWhereKeyValue(in formatWhereKeyValueInput) (newArgs []any) { reflectValue = reflect.ValueOf(in.Value) reflectKind = reflectValue.Kind() ) + // Check if the value implements iString interface (like uuid.UUID). + // These types should be treated as single values, not arrays. + if reflectKind == reflect.Array { + if v, ok := in.Value.(iString); ok { + in.Value = v.String() + reflectKind = reflect.String + } + } switch reflectKind { // Slice argument. case reflect.Slice, reflect.Array: @@ -780,9 +788,7 @@ func formatWhereKeyValue(in formatWhereKeyValueInput) (newArgs []any) { // handleSliceAndStructArgsForSql is an important function, which handles the sql and all its arguments // before committing them to underlying driver. -func handleSliceAndStructArgsForSql( - oldSql string, oldArgs []any, -) (newSql string, newArgs []any) { +func handleSliceAndStructArgsForSql(oldSql string, oldArgs []any) (newSql string, newArgs []any) { newSql = oldSql if len(oldArgs) == 0 { return @@ -800,6 +806,13 @@ func handleSliceAndStructArgsForSql( newArgs = append(newArgs, oldArg) continue } + // It does not split types that implement fmt.Stringer interface (like uuid.UUID). + // These types should be converted to string instead of being expanded as arrays. + // Eg: table.Where("uuid = ?", uuid.UUID{...}) + if v, ok := oldArg.(iString); ok { + newArgs = append(newArgs, v.String()) + continue + } var ( valueHolderCount = gstr.Count(newSql, "?") argSliceLength = argReflectInfo.OriginValue.Len() diff --git a/util/gconv/gconv_slice_bool.go b/util/gconv/gconv_slice_bool.go new file mode 100644 index 000000000..e7b09df1f --- /dev/null +++ b/util/gconv/gconv_slice_bool.go @@ -0,0 +1,20 @@ +// 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 gconv + +// SliceBool is alias of Bools. +func SliceBool(anyInput any) []bool { + return Bools(anyInput) +} + +// Bools converts `any` to []bool. +func Bools(anyInput any) []bool { + result, _ := defaultConverter.SliceBool(anyInput, SliceOption{ + ContinueOnError: true, + }) + return result +} diff --git a/util/gconv/gconv_z_unit_bool_test.go b/util/gconv/gconv_z_unit_bool_test.go index 774af4483..9afa2aafc 100644 --- a/util/gconv/gconv_z_unit_bool_test.go +++ b/util/gconv/gconv_z_unit_bool_test.go @@ -71,3 +71,26 @@ func TestBool(t *testing.T) { } }) } + +func TestBools(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + t.AssertEQ(gconv.Bools(nil), nil) + t.AssertEQ(gconv.Bools([]bool{true, false}), []bool{true, false}) + t.AssertEQ(gconv.Bools([]int{1, 0, 2}), []bool{true, false, true}) + t.AssertEQ(gconv.Bools([]string{"true", "false", "1", "0"}), []bool{true, false, true, false}) + t.AssertEQ(gconv.Bools([]string{"t", "f", "T", "F"}), []bool{true, false, true, false}) + t.AssertEQ(gconv.Bools([]string{"True", "False", "TRUE", "FALSE"}), []bool{true, false, true, false}) + t.AssertEQ(gconv.Bools([]string{"yes", "no", "YES", "NO"}), []bool{true, false, true, false}) + t.AssertEQ(gconv.Bools([]string{"on", "off", "ON", "OFF"}), []bool{true, false, true, false}) + t.AssertEQ(gconv.Bools([]any{true, 0, "false", 1}), []bool{true, false, false, true}) + t.AssertEQ(gconv.Bools(`[true, false, true]`), []bool{true, false, true}) + t.AssertEQ(gconv.Bools(""), []bool{}) + t.AssertEQ(gconv.Bools("true"), []bool{true}) + }) +} + +func TestSliceBool(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + t.AssertEQ(gconv.SliceBool([]bool{true, false}), []bool{true, false}) + }) +} diff --git a/util/gconv/internal/converter/converter_bool.go b/util/gconv/internal/converter/converter_bool.go index ae1cb4afd..60bceda06 100644 --- a/util/gconv/internal/converter/converter_bool.go +++ b/util/gconv/internal/converter/converter_bool.go @@ -8,6 +8,7 @@ package converter import ( "reflect" + "strconv" "strings" "github.com/gogf/gf/v2/internal/empty" @@ -23,11 +24,17 @@ func (c *Converter) Bool(anyInput any) (bool, error) { case bool: return value, nil case []byte: + if parsed, err := strconv.ParseBool(string(value)); err == nil { + return parsed, nil + } if _, ok := emptyStringMap[strings.ToLower(string(value))]; ok { return false, nil } return true, nil case string: + if parsed, err := strconv.ParseBool(value); err == nil { + return parsed, nil + } if _, ok := emptyStringMap[strings.ToLower(value)]; ok { return false, nil } diff --git a/util/gconv/internal/converter/converter_slice_bool.go b/util/gconv/internal/converter/converter_slice_bool.go new file mode 100644 index 000000000..e0f398bd0 --- /dev/null +++ b/util/gconv/internal/converter/converter_slice_bool.go @@ -0,0 +1,173 @@ +// 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 converter + +import ( + "reflect" + + "github.com/gogf/gf/v2/internal/empty" + "github.com/gogf/gf/v2/internal/json" + "github.com/gogf/gf/v2/internal/reflection" + "github.com/gogf/gf/v2/util/gconv/internal/localinterface" +) + +// SliceBool converts `any` to []bool. +func (c *Converter) SliceBool(anyInput any, option ...SliceOption) ([]bool, error) { + if empty.IsNil(anyInput) { + return nil, nil + } + var ( + err error + bb bool + array []bool + sliceOption = c.getSliceOption(option...) + ) + switch value := anyInput.(type) { + case []string: + array = make([]bool, len(value)) + for k, v := range value { + bb, err = c.Bool(v) + if err != nil && !sliceOption.ContinueOnError { + return nil, err + } + array[k] = bb + } + case []int: + array = make([]bool, len(value)) + for k, v := range value { + array[k] = v != 0 + } + case []int8: + array = make([]bool, len(value)) + for k, v := range value { + array[k] = v != 0 + } + case []int16: + array = make([]bool, len(value)) + for k, v := range value { + array[k] = v != 0 + } + case []int32: + array = make([]bool, len(value)) + for k, v := range value { + array[k] = v != 0 + } + case []int64: + array = make([]bool, len(value)) + for k, v := range value { + array[k] = v != 0 + } + case []uint: + array = make([]bool, len(value)) + for k, v := range value { + array[k] = v != 0 + } + case []uint8: + if json.Valid(value) { + if err = json.UnmarshalUseNumber(value, &array); array != nil { + return array, err + } + } + array = make([]bool, len(value)) + for k, v := range value { + array[k] = v != 0 + } + case []uint16: + array = make([]bool, len(value)) + for k, v := range value { + array[k] = v != 0 + } + case []uint32: + array = make([]bool, len(value)) + for k, v := range value { + array[k] = v != 0 + } + case []uint64: + array = make([]bool, len(value)) + for k, v := range value { + array[k] = v != 0 + } + case []bool: + array = value + case []float32: + array = make([]bool, len(value)) + for k, v := range value { + array[k] = v != 0 + } + case []float64: + array = make([]bool, len(value)) + for k, v := range value { + array[k] = v != 0 + } + case []any: + array = make([]bool, len(value)) + for k, v := range value { + bb, err = c.Bool(v) + if err != nil && !sliceOption.ContinueOnError { + return nil, err + } + array[k] = bb + } + case [][]byte: + array = make([]bool, len(value)) + for k, v := range value { + bb, err = c.Bool(v) + if err != nil && !sliceOption.ContinueOnError { + return nil, err + } + array[k] = bb + } + case string: + byteValue := []byte(value) + if json.Valid(byteValue) { + if err = json.UnmarshalUseNumber(byteValue, &array); array != nil { + return array, err + } + } + if value == "" { + return []bool{}, err + } + bb, err = c.Bool(value) + if err != nil && !sliceOption.ContinueOnError { + return nil, err + } + return []bool{bb}, err + } + if array != nil { + return array, err + } + if v, ok := anyInput.(localinterface.IInterfaces); ok { + return c.SliceBool(v.Interfaces(), option...) + } + // Not a common type, it then uses reflection for conversion. + originValueAndKind := reflection.OriginValueAndKind(anyInput) + switch originValueAndKind.OriginKind { + case reflect.Slice, reflect.Array: + var ( + length = originValueAndKind.OriginValue.Len() + slice = make([]bool, length) + ) + for i := 0; i < length; i++ { + bb, err = c.Bool(originValueAndKind.OriginValue.Index(i).Interface()) + if err != nil && !sliceOption.ContinueOnError { + return nil, err + } + slice[i] = bb + } + return slice, err + + default: + if originValueAndKind.OriginValue.IsZero() { + return []bool{}, err + } + bb, err = c.Bool(anyInput) + if err != nil && !sliceOption.ContinueOnError { + return nil, err + } + return []bool{bb}, err + } +}