From fe0f5874b4c679795dd1f285d1c6dcf0251e7bdc Mon Sep 17 00:00:00 2001 From: Yang Xiufeng Date: Mon, 22 Dec 2025 02:27:27 +0800 Subject: [PATCH] feat: support timestamp_tz, --- columntype.go | 27 +++++++++++++++++++++ tests/type_test.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/columntype.go b/columntype.go index 9d89654..3f64a70 100644 --- a/columntype.go +++ b/columntype.go @@ -134,6 +134,31 @@ func (c timestampColumnType) Desc() *TypeDesc { return &TypeDesc{Name: "Timestamp", Nullable: bool(c.isNullable)} } +type timestampTzColumnType struct { + columnTypeDefault + isNullable +} + +func (c timestampTzColumnType) Parse(s string) (driver.Value, error) { + if c.checkNull(s) { + return nil, nil + } + println(s) + return time.Parse("2006-01-02 15:04:05.999999 -0700", s) +} + +func (c timestampTzColumnType) ScanType() reflect.Type { + return reflectTypeTime +} + +func (c timestampTzColumnType) DatabaseTypeName() string { + return c.wrapName("Timestamp_Tz") +} + +func (c timestampTzColumnType) Desc() *TypeDesc { + return &TypeDesc{Name: "Timestamp_Tz", Nullable: bool(c.isNullable)} +} + type dateColumnType struct { columnTypeDefault isNullable @@ -224,6 +249,8 @@ func NewColumnType(dbType string, opts *ColumnTypeOptions) (ColumnType, error) { return &simpleColumnType{dbType: nullable.wrapName(desc.Name), scanType: reflectTypeFloat64, nullable: desc.Nullable, parseNull: parseNull}, nil case "Timestamp": return ×tampColumnType{isNullable: nullable, tz: opts.timezone}, nil + case "Timestamp_Tz": + return ×tampTzColumnType{isNullable: nullable}, nil case "Date": return &dateColumnType{isNullable: nullable}, nil case "Decimal": diff --git a/tests/type_test.go b/tests/type_test.go index 9de8c45..472f427 100644 --- a/tests/type_test.go +++ b/tests/type_test.go @@ -137,3 +137,61 @@ func (s *DatabendTestSuite) TestTimestamp() { }) } } + +func (s *DatabendTestSuite) TestTimestampTz() { + if semver.Compare(driverVersion, "v0.9.0") <= 0 || semver.Compare(serverVersion, "1.2.844") < 0 { + return + } + + type testCase struct { + name string + // scan should get Time with location in settings + setting *time.Location + data *time.Location + } + locShanghai, _ := time.LoadLocation("Asia/Shanghai") + locLos, _ := time.LoadLocation("America/Los_Angeles") + input := time.Date(2025, 1, 16, 2, 1, 26, 739219000, locLos) + + testCases := []testCase{ + {name: "1", setting: time.UTC}, + {name: "2", setting: locShanghai}, + } + + for i, tc := range testCases { + s.Run(tc.name, func() { + db := sql.OpenDB(s.cfg) + defer db.Close() + + tableName := fmt.Sprintf("test_tz_%s_%d", s.table, i) + result, err := db.Exec(fmt.Sprintf("create or replace table %s (t Timestamp_TZ)", tableName)) + s.r.NoError(err) + + result, err = db.Exec(fmt.Sprintf("set timezone='%v'", tc.setting.String())) + s.r.NoError(err) + + insertSQL := fmt.Sprintf("INSERT INTO %s VALUES (?)", tableName) + result, err = db.Exec(insertSQL, input) + s.r.NoError(err) + n, err := result.RowsAffected() + s.r.NoError(err) + s.r.Equal(int64(1), n) + + selectSQL := fmt.Sprintf("select * from %s", tableName) + rows, err := db.Query(selectSQL) + s.r.NoError(err) + s.r.True(rows.Next()) + s.r.NoError(err) + + var output time.Time + err = rows.Scan(&output) + s.r.NoError(err) + + s.r.Equal(input.UnixMicro(), output.UnixMicro()) + name, offset := output.Zone() + s.r.Equal(-8*3600, offset) + s.r.Equal("", name) + s.r.NoError(rows.Close()) + }) + } +}