IGNITE-22040: KeyValue/RecordView. Improve error messages for constraint violation errors. (#3619)

diff --git a/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueViewTest.java b/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueViewTest.java
index 3af37ae..88bafa3 100644
--- a/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueViewTest.java
+++ b/modules/client/src/test/java/org/apache/ignite/client/ClientKeyValueViewTest.java
@@ -28,7 +28,6 @@
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.math.BigDecimal;
@@ -105,10 +104,12 @@
         key.id = "1";
         key.gid = 1;
 
-        IgniteException e = assertThrows(IgniteException.class, () -> pojoView.get(null, key));
-        assertEquals("Failed to deserialize server response: No mapped object field found for column 'ZBOOLEAN'", e.getMessage());
+        Throwable e = assertThrowsWithCause(
+                () -> pojoView.get(null, key),
+                IgniteException.class,
+                "Failed to deserialize server response: No mapped object field found for column 'ZBOOLEAN'"
+        );
         assertThat(Arrays.asList(e.getStackTrace()), anyOf(hasToString(containsString("ClientKeyValueView"))));
-
     }
 
     @Test
@@ -206,9 +207,11 @@
     public void testMissingKeyColumnThrowsException() {
         var kvView = defaultTable().keyValueView(NamePojo.class, NamePojo.class);
 
-        IgniteException e = assertThrows(IgniteException.class, () -> kvView.get(null, new NamePojo()));
-
-        assertThat(e.getMessage(), containsString("No mapped object field found for column 'ID'"));
+        Throwable e = assertThrowsWithCause(
+                () -> kvView.get(null, new NamePojo()),
+                IgniteException.class,
+                "No mapped object field found for column 'ID'"
+        );
         assertThat(Arrays.asList(e.getStackTrace()), anyOf(hasToString(containsString("ClientKeyValueView"))));
     }
 
@@ -510,9 +513,11 @@
         var pojo = new DefaultValuesValPojo();
         pojo.strNonNull = null;
 
-        var ex = assertThrows(IgniteException.class, () -> pojoView.put(null, 1, pojo));
-
-        assertTrue(ex.getMessage().contains("null was passed, but column is not nullable"), ex.getMessage());
+        var ex = assertThrowsWithCause(
+                () -> pojoView.put(null, 1, pojo),
+                IgniteException.class,
+                "Column 'STRNONNULL' does not allow NULLs"
+        );
         assertThat(Arrays.asList(ex.getStackTrace()), anyOf(hasToString(containsString("ClientKeyValueView"))));
     }
 
diff --git a/modules/client/src/test/java/org/apache/ignite/client/ClientRecordViewTest.java b/modules/client/src/test/java/org/apache/ignite/client/ClientRecordViewTest.java
index 0e78078..0c9243f 100644
--- a/modules/client/src/test/java/org/apache/ignite/client/ClientRecordViewTest.java
+++ b/modules/client/src/test/java/org/apache/ignite/client/ClientRecordViewTest.java
@@ -527,7 +527,7 @@
 
         var ex = assertThrows(IgniteException.class, () -> pojoView.upsert(null, pojo));
 
-        assertTrue(ex.getMessage().contains("null was passed, but column is not nullable"), ex.getMessage());
+        assertThat(ex.getMessage(), containsString("Column 'STRNONNULL' does not allow NULLs"));
         assertThat(Arrays.asList(ex.getStackTrace()), anyOf(hasToString(containsString("ClientRecordView"))));
     }
 }
diff --git a/modules/client/src/test/java/org/apache/ignite/client/ClientTableTest.java b/modules/client/src/test/java/org/apache/ignite/client/ClientTableTest.java
index 77a5ab7..a23653e 100644
--- a/modules/client/src/test/java/org/apache/ignite/client/ClientTableTest.java
+++ b/modules/client/src/test/java/org/apache/ignite/client/ClientTableTest.java
@@ -400,7 +400,7 @@
 
         var ex = assertThrows(IgniteException.class, () -> table.upsert(null, tuple));
 
-        assertTrue(ex.getMessage().contains("null was passed, but column is not nullable"), ex.getMessage());
+        assertTrue(ex.getMessage().contains("Column 'STRNONNULL' does not allow NULLs"), ex.getMessage());
     }
 
     @Test
