You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by pt...@apache.org on 2022/10/13 06:54:33 UTC

[ignite-3] branch main updated: IGNITE-17876 .NET: Implement single column mapping (#1201)

This is an automated email from the ASF dual-hosted git repository.

ptupitsyn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 11387af525 IGNITE-17876 .NET: Implement single column mapping (#1201)
11387af525 is described below

commit 11387af5258c69843ef083f2cedadd18b3cfa7c0
Author: Pavel Tupitsyn <pt...@apache.org>
AuthorDate: Thu Oct 13 09:54:27 2022 +0300

    IGNITE-17876 .NET: Implement single column mapping (#1201)
    
    Support the case when the first column is mapped directly to a simple type, for example:
    
    ```
    var view = Table.GetRecordView<long>();
    await view.UpsertAsync(null, 123L);
    ```
---
 .../Table/RecordViewPocoTests.cs                   |  4 +-
 .../Table/RecordViewPrimitiveTests.cs              | 92 ++++++++++++++++++++++
 .../Apache.Ignite.Tests/Table/TablesTests.cs       |  6 +-
 .../Table/Serialization/BinaryTupleMethods.cs      | 21 ++++-
 .../Table/Serialization/ObjectSerializerHandler.cs | 86 ++++++++++++++++++++
 .../runner/app/PlatformTestNodeRunner.java         | 30 +++++++
 6 files changed, 233 insertions(+), 6 deletions(-)

diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPocoTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPocoTests.cs
index 3db41a988d..c3d07c7b4f 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPocoTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPocoTests.cs
@@ -100,9 +100,9 @@ namespace Apache.Ignite.Tests.Table
         {
             var pocoView = Table.GetRecordView<object>();
 
-            var ex = Assert.ThrowsAsync<IgniteException>(async () => await pocoView.UpsertAsync(null, new object()));
+            var ex = Assert.ThrowsAsync<IgniteClientException>(async () => await pocoView.UpsertAsync(null, new object()));
 
-            StringAssert.Contains("Missed key column: KEY", ex!.Message);
+            Assert.AreEqual("Can't map 'System.Object' to columns 'Int64 KEY, String VAL'. Matching fields not found.", ex!.Message);
         }
 
         [Test]
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPrimitiveTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPrimitiveTests.cs
new file mode 100644
index 0000000000..97bfb330ef
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/RecordViewPrimitiveTests.cs
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+namespace Apache.Ignite.Tests.Table;
+
+using System.Collections;
+using System.Linq;
+using System.Numerics;
+using System.Threading.Tasks;
+using Ignite.Table;
+using NodaTime;
+using NUnit.Framework;
+
+/// <summary>
+/// Tests record view with single-column mapping to a primitive type.
+/// </summary>
+public class RecordViewPrimitiveTests : IgniteTestsBase
+{
+    [Test]
+    public async Task TestLongKey() => await TestKey(7L, Table.GetRecordView<long>());
+
+    [Test]
+    public async Task TestAllKeyTypes()
+    {
+        await TestKey((sbyte)1, "TBL_INT8");
+        await TestKey((short)1, "TBL_INT16");
+        await TestKey(1, "TBL_INT32");
+        await TestKey(1L, "TBL_INT64");
+        await TestKey(1.1f, "TBL_FLOAT");
+        await TestKey(1.1d, "TBL_DOUBLE");
+        await TestKey(1.234m, "TBL_DECIMAL");
+        await TestKey("foo", "TBL_STRING");
+        await TestKey(new LocalDateTime(2022, 10, 13, 8, 4, 42), "TBL_DATETIME");
+        await TestKey(new LocalTime(3, 4, 5), "TBL_TIME");
+        await TestKey(Instant.FromUnixTimeMilliseconds(123456789101112), "TBL_TIMESTAMP");
+        await TestKey(new BigInteger(123456789101112), "TBL_NUMBER");
+        await TestKey(new byte[] { 1, 2, 3 }, "TBL_BYTES");
+        await TestKey(new BitArray(new[] { byte.MaxValue }), "TBL_BITMASK");
+    }
+
+    [Test]
+    public void TestColumnTypeMismatchThrowsException()
+    {
+        var ex = Assert.ThrowsAsync<IgniteClientException>(async () => await TestKey(1f, Table.GetRecordView<float>()));
+        Assert.AreEqual("Can't map 'System.Single' to column 'KEY' of type 'System.Int64' - types do not match.", ex!.Message);
+    }
+
+    [Test]
+    public void TestUnmappedTypeThrowsException()
+    {
+        var ex = Assert.ThrowsAsync<IgniteClientException>(async () => await TestKey((byte)1, Table.GetRecordView<byte>()));
+        Assert.AreEqual("Can't map 'System.Byte' to columns 'Int64 KEY, String VAL'. Matching fields not found.", ex!.Message);
+    }
+
+    private static async Task TestKey<T>(T val, IRecordView<T> recordView)
+    {
+        // Tests EmitWriter.
+        await recordView.UpsertAsync(null, val);
+
+        // Tests EmitValuePartReader.
+        var (getRes, _) = await recordView.GetAsync(null, val);
+
+        // Tests EmitReader.
+        var getAllRes = await recordView.GetAllAsync(null, new[] { val });
+
+        Assert.AreEqual(val, getRes);
+        Assert.AreEqual(val, getAllRes.Single().Value);
+    }
+
+    private async Task TestKey<T>(T val, string tableName)
+    {
+        var table = await Client.Tables.GetTableAsync(tableName);
+
+        Assert.IsNotNull(table, tableName);
+
+        await TestKey(val, table!.GetRecordView<T>());
+    }
+}
\ No newline at end of file
diff --git a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/TablesTests.cs b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/TablesTests.cs
index aa80584cc4..163214e6b9 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Tests/Table/TablesTests.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Tests/Table/TablesTests.cs
@@ -33,9 +33,9 @@ namespace Apache.Ignite.Tests.Table
         {
             var tables = (await Client.Tables.GetTablesAsync()).OrderBy(x => x.Name).ToList();
 
-            Assert.AreEqual(2, tables.Count);
-            Assert.AreEqual(TableAllColumnsName, tables[0].Name);
-            Assert.AreEqual(TableName, tables[1].Name);
+            Assert.GreaterOrEqual(tables.Count, 2);
+            CollectionAssert.Contains(tables.Select(x => x.Name), TableName);
+            CollectionAssert.Contains(tables.Select(x => x.Name), TableAllColumnsName);
         }
 
         [Test]
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/BinaryTupleMethods.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/BinaryTupleMethods.cs
index 66a01f5a00..1d8ee61126 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/BinaryTupleMethods.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/BinaryTupleMethods.cs
@@ -20,6 +20,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
     using System;
     using System.Collections;
     using System.Collections.Generic;
+    using System.Numerics;
     using System.Reflection;
     using NodaTime;
     using Proto.BinaryTuple;
@@ -49,6 +50,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
         private static readonly MethodInfo AppendDateTime = typeof(BinaryTupleBuilder).GetMethod(nameof(BinaryTupleBuilder.AppendDateTime))!;
         private static readonly MethodInfo AppendTimestamp = typeof(BinaryTupleBuilder).GetMethod(nameof(BinaryTupleBuilder.AppendTimestamp))!;
         private static readonly MethodInfo AppendDecimal = typeof(BinaryTupleBuilder).GetMethod(nameof(BinaryTupleBuilder.AppendDecimal))!;
+        private static readonly MethodInfo AppendNumber = typeof(BinaryTupleBuilder).GetMethod(nameof(BinaryTupleBuilder.AppendNumber))!;
         private static readonly MethodInfo AppendBytes =
             typeof(BinaryTupleBuilder).GetMethod(nameof(BinaryTupleBuilder.AppendBytes), new[] { typeof(byte[]) })!;
 
@@ -66,6 +68,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
         private static readonly MethodInfo GetDateTime = typeof(BinaryTupleReader).GetMethod(nameof(BinaryTupleReader.GetDateTime))!;
         private static readonly MethodInfo GetTimestamp = typeof(BinaryTupleReader).GetMethod(nameof(BinaryTupleReader.GetTimestamp))!;
         private static readonly MethodInfo GetDecimal = typeof(BinaryTupleReader).GetMethod(nameof(BinaryTupleReader.GetDecimal))!;
+        private static readonly MethodInfo GetNumber = typeof(BinaryTupleReader).GetMethod(nameof(BinaryTupleReader.GetNumber))!;
         private static readonly MethodInfo GetBytes = typeof(BinaryTupleReader).GetMethod(nameof(BinaryTupleReader.GetBytes))!;
 
         private static readonly IReadOnlyDictionary<Type, MethodInfo> WriteMethods = new Dictionary<Type, MethodInfo>
@@ -84,7 +87,8 @@ namespace Apache.Ignite.Internal.Table.Serialization
             { typeof(LocalDateTime), AppendDateTime },
             { typeof(Instant), AppendTimestamp },
             { typeof(byte[]), AppendBytes },
-            { typeof(decimal), AppendDecimal }
+            { typeof(decimal), AppendDecimal },
+            { typeof(BigInteger), AppendNumber }
         };
 
         private static readonly IReadOnlyDictionary<Type, MethodInfo> ReadMethods = new Dictionary<Type, MethodInfo>
@@ -103,6 +107,7 @@ namespace Apache.Ignite.Internal.Table.Serialization
             { typeof(LocalDateTime), GetDateTime },
             { typeof(Instant), GetTimestamp },
             { typeof(decimal), GetDecimal },
+            { typeof(BigInteger), GetNumber },
             { typeof(byte[]), GetBytes }
         };
 
@@ -114,6 +119,13 @@ namespace Apache.Ignite.Internal.Table.Serialization
         public static MethodInfo GetWriteMethod(Type valueType) =>
             WriteMethods.TryGetValue(valueType, out var method) ? method : throw GetUnsupportedTypeException(valueType);
 
+        /// <summary>
+        /// Gets the write method.
+        /// </summary>
+        /// <param name="valueType">Type of the value to write.</param>
+        /// <returns>Write method for the specified value type.</returns>
+        public static MethodInfo? GetWriteMethodOrNull(Type valueType) => WriteMethods.GetValueOrDefault(valueType);
+
         /// <summary>
         /// Gets the read method.
         /// </summary>
@@ -122,6 +134,13 @@ namespace Apache.Ignite.Internal.Table.Serialization
         public static MethodInfo GetReadMethod(Type valueType) =>
             ReadMethods.TryGetValue(valueType, out var method) ? method : throw GetUnsupportedTypeException(valueType);
 
+        /// <summary>
+        /// Gets the read method.
+        /// </summary>
+        /// <param name="valueType">Type of the value to read.</param>
+        /// <returns>Read method for the specified value type.</returns>
+        public static MethodInfo? GetReadMethodOrNull(Type valueType) => ReadMethods.GetValueOrDefault(valueType);
+
         private static IgniteClientException GetUnsupportedTypeException(Type valueType) =>
             new(ErrorGroups.Client.Configuration, "Unsupported type: " + valueType);
     }
