using System.Data.Common; using System.Reflection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Omika.Core.Compatibility; using Omika.Core.Contracts.Common; using Omika.Core.Contracts.Planning; using Omika.Core.Domain.Commands; using Omika.Core.Domain.Entities.Planning; using Omika.Core.Interfaces; using Omika.Core.Runtime; using Omika.Core.Services; using Omika.Data; using Omika.Data.Configuration; using Omika.Data.Connection; using Omika.Data.Repositories; using Omika.Data.Repositories.Batik; using Omika.Data.Repositories.Omika; namespace Omika.Tests.Repositories; /// /// Tests unitaires de : délégation, /// comportement Unknown, mapping Batik/Omika, délégation du service, et DI. /// [Collection("ApplicationRuntimeContext")] public class AdaptiveSuiviTempsRepositoryTests : IDisposable { public AdaptiveSuiviTempsRepositoryTests() { ApplicationRuntimeContext.Reset(); } public void Dispose() { ApplicationRuntimeContext.Reset(); } // ========================================================================= // DELEGATION SEARCH // ========================================================================= [Fact] public async Task SearchAsync_BatikDetected_DelegatesToBatikRepository() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik); var batikConnection = new SpyingHfsqlConnection(); var omikaConnection = new SpyingHfsqlConnection(); batikConnection.SuiviTempsRows = [BatikSuiviTempsDict(1, "Réunion", "INT-001")]; var batikRepo = new BatikSuiviTempsRepository(batikConnection); var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection); var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo); await adaptive.SearchAsync(new SuiviTempsSearchCriteria()); Assert.True(batikConnection.QueryCount > 0, "Batik aurait dû exécuter des requêtes."); Assert.Equal(0, omikaConnection.QueryCount); } [Fact] public async Task SearchAsync_OmikaDetected_DelegatesToOmikaRepository() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika); var batikConnection = new SpyingHfsqlConnection(); var omikaConnection = new SpyingHfsqlConnection(); omikaConnection.SuiviTempsRows = [OmikaSuiviTempsDict(1, "Réunion", "INT-001")]; var batikRepo = new BatikSuiviTempsRepository(batikConnection); var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection); var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo); await adaptive.SearchAsync(new SuiviTempsSearchCriteria()); Assert.Equal(0, batikConnection.QueryCount); Assert.True(omikaConnection.QueryCount > 0, "Omika aurait dû exécuter des requêtes."); } [Fact] public async Task SearchAsync_UnknownDetected_ThrowsInvalidOperationException() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); var batikConnection = new SpyingHfsqlConnection(); var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikSuiviTempsRepository(batikConnection); var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection); var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo); var ex = await Assert.ThrowsAsync( () => adaptive.SearchAsync(new SuiviTempsSearchCriteria())); Assert.Contains("not been detected", ex.Message); Assert.Equal(0, batikConnection.QueryCount); Assert.Equal(0, omikaConnection.QueryCount); } [Fact] public async Task SearchAsync_EmptyCriteria_DoesNotCrash() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik); var batikConnection = new SpyingHfsqlConnection(); batikConnection.SuiviTempsRows = []; var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikSuiviTempsRepository(batikConnection); var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection); var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo); PagedResult result = await adaptive.SearchAsync(new SuiviTempsSearchCriteria()); Assert.NotNull(result); Assert.Empty(result.Items); } // ========================================================================= // MAPPING BATIK // ========================================================================= [Fact] public async Task SearchAsync_BatikMapping_ProducesCanonicalSuiviTemps() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik); var batikConnection = new SpyingHfsqlConnection(); batikConnection.SuiviTempsRows = [BatikSuiviTempsDict(1, "Réunion", "INT-001")]; var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikSuiviTempsRepository(batikConnection); var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection); var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo); PagedResult result = await adaptive.SearchAsync(new SuiviTempsSearchCriteria()); Assert.Single(result.Items); Assert.Equal("Réunion", result.Items[0].Description); } // ========================================================================= // MAPPING OMIKA // ========================================================================= [Fact] public async Task SearchAsync_OmikaMapping_ProducesCanonicalSuiviTemps() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika); var batikConnection = new SpyingHfsqlConnection(); var omikaConnection = new SpyingHfsqlConnection(); omikaConnection.SuiviTempsRows = [OmikaSuiviTempsDict(1, "Réunion", "INT-001")]; var batikRepo = new BatikSuiviTempsRepository(batikConnection); var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection); var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo); PagedResult result = await adaptive.SearchAsync(new SuiviTempsSearchCriteria()); Assert.Single(result.Items); Assert.Equal("Réunion", result.Items[0].Description); } // ========================================================================= // DELEGATION GET BY ID // ========================================================================= [Fact] public async Task GetByIdAsync_BatikDetected_DelegatesToBatik() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik); var batikConnection = new SpyingHfsqlConnection(); batikConnection.SuiviTempsRows = [BatikSuiviTempsDict(1, "Réunion", "INT-001")]; var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikSuiviTempsRepository(batikConnection); var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection); var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo); SuiviTemps? result = await adaptive.GetByIdAsync(Guid.NewGuid().ToString("D")); Assert.NotNull(result); Assert.True(batikConnection.QueryCount > 0, "Batik aurait dû exécuter des requêtes."); Assert.Equal(0, omikaConnection.QueryCount); } [Fact] public async Task GetByIdAsync_OmikaDetected_DelegatesToOmika() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika); var batikConnection = new SpyingHfsqlConnection(); var omikaConnection = new SpyingHfsqlConnection(); omikaConnection.SuiviTempsRows = [OmikaSuiviTempsDict(1, "Réunion", "INT-001")]; var batikRepo = new BatikSuiviTempsRepository(batikConnection); var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection); var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo); SuiviTemps? result = await adaptive.GetByIdAsync(Guid.NewGuid().ToString("D")); Assert.NotNull(result); Assert.Equal(0, batikConnection.QueryCount); Assert.True(omikaConnection.QueryCount > 0, "Omika aurait dû exécuter des requêtes."); } [Fact] public async Task GetByIdAsync_UnknownDetected_ThrowsInvalidOperationException() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); var batikConnection = new SpyingHfsqlConnection(); var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikSuiviTempsRepository(batikConnection); var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection); var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo); var ex = await Assert.ThrowsAsync( () => adaptive.GetByIdAsync(Guid.NewGuid().ToString("D"))); Assert.Contains("not been detected", ex.Message); Assert.Equal(0, batikConnection.QueryCount); Assert.Equal(0, omikaConnection.QueryCount); } // ========================================================================= // DELEGATION CREATE // ========================================================================= [Fact] public async Task CreateAsync_BatikDetected_DelegatesToBatik() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Batik); var batikConnection = new SpyingHfsqlConnection(); batikConnection.SuiviTempsRows = [BatikSuiviTempsDict(1, "Réunion", "INT-001")]; var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikSuiviTempsRepository(batikConnection); var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection); var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo); SuiviTemps result = await adaptive.CreateAsync( new SuiviTempsCreateCommand { Date = DateTime.Now, DureeMinutes = 60 }, CancellationToken.None); Assert.NotNull(result); Assert.True(batikConnection.QueryCount > 0, "Batik aurait dû exécuter des requêtes."); Assert.Equal(0, omikaConnection.QueryCount); } [Fact] public async Task CreateAsync_OmikaDetected_DelegatesToOmika() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); ApplicationRuntimeContext.SetDetectedVersion(DatabaseSchemaVersion.Omika); var batikConnection = new SpyingHfsqlConnection(); var omikaConnection = new SpyingHfsqlConnection(); omikaConnection.SuiviTempsRows = [OmikaSuiviTempsDict(1, "Réunion", "INT-001")]; var batikRepo = new BatikSuiviTempsRepository(batikConnection); var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection); var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo); SuiviTemps result = await adaptive.CreateAsync( new SuiviTempsCreateCommand { Date = DateTime.Now, DureeMinutes = 60 }, CancellationToken.None); Assert.NotNull(result); Assert.Equal(0, batikConnection.QueryCount); Assert.True(omikaConnection.QueryCount > 0, "Omika aurait dû exécuter des requêtes."); } [Fact] public async Task CreateAsync_UnknownDetected_ThrowsInvalidOperationException() { ApplicationRuntimeContext.Configure(DatabaseSchemaSelection.Auto); var batikConnection = new SpyingHfsqlConnection(); var omikaConnection = new SpyingHfsqlConnection(); var batikRepo = new BatikSuiviTempsRepository(batikConnection); var omikaRepo = new OmikaSuiviTempsRepository(omikaConnection); var adaptive = new AdaptiveSuiviTempsRepository(batikRepo, omikaRepo); var ex = await Assert.ThrowsAsync( () => adaptive.CreateAsync( new SuiviTempsCreateCommand { Date = DateTime.Now, DureeMinutes = 60 })); Assert.Contains("not been detected", ex.Message); Assert.Equal(0, batikConnection.QueryCount); Assert.Equal(0, omikaConnection.QueryCount); } // ========================================================================= // SERVICE DELEGATION // ========================================================================= [Fact] public async Task Service_SearchAsync_DelegatesToRepository() { var repo = new FakeSuiviTempsRepository(); var service = new SuiviTempsService(repo); PagedResult result = await service.SearchAsync(new SuiviTempsSearchCriteria()); Assert.True(repo.SearchCalled); Assert.Empty(result.Items); } [Fact] public async Task Service_GetByIdAsync_DelegatesToRepository() { var repo = new FakeSuiviTempsRepository(); var service = new SuiviTempsService(repo); SuiviTempsDto? result = await service.GetByIdAsync("abc"); Assert.True(repo.GetByIdCalled); Assert.Null(result); } [Fact] public async Task Service_CreateAsync_DelegatesToRepository() { var repo = new FakeSuiviTempsRepository(); var service = new SuiviTempsService(repo); SuiviTempsDto result = await service.CreateAsync( new CreateSuiviTempsRequest { Date = DateTime.Now, DureeMinutes = 60 }); Assert.True(repo.CreateCalled); Assert.NotNull(result); } // ========================================================================= // DI TESTS // ========================================================================= [Fact] public void AddOmikaData_RegistersBatikSuiviTempsRepositoryAsScoped() { var services = new ServiceCollection(); services.AddOmikaData(); var descriptor = services.Single(d => d.ServiceType == typeof(BatikSuiviTempsRepository)); Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime); } [Fact] public void AddOmikaData_RegistersOmikaSuiviTempsRepositoryAsScoped() { var services = new ServiceCollection(); services.AddOmikaData(); var descriptor = services.Single(d => d.ServiceType == typeof(OmikaSuiviTempsRepository)); Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime); } [Fact] public void AddOmikaData_RegistersISuiviTempsRepositoryAsScoped() { var services = new ServiceCollection(); services.AddOmikaData(); var descriptor = services.Single(d => d.ServiceType == typeof(ISuiviTempsRepository)); Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime); } [Fact] public void AddOmikaData_ISuiviTempsRepositoryPointsToAdaptiveSuiviTempsRepository() { var services = new ServiceCollection(); services.AddOmikaData(); var descriptor = services.Single(d => d.ServiceType == typeof(ISuiviTempsRepository)); Assert.Equal(typeof(AdaptiveSuiviTempsRepository), descriptor.ImplementationType); } [Fact] public void AddOmikaData_RegistersISuiviTempsServiceAsScoped() { var services = new ServiceCollection(); services.AddOmikaData(); var descriptor = services.Single(d => d.ServiceType == typeof(ISuiviTempsService)); Assert.Equal(ServiceLifetime.Scoped, descriptor.Lifetime); } [Fact] public void AddOmikaData_ISuiviTempsServicePointsToSuiviTempsService() { var services = new ServiceCollection(); services.AddOmikaData(); var descriptor = services.Single(d => d.ServiceType == typeof(ISuiviTempsService)); Assert.Equal(typeof(SuiviTempsService), descriptor.ImplementationType); } [Fact] public void AddOmikaData_OnlyOneISuiviTempsRepositoryRegistered() { var services = new ServiceCollection(); services.AddOmikaData(); var descriptors = services.Where(d => d.ServiceType == typeof(ISuiviTempsRepository)).ToList(); Assert.Single(descriptors); } [Fact] public async Task AddOmikaData_ResolvesAllSuiviTempsRepositories_WithFakeConnection() { var services = new ServiceCollection(); services.AddSingleton(new ConfigurationBuilder().Build()); services.AddOmikaData(); var oldDescriptor = services.Single(d => d.ServiceType == typeof(IHfsqlConnection)); services.Remove(oldDescriptor); services.AddScoped(); var provider = services.BuildServiceProvider(); await using var scope = provider.CreateAsyncScope(); var suiviTempsRepo = scope.ServiceProvider.GetRequiredService(); Assert.IsType(suiviTempsRepo); var batikRepo = scope.ServiceProvider.GetRequiredService(); Assert.NotNull(batikRepo); var omikaRepo = scope.ServiceProvider.GetRequiredService(); Assert.NotNull(omikaRepo); var service = scope.ServiceProvider.GetRequiredService(); Assert.IsType(service); } // ========================================================================= // ARCHITECTURE // ========================================================================= [Fact] public void Repository_ImplementsISuiviTempsRepository() { Assert.True(typeof(ISuiviTempsRepository).IsAssignableFrom(typeof(AdaptiveSuiviTempsRepository))); } [Fact] public void Repository_ContainsNoSql() { string source = ReadSourceFile(typeof(AdaptiveSuiviTempsRepository)); Assert.DoesNotContain("SELECT ", source, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("FROM ", source, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("WHERE ", source, StringComparison.OrdinalIgnoreCase); } [Fact] public void Repository_DoesNotDependOnHfsqlConnection() { var type = typeof(AdaptiveSuiviTempsRepository); bool hasHfsqlConnectionCtor = type.GetConstructors() .Any(c => c.GetParameters() .Any(p => typeof(IHfsqlConnection).IsAssignableFrom(p.ParameterType))); Assert.False(hasHfsqlConnectionCtor, "AdaptiveSuiviTempsRepository ne doit pas dépendre directement de IHfsqlConnection."); } // ========================================================================= // HELPERS // ========================================================================= private static string ReadSourceFile(Type type) { string baseDir = AppDomain.CurrentDomain.BaseDirectory; var dir = new DirectoryInfo(baseDir); while (dir != null && !File.Exists(Path.Combine(dir.FullName, "Omika.slnx"))) dir = dir.Parent; string root = dir?.FullName ?? baseDir; return File.ReadAllText(Path.Combine(root, "src", "Omika.Data", "Repositories", type.Name + ".cs")); } // --- Dictionnaires de lignes physiques ---------------------------------- private static Dictionary BatikSuiviTempsDict( long pkid, string? libelle, string? reference) => new() { ["SDT_PKID"] = pkid, ["SDT_DateDebut"] = new DateTime(2026, 1, 15, 9, 0, 0), ["SDT_IDIntervention"] = 0L, ["SDT_IDCollaborateur"] = 0L, ["SDT_IDPartenaire"] = 0L, ["SDT_NbMinutes"] = 60, ["SDT_Libelle"] = libelle, ["SDT_IDTypeTemps"] = 0L, ["SDT_IDRdv"] = 0L, ["SDT_Ponderation"] = 0L, ["SDT_IDImport"] = 0L, ["SDT_DateHeureFin"] = new DateTime(2026, 1, 15, 10, 0, 0), ["GUID"] = Guid.NewGuid().ToString("D"), ["SDT_IDTypeRdv"] = 0L, ["SDT_Controle"] = false, ["SDT_IDGroupe"] = 0L, ["CreationDate"] = new DateTime(2026, 1, 15, 9, 0, 0), ["CreationUser"] = 0L, ["ModifDate"] = new DateTime(2026, 1, 15, 9, 0, 0), ["ModifUser"] = 0L }; private static Dictionary OmikaSuiviTempsDict( long pkid, string? libelle, string? reference) => new() { ["SDT_PKID"] = pkid, ["SDT_DateDebut"] = new DateTime(2026, 1, 15, 9, 0, 0), ["SDT_IDIntervention"] = 0L, ["SDT_IDCollaborateur"] = 0L, ["SDT_IDPartenaire"] = 0L, ["SDT_NbMinutes"] = 60, ["SDT_Libelle"] = libelle, ["SDT_IDTypeTemps"] = 0L, ["SDT_IDRdv"] = 0L, ["SDT_Ponderation"] = 0L, ["SDT_IDImport"] = 0L, ["SDT_DateHeureFin"] = new DateTime(2026, 1, 15, 10, 0, 0), ["GUID"] = Guid.NewGuid().ToString("D"), ["SDT_IDTypeRdv"] = 0L, ["SDT_Controle"] = false, ["SDT_IDGroupe"] = 0L, ["CreationDate"] = new DateTime(2026, 1, 15, 9, 0, 0), ["CreationUser"] = 0L, ["ModifDate"] = new DateTime(2026, 1, 15, 9, 0, 0), ["ModifUser"] = 0L, ["SDT_CleType"] = null, ["SDT_CleTypeRDV"] = null, ["SDT_NoteColb"] = null, ["SDT_NoteAdmin"] = null, ["SDT_GUID_Rdv_Colb"] = null }; // ========================================================================= // FakeSuiviTempsRepository (service) // ========================================================================= private sealed class FakeSuiviTempsRepository : ISuiviTempsRepository { public bool SearchCalled { get; private set; } public bool GetByIdCalled { get; private set; } public bool CreateCalled { get; private set; } public Task> SearchAsync( SuiviTempsSearchCriteria criteria, CancellationToken cancellationToken = default) { SearchCalled = true; return Task.FromResult(new PagedResult([], 1, 50, 0, 0)); } public Task GetByIdAsync( string id, CancellationToken cancellationToken = default) { GetByIdCalled = true; return Task.FromResult(null); } public Task CreateAsync( SuiviTempsCreateCommand command, CancellationToken cancellationToken = default) { CreateCalled = true; return Task.FromResult(new SuiviTemps { Id = Guid.NewGuid(), Date = command.Date, DureeMinutes = command.DureeMinutes }); } } // ========================================================================= // SpyingHfsqlConnection // ========================================================================= private sealed class SpyingHfsqlConnection : IHfsqlConnection, IHfsqlWriteConnection { public HfsqlOptions Options => new(); public HfsqlConnectionState State { get; private set; } = HfsqlConnectionState.Closed; public DbTransaction? ActiveTransaction => null; public int QueryCount { get; private set; } public bool ThrowOnQuery { get; set; } public string? LastSql { get; private set; } public IReadOnlyList? LastParameters { get; private set; } public CancellationToken LastCancellationToken { get; private set; } public IReadOnlyList AllSql => _allSql; private readonly List _allSql = []; public IReadOnlyList> SuiviTempsRows { get; set; } = []; private void Record(string sql, IReadOnlyList? parameters, CancellationToken cancellationToken) { LastSql = sql; LastParameters = parameters; LastCancellationToken = cancellationToken; QueryCount++; _allSql.Add(sql); } public Task OpenAsync(CancellationToken cancellationToken = default) { State = HfsqlConnectionState.Open; return Task.CompletedTask; } public Task CloseAsync() { State = HfsqlConnectionState.Closed; return Task.CompletedTask; } public Task ExecuteScalarAsync(string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) { Record(sql, parameters, cancellationToken); return Task.FromResult(null); } public Task ExecuteNonQueryAsync(string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) { Record(sql, parameters, cancellationToken); return Task.FromResult(0); } public Task>> QueryAsync( string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) { Record(sql, parameters, cancellationToken); if (ThrowOnQuery) throw new InvalidOperationException("Panne d'infrastructure simulée."); return Task.FromResult(ResolveRows(sql)); } public Task> GetColumnsAsync( string tableName, CancellationToken cancellationToken = default) { return Task.FromResult>([]); } public Task BeginTransactionAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; public Task CommitAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; public Task RollbackAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; public Task> ExecuteInTransactionAsync( IReadOnlyList operations, CancellationToken cancellationToken = default, DbTransaction? transaction = null) { foreach (HfsqlWriteOperation operation in operations) { Record(operation.Sql, operation.Parameters, cancellationToken); } // Retourne une identité auto-générée pour chaque opération. return Task.FromResult>( operations.Select(_ => (long?)1L).ToList()); } public ValueTask DisposeAsync() { State = HfsqlConnectionState.Closed; return ValueTask.CompletedTask; } private IReadOnlyList> ResolveRows(string sql) { if (ContainsFrom(sql, "SuiviDesTemps")) return SuiviTempsRows; return []; } private static bool ContainsFrom(string sql, string table) { string marker = $"FROM {table}"; int index = sql.IndexOf(marker, StringComparison.OrdinalIgnoreCase); while (index >= 0) { int next = index + marker.Length; if (next >= sql.Length || (!char.IsLetterOrDigit(sql[next]) && sql[next] != '_')) { return true; } index = sql.IndexOf(marker, index + 1, StringComparison.OrdinalIgnoreCase); } return false; } } // ========================================================================= // MinimalFakeHfsqlConnection (DI) // ========================================================================= private sealed class MinimalFakeHfsqlConnection : IHfsqlConnection { public HfsqlOptions Options => new(); public HfsqlConnectionState State { get; private set; } = HfsqlConnectionState.Closed; public Task OpenAsync(CancellationToken cancellationToken = default) { State = HfsqlConnectionState.Open; return Task.CompletedTask; } public Task CloseAsync() { State = HfsqlConnectionState.Closed; return Task.CompletedTask; } public Task ExecuteScalarAsync(string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) => Task.FromResult(null); public Task ExecuteNonQueryAsync(string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) => Task.FromResult(0); public Task>> QueryAsync( string sql, IReadOnlyList? parameters = null, CancellationToken cancellationToken = default) => Task.FromResult>>([]); public Task> GetColumnsAsync( string tableName, CancellationToken cancellationToken = default) => Task.FromResult>([]); public ValueTask DisposeAsync() { State = HfsqlConnectionState.Closed; return ValueTask.CompletedTask; } } }