Skip to content

Commit 15a81a7

Browse files
feat: time and time64 column support
1 parent 2482bad commit 15a81a7

File tree

8 files changed

+1548
-18
lines changed

8 files changed

+1548
-18
lines changed

TYPES.md

Lines changed: 50 additions & 10 deletions
Large diffs are not rendered by default.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.23.0
55
toolchain go1.24.1
66

77
require (
8-
github.com/ClickHouse/ch-go v0.66.1
8+
github.com/ClickHouse/ch-go v0.66.2-0.20250716011122-97e525b8cb83
99
github.com/andybalholm/brotli v1.2.0
1010
github.com/docker/docker v28.3.2+incompatible
1111
github.com/docker/go-units v0.5.0

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af
44
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
55
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
66
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
7-
github.com/ClickHouse/ch-go v0.66.1 h1:LQHFslfVYZsISOY0dnOYOXGkOUvpv376CCm8g7W74A4=
8-
github.com/ClickHouse/ch-go v0.66.1/go.mod h1:NEYcg3aOFv2EmTJfo4m2WF7sHB/YFbLUuIWv9iq76xY=
7+
github.com/ClickHouse/ch-go v0.66.2-0.20250716011122-97e525b8cb83 h1:9EI1ln/5ilLTH7ExWFZtaHrQ+AXenyJoloSxDyycnuw=
8+
github.com/ClickHouse/ch-go v0.66.2-0.20250716011122-97e525b8cb83/go.mod h1:LaCKZZLe4gr9VFJk+mPgPJhBzt0R5qEaogaLhK8faO8=
99
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
1010
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
1111
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
@@ -173,8 +173,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMey
173173
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
174174
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
175175
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
176-
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
177-
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
176+
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
177+
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
178178
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
179179
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
180180
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=

lib/column/time.go

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
// Licensed to ClickHouse, Inc. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. ClickHouse, Inc. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package column
19+
20+
import (
21+
"database/sql"
22+
"database/sql/driver"
23+
"fmt"
24+
"reflect"
25+
"strconv"
26+
"strings"
27+
"time"
28+
29+
"github.com/ClickHouse/ch-go/proto"
30+
"github.com/ClickHouse/clickhouse-go/v2/lib/timezone"
31+
)
32+
33+
const defaultTimeFormat = "15:04:05"
34+
35+
type Time struct {
36+
chType Type
37+
timezone *time.Location
38+
name string
39+
col proto.ColTime
40+
}
41+
42+
func (col *Time) Reset() {
43+
col.col.Reset()
44+
}
45+
46+
func (col *Time) Name() string {
47+
return col.name
48+
}
49+
50+
func (col *Time) parse(t Type, tz *time.Location) (_ Interface, err error) {
51+
col.chType = t
52+
if strings.HasPrefix(string(t), "Time('") {
53+
timezoneName := strings.TrimSuffix(strings.TrimPrefix(string(t), "Time('"), "')")
54+
timezone, err := timezone.Load(timezoneName)
55+
if err != nil {
56+
return nil, err
57+
}
58+
col.timezone = timezone
59+
return col, nil
60+
}
61+
if string(t) == "Time" {
62+
col.timezone = tz
63+
return col, nil
64+
}
65+
return nil, &UnsupportedColumnTypeError{t: t}
66+
}
67+
68+
func (col *Time) Type() Type {
69+
return col.chType
70+
}
71+
72+
func (col *Time) ScanType() reflect.Type {
73+
return scanTypeTime
74+
}
75+
76+
func (col *Time) Rows() int {
77+
return col.col.Rows()
78+
}
79+
80+
func (col *Time) Row(i int, ptr bool) any {
81+
value := col.row(i)
82+
if ptr {
83+
return &value
84+
}
85+
return value
86+
}
87+
88+
func (col *Time) ScanRow(dest any, row int) error {
89+
switch d := dest.(type) {
90+
case *time.Time:
91+
*d = col.row(row)
92+
case **time.Time:
93+
*d = new(time.Time)
94+
**d = col.row(row)
95+
case *int64:
96+
t := col.row(row)
97+
*d = int64(t.Hour()*3600 + t.Minute()*60 + t.Second())
98+
case **int64:
99+
*d = new(int64)
100+
t := col.row(row)
101+
**d = int64(t.Hour()*3600 + t.Minute()*60 + t.Second())
102+
case *sql.NullTime:
103+
return d.Scan(col.row(row))
104+
case *string:
105+
*d = col.row(row).Format(defaultTimeFormat)
106+
case **string:
107+
*d = new(string)
108+
**d = col.row(row).Format(defaultTimeFormat)
109+
default:
110+
if scan, ok := dest.(sql.Scanner); ok {
111+
return scan.Scan(col.row(row))
112+
}
113+
return &ColumnConverterError{
114+
Op: "ScanRow",
115+
To: fmt.Sprintf("%T", dest),
116+
From: "Time",
117+
}
118+
}
119+
return nil
120+
}
121+
122+
func (col *Time) Append(v any) (nulls []uint8, err error) {
123+
switch v := v.(type) {
124+
case []int64:
125+
nulls = make([]uint8, len(v))
126+
for i := range v {
127+
seconds := v[i]
128+
hours := seconds / 3600
129+
minutes := (seconds % 3600) / 60
130+
secs := seconds % 60
131+
col.col.Append(proto.FromTime32(time.Date(1970, 1, 1, int(hours), int(minutes), int(secs), 0, time.UTC)))
132+
}
133+
case []*int64:
134+
nulls = make([]uint8, len(v))
135+
for i := range v {
136+
switch {
137+
case v[i] != nil:
138+
seconds := *v[i]
139+
hours := seconds / 3600
140+
minutes := (seconds % 3600) / 60
141+
secs := seconds % 60
142+
col.col.Append(proto.FromTime32(time.Date(1970, 1, 1, int(hours), int(minutes), int(secs), 0, time.UTC)))
143+
default:
144+
col.col.Append(proto.FromTime32(time.Time{}))
145+
nulls[i] = 1
146+
}
147+
}
148+
case []time.Time:
149+
nulls = make([]uint8, len(v))
150+
for i := range v {
151+
col.col.Append(proto.FromTime32(v[i]))
152+
}
153+
case []*time.Time:
154+
nulls = make([]uint8, len(v))
155+
for i := range v {
156+
switch {
157+
case v[i] != nil:
158+
col.col.Append(proto.FromTime32(*v[i]))
159+
default:
160+
col.col.Append(proto.FromTime32(time.Time{}))
161+
nulls[i] = 1
162+
}
163+
}
164+
case []sql.NullTime:
165+
nulls = make([]uint8, len(v))
166+
for i := range v {
167+
col.AppendRow(v[i])
168+
}
169+
case []*sql.NullTime:
170+
nulls = make([]uint8, len(v))
171+
for i := range v {
172+
if v[i] == nil {
173+
nulls[i] = 1
174+
}
175+
col.AppendRow(v[i])
176+
}
177+
case []string:
178+
nulls = make([]uint8, len(v))
179+
for i := range v {
180+
value, err := col.parseTime(v[i])
181+
if err != nil {
182+
return nil, err
183+
}
184+
col.col.Append(proto.FromTime32(value))
185+
}
186+
default:
187+
if valuer, ok := v.(driver.Valuer); ok {
188+
val, err := valuer.Value()
189+
if err != nil {
190+
return nil, &ColumnConverterError{
191+
Op: "Append",
192+
To: "Time",
193+
From: fmt.Sprintf("%T", v),
194+
Hint: "could not get driver.Valuer value",
195+
}
196+
}
197+
return col.Append(val)
198+
}
199+
return nil, &ColumnConverterError{
200+
Op: "Append",
201+
To: "Time",
202+
From: fmt.Sprintf("%T", v),
203+
}
204+
}
205+
return
206+
}
207+
208+
func (col *Time) AppendRow(v any) error {
209+
switch v := v.(type) {
210+
case int64:
211+
seconds := v
212+
hours := seconds / 3600
213+
minutes := (seconds % 3600) / 60
214+
secs := seconds % 60
215+
col.col.Append(proto.FromTime32(time.Date(1970, 1, 1, int(hours), int(minutes), int(secs), 0, time.UTC)))
216+
case *int64:
217+
switch {
218+
case v != nil:
219+
seconds := *v
220+
hours := seconds / 3600
221+
minutes := (seconds % 3600) / 60
222+
secs := seconds % 60
223+
col.col.Append(proto.FromTime32(time.Date(1970, 1, 1, int(hours), int(minutes), int(secs), 0, time.UTC)))
224+
default:
225+
col.col.Append(proto.FromTime32(time.Time{}))
226+
}
227+
case time.Time:
228+
col.col.Append(proto.FromTime32(v))
229+
case *time.Time:
230+
switch {
231+
case v != nil:
232+
col.col.Append(proto.FromTime32(*v))
233+
default:
234+
col.col.Append(proto.FromTime32(time.Time{}))
235+
}
236+
case sql.NullTime:
237+
if v.Valid {
238+
col.col.Append(proto.FromTime32(v.Time))
239+
} else {
240+
col.col.Append(proto.FromTime32(time.Time{}))
241+
}
242+
case *sql.NullTime:
243+
switch {
244+
case v != nil && v.Valid:
245+
col.col.Append(proto.FromTime32(v.Time))
246+
default:
247+
col.col.Append(proto.FromTime32(time.Time{}))
248+
}
249+
case string:
250+
value, err := col.parseTime(v)
251+
if err != nil {
252+
return err
253+
}
254+
col.col.Append(proto.FromTime32(value))
255+
case *string:
256+
switch {
257+
case v != nil:
258+
value, err := col.parseTime(*v)
259+
if err != nil {
260+
return err
261+
}
262+
col.col.Append(proto.FromTime32(value))
263+
default:
264+
col.col.Append(proto.FromTime32(time.Time{}))
265+
}
266+
default:
267+
if valuer, ok := v.(driver.Valuer); ok {
268+
val, err := valuer.Value()
269+
if err != nil {
270+
return &ColumnConverterError{
271+
Op: "AppendRow",
272+
To: "Time",
273+
From: fmt.Sprintf("%T", v),
274+
Hint: "could not get driver.Valuer value",
275+
}
276+
}
277+
return col.AppendRow(val)
278+
}
279+
return &ColumnConverterError{
280+
Op: "AppendRow",
281+
To: "Time",
282+
From: fmt.Sprintf("%T", v),
283+
}
284+
}
285+
return nil
286+
}
287+
288+
func (col *Time) Decode(reader *proto.Reader, rows int) error {
289+
return col.col.DecodeColumn(reader, rows)
290+
}
291+
292+
func (col *Time) Encode(buffer *proto.Buffer) {
293+
col.col.EncodeColumn(buffer)
294+
}
295+
296+
func (col *Time) row(i int) time.Time {
297+
return col.col.Row(i).ToTime32()
298+
}
299+
300+
func (col *Time) parseTime(value string) (tv time.Time, err error) {
301+
value = strings.TrimSpace(value)
302+
if value == "" {
303+
return time.Time{}, nil
304+
}
305+
306+
formats := []string{
307+
"15:04:05",
308+
"15:04",
309+
"15:04:05.999",
310+
"15:04:05.999999",
311+
"15:04:05.999999999",
312+
"3:04:05 PM",
313+
"3:04 PM",
314+
"15:04:05 -07:00",
315+
"15:04:05.999 -07:00",
316+
}
317+
318+
for _, format := range formats {
319+
if tv, err = time.Parse(format, value); err == nil {
320+
timezone := time.UTC
321+
if col.timezone != nil {
322+
timezone = col.timezone
323+
}
324+
return time.Date(1970, 1, 1, tv.Hour(), tv.Minute(), tv.Second(), tv.Nanosecond(), timezone), nil
325+
}
326+
}
327+
328+
if seconds, err := strconv.ParseInt(value, 10, 64); err == nil {
329+
hours := seconds / 3600
330+
minutes := (seconds % 3600) / 60
331+
secs := seconds % 60
332+
timezone := time.UTC
333+
if col.timezone != nil {
334+
timezone = col.timezone
335+
}
336+
return time.Date(1970, 1, 1, int(hours), int(minutes), int(secs), 0, timezone), nil
337+
}
338+
339+
return time.Time{}, fmt.Errorf("cannot parse time value: %s", value)
340+
}
341+
342+
func (col *Time) WriteStatePrefix(buffer *proto.Buffer) error {
343+
return nil
344+
}
345+
346+
func (col *Time) ReadStatePrefix(reader *proto.Reader) error {
347+
return nil
348+
}

0 commit comments

Comments
 (0)