Skip to content

Commit 5239ecf

Browse files
authored
Merge pull request #113 from mrt181/main
fix: empty array constructors are not valid
2 parents a47fabf + 4d35ee8 commit 5239ecf

File tree

3 files changed

+71
-17
lines changed

3 files changed

+71
-17
lines changed

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

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using Moq;
2020
using Moq.Protected;
2121
using NUnit.Framework;
22+
using ksqlDb.RestApi.Client.FluentAPI.Builders;
2223

2324
namespace ksqlDb.RestApi.Client.Tests.KSql.RestApi;
2425

@@ -153,7 +154,7 @@ public async Task CreateContent_KSqlContentWasSet()
153154

154155
//Assert
155156
var content = await GetContent(stringContent);
156-
157+
157158
content.Should().Be(@$"{{""ksql"":""{createOrReplaceTableStatement}"",""streamsProperties"":{{}}}}");
158159
}
159160

@@ -307,7 +308,7 @@ public async Task GetAllTopicsExtendedAsync()
307308
//Assert
308309
responses.Should().NotBeNull();
309310
var expectedContent = GetExpectedContent(StatementTemplates.ShowAllTopicsExtended);
310-
311+
311312
VerifySendAsync(expectedContent);
312313
}
313314

@@ -355,7 +356,7 @@ public async Task TerminatePushQueryAsync()
355356
{
356357
QueryId = queryId
357358
};
358-
359+
359360
var expectedContent = await KSqlDbRestApiClient.CreateContent(closeQuery, Encoding.UTF8).ReadAsStringAsync();
360361

361362
VerifySendAsync(expectedContent, "/close-query");
@@ -459,7 +460,7 @@ public async Task DropStreamAsync_WithDropEntityProperties()
459460
//Assert
460461
response.Should().NotBeNull();
461462
var expectedContent = GetExpectedContent(StatementTemplates.DropStream(streamName, properties.UseIfExistsClause, properties.DeleteTopic));
462-
463+
463464
VerifySendAsync(expectedContent);
464465
}
465466

@@ -479,7 +480,7 @@ public async Task DropStreamAsync_IfExistsAndDeleteTopic()
479480
//Assert
480481
response.Should().NotBeNull();
481482
var expectedContent = GetExpectedContent(StatementTemplates.DropStream(streamName, useIfExistsClause, deleteTopic));
482-
483+
483484
VerifySendAsync(expectedContent);
484485
}
485486

@@ -517,7 +518,7 @@ public async Task DropTableAsync_IfExistsAndDeleteTopic()
517518
//Assert
518519
response.Should().NotBeNull();
519520
var expectedContent = GetExpectedContent(StatementTemplates.DropTable(tableName, useIfExistsClause, deleteTopic));
520-
521+
521522
VerifySendAsync(expectedContent);
522523
}
523524

@@ -543,7 +544,7 @@ public async Task DropTableAsync_WithDropEntityProperties()
543544
//Assert
544545
response.Should().NotBeNull();
545546
var expectedContent = GetExpectedContent(StatementTemplates.DropTable(tableName, properties.UseIfExistsClause, properties.DeleteTopic));
546-
547+
547548
VerifySendAsync(expectedContent);
548549
}
549550

@@ -583,6 +584,54 @@ public void ToInsertStatement()
583584
insertStatement.Sql.Should().Be("INSERT INTO Movies (Title, Id, Release_Year) VALUES (NULL, 1, 0);");
584585
}
585586

