Skip to content

Commit 87e374c

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: 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 87e374c

File tree

9 files changed

+694
-0
lines changed

9 files changed

+694
-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

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

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)