Skip to content

Commit d5304fe

Browse files
authored
Merge pull request #115 from vidyaranya92/feat/nullable-inserts
fix: casting nulls while generating insert statement
2 parents 8515bca + d921b79 commit d5304fe

File tree

5 files changed

+48
-16
lines changed

5 files changed

+48
-16
lines changed

Tests/ksqlDB.RestApi.Client.Tests/KSql/RestApi/KSqlDbRestApiClientTests.cs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -589,12 +589,14 @@ public class POC
589589
public int Id { get; init; }
590590
public string? Description { get; init; }
591591
public POC2[]? Entities { get; init; }
592+
public int? Foo { get; init; }
592593
}
593594

594595
public class POC2
595596
{
596597
public POC3[]? Poc3 { get; set; }
597598
public POC4[]? Poc4 { get; set; }
599+
public POC5? Poc5 { get; set; }
598600
}
599601

600602
public class POC3
@@ -606,13 +608,24 @@ public class POC4
606608
{
607609
public string? Description { get; init; }
608610
}
611+
public class POC5
612+
{
613+
public double? Amount { get; init; }
614+
}
609615

610616
public static IEnumerable<(POC, string)> NullTestCases()
611-
{ // empty array constructors are invalid
612-
yield return (new POC { Id = 1 }, "INSERT INTO POCS (Id, Description, Entities) VALUES (1, NULL, NULL);");
613-
yield return (new POC { Id = 1, Entities = new POC2[0] }, "INSERT INTO POCS (Id, Description, Entities) VALUES (1, NULL, ARRAY_REMOVE(ARRAY[0], 0));");
614-
yield return (new POC { Id = 1, Entities = new POC2[] { new POC2 { Poc3 = new POC3[0] } } }, "INSERT INTO POCS (Id, Description, Entities) VALUES (1, NULL, ARRAY[STRUCT(Poc3 := ARRAY_REMOVE(ARRAY[0], 0), Poc4 := ARRAY_REMOVE(ARRAY[0], 0))]);");
615-
yield return (new POC { Id = 1, Entities = new POC2[] { new POC2 { Poc3 = new POC3[0], Poc4 = new POC4[0] } } }, "INSERT INTO POCS (Id, Description, Entities) VALUES (1, NULL, ARRAY[STRUCT(Poc3 := ARRAY_REMOVE(ARRAY[0], 0), Poc4 := ARRAY_REMOVE(ARRAY[0], 0))]);");
617+
{
618+
// empty array constructors are invalid
619+
yield return (new POC { Id = 1, Foo = null}, "INSERT INTO POCS (Id, Description, Entities, Foo) VALUES (1, NULL, NULL, NULL);");
620+
yield return (new POC { Id = 1, Entities = new POC2[0] }, "INSERT INTO POCS (Id, Description, Entities, Foo) VALUES (1, NULL, ARRAY_REMOVE(ARRAY[0], 0), NULL);");
621+
yield return (new POC { Id = 1, Entities = new POC2[] { new POC2 { Poc3 = new POC3[0]} } },
622+
"INSERT INTO POCS (Id, Description, Entities, Foo) VALUES (1, NULL, ARRAY[STRUCT(Poc3 := ARRAY_REMOVE(ARRAY[0], 0), Poc4 := ARRAY_REMOVE(ARRAY[0], 0), Poc5 := CAST(NULL AS STRUCT<Amount DOUBLE>))], NULL);");
623+
yield return (new POC { Id = 1, Entities = new POC2[] { new POC2 { Poc3 = new POC3[0], Poc4 = new POC4[0] } } },
624+
"INSERT INTO POCS (Id, Description, Entities, Foo) VALUES (1, NULL, ARRAY[STRUCT(Poc3 := ARRAY_REMOVE(ARRAY[0], 0), Poc4 := ARRAY_REMOVE(ARRAY[0], 0), Poc5 := CAST(NULL AS STRUCT<Amount DOUBLE>))], NULL);");
625+
yield return (new POC { Id = 1, Entities = new POC2[] { new POC2 { Poc3 = new[] { new POC3() {Description = null}}, Poc4 = new POC4[0] } } },
626+
"INSERT INTO POCS (Id, Description, Entities, Foo) VALUES (1, NULL, ARRAY[STRUCT(Poc3 := ARRAY[STRUCT(Description := CAST(NULL AS VARCHAR))], Poc4 := ARRAY_REMOVE(ARRAY[0], 0), Poc5 := CAST(NULL AS STRUCT<Amount DOUBLE>))], NULL);");
627+
yield return (new POC { Id = 1, Entities = new POC2[] { new POC2 { Poc3 = new[] { new POC3() {Description = null}}, Poc4 = new POC4[0], Poc5 = new POC5() {Amount = 10.05}} } },
628+
"INSERT INTO POCS (Id, Description, Entities, Foo) VALUES (1, NULL, ARRAY[STRUCT(Poc3 := ARRAY[STRUCT(Description := CAST(NULL AS VARCHAR))], Poc4 := ARRAY_REMOVE(ARRAY[0], 0), Poc5 := STRUCT(Amount := 10.05))], NULL);");
616629
}
617630

618631
[TestCaseSource(nameof(NullTestCases))]
@@ -623,6 +636,7 @@ public void ToInsertStatement_WithNullHandling((POC, string) testCase)
623636
var modelBuilder = new ModelBuilder();
624637
modelBuilder.Entity<POC>().HasKey(i => i.Id);
625638
modelBuilder.Entity<POC>().Property(c => c.Entities).AsStruct();
639+
modelBuilder.Entity<POC>().Property(c => c.Entities!.FirstOrDefault()!.Poc5).AsStruct();
626640
var sut = new KSqlDbRestApiClient(HttpClientFactory, modelBuilder, LoggerFactoryMock.Object);
627641

628642
//Act

ksqlDb.RestApi.Client/ChangeLog.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# ksqlDB.RestApi.Client
22

3+
# 7.1.5
4+
5+
## 🐛 Bug Fixes
6+
- casting nulls while generating insert statement. #115 (contributed by @vidyaranya92)
7+
38
# 7.1.4
49

510
## 🐛 Bug Fixes
@@ -80,7 +85,7 @@ This enables updating the credentials at runtime by setting them on the `KSqlDbC
8085
## 🚀 New Features
8186
1. **KSqlDbRestApiClient Constructor Update**:
8287
- The `KSqlDbRestApiClient` class constructor now includes a parameter for `KSqlDBRestApiClientOptions`.
83-
88+
8489
2. **EntityCreationMetadata Update**:
8590
- The `EntityCreationMetadata.ShouldPluralizeEntityName` property was modified to be a nullable boolean (`bool?`), and the default value of `true` was removed.
8691

@@ -91,7 +96,7 @@ This enables updating the credentials at runtime by setting them on the `KSqlDbC
9196
- `InsertProperties`
9297
- `DropFromItemProperties`
9398
- If `ShouldPluralizeEntityName` is null, the methods will set it using the value from the `KSqlDBRestApiClientOptions`.
94-
99+
95100
## 🐛 Bug Fix
96101
- `KSqlDBContext` removed `!NETSTANDARD` pragma from `OnDisposeAsync`
97102

@@ -124,7 +129,7 @@ This enables updating the credentials at runtime by setting them on the `KSqlDbC
124129
- see also [breakingchanges.md](https://github.com/tomasfabian/ksqlDB.RestApi.Client-DotNet/blob/main/docs/breaking_changes.md#v600)
125130

126131
## 🐛 Bug Fix
127-
- CreateQueryStream doesn't always use configured parameters. The PullQuery functionality was overriding the options configured for the PushQuery. #75 reported by @jbkuczma
132+
- CreateQueryStream doesn't always use configured parameters. The PullQuery functionality was overriding the options configured for the PushQuery. #75 reported by @jbkuczma
128133

129134
# 5.1.0
130135
- added `InsertIntoAsync` Qbservable extension for executing `INSERT INTO <stream-name> SELECT` statements.

ksqlDb.RestApi.Client/KSql/RestApi/Statements/CreateKSqlValue.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ internal sealed class CreateKSqlValue(IMetadataProvider metadataProvider) : Enti
1515
{
1616
private const string CREATE_EMPTY_ARRAY = "ARRAY_REMOVE(ARRAY[0], 0)";
1717

18-
public object ExtractValue<T>(T inputValue, IValueFormatters valueFormatters, MemberInfo memberInfo, Type type, Func<MemberInfo, string> formatter)
18+
public object ExtractValue<T>(T inputValue, IValueFormatters valueFormatters, MemberInfo memberInfo, Type type, Func<MemberInfo, string> formatter, bool castNulls = false)
1919
{
2020
Type valueType = inputValue.GetType();
2121

@@ -27,7 +27,15 @@ public object ExtractValue<T>(T inputValue, IValueFormatters valueFormatters, Me
2727
value = valueType.GetField(memberInfo.Name)?.GetValue(inputValue);
2828

2929
if (value == null)
30+
{
31+
if (castNulls && !type.IsArray)
32+
{
33+
KSqlTypeTranslator<T> typeTranslator = new(metadataProvider);
34+
var ksqlType = typeTranslator.Translate(type, memberInfo);
35+
return $"CAST({KSqlTypes.Null} AS {ksqlType})";
36+
}
3037
return KSqlTypes.Null;
38+
}
3139

3240
if (type == typeof(decimal))
3341
{
@@ -148,7 +156,7 @@ private void GenerateStruct<T>(IValueFormatters valueFormatters, Type type, Func
148156

149157
type = GetMemberType(memberInfo2);
150158

151-
var innerValue = ExtractValue(value, valueFormatters, memberInfo2, type, formatter);
159+
var innerValue = ExtractValue(value, valueFormatters, memberInfo2, type, formatter, true);
152160
var name = formatter(memberInfo2);
153161
if (type.IsArray && KSqlTypes.Null.Equals(innerValue))
154162
sb.Append($"{name} := {CREATE_EMPTY_ARRAY}");

ksqlDb.RestApi.Client/KSql/RestApi/Statements/EntityInfo.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,17 @@ protected bool IsStructType<TEntity>(Type type, MemberInfo? memberInfo)
6666
if (memberInfo == null)
6767
return false;
6868

69+
// First check if the exact type is directly marked as struct in metadata
6970
var entityMetadata = metadataProvider.GetEntities().FirstOrDefault(c => c.Type == typeof(TEntity));
7071
var fieldMetadata = entityMetadata?.GetFieldMetadataBy(memberInfo);
71-
return fieldMetadata is
72-
{
73-
IsStruct: true
74-
};
72+
73+
if (fieldMetadata is { IsStruct: true })
74+
return true;
75+
76+
// Check all entities for any struct field using this type
77+
return metadataProvider.GetEntities()
78+
.SelectMany(metadata => metadata.FieldsMetadataDict.Values)
79+
.Any(field => field.IsStruct && GetMemberType(field.MemberInfo) == type);
7580
}
7681

7782
protected static Type GetMemberType(MemberInfo memberInfo)

ksqlDb.RestApi.Client/ksqlDb.RestApi.Client.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
Documentation for the library can be found at https://github.com/tomasfabian/ksqlDB.RestApi.Client-DotNet/blob/main/README.md.
1616
</Description>
1717
<PackageTags>ksql ksqlDB LINQ .NET csharp push query</PackageTags>
18-
<Version>7.1.4</Version>
19-
<AssemblyVersion>7.1.4.0</AssemblyVersion>
18+
<Version>7.1.5</Version>
19+
<AssemblyVersion>7.1.5.0</AssemblyVersion>
2020
<LangVersion>13.0</LangVersion>
2121
<ImplicitUsings>enable</ImplicitUsings>
2222
<PackageReleaseNotes>https://github.com/tomasfabian/ksqlDB.RestApi.Client-DotNet/blob/main/ksqlDb.RestApi.Client/ChangeLog.md</PackageReleaseNotes>

0 commit comments

Comments
 (0)