587+
public class POC
588+
{
589+
public int Id { get; init; }
590+
public string? Description { get; init; }
591+
public POC2[]? Entities { get; init; }
592+
}
593+
594+
public class POC2
595+
{
596+
public POC3[]? Poc3 { get; set; }
597+
public POC4[]? Poc4 { get; set; }
598+
}
599+
600+
public class POC3
601+
{
602+
public string? Description { get; init; }
603+
}
604+
605+
public class POC4
606+
{
607+
public string? Description { get; init; }
608+
}
609+
610+
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))]);");
616+
}
617+
618+
[TestCaseSource(nameof(NullTestCases))]
619+
public void ToInsertStatement_WithNullHandling((POC, string) testCase)
620+
{
621+
//Arrange
622+
var (entity, expected) = testCase;
623+
var modelBuilder = new ModelBuilder();
624+
modelBuilder.Entity<POC>().HasKey(i => i.Id);
625+
modelBuilder.Entity<POC>().Property(c => c.Entities).AsStruct();
626+
var sut = new KSqlDbRestApiClient(HttpClientFactory, modelBuilder, LoggerFactoryMock.Object);
627+
628+
//Act
629+
var actual = sut.ToInsertStatement(entity);
630+
631+
//Assert
632+
actual.Sql.Should().Be(expected);
633+
}
634+
586635
[KSqlFunction]
587636
public static string FormatTimestamp(long input, string format) => throw new NotSupportedException();
588637

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

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ namespace ksqlDB.RestApi.Client.KSql.RestApi.Statements;
1313
#nullable disable
1414
internal sealed class CreateKSqlValue(IMetadataProvider metadataProvider) : EntityInfo(metadataProvider)
1515
{
16+
private const string CREATE_EMPTY_ARRAY = "ARRAY_REMOVE(ARRAY[0], 0)";
17+
1618
public object ExtractValue<T>(T inputValue, IValueFormatters valueFormatters, MemberInfo memberInfo, Type type, Func<MemberInfo, string> formatter)
1719
{
1820
Type valueType = inputValue.GetType();
@@ -25,7 +27,7 @@ public object ExtractValue<T>(T inputValue, IValueFormatters valueFormatters, Me
2527
value = valueType.GetField(memberInfo.Name)?.GetValue(inputValue);
2628

2729
if (value == null)
28-
return "NULL";
30+
return KSqlTypes.Null;
2931

3032
if (type == typeof(decimal))
3133
{
@@ -79,9 +81,7 @@ public object ExtractValue<T>(T inputValue, IValueFormatters valueFormatters, Me
7981
GenerateMap(valueFormatters, type, formatter, ref value);
8082
else if (type.IsArray)
8183
{
82-
var source = ((IEnumerable)value).Cast<object>();
83-
var array = source.Select(c => ExtractValue(c, valueFormatters, null, type.GetElementType(), formatter)).ToArray();
84-
value = PrintArray(array);
84+
value = GenerateEnumerableValue(type, value, valueFormatters, formatter);
8585
}
8686
else if (!type.IsGenericType && (type.IsClass || type.IsStruct()))
8787
{
@@ -150,7 +150,11 @@ private void GenerateStruct<T>(IValueFormatters valueFormatters, Type type, Func
150150

151151
var innerValue = ExtractValue(value, valueFormatters, memberInfo2, type, formatter);
152152
var name = formatter(memberInfo2);
153-
sb.Append($"{name} := {innerValue}");
153+
if (type.IsArray && KSqlTypes.Null.Equals(innerValue))
154+
sb.Append($"{name} := {CREATE_EMPTY_ARRAY}");
155+
else
156+
sb.Append($"{name} := {innerValue}");
157+
154158
}
155159

156160
sb.Append(')');
@@ -162,21 +166,21 @@ private object GenerateEnumerableValue(Type type, object value, IValueFormatters
162166
Func<MemberInfo, string> formatter)
163167
{
164168
if (value == null)
165-
return "NULL";
169+
return KSqlTypes.Null;
166170

167171
var enumerableType = type.GetEnumerableTypeDefinition();
168172

169173
if (enumerableType == null || !enumerableType.Any())
170174
return value;
171175

172-
type = enumerableType.First();
173-
type = type.GetGenericArguments()[0];
176+
var elementType = type.IsArray ? type.GetElementType() : enumerableType.First();
177+
elementType = type.IsArray ? elementType : elementType.GetGenericArguments()[0];
174178

175179
var source = ((IEnumerable)value).Cast<object>();
176-
var array = source.Select(c => ExtractValue(c, valueFormatters, null, type, formatter)).ToArray();
180+
var array = source.Select(c => ExtractValue(c, valueFormatters, null, elementType, formatter)).ToArray();
177181

178182
if (array.Length == 0)
179-
return "ARRAY_REMOVE(ARRAY[0], 0)";
183+
return CREATE_EMPTY_ARRAY;
180184

181185
return PrintArray(array);
182186
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ internal static class KSqlTypes
1515
internal const string Array = "ARRAY";
1616
internal const string Struct = "STRUCT";
1717
internal const string Map = "MAP";
18+
internal const string Null = "NULL";
1819
}
1920
}

0 commit comments

Comments
 (0)