diff --git a/CHANGELOG.md b/CHANGELOG.md index f4271699e..64d8b1049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +* Added `Dict` support for `ydb.ParamsBuilder()` + ## v3.57.3 * Added metrics over query service internals * Added session create and delete events into `trace.Query` diff --git a/internal/params/dict.go b/internal/params/dict.go new file mode 100644 index 000000000..3632f5c1f --- /dev/null +++ b/internal/params/dict.go @@ -0,0 +1,418 @@ +package params + +import ( + "time" + + "github.com/ydb-platform/ydb-go-sdk/v3/internal/value" +) + +type ( + dict struct { + parent Builder + name string + values []value.DictValueField + } + dictPair struct { + parent *dict + keyValue value.Value + } + dictValue struct { + pair *dictPair + } +) + +func (d *dict) Pair() *dictPair { + return &dictPair{ + parent: d, + } +} + +func (d *dict) AddPairs(pairs ...value.DictValueField) *dict { + d.values = append(d.values, pairs...) + + return d +} + +func (d *dictPair) Text(v string) *dictValue { + d.keyValue = value.TextValue(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) Bytes(v []byte) *dictValue { + d.keyValue = value.BytesValue(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) Bool(v bool) *dictValue { + d.keyValue = value.BoolValue(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) Uint64(v uint64) *dictValue { + d.keyValue = value.Uint64Value(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) Int64(v int64) *dictValue { + d.keyValue = value.Int64Value(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) Uint32(v uint32) *dictValue { + d.keyValue = value.Uint32Value(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) Int32(v int32) *dictValue { + d.keyValue = value.Int32Value(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) Uint16(v uint16) *dictValue { + d.keyValue = value.Uint16Value(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) Int16(v int16) *dictValue { + d.keyValue = value.Int16Value(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) Uint8(v uint8) *dictValue { + d.keyValue = value.Uint8Value(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) Int8(v int8) *dictValue { + d.keyValue = value.Int8Value(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) Float(v float32) *dictValue { + d.keyValue = value.FloatValue(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) Double(v float64) *dictValue { + d.keyValue = value.DoubleValue(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) Decimal(v [16]byte, precision, scale uint32) *dictValue { + d.keyValue = value.DecimalValue(v, precision, scale) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) Timestamp(v time.Time) *dictValue { + d.keyValue = value.TimestampValueFromTime(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) Date(v time.Time) *dictValue { + d.keyValue = value.DateValueFromTime(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) Datetime(v time.Time) *dictValue { + d.keyValue = value.DatetimeValueFromTime(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) Interval(v time.Duration) *dictValue { + d.keyValue = value.IntervalValueFromDuration(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) JSON(v string) *dictValue { + d.keyValue = value.JSONValue(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) JSONDocument(v string) *dictValue { + d.keyValue = value.JSONDocumentValue(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) YSON(v []byte) *dictValue { + d.keyValue = value.YSONValue(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictPair) UUID(v [16]byte) *dictValue { + d.keyValue = value.UUIDValue(v) + + return &dictValue{ + pair: d, + } +} + +func (d *dictValue) Text(v string) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.TextValue(v), + }) + + return d.pair.parent +} + +func (d *dictValue) Bytes(v []byte) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.BytesValue(v), + }) + + return d.pair.parent +} + +func (d *dictValue) Bool(v bool) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.BoolValue(v), + }) + + return d.pair.parent +} + +func (d *dictValue) Uint64(v uint64) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.Uint64Value(v), + }) + + return d.pair.parent +} + +func (d *dictValue) Int64(v int64) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.Int64Value(v), + }) + + return d.pair.parent +} + +func (d *dictValue) Uint32(v uint32) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.Uint32Value(v), + }) + + return d.pair.parent +} + +func (d *dictValue) Int32(v int32) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.Int32Value(v), + }) + + return d.pair.parent +} + +func (d *dictValue) Uint16(v uint16) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.Uint16Value(v), + }) + + return d.pair.parent +} + +func (d *dictValue) Int16(v int16) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.Int16Value(v), + }) + + return d.pair.parent +} + +func (d *dictValue) Uint8(v uint8) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.Uint8Value(v), + }) + + return d.pair.parent +} + +func (d *dictValue) Int8(v int8) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.Int8Value(v), + }) + + return d.pair.parent +} + +func (d *dictValue) Float(v float32) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.FloatValue(v), + }) + + return d.pair.parent +} + +func (d *dictValue) Double(v float64) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.DoubleValue(v), + }) + + return d.pair.parent +} + +func (d *dictValue) Decimal(v [16]byte, precision, scale uint32) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.DecimalValue(v, precision, scale), + }) + + return d.pair.parent +} + +func (d *dictValue) Timestamp(v time.Time) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.TimestampValueFromTime(v), + }) + + return d.pair.parent +} + +func (d *dictValue) Date(v time.Time) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.DateValueFromTime(v), + }) + + return d.pair.parent +} + +func (d *dictValue) Datetime(v time.Time) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.DatetimeValueFromTime(v), + }) + + return d.pair.parent +} + +func (d *dictValue) Interval(v time.Duration) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.IntervalValueFromDuration(v), + }) + + return d.pair.parent +} + +func (d *dictValue) JSON(v string) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.JSONValue(v), + }) + + return d.pair.parent +} + +func (d *dictValue) JSONDocument(v string) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.JSONDocumentValue(v), + }) + + return d.pair.parent +} + +func (d *dictValue) YSON(v []byte) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.YSONValue(v), + }) + + return d.pair.parent +} + +func (d *dictValue) UUID(v [16]byte) *dict { + d.pair.parent.values = append(d.pair.parent.values, value.DictValueField{ + K: d.pair.keyValue, + V: value.UUIDValue(v), + }) + + return d.pair.parent +} + +func (d *dict) Build() Builder { + d.parent.params = append(d.parent.params, &Parameter{ + parent: d.parent, + name: d.name, + value: value.DictValue(d.values...), + }) + + return d.parent +} diff --git a/internal/params/dict_test.go b/internal/params/dict_test.go new file mode 100644 index 000000000..835816698 --- /dev/null +++ b/internal/params/dict_test.go @@ -0,0 +1,474 @@ +package params + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/ydb-platform/ydb-go-genproto/protos/Ydb" + + "github.com/ydb-platform/ydb-go-sdk/v3/internal/allocator" + "github.com/ydb-platform/ydb-go-sdk/v3/internal/value" + "github.com/ydb-platform/ydb-go-sdk/v3/internal/xtest" +) + +func TestDict(t *testing.T) { + type expected struct { + kind *Ydb.Type + value *Ydb.Value + } + + tests := []struct { + method string + args []any + + expected expected + }{ + { + method: "Uint64", + args: []any{uint64(123)}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_UINT64}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_Uint64Value{ + Uint64Value: 123, + }, + }, + }, + }, + { + method: "Int64", + args: []any{int64(123)}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_INT64}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_Int64Value{ + Int64Value: 123, + }, + }, + }, + }, + { + method: "Uint32", + args: []any{uint32(123)}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_UINT32}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_Uint32Value{ + Uint32Value: 123, + }, + }, + }, + }, + { + method: "Int32", + args: []any{int32(123)}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_INT32}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_Int32Value{ + Int32Value: 123, + }, + }, + }, + }, + { + method: "Uint16", + args: []any{uint16(123)}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_UINT16}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_Uint32Value{ + Uint32Value: 123, + }, + }, + }, + }, + { + method: "Int16", + args: []any{int16(123)}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_INT16}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_Int32Value{ + Int32Value: 123, + }, + }, + }, + }, + { + method: "Uint8", + args: []any{uint8(123)}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_UINT8}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_Uint32Value{ + Uint32Value: 123, + }, + }, + }, + }, + { + method: "Int8", + args: []any{int8(123)}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_INT8}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_Int32Value{ + Int32Value: 123, + }, + }, + }, + }, + { + method: "Bool", + args: []any{true}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_BOOL}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_BoolValue{ + BoolValue: true, + }, + }, + }, + }, + { + method: "Text", + args: []any{"test"}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_UTF8}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_TextValue{ + TextValue: "test", + }, + }, + }, + }, + { + method: "Bytes", + args: []any{[]byte("test")}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_STRING}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_BytesValue{ + BytesValue: []byte("test"), + }, + }, + }, + }, + { + method: "Float", + args: []any{float32(123)}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_FLOAT}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_FloatValue{ + FloatValue: float32(123), + }, + }, + }, + }, + { + method: "Double", + args: []any{float64(123)}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_DOUBLE}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_DoubleValue{ + DoubleValue: float64(123), + }, + }, + }, + }, + { + method: "Interval", + args: []any{time.Second}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_INTERVAL}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_Int64Value{ + Int64Value: 1000000, + }, + }, + }, + }, + { + method: "Datetime", + args: []any{time.Unix(123456789, 456)}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_DATETIME}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_Uint32Value{ + Uint32Value: 123456789, + }, + }, + }, + }, + { + method: "Date", + args: []any{time.Unix(123456789, 456)}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_DATE}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_Uint32Value{ + Uint32Value: 1428, + }, + }, + }, + }, + { + method: "Timestamp", + args: []any{time.Unix(123456789, 456)}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_TIMESTAMP}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_Uint64Value{ + Uint64Value: 123456789000000, + }, + }, + }, + }, + { + method: "Decimal", + args: []any{[...]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6}, uint32(22), uint32(9)}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_DecimalType{ + DecimalType: &Ydb.DecimalType{ + Precision: 22, + Scale: 9, + }, + }, + }, + value: &Ydb.Value{ + High_128: 72623859790382856, + Value: &Ydb.Value_Low_128{ + Low_128: 648519454493508870, + }, + }, + }, + }, + { + method: "JSON", + args: []any{`{"a": 1,"b": "B"}`}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_JSON}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_TextValue{ + TextValue: `{"a": 1,"b": "B"}`, + }, + }, + }, + }, + { + method: "JSONDocument", + args: []any{`{"a": 1,"b": "B"}`}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_JSON_DOCUMENT}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_TextValue{ + TextValue: `{"a": 1,"b": "B"}`, + }, + }, + }, + }, + { + method: "YSON", + args: []any{[]byte(`{"a": 1,"b": "B"}`)}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_YSON}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_BytesValue{ + BytesValue: []byte(`{"a": 1,"b": "B"}`), + }, + }, + }, + }, + { + method: "UUID", + args: []any{[...]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}}, + + expected: expected{ + kind: &Ydb.Type{ + Type: &Ydb.Type_TypeId{TypeId: Ydb.Type_UUID}, + }, + value: &Ydb.Value{ + Value: &Ydb.Value_Low_128{ + Low_128: 651345242494996240, + }, + High_128: 72623859790382856, + }, + }, + }, + } + + for _, key := range tests { + for _, val := range tests { + t.Run(fmt.Sprintf("%s:%s", key.method, val.method), func(t *testing.T) { + a := allocator.New() + defer a.Free() + + item := Builder{}.Param("$x").Dict().Pair() + + addedKey, ok := xtest.CallMethod(item, key.method, key.args...)[0].(*dictValue) + require.True(t, ok) + + d, ok := xtest.CallMethod(addedKey, val.method, val.args...)[0].(*dict) + require.True(t, ok) + + params := d.Build().Build().ToYDB(a) + require.Equal(t, paramsToJSON( + map[string]*Ydb.TypedValue{ + "$x": { + Type: &Ydb.Type{ + Type: &Ydb.Type_DictType{ + DictType: &Ydb.DictType{ + Key: key.expected.kind, + Payload: val.expected.kind, + }, + }, + }, + Value: &Ydb.Value{ + Pairs: []*Ydb.ValuePair{ + { + Key: key.expected.value, + Payload: val.expected.value, + }, + }, + }, + }, + }), paramsToJSON(params)) + }) + } + } +} + +func TestDict_AddPairs(t *testing.T) { + a := allocator.New() + defer a.Free() + + pairs := []value.DictValueField{ + { + K: value.Int64Value(123), + V: value.BoolValue(true), + }, + { + K: value.Int64Value(321), + V: value.BoolValue(false), + }, + } + + params := Builder{}.Param("$x").Dict().AddPairs(pairs...).Build().Build().ToYDB(a) + + require.Equal(t, paramsToJSON( + map[string]*Ydb.TypedValue{ + "$x": { + Type: &Ydb.Type{ + Type: &Ydb.Type_DictType{ + DictType: &Ydb.DictType{ + Key: &Ydb.Type{ + Type: &Ydb.Type_TypeId{ + TypeId: Ydb.Type_INT64, + }, + }, + Payload: &Ydb.Type{ + Type: &Ydb.Type_TypeId{ + TypeId: Ydb.Type_BOOL, + }, + }, + }, + }, + }, + Value: &Ydb.Value{ + Pairs: []*Ydb.ValuePair{ + { + Key: &Ydb.Value{ + Value: &Ydb.Value_Int64Value{ + Int64Value: 123, + }, + }, + Payload: &Ydb.Value{ + Value: &Ydb.Value_BoolValue{ + BoolValue: true, + }, + }, + }, + { + Key: &Ydb.Value{ + Value: &Ydb.Value_Int64Value{ + Int64Value: 321, + }, + }, + Payload: &Ydb.Value{ + Value: &Ydb.Value_BoolValue{ + BoolValue: false, + }, + }, + }, + }, + }, + }, + }), paramsToJSON(params)) +} diff --git a/internal/params/parameters.go b/internal/params/parameters.go index 96ef92591..21d1a3b8d 100644 --- a/internal/params/parameters.go +++ b/internal/params/parameters.go @@ -116,6 +116,13 @@ func (p *Parameter) Set() *set { } } +func (p *Parameter) Dict() *dict { + return &dict{ + parent: p.parent, + name: p.name, + } +} + func (p *Parameter) Text(v string) Builder { p.value = value.TextValue(v) p.parent.params = append(p.parent.params, p) diff --git a/internal/xtest/call_method.go b/internal/xtest/call_method.go new file mode 100644 index 000000000..a7e060a8a --- /dev/null +++ b/internal/xtest/call_method.go @@ -0,0 +1,25 @@ +package xtest + +import ( + "reflect" +) + +func CallMethod(object any, name string, args ...any) []any { + method := reflect.ValueOf(object).MethodByName(name) + + inputs := make([]reflect.Value, len(args)) + + for i := range args { + inputs[i] = reflect.ValueOf(args[i]) + } + + output := method.Call(inputs) + + result := make([]any, len(output)) + + for i := range output { + result[i] = output[i].Interface() + } + + return result +} diff --git a/internal/xtest/call_method_test.go b/internal/xtest/call_method_test.go new file mode 100644 index 000000000..fe721761d --- /dev/null +++ b/internal/xtest/call_method_test.go @@ -0,0 +1,39 @@ +package xtest + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCallMethod(t *testing.T) { + object := bytes.NewBuffer(nil) + + result := CallMethod(object, "WriteString", "Hello world!") + n := result[0].(int) + err := result[1] + + require.Equal(t, 12, n) + require.Nil(t, err) + + result = CallMethod(object, "String") + + str, ok := result[0].(string) + require.True(t, ok) + + require.Equal(t, object.String(), str) + + require.Panics(t, func() { + CallMethod(object, "NonameMethod") + }) + + require.Panics(t, func() { + CallMethod(object, "String", "wrong", "arguments", "count") + }) + + require.Panics(t, func() { + // Wrong argument type. + CallMethod(object, "WriteString", 123) + }) +}