diff --git a/Directory.Packages.props b/Directory.Packages.props index 29e6149..ccecaf9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,31 +4,32 @@ - - - - - - - + + + + + - - - - + + + + + + + - \ No newline at end of file + diff --git a/Tests/Benchmarks/AltaSoft.Storm.Benchmarks.csproj b/Tests/Benchmarks/AltaSoft.Storm.Benchmarks.csproj index 098f730..af336b8 100644 --- a/Tests/Benchmarks/AltaSoft.Storm.Benchmarks.csproj +++ b/Tests/Benchmarks/AltaSoft.Storm.Benchmarks.csproj @@ -10,11 +10,11 @@ - - + + - + diff --git a/Tests/OrmTests/AltaSoft.Storm.Tests.csproj b/Tests/OrmTests/AltaSoft.Storm.Tests.csproj index 633cba7..c545727 100644 --- a/Tests/OrmTests/AltaSoft.Storm.Tests.csproj +++ b/Tests/OrmTests/AltaSoft.Storm.Tests.csproj @@ -14,10 +14,10 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Tests/OrmTests/DeleteTopIntegrationTests.cs b/Tests/OrmTests/DeleteTopIntegrationTests.cs new file mode 100644 index 0000000..446a43f --- /dev/null +++ b/Tests/OrmTests/DeleteTopIntegrationTests.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using AltaSoft.Storm.TestModels; +using FluentAssertions; +using Xunit; + +namespace AltaSoft.Storm.Tests; + +/// +/// End-to-end (real database) verification that DELETE ... Top(n) actually +/// limits the number of deleted rows per call, instead of deleting every +/// row that matches the WHERE clause. +/// +public class DeleteTopIntegrationTests : IClassFixture, IAsyncLifetime +{ + private const int BatchSize = 10; + private const int TotalUsers = 100; + private const int FirstUserId = 1000; + + private readonly TestStormContext _context; + + public DeleteTopIntegrationTests(DatabaseFixture fixture) + { + _context = new TestStormContext(fixture.ConnectionString); + } + + public async Task InitializeAsync() + { + // Inserted in small batches - a single 100-row batch with all of User's + // detail-table columns (Cars, Dates, lists, ...) exceeds SQL Server's + // 2100-parameter-per-request limit. + const int insertBatchSize = 10; + for (var batchStart = 0; batchStart < TotalUsers; batchStart += insertBatchSize) + { + var batch = new List(insertBatchSize); + for (var i = batchStart; i < batchStart + insertBatchSize; i++) + { + batch.Add(DatabaseHelper.NewUser(FirstUserId + i)); + } + + await _context.InsertIntoUsersTable().Values(batch).GoAsync(); + } + } + + [Fact] + public async Task DeleteWithTop_RemovesExactlyTopRowsPerCall_UntilNoneLeft() + { + // Sanity check: all 100 rows were inserted + var remainingBeforeDelete = await _context.SelectFromUsersTable() + .Where(x => x.UserId >= FirstUserId) + .ListAsync(); + remainingBeforeDelete.Should().HaveCount(TotalUsers); + + var iterations = 0; + var totalDeleted = 0; + + while (true) + { + var deletedCount = await _context.DeleteFromUsersTable() + .Where(x => x.UserId >= FirstUserId) + .Top(BatchSize) + .GoAsync(); + + if (deletedCount == 0) + break; + + // Every call must delete exactly BatchSize rows, never more. + deletedCount.Should().Be(BatchSize); + + iterations++; + totalDeleted += deletedCount; + } + + iterations.Should().Be(TotalUsers / BatchSize); + totalDeleted.Should().Be(TotalUsers); + + var remainingAfterDelete = await _context.SelectFromUsersTable() + .Where(x => x.UserId >= FirstUserId) + .ListAsync(); + remainingAfterDelete.Should().BeEmpty(); + } + + public async Task DisposeAsync() + { + await _context.DisposeAsync().ConfigureAwait(false); + } +} diff --git a/Tests/OrmTests/ODataFilterStatementGeneratorTests.cs b/Tests/OrmTests/ODataFilterStatementGeneratorTests.cs new file mode 100644 index 0000000..bb9d1e6 --- /dev/null +++ b/Tests/OrmTests/ODataFilterStatementGeneratorTests.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Text; +using System.Threading.Tasks; +using AltaSoft.Storm.Crud; +using AltaSoft.Storm.Extensions; +using AltaSoft.Storm.Helpers; +using AltaSoft.Storm.TestModels; +using FluentAssertions; +using Microsoft.Data.SqlClient; +using Xunit; +using Xunit.Abstractions; +using StormDbParameter = Microsoft.Data.SqlClient.SqlParameter; + +namespace AltaSoft.Storm.Tests; + +/// +/// Verifies that OData $filter expressions are translated to parameterized SQL correctly, +/// specifically for DateOnly and TimeOnly properties (Edm.Date / Edm.TimeOfDay). +/// +/// Mirrors SqlStatementGeneratorTests, but drives the OData filter generator +/// (ODataFilterStatementGenerator.GenerateSql) instead of LINQ where-expressions. +/// No database connection is opened - only the SQL-generation pipeline runs. +/// +public class ODataFilterStatementGeneratorTests : IClassFixture, IAsyncLifetime +{ + private readonly TestStormContext _context; + + public ODataFilterStatementGeneratorTests(DatabaseFixture fixture, ITestOutputHelper output) + { + var logger = new XunitLogger(output); + + StormManager.SetLogger(logger); + + _context = new TestStormContext(fixture.ConnectionString); + } + + public Task InitializeAsync() => Task.CompletedTask; + + public async Task DisposeAsync() => await _context.DisposeAsync().ConfigureAwait(false); + + private sealed class FakeCommand : IVirtualStormDbCommand + { + public readonly List<(string Name, UnifiedDbType DbType, int Size, object? Value)> Params = []; + public string CommandText { get; set; } = string.Empty; + public CommandType CommandType { get; set; } + + public string AddDbParameter(int paramIdx, StormColumnDef column, object? value) + { + var name = "@p" + paramIdx.ToString(System.Globalization.CultureInfo.InvariantCulture); + Params.Add((name, column.DbType, column.Size, value)); + return name; + } + + public StormDbParameter AddDbParameter(string paramName, UnifiedDbType dbType, int size, object? value) + { + Params.Add((paramName, dbType, size, value)); + return default!; + } + + public void AddDbParameters(List callParameters) { } + + public void SetStormCommandBaseParameters(SqlConnection connection, SqlTransaction? transaction, string sql, + QueryParameters queryParameters, CommandType commandType = CommandType.Text) => throw new NotImplementedException(); + + public void SetStormCommandBaseParameters(string sql, QueryParameters queryParameters, + CommandType commandType = CommandType.Text) => throw new NotImplementedException(); + + public void SetStormCommandBaseParameters(SqlConnection connection, SqlTransaction? transaction) => throw new NotImplementedException(); + + public string? GenerateCallParameters(List? queryParametersCallParameters, CallParameterType type) => null; + } + + private static (string sql, FakeCommand cmd) RunODataFilter(string filter, StormColumnDef[] columns) + { + var sb = new StringBuilder(); + var cmd = new FakeCommand(); + var idx = 0; + ODataFilterStatementGenerator.GenerateSql(cmd, filter, columns, null, ref idx, sb); + return (sb.ToString(), cmd); + } + + [Fact] + public void DateOnly_Equal_ShouldGenerateParameterAndSql() + { + var ctrl = StormControllerCache.Get(0); + var cols = ctrl.ColumnDefs; + + var (sql, cmd) = RunODataFilter("DateValue eq 2024-05-17", cols); + + sql.Should().Be("[DateValue] = @p0"); + cmd.Params.Should().HaveCount(1); + cmd.Params[0].DbType.Should().Be(UnifiedDbType.Date); + cmd.Params[0].Value.Should().Be(new DateOnly(2024, 5, 17)); + } + + [Fact] + public void DateOnly_GreaterThan_ShouldGenerateParameterAndSql() + { + var ctrl = StormControllerCache.Get(0); + var cols = ctrl.ColumnDefs; + + var (sql, cmd) = RunODataFilter("DateValue gt 2024-01-01", cols); + + sql.Should().Be("[DateValue] > @p0"); + cmd.Params.Should().HaveCount(1); + cmd.Params[0].Value.Should().Be(new DateOnly(2024, 1, 1)); + } + + [Fact] + public void NullableDateOnly_EqualNull_ShouldGenerateIsNull() + { + var ctrl = StormControllerCache.Get(0); + var cols = ctrl.ColumnDefs; + + var (sql, cmd) = RunODataFilter("DateValueN eq null", cols); + + sql.Should().Be("[DateValueN] IS NULL"); + cmd.Params.Should().BeEmpty(); + } + + [Fact] + public void NullableDateOnly_NotEqualNull_ShouldGenerateIsNotNull() + { + var ctrl = StormControllerCache.Get(0); + var cols = ctrl.ColumnDefs; + + var (sql, cmd) = RunODataFilter("DateValueN ne null", cols); + + sql.Should().Be("[DateValueN] IS NOT NULL"); + cmd.Params.Should().BeEmpty(); + } + + [Fact] + public void TimeOnly_Equal_ShouldGenerateParameterAndSql() + { + var ctrl = StormControllerCache.Get(0); + var cols = ctrl.ColumnDefs; + + var (sql, cmd) = RunODataFilter("TimeValue eq 13:45:30", cols); + + sql.Should().Be("[TimeValue] = @p0"); + cmd.Params.Should().HaveCount(1); + cmd.Params[0].DbType.Should().Be(UnifiedDbType.Time); + cmd.Params[0].Value.Should().Be(new TimeOnly(13, 45, 30)); + } + + [Fact] + public void TimeOnly_LessThan_ShouldGenerateParameterAndSql() + { + var ctrl = StormControllerCache.Get(0); + var cols = ctrl.ColumnDefs; + + var (sql, cmd) = RunODataFilter("TimeValue lt 08:30:00", cols); + + sql.Should().Be("[TimeValue] < @p0"); + cmd.Params.Should().HaveCount(1); + cmd.Params[0].Value.Should().Be(new TimeOnly(8, 30, 0)); + } + + [Fact] + public void NullableTimeOnly_EqualNull_ShouldGenerateIsNull() + { + var ctrl = StormControllerCache.Get(0); + var cols = ctrl.ColumnDefs; + + var (sql, cmd) = RunODataFilter("TimeValueN eq null", cols); + + sql.Should().Be("[TimeValueN] IS NULL"); + cmd.Params.Should().BeEmpty(); + } + + [Fact] + public void NullableTimeOnly_NotEqualNull_ShouldGenerateIsNotNull() + { + var ctrl = StormControllerCache.Get(0); + var cols = ctrl.ColumnDefs; + + var (sql, cmd) = RunODataFilter("TimeValueN ne null", cols); + + sql.Should().Be("[TimeValueN] IS NOT NULL"); + cmd.Params.Should().BeEmpty(); + } +} diff --git a/Tests/OrmTests/TopClauseSqlGenerationTests.cs b/Tests/OrmTests/TopClauseSqlGenerationTests.cs new file mode 100644 index 0000000..0a115e9 --- /dev/null +++ b/Tests/OrmTests/TopClauseSqlGenerationTests.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using AltaSoft.Storm.Crud; +using AltaSoft.Storm.TestModels; +using FluentAssertions; +using Microsoft.Data.SqlClient; +using Xunit; + +namespace AltaSoft.Storm.Tests; + +/// +/// Verifies that Top(n) on DELETE / UPDATE actually writes a TOP (n) clause +/// into the generated SQL string, instead of being silently ignored. +/// +/// Uses GenerateBatchCommands() instead of GoAsync() — the full SQL-generation +/// pipeline runs without opening a database connection, so the CommandText of +/// every SqlBatchCommand can be inspected directly. +/// IClassFixture<DatabaseFixture> is kept only to guarantee StormManager is +/// initialized before the first test runs. +/// +public class TopClauseSqlGenerationTests : IClassFixture +{ + private readonly TestStormContext _context; + + public TopClauseSqlGenerationTests(DatabaseFixture fixture) + { + _context = new TestStormContext(fixture.ConnectionString); + } + + private static string Sql(ISqlGo command) + { + var batch = new List(); + command.GenerateBatchCommands(batch); + batch.Should().NotBeEmpty("GenerateBatchCommands must produce at least one command"); + return batch[0].CommandText; + } + + [Fact] + public void Delete_Where_WithTop_SqlContainsTopClause() + { + var sql = Sql(_context.DeleteFromUsersTable() + .Where(x => x.UserId == 1) + .Top(100)); + + sql.Should().Contain("TOP (100)", "Top(100) on a DELETE must be reflected in the generated SQL"); + } + + [Fact] + public void Delete_Where_WithTop_TopClauseAppearsRightAfterDeleteFrom() + { + var sql = Sql(_context.DeleteFromUsersTable() + .Where(x => x.UserId == 1) + .Top(100)); + + var deletePos = sql.IndexOf("DELETE FROM", System.StringComparison.OrdinalIgnoreCase); + var topPos = sql.IndexOf("TOP (100)", System.StringComparison.OrdinalIgnoreCase); + + deletePos.Should().BeGreaterThanOrEqualTo(0); + topPos.Should().BeGreaterThan(deletePos, "TOP clause must come after DELETE FROM"); + } + + [Fact] + public void Update_SetInstruction_WithTop_SqlContainsTopClause() + { + var sql = Sql(_context.UpdateUsersTable() + .Set(x => x.FullName, "test") + .Top(100)); + + sql.Should().Contain("TOP (100)", "Top(100) on an UPDATE must be reflected in the generated SQL"); + } +} diff --git a/Tests/TestModels/AltaSoft.Storm.TestModels.csproj b/Tests/TestModels/AltaSoft.Storm.TestModels.csproj index 4b37aee..4453544 100644 --- a/Tests/TestModels/AltaSoft.Storm.TestModels.csproj +++ b/Tests/TestModels/AltaSoft.Storm.TestModels.csproj @@ -25,11 +25,11 @@ - - + + compile; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CurrencyId.g.cs b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CurrencyId.g.cs index 42cb6f5..8559801 100644 --- a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CurrencyId.g.cs +++ b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CurrencyId.g.cs @@ -7,6 +7,14 @@ #nullable enable +#pragma warning disable CS8774 // Member must have a non-null value when exiting. +#pragma warning disable CS0628 // New protected member declared in sealed type +#pragma warning disable CS0612 // Type or member is obsolete +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable IDE0290 // Use primary constructor + using System; using System.Numerics; using System.Diagnostics; @@ -67,7 +75,7 @@ public CurrencyId() #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. /// - /// Tries to create an instance of AsciiString from the specified value. + /// Tries to create an instance of CurrencyId from the specified value. /// /// The value to create CurrencyId from /// When this method returns, contains the created CurrencyId if the conversion succeeded, or null if the conversion failed. @@ -78,13 +86,13 @@ public static bool TryCreate(string value, [NotNullWhen(true)] out CurrencyId? r } /// - /// Tries to create an instance of AsciiString from the specified value. + /// Tries to create an instance of CurrencyId from the specified value. /// /// The value to create CurrencyId from /// When this method returns, contains the created CurrencyId if the conversion succeeded, or null if the conversion failed. /// When this method returns, contains the error message if the conversion failed; otherwise, null. /// true if the conversion succeeded; otherwise, false. - public static bool TryCreate(string value, [NotNullWhen(true)] out CurrencyId? result, [NotNullWhen(false)] out string? errorMessage) + public static bool TryCreate(string value, [NotNullWhen(true)] out CurrencyId? result, [NotNullWhen(false)] out string? errorMessage) { var validationResult = Validate(value); if (!validationResult.IsValid) diff --git a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CurrencyIdJsonConverter.g.cs b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CurrencyIdJsonConverter.g.cs index 56748f9..45b07e8 100644 --- a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CurrencyIdJsonConverter.g.cs +++ b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CurrencyIdJsonConverter.g.cs @@ -7,6 +7,14 @@ #nullable enable +#pragma warning disable CS8774 // Member must have a non-null value when exiting. +#pragma warning disable CS0628 // New protected member declared in sealed type +#pragma warning disable CS0612 // Type or member is obsolete +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable IDE0290 // Use primary constructor + using AltaSoft.Storm.TestModels.VeryBadNamespace; using System; using System.Text.Json; diff --git a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CurrencyIdTypeConverter.g.cs b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CurrencyIdTypeConverter.g.cs index 433bad7..4b11715 100644 --- a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CurrencyIdTypeConverter.g.cs +++ b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CurrencyIdTypeConverter.g.cs @@ -7,6 +7,14 @@ #nullable enable +#pragma warning disable CS8774 // Member must have a non-null value when exiting. +#pragma warning disable CS0628 // New protected member declared in sealed type +#pragma warning disable CS0612 // Type or member is obsolete +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable IDE0290 // Use primary constructor + using AltaSoft.Storm.TestModels.VeryBadNamespace; using System; using System.ComponentModel; diff --git a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CustomerId.g.cs b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CustomerId.g.cs index c33f27b..a8a8878 100644 --- a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CustomerId.g.cs +++ b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CustomerId.g.cs @@ -7,6 +7,14 @@ #nullable enable +#pragma warning disable CS8774 // Member must have a non-null value when exiting. +#pragma warning disable CS0628 // New protected member declared in sealed type +#pragma warning disable CS0612 // Type or member is obsolete +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable IDE0290 // Use primary constructor + using System; using System.Numerics; using System.Diagnostics; @@ -74,7 +82,7 @@ public CustomerId() } /// - /// Tries to create an instance of AsciiString from the specified value. + /// Tries to create an instance of CustomerId from the specified value. /// /// The value to create CustomerId from /// When this method returns, contains the created CustomerId if the conversion succeeded, or null if the conversion failed. @@ -85,13 +93,13 @@ public static bool TryCreate(long value, [NotNullWhen(true)] out CustomerId? res } /// - /// Tries to create an instance of AsciiString from the specified value. + /// Tries to create an instance of CustomerId from the specified value. /// /// The value to create CustomerId from /// When this method returns, contains the created CustomerId if the conversion succeeded, or null if the conversion failed. /// When this method returns, contains the error message if the conversion failed; otherwise, null. /// true if the conversion succeeded; otherwise, false. - public static bool TryCreate(long value, [NotNullWhen(true)] out CustomerId? result, [NotNullWhen(false)] out string? errorMessage) + public static bool TryCreate(long value, [NotNullWhen(true)] out CustomerId? result, [NotNullWhen(false)] out string? errorMessage) { var validationResult = Validate(value); if (!validationResult.IsValid) diff --git a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CustomerIdJsonConverter.g.cs b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CustomerIdJsonConverter.g.cs index e22166e..59ed8df 100644 --- a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CustomerIdJsonConverter.g.cs +++ b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CustomerIdJsonConverter.g.cs @@ -7,6 +7,14 @@ #nullable enable +#pragma warning disable CS8774 // Member must have a non-null value when exiting. +#pragma warning disable CS0628 // New protected member declared in sealed type +#pragma warning disable CS0612 // Type or member is obsolete +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable IDE0290 // Use primary constructor + using AltaSoft.Storm.TestModels.VeryBadNamespace; using System; using System.Text.Json; diff --git a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CustomerIdTypeConverter.g.cs b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CustomerIdTypeConverter.g.cs index 66937e9..9526e47 100644 --- a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CustomerIdTypeConverter.g.cs +++ b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/CustomerIdTypeConverter.g.cs @@ -7,6 +7,14 @@ #nullable enable +#pragma warning disable CS8774 // Member must have a non-null value when exiting. +#pragma warning disable CS0628 // New protected member declared in sealed type +#pragma warning disable CS0612 // Type or member is obsolete +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable IDE0290 // Use primary constructor + using AltaSoft.Storm.TestModels.VeryBadNamespace; using System; using System.ComponentModel; diff --git a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/OpenApiHelper.g.cs b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/OpenApiHelper.g.cs index 3af0375..ea206b1 100644 --- a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/OpenApiHelper.g.cs +++ b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/OpenApiHelper.g.cs @@ -7,6 +7,14 @@ #nullable enable +#pragma warning disable CS8774 // Member must have a non-null value when exiting. +#pragma warning disable CS0628 // New protected member declared in sealed type +#pragma warning disable CS0612 // Type or member is obsolete +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable IDE0290 // Use primary constructor + using AltaSoft.Storm.TestModels.VeryBadNamespace; using AltaSoft.Storm.TestModels.DomainTypes; using Microsoft.OpenApi; diff --git a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/UserId.g.cs b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/UserId.g.cs index c47e57a..6befb17 100644 --- a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/UserId.g.cs +++ b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/UserId.g.cs @@ -7,6 +7,14 @@ #nullable enable +#pragma warning disable CS8774 // Member must have a non-null value when exiting. +#pragma warning disable CS0628 // New protected member declared in sealed type +#pragma warning disable CS0612 // Type or member is obsolete +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable IDE0290 // Use primary constructor + using System; using System.Numerics; using System.Diagnostics; @@ -74,7 +82,7 @@ public UserId() } /// - /// Tries to create an instance of AsciiString from the specified value. + /// Tries to create an instance of UserId from the specified value. /// /// The value to create UserId from /// When this method returns, contains the created UserId if the conversion succeeded, or null if the conversion failed. @@ -85,13 +93,13 @@ public static bool TryCreate(int value, [NotNullWhen(true)] out UserId? result) } /// - /// Tries to create an instance of AsciiString from the specified value. + /// Tries to create an instance of UserId from the specified value. /// /// The value to create UserId from /// When this method returns, contains the created UserId if the conversion succeeded, or null if the conversion failed. /// When this method returns, contains the error message if the conversion failed; otherwise, null. /// true if the conversion succeeded; otherwise, false. - public static bool TryCreate(int value, [NotNullWhen(true)] out UserId? result, [NotNullWhen(false)] out string? errorMessage) + public static bool TryCreate(int value, [NotNullWhen(true)] out UserId? result, [NotNullWhen(false)] out string? errorMessage) { var validationResult = Validate(value); if (!validationResult.IsValid) diff --git a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/UserIdJsonConverter.g.cs b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/UserIdJsonConverter.g.cs index 63d0a23..cf29d25 100644 --- a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/UserIdJsonConverter.g.cs +++ b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/UserIdJsonConverter.g.cs @@ -7,6 +7,14 @@ #nullable enable +#pragma warning disable CS8774 // Member must have a non-null value when exiting. +#pragma warning disable CS0628 // New protected member declared in sealed type +#pragma warning disable CS0612 // Type or member is obsolete +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable IDE0290 // Use primary constructor + using AltaSoft.Storm.TestModels.DomainTypes; using System; using System.Text.Json; diff --git a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/UserIdTypeConverter.g.cs b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/UserIdTypeConverter.g.cs index 244f9be..d3cc130 100644 --- a/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/UserIdTypeConverter.g.cs +++ b/Tests/TestModels/Generated/AltaSoft.DomainPrimitives.Generator/AltaSoft.DomainPrimitives.Generator.DomainPrimitiveGenerator/UserIdTypeConverter.g.cs @@ -7,6 +7,14 @@ #nullable enable +#pragma warning disable CS8774 // Member must have a non-null value when exiting. +#pragma warning disable CS0628 // New protected member declared in sealed type +#pragma warning disable CS0612 // Type or member is obsolete +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable IDE0290 // Use primary constructor + using AltaSoft.Storm.TestModels.DomainTypes; using System; using System.ComponentModel; diff --git a/Tests/TestModels/Generated/AltaSoft.Storm.Generator/AltaSoft.Storm.Generator.StormGenerator/SqlWhereTestEntity.g.cs b/Tests/TestModels/Generated/AltaSoft.Storm.Generator/AltaSoft.Storm.Generator.StormGenerator/SqlWhereTestEntity.g.cs index e627bdb..bebc5b1 100644 --- a/Tests/TestModels/Generated/AltaSoft.Storm.Generator/AltaSoft.Storm.Generator.StormGenerator/SqlWhereTestEntity.g.cs +++ b/Tests/TestModels/Generated/AltaSoft.Storm.Generator/AltaSoft.Storm.Generator.StormGenerator/SqlWhereTestEntity.g.cs @@ -10,9 +10,9 @@ #nullable enable +using System; using AltaSoft.Storm.Attributes; using AltaSoft.Storm.TestModels.VeryBadNamespace; -using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; @@ -66,6 +66,10 @@ protected internal SqlWhereTestEntity(StormDbDataReader dr, uint partialLoadFlag CcyN = (AltaSoft.Storm.TestModels.VeryBadNamespace.CurrencyId?)(string?)dr.GetStringOrNull(idx++); BoolValue = (bool)dr.GetBoolean(idx++); BoolValueN = (bool?)dr.GetBooleanOrNull(idx++); + DateValue = (System.DateOnly)dr.GetDate(idx++); + DateValueN = (System.DateOnly?)dr.GetDateOrNull(idx++); + TimeValue = (System.TimeOnly)dr.GetTime(idx++); + TimeValueN = (System.TimeOnly?)dr.GetTimeOrNull(idx++); } @@ -119,7 +123,15 @@ public enum OrderBy BoolValue = 12, BoolValue_Desc = -12, BoolValueN = 13, - BoolValueN_Desc = -13 + BoolValueN_Desc = -13, + DateValue = 14, + DateValue_Desc = -14, + DateValueN = 15, + DateValueN_Desc = -15, + TimeValue = 16, + TimeValue_Desc = -16, + TimeValueN = 17, + TimeValueN_Desc = -17 } /// @@ -151,7 +163,11 @@ public enum OrderBy (columnDefs[9], (string)Ccy), (columnDefs[10], (string?)CcyN), (columnDefs[11], BoolValue), - (columnDefs[12], BoolValueN) + (columnDefs[12], BoolValueN), + (columnDefs[13], (DateOnly)DateValue), + (columnDefs[14], (DateOnly?)DateValueN), + (columnDefs[15], (TimeOnly)TimeValue), + (columnDefs[16], (TimeOnly?)TimeValueN) ]; } @@ -188,6 +204,10 @@ public virtual (string propertyName, IChangeTrackable? value)[] __TrackableMembe private void __PropertySet_CcyN(ref AltaSoft.Storm.TestModels.VeryBadNamespace.CurrencyId? newValue, ref AltaSoft.Storm.TestModels.VeryBadNamespace.CurrencyId? oldValue) { if (_isChangeTrackingActive && oldValue != newValue) _changeTrackingStateMachine!.PropertyChanged(nameof(CcyN), newValue); } private void __PropertySet_BoolValue(ref bool newValue, ref bool oldValue) { if (_isChangeTrackingActive && oldValue != newValue) _changeTrackingStateMachine!.PropertyChanged(nameof(BoolValue), newValue); } private void __PropertySet_BoolValueN(ref bool? newValue, ref bool? oldValue) { if (_isChangeTrackingActive && oldValue != newValue) _changeTrackingStateMachine!.PropertyChanged(nameof(BoolValueN), newValue); } + private void __PropertySet_DateValue(ref System.DateOnly newValue, ref System.DateOnly oldValue) { if (_isChangeTrackingActive && oldValue != newValue) _changeTrackingStateMachine!.PropertyChanged(nameof(DateValue), newValue); } + private void __PropertySet_DateValueN(ref System.DateOnly? newValue, ref System.DateOnly? oldValue) { if (_isChangeTrackingActive && oldValue != newValue) _changeTrackingStateMachine!.PropertyChanged(nameof(DateValueN), newValue); } + private void __PropertySet_TimeValue(ref System.TimeOnly newValue, ref System.TimeOnly oldValue) { if (_isChangeTrackingActive && oldValue != newValue) _changeTrackingStateMachine!.PropertyChanged(nameof(TimeValue), newValue); } + private void __PropertySet_TimeValueN(ref System.TimeOnly? newValue, ref System.TimeOnly? oldValue) { if (_isChangeTrackingActive && oldValue != newValue) _changeTrackingStateMachine!.PropertyChanged(nameof(TimeValueN), newValue); } #endregion Change Tracking Support } @@ -237,6 +257,10 @@ public override IDataBindable Create(StormDbDataReader dr, uint partialLoadFlags nameof(SqlWhereTestEntity.CcyN) => (AltaSoft.Storm.TestModels.VeryBadNamespace.CurrencyId?)(string?)dr.GetStringOrNull(idx++), nameof(SqlWhereTestEntity.BoolValue) => (bool)dr.GetBoolean(idx++), nameof(SqlWhereTestEntity.BoolValueN) => (bool?)dr.GetBooleanOrNull(idx++), + nameof(SqlWhereTestEntity.DateValue) => (System.DateOnly)dr.GetDate(idx++), + nameof(SqlWhereTestEntity.DateValueN) => (System.DateOnly?)dr.GetDateOrNull(idx++), + nameof(SqlWhereTestEntity.TimeValue) => (System.TimeOnly)dr.GetTime(idx++), + nameof(SqlWhereTestEntity.TimeValueN) => (System.TimeOnly?)dr.GetTimeOrNull(idx++), _ => throw new StormException($"'{propertyName}' is not a column of the table") }; } @@ -264,7 +288,11 @@ public override object CreateDetailRow(StormColumnDef column, StormDbDataReader new(nameof(SqlWhereTestEntity.Ccy), null, "Ccy", StormColumnFlags.CanSelect | StormColumnFlags.CanInsert | StormColumnFlags.CanUpdate, UnifiedDbType.String, -1, 0, 0, SaveAs.Default, 0, false, null, null, typeof(AltaSoft.Storm.TestModels.VeryBadNamespace.CurrencyId), null), new(nameof(SqlWhereTestEntity.CcyN), null, "CcyN", StormColumnFlags.CanSelect | StormColumnFlags.CanInsert | StormColumnFlags.CanUpdate, UnifiedDbType.String, -1, 0, 0, SaveAs.Default, 0, true, null, null, typeof(AltaSoft.Storm.TestModels.VeryBadNamespace.CurrencyId), null), new(nameof(SqlWhereTestEntity.BoolValue), null, "BoolValue", StormColumnFlags.CanSelect | StormColumnFlags.CanInsert | StormColumnFlags.CanUpdate, UnifiedDbType.Boolean, 0, 0, 0, SaveAs.Default, 0, false, null, null, typeof(bool), null), - new(nameof(SqlWhereTestEntity.BoolValueN), null, "BoolValueN", StormColumnFlags.CanSelect | StormColumnFlags.CanInsert | StormColumnFlags.CanUpdate, UnifiedDbType.Boolean, 0, 0, 0, SaveAs.Default, 0, true, null, null, typeof(bool), null) + new(nameof(SqlWhereTestEntity.BoolValueN), null, "BoolValueN", StormColumnFlags.CanSelect | StormColumnFlags.CanInsert | StormColumnFlags.CanUpdate, UnifiedDbType.Boolean, 0, 0, 0, SaveAs.Default, 0, true, null, null, typeof(bool), null), + new(nameof(SqlWhereTestEntity.DateValue), null, "DateValue", StormColumnFlags.CanSelect | StormColumnFlags.CanInsert | StormColumnFlags.CanUpdate, UnifiedDbType.Date, 0, 0, 0, SaveAs.Default, 0, false, null, null, typeof(System.DateOnly), null), + new(nameof(SqlWhereTestEntity.DateValueN), null, "DateValueN", StormColumnFlags.CanSelect | StormColumnFlags.CanInsert | StormColumnFlags.CanUpdate, UnifiedDbType.Date, 0, 0, 0, SaveAs.Default, 0, true, null, null, typeof(System.DateOnly), null), + new(nameof(SqlWhereTestEntity.TimeValue), null, "TimeValue", StormColumnFlags.CanSelect | StormColumnFlags.CanInsert | StormColumnFlags.CanUpdate, UnifiedDbType.Time, 0, 0, 0, SaveAs.Default, 0, false, null, null, typeof(System.TimeOnly), null), + new(nameof(SqlWhereTestEntity.TimeValueN), null, "TimeValueN", StormColumnFlags.CanSelect | StormColumnFlags.CanInsert | StormColumnFlags.CanUpdate, UnifiedDbType.Time, 0, 0, 0, SaveAs.Default, 0, true, null, null, typeof(System.TimeOnly), null) }; /// diff --git a/Tests/TestModels/SqlWhereTestEntity.cs b/Tests/TestModels/SqlWhereTestEntity.cs index 856ba12..9e64476 100644 --- a/Tests/TestModels/SqlWhereTestEntity.cs +++ b/Tests/TestModels/SqlWhereTestEntity.cs @@ -1,4 +1,5 @@ -using AltaSoft.Storm.Attributes; +using System; +using AltaSoft.Storm.Attributes; using AltaSoft.Storm.TestModels.VeryBadNamespace; namespace AltaSoft.Storm.TestModels; @@ -32,5 +33,13 @@ public partial record SqlWhereTestEntity public bool BoolValue { get; set; } public bool? BoolValueN { get; set; } + + public DateOnly DateValue { get; set; } + + public DateOnly? DateValueN { get; set; } + + public TimeOnly TimeValue { get; set; } + + public TimeOnly? TimeValueN { get; set; } } diff --git a/Tests/TrackingListTests/AltaSoft.Storm.TrackingListTests.csproj b/Tests/TrackingListTests/AltaSoft.Storm.TrackingListTests.csproj index 3e40239..3a1e242 100644 --- a/Tests/TrackingListTests/AltaSoft.Storm.TrackingListTests.csproj +++ b/Tests/TrackingListTests/AltaSoft.Storm.TrackingListTests.csproj @@ -19,9 +19,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/AltaSoft.Storm/Helpers/ODataWhereStatementGenerator.cs b/src/AltaSoft.Storm/Helpers/ODataWhereStatementGenerator.cs index bae873b..6535c5d 100644 --- a/src/AltaSoft.Storm/Helpers/ODataWhereStatementGenerator.cs +++ b/src/AltaSoft.Storm/Helpers/ODataWhereStatementGenerator.cs @@ -183,6 +183,13 @@ public override QueryNode Visit(ConstantNode nodeIn) } else { +#if NET10_0_OR_GREATER + value = value switch + { + DateTimeOffset { DateTime: var dt } when dbType is UnifiedDbType.DateTime or UnifiedDbType.DateTime2 or UnifiedDbType.SmallDateTime => dt, + _ => value + }; +#else value = value switch { //Microsoft.OData.Edm.Date @@ -192,6 +199,7 @@ public override QueryNode Visit(ConstantNode nodeIn) DateTimeOffset { DateTime: var dt } when dbType is UnifiedDbType.DateTime or UnifiedDbType.DateTime2 or UnifiedDbType.SmallDateTime => dt, _ => value }; +#endif } var paramName = ParamPrefix + _paramIndex++.ToString(CultureInfo.InvariantCulture); diff --git a/src/AltaSoft.Storm/StormControllerBase.cs b/src/AltaSoft.Storm/StormControllerBase.cs index 5fa2c10..3590921 100644 --- a/src/AltaSoft.Storm/StormControllerBase.cs +++ b/src/AltaSoft.Storm/StormControllerBase.cs @@ -251,6 +251,19 @@ private static void AppendTableHints(StormTableHints hints, StringBuilder sb) sb.Append(')'); } + /// + /// Appends a TOP (n) clause to a StringBuilder, if is set. + /// + /// The number of rows to limit the statement to, or null for no limit. + /// The StringBuilder to which the clause will be appended. + private static void AppendTopClause(int? topRows, StringBuilder sb) + { + if (topRows is not { } rows) + return; + + sb.Append("TOP (").Append(rows).Append(") "); + } + /// /// Returns the corresponding SQL hint string for the given StormTableHints enum value. /// diff --git a/src/AltaSoft.Storm/StormControllerBase.delete.cs b/src/AltaSoft.Storm/StormControllerBase.delete.cs index 6076997..843b3bc 100644 --- a/src/AltaSoft.Storm/StormControllerBase.delete.cs +++ b/src/AltaSoft.Storm/StormControllerBase.delete.cs @@ -77,7 +77,7 @@ internal void PrepareDelete( { var sb = StormManager.GetStringBuilderFromPool(); - GenerateDeleteSql(command, queryParameters, queryParameters.TableHints, sb); + GenerateDeleteSql(command, queryParameters, queryParameters.TableHints, queryParameters.TopRows, sb); command.SetStormCommandBaseParameters(sb.ToStringAndReturnToPool(), queryParameters); } @@ -123,7 +123,7 @@ internal void PrepareDelete( command.SetStormCommandBaseParameters(sb.ToStringAndReturnToPool(), queryParameters); } - private void GenerateDeleteSql(IVirtualStormDbCommand command, IKeyAndWhereExpression queryParameters, StormTableHints tableHints, StringBuilder sb) where T : IDataBindable + private void GenerateDeleteSql(IVirtualStormDbCommand command, IKeyAndWhereExpression queryParameters, StormTableHints tableHints, int? topRows, StringBuilder sb) where T : IDataBindable { var paramIndex = 1; var (startIndex, len) = (-1, 0); @@ -141,7 +141,9 @@ private void GenerateDeleteSql(IVirtualStormDbCommand command, IKeyAndWhereEx } sb.AppendLine("SET NOCOUNT OFF;"); - sb.Append("DELETE FROM ").Append(QuotedObjectFullName); + sb.Append("DELETE "); + AppendTopClause(topRows, sb); + sb.Append("FROM ").Append(QuotedObjectFullName); AppendTableHints(tableHints, sb); if (startIndex < 0) diff --git a/src/AltaSoft.Storm/StormControllerBase.update.cs b/src/AltaSoft.Storm/StormControllerBase.update.cs index bb0d2b1..59f0816 100644 --- a/src/AltaSoft.Storm/StormControllerBase.update.cs +++ b/src/AltaSoft.Storm/StormControllerBase.update.cs @@ -102,7 +102,9 @@ internal void PrepareUpdate( var paramIndex = 1; var sb = StormManager.GetStringBuilderFromPool(); - sb.Append("UPDATE ").Append(QuotedObjectFullName); + sb.Append("UPDATE "); + AppendTopClause(queryParameters.TopRows, sb); + sb.Append(QuotedObjectFullName); AppendTableHints(queryParameters.TableHints, sb); sb.AppendLine(); sb.Append("SET "); diff --git a/src/SharedAssemblyInfo.props b/src/SharedAssemblyInfo.props index 9907c84..dba2e50 100644 --- a/src/SharedAssemblyInfo.props +++ b/src/SharedAssemblyInfo.props @@ -7,7 +7,7 @@ Gregory Nikolaishvili, Teimuraz Nikolaishvili ALTA Software llc. Copyright © 2024-2026 ALTA Software llc. - 3.4.3 + 3.5.0