[PATCH] box: add support for decimals in update ops

Serge Petrenko sergepetrenko at tarantool.org
Thu Aug 15 16:23:51 MSK 2019


Closes #4413

@TarantoolBot document
Title: update operations on decimal fields.

tuple:update and space:update now support deicmal operands for
arithmetic operations ('+' and '-'). The syntax is as usual:
```
d = box.tuple.new(decimal.new('1'))
---
...
d:update{{'+', 1, decimal.new('0.5')}}
---
- [1.5]
...
```

Insertion ('!') and assignment ('=') are also supported:
```
a = decimal.new('1')
---
...
b = decimal.new('1e10')
---
...
c = decimal.new('1e-10')
---
...
d = box.tuple.new{5, a, 6, b, 7, c, "string"}
---
...
d
---
- [5, 1, 6, 10000000000, 7, 0.0000000001, 'string']
...

d:update{{'!', 3, dec.new('1234.5678')}}
---
- [5, 1, 1234.5678, 6, 10000000000, 7, 0.0000000001, 'string']
...
d:update{{'=', -1, dec.new('0.12345678910111213')}}
---
- [5, 1, 6, 10000000000, 7, 0.0000000001, 0.12345678910111213]
...
```
---
https://github.com/tarantool/tarantool/issues/4413
https://github.com/tarantool/tarantool/tree/sp/gh-4413-decimal-update

 src/box/errcode.h            |   2 +-
 src/box/tuple_update.c       | 103 ++++++++++++++++++++++++++++++-----
 test/box/misc.result         |   1 +
 test/box/tuple.result        |  58 ++++++++++++++++++++
 test/box/tuple.test.lua      |  24 ++++++++
 test/engine/decimal.result   |  36 ++++++++++++
 test/engine/decimal.test.lua |  11 ++++
 7 files changed, 221 insertions(+), 14 deletions(-)