diff --git a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
index f518175d00..ba9b923739 100644
--- a/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
+++ b/modules/platforms/dotnet/Apache.Ignite/Internal/Table/Serialization/ObjectSerializerHandler.cs
@@ -19,6 +19,8 @@ namespace Apache.Ignite.Internal.Table.Serialization
 {
     using System;
     using System.Collections.Concurrent;
+    using System.Collections.Generic;
+    using System.Linq;
     using System.Reflection;
     using System.Reflection.Emit;
     using MessagePack;
@@ -113,6 +115,36 @@ namespace Apache.Ignite.Internal.Table.Serialization
             var columns = schema.Columns;
             var count = keyOnly ? schema.KeyColumnCount : columns.Count;
 
+            if (BinaryTupleMethods.GetWriteMethodOrNull(type) is { } directWriteMethod)
+            {
+                // Single column to primitive type mapping.
+                var col = columns[0];
+                ValidateSingleFieldMappingType(type, col);
+
+                il.Emit(OpCodes.Ldarg_0); // writer
+                il.Emit(OpCodes.Ldarg_2); // value
+
+                if (col.Type == ClientDataType.Decimal)
+                {
+                    EmitLdcI4(il, col.Scale);
+                }
+
+                il.Emit(OpCodes.Call, directWriteMethod);
+
+                for (var index = 1; index < count; index++)
+                {
+                    il.Emit(OpCodes.Ldarg_0); // writer
+                    il.Emit(OpCodes.Ldarg_1); // noValueSet
+                    il.Emit(OpCodes.Call, BinaryTupleMethods.WriteNoValue);
+                }
+
+                il.Emit(OpCodes.Ret);
+
+                return (WriteDelegate<T>)method.CreateDelegate(typeof(WriteDelegate<T>));
+            }
+
+            int mappedCount = 0;
+
             for (var index = 0; index < count; index++)
             {
                 var col = columns[index];
@@ -138,9 +170,13 @@ namespace Apache.Ignite.Internal.Table.Serialization
 
                     var writeMethod = BinaryTupleMethods.GetWriteMethod(fieldInfo.FieldType);
                     il.Emit(OpCodes.Call, writeMethod);
+
+                    mappedCount++;
                 }
             }
 
