diff --git a/docs/numbers.rst b/docs/numbers.rst index c24ecb1..b9da824 100644 --- a/docs/numbers.rst +++ b/docs/numbers.rst @@ -211,8 +211,8 @@ and precision. Both store their respective values as fractions. For example, ``1.23`` could be represented as ``123/100``. The main difference between these two types comes down to the allowed -denominators. In short, a ``NUMERIC`` may have any denominator, whereas a -``DECIMAL`` must have a denominator of exactly 10^scale. This can also be +denominators. In short, a ``DECIMAL`` may have any denominator, whereas a +``NUMERIC`` must have a denominator of exactly 10^scale. This can also be expressed as: .. list-table:: @@ -222,31 +222,31 @@ expressed as: - Numerator - Denominator - * - ``NUMERIC(scale, precision)`` + * - ``DECIMAL(scale, precision)`` - ± 10^scale (exclusive) - ± 10^scale (exclusive) - * - ``DECIMAL(scale, precision)`` + * - ``NUMERIC(scale, precision)`` - ± 10^scale (exclusive) - 10^scale -When calculations are performed on a ``DECIMAL``, the result from each operation +When calculations are performed on a ``NUMERIC``, the result from each operation will be normalized to always satisfy this constraint. -This means that a ``DECIMAL`` is always exact at the scale and precision +This means that a ``NUMERIC`` is always exact at the scale and precision specified and casting to a higher precision will not alter the value. In -contrast, a ``NUMERIC`` promises to have *at least* the precision specified but +contrast, a ``DECIMAL`` promises to have *at least* the precision specified but the value may change as to be more exact if the precision is increased. This is best understood with some examples: .. code-block:: sql - VALUES CAST(1.23 AS DECIMAL(3,2)) / CAST(5 AS DECIMAL) * CAST(5 AS DECIMAL); - -- 1.20 + VALUES CAST(1.23 AS NUMERIC(3,2)) / CAST(5 AS NUMERIC) * CAST(5 AS NUMERIC); + -- 1.2 Because: -1. ``1.23 AS DECIMAL(3,2)`` -> ``123/100`` +1. ``1.23 AS NUMERIC(3,2)`` -> ``123/100`` 2. Normalize denominator -> ``123/100`` 3. Divide by ``5`` -> ``123/500`` 4. Normalize denominator -> ``24/100`` @@ -257,63 +257,63 @@ Whereas, .. code-block:: sql - VALUES CAST(1.23 AS NUMERIC(3,2)) / 5 * 5; + VALUES CAST(1.23 AS DECIMAL(3,2)) / 5 * 5; -- 1.23 Because: -1. ``1.23 AS NUMERIC(3,2)`` -> ``123/100`` +1. ``1.23 AS DECIMAL(3,2)`` -> ``123/100`` 2. Divide by ``5`` -> ``123/500`` 3. Multiply by ``5`` -> ``615/500`` -This may seem like the only difference is that ``NUMERIC`` does not normalize +This may seem like the only difference is that ``DECIMAL`` does not normalize the denominator, but actually they both need to normalize a denominator that would be out of bounds. Consider the example: .. code-block:: sql - VALUES CAST(1.23 AS NUMERIC(3,2)) / 11; + VALUES CAST(1.23 AS DECIMAL(3,2)) / 11; -- 0.11 -1. ``1.23 AS NUMERIC(3,2)`` -> ``123/100`` +1. ``1.23 AS DECIMAL(3,2)`` -> ``123/100`` 2. Divide by ``11`` -> ``123/1100`` 3. Denominator is out of bounds as it cannot be larger than 100. Highest precision equivalent would be -> ``11/100`` -This the same process and result that a ``DECIMAL`` that the equivalent decimal +This the same process and result that a ``NUMERIC`` that the equivalent decimal operation. Casting to higher precision might result in a different value for -``NUMERIC`` values, for example: +``DECIMAL`` values, for example: .. code-block:: sql - VALUES CAST(CAST(5 AS NUMERIC(3,2)) / CAST(7 AS NUMERIC(5,4)) AS NUMERIC(5,4)); + VALUES CAST(CAST(5 AS DECIMAL(3,2)) / CAST(7 AS DECIMAL(5,4)) AS DECIMAL(5,4)); -- 0.7142 Because: -1. ``5 AS NUMERIC(3,2)`` -> ``5/1`` +1. ``5 AS DECIMAL(3,2)`` -> ``5/1`` 2. Divide by ``7`` -> ``5/7`` -3. Cast to ``NUMERIC(5,4)`` -> ``5/7`` +3. Cast to ``DECIMAL(5,4)`` -> ``5/7`` 4. Formatted result based on 4 precision -> ``0.7142`` .. code-block:: sql - VALUES CAST(CAST(5 AS DECIMAL(3,2)) / CAST(7 AS DECIMAL) AS DECIMAL(5,4)); + VALUES CAST(CAST(5 AS NUMERIC(3,2)) / CAST(7 AS NUMERIC) AS NUMERIC(5,4)); -- 0.7100 Because: -1. ``5 AS DECIMAL(3,2)`` -> ``500/100`` +1. ``5 AS NUMERIC(3,2)`` -> ``500/100`` 2. Divide by ``7`` -> ``500/700`` 3. Normalize denominator -> ``71/100`` -4. Cast to ``DECIMAL(5,4)`` -> ``7100/10000`` +4. Cast to ``NUMERIC(5,4)`` -> ``7100/10000`` 5. Formatted result based on 4 precision -> ``0.7100`` Operations Between Exact Types ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Arithmetic operations can only be performed when both operates are the same -fundamental types, ``NUMERIC`` or ``DECIMAL``, although they do not need to +fundamental types, ``DECIMAL`` or ``NUMERIC``, although they do not need to share the same scale or precision. .. list-table:: @@ -322,27 +322,27 @@ share the same scale or precision. * - Operation - Result Type - * - ``NUMERIC(s1, s2) + NUMERIC(s2, p2)`` - - ``NUMERIC(MAX(s1, s2), MIN(p1, p2))`` + * - ``T(s1, s2) + T(s2, p2)`` + - ``T(MAX(s1, s2), MIN(p1, p2))`` - * - ``NUMERIC(s1, s2) - NUMERIC(s2, p2)`` - - ``NUMERIC(MAX(s1, s2), MIN(p1, p2))`` + * - ``T(s1, s2) - T(s2, p2)`` + - ``T(MAX(s1, s2), MIN(p1, p2))`` - * - ``NUMERIC(s1, s2) * NUMERIC(s2, p2)`` - - ``NUMERIC(s1 * s2, p1 + p2)`` + * - ``T(s1, s2) * T(s2, p2)`` + - ``T(s1 * s2, p1 + p2)`` - * - ``NUMERIC(s1, s2) / NUMERIC(s2, p2)`` - - ``NUMERIC(s1 * s2, p1 + p2)`` + * - ``T(s1, s2) / T(s2, p2)`` + - ``T(s1 * s2, p1 + p2)`` Examples: .. code-block:: sql - VALUES CAST(10.24 AS NUMERIC(4,2)) + CAST(12.123 AS NUMERIC(8,3)); - -- 22.36 as NUMERIC(32, 3) + VALUES CAST(10.24 AS DECIMAL(4,2)) + CAST(12.123 AS DECIMAL(8,3)); + -- 22.36 as DECIMAL(32, 3) - VALUES CAST(10.24 AS NUMERIC(4,2)) * CAST(12.123 AS NUMERIC(8,3)); - -- 124.13952 as NUMERIC(32, 5) + VALUES CAST(10.24 AS DECIMAL(4,2)) * CAST(12.123 AS DECIMAL(8,3)); + -- 124.13952 as DECIMAL(32, 5) Casting ------- diff --git a/tests/comparison.sql b/tests/comparison.sql index 8e978bd..6b5081a 100644 --- a/tests/comparison.sql +++ b/tests/comparison.sql @@ -33,3 +33,21 @@ VALUES 1 <= 2; VALUES 1 <= 1; -- COL1: TRUE + +VALUES 1.0 = 1; +-- COL1: TRUE + +VALUES CAST(1.0 AS NUMERIC(5, 3)) = CAST(1 AS NUMERIC(5, 4)); +-- COL1: TRUE + +VALUES CAST(1.0 AS DECIMAL(5, 3)) = CAST(1 AS DECIMAL(5, 4)); +-- COL1: TRUE + +VALUES CAST(1.0 AS DECIMAL) = CAST(1 AS DECIMAL); +-- COL1: TRUE + +VALUES CAST(1.0 AS DECIMAL) = 1; +-- COL1: TRUE + +VALUES CAST(1 AS DECIMAL) = 1.0; +-- COL1: TRUE diff --git a/tests/decimal.sql b/tests/decimal.sql index 4f22906..f00d52c 100644 --- a/tests/decimal.sql +++ b/tests/decimal.sql @@ -17,7 +17,7 @@ SELECT -bar FROM foo; -- COL1: 1.24 VALUES CAST(1.23 AS DECIMAL(10, 6)); --- COL1: 1.230000 +-- COL1: 1.23 VALUES CAST(1.23 AS DECIMAL(10, 2)); -- COL1: 1.23 @@ -44,19 +44,19 @@ VALUES CAST(-12.34 AS DECIMAL(3, 2)); -- error 22003: numeric value out of range VALUES CAST(1.23 AS DECIMAL(6, 2)) + CAST(1.5 AS DECIMAL(6, 3)); --- COL1: 2.730 +-- COL1: 2.73 VALUES CAST(1.23 AS DECIMAL(6, 2)) - CAST(1.5 AS DECIMAL(6, 3)); --- COL1: -0.270 +-- COL1: -0.27 VALUES CAST(1.23 AS DECIMAL(6, 2)) - CAST(-1.5 AS DECIMAL(6, 3)); --- COL1: 2.730 +-- COL1: 2.73 VALUES CAST(1.23 AS DECIMAL(6, 2)) * CAST(1.5 AS DECIMAL(6, 3)); --- COL1: 1.84500 +-- COL1: 1.845 VALUES CAST(CAST(1.23 AS DECIMAL(6, 2)) * CAST(1.5 AS DECIMAL(6, 3)) AS DECIMAL(6, 4)); --- COL1: 1.8450 +-- COL1: 1.845 VALUES CAST(1.24 AS DECIMAL(6, 2)) / CAST(1.5 AS DECIMAL(6, 3)); -- COL1: 0.82666 @@ -67,29 +67,39 @@ VALUES CAST(1.24 AS DECIMAL(6, 3)) / CAST(1.5 AS DECIMAL(6, 2)); VALUES CAST(CAST(1.24 AS DECIMAL(6, 2)) / CAST(1.5 AS DECIMAL(6, 3)) AS DECIMAL(6, 4)); -- COL1: 0.8266 +/* types */ VALUES CAST(1.23 AS DECIMAL(3,2)) / 5; --- error 42883: operator does not exist: DECIMAL / SMALLINT +-- COL1: 0.24 (DECIMAL(3, 2)) + +/* types */ +VALUES CAST(CAST(1.23 AS DECIMAL(3,2)) / 5 AS DECIMAL(4, 3)); +-- COL1: 0.246 (DECIMAL(4, 3)) + +-- # This is an important case because it's described in detail in the docs for +-- # DECIMAL vs DECIMAL. +VALUES CAST(1.23 AS DECIMAL(3,2)) / 5 * 5; +-- COL1: 1.23 -- # This is an important case because it's described in detail in the docs for --- # NUMERIC vs DECIMAL. -VALUES CAST(CAST(5 AS DECIMAL(3,2)) / CAST(7 AS DECIMAL) AS DECIMAL(5,4)); --- COL1: 0.7100 +-- # DECIMAL vs DECIMAL. +VALUES CAST(1.23 AS DECIMAL(3,2)) / 11; +-- COL1: 0.11 +-- # This is an important case because it's described in detail in the docs for +-- # DECIMAL vs DECIMAL. +VALUES CAST(CAST(5 AS DECIMAL(3,2)) / CAST(7 AS DECIMAL(5,4)) AS DECIMAL(5,4)); +-- COL1: 0.7142 + +/* types */ VALUES CAST(10.24 AS DECIMAL(4,2)) + CAST(12.123 AS DECIMAL(8,3)); --- COL1: 22.360 +-- COL1: 22.36 (DECIMAL(8, 3)) +/* types */ VALUES CAST(10.24 AS DECIMAL(4,2)) * CAST(12.123 AS DECIMAL(8,3)); --- COL1: 124.13952 +-- COL1: 124.13952 (DECIMAL(32, 5)) --- # This is an important case because it's described in detail in the docs for --- # NUMERIC vs DECIMAL. -VALUES CAST(1.23 AS DECIMAL(3,2)) / CAST(5 AS DECIMAL) * CAST(5 AS DECIMAL); --- COL1: 1.20 +VALUES CAST(1 AS DECIMAL(2,1)) / CAST(3 AS DECIMAL(2,1)); +-- COL1: 0.33 -VALUES CAST(5 AS DECIMAL(3,2)) / CAST(7 AS DECIMAL(5,4)); --- COL1: 0.714285 - --- # This is an important case because it's described in detail in the docs for --- # NUMERIC vs DECIMAL. -VALUES CAST(1.23 AS DECIMAL(3,2)) / CAST(5 AS DECIMAL) * CAST(5 AS DECIMAL); --- COL1: 1.20 +VALUES CAST(CAST(1 AS DECIMAL(2,1)) / CAST(3 AS DECIMAL(2,1)) AS DECIMAL(10, 8)); +-- COL1: 0.33333333 diff --git a/tests/numeric.sql b/tests/numeric.sql index 9b05074..661f8ea 100644 --- a/tests/numeric.sql +++ b/tests/numeric.sql @@ -77,28 +77,32 @@ VALUES CAST(CAST(1.24 AS NUMERIC(6, 2)) / CAST(1.5 AS NUMERIC(6, 3)) AS NUMERIC( VALUES CAST(1.23 AS NUMERIC(3,2)) / 5; -- COL1: 0.24 -VALUES CAST(CAST(1.23 AS NUMERIC(3,2)) / 5 AS NUMERIC(4, 3)); --- COL1: 0.246 - -- # This is an important case because it's described in detail in the docs for --- # NUMERIC vs DECIMAL. -VALUES CAST(1.23 AS NUMERIC(3,2)) / 5 * 5; --- COL1: 1.23 +-- # NUMERIC vs NUMERIC. +VALUES CAST(CAST(5 AS NUMERIC(3,2)) / CAST(7 AS NUMERIC) AS NUMERIC(5,4)); +-- COL1: 0.71 + +VALUES CAST(10.24 AS NUMERIC(4,2)) + CAST(12.123 AS NUMERIC(8,3)); +-- COL1: 22.36 + +VALUES CAST(10.24 AS NUMERIC(4,2)) * CAST(12.123 AS NUMERIC(8,3)); +-- COL1: 124.13952 -- # This is an important case because it's described in detail in the docs for --- # NUMERIC vs DECIMAL. -VALUES CAST(1.23 AS NUMERIC(3,2)) / 11; --- COL1: 0.11 +-- # NUMERIC vs NUMERIC. +VALUES CAST(1.23 AS NUMERIC(3,2)) / CAST(5 AS NUMERIC) * CAST(5 AS NUMERIC); +-- COL1: 1.2 + +VALUES CAST(5 AS NUMERIC(3,2)) / CAST(7 AS NUMERIC(5,4)); +-- COL1: 0.714285 -- # This is an important case because it's described in detail in the docs for --- # NUMERIC vs DECIMAL. -VALUES CAST(CAST(5 AS NUMERIC(3,2)) / CAST(7 AS NUMERIC(5,4)) AS NUMERIC(5,4)); --- COL1: 0.7142 +-- # NUMERIC vs NUMERIC. +VALUES CAST(1.23 AS NUMERIC(3,2)) / CAST(5 AS NUMERIC) * CAST(5 AS NUMERIC); +-- COL1: 1.2 -/* types */ -VALUES CAST(10.24 AS NUMERIC(4,2)) + CAST(12.123 AS NUMERIC(8,3)); --- COL1: 22.36 (NUMERIC(8, 3)) +VALUES CAST(1 AS NUMERIC(2,1)) / CAST(3 AS NUMERIC(2,1)); +-- COL1: 0.33 -/* types */ -VALUES CAST(10.24 AS NUMERIC(4,2)) * CAST(12.123 AS NUMERIC(8,3)); --- COL1: 124.13952 (NUMERIC(32, 5)) +VALUES CAST(CAST(1 AS NUMERIC(2,1)) / CAST(3 AS NUMERIC(2,1)) AS NUMERIC(10, 8)); +-- COL1: 0.33 diff --git a/vsql/numeric.v b/vsql/numeric.v index 765906c..498d353 100644 --- a/vsql/numeric.v +++ b/vsql/numeric.v @@ -298,13 +298,13 @@ fn (n Numeric) normalize_denominator(typ Type) Numeric { denominator := big.integer_from_int(10).pow(u32(typ.scale)) max_denominator := big.integer_from_int(10).pow(u32(typ.scale + 1)) - big.one_int - // NUMERICAL only need to scale when the denominator goes beyond the bounds. - if typ.typ == .is_numeric && n.denominator > max_denominator { + // DECIMAL only need to scale when the denominator goes beyond the bounds. + if typ.typ == .is_decimal && n.denominator > max_denominator { return n.scale_numerator(denominator) } - // DECIMAL always needs to have a fixed denominator. - if typ.typ == .is_decimal && n.denominator != denominator { + // NUMERIC always needs to have a fixed denominator. + if typ.typ == .is_numeric && n.denominator != denominator { return n.scale_numerator(denominator) } @@ -326,6 +326,20 @@ fn common_denominator(n1 Numeric, n2 Numeric) (Numeric, Numeric) { return n3, n4 } +fn (n Numeric) compare(n2 Numeric) CompareResult { + n3, n4 := common_denominator(n, n2) + + if n3.numerator < n4.numerator { + return .is_less + } + + if n3.numerator > n4.numerator { + return .is_greater + } + + return .is_equal +} + fn (n Numeric) equals(n2 Numeric) bool { n3, n4 := common_denominator(n, n2) diff --git a/vsql/operators.v b/vsql/operators.v index 3abb464..1967a7b 100644 --- a/vsql/operators.v +++ b/vsql/operators.v @@ -277,5 +277,9 @@ fn binary_decimal_multiply_decimal(conn &Connection, a Value, b Value) !Value { } fn binary_decimal_divide_decimal(conn &Connection, a Value, b Value) !Value { + if b.as_f64()! == 0 { + return sqlstate_22012() // division by zero + } + return new_decimal_value_from_numeric(a.numeric_value().divide(b.numeric_value())!) } diff --git a/vsql/std_comparison_predicate.v b/vsql/std_comparison_predicate.v index ffdd402..4143719 100644 --- a/vsql/std_comparison_predicate.v +++ b/vsql/std_comparison_predicate.v @@ -385,9 +385,27 @@ fn compare_datetimes(xv Value, yv Value) CompareResult { fn compare_numbers(xv Value, yv Value) !CompareResult { // 2) Numbers are compared with respect to their algebraic value. - // - // TODO(elliotchance): as_f64() will not be safe when NUMERIC and DECIMAL are - // supported, so special handling will be required. + if (xv.typ.typ == .is_decimal || xv.typ.typ == .is_numeric) + && (yv.typ.typ == .is_decimal || yv.typ.typ == .is_numeric) { + return xv.numeric_value().compare(yv.numeric_value()) + } + + if (xv.typ.typ == .is_smallint || xv.typ.typ == .is_integer || xv.typ.typ == .is_bigint) + && (yv.typ.typ == .is_smallint || yv.typ.typ == .is_integer || yv.typ.typ == .is_bigint) { + x := xv.int_value() + y := yv.int_value() + + if x < y { + return .is_less + } + + if x > y { + return .is_greater + } + + return .is_equal + } + x := xv.as_f64()! y := yv.as_f64()! diff --git a/vsql/std_numeric_value_expression.v b/vsql/std_numeric_value_expression.v index 6ac3cb9..80cd83a 100644 --- a/vsql/std_numeric_value_expression.v +++ b/vsql/std_numeric_value_expression.v @@ -214,11 +214,6 @@ fn eval_binary(mut conn Connection, data Row, x Value, op string, y Value, param mut left := x mut right := y - // There is a special case we need to deal with when using literals against - // other approximate types. - // - // TODO(elliotchance): This won't be needed when we properly implement - // ISO/IEC 9075-2:2016(E), 6.29, mut key := '${left.typ.typ} ${op} ${right.typ.typ}' if left.typ.typ.is_number() && right.typ.typ.is_number() { supertype := most_specific_type(left.typ, right.typ) or { diff --git a/vsql/std_store_assignment.v b/vsql/std_store_assignment.v index ed3262c..096bdc2 100644 --- a/vsql/std_store_assignment.v +++ b/vsql/std_store_assignment.v @@ -394,7 +394,7 @@ fn cast_numeric(mut conn Connection, v Value, t Type) !Value { // There is a special case where we should pass through an already // NUMERIC/DECIMAL value if there is no specified destination precision. // This will prevent as_numeric() from truncating the extra NUMERIC - // precision. Technically this is not required for DECIMAL, but it's just + // precision. Technically this is not required for NUMERIC, but it's just // more efficient to avoid the reparsing below. if t.size == 0 && (v.typ.typ == .is_numeric || v.typ.typ == .is_decimal) { return v diff --git a/vsql/type.v b/vsql/type.v index 9c10b7d..b75ccb1 100644 --- a/vsql/type.v +++ b/vsql/type.v @@ -57,8 +57,13 @@ fn (t SQLType) str() string { fn (t SQLType) is_number() bool { return match t { - .is_bigint, .is_double_precision, .is_integer, .is_real, .is_smallint, .is_numeric { true } - else { false } + .is_bigint, .is_double_precision, .is_integer, .is_real, .is_smallint, .is_decimal, + .is_numeric { + true + } + else { + false + } } } @@ -106,10 +111,10 @@ fn (t SQLType) supertype() (i16, i16) { .is_bigint { 2, 2 } - .is_decimal { + .is_numeric { 2, 3 } - .is_numeric { + .is_decimal { 2, 4 } // Approximate numeric types. diff --git a/vsql/value.v b/vsql/value.v index 57825e2..0295d9f 100644 --- a/vsql/value.v +++ b/vsql/value.v @@ -196,9 +196,6 @@ pub fn new_numeric_value(x string) Value { // 12.00 -> DECIMAL(4, 2) // pub fn new_decimal_value(x string) Value { - // All the same rules for determining NUMERIC can be used for DECIMAL, - // including denoninator being a power of 10. We just need to change it to a - // DECIMAL type. n := new_numeric_from_string(x) typ := new_type('DECIMAL', n.typ.size, n.typ.scale) @@ -339,7 +336,7 @@ fn (v Value) as_numeric() !Numeric { return sqlstate_22003() } - if v.typ.typ == .is_numeric { + if v.typ.typ == .is_numeric || v.typ.typ == .is_decimal { return v.numeric_value() } @@ -387,10 +384,7 @@ pub fn (v Value) str() string { .is_timestamp_with_time_zone, .is_timestamp_without_time_zone { v.time_value().str() } - .is_decimal { - v.numeric_value().str() - } - .is_numeric { + .is_decimal, .is_numeric { s := v.numeric_value().str() if s.contains('.') { return s.trim_right('0').trim_right('.')