diff --git a/src/box/errcode.h b/src/box/errcode.h
index 817275b97..46b0b365a 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -212,7 +212,7 @@ struct errcode_record {
 	/*157 */_(ER_SQL_BIND_TYPE,             "Bind value type %s for parameter %s is not supported") \
 	/*158 */_(ER_SQL_BIND_PARAMETER_MAX,    "SQL bind parameter limit reached: %d") \
 	/*159 */_(ER_SQL_EXECUTE,               "Failed to execute SQL statement: %s") \
-	/*160 */_(ER_UNUSED,			"") \
+	/*160 */_(ER_UPDATE_DECIMAL_OVERFLOW,	"Decimal overflow when performing operation '%c' on field %u") \
 	/*161 */_(ER_SQL_BIND_NOT_FOUND,	"Parameter %s was not found in the statement") \
 	/*162 */_(ER_ACTION_MISMATCH,		"Field %s contains %s on conflict action, but %s in index parts") \
 	/*163 */_(ER_VIEW_MISSING_SQL,		"Space declared as a view must have SQL statement") \
diff --git a/src/box/tuple_update.c b/src/box/tuple_update.c
index 7a203ced8..351979202 100644
--- a/src/box/tuple_update.c
+++ b/src/box/tuple_update.c
@@ -43,6 +43,9 @@
 #include <bit/int96.h>
 #include <salad/rope.h>
 #include "column_mask.h"
+#include "mp_user_types.h"
+#include "decimal.h"
+#include "mp_decimal.h"
 
 
 /** UPDATE request implementation.
@@ -125,9 +128,10 @@ struct op_del_arg {
  * MsgPack codes are not used to simplify type calculation.
  */
 enum arith_type {
-	AT_DOUBLE = 0, /* MP_DOUBLE */
-	AT_FLOAT = 1, /* MP_FLOAT */
-	AT_INT = 2 /* MP_INT/MP_UINT */
+	AT_DECIMAL = 0, /* MP_EXT + MP_DECIMAL */
+	AT_DOUBLE = 1, /* MP_DOUBLE */
+	AT_FLOAT = 2, /* MP_FLOAT */
+	AT_INT = 3 /* MP_INT/MP_UINT */
 };
 
 /**
@@ -157,6 +161,7 @@ struct op_arith_arg {
 		double dbl;
 		float flt;
 		struct int96_num int96;
+		decimal_t dec;
 	};
 };
 
@@ -279,19 +284,29 @@ static inline int
 mp_read_arith_arg(int index_base, struct update_op *op,
 		  const char **expr, struct op_arith_arg *ret)
 {
-	if (mp_typeof(**expr) == MP_UINT) {
+	switch(msgpack_to_field_type(*expr))
+	{
+	case MP_FIELD_UINT:
 		ret->type = AT_INT;
 		int96_set_unsigned(&ret->int96, mp_decode_uint(expr));
-	} else if (mp_typeof(**expr) == MP_INT) {
+		break;
+	case MP_FIELD_INT:
 		ret->type = AT_INT;
 		int96_set_signed(&ret->int96, mp_decode_int(expr));
-	} else if (mp_typeof(**expr) == MP_DOUBLE) {
+		break;
+	case MP_FIELD_DOUBLE:
 		ret->type = AT_DOUBLE;
 		ret->dbl = mp_decode_double(expr);
-	} else if (mp_typeof(**expr) == MP_FLOAT) {
+		break;
+	case MP_FIELD_FLOAT:
 		ret->type = AT_FLOAT;
 		ret->flt = mp_decode_float(expr);
-	} else {
+		break;
+	case MP_FIELD_DECIMAL:
+		ret->type = AT_DECIMAL;
+		mp_decode_decimal(expr, &ret->dec);
+		break;
+	default:
 		diag_set(ClientError, ER_UPDATE_ARG_TYPE, (char)op->opcode,
 			 index_base + op->field_no, "a number");
 		return -1;
@@ -421,6 +436,32 @@ cast_arith_arg_to_double(struct op_arith_arg arg)
 	}
 }
 
+static inline decimal_t *
+cast_arith_arg_to_decimal(struct op_arith_arg arg, decimal_t *dec)
+{
+	decimal_t *ret;
+	if (arg.type == AT_DECIMAL) {
+		*dec = arg.dec;
+		return dec;
+	} else if (arg.type == AT_DOUBLE) {
+		ret = decimal_from_double(dec, arg.dbl);
+	} else if (arg.type == AT_FLOAT) {
+		ret = decimal_from_double(dec, arg.flt);
+	} else {
+		assert(arg.type == AT_INT);
+		if (int96_is_uint64(&arg.int96)) {
+			uint64_t val = int96_extract_uint64(&arg.int96);
+			ret = decimal_from_uint64(dec, val);
+		} else {
+			assert(int96_is_neg_int64(&arg.int96));
+			int64_t val = int96_extract_neg_int64(&arg.int96);
+			ret = decimal_from_int64(dec, val);
+		}
+	}
+
+	return ret;
+}
+
 /** Return the MsgPack size of an arithmetic operation result. */
 static inline uint32_t
 mp_sizeof_op_arith_arg(struct op_arith_arg arg)
@@ -435,9 +476,11 @@ mp_sizeof_op_arith_arg(struct op_arith_arg arg)
 		}
 	} else if (arg.type == AT_DOUBLE) {
 		return mp_sizeof_double(arg.dbl);
-	} else {
-		assert(arg.type == AT_FLOAT);
+	} else if (arg.type == AT_FLOAT) {
 		return mp_sizeof_float(arg.flt);
+	} else {
+		assert(arg.type == AT_DECIMAL);
+		return mp_sizeof_decimal(&arg.dec);
 	}
 }
 
@@ -471,7 +514,7 @@ make_arith_operation(struct op_arith_arg arg1, struct op_arith_arg arg2,
 		}
 		*ret = arg1;
 		return 0;
