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 CLR structs, both nullable and non-nullable #495 #568

Merged
merged 2 commits into from
Nov 12, 2024
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

env:
VERSION: 5.0.2
PACKAGE_SUFFIX: '-pre.1'
PACKAGE_SUFFIX: '-pre.2'
# PACKAGE_SUFFIX: ''
ASM_VERSION: 5.0.0
DOC_INSTANCE: wrs/pq
Expand Down
1 change: 1 addition & 0 deletions docs/rn/5.0.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Improvements

- Serialisation of CLR (not Parquet) structs and nullable structs is now properly handled and supported, thanks to @paulengineer.
- For Windows, run unit tests on x86 and x32 explicitly.
- Improved GHA build/release process, combining all workflows into one and simplifying it, most importantly release management.

Expand Down
28 changes: 28 additions & 0 deletions src/Parquet.Test/Serialisation/ParquetSerializerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1050,5 +1050,33 @@ public async Task DateOnlyTimeOnly_Nullable_Serde() {
}

#endif

private struct StructWithIntProp {
public int Id { get; set; }
}

private class ClassWithNullableCustomStruct {
public StructWithIntProp? NullableStruct { get; set; }
}

[Fact]
public async Task Class_With_Nullable_Struct() {

ParquetSchema schema = typeof(ClassWithNullableCustomStruct).GetParquetSchema(true);

var data = new List<ClassWithNullableCustomStruct> {
new ClassWithNullableCustomStruct() {
NullableStruct = null
}
};

using var ms = new MemoryStream();
await ParquetSerializer.SerializeAsync(data, ms);

ms.Position = 0;
IList<ClassWithNullableCustomStruct> data2 = await ParquetSerializer.DeserializeAsync<ClassWithNullableCustomStruct>(ms);

Assert.Equivalent(data2, data);
}
}
}
50 changes: 50 additions & 0 deletions src/Parquet.Test/Serialisation/SchemaReflectorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

public int[]? IntArray { get; set; }

public bool MarkerField;

Check warning on line 26 in src/Parquet.Test/Serialisation/SchemaReflectorTest.cs

View workflow job for this annotation

GitHub Actions / 🏛️ Build NuGet

Field 'SchemaReflectorTest.PocoClass.MarkerField' is never assigned to, and will always have its default value false

Check warning on line 26 in src/Parquet.Test/Serialisation/SchemaReflectorTest.cs

View workflow job for this annotation

GitHub Actions / 🏛️ Build NuGet

Field 'SchemaReflectorTest.PocoClass.MarkerField' is never assigned to, and will always have its default value false

Check warning on line 26 in src/Parquet.Test/Serialisation/SchemaReflectorTest.cs

View workflow job for this annotation

GitHub Actions / 🏛️ Build NuGet

Field 'SchemaReflectorTest.PocoClass.MarkerField' is never assigned to, and will always have its default value false

Check warning on line 26 in src/Parquet.Test/Serialisation/SchemaReflectorTest.cs

View workflow job for this annotation

GitHub Actions / 🧪 Unit Tests - Linux x64

Field 'SchemaReflectorTest.PocoClass.MarkerField' is never assigned to, and will always have its default value false

Check warning on line 26 in src/Parquet.Test/Serialisation/SchemaReflectorTest.cs

View workflow job for this annotation

GitHub Actions / 🧪 Unit Tests - Linux x64

Field 'SchemaReflectorTest.PocoClass.MarkerField' is never assigned to, and will always have its default value false

Check warning on line 26 in src/Parquet.Test/Serialisation/SchemaReflectorTest.cs

View workflow job for this annotation

GitHub Actions / 🧪 Unit Tests - MacOS ARM64

Field 'SchemaReflectorTest.PocoClass.MarkerField' is never assigned to, and will always have its default value false

Check warning on line 26 in src/Parquet.Test/Serialisation/SchemaReflectorTest.cs

View workflow job for this annotation

GitHub Actions / 🧪 Unit Tests - MacOS ARM64

Field 'SchemaReflectorTest.PocoClass.MarkerField' is never assigned to, and will always have its default value false

Check warning on line 26 in src/Parquet.Test/Serialisation/SchemaReflectorTest.cs

View workflow job for this annotation

GitHub Actions / 🧪 Unit Tests - Windows x86

