Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for serialization of records with no default constructor #510

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 227 additions & 0 deletions src/Parquet.Test/Serialisation/ParquetSerializerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@
["Timestamp"] = DateTime.UtcNow.AddSeconds(i),
["EventName"] = i % 2 == 0 ? "on" : "off",
["MeterValue"] = (double)i,
["ParentId"] = (i % 4 == 0) ? null : i,

Check warning on line 206 in src/Parquet.Test/Serialisation/ParquetSerializerTest.cs

View workflow job for this annotation

GitHub Actions / Build NuGet

Possible null reference assignment.

Check warning on line 206 in src/Parquet.Test/Serialisation/ParquetSerializerTest.cs

View workflow job for this annotation

GitHub Actions / Build NuGet

Possible null reference assignment.

Check warning on line 206 in src/Parquet.Test/Serialisation/ParquetSerializerTest.cs

View workflow job for this annotation

GitHub Actions / Unit Tests (macos-latest)

Possible null reference assignment.

Check warning on line 206 in src/Parquet.Test/Serialisation/ParquetSerializerTest.cs

View workflow job for this annotation

GitHub Actions / Unit Tests (macos-latest)

Possible null reference assignment.

Check warning on line 206 in src/Parquet.Test/Serialisation/ParquetSerializerTest.cs

View workflow job for this annotation

GitHub Actions / Unit Tests (ubuntu-latest)

Possible null reference assignment.

Check warning on line 206 in src/Parquet.Test/Serialisation/ParquetSerializerTest.cs

View workflow job for this annotation

GitHub Actions / Unit Tests (ubuntu-latest)

Possible null reference assignment.

Check warning on line 206 in src/Parquet.Test/Serialisation/ParquetSerializerTest.cs

View workflow job for this annotation

GitHub Actions / Unit Tests (windows-latest)

Possible null reference assignment.

Check warning on line 206 in src/Parquet.Test/Serialisation/ParquetSerializerTest.cs

View workflow job for this annotation

GitHub Actions / Unit Tests (windows-latest)

Possible null reference assignment.
["ExternalId"] = Guid.NewGuid()
}).ToList();

