Skip to content

Commit 9096c23

Browse files
committed
Support decimals
Follows up tarantool/tarantool#692
1 parent bec9f72 commit 9096c23

File tree

7 files changed

+622
-0
lines changed

7 files changed

+622
-0
lines changed

README.md

+57
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,57 @@ func main() {
329329
}
330330
```
331331

332+
To enable support of decimal in msgpack with
333+
[shopspring/decimal](https://github.com/shopspring/decimal),
334+
import tarantool/decimal submodule.
335+
```go
336+
package main
337+
338+
import (
339+
"log"
340+
"time"
341+
342+
"github.com/shopspring/decimal"
343+
"github.com/tarantool/go-tarantool"
344+
_ "github.com/tarantool/go-tarantool/decimal"
345+
)
346+
347+
func main() {
348+
server := "127.0.0.1:3013"
349+
opts := tarantool.Opts{
350+
Timeout: 500 * time.Millisecond,
351+
Reconnect: 1 * time.Second,
352+
MaxReconnects: 3,
353+
User: "test",
354+
Pass: "test",
355+
}
356+
client, err := tarantool.Connect(server, opts)
357+
if err != nil {
358+
log.Fatalf("Failed to connect: %s", err.Error())
359+
}
360+
361+
spaceNo := uint32(524)
362+
363+
number, err := decimal.NewFromString("-22.804")
364+
if err != nil {
365+
log.Fatalf("Failed to prepare test decimal: %s", err)
366+
}
367+
368+
resp, err := client.Replace(spaceNo, []interface{}{number})
369+
if err != nil {
370+
log.Fatalf("Decimal replace failed: %s", err)
371+
}
372+
if resp == nil {
373+
log.Fatalf("Response is nil after Replace")
374+
}
375+
376+
log.Println("Decimal tuple replace")
377+
log.Println("Error", err)
378+
log.Println("Code", resp.Code)
379+
log.Println("Data", resp.Data)
380+
}
381+
```
382+
332383
## Schema
333384

334385
```go
@@ -702,6 +753,12 @@ Use the same for main `tarantool` package and `queue` and `uuid` subpackages.
702753
`uuid` tests require
703754
[Tarantool 2.4.1 or newer](https://github.com/tarantool/tarantool/commit/d68fc29246714eee505bc9bbcd84a02de17972c5).
704755

756+
Decimal tests require [Tarantool 2.2 or newer](https://github.com/tarantool/tarantool/issues/692).
757+
```bash
758+
cd decimal
759+
go clean -testcache && go test -v
760+
```
761+
705762
## Alternative connectors
706763

707764
There are two more connectors from the open-source community available:

decimal/bcd.go

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package decimal
2+
3+
import (
4+
_ "fmt"
5+
"strconv"
6+
"errors"
7+
)
8+
9+
var (
10+
ErrNumberIsNotADecimal = errors.New("Number is not a decimal.")
11+
ErrWrongExponentaRange = errors.New("Exponenta has a wrong range.")
12+
)
13+
14+
var mpDecimalSign = map[rune]byte{
15+
'+': 0x0a,
16+
'-': 0x0b,
17+
}
18+
19+
var mpIsDecimalNegative = map[byte]bool{
20+
0x0a: false,
21+
0x0b: true,
22+
0x0c: false,
23+
0x0d: true,
24+
0x0e: false,
25+
0x0f: false,
26+
}
27+
28+
var hex_digit = map[rune]byte{
29+
'1': 0x1,
30+
'2': 0x2,
31+
'3': 0x3,
32+
'4': 0x4,
33+
'5': 0x5,
34+
'6': 0x6,
35+
'7': 0x7,
36+
'8': 0x8,
37+
'9': 0x9,
38+
'0': 0x0,
39+
}
40+
41+
func MPEncodeNumberToBCD(buf string) []byte {
42+
scale := 0
43+
sign := '+'
44+
// TODO: The first nibble contains 0 if the decimal number has an even number of digits.
45+
nibbleIdx := 2 /* First nibble is for sign */
46+
byteBuf := make([]byte, 1)
47+
for i, ch := range buf {
48+
// TODO: ignore leading zeroes
49+
// Check for sign in a first nibble.
50+
if (i == 0) && (ch == '-' || ch == '+') {
51+
sign = ch
52+
continue
53+
}
54+
55+
// Remember a number of digits after the decimal point.
56+
if ch == '.' {
57+
scale = len(buf) - i - 1
58+
continue
59+
}
60+
61+
//digit := byte(ch)
62+
digit := hex_digit[ch]
63+
//fmt.Printf("DEBUG: ch %c\n", ch)
64+
//fmt.Printf("DEBUG: digit byte %x\n", digit)
65+
highNibble := nibbleIdx%2 != 0
66+
lowByte := len(byteBuf) - 1
67+
if highNibble {
68+
digit = digit << 4
69+
byteBuf = append(byteBuf, digit)
70+
} else {
71+
if nibbleIdx == 2 {
72+
byteBuf[0] = digit
73+
} else {
74+
byteBuf[lowByte] = byteBuf[lowByte] | digit
75+
}
76+
}
77+
//fmt.Printf("DEBUG: %x\n", byteBuf)
78+
nibbleIdx += 1
79+
}
80+
if nibbleIdx%2 != 0 {
81+
byteBuf = append(byteBuf, mpDecimalSign[sign])
82+
} else {
83+
lowByte := len(byteBuf) - 1
84+
byteBuf[lowByte] = byteBuf[lowByte] | mpDecimalSign[sign]
85+
}
86+
byteBuf = append([]byte{byte(scale)}, byteBuf...)
87+
//fmt.Printf("DEBUG: Encoded byteBuf %x\n", byteBuf)
88+
89+
return byteBuf
90+
}
91+
92+
func highNibble(b byte) byte {
93+
return b >> 4
94+
}
95+
96+
func lowNibble(b byte) byte {
97+
return b & 0x0f
98+
}
99+
100+
// TODO: BitReader https://go.dev/play/p/Wyr_K9YAro
101+
// The first byte of the BCD array contains the first digit of the number.
102+
// The first nibble contains 0 if the decimal number has an even number of digits.
103+
// The last byte of the BCD array contains the last digit of the number
104+
// and the final nibble that represents the number's sign.
105+
func MPDecodeNumberFromBCD(bcdBuf []byte) ([]string, error) {
106+
// Maximum decimal digits taken by a decimal representation.
107+
const DecimalMaxDigits = 38
108+
const mpScaleIdx = 0
109+
110+
scale := int32(bcdBuf[mpScaleIdx])
111+
// scale == -exponent, the exponent must be in range
112+
// [ -DecimalMaxDigits; DecimalMaxDigits )
113+
if scale < -DecimalMaxDigits || scale >= DecimalMaxDigits {
114+
return nil, ErrWrongExponentaRange
115+
}
116+
117+
bcdBuf = bcdBuf[mpScaleIdx+1:]
118+
//fmt.Printf("DEBUG: MPDecodeNumberFromBCD %x\n", bcdBuf)
119+
length := len(bcdBuf)
120+
var digits []string
121+
for i, bcd_byte := range bcdBuf {
122+
// Skip leading zeros.
123+
if len(digits) == 0 && int(bcd_byte) == 0 {
124+
continue
125+
}
126+
if high := highNibble(bcd_byte); high != 0 {
127+
digit := strconv.Itoa(int(high))
128+
digits = append(digits, digit)
129+
}
130+
low := lowNibble(bcd_byte)
131+
if int(i) != length-1 {
132+
digit := strconv.Itoa(int(low))
133+
digits = append(digits, digit)
134+
}
135+
/* TODO: Make sure every digit is less than 9 and bigger than 0 */
136+
}
137+
138+
digits = append(digits[:scale+1], digits[scale:]...)
139+
digits[scale] = "."
140+
last_byte := bcdBuf[length-1]
141+
sign := lowNibble(last_byte)
142+
if mpIsDecimalNegative[sign] {
143+
digits = append([]string{"-"}, digits...)
144+
}
145+
146+
return digits, nil
147+
}

decimal/config.lua

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
local decimal = require('decimal')
2+
local msgpack = require('msgpack')
3+
4+
-- Do not set listen for now so connector won't be
5+
-- able to send requests until everything is configured.
6+
box.cfg{
7+
work_dir = os.getenv("TEST_TNT_WORK_DIR"),
8+
}
9+
10+
box.schema.user.create('test', { password = 'test' , if_not_exists = true })
11+
box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true })
12+
13+
local decimal_msgpack_supported = pcall(msgpack.encode, decimal.new(1))
14+
if not decimal_msgpack_supported then
15+
error('Decimal unsupported, use Tarantool 2.2 or newer')
16+
end
17+
18+
local s = box.schema.space.create('testDecimal', {
19+
id = 524,
20+
if_not_exists = true,
21+
})
22+
s:create_index('primary', {
23+
type = 'TREE',
24+
parts = {
25+
{
26+
field = 1,
27+
type = 'decimal',
28+
},
29+
},
30+
if_not_exists = true
31+
})
32+
s:truncate()
33+
34+
box.schema.user.grant('test', 'read,write', 'space', 'testDecimal', { if_not_exists = true })
35+
36+
-- Set listen only when every other thing is configured.
37+
box.cfg{
38+
listen = os.getenv("TEST_TNT_LISTEN"),
39+
}
40+
41+
require('console').start()