Field 'SchemaReflectorTest.PocoClass.MarkerField' is never assigned to, and will always have its default value false

Check warning on line 26 in src/Parquet.Test/Serialisation/SchemaReflectorTest.cs

View workflow job for this annotation

GitHub Actions / 🧪 Unit Tests - Windows x86

Field 'SchemaReflectorTest.PocoClass.MarkerField' is never assigned to, and will always have its default value false

Check warning on line 26 in src/Parquet.Test/Serialisation/SchemaReflectorTest.cs

View workflow job for this annotation

GitHub Actions / 🧪 Unit Tests - Windows x64

Field 'SchemaReflectorTest.PocoClass.MarkerField' is never assigned to, and will always have its default value false

Check warning on line 26 in src/Parquet.Test/Serialisation/SchemaReflectorTest.cs

View workflow job for this annotation

GitHub Actions / 🧪 Unit Tests - Windows x64

Field 'SchemaReflectorTest.PocoClass.MarkerField' is never assigned to, and will always have its default value false
}

[Fact]
Expand Down Expand Up @@ -662,5 +662,55 @@
Assert.Equal(typeof(short), sedf.ClrType);
Assert.False(dedf.IsNullable);
}

struct SimpleClrStruct {
public int Id { get; set; }
}

[Fact]
public void ClrStruct_IsSupported() {
ParquetSchema schema = typeof(SimpleClrStruct).GetParquetSchema(true);
Assert.Single(schema.Fields);
Assert.Equal(typeof(int), schema.DataFields[0].ClrType);
}

class StructWithClrStruct {
public SimpleClrStruct S { get; set; }
}

[Fact]
public void ClrStruct_AsMember_IsSupported() {
ParquetSchema schema = typeof(StructWithClrStruct).GetParquetSchema(false);
Assert.Single(schema.Fields);

// check it's a required struct
StructField sf = (StructField)schema[0];
Assert.False(sf.IsNullable, "struct cannot be optional");

// check the struct field
Assert.Single(sf.Children);
var idField = (DataField)sf.Children[0];
Assert.Equal(typeof(int), idField.ClrType);
}

class StructWithNullableClrStruct {
// as CLR struct is ValueType, this resolves to System.Nullable<SimpleClrStruct>
public SimpleClrStruct? N { get; set; }
}

[Fact]
public void ClrStruct_AsNullableMember_IsSupported() {
ParquetSchema schema = typeof(StructWithNullableClrStruct).GetParquetSchema(false);
Assert.Single(schema.Fields);

// check it's a required struct
StructField sf = (StructField)schema[0];
Assert.True(sf.IsNullable, "struct must be nullable");

// check the struct field
Assert.Single(sf.Children);
var idField = (DataField)sf.Children[0];
Assert.Equal(typeof(int), idField.ClrType);
}
}
}
1 change: 1 addition & 0 deletions src/Parquet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "rn", "rn", "{EE4ECD1F-5A44-4FC7-BC73-930AFB15D372}"
ProjectSection(SolutionItems) = preProject
..\docs\rn\5.0.2.md = ..\docs\rn\5.0.2.md
..\docs\rn\previous.md = ..\docs\rn\previous.md
EndProjectSection
EndProject
Global
Expand Down
4 changes: 2 additions & 2 deletions src/Parquet/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public static bool IsNullable(this Type t) {
TypeInfo ti = t.GetTypeInfo();

return
ti.IsClass ||
ti.IsClass || ti.IsInterface ||
(ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(Nullable<>));
}

