This is an automated email from the ASF dual-hosted git repository.
mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new a2e574ec27 [CALCITE-7870] RexSimplify removes some casts which change
semantics
a2e574ec27 is described below
commit a2e574ec271866eb352f98d7101a386a30dc8bc8
Author: Mihai Budiu <[email protected]>
AuthorDate: Mon Oct 19 11:55:11 2026 -0700
[CALCITE-7870] RexSimplify removes some casts which change semantics
Signed-off-by: Mihai Budiu <[email protected]>
---
.../java/org/apache/calcite/rex/RexSimplify.java | 106 ++++++++++++++-----
.../main/java/org/apache/calcite/rex/RexUtil.java | 22 +++-
.../calcite/rel/rel2sql/RelToSqlConverterTest.java | 4 +-
.../apache/calcite/rex/RexLosslessCastTest.java | 87 ++++++++++++++--
core/src/test/resources/sql/cast.iq | 112 +++++++++++++++++++++
5 files changed, 290 insertions(+), 41 deletions(-)
diff --git a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
index 0f5174a219..c8e62cb745 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
@@ -2692,44 +2692,29 @@ && sameTypeOrNarrowsNullability(e.getType(),
operand.getType())
}
if (RexUtil.isLosslessCast(operand)) {
// x :: y below means cast(x as y) (which is PostgreSQL-specific cast by
the way)
- // A) Remove lossless casts:
+ // A) Both casts can be removed:
// A.1) intExpr :: bigint :: int => intExpr
// A.2) char2Expr :: char(5) :: char(2) => char2Expr
- // B) There are cases when we can't remove two casts, but we could
probably remove inner one
- // B.1) char2expression :: char(4) :: char(5) -> char2expression ::
char(5)
- // B.2) char2expression :: char(10) :: char(5) -> char2expression ::
char(5)
- // B.3) char2expression :: varchar(10) :: char(5) -> char2expression ::
char(5)
- // B.4) char6expression :: varchar(10) :: char(5) -> char6expression ::
char(5)
- // C) Simplification is not possible:
- // C.1) char6expression :: char(3) :: char(5) -> must not be changed
+ // B) Simplification is not possible:
+ // B.1) char6expression :: char(3) :: char(5) => must not be changed
// the input is truncated to 3 chars, so we can't use
char6expression :: char(5)
- // C.2) varchar2Expr :: char(5) :: varchar(2) -> must not be changed
- // the input have to be padded with spaces (up to 2 chars)
- // C.3) char2expression :: char(4) :: varchar(5) -> must not be changed
+ // B.2) varchar2Expr :: char(5) :: varchar(2) => must not be changed
+ // the input has to be padded with spaces (up to 2 chars)
+ // B.3) char2expression :: char(4) :: varchar(5) => must not be changed
// would not have the padding
- // The approach seems to be:
- // 1) Ensure inner cast is lossless (see if above)
- // 2) If operand of the inner cast has the same type as the outer cast,
- // remove two casts except C.2 or C.3-like pattern (== inner cast is
CHAR)
- // 3) If outer cast is lossless, remove inner cast (B-like cases)
-
- // Here we try to remove two casts in one go (A-like cases)
RexNode intExpr = ((RexCall) operand).operands.get(0);
- // intExpr == CHAR detects A.1
- // operand != CHAR detects C.2
if ((intExpr.getType().getSqlTypeName() == SqlTypeName.CHAR
|| operand.getType().getSqlTypeName() != SqlTypeName.CHAR)
&& sameTypeOrNarrowsNullability(e.getType(), intExpr.getType())) {
+ // Remove both casts (A-like cases): the outer type is the source type,
+ // so the two casts are a round trip, except through a CHAR type from a
+ // non-CHAR source, as in int :: char(11) :: int: not every runtime
+ // parses the padded text back
return intExpr;
}
- // Here we try to remove inner cast (B-like cases)
if (RexUtil.isLosslessCast(intExpr.getType(), operand.getType())
- && (e.getType().getSqlTypeName() ==
operand.getType().getSqlTypeName()
- || e.getType().getSqlTypeName() == SqlTypeName.CHAR
- || operand.getType().getSqlTypeName() != SqlTypeName.CHAR)
- && SqlTypeCoercionRule.instance()
- .canApplyFrom(intExpr.getType().getSqlTypeName(),
e.getType().getSqlTypeName())) {
+ && canRemoveInnerCast(intExpr.getType(), operand.getType(),
e.getType())) {
return rexBuilder.makeCast(e.getParserPosition(), e.getType(),
intExpr);
}
}
@@ -2737,7 +2722,7 @@ && sameTypeOrNarrowsNullability(e.getType(),
intExpr.getType())) {
switch (operand.getKind()) {
case LITERAL:
final RexLiteral literal = (RexLiteral) operand;
- final Comparable value = literal.getValueAs(Comparable.class);
+ final Comparable<?> value = literal.getValueAs(Comparable.class);
final SqlTypeName typeName = literal.getTypeName();
// First, try to remove the cast without changing the value.
@@ -2787,6 +2772,73 @@ && sameTypeOrNarrowsNullability(e.getType(),
intExpr.getType())) {
}
}
+ /** Returns whether {@code CAST(CAST(x AS inner) AS outer)} can be
simplified to
+ * {@code CAST(x AS outer)}, where x has type {@code source} and the cast
from
+ * {@code source} to {@code inner} is lossless. */
+ private static boolean canRemoveInnerCast(RelDataType source, RelDataType
inner,
+ RelDataType outer) {
+ // x :: y below means cast(x as y)
+ // Removing the inner cast changes the type of the operand of the outer
cast
+ // from inner to source, but not its value. The result of the outer cast
can
+ // still change, because a cast to a character or binary type formats its
+ // operand according to the operand's type, and a cast of a character
operand
+ // parses its text.
+ // Cases A and B are in simplifyCast.
+ // C) The inner cast can be removed:
+ // C.1) char2expression :: char(4) :: char(5) => char2expression :: char(5)
+ // C.2) char2expression :: char(10) :: char(5) => char2expression ::
char(5)
+ // C.3) char2expression :: varchar(10) :: char(5) => char2expression ::
char(5)
+ // C.4) char6expression :: varchar(10) :: char(5) => char6expression ::
char(5)
+ // C.5) smallintExpr :: int :: varchar => smallintExpr :: varchar
+ // 20 => '20' in both cases
+ // C.6) intExpr :: varchar :: int => intExpr :: int
+ // 20 => 20 in both cases
+ // C.7) intExpr :: varchar :: bigint => intExpr :: bigint
+ // 7369 => 7369 in both cases; the two casts to tinyint give an error
in both cases
+ // D) The inner cast cannot be removed (see also B.2 and B.3 in
simplifyCast):
+ // D.1) intExpr :: double :: varchar => must not be changed
+ // 20 => right: '20.0', wrong: '20'
+ // realExpr :: double :: varchar => must not be changed
+ // 266.66666 => right: '266.6666564941406', wrong: '266.66666'
+ // D.2) intExpr :: decimal(12, 2) :: varchar => must not be changed
+ // 20 => right: '20.00', wrong: '20'
+ // D.3) smallintExpr :: bigint :: varbinary => must not be changed
+ // 20 => right: X'0000000000000014', wrong: X'0014'
+ // D.4) intExpr :: varchar :: timestamp => must not be changed
+ // 7369 => right: error, wrong: 1970-01-01 00:00:07
+ // D.5) tinyintExpr :: smallint :: variant => must not be changed
+ // a VARIANT keeps the type of its value; TYPEOF gives
+ // right: SMALLINT, wrong: TINYINT
+ if (SqlTypeUtil.isBinary(outer)) {
+ // D.3
+ return false;
+ }
+ if (outer.getSqlTypeName() == SqlTypeName.VARIANT) {
+ // D.5
+ return false;
+ }
+ if (SqlTypeUtil.inCharFamily(inner)) {
+ if (inner.getSqlTypeName() == SqlTypeName.CHAR
+ && outer.getSqlTypeName() != SqlTypeName.CHAR) {
+ // B.2 and B.3 from simplifyCast
+ return false;
+ }
+ if (!SqlTypeUtil.inCharFamily(source)
+ && !SqlTypeUtil.inCharFamily(outer)
+ && outer.getSqlTypeName() != source.getSqlTypeName()
+ && !(SqlTypeUtil.isIntType(source) && SqlTypeUtil.isNumeric(outer)))
{
+ // D.4; C.6 and C.7 are the exceptions
+ return false;
+ }
+ } else if (SqlTypeUtil.inCharFamily(outer)
+ && !(SqlTypeUtil.isIntType(source) && SqlTypeUtil.isIntType(inner))) {
+ // D.1 and D.2; C.5 is the exception
+ return false;
+ }
+ return SqlTypeCoercionRule.instance()
+ .canApplyFrom(outer.getSqlTypeName(), source.getSqlTypeName());
+ }
+
/** Tries to simplify CEIL/FLOOR function on top of CEIL/FLOOR.
*
* <p>Examples:
diff --git a/core/src/main/java/org/apache/calcite/rex/RexUtil.java
b/core/src/main/java/org/apache/calcite/rex/RexUtil.java
index 5c51eab9ce..71f4acfef9 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexUtil.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexUtil.java
@@ -1906,12 +1906,26 @@ public static boolean isLosslessCast(RelDataType
source, RelDataType target) {
// NUMERIC -> CHARACTER: allow when target length accommodates sign/scale
if (sourceSqlTypeName.getFamily() == SqlTypeFamily.NUMERIC
&& targetSqlTypeName.getFamily() == SqlTypeFamily.CHARACTER) {
+ final int targetPrecision = target.getPrecision();
+ if (targetPrecision == PRECISION_NOT_SPECIFIED) {
+ return true;
+ }
+ // The precision does not bound the length of the text of an approximate
+ // number, or of a DECIMAL with a negative scale:
+ // CAST(6.666666666666667E0 AS VARCHAR(3)) is '6.6'
+ // CAST(CAST(20000 AS DECIMAL(2, -3)) AS VARCHAR(3)) is '200'
+ // The text of a DOUBLE has at most 24 characters, and that of a REAL at
most 15,
+ // but this value depends on the formatting performed by the runtime,
+ // so we are NOT considering such casts as lossless currently.
+ final int sourceScale = source.getScale();
+ if (!SqlTypeUtil.isExactNumeric(source) || sourceScale < 0) {
+ return false;
+ }
int sourceLength = source.getPrecision() + 1; // include sign
- if (source.getScale() != -1 && source.getScale() != 0) {
- sourceLength += source.getScale() + 1; // include decimal mark
+ if (sourceScale != 0) {
+ sourceLength += sourceScale + 1; // include decimal mark
}
- final int targetPrecision = target.getPrecision();
- return targetPrecision == PRECISION_NOT_SPECIFIED || targetPrecision >=
sourceLength;
+ return targetPrecision >= sourceLength;
}
// DECIMAL -> DECIMAL: allow when precision/scale can only expand
diff --git
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
index e90b967482..ee7f9d5803 100644
---
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
+++
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
@@ -3559,7 +3559,9 @@ private SqlDialect nonOrdinalDialect() {
+ "CAST(employee_id AS STRING), "
+ "CAST(CAST(employee_id AS STRING) AS BYTES), "
+ "CAST(CAST(employee_id AS STRING) AS BYTES), "
- + "CAST(employee_id AS TIMESTAMP), "
+ // CAST('7369' AS TIMESTAMP) is an error,
+ // but CAST(7369 AS TIMESTAMP) is 1970-01-01 00:00:07
+ + "CAST(CAST(employee_id AS STRING) AS TIMESTAMP), "
+ "CAST(employee_id AS FLOAT64), "
+ "CAST(employee_id AS NUMERIC), "
+ "CAST(CAST(employee_id AS STRING) AS DATE), "
diff --git a/core/src/test/java/org/apache/calcite/rex/RexLosslessCastTest.java
b/core/src/test/java/org/apache/calcite/rex/RexLosslessCastTest.java
index 86c13c7427..70684c5174 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexLosslessCastTest.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexLosslessCastTest.java
@@ -384,6 +384,27 @@ class RexLosslessCastTest extends RexProgramTestBase {
rexBuilder.makeCast(dec_19_0, rexBuilder.makeInputRef(doubleType,
0))), is(false));
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7870">[CALCITE-7870]
+ * RexSimplify removes some casts which change semantics</a>. */
+ @Test void testLosslessCastApproximateToChar() {
+ final RelDataType realType = typeFactory.createSqlType(SqlTypeName.REAL);
+ final RelDataType doubleType =
typeFactory.createSqlType(SqlTypeName.DOUBLE);
+ final RelDataType varcharType =
typeFactory.createSqlType(SqlTypeName.VARCHAR);
+ final RelDataType varcharType3 =
typeFactory.createSqlType(SqlTypeName.VARCHAR, 3);
+
+ // The text of an approximate number may not fit in a bounded string
+ assertThat(
+ RexUtil.isLosslessCast(
+ rexBuilder.makeCast(varcharType3, rexBuilder.makeInputRef(doubleType,
0))), is(false));
+ assertThat(
+ RexUtil.isLosslessCast(
+ rexBuilder.makeCast(varcharType3, rexBuilder.makeInputRef(realType,
0))), is(false));
+ assertThat(
+ RexUtil.isLosslessCast(
+ rexBuilder.makeCast(varcharType, rexBuilder.makeInputRef(doubleType,
0))), is(true));
+ }
+
@Test void testLosslessCastWithCustomTypeSystem() {
final RelDataType dec_10_0 =
typeFactory.createSqlType(SqlTypeName.DECIMAL, 10, 0);
final RelDataType decDefault =
typeFactory.createSqlType(SqlTypeName.DECIMAL);
@@ -428,6 +449,7 @@ class RexLosslessCastTest extends RexProgramTestBase {
@Test void removeLosslessCastInt() {
checkSimplifyUnchanged(cast(vInt(), tBigInt()));
checkSimplifyUnchanged(cast(vInt(), tDouble()));
+ // The case labels below are those of RexSimplify.simplifyCast
// A.1 INT -> BIGINT
checkSimplify(cast(cast(vInt(), tBigInt()), tInt()),
"CAST(?0.int0):INTEGER NOT NULL");
checkSimplify(cast(cast(vInt(), tDouble()), tInt()),
"CAST(?0.int0):INTEGER NOT NULL");
@@ -476,35 +498,82 @@ class RexLosslessCastTest extends RexProgramTestBase {
RexNode char2 = vParam("char(2)_", tChar(2));
RexNode char6 = vParam("char(6)_", tChar(6));
- RexNode varchar2 = vParam("varchar(2)_", tChar(2));
- // A.2 in RexSimplify
+ RexNode varchar2 = vParam("varchar(2)_", tVarchar(2));
+ // The case labels below are those of RexSimplify.simplifyCast (A and B)
+ // and RexSimplify.canRemoveInnerCast (C)
+ // A.2
checkSimplify(
cast(cast(char2, tChar(5)), tChar(2)),
"CAST(?0.char(2)_0):CHAR(2) NOT NULL");
- // B.1
+ // C.1
checkSimplify(
cast(cast(char2, tChar(4)), tChar(5)),
"CAST(?0.char(2)_0):CHAR(5) NOT NULL");
- // B.2
+ // C.2
checkSimplify(
cast(cast(char2, tChar(10)), tChar(5)),
"CAST(?0.char(2)_0):CHAR(5) NOT NULL");
- // B.3
+ // C.3
checkSimplify(
cast(cast(char2, tVarchar(10)), tChar(5)),
"CAST(?0.char(2)_0):CHAR(5) NOT NULL");
- // B.4
+ // C.4
checkSimplify(
cast(cast(char6, tVarchar(10)), tChar(5)),
"CAST(?0.char(6)_0):CHAR(5) NOT NULL");
- // C.1
+ // B.1
checkSimplifyUnchanged(
cast(cast(char6, tChar(3)), tChar(5)));
- // C.2
+ // B.2
checkSimplifyUnchanged(
cast(cast(varchar2, tChar(5)), tVarchar(2)));
- // C.3
+ // B.3
checkSimplifyUnchanged(
cast(cast(char2, tChar(4)), tVarchar(5)));
}
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7870">[CALCITE-7870]
+ * RexSimplify removes some casts which change semantics</a>. */
+ @Test void keepInnerCastThatChangesResult() {
+ final RelDataType dec_5_2 = typeFactory.createSqlType(SqlTypeName.DECIMAL,
5, 2);
+ final RelDataType dec_10_4 =
typeFactory.createSqlType(SqlTypeName.DECIMAL, 10, 4);
+ final RelDataType dec_12_2 =
typeFactory.createSqlType(SqlTypeName.DECIMAL, 12, 2);
+ // The case labels below are those of RexSimplify.canRemoveInnerCast
+ // D.1
+ checkSimplifyUnchanged(cast(cast(vInt(), tDouble()), tVarchar()));
+ checkSimplifyUnchanged(cast(cast(vParam("real_", tReal()), tDouble()),
tVarchar()));
+ // D.2
+ checkSimplifyUnchanged(cast(cast(vInt(), dec_12_2), tVarchar()));
+ checkSimplifyUnchanged(cast(cast(vParam("decimal(5,2)_", dec_5_2),
dec_10_4), tVarchar()));
+ // D.3
+ checkSimplifyUnchanged(cast(cast(vSmallInt(), tBigInt()), tVarbinary()));
+ // D.4
+ checkSimplifyUnchanged(cast(cast(vInt(), tVarchar()), tBool()));
+ // D.5
+ final RelDataType variant = typeFactory.createSqlType(SqlTypeName.VARIANT);
+ checkSimplifyUnchanged(cast(cast(vSmallInt(), tInt()), variant));
+ checkSimplifyUnchanged(cast(cast(vParam("varchar(5)_", tVarchar(5)),
tVarchar(10)), variant));
+ // C.5
+ checkSimplify(cast(cast(vSmallInt(), tInt()), tVarchar()),
+ "CAST(?0.smallint0):VARCHAR NOT NULL");
+ // C.6
+ checkSimplify(cast(cast(vInt(), tVarchar()), tInt()),
"CAST(?0.int0):INTEGER NOT NULL");
+ // C.7
+ checkSimplify(cast(cast(vInt(), tVarchar()), tBigInt()),
"CAST(?0.int0):BIGINT NOT NULL");
+ checkSimplify(cast(cast(vInt(), tVarchar()), tSmallInt()),
+ "CAST(?0.int0):SMALLINT NOT NULL");
+ // The text of a DOUBLE may not fit in VARCHAR(3)
+ final RexNode d = vParam("double_", tDouble(true));
+ checkSimplifyUnchanged(cast(cast(d, tVarchar(3)), tVarchar()));
+ checkSimplifyUnchanged(cast(cast(d, tVarchar(3)), tDouble()));
+ // C-like case; BOOLEAN is castable from REAL, but REAL is not castable
from BOOLEAN.
+ // (A cast from an exact type to BOOLEAN becomes a comparison before it is
simplified.)
+ checkSimplify(cast(cast(vParam("real_", tReal()), tDouble()), tBool()),
+ "CAST(?0.real_0):BOOLEAN NOT NULL");
+ // C-like case; GEOMETRY is castable from VARCHAR, but VARCHAR is not
castable from GEOMETRY
+ final RelDataType geometryType =
typeFactory.createSqlType(SqlTypeName.GEOMETRY);
+ checkSimplify(cast(cast(vParam("varchar(5)_", tVarchar(5)), tVarchar(10)),
geometryType),
+ "CAST(?0.varchar(5)_0):GEOMETRY NOT NULL");
+ }
}
diff --git a/core/src/test/resources/sql/cast.iq
b/core/src/test/resources/sql/cast.iq
index 9bfa71ed5f..9ec5a91829 100644
--- a/core/src/test/resources/sql/cast.iq
+++ b/core/src/test/resources/sql/cast.iq
@@ -2129,4 +2129,116 @@ values (cast(0.1e0 as real) = cast(0.1 as real),
!ok
+# Test case for [CALCITE-7870] RexSimplify removes some casts which change
+# semantics
+SELECT CAST(CAST(deptno AS DOUBLE) AS VARCHAR) AS d1,
+ CAST(CAST(deptno AS DECIMAL(5, 2)) AS VARCHAR) AS d2,
+ CAST(CAST(sal AS DECIMAL(10, 4)) AS VARCHAR) AS d3,
+ CAST(CAST(deptno AS INTEGER) AS VARCHAR) AS d4
+FROM emp
+WHERE empno = 7369;
++------+-------+----------+----+
+| D1 | D2 | D3 | D4 |
++------+-------+----------+----+
+| 20.0 | 20.00 | 800.0000 | 20 |
++------+-------+----------+----+
+(1 row)
+
+!ok
+
+SELECT CAST(CAST(deptno AS BIGINT) AS VARBINARY) AS b1,
+ CAST(CAST(CAST(deptno AS DOUBLE) / 3e0 AS VARCHAR(3)) AS VARCHAR) AS s1,
+ CAST(CAST(CAST(deptno AS DOUBLE) / 3e0 AS VARCHAR(3)) AS DOUBLE) AS s2
+FROM emp
+WHERE empno = 7369;
++------------------+-----+-----+
+| B1 | S1 | S2 |
++------------------+-----+-----+
+| 0000000000000014 | 6.6 | 6.6 |
++------------------+-----+-----+
+(1 row)
+
+!ok
+
+SELECT CAST(CAST(deptno AS VARCHAR) AS BOOLEAN) AS b
+FROM emp
+WHERE empno = 7369;
+Invalid character for cast: 20
+!error
+
+SELECT CAST(CAST(deptno AS VARCHAR) AS TIMESTAMP) AS t
+FROM emp
+WHERE empno = 7369;
+Invalid DATE value, '20'
+!error
+
+SELECT CAST(CAST(deptno AS VARCHAR) AS BIGINT) AS b1,
+ CAST(CAST(deptno AS VARCHAR) AS DOUBLE) AS b2
+FROM emp
+WHERE empno = 7369;
++----+------+
+| B1 | B2 |
++----+------+
+| 20 | 20.0 |
++----+------+
+(1 row)
+
+!ok
+
+SELECT CAST(CAST(sal / 3 AS REAL) AS VARCHAR) AS r1,
+ CAST(CAST(CAST(sal / 3 AS REAL) AS DOUBLE) AS VARCHAR) AS r2
+FROM emp
+WHERE empno = 7369;
++-----------+-------------------+
+| R1 | R2 |
++-----------+-------------------+
+| 266.66666 | 266.6666564941406 |
++-----------+-------------------+
+(1 row)
+
+!ok
+
+# The inner casts to DOUBLE are removed; the results do not change
+SELECT CAST(CAST(CAST(deptno AS REAL) AS DOUBLE) AS BOOLEAN) AS b1,
+ CAST(CAST(CAST(deptno - 20 AS REAL) AS DOUBLE) AS BOOLEAN) AS b2
+FROM emp
+WHERE empno = 7369;
++------+-------+
+| B1 | B2 |
++------+-------+
+| true | false |
++------+-------+
+(1 row)
+
+!ok
+
+# A VARIANT keeps the type of its value, so the inner casts must stay
+SELECT TYPEOF(CAST(CAST(deptno AS SMALLINT) AS VARIANT)) AS t1,
+ TYPEOF(CAST(deptno AS VARIANT)) AS t2,
+ CAST(CAST(deptno AS SMALLINT) AS VARIANT) = CAST(deptno AS VARIANT) AS e,
+ TYPEOF(CAST(CAST(ename AS VARCHAR(20)) AS VARIANT)) AS t3
+FROM emp
+WHERE empno = 7369;
++----------+---------+-------+---------+
+| T1 | T2 | E | T3 |
++----------+---------+-------+---------+
+| SMALLINT | TINYINT | false | VARCHAR |
++----------+---------+-------+---------+
+(1 row)
+
+!ok
+
+!use scott-negative-scale
+SELECT CAST(CAST(CAST(deptno * 1000 AS DECIMAL(2, -3)) AS VARCHAR(3)) AS
VARCHAR) AS n1
+FROM emp
+WHERE empno = 7369;
++-----+
+| N1 |
++-----+
+| 200 |
++-----+
+(1 row)
+
+!ok
+
# End cast.iq