Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: type-safe conversion for line-protocol Addfield #43 #42

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## 0.4.0 [unreleased]

### Features

1. [#42](https://github.com/InfluxCommunity/influxdb3-go/pull/42): Add type-safe conversion for line-protocol

## 0.3.0 [2023-10-02]

### Features
Expand Down
50 changes: 50 additions & 0 deletions influxdb3/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
The MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

package influxdb3

import (
"fmt"
"time"

"github.com/influxdata/line-protocol/v2/lineprotocol"
)

func ExamplePoint_AddFieldFromValue() {
p := NewPoint("measurement", map[string]string{}, map[string]interface{}{}, time.Now())
p.AddFieldFromValue("hello", NewValueFromString("world"))
p.AddFieldFromValue("float", NewValueFromFloat(55.101))
p.AddFieldFromValue("time.Time", NewValueFromTime(time.Date(2020, time.March, 20, 10, 30, 23, 123456789, time.UTC)))
p.SetTimestamp(time.Date(2020, time.March, 20, 10, 30, 23, 123456789, time.UTC))
line, _ := p.MarshalBinary(lineprotocol.Nanosecond)
fmt.Println(string(line))
// Output: measurement float=55.101,hello="world",time.Time="2020-03-20T10:30:23.123456789Z" 1584700223123456789
}

func ExampleNewValueFromStringer() {
p := NewPoint("measurement", map[string]string{}, map[string]interface{}{}, time.Now())
p.AddFieldFromValue("Supports time.Duration", NewValueFromStringer(4*time.Hour))
p.SetTimestamp(time.Date(2020, time.March, 20, 10, 30, 23, 123456789, time.UTC))
line, _ := p.MarshalBinary(lineprotocol.Nanosecond)
fmt.Println(string(line))
// Output: measurement Supports\ time.Duration="4h0m0s" 1584700223123456789
}
22 changes: 22 additions & 0 deletions influxdb3/point.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,28 @@ func (m *Point) AddField(k string, v interface{}) *Point {
return m
}

// AddFieldFromValue adds a [lineprotocol.Value] to the Point.
//
// Parameters:
// - k: The key of the field.
// - v: The value of the line protocol format.
//
// Returns:
// - The updated Point with the field added.
//
// [lineprotocol.Value]: https://pkg.go.dev/github.com/influxdata/line-protocol/v2/lineprotocol#Value
func (m *Point) AddFieldFromValue(k string, v lineprotocol.Value) *Point {
for i, field := range m.Fields {
if k == field.Key {
m.Fields[i].Value = v
return m
}
}

m.Fields = append(m.Fields, Field{Key: k, Value: v})
return m
}

// AddField adds a field to the Point.
//
// Parameters:
Expand Down
49 changes: 49 additions & 0 deletions influxdb3/point_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ type st struct {
b bool
}

type tfloat64u float64
type tint64u int64
type tuint64u uint64
type tstringu string
type tbytesu []byte
type tboolu bool

func (s st) String() string {
return fmt.Sprintf("%.2f d %v", s.d, s.b)
}
Expand Down Expand Up @@ -129,3 +136,45 @@ func TestPoint(t *testing.T) {
require.NoError(t, err)
assert.EqualValues(t, `test,host"name=ho\st\ "a",id=10ad\=,ven\=dor=GCP,x\"\ x=a\ b "string"="six, \"seven\", eight",bo\ol=false,duration="4h24m3s",float32=80,float64=80.1234567,int=-1234567890i,int16=-3456i,int32=-34567i,int64=-1234567890i,int8=-34i,stri\=ng="six=seven\\, eight",time="2020-03-20T10:30:23.123456789Z",uint=12345677890u,uint\ 64=41234567890u,uint16=3456u,uint32=345780u,uint8=34u 60000000070`+"\n", string(line))
}

func TestAddFieldFromValue(t *testing.T) {
p := NewPoint(
"test",
map[string]string{
"id": "10ad=",
"ven=dor": "AWS",
`host"name`: `ho\st "a"`,
`x\" x`: "a b",
},
map[string]interface{}{},
time.Unix(60, 70))

p.AddFieldFromValue("float64", NewValueFromFloat(tfloat64u(80.1234567)))
p.AddFieldFromValue("float32", NewValueFromFloat(float32(80.0)))
p.AddFieldFromValue("int", NewValueFromInt(-1234567890))
p.AddFieldFromValue("int8", NewValueFromInt(int8(-34)))
p.AddFieldFromValue("int16", NewValueFromInt(int16(-3456)))
p.AddFieldFromValue("int32", NewValueFromInt(int32(-34567)))
p.AddFieldFromValue("int64", NewValueFromInt(tint64u(-1234567890)))
p.AddFieldFromValue("uint", NewValueFromUInt(uint(12345677890)))
p.AddFieldFromValue("uint8", NewValueFromUInt(uint8(34)))
p.AddFieldFromValue("uint16", NewValueFromUInt(uint16(3456)))
p.AddFieldFromValue("uint32", NewValueFromUInt(uint32(34578)))
p.AddFieldFromValue("uint 64", NewValueFromUInt(tuint64u(41234567890)))
p.AddFieldFromValue("bo\\ol", NewValueFromBoolean(tboolu(false)))
p.AddFieldFromValue(`"string"`, NewValueFromString(tstringu(`six, "seven", eight`)))
p.AddFieldFromValue("stri=ng", NewValueFromString(tbytesu([]byte(`six=seven\, eight`))))
p.AddFieldFromValue("time", NewValueFromTime(time.Date(2020, time.March, 20, 10, 30, 23, 123456789, time.UTC)))
p.AddFieldFromValue("duration", NewValueFromStringer(4*time.Hour+24*time.Minute+3*time.Second))

// Test duplicate tag and duplicate field
p.AddTag("ven=dor", "GCP").AddField("uint32", uint32(345780))

line, err := p.MarshalBinary(lineprotocol.Nanosecond)
require.NoError(t, err)
assert.EqualValues(t, `test,host"name=ho\st\ "a",id=10ad\=,ven\=dor=GCP,x\"\ x=a\ b "string"="six, \"seven\", eight",bo\ol=false,duration="4h24m3s",float32=80,float64=80.1234567,int=-1234567890i,int16=-3456i,int32=-34567i,int64=-1234567890i,int8=-34i,stri\=ng="six=seven\\, eight",time="2020-03-20T10:30:23.123456789Z",uint=12345677890u,uint\ 64=41234567890u,uint16=3456u,uint32=345780u,uint8=34u 60000000070`+"\n", string(line))

assert.PanicsWithError(t, "invalid float value for NewValueFromFloat: float64 (+Inf)", func() { NewValueFromFloat(math.Inf(1)) })
assert.PanicsWithError(t, "invalid float value for NewValueFromFloat: float64 (-Inf)", func() { NewValueFromFloat(math.Inf(-1)) })
assert.PanicsWithError(t, "invalid utf-8 string value for NewValueFromString: string (\"\\xed\\x9f\\xc1\")", func() { NewValueFromString(string([]byte{237, 159, 193})) })
}
186 changes: 186 additions & 0 deletions influxdb3/value.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*
The MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

package influxdb3

import (
"fmt"
"time"

"github.com/influxdata/line-protocol/v2/lineprotocol"
)

// NativeType are unions of type sets that can converted to [lineprotocol.NewValue].
//
// [lineprotocol.NewValue]: https://pkg.go.dev/github.com/influxdata/line-protocol/v2/lineprotocol#NewValue
type NativeType interface {
float64 | int64 | uint64 | string | []byte | bool
}

// Float [Float] is IEEE-754 64-bit floating-point numbers. Default numerical type. InfluxDB supports scientific notation in float field values.
//
// [Float]: https://docs.influxdata.com/influxdb/cloud-serverless/reference/syntax/line-protocol/#float
type Float interface {
~float32 | ~float64
}

// Integer [Integer] is signed 64-bit integers.
//
// [Integer]: https://docs.influxdata.com/influxdb/cloud-serverless/reference/syntax/line-protocol/#integer
type Integer interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}

