Skip to content

Commit df29353

Browse files
feat: FIR-48856 add implicit struct deserialization to custom objects with mapping (#135)
1 parent 978c929 commit df29353

File tree

7 files changed

+895
-3
lines changed

7 files changed

+895
-3
lines changed

FireboltDotNetSdk.Tests/Integration/DataTypesTest.cs

Lines changed: 356 additions & 3 deletions
Large diffs are not rendered by default.
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
using System.Data.Common;
2+
using FireboltDotNetSdk.Client;
3+
using FireboltDotNetSdk.Utils;
4+
5+
namespace FireboltDotNetSdk.Tests
6+
{
7+
[TestFixture]
8+
[Category("v2")]
9+
[Category("engine-v2")]
10+
internal class StructMappingTest : IntegrationTest
11+
{
12+
private const string TableName = "struct_test";
13+
14+
private static readonly string[] SetupSql =
15+
{
16+
"SET advanced_mode=1",
17+
"SET enable_create_table_v2=true",
18+
"SET enable_struct_syntax=true",
19+
"SET prevent_create_on_information_schema=true",
20+
"SET enable_create_table_with_struct_type=true",
21+
$"DROP TABLE IF EXISTS {TableName}",
22+
$"CREATE TABLE IF NOT EXISTS {TableName} (" +
23+
"id numeric(5,0), " +
24+
"plain struct(int_val int, str_val text, arr_val array(numeric(5,3)))" +
25+
")"
26+
};
27+
28+
private const string CleanupSql = $"DROP TABLE IF EXISTS {TableName}";
29+
30+
[OneTimeSetUp]
31+
public void GlobalSetup()
32+
{
33+
using var conn = new FireboltConnection(ConnectionString());
34+
conn.Open();
35+
SetupStructTable(conn);
36+
}
37+
38+
[OneTimeTearDown]
39+
public void GlobalTearDown()
40+
{
41+
using var conn = new FireboltConnection(ConnectionString());
42+
conn.Open();
43+
CreateCommand(conn, CleanupSql).ExecuteNonQuery();
44+
}
45+
46+
[Test]
47+
public void InsertAndSelectStruct_PocoWithAttributesMapping_Sync()
48+
{
49+
using var conn = new FireboltConnection(ConnectionString());
50+
conn.Open();
51+
52+
var select = CreateCommand(conn, $"SELECT id, plain FROM {TableName} ORDER BY id ASC");
53+
using var reader = select.ExecuteReader();
54+
Assert.Multiple(() =>
55+
{
56+
Assert.That(reader.Read(), Is.True);
57+
Assert.That(reader.GetFieldValue<decimal>(0), Is.EqualTo(1));
58+
});
59+
var plain = reader.GetFieldValue<PlainStructWithAttributes>(1);
60+
AssertPocoWithAttributes(plain);
61+
Assert.That(reader.Read(), Is.False);
62+
}
63+
64+
[Test]
65+
public async Task InsertAndSelectStruct_PocoWithAttributesMapping_Async()
66+
{
67+
await using var conn = new FireboltConnection(ConnectionString());
68+
await conn.OpenAsync();
69+
70+
var select = CreateCommand(conn, $"SELECT id, plain FROM {TableName} ORDER BY id ASC");
71+
await using var reader = await select.ExecuteReaderAsync();
72+
Assert.Multiple(async () =>
73+
{
74+
Assert.That(await reader.ReadAsync(), Is.True);
75+
Assert.That(await reader.GetFieldValueAsync<decimal>(0), Is.EqualTo(1));
76+
});
77+
var plain = await reader.GetFieldValueAsync<PlainStructWithAttributes>(1);
78+
AssertPocoWithAttributes(plain);
79+
Assert.That(await reader.ReadAsync(), Is.False);
80+
}
81+
82+
[Test]
83+
public void InsertAndSelectStruct_PocoWithoutAttributesMapping_Sync()
84+
{
85+
using var conn = new FireboltConnection(ConnectionString());
86+
conn.Open();
87+
88+
var select = CreateCommand(conn, $"SELECT id, plain FROM {TableName} ORDER BY id ASC");
89+
using var reader = select.ExecuteReader();
90+
Assert.Multiple(() =>
91+
{
92+
Assert.That(reader.Read(), Is.True);
93+
Assert.That(reader.GetFieldValue<decimal>(0), Is.EqualTo(1));
94+
});
95+
var plain = reader.GetFieldValue<PlainStructWithoutAttributes>(1);
96+
AssertPocoWithoutAttributes(plain);
97+
Assert.That(reader.Read(), Is.False);
98+
}
99+
100+
[Test]
101+
public async Task InsertAndSelectStruct_PocoWithoutAttributesMapping_Async()
102+
{
103+
await using var conn = new FireboltConnection(ConnectionString());
104+
await conn.OpenAsync();
105+
106+
var select = CreateCommand(conn, $"SELECT id, plain FROM {TableName} ORDER BY id ASC");
107+
await using var reader = await select.ExecuteReaderAsync();
108+
Assert.Multiple(async () =>
109+
{
110+
Assert.That(await reader.ReadAsync(), Is.True);
111+
Assert.That(await reader.GetFieldValueAsync<decimal>(0), Is.EqualTo(1));
112+
});
113+
var plain = await reader.GetFieldValueAsync<PlainStructWithoutAttributes>(1);
114+
AssertPocoWithoutAttributes(plain);
115+
Assert.That(await reader.ReadAsync(), Is.False);
116+
}
117+
118+
private static void AssertPocoWithAttributes(PlainStructWithAttributes plain)
119+
{
120+
Assert.Multiple(() =>
121+
{
122+
Assert.That(plain.IntVal, Is.EqualTo(1));
123+
Assert.That(plain.StrVal, Is.EqualTo("test"));
124+
Assert.That(plain.ArrVal, Is.Not.Null);
125+
Assert.That(plain.ArrVal, Has.Length.EqualTo(2));
126+
Assert.That(plain.ArrVal[0], Is.EqualTo(12.34f).Within(1e-4));
127+
Assert.That(plain.ArrVal[1], Is.EqualTo(56.789f).Within(1e-4));
128+
});
129+
}
130+
131+
private static void AssertPocoWithoutAttributes(PlainStructWithoutAttributes plain)
132+
{
133+
Assert.Multiple(() =>
134+
{
135+
Assert.That(plain.IntVal, Is.EqualTo(1));
136+
Assert.That(plain.StrVal, Is.EqualTo("test"));
137+
Assert.That(plain.ArrVal, Is.Not.Null);
138+
Assert.That(plain.ArrVal, Has.Length.EqualTo(2));
139+
Assert.That(plain.ArrVal[0], Is.EqualTo(12.34f).Within(1e-4));
140+
Assert.That(plain.ArrVal[1], Is.EqualTo(56.789f).Within(1e-4));
141+
});
142+
}
143+
144+
private static void SetupStructTable(FireboltConnection conn)
145+
{
146+
foreach (var sql in SetupSql)
147+
{
148+
CreateCommand(conn, sql).ExecuteNonQuery();
149+
}
150+
151+
// Insert a struct row
152+
const string insert = $"INSERT INTO {TableName} (id, plain) VALUES (1, struct(1, 'test', [12.34, 56.789]))";
153+
CreateCommand(conn, insert).ExecuteNonQuery();
154+
}
155+
156+
private class PlainStructWithAttributes
157+
{
158+
159+
[FireboltStructName("int_val")]
160+
public int IntVal { get; init; }
161+
162+
[FireboltStructName("str_val")]
163+
public string StrVal { get; init; } = null!;
164+
165+
[FireboltStructName("arr_val")]
166+
public float[] ArrVal { get; init; } = null!;
167+
}
168+
169+
private class PlainStructWithoutAttributes
170+
{
171+
172+
public int IntVal { get; init; }
173+
174+
public string StrVal { get; init; } = null!;
175+
176+
public float[] ArrVal { get; init; } = null!;
177+
}
178+
179+
private static DbCommand CreateCommand(DbConnection conn, string query)
180+
{
181+
var command = conn.CreateCommand();
182+
command.CommandText = query;
183+
return command;
184+
}
185+
}
186+
}
187+
188+

FireboltDotNetSdk.Tests/Unit/FireboltDataReaderTest.cs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,5 +721,134 @@ public void DataTableCompatibility()
721721
Assert.That(dataTable.Rows[0]["i"], Is.EqualTo(123));
722722
});
723723
}
724+
725+
private static DbDataReader CreateStructReader()
726+
{
727+
var meta = new List<Meta>() { new Meta { Name = "plain", Type = "struct(int_val int, str_val text, arr_val array(decimal(5, 3)))" } };
728+
const string json = "{\"int_val\":42,\"str_val\":\"hello\",\"arr_val\":[1.23,4.56]}";
729+
var data = new List<List<object?>> { new List<object?> { json } };
730+
var result = new QueryResult { Rows = 1, Meta = meta, Data = data };
731+
732+
DbDataReader reader = new FireboltDataReader(null, result);
733+
Assert.That(reader.Read(), Is.True);
734+
return reader;
735+
}
736+
737+
[Test]
738+
public void GetFieldValueGeneric_StructHappyFlow()
739+
{
740+
var reader = CreateStructReader();
741+
742+
// As dictionary
743+
var asDict = reader.GetFieldValue<Dictionary<string, object?>>(0);
744+
Assert.Multiple(() =>
745+
{
746+
Assert.That(asDict["int_val"], Is.EqualTo(42));
747+
Assert.That(asDict["str_val"], Is.EqualTo("hello"));
748+
});
749+
750+
// Also verify POCO mapping using attribute names
751+
var poco = reader.GetFieldValue<TestPoco>(0);
752+
Assert.Multiple(() =>
753+
{
754+
Assert.That(poco.IntVal, Is.EqualTo(42));
755+
Assert.That(poco.StrVal, Is.EqualTo("hello"));
756+
Assert.That(poco.ArrVal!, Has.Length.EqualTo(2));
757+
});
758+
}
759+
760+
[Test]
761+
public void GetFieldValueGeneric_Struct_UnhappyFlows()
762+
{
763+
var meta = new List<Meta>() { new Meta { Name = "plain", Type = "struct(int_val int, str_val text, arr_val array(decimal(5, 3)))" } };
764+
const string json = "{\"int_val\":42,\"str_val\":\"hello\",\"arr_val\":[1.23,4.56]}";
765+
var data = new List<List<object?>> { new List<object?> { json } };
766+
DbDataReader reader = new FireboltDataReader(null, new QueryResult { Rows = 1, Meta = meta, Data = data });
767+
Assert.That(reader.Read(), Is.True);
768+
769+
// Not assignable cast
770+
Assert.Throws<InvalidCastException>(() => reader.GetFieldValue<int>(0));
771+
772+
// Null column should throw
773+
var meta2 = new List<Meta>() { new Meta { Name = "plain", Type = "struct" } };
774+
var data2 = new List<List<object?>> { new List<object?> { null } };
775+
DbDataReader reader2 = new FireboltDataReader(null, new QueryResult { Rows = 1, Meta = meta2, Data = data2 });
776+
Assert.That(reader2.Read(), Is.True);
777+
Assert.Throws<InvalidCastException>(() => reader2.GetFieldValue<TestPoco>(0));
778+
}
779+
780+
[Test]
781+
public void GetFieldValueGeneric_Struct_MixedAttributeAndImplicitNames()
782+
{
783+
var reader = CreateStructReader();
784+
785+
var poco = reader.GetFieldValue<TestPocoMixed>(0);
786+
Assert.Multiple(() =>
787+
{
788+
Assert.That(poco.IntVal, Is.EqualTo(42));
789+
Assert.That(poco.StrVal, Is.EqualTo("hello"));
790+
Assert.That(poco.ArrVal!, Has.Length.EqualTo(2));
791+
});
792+
}
793+
794+
[Test]
795+
public void GetFieldValueGeneric_Struct_PocoMissingFieldFromStruct()
796+
{
797+
var reader = CreateStructReader();
798+
799+
var poco = reader.GetFieldValue<TestPocoMissing>(0);
800+
Assert.Multiple(() =>
801+
{
802+
Assert.That(poco.IntVal, Is.EqualTo(42));
803+
Assert.That(poco.StrVal, Is.EqualTo("hello"));
804+
// Missing field (arr_val) should be ignored without exceptions
805+
});
806+
}
807+
808+
[Test]
809+
public void GetFieldValueGeneric_Struct_PocoDifferentShape_Throws()
810+
{
811+
var reader = CreateStructReader();
812+
813+
var poco = reader.GetFieldValue<DifferentPocoWrongType>(0);
814+
Assert.Multiple(() =>
815+
{
816+
Assert.That(poco.TimestampVal, Is.EqualTo(default(DateTime)));
817+
Assert.That(poco.DoubleVal, Is.EqualTo(0.0));
818+
});
819+
}
820+
821+
class TestPoco
822+
{
823+
[FireboltStructName("int_val")] public int IntVal { get; init; }
824+
[FireboltStructName("str_val")] public string StrVal { get; init; } = string.Empty;
825+
[FireboltStructName("arr_val")] public float[]? ArrVal { get; init; }
826+
}
827+
828+
class TestPocoMixed
829+
{
830+
// Attribute for one field
831+
[FireboltStructName("int_val")] public int IntVal { get; init; }
832+
833+
// Implicit snake_case mapping (no attribute)
834+
public string StrVal { get; init; } = string.Empty;
835+
836+
// Implicit snake_case mapping (no attribute)
837+
public float[]? ArrVal { get; init; }
838+
}
839+
840+
class TestPocoMissing
841+
{
842+
[FireboltStructName("int_val")] public int IntVal { get; init; }
843+
// No attribute, mapped by snake_case to str_val
844+
public string StrVal { get; init; } = string.Empty;
845+
// Deliberately missing ArrVal property
846+
}
847+
848+
class DifferentPocoWrongType
849+
{
850+
public DateTime TimestampVal { get; init; }
851+
[FireboltStructName("double_val")] public double DoubleVal { get; init; }
852+
}
724853
}
725854
}

0 commit comments

Comments
 (0)