diff --git a/modules/marshaller-common/src/main/java/org/apache/ignite/internal/marshaller/FieldAccessor.java b/modules/marshaller-common/src/main/java/org/apache/ignite/internal/marshaller/FieldAccessor.java
index d6712b5..8340ac4 100644
--- a/modules/marshaller-common/src/main/java/org/apache/ignite/internal/marshaller/FieldAccessor.java
+++ b/modules/marshaller-common/src/main/java/org/apache/ignite/internal/marshaller/FieldAccessor.java
@@ -398,7 +398,7 @@
         try {
             write0(writer, obj);
         } catch (Exception ex) {
-            throw new MarshallerException("Failed to write field [id=" + colIdx + ']', ex);
+            throw new MarshallerException(ex.getMessage(), ex);
         }
     }
 
@@ -422,7 +422,7 @@
         try {
             read0(reader, obj);
         } catch (Exception ex) {
-            throw new MarshallerException("Failed to read field [id=" + colIdx + ']', ex);
+            throw new MarshallerException(ex.getMessage(), ex);
         }
     }
 
diff --git a/modules/marshaller-common/src/test/java/org/apache/ignite/internal/marshaller/FieldAccessorTest.java b/modules/marshaller-common/src/test/java/org/apache/ignite/internal/marshaller/FieldAccessorTest.java
index bb21ffc..a2c804b 100644
--- a/modules/marshaller-common/src/test/java/org/apache/ignite/internal/marshaller/FieldAccessorTest.java
+++ b/modules/marshaller-common/src/test/java/org/apache/ignite/internal/marshaller/FieldAccessorTest.java
@@ -294,7 +294,7 @@
         assertThrowsWithCause(
                 () -> accessor.write(mocks.getFirst(), "Other string"),
                 MarshallerException.class,
-                "Failed to write field [id=42]"
+                "class java.lang.String cannot be cast to class java.util.UUID"
         );
     }
 
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/SchemaValidationTest.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/SchemaValidationTest.cs
index a512f72..e10e12d 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/SchemaValidationTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/SchemaValidationTest.cs
@@ -119,8 +119,7 @@
         };
 
         var ex = Assert.ThrowsAsync<MarshallerException>(async () => await TableRequiredVal.RecordBinaryView.UpsertAsync(null, igniteTuple));
-        StringAssert.StartsWith("Failed to set column (null was passed, but column is not null", ex!.Message);
-        StringAssert.Contains("name=VAL", ex.Message);
+        StringAssert.StartsWith("Column 'VAL' does not allow NULLs", ex!.Message);
     }
 
     [Test]
@@ -149,8 +148,7 @@
 
         var ex = Assert.ThrowsAsync<MarshallerException>(
             async () => await TableRequiredVal.KeyValueBinaryView.PutAsync(null, keyTuple, valTuple));
-        StringAssert.StartsWith("Failed to set column (null was passed, but column is not null", ex!.Message);
-        StringAssert.Contains("name=VAL", ex.Message);
+        StringAssert.StartsWith("Column 'VAL' does not allow NULLs", ex!.Message);
     }
 
     [Test]
@@ -259,8 +257,7 @@
         var ex = Assert.ThrowsAsync<MarshallerException>(
             async () => await TableRequiredVal.GetRecordView<KeyPoco>().UpsertAsync(null, new KeyPoco()));
 
-        StringAssert.StartsWith("Failed to set column (null was passed, but column is not null", ex!.Message);
-        StringAssert.Contains("name=VAL", ex.Message);
+        StringAssert.StartsWith("Column 'VAL' does not allow NULLs", ex!.Message);
     }
 
     [Test]
@@ -278,8 +275,7 @@
         var ex = Assert.ThrowsAsync<MarshallerException>(
             async () => await TableRequiredVal.GetKeyValueView<long, KeyPoco>().PutAsync(null, 1L, new KeyPoco()));
 
