Skip to content

Commit a2552c3

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

File tree

5 files changed

+534
-0
lines changed

5 files changed

+534
-0
lines changed

decimal/config.lua

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
box.schema.user.grant('guest', 'read,write', 'space', 'testDecimal', { if_not_exists = true })
36+
37+
--s:insert({ decimal.new(-12.34) })
38+
--s:insert({ decimal.new(1) })
39+
--s:insert({ decimal.new(2) })
40+
--s:insert({ decimal.new(0) })
41+
42+
-- Set listen only when every other thing is configured.
43+
box.cfg{
44+
listen = os.getenv("TEST_TNT_LISTEN"),
45+
}
46+
47+
require('console').start()

decimal/decimal.go

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
package decimal
2+
3+
import (
4+
"errors"
5+
"strings"
6+
"strconv"
7+
"fmt"
8+
"reflect"
9+
10+
"github.com/shopspring/decimal"
11+
"gopkg.in/vmihailenco/msgpack.v2"
12+
)
13+
14+
var (
15+
ErrNumberIsNotADecimal = errors.New("Number is not a decimal.")
16+
ErrWrongExponentaRange = errors.New("Exponenta has a wrong range.")
17+
)
18+
19+
// Decimal external type
20+
// Supported since Tarantool 2.2. See more details in issue
21+
// https://github.com/tarantool/tarantool/issues/692
22+
//
23+
// Documentation:
24+
// https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type
25+
26+
const Decimal_extId = 1
27+
28+
func mpEncodeExtType(l int) byte {
29+
if l == 1 {
30+
return 0xd4
31+
} else if l == 2 {
32+
return 0xd5
33+
} else if l > 2 && l <= 4 {
34+
return 0xd6
35+
} else if l > 4 && l <= 8 {
36+
return 0xd7
37+
} else if l > 8 && l <= 16 {
38+
return 0xd8
39+
}
40+
41+
panic("Unreachable")
42+
}
43+
44+
var hex_sign = map[rune]byte{
45+
'+': 0x0a,
46+
'-': 0x0b,
47+
}
48+
49+
var hex_digit = map[rune]byte{
50+
'1': 0x1,
51+
'2': 0x2,
52+
'3': 0x3,
53+
'4': 0x4,
54+
'5': 0x5,
55+
'6': 0x6,
56+
'7': 0x7,
57+
'8': 0x8,
58+
'9': 0x9,
59+
'0': 0x0,
60+
}
61+
62+
func mpEncodeNumberToBCD(buf string) []byte {
63+
scale := 0
64+
sign := '+'
65+
nibble_idx := 2 /* First nibble is for sign */
66+
byte_buf := make([]byte, 1)
67+
fmt.Println("To encode string:", buf)
68+
for i, ch := range buf {
69+
// TODO: ignore leading zeroes
70+
fmt.Printf("index %d, ch %c\n", i, ch)
71+
if (i == 0) && (ch == '-' || ch == '+') {
72+
sign = ch
73+
continue
74+
}
75+
if ch == '.' {
76+
scale = len(buf) - i - 1
77+
continue
78+
}
79+
80+
digit := hex_digit[ch]
81+
fmt.Printf("digit %x\n", digit)
82+
high_half_nibble := nibble_idx%2 != 0
83+
last_byte := len(byte_buf) - 1
84+
if high_half_nibble {
85+
digit = digit << 4
86+
byte_buf = append(byte_buf, digit)
87+
} else {
88+
if nibble_idx == 2 {
89+
byte_buf[0] = digit
90+
} else {
91+
byte_buf[last_byte] = byte_buf[last_byte] | digit
92+
}
93+
}
94+
nibble_idx += 1
95+
fmt.Printf("byte_buf %x\n", byte_buf)
96+
}
97+
if nibble_idx%2 != 0 {
98+
byte_buf = append(byte_buf, hex_sign[sign])
99+
} else {
100+
last_byte := len(byte_buf) - 1
101+
byte_buf[last_byte] = byte_buf[last_byte] | hex_sign[sign]
102+
}
103+
scale_hex := byte(0x02) /* FIXME */
104+
byte_buf = append([]byte{scale_hex}, byte_buf...)
105+
fmt.Println("Scale:", scale)
106+
fmt.Printf("Final byte_buf %x\n", byte_buf)
107+
108+
return byte_buf
109+
}
110+
111+
func encodeDecimal(e *msgpack.Encoder, v reflect.Value) error {
112+
number := v.Interface().(decimal.Decimal)
113+
dec := number.String()
114+
bcd_buf := mpEncodeNumberToBCD(dec)
115+
fmt.Printf("bcd_buf %x\n", bcd_buf)
116+
fmt.Println("Encoded bytes (expected 1, 2, 1, 23, 4d):", bcd_buf)
117+
_, err := e.Writer().Write(bcd_buf)
118+
if err != nil {
119+
return fmt.Errorf("msgpack: can't write bytes to encoder writer: %w", err)
120+
}
121+
122+
return nil
123+
}
124+
125+
func highHalfByte(b byte) byte {
126+
return b >> 4
127+
}
128+
129+
func lowHalfByte(b byte) byte {
130+
return b & 0x0f
131+
}
132+
133+
func isNegative(b byte) bool {
134+
b = lowHalfByte(b)
135+
switch b {
136+
case 0x0a:
137+
return false
138+
case 0x0b:
139+
return true
140+
case 0x0c:
141+
return false
142+
case 0x0d:
143+
return true
144+
case 0x0e:
145+
return false
146+
case 0x0f:
147+
return false
148+
default:
149+
panic("Sign is undefined")
150+
}
151+
}
152+
153+
154+
// TODO: BitReader https://go.dev/play/p/Wyr_K9YAro
155+
// The first byte of the BCD array contains the first digit of the number.
156+
// The first nibble contains 0 if the decimal number has an even number of digits.
157+
// The last byte of the BCD array contains the last digit of the number
158+
// and the final nibble that represents the number's sign.
159+
func DecodeNumberFromBCD(bcd_buf []byte, length int, scale int32) ([]string, error) {
160+
var digits []string
161+
//for i, bcd_byte := range bcd_buf {
162+
for i := 0; i < length; i++ {
163+
bcd_byte := bcd_buf[i]
164+
fmt.Printf("bcd_byte %x\n", bcd_byte)
165+
if int(bcd_byte) == 0 {
166+
continue
167+
}
168+
high := int(highHalfByte(bcd_byte))
169+
if high != 0 {
170+
digits = append(digits, strconv.Itoa(high))
171+
}
172+
173+
low := int(lowHalfByte(bcd_byte))
174+
if i != len(bcd_buf)-1 {
175+
digits = append(digits, strconv.Itoa(low))
176+
}
177+
/* Make sure every digit is less than 9 */
178+
}
179+
180+
digits = append(digits[:scale+1], digits[scale:]...)
181+
digits[scale] = "."
182+
183+
last_byte := bcd_buf[length - 1]
184+
fmt.Println("Last byte", last_byte)
185+
if isNegative(last_byte) {
186+
digits = append([]string{"-"}, digits...)
187+
}
188+
189+
return digits, nil
190+
}
191+
192+
193+
func decodeDecimal(d *msgpack.Decoder, v reflect.Value) error {
194+
var bytesCount int = 8
195+
b := make([]byte, bytesCount)
196+
197+
n, err := d.Buffered().Read(b)
198+
if err != nil {
199+
return fmt.Errorf("msgpack: can't read bytes on datetime decode: %w", err)
200+
}
201+
fmt.Printf("decodeDecimal(): n %d, b %x\n", n, b)
202+
203+
// Maximum decimal digits taken by a decimal representation.
204+
const DecimalMaxDigits = 38
205+
206+
// scale = -exponent, the exponent must be in range
207+
// [ -DecimalMaxDigits; DecimalMaxDigits )
208+
scale := int32(b[0])
209+
fmt.Println("decodeDecimal() scale (exp 2)", scale)
210+
if scale < -DecimalMaxDigits || scale >= DecimalMaxDigits {
211+
return ErrWrongExponentaRange
212+
}
213+
214+
bcd_buf := b[1:]
215+
fmt.Printf("decodeDecimal(): bcd_buf %x\n", bcd_buf)
216+
digits, err := DecodeNumberFromBCD(bcd_buf, 3, scale)
217+
if err != nil {
218+
return err
219+
}
220+
dec, err := decimal.NewFromString(strings.Join(digits, ""))
221+
if err == nil {
222+
return err
223+
}
224+
225+
v.Set(reflect.ValueOf(dec))
226+
227+
return nil
228+
}
229+
230+
func init() {
231+
msgpack.Register(reflect.TypeOf((*decimal.Decimal)(nil)).Elem(), encodeDecimal, decodeDecimal)
232+
msgpack.RegisterExt(Decimal_extId, (*decimal.Decimal)(nil))
233+
}

0 commit comments

Comments
 (0)