diff --git a/.github/workflows/e2eTests.yml b/.github/workflows/e2eTests.yml new file mode 100644 index 0000000..25f8b80 --- /dev/null +++ b/.github/workflows/e2eTests.yml @@ -0,0 +1,27 @@ +name: End-to-End Tests + +on: [push, pull_request] + +jobs: + e2eTests: + runs-on: ubuntu-latest + name: End-to-End Tests + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + + - name: Set up Docker + uses: docker/setup-buildx-action@v2 + + - name: Start local Kusto cluster + run: | + docker run -e ACCEPT_EULA=Y -m 4G -d -p 8080:8080 -t mcr.microsoft.com/azuredataexplorer/kustainer-linux:latest + + - name: Set E2E_TESTING environment variables + run: echo "E2E_TESTING=1" >> $GITHUB_ENV + + - name: Run end-to-end tests + run: dotnet test ./CSharp.OpenSource.LinqToKql.Test diff --git a/.github/workflows/unitTests.yml b/.github/workflows/unitTests.yml index df8836a..e9f58ff 100644 --- a/.github/workflows/unitTests.yml +++ b/.github/workflows/unitTests.yml @@ -1,14 +1,17 @@ -name: Unit Tests - -on: [push, pull_request] - -jobs: - UnitTests: - runs-on: ubuntu-latest - name: Unit Tests - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Run unit tests - run: dotnet test ./CSharp.OpenSource.LinqToKql.Test \ No newline at end of file +name: Unit Tests + +on: [push, pull_request] + +jobs: + UnitTests: + runs-on: ubuntu-latest + name: Unit Tests + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set E2E_TESTING environment variable + run: echo "E2E_TESTING=0" >> $GITHUB_ENV + + - name: Run unit tests + run: dotnet test ./CSharp.OpenSource.LinqToKql.Test diff --git a/CSharp.OpenSource.LinqToKql.Test/EndToEndTests.cs b/CSharp.OpenSource.LinqToKql.Test/EndToEndTests.cs new file mode 100644 index 0000000..b6e168b --- /dev/null +++ b/CSharp.OpenSource.LinqToKql.Test/EndToEndTests.cs @@ -0,0 +1,50 @@ +using CSharp.OpenSource.LinqToKql.Http; +using CSharp.OpenSource.LinqToKql.Provider; +using Microsoft.Extensions.DependencyInjection; + +namespace CSharp.OpenSource.LinqToKql.Test; + +public class EndToEndTests +{ + private string _cluster = "https://localhost:8080"; + private string _auth = "myBearerToken"; + private string _defaultDbName = "myDatabaseName"; + + [Fact] + public async Task VerifyKqlValidityOnServer() + { + var e2eTesting = Environment.GetEnvironmentVariable("E2E_TESTING"); + if (e2eTesting == "1") + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddKustoDbContext(sp => new KustoHttpClient(_cluster, _auth, _defaultDbName)); + var sp = serviceCollection.BuildServiceProvider(); + var dbContext = sp.GetRequiredService(); + + var kql = "SampleKqlQuery"; + var result = await dbContext.ExecuteKqlAsync(kql); + + Assert.NotNull(result); + Assert.True(result.IsSuccess); + } + } +} + +public class MyDbContext : KustoDbContext +{ + public MyDbContext(IKustoDbContextExecutor executor) : base(executor) + { + } + + public async Task ExecuteKqlAsync(string kql) + { + var providerExecutor = (KustoHttpClient)ProviderExecutor; + var result = await providerExecutor.QueryAsync(kql); + return result; + } +} + +public class KqlResult +{ + public bool IsSuccess { get; set; } +} diff --git a/CSharp.OpenSource.LinqToKql.Test/ORMGen/ORMGeneratorTests.cs b/CSharp.OpenSource.LinqToKql.Test/ORMGen/ORMGeneratorTests.cs index 2e6f521..35bd190 100644 --- a/CSharp.OpenSource.LinqToKql.Test/ORMGen/ORMGeneratorTests.cs +++ b/CSharp.OpenSource.LinqToKql.Test/ORMGen/ORMGeneratorTests.cs @@ -189,4 +189,47 @@ public void Match_ShouldReturnCorrectMatchResult() Assert.True(isMatch2); Assert.False(isMatch3); } + + [Fact] + public async Task VerifyKqlValidityOnServer() + { + var e2eTesting = Environment.GetEnvironmentVariable("E2E_TESTING"); + if (e2eTesting == "1") + { + SetupProviderExecutor(); + var providerExecutor = _mockExecutor.Object; + var ormGenerator = new ORMGenerator(new() + { + ProviderExecutor = providerExecutor, + ModelsFolderPath = "Models", + DbContextFolderPath = "../../../../Samples/ORMGeneratorTest/AutoGen", + DbContextName = "AutoGenORMKustoDbContext", + Namespace = "AutoGen", + ModelsNamespace = "AutoGen", + DbContextNamespace = "AutoGen", + CreateDbContext = true, + CleanFolderBeforeCreate = true, + EnableNullable = true, + FileScopedNamespaces = true, + DatabaseConfigs = new List + { + new ORMGeneratorDatabaseConfig + { + DatabaseName = DbName1, + Filters = new ORMGeneratorFilterConfig() + }, + new ORMGeneratorDatabaseConfig + { + DatabaseName = DbName2, + DatabaseDisplayName = "db2", + Filters = new ORMGeneratorFilterConfig() + } + } + }); + await ormGenerator.GenerateAsync(); + + Assert.True(File.Exists(ormGenerator.Config.DbContextFilePath)); + Assert.True(Directory.GetFiles(ormGenerator.Config.ModelsFolderPath, "*.cs", SearchOption.AllDirectories).Any()); + } + } } diff --git a/CSharp.OpenSource.LinqToKql.Test/Translator/GroupByTranslatorTests.cs b/CSharp.OpenSource.LinqToKql.Test/Translator/GroupByTranslatorTests.cs index 7b44427..ce34052 100644 --- a/CSharp.OpenSource.LinqToKql.Test/Translator/GroupByTranslatorTests.cs +++ b/CSharp.OpenSource.LinqToKql.Test/Translator/GroupByTranslatorTests.cs @@ -53,4 +53,44 @@ public void Translate_ShouldHandleGroupByWithObject2(bool disableNestedProjectio [_tableName, "where Id == 1", "summarize Key=take_any(Date), Count=count() by Date"], new() { DisableNestedProjection = disableNestedProjection, } ); -} \ No newline at end of file + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Translate_ShouldHandleGroupByWithSum(bool disableNestedProjection) + => AssertQuery( + _q.GroupBy(x => x.Date).Select(g => new { Date = g.Key, Sum = g.Sum(x => x.Value) }), + [_tableName, "summarize Sum=sum(Value) by Date"], + new() { DisableNestedProjection = disableNestedProjection, } + ); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Translate_ShouldHandleGroupByWithAverage(bool disableNestedProjection) + => AssertQuery( + _q.GroupBy(x => x.Date).Select(g => new { Date = g.Key, Average = g.Average(x => x.Value) }), + [_tableName, "summarize Average=avg(Value) by Date"], + new() { DisableNestedProjection = disableNestedProjection, } + ); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Translate_ShouldHandleGroupByWithMin(bool disableNestedProjection) + => AssertQuery( + _q.GroupBy(x => x.Date).Select(g => new { Date = g.Key, Min = g.Min(x => x.Value) }), + [_tableName, "summarize Min=min(Value) by Date"], + new() { DisableNestedProjection = disableNestedProjection, } + ); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Translate_ShouldHandleGroupByWithMax(bool disableNestedProjection) + => AssertQuery( + _q.GroupBy(x => x.Date).Select(g => new { Date = g.Key, Max = g.Max(x => x.Value) }), + [_tableName, "summarize Max=max(Value) by Date"], + new() { DisableNestedProjection = disableNestedProjection, } + ); +} diff --git a/CSharp.OpenSource.LinqToKql.Test/Translator/OrderByTranslatorTests.cs b/CSharp.OpenSource.LinqToKql.Test/Translator/OrderByTranslatorTests.cs index 96a5a2f..d9beb09 100644 --- a/CSharp.OpenSource.LinqToKql.Test/Translator/OrderByTranslatorTests.cs +++ b/CSharp.OpenSource.LinqToKql.Test/Translator/OrderByTranslatorTests.cs @@ -15,4 +15,18 @@ public void Translate_ShouldHandleOrderByDesc() _q.OrderByDescending(x => x.Date), [_tableName, "sort by Date desc"] ); -} \ No newline at end of file + + [Fact] + public void Translate_ShouldHandleThenBy() + => AssertQuery( + _q.OrderBy(x => x.Date).ThenBy(x => x.Id), + [_tableName, "sort by Date asc", "sort by Id asc"] + ); + + [Fact] + public void Translate_ShouldHandleThenByDesc() + => AssertQuery( + _q.OrderBy(x => x.Date).ThenByDescending(x => x.Id), + [_tableName, "sort by Date asc", "sort by Id desc"] + ); +} diff --git a/CSharp.OpenSource.LinqToKql.Test/Translator/SelectTranslatorTests.cs b/CSharp.OpenSource.LinqToKql.Test/Translator/SelectTranslatorTests.cs index ae221ec..fcc272d 100644 --- a/CSharp.OpenSource.LinqToKql.Test/Translator/SelectTranslatorTests.cs +++ b/CSharp.OpenSource.LinqToKql.Test/Translator/SelectTranslatorTests.cs @@ -10,4 +10,32 @@ public void Translate_ShouldHandleSelectWithInit() _q.Select(x => new SampleObject2 { Name2 = x.Name, Id2 = x.Id }), [_tableName, "project Name2=Name, Id2=Id"] ); -} \ No newline at end of file + + [Fact] + public void Translate_ShouldHandleFirst() + => AssertQuery( + _q.First(), + [_tableName, "take 1"] + ); + + [Fact] + public void Translate_ShouldHandleFirstOrDefault() + => AssertQuery( + _q.FirstOrDefault(), + [_tableName, "take 1"] + ); + + [Fact] + public void Translate_ShouldHandleLast() + => AssertQuery( + _q.OrderBy(x => x.Id).Last(), + [_tableName, "sort by Id asc", "take 1"] + ); + + [Fact] + public void Translate_ShouldHandleLastOrDefault() + => AssertQuery( + _q.OrderBy(x => x.Id).LastOrDefault(), + [_tableName, "sort by Id asc", "take 1"] + ); +} diff --git a/CSharp.OpenSource.LinqToKql.Test/Translator/TakeTranslatorTests.cs b/CSharp.OpenSource.LinqToKql.Test/Translator/TakeTranslatorTests.cs index a0f2f13..4083118 100644 --- a/CSharp.OpenSource.LinqToKql.Test/Translator/TakeTranslatorTests.cs +++ b/CSharp.OpenSource.LinqToKql.Test/Translator/TakeTranslatorTests.cs @@ -8,4 +8,4 @@ public void Translate_ShouldHandleTake() _q.Take(50), [_tableName, "take 50"] ); -} \ No newline at end of file +} diff --git a/CSharp.OpenSource.LinqToKql/Translator/Builders/OrderByLinqToKQLTranslator.cs b/CSharp.OpenSource.LinqToKql/Translator/Builders/OrderByLinqToKQLTranslator.cs index 2a53832..310d663 100644 --- a/CSharp.OpenSource.LinqToKql/Translator/Builders/OrderByLinqToKQLTranslator.cs +++ b/CSharp.OpenSource.LinqToKql/Translator/Builders/OrderByLinqToKQLTranslator.cs @@ -4,13 +4,13 @@ namespace CSharp.OpenSource.LinqToKql.Translator.Builders; public class OrderByLinqToKQLTranslator : LinqToKQLTranslatorBase { - public OrderByLinqToKQLTranslator(LinqToKQLQueryTranslatorConfig config) : base(config, new() { nameof(Enumerable.OrderBy), nameof(Enumerable.OrderByDescending) }) + public OrderByLinqToKQLTranslator(LinqToKQLQueryTranslatorConfig config) : base(config, new() { nameof(Enumerable.OrderBy), nameof(Enumerable.OrderByDescending), nameof(Enumerable.ThenBy), nameof(Enumerable.ThenByDescending) }) { } public override string Handle(MethodCallExpression methodCall, Expression? parent) { - return Handle(methodCall, methodCall.Method.Name == nameof(Enumerable.OrderByDescending)); + return Handle(methodCall, methodCall.Method.Name == nameof(Enumerable.OrderByDescending) || methodCall.Method.Name == nameof(Enumerable.ThenByDescending)); } public string Handle(MethodCallExpression methodCall, bool descending) @@ -20,4 +20,4 @@ public string Handle(MethodCallExpression methodCall, bool descending) var direction = descending ? "desc" : "asc"; return $"sort by {key} {direction}"; } -} \ No newline at end of file +} diff --git a/CSharp.OpenSource.LinqToKql/Translator/Builders/SelectLinqToKQLTranslator.cs b/CSharp.OpenSource.LinqToKql/Translator/Builders/SelectLinqToKQLTranslator.cs index 8ef1f2f..5467af8 100644 --- a/CSharp.OpenSource.LinqToKql/Translator/Builders/SelectLinqToKQLTranslator.cs +++ b/CSharp.OpenSource.LinqToKql/Translator/Builders/SelectLinqToKQLTranslator.cs @@ -22,4 +22,4 @@ public override string Handle(MethodCallExpression methodCall, Expression? paren var action = _extendOps.Any(props.Contains) ? "extend" : "project"; return $"{action} {props}"; } -} \ No newline at end of file +} diff --git a/CSharp.OpenSource.LinqToKql/Translator/Builders/TaskLinqToKQLTranslator.cs b/CSharp.OpenSource.LinqToKql/Translator/Builders/TaskLinqToKQLTranslator.cs index 9c7594a..9756940 100644 --- a/CSharp.OpenSource.LinqToKql/Translator/Builders/TaskLinqToKQLTranslator.cs +++ b/CSharp.OpenSource.LinqToKql/Translator/Builders/TaskLinqToKQLTranslator.cs @@ -5,14 +5,18 @@ namespace CSharp.OpenSource.LinqToKql.Translator.Builders { public class TaskLinqToKQLTranslator : LinqToKQLTranslatorBase { - public TaskLinqToKQLTranslator(LinqToKQLQueryTranslatorConfig config) : base(config, new() { nameof(Enumerable.Take), }) + public TaskLinqToKQLTranslator(LinqToKQLQueryTranslatorConfig config) : base(config, new() { nameof(Enumerable.Take) }) { } public override string Handle(MethodCallExpression methodCall, Expression? parent) { var count = ((ConstantExpression)methodCall.Arguments[1]).Value.GetKQLValue(); - return $"take {count}"; + return methodCall.Method.Name switch + { + nameof(Enumerable.Take) => $"take {count}", + _ => throw new NotSupportedException($"{GetType().Name} - Method {methodCall.Method.Name} is not supported.") + }; } } -} \ No newline at end of file +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..66fb34a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.8' + +services: + kusto-cluster: + image: microsoft/azure-kusto-emulator:latest + environment: + - ACCEPT_EULA=Y + - CLUSTER_NAME=localKustoCluster + - NODE_NAME=node1 + - CLUSTER_TYPE=Dev + ports: + - "8080:8080" + - "8081:8081"