-	} else {
+	} else if (lowest_type >= AT_DOUBLE) {
 		/* At least one of operands is double or float */
 		double a = cast_arith_arg_to_double(arg1);
 		double b = cast_arith_arg_to_double(arg2);
@@ -494,6 +537,38 @@ make_arith_operation(struct op_arith_arg arg1, struct op_arith_arg arg2,
 			ret->type = AT_FLOAT;
 			ret->flt = (float)c;
 		}
+	} else {
+		/* At least one of the operands is decimal. */
+		decimal_t a, b, c;
+		if (! cast_arith_arg_to_decimal(arg1, &a) ||
+		    ! cast_arith_arg_to_decimal(arg2, &b)) {
+			diag_set(ClientError, ER_UPDATE_ARG_TYPE, (char)opcode,
+				 err_fieldno, "a number convertible to decimal.");
+			return -1;
+		}
+
+		switch(opcode) {
+		case '+':
+			if (decimal_add(&c, &a, &b) == NULL) {
+				diag_set(ClientError, ER_UPDATE_DECIMAL_OVERFLOW,
+					 opcode, err_fieldno);
+				return -1;
+			}
+			break;
+		case '-':
+			if (decimal_sub(&c, &a, &b) == NULL) {
+				diag_set(ClientError, ER_UPDATE_DECIMAL_OVERFLOW,
+					 opcode, err_fieldno);
+				return -1;
+			}
+			break;
+		default:
+			diag_set(ClientError, ER_UPDATE_ARG_TYPE, (char)opcode,
+				 err_fieldno);
+			return -1;
+		}
+		ret->type = AT_DECIMAL;
+		ret->dec = c;
 	}
 	return 0;
 }
@@ -722,9 +797,11 @@ store_op_arith(struct op_arith_arg *arg, const char *in, char *out)
 		}
 	} else if (arg->type == AT_DOUBLE) {
 		mp_encode_double(out, arg->dbl);
-	} else {
-		assert(arg->type == AT_FLOAT);
+	} else if (arg->type == AT_FLOAT) {
 		mp_encode_float(out, arg->flt);
+	} else {
+		assert (arg->type == AT_DECIMAL);
+		mp_encode_decimal(out, &arg->dec);
 	}
 }
 
diff --git a/test/box/misc.result b/test/box/misc.result
index 7a15dabf0..287d84e5b 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -490,6 +490,7 @@ t;
   157: box.error.SQL_BIND_TYPE
   158: box.error.SQL_BIND_PARAMETER_MAX
   159: box.error.SQL_EXECUTE
+  160: box.error.UPDATE_DECIMAL_OVERFLOW
   161: box.error.SQL_BIND_NOT_FOUND
   162: box.error.ACTION_MISMATCH
   163: box.error.VIEW_MISSING_SQL
diff --git a/test/box/tuple.result b/test/box/tuple.result
index a5010538d..895462518 100644
--- a/test/box/tuple.result
+++ b/test/box/tuple.result
@@ -1337,3 +1337,61 @@ d:update{{'=', -1, dec.new('0.12345678910111213')}}
 ---
 - [5, 1, 6, 10000000000, 7, 0.0000000001, 0.12345678910111213]
 ...
+--
+-- gh-4413: tuple:update arithmetic for decimals
+--
+ffi = require('ffi')
+---
+...
+d = box.tuple.new(dec.new('1'))
+---
+...
+d:update{{'+', 1, dec.new('0.5')}}
+---
+- [1.5]
+...
+d:update{{'-', 1, dec.new('0.5')}}
+---
+- [0.5]
+...
+d:update{{'+', 1, 1.36}}
+---
+- [2.36]
+...
+d:update{{'+', 1, ffi.new('uint64_t', 1712)}}
+---
+- [1713]
+...
+d:update{{'-', 1, ffi.new('float', 635)}}
+---
+- [-634]
+...
+-- test erroneous values
+-- nan
+d:update{{'+', 1, 0/0}}
+---
+- error: 'Argument type in operation ''+'' on field 1 does not match field type: expected
+    a number convertible to decimal.'
+...
+-- inf
+d:update{{'-', 1, 1/0}}
+---
+- error: 'Argument type in operation ''-'' on field 1 does not match field type: expected
+    a number convertible to decimal.'
+...
+-- decimal overflow
+d = box.tuple.new(dec.new('9e37'))
+---
+...
+d
+---
+- [90000000000000000000000000000000000000]
+...
+d:update{{'+', 1, dec.new('1e37')}}
+---
+- error: Decimal overflow when performing operation '+' on field 1
+...
+d:update{{'-', 1, dec.new('1e37')}}
+---
+- [80000000000000000000000000000000000000]
+...
diff --git a/test/box/tuple.test.lua b/test/box/tuple.test.lua
index 8d4431bc6..9762fc8b3 100644
--- a/test/box/tuple.test.lua
+++ b/test/box/tuple.test.lua
@@ -449,3 +449,27 @@ msgpack.decode(msgpackffi.encode(d))
 d:bsize()
 d:update{{'!', 3, dec.new('1234.5678')}}
 d:update{{'=', -1, dec.new('0.12345678910111213')}}