+            ValidateMappedCount(mappedCount, type, columns);
+
             il.Emit(OpCodes.Ret);
 
             return (WriteDelegate<T>)method.CreateDelegate(typeof(WriteDelegate<T>));
@@ -158,6 +194,24 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 skipVisibility: true);
 
             var il = method.GetILGenerator();
+
+            if (BinaryTupleMethods.GetReadMethodOrNull(type) is { } readMethod)
+            {
+                // Single column to primitive type mapping.
+                il.Emit(OpCodes.Ldarg_0); // reader
+                il.Emit(OpCodes.Ldc_I4_0); // index
+
+                if (schema.Columns[0] is { Type: ClientDataType.Decimal } col)
+                {
+                    EmitLdcI4(il, col.Scale);
+                }
+
+                il.Emit(OpCodes.Call, readMethod);
+                il.Emit(OpCodes.Ret);
+
+                return (ReadDelegate<T>)method.CreateDelegate(typeof(ReadDelegate<T>));
+            }
+
             var local = il.DeclareLocal(type);
 
             il.Emit(OpCodes.Ldtoken, type);
@@ -187,6 +241,12 @@ namespace Apache.Ignite.Internal.Table.Serialization
         {
             var type = typeof(T);
 
+            if (BinaryTupleMethods.GetReadMethodOrNull(type) != null)
+            {
+                // Single column to primitive type mapping - return key as is.
+                return (ref BinaryTupleReader _, T key) => key;
+            }
+
             var method = new DynamicMethod(
                 name: "ReadValuePart" + type.Name,
                 returnType: type,
@@ -322,5 +382,31 @@ namespace Apache.Ignite.Internal.Table.Serialization
                 throw new IgniteClientException(ErrorGroups.Client.Configuration, message);
             }
         }
