[ 
https://issues.apache.org/jira/browse/IGNITE-26491?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Pavel Pereslegin updated IGNITE-26491:
--------------------------------------
    Description: 
 

The behavior of the embedded client is inconsistent with the thin client when 
trying to read a narrower integer type from tuple that contains long value.

For example, when we have table with bigint column val.
{code:sql}
create table test_int (id bigint primary key, val bigint not null)
{code}
Getting existing row from view
{code:java}
tuple = kvVIew.get(null, key);
{code}
the next
{code:java}
tuple.intValue("VAL") 
{code}
in thin client produces
{noformat}
ClassCastException: Column with name 'VAL' has type INT64 but INT32 was 
requested
{noformat}
but does not produces any errors in embedded client.

(need to check the same case with record view also)

Full reproducer
{code:java}
        sql("create table test_int (id bigint primary key, val bigint)");
        KeyValueView<Tuple, Tuple> kvVIew = 
client.tables().table("test_int").keyValueView();
        KeyValueView<Tuple, Tuple> kvVIewEmbedded = 
CLUSTER.aliveNode().tables().table("test_int").keyValueView();

        Tuple key = Tuple.create().set("id", 1L);
        Tuple val = Tuple.create().set("val", 1L);

        kvVIew.put(null, key, val);

        Tuple thinReturnedVal = kvVIew.get(null, key);
        Tuple embeddedReturnedVal = kvVIewEmbedded.get(null, key);

        assertEquals(1, embeddedReturnedVal.longValue(0));
        assertEquals(1, thinReturnedVal.longValue(0));

        // The following statement should fail, but does not fail.
        assertEquals(1, embeddedReturnedVal.intValue("VAL"));
        // The following statement fails with ClassCastException.
        assertEquals(1, thinReturnedVal.intValue("VAL"));
{code}
Exception stacktrace:
{noformat}
java.lang.ClassCastException: Column with name 'VAL' has type INT64 but INT32 
was requested
        at 
org.apache.ignite.internal.client.table.MutableTupleBinaryTupleAdapter.binaryTupleIndex(MutableTupleBinaryTupleAdapter.java:468)
        at 
org.apache.ignite.internal.client.table.MutableTupleBinaryTupleAdapter.validateSchemaColumnType(MutableTupleBinaryTupleAdapter.java:477)
        at 
org.apache.ignite.internal.client.table.MutableTupleBinaryTupleAdapter.intValue(MutableTupleBinaryTupleAdapter.java:202)
{noformat}
It is necessary to determine which behavior is correct and eliminate 
inconsistencies.
h2. UPDATE

We want to make the behavior consistent in all clients:
 # align the behavior with the [column upgrade 
rules|https://docs.google.com/document/d/17dx7hKmKLMGQ460O3i4k5ZNXO9ylW1pHlCyD8xg23L0/edit?tab=t.0]
 # relax current validation to support downcast where possible because we don't 
want to break column upgrade scenarios

For example the following scenario should work:
 * The user upgraded the column in the database from int to long, the user 
application continues to treat the column as an int and this shouldn't break 
application during reading or writing.
 * The user updates the application

h3. Rules

For the following type families:
 # Integer types: {*}INT8{*}{*},{*} {*}INT16{*}, {*}INT32{*}, *INT64*
 # Floating‑point types: {*}FLOAT{*}, *DOUBLE*

Within each family the following implicit conversion rules apply:
 # *Implicit widening conversions* are always permitted.
 # *Implicit narrowing conversions* are permitted only if the source value can 
be exactly represented in the target type without loss of precision or overflow.

 

These rules essentially reflect how we write numeric types in binary format.
h3. Example
{code:java}
Column VAL has type INT16.
 
Implicit conversion when reading:
 
// allowed for all non null values
// for null values throws NullPointerException
long val = tuple.longValue(“val”);
int val = tuple.intValue(“val”);
short val = tuple.shortValue(“val”);
 
// allowed only if the value is in range [-127, 127],
// if the value is null throws NullPointerException
// otherwise throws ClassCastException
byte val = tuple.byteValue(“val"); {code}
 
h3. Implementation notes

We have several Tuple implementations. The behavior should be the same across 
all implementations.

The following table shows the base implementation classes that must be updated 
to support implicit conversion and consistent type checking.

 
|Class|Mutable|Usage|Implementation details|
|TupleImpl|yes| |Map|
|MutableTupleBinaryTupleAdapter

|yes|java thin

child classes:
* ClientSqlRow
* ClientHandlerTuple
* ClientTuple|BinaryTupleReader for reading
TupleImpl for modifications|
|MutableRowTupleAdapter|yes|Embedded client
binary table views.

child classes:
* TableRow
* KeyRowChunk
* ValueRowChunk|BinaryRow for reading
TupleImpl for modifications|
|SqlRowImpl|no|Embedded
SQL resultset| |

 

This class is not aware about schema and is currently used as the main 
implementation of Tuple in the public API, and is also used in the internal API 
when modifying an existing Tuple (for example, in MutableRowTupleAdapter, etc.)

Currently it does not support implicit type conversion and throws a 
ClassCastException if the value type does not match the required one. The 
behavior of this class must conform to the specified implicit type conversion 
rules. We must keep this class {*}schema unaware{*}.

The “set” method will remain unchanged and is expected not to generate any 
exceptions.
h3. SqlRowImpl

This class always reads the boxed value according to the schema, and then casts 
it to the desired type, 

Similar to TupleImpl it throws ClassCastException if the value type doesn’t 
match the required type. And similar to TupleImpl we must support implicit 
conversion for numeric types.

 

Below is a pseudo code that can be used for implement implicit conversion when 
reading short and float values (in TupleImpl and SqlRowImpl):

 
{code:java}
public short shortValue(String columnName) {
    Number number = (Number) row.get(columnIndexChecked(columnName));
 
    if (obj instanceof Long
            || obj instanceof Integer
            || obj instanceof Byte) {
        long longVal = number.longValue();
        short shortVal = number.shortValue();
 
        if (longVal == shortVal)
{             return shortVal;         }
 
        throw new ArithmeticException("short value overflow"); 
    }
 
    return (short) number;
}
 
public float floatValue(String columnName) {
    Object obj = row.get(columnIndexChecked(columnName));
 
    if (obj instanceof Double) {
        Double doubleValue = (Double) obj;
        float floatValue = doubleValue.floatValue();
 
        if (doubleValue == floatValue) {
             return floatValue;
         }
 
        throw new ArithmeticException("float value overflow"); 
    }
 
    return (float) obj;
 } {code}
 

MutableTupleBinaryTupleAdapter

This implementation is used in the thin client and, unlike SqlRow, can read 
primitives without value boxing.

This class performs strict schema validation before reading value from the 
binary tuple.

Validation needs to be relaxed and behavior needs to be aligned with the 
described type conversion rules. At the same time, the ability to read 
primitives without value boxing must be preserved.

 
{code:java}
 
public float floatValue(String columnName) {
    if (tuple != null) {
        return tuple.floatValue(columnName);
    }
 
    var binaryTupleIndex = binaryTupleIndex(columnName);
 
    // validate index
    // ...
 
    ColumnType actualType = schemaColumnType(binaryTupleIndex);
 
    if (actualType == ColumnType.FLOAT) {
        return binaryTuple.floatValue(binaryTupleIndex);
    }
 
    if (actualType == ColumnType.DOUBLE) {
        double doubleValue = binaryTuple.doubleValue(binaryTupleIndex);         
float floatValue = (float) doubleValue;

        if (doubleValue == floatValue) {
            return floatValue;
        }
    }
 
    throw new ClassCastException("...");
} {code}
Note: validation in the serializer must also be relaxed to comply with the 
specified rules (see ClientTupleSerializer.writeTupleRow).

 
h3. MutableRowTupleAdapter

Similar to MutableTupleBinaryTupleAdapter this class is aware about schema and 
able to read values without unboxing, but doesn’t perform any validation 
currently.

We need to align its behavior according to the described rules, similar to 
MutableTupleBinaryTupleAdapter.

 

Note: validation at the marshaller-level must also be relaxed to comply with 
the specified rules (see Column.validate).

  was:
The behavior of the embedded client is inconsistent with the thin client when 
trying to read a narrower integer type from tuple that contains long value.

For example, when we have table with bigint column val.
{code:sql}
create table test_int (id bigint primary key, val bigint not null)
{code}
Getting existing row from view
{code:java}
tuple = kvVIew.get(null, key);
{code}
the next
{code:java}
tuple.intValue("VAL") 
{code}
in thin client produces
{noformat}
ClassCastException: Column with name 'VAL' has type INT64 but INT32 was 
requested
{noformat}
but does not produces any errors in embedded client.

(need to check the same case with record view also)

Full reproducer
{code:java}
        sql("create table test_int (id bigint primary key, val bigint)");
        KeyValueView<Tuple, Tuple> kvVIew = 
client.tables().table("test_int").keyValueView();
        KeyValueView<Tuple, Tuple> kvVIewEmbedded = 
CLUSTER.aliveNode().tables().table("test_int").keyValueView();

        Tuple key = Tuple.create().set("id", 1L);
        Tuple val = Tuple.create().set("val", 1L);

        kvVIew.put(null, key, val);

        Tuple thinReturnedVal = kvVIew.get(null, key);
        Tuple embeddedReturnedVal = kvVIewEmbedded.get(null, key);

        assertEquals(1, embeddedReturnedVal.longValue(0));
        assertEquals(1, thinReturnedVal.longValue(0));

        // The following statement should fail, but does not fail.
        assertEquals(1, embeddedReturnedVal.intValue("VAL"));
        // The following statement fails with ClassCastException.
        assertEquals(1, thinReturnedVal.intValue("VAL"));
{code}
Exception stacktrace:
{noformat}
java.lang.ClassCastException: Column with name 'VAL' has type INT64 but INT32 
was requested
        at 
org.apache.ignite.internal.client.table.MutableTupleBinaryTupleAdapter.binaryTupleIndex(MutableTupleBinaryTupleAdapter.java:468)
        at 
org.apache.ignite.internal.client.table.MutableTupleBinaryTupleAdapter.validateSchemaColumnType(MutableTupleBinaryTupleAdapter.java:477)
        at 
org.apache.ignite.internal.client.table.MutableTupleBinaryTupleAdapter.intValue(MutableTupleBinaryTupleAdapter.java:202)
{noformat}
It is necessary to determine which behavior is correct and eliminate 
inconsistencies.
h2. UPDATE

We want to make the behavior consistent in all clients:
 # align the behavior with the [column upgrade 
rules|https://docs.google.com/document/d/17dx7hKmKLMGQ460O3i4k5ZNXO9ylW1pHlCyD8xg23L0/edit?tab=t.0]
 # relax current validation to support downcast where possible because we don't 
want to break column upgrade scenarios

For example the following scenario should work:
 * The user upgraded the column in the database from int to long, the user 
application continues to treat the column as an int and this shouldn't break 
application during reading or writing.
 * The user updates the application

h3. Rules

For the following type families:
 # Integer types: {*}INT8{*}{*},{*} {*}INT16{*}, {*}INT32{*}, *INT64*
 # Floating‑point types: {*}FLOAT{*}, *DOUBLE*

Within each family the following implicit conversion rules apply:
 # *Implicit widening conversions* are always permitted.
 # *Implicit narrowing conversions* are permitted only if the source value can 
be exactly represented in the target type without loss of precision or overflow.

 

These rules essentially reflect how we write numeric types in binary format.
h3. Example

 
{code:java}
Column VAL has type INT16.
 
Implicit conversion when reading:
 
// allowed for all non null values
// for null values throws NullPointerException
long val = tuple.longValue(“val”);
int val = tuple.intValue(“val”);
short val = tuple.shortValue(“val”);
 
// allowed only if the value is in range [-127, 127],
// if the value is null throws NullPointerException
// otherwise throws ClassCastException
byte val = tuple.byteValue(“val"); {code}
 
h3. Implementation notes

We have several Tuple implementations. The behavior should be the same across 
all implementations.

The following table shows the base implementation classes that must be updated 
to support implicit conversion and consistent type checking.

 
|Class|Mutable|Usage|Implementation details|Usage|Implementation details|
| | | | | | |
| | | | | | |
| | | | | | |
|Col A1| | |Col A2| | |

 

This class is not aware about schema and is currently used as the main 
implementation of Tuple in the public API, and is also used in the internal API 
when modifying an existing Tuple (for example, in MutableRowTupleAdapter, etc.)

Currently it does not support implicit type conversion and throws a 
ClassCastException if the value type does not match the required one. The 
behavior of this class must conform to the specified implicit type conversion 
rules. We must keep this class {*}schema unaware{*}.

The “set” method will remain unchanged and is expected not to generate any 
exceptions.
h3. SqlRowImpl

This class always reads the boxed value according to the schema, and then casts 
it to the desired type, 

Similar to TupleImpl it throws ClassCastException if the value type doesn’t 
match the required type. And similar to TupleImpl we must support implicit 
conversion for numeric types.

 

Below is a pseudo code that can be used for implement implicit conversion when 
reading short and float values (in TupleImpl and SqlRowImpl):

public short shortValue(String columnName) {

    Number number = (Number) row.get(columnIndexChecked(columnName));

 

    if (obj instanceof Long

            || obj instanceof Integer

            || obj instanceof Byte) {

        long longVal = number.longValue();

        short shortVal = number.shortValue();

 

        if (longVal == shortVal)

{             return shortVal;         }

 

        throw new ArithmeticException("short value overflow"); 

    }

 

    return (short) number;

}

 

public float floatValue(String columnName) {

    Object obj = row.get(columnIndexChecked(columnName));

 

    if (obj instanceof Double) {

        Double doubleValue = (Double) obj;

        float floatValue = doubleValue.floatValue();

 

        if (doubleValue == floatValue)

{             return floatValue;         }

 

        throw new ArithmeticException("float value overflow"); 

    }

 

    return (float) obj;

}
h3. MutableTupleBinaryTupleAdapter

This implementation is used in the thin client and, unlike SqlRow, can read 
primitives without value boxing.

This class performs strict schema validation before reading value from the 
binary tuple.

Validation needs to be relaxed and behavior needs to be aligned with the 
described type conversion rules. At the same time, the ability to read 
primitives without value boxing must be preserved.

 

public float floatValue(String columnName) {

    if (tuple != null) \{         return tuple.floatValue(columnName);     }

 

    var binaryTupleIndex = binaryTupleIndex(columnName);

 

    // validate index

    // ...

 

    ColumnType actualType = schemaColumnType(binaryTupleIndex);

 

    if (actualType == ColumnType.FLOAT) \{         return 
binaryTuple.floatValue(binaryTupleIndex);     }

 

    if (actualType == ColumnType.DOUBLE) {

        double doubleValue = binaryTuple.doubleValue(binaryTupleIndex);

        float floatValue = (float) doubleValue;

 

        if (doubleValue == floatValue) \{             return floatValue;        
 }

    }

 

    throw new ClassCastException("...");

}

Note: validation in the serializer must also be relaxed to comply with the 
specified rules (see ClientTupleSerializer.writeTupleRow).
h3. MutableRowTupleAdapter

Similar to MutableTupleBinaryTupleAdapter this class is aware about schema and 
able to read values without unboxing, but doesn’t perform any validation 
currently.

We need to align its behavior according to the described rules, similar to 
MutableTupleBinaryTupleAdapter.

 

Note: validation at the marshaller-level must also be relaxed to comply with 
the specified rules (see Column.validate).


> Inconsistent behavior of thin/embedded client when reading tuple value
> ----------------------------------------------------------------------
>
>                 Key: IGNITE-26491
>                 URL: https://issues.apache.org/jira/browse/IGNITE-26491
>             Project: Ignite
>          Issue Type: Bug
>          Components: sql ai3
>            Reporter: Pavel Pereslegin
>            Priority: Major
>              Labels: ignite-3
>
>  
> The behavior of the embedded client is inconsistent with the thin client when 
> trying to read a narrower integer type from tuple that contains long value.
> For example, when we have table with bigint column val.
> {code:sql}
> create table test_int (id bigint primary key, val bigint not null)
> {code}
> Getting existing row from view
> {code:java}
> tuple = kvVIew.get(null, key);
> {code}
> the next
> {code:java}
> tuple.intValue("VAL") 
> {code}
> in thin client produces
> {noformat}
> ClassCastException: Column with name 'VAL' has type INT64 but INT32 was 
> requested
> {noformat}
> but does not produces any errors in embedded client.
> (need to check the same case with record view also)
> Full reproducer
> {code:java}
>         sql("create table test_int (id bigint primary key, val bigint)");
>         KeyValueView<Tuple, Tuple> kvVIew = 
> client.tables().table("test_int").keyValueView();
>         KeyValueView<Tuple, Tuple> kvVIewEmbedded = 
> CLUSTER.aliveNode().tables().table("test_int").keyValueView();
>         Tuple key = Tuple.create().set("id", 1L);
>         Tuple val = Tuple.create().set("val", 1L);
>         kvVIew.put(null, key, val);
>         Tuple thinReturnedVal = kvVIew.get(null, key);
>         Tuple embeddedReturnedVal = kvVIewEmbedded.get(null, key);
>         assertEquals(1, embeddedReturnedVal.longValue(0));
>         assertEquals(1, thinReturnedVal.longValue(0));
>         // The following statement should fail, but does not fail.
>         assertEquals(1, embeddedReturnedVal.intValue("VAL"));
>         // The following statement fails with ClassCastException.
>         assertEquals(1, thinReturnedVal.intValue("VAL"));
> {code}
> Exception stacktrace:
> {noformat}
> java.lang.ClassCastException: Column with name 'VAL' has type INT64 but INT32 
> was requested
>       at 
> org.apache.ignite.internal.client.table.MutableTupleBinaryTupleAdapter.binaryTupleIndex(MutableTupleBinaryTupleAdapter.java:468)
>       at 
> org.apache.ignite.internal.client.table.MutableTupleBinaryTupleAdapter.validateSchemaColumnType(MutableTupleBinaryTupleAdapter.java:477)
>       at 
> org.apache.ignite.internal.client.table.MutableTupleBinaryTupleAdapter.intValue(MutableTupleBinaryTupleAdapter.java:202)
> {noformat}
> It is necessary to determine which behavior is correct and eliminate 
> inconsistencies.
> h2. UPDATE
> We want to make the behavior consistent in all clients:
>  # align the behavior with the [column upgrade 
> rules|https://docs.google.com/document/d/17dx7hKmKLMGQ460O3i4k5ZNXO9ylW1pHlCyD8xg23L0/edit?tab=t.0]
>  # relax current validation to support downcast where possible because we 
> don't want to break column upgrade scenarios
> For example the following scenario should work:
>  * The user upgraded the column in the database from int to long, the user 
> application continues to treat the column as an int and this shouldn't break 
> application during reading or writing.
>  * The user updates the application
> h3. Rules
> For the following type families:
>  # Integer types: {*}INT8{*}{*},{*} {*}INT16{*}, {*}INT32{*}, *INT64*
>  # Floating‑point types: {*}FLOAT{*}, *DOUBLE*
> Within each family the following implicit conversion rules apply:
>  # *Implicit widening conversions* are always permitted.
>  # *Implicit narrowing conversions* are permitted only if the source value 
> can be exactly represented in the target type without loss of precision or 
> overflow.
>  
> These rules essentially reflect how we write numeric types in binary format.
> h3. Example
> {code:java}
> Column VAL has type INT16.
>  
> Implicit conversion when reading:
>  
> // allowed for all non null values
> // for null values throws NullPointerException
> long val = tuple.longValue(“val”);
> int val = tuple.intValue(“val”);
> short val = tuple.shortValue(“val”);
>  
> // allowed only if the value is in range [-127, 127],
> // if the value is null throws NullPointerException
> // otherwise throws ClassCastException
> byte val = tuple.byteValue(“val"); {code}
>  
> h3. Implementation notes
> We have several Tuple implementations. The behavior should be the same across 
> all implementations.
> The following table shows the base implementation classes that must be 
> updated to support implicit conversion and consistent type checking.
>  
> |Class|Mutable|Usage|Implementation details|
> |TupleImpl|yes| |Map|
> |MutableTupleBinaryTupleAdapter
> |yes|java thin
> child classes:
> * ClientSqlRow
> * ClientHandlerTuple
> * ClientTuple|BinaryTupleReader for reading
> TupleImpl for modifications|
> |MutableRowTupleAdapter|yes|Embedded client
> binary table views.
> child classes:
> * TableRow
> * KeyRowChunk
> * ValueRowChunk|BinaryRow for reading
> TupleImpl for modifications|
> |SqlRowImpl|no|Embedded
> SQL resultset| |
>  
> This class is not aware about schema and is currently used as the main 
> implementation of Tuple in the public API, and is also used in the internal 
> API when modifying an existing Tuple (for example, in MutableRowTupleAdapter, 
> etc.)
> Currently it does not support implicit type conversion and throws a 
> ClassCastException if the value type does not match the required one. The 
> behavior of this class must conform to the specified implicit type conversion 
> rules. We must keep this class {*}schema unaware{*}.
> The “set” method will remain unchanged and is expected not to generate any 
> exceptions.
> h3. SqlRowImpl
> This class always reads the boxed value according to the schema, and then 
> casts it to the desired type, 
> Similar to TupleImpl it throws ClassCastException if the value type doesn’t 
> match the required type. And similar to TupleImpl we must support implicit 
> conversion for numeric types.
>  
> Below is a pseudo code that can be used for implement implicit conversion 
> when reading short and float values (in TupleImpl and SqlRowImpl):
>  
> {code:java}
> public short shortValue(String columnName) {
>     Number number = (Number) row.get(columnIndexChecked(columnName));
>  
>     if (obj instanceof Long
>             || obj instanceof Integer
>             || obj instanceof Byte) {
>         long longVal = number.longValue();
>         short shortVal = number.shortValue();
>  
>         if (longVal == shortVal)
> {             return shortVal;         }
>  
>         throw new ArithmeticException("short value overflow"); 
>     }
>  
>     return (short) number;
> }
>  
> public float floatValue(String columnName) {
>     Object obj = row.get(columnIndexChecked(columnName));
>  
>     if (obj instanceof Double) {
>         Double doubleValue = (Double) obj;
>         float floatValue = doubleValue.floatValue();
>  
>         if (doubleValue == floatValue) {
>              return floatValue;
>          }
>  
>         throw new ArithmeticException("float value overflow"); 
>     }
>  
>     return (float) obj;
>  } {code}
>  
> MutableTupleBinaryTupleAdapter
> This implementation is used in the thin client and, unlike SqlRow, can read 
> primitives without value boxing.
> This class performs strict schema validation before reading value from the 
> binary tuple.
> Validation needs to be relaxed and behavior needs to be aligned with the 
> described type conversion rules. At the same time, the ability to read 
> primitives without value boxing must be preserved.
>  
> {code:java}
>  
> public float floatValue(String columnName) {
>     if (tuple != null) {
>         return tuple.floatValue(columnName);
>     }
>  
>     var binaryTupleIndex = binaryTupleIndex(columnName);
>  
>     // validate index
>     // ...
>  
>     ColumnType actualType = schemaColumnType(binaryTupleIndex);
>  
>     if (actualType == ColumnType.FLOAT) {
>         return binaryTuple.floatValue(binaryTupleIndex);
>     }
>  
>     if (actualType == ColumnType.DOUBLE) {
>         double doubleValue = binaryTuple.doubleValue(binaryTupleIndex);       
>   float floatValue = (float) doubleValue;
>         if (doubleValue == floatValue) {
>             return floatValue;
>         }
>     }
>  
>     throw new ClassCastException("...");
> } {code}
> Note: validation in the serializer must also be relaxed to comply with the 
> specified rules (see ClientTupleSerializer.writeTupleRow).
>  
> h3. MutableRowTupleAdapter
> Similar to MutableTupleBinaryTupleAdapter this class is aware about schema 
> and able to read values without unboxing, but doesn’t perform any validation 
> currently.
> We need to align its behavior according to the described rules, similar to 
> MutableTupleBinaryTupleAdapter.
>  
> Note: validation at the marshaller-level must also be relaxed to comply with 
> the specified rules (see Column.validate).



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to