+
+--
+-- gh-4413: tuple:update arithmetic for decimals
+--
+ffi = require('ffi')
+
+d = box.tuple.new(dec.new('1'))
+d:update{{'+', 1, dec.new('0.5')}}
+d:update{{'-', 1, dec.new('0.5')}}
+d:update{{'+', 1, 1.36}}
+d:update{{'+', 1, ffi.new('uint64_t', 1712)}}
+d:update{{'-', 1, ffi.new('float', 635)}}
+
+-- test erroneous values
+-- nan
+d:update{{'+', 1, 0/0}}
+-- inf
+d:update{{'-', 1, 1/0}}
+
+-- decimal overflow
+d = box.tuple.new(dec.new('9e37'))
+d
+d:update{{'+', 1, dec.new('1e37')}}
+d:update{{'-', 1, dec.new('1e37')}}
diff --git a/test/engine/decimal.result b/test/engine/decimal.result
index 415868c89..2bf71bfec 100644
--- a/test/engine/decimal.result
+++ b/test/engine/decimal.result
@@ -351,6 +351,42 @@ box.space.test.index.sk:select{}
  |   - [3, 3]
  | ...
 
+box.space.test:truncate()
+ | ---
+ | ...
+
+-- test update operations
+box.space.test:insert{1, decimal.new(1.10)}
+ | ---
+ | - [1, 1.1]
+ | ...
+box.space.test:insert{2, 2}
+ | ---
+ | - [2, 2]
+ | ...
+box.space.test:update(1, {{'+', 2, 3.1}})
+ | ---
+ | - [1, 4.2]
+ | ...
+box.space.test.index.sk:select{}
+ | ---
+ | - - [2, 2]
+ |   - [1, 4.2]
+ | ...
+box.space.test:update(1, {{'-', 2, decimal.new(3.3)}})
+ | ---
+ | - [1, 0.9]
+ | ...
+box.space.test:update(2, {{'+', 2, decimal.new(0.1)}})
+ | ---
+ | - [2, 2.1]
+ | ...
+box.space.test.index.sk:select{}
+ | ---
+ | - - [1, 0.9]
+ |   - [2, 2.1]
+ | ...
+
 box.space.test:drop()
  | ---
  | ...
diff --git a/test/engine/decimal.test.lua b/test/engine/decimal.test.lua
index 3763bf0a3..3efd5c3e9 100644
--- a/test/engine/decimal.test.lua
+++ b/test/engine/decimal.test.lua
@@ -101,4 +101,15 @@ box.space.test.index.sk:alter{parts={2, 'number'}}
 box.space.test:insert{2, -5}
 box.space.test.index.sk:select{}
 
+box.space.test:truncate()
+
+-- test update operations
+box.space.test:insert{1, decimal.new(1.10)}
+box.space.test:insert{2, 2}
+box.space.test:update(1, {{'+', 2, 3.1}})
+box.space.test.index.sk:select{}
+box.space.test:update(1, {{'-', 2, decimal.new(3.3)}})
+box.space.test:update(2, {{'+', 2, decimal.new(0.1)}})
+box.space.test.index.sk:select{}
+
 box.space.test:drop()
-- 
2.20.1 (Apple Git-117)




More information about the Tarantool-patches mailing list