Expand Down Expand Up @@ -397,7 +397,7 @@
public async Task List_Structs_Serde_Dict() {
var data = Enumerable.Range(0, 1_000).Select(i => new Dictionary<string, object> {
["PersonId"] = i,
["Comments"] = i % 2 == 0 ? "none" : null,

Check warning on line 400 in src/Parquet.Test/Serialisation/ParquetSerializerTest.cs

View workflow job for this annotation

GitHub Actions / Build NuGet

Possible null reference assignment.

Check warning on line 400 in src/Parquet.Test/Serialisation/ParquetSerializerTest.cs

View workflow job for this annotation

GitHub Actions / Unit Tests (macos-latest)

Possible null reference assignment.

Check warning on line 400 in src/Parquet.Test/Serialisation/ParquetSerializerTest.cs

View workflow job for this annotation

GitHub Actions / Unit Tests (ubuntu-latest)

Possible null reference assignment.

Check warning on line 400 in src/Parquet.Test/Serialisation/ParquetSerializerTest.cs

View workflow job for this annotation

GitHub Actions / Unit Tests (windows-latest)

Possible null reference assignment.
["Addresses"] = Enumerable.Range(0, 4).Select(a => new Dictionary<string, object> {
["City"] = "Birmingham",
["Country"] = "United Kingdom"
Expand Down Expand Up @@ -854,6 +854,233 @@
Assert.Equivalent(data2, data);
}

public record TrivialRecord(int E, string F);

[Fact]
public async Task TrivialRecord_Roundtrip()
{
var expected = new[]
{
new TrivialRecord(4, "[4]"),
new TrivialRecord(7, "[7]"),
};

Assert.Equivalent(await RoundTripAsync(expected), expected);
}

public record NonTrivialRecord(int E, string F)
{
// Private field refers to constructor params/props,
// so generates code in the primary constructor.
private readonly string _g = $@"{E} {F}";

public string GetG() => _g;
}

[Fact]
public async Task NonTrivialRecord_Roundtrip()
{
var expected = new[]
{
new NonTrivialRecord(5, "{5}"),
new NonTrivialRecord(15, "{15}"),
new NonTrivialRecord(25, "{25}"),
new NonTrivialRecord(35, "{35}"),
};

var actual = await RoundTripAsync(expected);
Assert.Equivalent(expected, actual);

Assert.Equivalent(expected.Select(x => x.GetG()), actual.Select(x => x.GetG()));
}

public record RecordWithNestedList(int A, string B, List<NonTrivialRecord> NestedList)
{
private readonly string _c = $@"{A} {B}";

public string GetC() => _c;
}

[Fact]
public async Task RecordWithNestedList_Roundtrip()
{
var expected = new[]
{
new RecordWithNestedList(4, "[4]",
[
new NonTrivialRecord(5, "{5}"),
new NonTrivialRecord(15, "{15}"),
new NonTrivialRecord(25, "{25}"),
new NonTrivialRecord(35, "{35}"),
]),

new RecordWithNestedList(7, "[4]",
[
new NonTrivialRecord(2, "{2}"),
new NonTrivialRecord(4, "{4}"),
new NonTrivialRecord(8, "{8}"),
new NonTrivialRecord(16, "{16}"),
]),
};

var actual = await RoundTripAsync(expected);

Assert.Equivalent(expected, actual);

Assert.Equivalent(expected.Select(x => x.GetC()), actual.Select(x => x.GetC()));

Assert.Equivalent(expected[0].NestedList.Select(x => x.GetG()), actual[0].NestedList.Select(x => x.GetG()));
Assert.Equivalent(expected[1].NestedList.Select(x => x.GetG()), actual[1].NestedList.Select(x => x.GetG()));
}

public class ClassWithNullableRecordProperties
{
public TrivialRecord? Trivial { get; set; }

public NonTrivialRecord? NonTrivial { get; set; }

public RecordWithNestedList? NestedList { get; set; }

public List<TrivialRecord>? ListOfTrivial { get; set; }

public List<NonTrivialRecord>? ListOfNonTrivial { get; set; }

public List<RecordWithNestedList>? ListOfNestedList { get; set; }
}

[Fact]
public async Task ClassWithNullableRecordProperties_AllNull()
{
var expected = new[]
{
new ClassWithNullableRecordProperties(),
};

var actual = await RoundTripAsync(expected);

Assert.Equivalent(expected, actual);
}

[Fact]
public async Task ClassWithNullableRecordProperties_SimpleProperties()
{
var expected = new[]
{
new ClassWithNullableRecordProperties(),

new ClassWithNullableRecordProperties
{
Trivial = new(11, "[11]"),
NonTrivial = new(22, "{22}"),
},
};

var actual = await RoundTripAsync(expected);

Assert.Equivalent(expected, actual);

Assert.Equivalent(expected[1].NonTrivial!.GetG(), actual[1].NonTrivial!.GetG());
}

[Fact]
public async Task ClassWithNullableRecordProperties_Lists()
{
var expected = new[]
{
new ClassWithNullableRecordProperties(),

new ClassWithNullableRecordProperties
{
ListOfTrivial =
[
new(33, "[33]"),
new(44, "[44]"),
],
ListOfNonTrivial =
[
new(55, "[55]"),
new(66, "[66]"),
],
},
};

var actual = await RoundTripAsync(expected);

Assert.Equivalent(expected, actual);

Assert.Equivalent(
expected[1].ListOfNonTrivial!.Select(x => x?.GetG()),
actual[1].ListOfNonTrivial!.Select(x => x?.GetG()));
}

[Fact]
public async Task ClassWithNullableRecordProperties_Mixed()
{
var expected = new[]
{
new ClassWithNullableRecordProperties(),

new ClassWithNullableRecordProperties
{
Trivial = new(11, "[11]"),
NonTrivial = new(22, "{22}"),
ListOfTrivial =
[
new(33, "[33]"),
new(44, "[44]"),
],
ListOfNonTrivial =
[
new(55, "[55]"),
new(66, "[66]"),
],
ListOfNestedList =
[
new RecordWithNestedList(4, "[4]",
[
new NonTrivialRecord(5, "{5}"),
new NonTrivialRecord(15, "{15}"),
new NonTrivialRecord(25, "{25}"),
new NonTrivialRecord(35, "{35}"),
]),

new RecordWithNestedList(7, "[4]",
[
new NonTrivialRecord(2, "{2}"),
new NonTrivialRecord(4, "{4}"),
new NonTrivialRecord(8, "{8}"),
new NonTrivialRecord(16, "{16}"),
]),
]
},
};

var actual = await RoundTripAsync(expected);

Assert.Equivalent(expected, actual);

Assert.Equivalent(expected[1].NonTrivial!.GetG(), actual[1].NonTrivial!.GetG());

Assert.Equivalent(
expected[1].ListOfNonTrivial!.Select(x => x?.GetG()),
actual[1].ListOfNonTrivial!.Select(x => x?.GetG()));

Assert.Equivalent(
expected[1].ListOfNestedList!.Select(x => x?.GetC()),
actual[1].ListOfNestedList!.Select(x => x?.GetC()));

Assert.Equivalent(
expected[1].ListOfNestedList!.SelectMany(x => x.NestedList.Select(y => y?.GetG())),
actual[1].ListOfNestedList!.SelectMany(x => x.NestedList.Select(y => y?.GetG())));
}

private static async Task<IList<T>> RoundTripAsync<T>(IList<T> records)
{
using var ms = new MemoryStream();
await ParquetSerializer.SerializeAsync(records, ms);
ms.Position = 0;
return await ParquetSerializer.DeserializeAsync<T>(ms);
}
#endif
}
}
9 changes: 7 additions & 2 deletions src/Parquet/Serialization/Dremel/FieldAssemblerCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using Parquet.Data;
using Parquet.Extensions;
using Parquet.Schema;
Expand Down Expand Up @@ -170,6 +172,9 @@ private Expression TransitionRSM() {
_rsmVar.ClearArray(_rlVar)));
}

private static Expression AllocateNew(Type type)
=> PreConstructor.AllocateNew(type);

private Expression GetCollectionElement(Expression collection, int rlDepth,
Type collectionType, Type elementType) {
ParameterExpression indexVar = Expression.Variable(typeof(int), "index");
Expand All @@ -185,7 +190,7 @@ private Expression GetCollectionElement(Expression collection, int rlDepth,
Expression.LessThanOrEqual(downcastedCollection.CollectionCount(collectionType), indexVar),

Expression.Block(
Expression.Assign(resultElementVar, Expression.New(elementType)),
Expression.Assign(resultElementVar, AllocateNew(elementType)),
downcastedCollection.CollectionAdd(collectionType, resultElementVar, elementType)),

Expression.Assign(resultElementVar, Expression.Property(downcastedCollection, "Item", indexVar))
Expand Down Expand Up @@ -245,7 +250,7 @@ private static Expression CreateInstance(Type t) {
return Expression.NewArrayBounds(t.GetElementType()!, Zero);
}

return Expression.New(t);
return AllocateNew(t);
}

private static Expression RebuildArray(Expression arrayAccessor, Type arrayType, Expression newElement) {
Expand Down
Loading
Loading