Expand All @@ -121,7 +121,7 @@ public static bool IsSystemNullable(this Type t) {
public static Type GetNonNullable(this Type t) {
TypeInfo ti = t.GetTypeInfo();

if(ti.IsClass) {
if(ti.IsClass || ti.IsInterface) {
return t;
}

Expand Down
15 changes: 10 additions & 5 deletions src/Parquet/Serialization/Dremel/FieldAssemblerCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -304,20 +304,25 @@ private ClassMember GetClassMember(Type rootType, Expression rootVar,

// Dictionary is a special case, because it cannot be constructed independently in one go, so the client needs to know it a dictionary

Type? type = null;
MemberExpression? accessor = null;
Type type = rootType;
Expression accessor = rootVar;

PropertyInfo? pi = rootType.GetProperty(name);
if(type.IsSystemNullable()) {
type = type.GetNonNullable();
accessor = Expression.Property(accessor, "Value");
}

PropertyInfo? pi = type.GetProperty(name);
if(pi != null) {
type = pi.PropertyType;
accessor = Expression.Property(rootVar, name);
accessor = Expression.Property(accessor, name);
}

if(pi == null) {
FieldInfo? fi = rootType.GetField(name);
if(fi != null) {
type = fi.FieldType;
accessor = Expression.Field(rootVar, name);
accessor = Expression.Field(accessor, name);
}
}

Expand Down
29 changes: 20 additions & 9 deletions src/Parquet/Serialization/Dremel/FieldStriperCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,9 @@ private Expression WhileBody(Expression element, bool isAtomic, int dl, Paramete

isAtomic
? Expression.Assign(isLeafVar, Expression.Constant(true))
: Expression.Assign(isLeafVar, elementType.IsValueType ? Expression.Constant(false) : valueVar.IsNull()),
: (elementType.IsValueType && !elementType.IsSystemNullable())
? Expression.Assign(isLeafVar, Expression.Constant(false))
: Expression.Assign(isLeafVar, valueVar.IsNull()),

Expression.IfThenElse(
Expression.IsTrue(isLeafVar),
Expand Down Expand Up @@ -248,19 +250,28 @@ private Expression GetClassMemberAccessorAndType(
Expression.Default(type)));
}

PropertyInfo? pi = rootType.GetProperty(name);
if(pi != null) {
type = pi.PropertyType;
return Expression.Property(rootVar, name);
Expression? result = rootVar;
type = rootType;

if(rootType.IsSystemNullable()) {
result = Expression.Property(result, "Value");
type = rootType.GetNonNullable();
}

FieldInfo? fi = rootType.GetField(name);
if(fi != null) {
PropertyInfo? pi = type.GetProperty(name);
FieldInfo? fi = type.GetField(name);

if(pi != null) {
type = pi.PropertyType;
result = Expression.Property(result, name);
} else if(fi != null) {
type = fi.FieldType;
return Expression.Field(rootVar, name);
result = Expression.Field(result, name);
} else {
throw new NotSupportedException($"There is no class property of field called '{name}'.");
}

throw new NotSupportedException($"There is no class property of field called '{name}'.");
return result;
}

private Expression DissectRecord(
Expand Down
15 changes: 8 additions & 7 deletions src/Parquet/Serialization/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -242,20 +242,20 @@ private static Field MakeField(Type t, string columnName, string propertyName,
ClassMember? member,
bool forWriting) {

Type bt = t.IsNullable() ? t.GetNonNullable() : t;
if(member != null && member.IsLegacyRepeatable && !bt.IsGenericIDictionary() && bt.TryExtractIEnumerableType(out Type? bti)) {
bt = bti!;
Type baseType = t.IsNullable() ? t.GetNonNullable() : t;
if(member != null && member.IsLegacyRepeatable && !baseType.IsGenericIDictionary() && baseType.TryExtractIEnumerableType(out Type? bti)) {
baseType = bti!;
}

if(SchemaEncoder.IsSupported(bt)) {
if(SchemaEncoder.IsSupported(baseType)) {
return ConstructDataField(columnName, propertyName, t, member);
} else if(t.TryExtractDictionaryType(out Type? tKey, out Type? tValue)) {
return ConstructMapField(columnName, propertyName, tKey!, tValue!, forWriting);
} else if(t.TryExtractIEnumerableType(out Type? elementType)) {
return ConstructListField(columnName, propertyName, elementType!, member, forWriting);
} else if(t.IsClass || t.IsInterface || t.IsValueType) {
// must be a struct then (c# class or c# struct)!
List<ClassMember> props = FindMembers(t, forWriting);
} else if(baseType.IsClass || baseType.IsInterface || baseType.IsValueType) {
// must be a struct then (c# class, interface or struct)
List<ClassMember> props = FindMembers(baseType, forWriting);
Field[] fields = props
.Select(p => MakeField(p, forWriting))
.Where(f => f != null)
Expand All @@ -268,6 +268,7 @@ private static Field MakeField(Type t, string columnName, string propertyName,

StructField sf = new StructField(columnName, fields);
sf.ClrPropName = propertyName;
sf.IsNullable = baseType.IsNullable() || t.IsSystemNullable();
return sf;
}

Expand Down
Loading