// UInteger [UInteger] is unsigned 64-bit integers.
//
// [UInteger]: https://docs.influxdata.com/influxdb/cloud-serverless/reference/syntax/line-protocol/#uinteger
type UInteger interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}

// String [String] is plain text string. Length limit 64KB.
//
// [String]: https://docs.influxdata.com/influxdb/cloud-serverless/reference/syntax/line-protocol/#string
type String interface {
~string | ~[]byte
}

// Boolean [Boolean] is true or false values.
//
// [Boolean]: https://docs.influxdata.com/influxdb/cloud-serverless/reference/syntax/line-protocol/#boolean
type Boolean interface {
~bool
}

// NewValueFromNative is a convenient function for creating a [lineprotocol.Value] from NativeType.
//
// Parameters:
// - v: The value of the field value.
//
// Returns:
// - The created [lineprotocol.Value].
//
// [lineprotocol.Value]: https://pkg.go.dev/github.com/influxdata/line-protocol/v2/lineprotocol#Value
func NewValueFromNative[N NativeType](v N) lineprotocol.Value {
return lineprotocol.MustNewValue(v)
}

// NewValueFromFloat is a convenient function for creating a [lineprotocol.Value] from Float.
// Non-finite floating-point field values (+/- infinity and NaN from IEEE 754) are not currently supported.
//
// Parameters:
// - v: The value of the Float value.
//
// Returns:
// - The created [lineprotocol.Value].
//
// [lineprotocol.Value]: https://pkg.go.dev/github.com/influxdata/line-protocol/v2/lineprotocol#Value
func NewValueFromFloat[F Float](v F) lineprotocol.Value {
data, ok := lineprotocol.FloatValue(float64(v))
if !ok {
panic(fmt.Errorf("invalid float value for NewValueFromFloat: %T (%#v)", v, v))
}
return data
}

