From 1a22339166cd3f0f319bf3b0cd92239b4926814e Mon Sep 17 00:00:00 2001 From: Elliot Chance Date: Sat, 23 Dec 2023 17:40:13 +0100 Subject: [PATCH] language: Integers now use the smallest exact type (#184) > ISO/IEC 9075-2:2016(E), 5.3 : > 22) The declared type of an ENL is an > implementation-defined exact numeric type whose scale is the number of > s to the right of the . There shall be an exact numeric > type capable of representing the value of ENL exactly. I misunderstood "numeric" to mean the NUMERIC type, but clearly they mean any exact numeric type (which includes SMALLINT, INTEGER and BIGINT). Integers will now use the smallest type which makes casting up to super-types much more straight forward. Also, SQL tests will not stop on the first failure making it much easier to cleanup multiple failing tests. --- docs/sqlstate.rst | 2 +- tests/arithmetic.sql | 10 ++-- tests/cast.sql | 2 +- tests/coalesce.sql | 16 ++++--- tests/concatenation.sql | 2 +- tests/fold.sql | 2 +- tests/insert.sql | 2 +- tests/literal.sql | 59 +++++++++++++++++++++-- tests/nullif.sql | 6 +-- tests/values.sql | 42 ++++++++--------- vsql/sql_test.v | 1 + vsql/std_literal.v | 73 ++++++++++++++++++++++------- vsql/std_numeric_value_expression.v | 43 +++++++++++++++++ 13 files changed, 198 insertions(+), 62 deletions(-) diff --git a/docs/sqlstate.rst b/docs/sqlstate.rst index c89c322..9794ab5 100644 --- a/docs/sqlstate.rst +++ b/docs/sqlstate.rst @@ -253,7 +253,7 @@ A client that receives this error should retry the transaction. .. code-block:: sql VALUES CAST(123 AS BOOLEAN); - -- error 42846: cannot coerce BIGINT to BOOLEAN + -- error 42846: cannot coerce SMALLINT to BOOLEAN ``42883`` function does not exist ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/arithmetic.sql b/tests/arithmetic.sql index afd4671..f7cc355 100644 --- a/tests/arithmetic.sql +++ b/tests/arithmetic.sql @@ -1,18 +1,18 @@ /* types */ VALUES 1 + 2; --- COL1: 3 (INTEGER) +-- COL1: 3 (SMALLINT) /* types */ VALUES 1 - 2; --- COL1: -1 (INTEGER) +-- COL1: -1 (SMALLINT) /* types */ VALUES 2 * 3; --- COL1: 6 (INTEGER) +-- COL1: 6 (SMALLINT) /* types */ VALUES 6 / 2; --- COL1: 3 (INTEGER) +-- COL1: 3 (SMALLINT) /* types */ VALUES 1.2 + 2.4; @@ -40,7 +40,7 @@ VALUES 2.5 / 0.0; /* types */ VALUES -123; --- COL1: -123 (NUMERIC) +-- COL1: -123 (SMALLINT) /* types */ VALUES +1.23; diff --git a/tests/cast.sql b/tests/cast.sql index 0aadfe7..e2d155b 100644 --- a/tests/cast.sql +++ b/tests/cast.sql @@ -40,7 +40,7 @@ VALUES CAST(123 AS SMALLINT); -- COL1: 123 (SMALLINT) VALUES CAST(123 AS BOOLEAN); --- error 42846: cannot coerce NUMERIC to BOOLEAN +-- error 42846: cannot coerce SMALLINT to BOOLEAN EXPLAIN VALUES CAST(123 AS SMALLINT); -- EXPLAIN: VALUES (COL1 SMALLINT) = ROW(CAST(123 AS SMALLINT)) diff --git a/tests/coalesce.sql b/tests/coalesce.sql index 03f080d..e23dab9 100644 --- a/tests/coalesce.sql +++ b/tests/coalesce.sql @@ -1,19 +1,23 @@ EXPLAIN VALUES COALESCE(1); --- EXPLAIN: VALUES (COL1 NUMERIC) = ROW(COALESCE(1)) +-- EXPLAIN: VALUES (COL1 SMALLINT) = ROW(COALESCE(1)) EXPLAIN VALUES COALESCE(1, 2); --- EXPLAIN: VALUES (COL1 NUMERIC) = ROW(COALESCE(1, 2)) +-- EXPLAIN: VALUES (COL1 SMALLINT) = ROW(COALESCE(1, 2)) EXPLAIN VALUES COALESCE(1, 2, 3); --- EXPLAIN: VALUES (COL1 NUMERIC) = ROW(COALESCE(1, 2, 3)) +-- EXPLAIN: VALUES (COL1 SMALLINT) = ROW(COALESCE(1, 2, 3)) /* types */ VALUES COALESCE(1); --- COL1: 1 (NUMERIC) +-- COL1: 1 (SMALLINT) /* types */ VALUES COALESCE(1, 2); --- COL1: 1 (NUMERIC) +-- COL1: 1 (SMALLINT) + +/* types */ +VALUES COALESCE(1, 200000000); +-- error 42804: data type mismatch in argument 2 of COALESCE: expected SMALLINT but got INTEGER CREATE TABLE foo (f1 BIGINT); INSERT INTO foo (f1) VALUES (NULL); @@ -29,4 +33,4 @@ SELECT COALESCE(f1) FROM foo; -- COL1: 3 VALUES COALESCE(1, 'hello'); --- error 42804: data type mismatch in argument 2 of COALESCE: expected NUMERIC but got CHARACTER +-- error 42804: data type mismatch in argument 2 of COALESCE: expected SMALLINT but got CHARACTER diff --git a/tests/concatenation.sql b/tests/concatenation.sql index d251e71..10d5d6a 100644 --- a/tests/concatenation.sql +++ b/tests/concatenation.sql @@ -3,4 +3,4 @@ VALUES 'foo' || 'bar'; -- COL1: foobar (CHARACTER VARYING(6)) VALUES 123 || 'bar'; --- error 42883: operator does not exist: NUMERIC || CHARACTER +-- error 42883: operator does not exist: SMALLINT || CHARACTER diff --git a/tests/fold.sql b/tests/fold.sql index 4688c95..2b6ab60 100644 --- a/tests/fold.sql +++ b/tests/fold.sql @@ -7,7 +7,7 @@ VALUES LOWER('Hello'); -- COL1: hello (CHARACTER VARYING(5)) VALUES UPPER(123); --- error 42883: function does not exist: UPPER(DOUBLE PRECISION) +-- error 42883: function does not exist: UPPER(SMALLINT) VALUES LOWER(TRUE); -- error 42883: function does not exist: LOWER(BOOLEAN) diff --git a/tests/insert.sql b/tests/insert.sql index 997cf47..5067b45 100644 --- a/tests/insert.sql +++ b/tests/insert.sql @@ -47,7 +47,7 @@ INSERT INTO foo (x) VALUES (true); CREATE TABLE foo (b BOOLEAN); INSERT INTO foo (b) VALUES (123); -- msg: CREATE TABLE 1 --- error 42846: cannot coerce NUMERIC to BOOLEAN +-- error 42846: cannot coerce SMALLINT to BOOLEAN CREATE TABLE t1 (f1 CHARACTER VARYING(10), f2 FLOAT NOT NULL); INSERT INTO t1 (f1, f2) VALUES ('a', 1.23); diff --git a/tests/literal.sql b/tests/literal.sql index 23c8102..4ade5cb 100644 --- a/tests/literal.sql +++ b/tests/literal.sql @@ -1,6 +1,6 @@ /* types */ VALUES 1; --- COL1: 1 (NUMERIC) +-- COL1: 1 (SMALLINT) /* types */ VALUES 1.23; @@ -16,7 +16,7 @@ VALUES .23; /* types */ VALUES 789; --- COL1: 789 (NUMERIC) +-- COL1: 789 (SMALLINT) /* types */ VALUES 'hello'; @@ -24,8 +24,59 @@ VALUES 'hello'; /* types */ VALUES ROW(123, 456); --- COL1: 123 (NUMERIC) COL2: 456 (NUMERIC) +-- COL1: 123 (SMALLINT) COL2: 456 (SMALLINT) /* types */ VALUES ROW(2 + 3 * 5, (2 + 3) * 5); --- COL1: 17 (INTEGER) COL2: 25 (INTEGER) +-- COL1: 17 (SMALLINT) COL2: 25 (SMALLINT) + +-- # These are range tests to make sure that literals are given the correct +-- # smallest exact types. + +/* types */ +VALUES -9223372036854775809; +-- COL1: -9223372036854775809 (NUMERIC) + +/* types */ +VALUES -9223372036854775808; +-- COL1: -9223372036854775808 (BIGINT) + +/* types */ +VALUES -2147483649; +-- COL1: -2147483649 (BIGINT) + +/* types */ +VALUES -2147483648; +-- COL1: -2147483648 (INTEGER) + +/* types */ +VALUES -32769; +-- COL1: -32769 (INTEGER) + +/* types */ +VALUES -32768; +-- COL1: -32768 (SMALLINT) + +/* types */ +VALUES 32767; +-- COL1: 32767 (SMALLINT) + +/* types */ +VALUES 32768; +-- COL1: 32768 (INTEGER) + +/* types */ +VALUES 2147483647; +-- COL1: 2147483647 (INTEGER) + +/* types */ +VALUES 2147483648; +-- COL1: 2147483648 (BIGINT) + +/* types */ +VALUES 9223372036854775807; +-- COL1: 9223372036854775807 (BIGINT) + +/* types */ +VALUES 9223372036854775808; +-- COL1: 9223372036854775808 (NUMERIC) diff --git a/tests/nullif.sql b/tests/nullif.sql index 6c42597..791bf57 100644 --- a/tests/nullif.sql +++ b/tests/nullif.sql @@ -3,11 +3,11 @@ EXPLAIN VALUES NULLIF(TRUE, TRUE); /* types */ VALUES NULLIF(123, 123); --- COL1: NULL (NUMERIC) +-- COL1: NULL (SMALLINT) /* types */ VALUES NULLIF(123, 456); --- COL1: 123 (NUMERIC) +-- COL1: 123 (SMALLINT) VALUES NULLIF(123, 'hello'); --- error 42804: data type mismatch in NULLIF: expected NUMERIC but got CHARACTER(5) +-- error 42804: data type mismatch in NULLIF: expected SMALLINT but got CHARACTER(5) diff --git a/tests/values.sql b/tests/values.sql index 58f2c2e..a124ab5 100644 --- a/tests/values.sql +++ b/tests/values.sql @@ -23,9 +23,9 @@ SELECT * FROM (VALUES 1, 'foo', TRUE); EXPLAIN SELECT * FROM (VALUES ROW(1, 'foo', TRUE)); -- EXPLAIN: $1: --- EXPLAIN: VALUES (COL1 NUMERIC, COL2 CHARACTER VARYING(3), COL3 BOOLEAN) = ROW(1, 'foo', TRUE) --- EXPLAIN: TABLE $1 (COL1 NUMERIC, COL2 CHARACTER VARYING(3), COL3 BOOLEAN) --- EXPLAIN: EXPR ($1.COL1 NUMERIC, $1.COL2 CHARACTER VARYING(3), $1.COL3 BOOLEAN) +-- EXPLAIN: VALUES (COL1 SMALLINT, COL2 CHARACTER VARYING(3), COL3 BOOLEAN) = ROW(1, 'foo', TRUE) +-- EXPLAIN: TABLE $1 (COL1 SMALLINT, COL2 CHARACTER VARYING(3), COL3 BOOLEAN) +-- EXPLAIN: EXPR ($1.COL1 SMALLINT, $1.COL2 CHARACTER VARYING(3), $1.COL3 BOOLEAN) EXPLAIN SELECT * FROM (VALUES 1, 'foo', TRUE) AS t1 (abc, col2, "f"); -- EXPLAIN: T1: @@ -35,9 +35,9 @@ EXPLAIN SELECT * FROM (VALUES 1, 'foo', TRUE) AS t1 (abc, col2, "f"); EXPLAIN SELECT * FROM (VALUES ROW(1, 'foo', TRUE)) AS t1 (abc, col2, "f"); -- EXPLAIN: T1: --- EXPLAIN: VALUES (ABC NUMERIC, COL2 CHARACTER VARYING(3), "f" BOOLEAN) = ROW(1, 'foo', TRUE) --- EXPLAIN: TABLE T1 (ABC NUMERIC, COL2 CHARACTER VARYING(3), "f" BOOLEAN) --- EXPLAIN: EXPR (T1.ABC NUMERIC, T1.COL2 CHARACTER VARYING(3), T1."f" BOOLEAN) +-- EXPLAIN: VALUES (ABC SMALLINT, COL2 CHARACTER VARYING(3), "f" BOOLEAN) = ROW(1, 'foo', TRUE) +-- EXPLAIN: TABLE T1 (ABC SMALLINT, COL2 CHARACTER VARYING(3), "f" BOOLEAN) +-- EXPLAIN: EXPR (T1.ABC SMALLINT, T1.COL2 CHARACTER VARYING(3), T1."f" BOOLEAN) SELECT * FROM (VALUES 1, 'foo', TRUE) AS t1 (abc, col2, "f"); -- error 42601: syntax error: unknown column: T1.COL2 @@ -81,9 +81,9 @@ SELECT * FROM (VALUES ROW(123), ROW(456)); EXPLAIN SELECT * FROM (VALUES ROW(123), ROW(456)); -- EXPLAIN: $1: --- EXPLAIN: VALUES (COL1 NUMERIC) = ROW(123), ROW(456) --- EXPLAIN: TABLE $1 (COL1 NUMERIC) --- EXPLAIN: EXPR ($1.COL1 NUMERIC) +-- EXPLAIN: VALUES (COL1 SMALLINT) = ROW(123), ROW(456) +-- EXPLAIN: TABLE $1 (COL1 SMALLINT) +-- EXPLAIN: EXPR ($1.COL1 SMALLINT) SELECT * FROM (VALUES ROW(123, 'hi'), ROW(456, 'there')); -- COL1: 123 COL2: hi @@ -91,9 +91,9 @@ SELECT * FROM (VALUES ROW(123, 'hi'), ROW(456, 'there')); EXPLAIN SELECT * FROM (VALUES ROW(123, 'hi'), ROW(456, 'there')); -- EXPLAIN: $1: --- EXPLAIN: VALUES (COL1 NUMERIC, COL2 CHARACTER VARYING(2)) = ROW(123, 'hi'), ROW(456, 'there') --- EXPLAIN: TABLE $1 (COL1 NUMERIC, COL2 CHARACTER VARYING(2)) --- EXPLAIN: EXPR ($1.COL1 NUMERIC, $1.COL2 CHARACTER VARYING(2)) +-- EXPLAIN: VALUES (COL1 SMALLINT, COL2 CHARACTER VARYING(2)) = ROW(123, 'hi'), ROW(456, 'there') +-- EXPLAIN: TABLE $1 (COL1 SMALLINT, COL2 CHARACTER VARYING(2)) +-- EXPLAIN: EXPR ($1.COL1 SMALLINT, $1.COL2 CHARACTER VARYING(2)) SELECT * FROM (VALUES ROW(123, 'hi'), ROW(456, 'there')) AS foo (bar, baz); -- BAR: 123 BAZ: hi @@ -102,9 +102,9 @@ SELECT * FROM (VALUES ROW(123, 'hi'), ROW(456, 'there')) AS foo (bar, baz); EXPLAIN SELECT * FROM (VALUES ROW(123, 'hi'), ROW(456, 'there')) AS foo (bar, baz); -- EXPLAIN: FOO: --- EXPLAIN: VALUES (BAR NUMERIC, BAZ CHARACTER VARYING(2)) = ROW(123, 'hi'), ROW(456, 'there') --- EXPLAIN: TABLE FOO (BAR NUMERIC, BAZ CHARACTER VARYING(2)) --- EXPLAIN: EXPR (FOO.BAR NUMERIC, FOO.BAZ CHARACTER VARYING(2)) +-- EXPLAIN: VALUES (BAR SMALLINT, BAZ CHARACTER VARYING(2)) = ROW(123, 'hi'), ROW(456, 'there') +-- EXPLAIN: TABLE FOO (BAR SMALLINT, BAZ CHARACTER VARYING(2)) +-- EXPLAIN: EXPR (FOO.BAR SMALLINT, FOO.BAZ CHARACTER VARYING(2)) SELECT * FROM (VALUES 1, 2) AS t1 (foo); -- FOO: 1 @@ -126,10 +126,10 @@ SELECT * FROM (VALUES ROW(1), ROW(3, 4)) AS t1 (foo, bar); EXPLAIN SELECT * FROM (VALUES ROW(1, 2), ROW(3, 4), ROW(5, 6)) AS t1 (foo, bar) FETCH FIRST 2 ROWS ONLY; -- EXPLAIN: T1: --- EXPLAIN: VALUES (FOO NUMERIC, BAR NUMERIC) = ROW(1, 2), ROW(3, 4), ROW(5, 6) --- EXPLAIN: TABLE T1 (FOO NUMERIC, BAR NUMERIC) +-- EXPLAIN: VALUES (FOO SMALLINT, BAR NUMERIC) = ROW(1, 2), ROW(3, 4), ROW(5, 6) +-- EXPLAIN: TABLE T1 (FOO SMALLINT, BAR NUMERIC) -- EXPLAIN: FETCH FIRST 2 ROWS ONLY --- EXPLAIN: EXPR (T1.FOO NUMERIC, T1.BAR NUMERIC) +-- EXPLAIN: EXPR (T1.FOO SMALLINT, T1.BAR NUMERIC) SELECT * FROM (VALUES ROW(1, 2), ROW(3, 4), ROW(5, 6)) AS t1 (foo, bar) FETCH FIRST 2 ROWS ONLY; @@ -139,10 +139,10 @@ FETCH FIRST 2 ROWS ONLY; EXPLAIN SELECT * FROM (VALUES ROW(1, 2), ROW(3, 4), ROW(5, 6)) AS t1 (foo, bar) OFFSET 1 ROW; -- EXPLAIN: T1: --- EXPLAIN: VALUES (FOO NUMERIC, BAR NUMERIC) = ROW(1, 2), ROW(3, 4), ROW(5, 6) --- EXPLAIN: TABLE T1 (FOO NUMERIC, BAR NUMERIC) +-- EXPLAIN: VALUES (FOO SMALLINT, BAR NUMERIC) = ROW(1, 2), ROW(3, 4), ROW(5, 6) +-- EXPLAIN: TABLE T1 (FOO SMALLINT, BAR NUMERIC) -- EXPLAIN: OFFSET 1 ROWS --- EXPLAIN: EXPR (T1.FOO NUMERIC, T1.BAR NUMERIC) +-- EXPLAIN: EXPR (T1.FOO SMALLINT, T1.BAR NUMERIC) SELECT * FROM (VALUES ROW(1, 2), ROW(3, 4), ROW(5, 6)) AS t1 (foo, bar) OFFSET 1 ROW; diff --git a/vsql/sql_test.v b/vsql/sql_test.v index 60a089d..4f0cd4b 100644 --- a/vsql/sql_test.v +++ b/vsql/sql_test.v @@ -154,6 +154,7 @@ fn test_all() ! { } } +@[assert_continues] fn run_single_test(test SQLTest, query_cache &QueryCache, verbose bool, filter_line int) ! { if filter_line != 0 && test.line_number != filter_line { if verbose { diff --git a/vsql/std_literal.v b/vsql/std_literal.v index 66bf4bf..152d2e1 100644 --- a/vsql/std_literal.v +++ b/vsql/std_literal.v @@ -1,5 +1,7 @@ module vsql +import math.big + // ISO/IEC 9075-2:2016(E), 5.3, // // # Function @@ -13,7 +15,7 @@ module vsql //~ | //~ //~ /* Value */ ::= -//~ +//~ -> signed_numeric_literal_1 //~ | //~ //~ /* Value */ ::= @@ -25,17 +27,17 @@ module vsql //~ ^string //~ //~ /* Value */ ::= -//~ +//~ -> signed_numeric_literal_1 //~ | -> signed_numeric_literal_2 //~ -//~ /* Value */ ::= +//~ /* string */ ::= //~ //~ -//~ /* Value */ ::= -//~ -> int_value -//~ | -> int_value -//~ | -> exact_numeric_literal_1 -//~ | -> exact_numeric_literal_2 +//~ /* string */ ::= +//~ -> exact_numeric_literal_1 +//~ | -> exact_numeric_literal_2 +//~ | -> exact_numeric_literal_3 +//~ | -> exact_numeric_literal_4 //~ //~ /* string */ ::= //~ @@ -72,16 +74,20 @@ module vsql //~ | FALSE -> false //~ | UNKNOWN -> unknown -fn parse_int_value(x string) !Value { - return new_numeric_value(x) +fn parse_exact_numeric_literal_1(x string) !string { + return x +} + +fn parse_exact_numeric_literal_2(a string) !string { + return '${a}.' } -fn parse_exact_numeric_literal_1(a string, b string) !Value { - return new_numeric_value('${a}.${b}') +fn parse_exact_numeric_literal_3(a string, b string) !string { + return '${a}.${b}' } -fn parse_exact_numeric_literal_2(a string) !Value { - return new_numeric_value('0.${a}') +fn parse_exact_numeric_literal_4(a string) !string { + return '0.${a}' } fn parse_date_literal(v Value) !Value { @@ -96,10 +102,41 @@ fn parse_timestamp_literal(v Value) !Value { return new_timestamp_value(v.string_value()) } -fn parse_signed_numeric_literal_2(sign string, v Value) !Value { - if sign == '-' { - return new_numeric_value('-' + v.str()) +fn numeric_literal(x string) !Value { + // Any number that contains a decimal (even if its a whole number) must be + // treated as a NUMERIC. + if x.contains('.') { + // The trim handles cases of "123." which should be treated as "123". + return new_numeric_value(x.trim_right('.')) + } + + // Otherwise, we know this is an int but we have to choose the smallest type. + // + // Note: There is an edge case where the negative sign may be consumed as part + // of rather than . See parse_factor_2() for + // those edge cases. + n := big.integer_from_string(x)! + + if n >= big.integer_from_i64(-32768) && n <= big.integer_from_i64(32767) { + return new_smallint_value(i16(x.i64())) + } + + if n >= big.integer_from_i64(-2147483648) && n <= big.integer_from_i64(2147483647) { + return new_integer_value(int(x.i64())) + } + + if n >= big.integer_from_i64(-9223372036854775808) + && n <= big.integer_from_i64(9223372036854775807) { + return new_bigint_value(x.i64()) } - return v + return new_numeric_value(x) +} + +fn parse_signed_numeric_literal_1(v string) !Value { + return numeric_literal(v) +} + +fn parse_signed_numeric_literal_2(sign string, v string) !Value { + return numeric_literal(sign + v) } diff --git a/vsql/std_numeric_value_expression.v b/vsql/std_numeric_value_expression.v index 3f34bfa..b2ed44e 100644 --- a/vsql/std_numeric_value_expression.v +++ b/vsql/std_numeric_value_expression.v @@ -146,7 +146,50 @@ fn (e NumericPrimary) compile(mut c Compiler) !CompileResult { } } +fn (e NumericPrimary) value() ?Value { + if e is ValueExpressionPrimary { + if e is NonparenthesizedValueExpressionPrimary { + if e is ValueSpecification { + if e is Value { + return e + } + } + } + } + + return none +} + fn parse_factor_2(sign string, expr NumericPrimary) !NumericPrimary { + // Due to the graph nature of the parser, a negative number might end up be + // parsed as an and take the sign from this stage + // instead. + // + // Normally that wouldn't be an issue because it would negate the value at + // runtime. However, literals needs to be given the smallest exact type that + // encapsulates the value. This leaves an edge case where negative values need + // to support one value lower than their positive counterpart. + // + // For example, SMALLINT range is -32768 to 32767. However, if we consume + // 32768 first it would be given an INTEGER type (because it's out of range) + // so the negation would also be an INTEGER when -32768 really should be + // stored in a SMALLINT. + if v := expr.value() { + if sign == '-' { + if v.typ.typ == .is_integer && v.int_value() == 32768 { + return ValueExpressionPrimary(NonparenthesizedValueExpressionPrimary(ValueSpecification(new_smallint_value(-32768)))) + } + + if v.typ.typ == .is_bigint && v.int_value() == 2147483648 { + return ValueExpressionPrimary(NonparenthesizedValueExpressionPrimary(ValueSpecification(new_integer_value(-2147483648)))) + } + + if v.typ.typ == .is_numeric && v.string_value() == '9223372036854775808' { + return ValueExpressionPrimary(NonparenthesizedValueExpressionPrimary(ValueSpecification(new_bigint_value(-9223372036854775808)))) + } + } + } + return SignedValueExpressionPrimary{sign, expr} }