-        StringAssert.StartsWith("Failed to set column (null was passed, but column is not null", ex!.Message);
-        StringAssert.Contains("name=VAL", ex.Message);
+        StringAssert.StartsWith("Column 'VAL' does not allow NULLs", ex!.Message);
     }
 
     [Test]
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientMarshallingTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientMarshallingTest.java
index 81bc047..b966841 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientMarshallingTest.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/client/ItThinClientMarshallingTest.java
@@ -19,6 +19,7 @@
 
 import static org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrowsWithCause;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.startsWith;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -164,9 +165,7 @@
         var tupleView = table.recordView();
 
         Throwable ex = assertThrowsWithCause(() -> tupleView.upsert(null, Tuple.create().set("KEY", 1)), IgniteException.class);
-        assertThat(ex.getMessage(), startsWith(
-                "Failed to set column (null was passed, but column is not nullable): "
-                        + "[col=Column [rowPosition=1, keyPosition=-1, valuePosition=0, colocationPosition=-1, name=VAL"));
+        assertThat(ex.getMessage(), containsString("Column 'VAL' does not allow NULLs"));
     }
 
     @Test
@@ -190,9 +189,7 @@
                 () -> tupleView.put(null, Tuple.create().set("KEY", 1), Tuple.create()),
                 IgniteException.class);
 
-        assertThat(ex.getMessage(), startsWith(
-                "Failed to set column (null was passed, but column is not nullable): "
-                        + "[col=Column [rowPosition=1, keyPosition=-1, valuePosition=0, colocationPosition=-1, name=VAL"));
+        assertThat(ex.getMessage(), containsString("Column 'VAL' does not allow NULLs"));
     }
 
     @Test
@@ -276,9 +273,7 @@
         var tupleView = table.recordView();
 
         Throwable ex = assertThrowsWithCause(() -> tupleView.upsert(null, Tuple.create().set("KEY", null)), IgniteException.class);
-        assertThat(ex.getMessage(), startsWith(
-                "Failed to set column (null was passed, but column is not nullable): "
-                        + "[col=Column [rowPosition=0, keyPosition=0, valuePosition=-1, colocationPosition=0, name=KEY"));
+        assertThat(ex.getMessage(), containsString("Column 'KEY' does not allow NULLs"));
     }
 
     @Test
@@ -291,9 +286,7 @@
 
         Tuple rec = Tuple.create().set("KEY", 1).set("VAL", null);
         Throwable ex = assertThrowsWithCause(() -> tupleView.upsert(null, rec), IgniteException.class);
-        assertThat(ex.getMessage(), startsWith(
-                "Failed to set column (null was passed, but column is not nullable): "
-                        + "[col=Column [rowPosition=1, keyPosition=-1, valuePosition=0, colocationPosition=-1, name=VAL"));
+        assertThat(ex.getMessage(), containsString("Column 'VAL' does not allow NULLs"));
     }
 
     @Test
@@ -317,9 +310,7 @@
                 () -> tupleView.put(null, Tuple.create().set("KEY", 1), Tuple.create().set("VAL", null)),
                 IgniteException.class);
 
