diff --git a/README.md b/README.md index d53d031..221a672 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Main features are: - [Bun](https://github.com/uptrace/bun/)-like query builder. - [Selecting](https://clickhouse.uptrace.dev/guide/clickhouse-select.html) into scalars, structs, maps, slices of maps/structs/scalars. +- `Date`, `DateTime`, and `DateTime64`. - `Array(T)` including nested arrays. - Enums and `LowCardinality(String)`. - `Nullable(T)` except `Nullable(Array(T))`. @@ -28,10 +29,6 @@ Main features are: support. - In production at [Uptrace](https://uptrace.dev/) -Unsupported: - -- `DateTime64` - Resources: - [**Get started**](https://clickhouse.uptrace.dev/guide/getting-started.html) diff --git a/ch/chschema/column.go b/ch/chschema/column.go index 958ee3b..6f60602 100644 --- a/ch/chschema/column.go +++ b/ch/chschema/column.go @@ -855,6 +855,54 @@ func (c DateTimeColumn) WriteTo(wr *chproto.Writer) error { //------------------------------------------------------------------------------ +type DateTime64Column struct { + ColumnOf[time.Time] + prec int +} + +var _ Columnar = (*DateTime64Column)(nil) + +func NewDateTime64Column(typ reflect.Type, chType string, numRow int) Columnar { + return &DateTime64Column{ + ColumnOf: NewColumnOf[time.Time](numRow), + prec: parseDateTime64Prec(chType), + } +} + +func (c *DateTime64Column) Type() reflect.Type { + return timeType +} + +func (c *DateTime64Column) ConvertAssign(idx int, v reflect.Value) error { + v.Set(reflect.ValueOf(c.Column[idx])) + return nil +} + +func (c *DateTime64Column) ReadFrom(rd *chproto.Reader, numRow int) error { + c.Alloc(numRow) + + mul := int64(math.Pow10(9 - c.prec)) + for i := range c.Column { + n, err := rd.Int64() + if err != nil { + return err + } + c.Column[i] = time.Unix(0, n*mul) + } + + return nil +} + +func (c *DateTime64Column) WriteTo(wr *chproto.Writer) error { + div := int64(math.Pow10(9 - c.prec)) + for i := range c.Column { + wr.Int64(c.Column[i].UnixNano() / div) + } + return nil +} + +//------------------------------------------------------------------------------ + type Int64TimeColumn struct { Int64Column } diff --git a/ch/chschema/types.go b/ch/chschema/types.go index 089cd33..3f848e7 100644 --- a/ch/chschema/types.go +++ b/ch/chschema/types.go @@ -4,6 +4,7 @@ import ( "fmt" "net" "reflect" + "strconv" "strings" "time" @@ -132,6 +133,9 @@ func ColumnFactory(typ reflect.Type, chType string) NewColumnFunc { if s := enumType(chType); s != "" { return NewEnumColumn } + if isDateTime64Type(chType) { + return NewDateTime64Column + } if strings.HasPrefix(chType, "SimpleAggregateFunction(") { chType = chSubType(chType, "SimpleAggregateFunction(") @@ -236,6 +240,8 @@ func columnFromCHType(chType string) NewColumnFunc { return NewFloat64Column case chtype.DateTime: return NewDateTimeColumn + case chtype.DateTime64: + return NewDateTime64Column case chtype.Date: return NewDateColumn case chtype.IPv6: @@ -258,13 +264,12 @@ var ( float32Type = reflect.TypeOf(float32(0)) float64Type = reflect.TypeOf(float64(0)) - stringType = reflect.TypeOf("") - bytesType = reflect.TypeOf((*[]byte)(nil)).Elem() - uuidType = reflect.TypeOf((*UUID)(nil)).Elem() - timeType = reflect.TypeOf((*time.Time)(nil)).Elem() - ipType = reflect.TypeOf((*net.IP)(nil)).Elem() - ipNetType = reflect.TypeOf((*net.IPNet)(nil)).Elem() - bfloat16HistType = reflect.TypeOf((*map[chtype.BFloat16]uint64)(nil)).Elem() + stringType = reflect.TypeOf("") + bytesType = reflect.TypeOf((*[]byte)(nil)).Elem() + uuidType = reflect.TypeOf((*UUID)(nil)).Elem() + timeType = reflect.TypeOf((*time.Time)(nil)).Elem() + ipType = reflect.TypeOf((*net.IP)(nil)).Elem() + ipNetType = reflect.TypeOf((*net.IPNet)(nil)).Elem() int64SliceType = reflect.TypeOf((*[]int64)(nil)).Elem() uint64SliceType = reflect.TypeOf((*[]uint64)(nil)).Elem() @@ -318,6 +323,9 @@ func goType(chType string) reflect.Type { if s := dateTimeType(chType); s != "" { return timeType } + if isDateTime64Type(chType) { + return timeType + } if s := nullableType(chType); s != "" { return reflect.PtrTo(goType(s)) } @@ -366,6 +374,22 @@ func dateTimeType(s string) string { return chtype.DateTime } +func isDateTime64Type(s string) bool { + return chSubType(s, "DateTime64(") != "" +} + +func parseDateTime64Prec(s string) int { + s = chSubType(s, "DateTime64(") + if s == "" { + return 0 + } + prec, err := strconv.Atoi(s) + if err != nil { + return 0 + } + return prec +} + func nullableType(s string) string { return chSubType(s, "Nullable(") } diff --git a/ch/chtype/chtype.go b/ch/chtype/chtype.go index 2f6ac8d..c9e03b6 100644 --- a/ch/chtype/chtype.go +++ b/ch/chtype/chtype.go @@ -1,20 +1,21 @@ package chtype const ( - Any = "_" // for decoding into interface{} - String = "String" - UUID = "UUID" - Int8 = "Int8" - Int16 = "Int16" - Int32 = "Int32" - Int64 = "Int64" - UInt8 = "UInt8" - UInt16 = "UInt16" - UInt32 = "UInt32" - UInt64 = "UInt64" - Float32 = "Float32" - Float64 = "Float64" - DateTime = "DateTime" - Date = "Date" - IPv6 = "IPv6" + Any = "_" // for decoding into interface{} + String = "String" + UUID = "UUID" + Int8 = "Int8" + Int16 = "Int16" + Int32 = "Int32" + Int64 = "Int64" + UInt8 = "UInt8" + UInt16 = "UInt16" + UInt32 = "UInt32" + UInt64 = "UInt64" + Float32 = "Float32" + Float64 = "Float64" + DateTime = "DateTime" + DateTime64 = "DateTime64" + Date = "Date" + IPv6 = "IPv6" ) diff --git a/ch/chtype/chtype_uptrace.go b/ch/chtype/chtype_uptrace.go deleted file mode 100644 index 2778414..0000000 --- a/ch/chtype/chtype_uptrace.go +++ /dev/null @@ -1,13 +0,0 @@ -package chtype - -import "math" - -type BFloat16 uint16 - -func ToBFloat16(f float64) BFloat16 { - return BFloat16(math.Float32bits(float32(f)) >> 16) -} - -func (f BFloat16) Float32() float32 { - return math.Float32frombits(uint32(f) << 16) -} diff --git a/ch/db_test.go b/ch/db_test.go index ae63501..7989776 100644 --- a/ch/db_test.go +++ b/ch/db_test.go @@ -244,6 +244,29 @@ func TestScanArrayUint8(t *testing.T) { require.Equal(t, map[string]any{"ns": []uint8{0, 1, 2}}, m) } +func TestDateTime64(t *testing.T) { + type Model struct { + Time time.Time `ch:"type:DateTime64(9)"` + } + + ctx := context.Background() + + db := chDB() + defer db.Close() + + err := db.ResetModel(ctx, (*Model)(nil)) + require.NoError(t, err) + + in := &Model{Time: time.Unix(0, 12345678912345)} + _, err = db.NewInsert().Model(in).Exec(ctx) + require.NoError(t, err) + + out := new(Model) + err = db.NewSelect().Model(out).Scan(ctx) + require.NoError(t, err) + require.Equal(t, in.Time.UnixNano(), out.Time.UnixNano()) +} + type Event struct { ch.CHModel `ch:"goch_events,partition:toYYYYMM(created_at)"` diff --git a/package.json b/package.json index e1a10d9..7ca66f7 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "uptrace/go-clickhouse", + "name": "goclickhouse", "version": "0.2.5", "main": "index.js", "repository": "git@github.com:uptrace/go-clickhouse.git",