+
+        private static void ValidateSingleFieldMappingType(Type type, Column column)
+        {
+            var columnType = column.Type.ToType();
+
+            if (type != columnType)
+            {
+                var message = $"Can't map '{type}' to column '{column.Name}' of type '{columnType}' - types do not match.";
+
+                throw new IgniteClientException(ErrorGroups.Client.Configuration, message);
+            }
+        }
+
+        private static void ValidateMappedCount(int mappedCount, Type type, IEnumerable<Column> columns)
+        {
+            if (mappedCount > 0)
+            {
+                return;
+            }
+
+            var columnStr = string.Join(", ", columns.Select(x => x.Type + " " + x.Name));
+
+            throw new IgniteClientException(
+                ErrorGroups.Client.Configuration,
+                $"Can't map '{type}' to columns '{columnStr}'. Matching fields not found.");
+        }
     }
 }
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/PlatformTestNodeRunner.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/PlatformTestNodeRunner.java
index e97ed6c7c1..04609dc5fc 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/PlatformTestNodeRunner.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/runner/app/PlatformTestNodeRunner.java
@@ -179,6 +179,36 @@ public class PlatformTestNodeRunner {
                         .changeReplicas(1)
                         .changePartitions(10)
         ));
+
+        createTwoColumnTable(node, ColumnType.INT8);
+        createTwoColumnTable(node, ColumnType.INT16);
+        createTwoColumnTable(node, ColumnType.INT32);
+        createTwoColumnTable(node, ColumnType.INT64);
+        createTwoColumnTable(node, ColumnType.FLOAT);
+        createTwoColumnTable(node, ColumnType.DOUBLE);
+        createTwoColumnTable(node, ColumnType.decimal());
+        createTwoColumnTable(node, ColumnType.string());
+        createTwoColumnTable(node, ColumnType.datetime());
+        createTwoColumnTable(node, ColumnType.time());
+        createTwoColumnTable(node, ColumnType.timestamp());
+        createTwoColumnTable(node, ColumnType.number());
+        createTwoColumnTable(node, ColumnType.blob());
+        createTwoColumnTable(node, ColumnType.bitmaskOf(32));
+    }
+
+    private static void createTwoColumnTable(Ignite node, ColumnType type) {
+        var keyCol = "key";
+
+        TableDefinition schTbl = SchemaBuilders.tableBuilder(SCHEMA_NAME, "tbl_" + type.typeSpec().name()).columns(
+                SchemaBuilders.column(keyCol, type).build(),
+                SchemaBuilders.column("val", type).asNullable(true).build()
+        ).withPrimaryKey(keyCol).build();
+
+        await(((TableManager) node.tables()).createTableAsync(schTbl.name(), tblCh ->
+                SchemaConfigurationConverter.convert(schTbl, tblCh)
+                        .changeReplicas(1)
+                        .changePartitions(10)
+        ));
     }
 
     /**