decimal/decimal.go

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package decimal
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"strings"
7+
8+
"github.com/shopspring/decimal"
9+
"gopkg.in/vmihailenco/msgpack.v2"
10+
)
11+
12+
// Decimal external type
13+
// Supported since Tarantool 2.2. See more details in issue
14+
// https://github.com/tarantool/tarantool/issues/692
15+
//
16+
// Documentation:
17+
// https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type
18+
19+
const Decimal_extId = 1
20+
21+
func encodeDecimal(e *msgpack.Encoder, v reflect.Value) error {
22+
number := v.Interface().(decimal.Decimal)
23+
dec := number.String()
24+
bcdBuf := MPEncodeNumberToBCD(dec)
25+
_, err := e.Writer().Write(bcdBuf)
26+
if err != nil {
27+
return fmt.Errorf("msgpack: can't write bytes to encoder writer: %w", err)
28+
}
29+
30+
return nil
31+
}
32+
33+
func decodeDecimal(d *msgpack.Decoder, v reflect.Value) error {
34+
var bytesCount int = 4 // FIXME
35+
b := make([]byte, bytesCount)
36+
37+
_, err := d.Buffered().Read(b)
38+
if err != nil {
39+
return fmt.Errorf("msgpack: can't read bytes on decimal decode: %w", err)
40+
}
41+
42+
digits, err := MPDecodeNumberFromBCD(b)
43+
if err != nil {
44+
return err
45+
}
46+
str := strings.Join(digits, "")
47+
dec, err := decimal.NewFromString(str)
48+
if err != nil {
49+
return err
50+
}
51+
52+
v.Set(reflect.ValueOf(dec))
53+
54+
return nil
55+
}
56+
57+
func init() {
58+
msgpack.Register(reflect.TypeOf((*decimal.Decimal)(nil)).Elem(), encodeDecimal, decodeDecimal)
59+
msgpack.RegisterExt(Decimal_extId, (*decimal.Decimal)(nil))
60+
}

0 commit comments

Comments
 (0)