-        assertThat(ex.getMessage(), startsWith(
-                "Failed to set column (null was passed, but column is not nullable): "
-                        + "[col=Column [rowPosition=1, keyPosition=-1, valuePosition=0, colocationPosition=-1, name=VAL"));
+        assertThat(ex.getMessage(), containsString("Column 'VAL' does not allow NULLs"));
     }
 
     private static class TestPojo2 {
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/Column.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/Column.java
index ec7ca7f..2f371a6 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/Column.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/Column.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.schema;
 
+import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
+
 import org.apache.ignite.internal.tostring.IgniteToStringExclude;
 import org.apache.ignite.internal.tostring.S;
 import org.apache.ignite.internal.type.NativeType;
@@ -199,8 +201,7 @@
      */
     public void validate(@Nullable Object val) {
         if (val == null && !nullable) {
-            throw new IllegalArgumentException("Failed to set column (null was passed, but column is not nullable): "
-                    + "[col=" + this + ']');
+            throw new SchemaMismatchException(nullConstraintViolationMessage(name));
         }
 
         NativeType objType = NativeTypes.fromObject(val);
@@ -239,4 +240,14 @@
     public String toString() {
         return S.toString(Column.class, this);
     }
+
+    /**
+     * Returns an error message for NOT NULL constraint violation.
+     *
+     * @param columnName Column name.
+     * @return Error message.
+     */
+    public static String nullConstraintViolationMessage(String columnName) {
+        return format("Column '{}' does not allow NULLs", columnName);
+    }
 }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/RowAssembler.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/RowAssembler.java
index 8dedf88..1ce7eb3 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/RowAssembler.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/RowAssembler.java
@@ -165,8 +165,8 @@
      */
     public RowAssembler appendNull() throws SchemaMismatchException {
         if (!columns.get(curCol).nullable()) {
-            throw new SchemaMismatchException(
-                    "Failed to set column (null was passed, but column is not nullable): " + columns.get(curCol));
+            String name = columns.get(curCol).name();
+            throw new SchemaMismatchException(Column.nullConstraintViolationMessage(name));
         }
 
         builder.appendNull();
diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItNotNullConstraintClientTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItNotNullConstraintClientTest.java
index dec43c7..9fe2090 100644
--- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItNotNullConstraintClientTest.java
+++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItNotNullConstraintClientTest.java
@@ -21,8 +21,6 @@
 import org.apache.ignite.client.IgniteClient;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
 
 /**
  * NOT NULL constraint test for thin client API.
@@ -47,33 +45,4 @@
     protected Ignite ignite() {
         return client;
     }
-
-    // TODO https://issues.apache.org/jira/browse/IGNITE-22040 is resolved error messages for client and server API should be the same.
-    @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-22040")
-    @Override
-    public void testKeyValueView() {
-        super.testKeyValueView();
-    }
-
-    @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-22040")
-    @Override
-    public void testRecordView() {
-        super.testRecordView();
-    }
-
-    @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-22040")
-    @Override
-    public void testKeyValueViewDataStreamer() {
-        super.testRecordView();
-    }
-
-    @Test
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-22040")
-    @Override
-    public void testRecordViewDataStreamer() {
-        super.testRecordView();
-    }
 }
diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItNotNullConstraintTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItNotNullConstraintTest.java
index d9b6e0d..ee98b8a 100644
--- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItNotNullConstraintTest.java
+++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItNotNullConstraintTest.java
@@ -143,14 +143,14 @@
             KeyValueView<Integer, Integer> view = table.keyValueView(Integer.class, Integer.class);
             assertThrows(MarshallerException.class, () -> {
                 view.put(null, 1, null);
-            }, "Failed to write field [id=1]");
+            }, "Column 'VAL' does not allow NULLs");
         }
 
         {
             KeyValueView<Tuple, Tuple> view = table.keyValueView();
             assertThrows(MarshallerException.class, () -> {
                 view.put(null, Tuple.create(Map.of("id", 1)), Tuple.create());
-            }, "Failed to set column (null was passed, but column is not nullable)");
+            }, "Column 'VAL' does not allow NULLs");
         }
 
         {
@@ -158,7 +158,7 @@
             assertThrows(MarshallerException.class, () -> {
                 Val val = new Val();
                 view.put(null, 1, val);
-            }, "Failed to write field [id=0]");
+            }, "Column 'VAL' does not allow NULLs");
         }
     }
 
@@ -174,7 +174,7 @@
             assertThrows(MarshallerException.class, () -> {
                 Rec rec = new Rec();
                 view.insert(null, rec);
-            }, "Failed to write field [id=0]");
+            }, "Column 'ID' does not allow NULLs");
         }
 
         {
@@ -183,14 +183,14 @@
                 Rec rec = new Rec();
                 rec.id = 42;
                 view.insert(null, rec);
-            }, "Failed to write field [id=1]");
+            }, "Column 'VAL' does not allow NULLs");
         }
 
         {
             RecordView<Tuple> view = table.recordView();
             assertThrows(MarshallerException.class, () -> {
                 view.insert(null, Tuple.create(Map.of("id", 1)));
-            }, "Failed to set column (null was passed, but column is not nullable)");
+            }, "Column 'VAL' does not allow NULLs");
         }
     }
 
@@ -203,19 +203,18 @@
         {
             KeyValueView<Tuple, Tuple> view = table.keyValueView();
 
-            checkDataStreamer(view, new SimpleEntry<>(Tuple.create(Map.of("id", 1)), Tuple.create()),
-                    "Failed to set column (null was passed, but column is not nullable)");
+            checkDataStreamer(view, new SimpleEntry<>(Tuple.create(Map.of("id", 1)), Tuple.create()), "Column 'VAL' does not allow NULLs");
         }
 
         {
             KeyValueView<Integer, Integer> view = table.keyValueView(Integer.class, Integer.class);
-            checkDataStreamer(view, new SimpleEntry<>(1, null), "Failed to write field [id=1]");
+            checkDataStreamer(view, new SimpleEntry<>(1, null), "Column 'VAL' does not allow NULLs");
         }
 
         {
             KeyValueView<Integer, Val> view = table.keyValueView(Integer.class, Val.class);
             Val val = new Val();
-            checkDataStreamer(view, new SimpleEntry<>(1, val), "Failed to write field [id=0]");
+            checkDataStreamer(view, new SimpleEntry<>(1, val), "Column 'VAL' does not allow NULLs");
         }
     }
 
@@ -227,20 +226,20 @@
 
         {
             RecordView<Tuple> view = table.recordView();
-            checkDataStreamer(view, Tuple.create(Map.of("id", 1)), "Failed to set column (null was passed, but column is not nullable)");
+            checkDataStreamer(view, Tuple.create(Map.of("id", 1)), "Column 'VAL' does not allow NULLs");
         }
 
         {
             RecordView<Rec> view = table.recordView(Rec.class);
             Rec rec = new Rec();
-            checkDataStreamer(view, rec, "Failed to write field [id=0]");
+            checkDataStreamer(view, rec, "Column 'ID' does not allow NULLs");
         }
 
         {
             RecordView<Rec> view = table.recordView(Rec.class);
             Rec rec = new Rec();
             rec.id = 1;
-            checkDataStreamer(view, rec, "Failed to write field [id=1]");
+            checkDataStreamer(view, rec, "Column 'VAL' does not allow NULLs");
         }
     }
 
diff --git a/modules/sql-engine/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties b/modules/sql-engine/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
index 140e83d..2671960 100644
--- a/modules/sql-engine/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
+++ b/modules/sql-engine/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
@@ -16,5 +16,6 @@
 #
 
 # Overrides messages used by Apache Calcite.
-#
+# This message should be the same as or very similar to the one produced by
+# org.apache.ignite.internal.schema.Column::nullConstraintViolationMessage
 ColumnNotNullable=Column ''{0}'' does not allow NULLs
diff --git a/modules/table/src/main/java/org/apache/ignite/internal/table/KeyValueViewImpl.java b/modules/table/src/main/java/org/apache/ignite/internal/table/KeyValueViewImpl.java
index 78b719d..f521a79 100644
--- a/modules/table/src/main/java/org/apache/ignite/internal/table/KeyValueViewImpl.java
+++ b/modules/table/src/main/java/org/apache/ignite/internal/table/KeyValueViewImpl.java
@@ -50,7 +50,6 @@
 import org.apache.ignite.internal.thread.PublicApiThreading;
 import org.apache.ignite.internal.tx.InternalTransaction;
 import org.apache.ignite.internal.util.IgniteUtils;
-import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.lang.NullableValue;
 import org.apache.ignite.lang.UnexpectedNullValueException;
 import org.apache.ignite.sql.IgniteSql;
@@ -500,7 +499,7 @@
         try {
             return marsh.marshal(key);
         } catch (MarshallerException e) {
-            throw new IgniteException(e);
+            throw new org.apache.ignite.lang.MarshallerException(e);
         }
     }
 
diff --git a/modules/table/src/test/java/org/apache/ignite/internal/table/SchemaValidationTest.java b/modules/table/src/test/java/org/apache/ignite/internal/table/SchemaValidationTest.java
index 7d280be..582d7fc 100644
--- a/modules/table/src/test/java/org/apache/ignite/internal/table/SchemaValidationTest.java
+++ b/modules/table/src/test/java/org/apache/ignite/internal/table/SchemaValidationTest.java
@@ -100,7 +100,7 @@
         RecordView<Tuple> tbl = createTable(schema).recordView();
 
         // Check not-nullable column.
-        assertThrowsWithCause(IllegalArgumentException.class, () -> tbl.insert(null, Tuple.create().set("id", null)));
+        assertThrowsWithCause(SchemaMismatchException.class, () -> tbl.insert(null, Tuple.create().set("id", null)));
 
         // Check length of the string column
         assertThrowsWithCause(InvalidTypeException.class,