// NewValueFromInt is a convenient function for creating a [lineprotocol.Value] from Integer.
//
// Parameters:
// - v: The value of the Integer value.
//
// Returns:
// - The created [lineprotocol.Value].
//
// [lineprotocol.Value]: https://pkg.go.dev/github.com/influxdata/line-protocol/v2/lineprotocol#Value
func NewValueFromInt[I Integer](v I) lineprotocol.Value {
return lineprotocol.IntValue(int64(v))
}

// NewValueFromUInt is a convenient function for creating a [lineprotocol.Value] from UInteger.
//
// Parameters:
// - v: The value of the UInteger value.
//
// Returns:
// - The created [lineprotocol.Value].
//
// [lineprotocol.Value]: https://pkg.go.dev/github.com/influxdata/line-protocol/v2/lineprotocol#Value
func NewValueFromUInt[U UInteger](v U) lineprotocol.Value {
return lineprotocol.UintValue(uint64(v))
}

// NewValueFromString is a convenient function for creating a [lineprotocol.Value] from String.
// Non-UTF-8 string field values are not currently supported.
//
// Parameters:
// - v: The value of the String value.
//
// Returns:
// - The created [lineprotocol.Value].
//
// [lineprotocol.Value]: https://pkg.go.dev/github.com/influxdata/line-protocol/v2/lineprotocol#Value
func NewValueFromString[S String](v S) lineprotocol.Value {
data, ok := lineprotocol.StringValue(string(v))
if !ok {
panic(fmt.Errorf("invalid utf-8 string value for NewValueFromString: %T (%#v)", v, v))
}
return data
}

// NewValueFromStringer is a convenient function for creating a [lineprotocol.Value] from [fmt.Stringer].
//
// Parameters:
// - v: The value of the [fmt.Stringer] value.
//
// Returns:
// - The created [lineprotocol.Value].
//
// [lineprotocol.Value]: https://pkg.go.dev/github.com/influxdata/line-protocol/v2/lineprotocol#Value
func NewValueFromStringer[S fmt.Stringer](v S) lineprotocol.Value {
return NewValueFromString(v.String())
}

// NewValueFromBoolean is a convenient function for creating a [lineprotocol.Value] from Boolean.
//
// Parameters:
// - v: The value of the Boolean value.
//
// Returns:
// - The created [lineprotocol.Value].
//
// [lineprotocol.Value]: https://pkg.go.dev/github.com/influxdata/line-protocol/v2/lineprotocol#Value
func NewValueFromBoolean[B Boolean](v B) lineprotocol.Value {
return lineprotocol.BoolValue(bool(v))
}

// NewValueFromTime is a convenient function for creating a [lineprotocol.Value] from [time.Time].
//
// Parameters:
// - v: The value of the [time.Time] value.
//
// Returns:
// - The created [lineprotocol.Value].
//
// [lineprotocol.Value]: https://pkg.go.dev/github.com/influxdata/line-protocol/v2/lineprotocol#Value
func NewValueFromTime(v time.Time) lineprotocol.Value {
return NewValueFromString(v.Format(time.RFC3339Nano))
}
61 changes: 61 additions & 0 deletions influxdb3/value_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
The MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

package influxdb3

import (
"math"
"testing"
"time"

"github.com/influxdata/line-protocol/v2/lineprotocol"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNewValueFromNative(t *testing.T) {
p := NewPoint(
"test",
map[string]string{
"id": "10ad=",
"ven=dor": "AWS",
`host"name`: `ho\st "a"`,
`x\" x`: "a b",
},
map[string]interface{}{},
time.Unix(60, 70))

p.AddFieldFromValue("float64", NewValueFromNative(80.1234567))
p.AddFieldFromValue("int64", NewValueFromNative(int64(-1234567890)))
p.AddFieldFromValue("uint64", NewValueFromNative(uint64(12345677890)))
p.AddFieldFromValue("string", NewValueFromNative(`six, "seven", eight`))
p.AddFieldFromValue("bytes", NewValueFromNative([]byte(`six=seven\, eight`)))
p.AddFieldFromValue("bool", NewValueFromNative(false))

line, err := p.MarshalBinary(lineprotocol.Nanosecond)
require.NoError(t, err)
assert.EqualValues(t, `test,host"name=ho\st\ "a",id=10ad\=,ven\=dor=AWS,x\"\ x=a\ b bool=false,bytes="six=seven\\, eight",float64=80.1234567,int64=-1234567890i,string="six, \"seven\", eight",uint64=12345677890u 60000000070`+"\n", string(line))

assert.PanicsWithError(t, "invalid value for NewValue: float64 (+Inf)", func() { NewValueFromNative(math.Inf(1)) })
assert.PanicsWithError(t, "invalid value for NewValue: float64 (-Inf)", func() { NewValueFromNative(math.Inf(-1)) })
assert.PanicsWithError(t, "invalid value for NewValue: string (\"\\xed\\x9f\\xc1\")", func() { NewValueFromNative(string([]byte{237, 159, 193})) })
}