Skip to content

Commit 346bc89

Browse files
committed
Support decimal data type in msgpack
This patch provides decimal support for all space operations and as function return result. Decimal type was introduced in Tarantool 2.2. See more about decimal type in [1] and [2]. To use decimal with github.com/shopspring/decimal in msgpack, import tarantool/decimal submodule. 1. https://www.tarantool.io/en/doc/latest/book/box/data_model/ 2. https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type Lua snippet for encoding number to MsgPack representation: ```lua local decimal = require('decimal') local function mp_encode_dec(num) local dec = msgpack.encode(decimal.new(num)) return dec:gsub('.', function (c) return string.format('%02x', string.byte(c)) end) end print(mp_encode_dec(-12.34)) -- 0xd6010201234d ``` Follows up tarantool/tarantool#692 Closes #96
1 parent 7897baf commit 346bc89

File tree

9 files changed

+648
-0
lines changed

9 files changed

+648
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1616
- Support UUID type in msgpack (#90)
1717
- Go modules support (#91)
1818
- queue-utube handling (#85)
19+
- Support decimal type in msgpack (#96)
1920

2021
### Fixed
2122

Makefile

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ test:
1515
go clean -testcache
1616
go test ./... -v -p 1
1717

18+
.PHONY: test-decimal
19+
test-decimal:
20+
@echo "Running tests in decimal package"
21+
go clean -testcache
22+
go test ./decimal/ -v -p 1
23+
1824
.PHONY: coverage
1925
coverage:
2026
go clean -testcache

decimal/bcd.go

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package decimal
2+
3+
import (
4+
"errors"
5+
_ "fmt"
6+
"strconv"
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

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

0 commit comments

Comments
 (0)