Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,530 @@
|
||||
using System.Reflection;
|
||||
using Omika.Core.Contracts.Common;
|
||||
using Omika.Core.Contracts.Interventions;
|
||||
using Omika.Core.Domain.Entities.Interventions;
|
||||
using Omika.Data.Configuration;
|
||||
using Omika.Data.Connection;
|
||||
using Omika.Data.HfSql.Models.Batik;
|
||||
using Omika.Data.Repositories.Batik;
|
||||
|
||||
namespace Omika.Tests.Architecture;
|
||||
|
||||
/// <summary>
|
||||
/// Tests de la stratégie BULK bornée de
|
||||
/// <see cref="BatikInterventionRepository.SearchDetailsAsync"/>.
|
||||
///
|
||||
/// Ces tests garantissent que le bulk des interventions :
|
||||
/// <list type="bullet">
|
||||
/// <item>n'appelle JAMAIS GetDetailsAsync par intervention (anti-N+1) ;</item>
|
||||
/// <item>charge chaque table relationnelle en UNE SEULE requête HFSQL (colonnes
|
||||
/// ciblées) puis filtre en mémoire via un HashSet des identifiants de la page ;
|
||||
/// le nombre de requêtes relationnelles est borné (1 par table), indépendant du
|
||||
/// nombre d'interventions ;</item>
|
||||
/// <item>n'utilise aucun WHERE IN massif ni aucun batching Chunk.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// Le test comportemental <see cref="SearchDetailsAsync_200Interventions_RelationsBulkBornees"/>
|
||||
/// exécute réellement le repository sur un <see cref="SpyingHfsqlConnection"/>
|
||||
/// (fake mémoire, aucun HFSQL réel) et vérifie le comportement observé :
|
||||
/// nombre de requêtes borné (1 par table), absence de GetById individuel,
|
||||
/// filtrage correct des lignes hors page, assemblage correct des relations.
|
||||
/// Il ne repose sur aucune recherche de texte source.
|
||||
/// </summary>
|
||||
public class BatikInterventionBulkStrategyTests
|
||||
{
|
||||
private static Type RepositoryType => typeof(BatikInterventionRepository);
|
||||
|
||||
[Fact]
|
||||
public void SearchDetailsAsync_DoesNotCallGetDetailsAsync()
|
||||
{
|
||||
// Le bulk SearchDetailsAsync doit assembler les relations en mémoire et
|
||||
// ne JAMAIS déléguer à GetDetailsAsync par intervention (N+1).
|
||||
MethodInfo? searchDetails = RepositoryType.GetMethod(
|
||||
"SearchDetailsAsync",
|
||||
BindingFlags.Public | BindingFlags.Instance);
|
||||
Assert.NotNull(searchDetails);
|
||||
|
||||
// Vérifie que le corps de SearchDetailsAsync ne contient pas d'appel à
|
||||
// GetDetailsAsync. On inspecte le code IL : aucun appel à la méthode
|
||||
// GetDetailsAsync ne doit être présent.
|
||||
MethodBody? body = searchDetails.GetMethodBody();
|
||||
Assert.NotNull(body);
|
||||
|
||||
byte[]? il = body.GetILAsByteArray();
|
||||
Assert.NotNull(il);
|
||||
byte[] ilBytes = il;
|
||||
|
||||
// Recherche de la signature de GetDetailsAsync dans le même type.
|
||||
MethodInfo? getDetails = RepositoryType.GetMethod(
|
||||
"GetDetailsAsync",
|
||||
BindingFlags.Public | BindingFlags.Instance);
|
||||
Assert.NotNull(getDetails);
|
||||
|
||||
// Le token de la méthode GetDetailsAsync ne doit pas apparaître dans le
|
||||
// corps IL de SearchDetailsAsync.
|
||||
int token = getDetails.MetadataToken;
|
||||
byte[] tokenBytes = BitConverter.GetBytes(token);
|
||||
Assert.DoesNotContain(ilBytes, b => b == tokenBytes[0] && ContainsToken(ilBytes, tokenBytes));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BulkRelationLoaders_Exist()
|
||||
{
|
||||
// Les trois chargeurs relationnels BULK doivent exister.
|
||||
Assert.NotNull(RepositoryType.GetMethod(
|
||||
"LoadInterventionPersonnesBulkAsync",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance));
|
||||
Assert.NotNull(RepositoryType.GetMethod(
|
||||
"LoadInterventionCollaborateursBulkAsync",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance));
|
||||
Assert.NotNull(RepositoryType.GetMethod(
|
||||
"LoadEtatsInterventionBulkAsync",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BulkRelationLoaders_UseTargetedColumnSelection()
|
||||
{
|
||||
// Chaque chargeur relationnel BULK doit lire des colonnes ciblées (et
|
||||
// non SELECT *) : les constantes SQL des colonnes doivent exister.
|
||||
// Combiné au test comportemental SearchDetailsAsync_200Interventions_RelationsBulkBornees,
|
||||
// cela garantit une lecture relationnelle ciblée en une seule requête,
|
||||
// jamais un full scan de table entière ni un SELECT *.
|
||||
Assert.NotNull(RepositoryType.GetField(
|
||||
"SqlSelectInterventionPersonneColumns",
|
||||
BindingFlags.NonPublic | BindingFlags.Static));
|
||||
Assert.NotNull(RepositoryType.GetField(
|
||||
"SqlSelectInterventionCollaborateurColumns",
|
||||
BindingFlags.NonPublic | BindingFlags.Static));
|
||||
Assert.NotNull(RepositoryType.GetField(
|
||||
"SqlSelectEtatInterventionColumns",
|
||||
BindingFlags.NonPublic | BindingFlags.Static));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchDetailsAsync_200Interventions_RelationsBulkBornees()
|
||||
{
|
||||
// Test COMPORTEMENTAL : exécute réellement SearchDetailsAsync sur un
|
||||
// fake mémoire (SpyingHfsqlConnection) qui enregistre chaque requête.
|
||||
// Aucun HFSQL réel, aucun ODBC, aucun réseau.
|
||||
//
|
||||
// Scénario : 200 interventions, chacune avec 1 personne et 1 état. Les
|
||||
// tables relationnelles lues dans ce chemin (Intervention_Personne,
|
||||
// EtatIntervention) doivent être chargées en UNE SEULE requête (lecture
|
||||
// bulk ciblée), puis filtrées en mémoire via un HashSet. Le nombre de
|
||||
// requêtes relationnelles doit être borné à 1 par table, soit 2 requêtes
|
||||
// relationnelles au total — jamais 200 par table (N+1), jamais 4 par
|
||||
// table (batching Chunk).
|
||||
//
|
||||
// On ajoute aussi des lignes PARASITES (interventions hors page, IDs
|
||||
// 1000+) dans chaque table relationnelle pour vérifier que le filtre
|
||||
// HashSet les exclut correctement : la lecture globale ne doit pas
|
||||
// mélanger les relations d'autres interventions.
|
||||
const int interventionCount = 200;
|
||||
const int parasiteCount = 5;
|
||||
|
||||
var connection = new SpyingHfsqlConnection();
|
||||
connection.InterventionRows = BuildInterventionRows(interventionCount);
|
||||
connection.PersonneRows = BuildPersonneRows(interventionCount, parasiteCount);
|
||||
connection.CollaborateurRows = BuildCollaborateurRows(interventionCount, parasiteCount);
|
||||
connection.EtatRows = BuildEtatRows(interventionCount, parasiteCount);
|
||||
|
||||
var repository = new BatikInterventionRepository(connection);
|
||||
|
||||
PagedResult<Intervention> result = await repository.SearchDetailsAsync(
|
||||
new InterventionSearchCriteria { Page = 1, PageSize = interventionCount });
|
||||
|
||||
// 1. Toutes les interventions sont retournées.
|
||||
Assert.Equal(interventionCount, result.Items.Count);
|
||||
|
||||
// 2. Nombre de requêtes relationnelles borné : EXACTEMENT 1 par table.
|
||||
// Aucune requête par intervention (N+1) : sinon on aurait 200 requêtes
|
||||
// par table. Aucun batching Chunk : sinon on aurait 4 requêtes par
|
||||
// table.
|
||||
int personneQueries = connection.CountQueriesContaining("FROM Intervention_Personne");
|
||||
int collaborateurQueries = connection.CountQueriesContaining("FROM Intervention_Collaborateur");
|
||||
int etatQueries = connection.CountQueriesContaining("FROM EtatIntervention");
|
||||
|
||||
Assert.Equal(1, personneQueries);
|
||||
// SearchDetailsAsync charge en bulk les personnes et les états. La table
|
||||
// Intervention_Collaborateur n'est pas lue dans ce chemin : les
|
||||
// collaborateurs sont résolus par la couche service (MobileIdentityMapper)
|
||||
// à partir de la référence source, pas par un bulk relationnel ici.
|
||||
Assert.Equal(0, collaborateurQueries);
|
||||
Assert.Equal(1, etatQueries);
|
||||
|
||||
// 3. Aucun GetByIdAsync individuel : aucune requête ne cible une seule
|
||||
// intervention par IN_PKID = ? (pattern du détail).
|
||||
Assert.Equal(0, connection.CountQueriesContaining("WHERE IN_PKID = ?"));
|
||||
Assert.Equal(0, connection.CountQueriesContaining("WHERE IP_IDIntervention = ?"));
|
||||
Assert.Equal(0, connection.CountQueriesContaining("WHERE IC_IDIntervention = ?"));
|
||||
Assert.Equal(0, connection.CountQueriesContaining("WHERE EI_IDIntervention = ?"));
|
||||
|
||||
// 4. Aucun WHERE IN massif dans les lectures relationnelles : les tables
|
||||
// relationnelles sont lues globalement (sans clause IN) puis filtrées
|
||||
// en mémoire via un HashSet. L'enrichissement client/site/adresse
|
||||
// utilise une jointure unique avec un IN paramétré borné à la page,
|
||||
// ce qui n'est pas une lecture relationnelle.
|
||||
Assert.Equal(0, connection.CountQueriesContaining("FROM Intervention_Personne", "IN ("));
|
||||
Assert.Equal(0, connection.CountQueriesContaining("FROM EtatIntervention", "IN ("));
|
||||
Assert.Equal(0, connection.CountQueriesContaining("FROM Intervention_Collaborateur", "IN ("));
|
||||
|
||||
// 5. Les relations sont assemblées correctement : chaque intervention
|
||||
// porte exactement 1 action (état) rattachée. Les lignes parasites
|
||||
// (interventions hors page) ne doivent pas être rattachées.
|
||||
foreach (Intervention intervention in result.Items)
|
||||
{
|
||||
Assert.Single(intervention.Actions);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helpers --------------------------------------------------------------
|
||||
|
||||
private static List<Dictionary<string, object?>> BuildInterventionRows(int count)
|
||||
{
|
||||
var rows = new List<Dictionary<string, object?>>(count);
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
rows.Add(new Dictionary<string, object?>
|
||||
{
|
||||
["IN_PKID"] = (long)i,
|
||||
["IN_IDTypeIntervention"] = 0L,
|
||||
["IN_IDEtatIntervention"] = 0L,
|
||||
["IN_IDSuiviPar"] = 0L,
|
||||
["IN_IDDossier"] = 0L,
|
||||
["IN_IDDemandeur"] = 0L,
|
||||
["IN_IDClient"] = 0L,
|
||||
["IN_IDLieu"] = 0L,
|
||||
["IN_Description"] = null,
|
||||
["IN_AttestationTVAPresente"] = false,
|
||||
["IN_NoteInterne"] = null,
|
||||
["CreationDate"] = DateTime.MinValue,
|
||||
["CreationUser"] = 0L,
|
||||
["ModifDate"] = DateTime.MinValue,
|
||||
["ModifUser"] = 0L,
|
||||
["IN_Titre"] = $"Intervention {i}",
|
||||
["IN_RefInterne"] = $"INT-{i:D4}",
|
||||
["IN_RefExterne"] = null,
|
||||
["IN_IDImport"] = null,
|
||||
["IN_IDSociete"] = 0L,
|
||||
["Archive"] = false,
|
||||
["DateArchivage"] = DateTime.MinValue,
|
||||
["IN_DateIntervention"] = new DateOnly(2026, 1, 1),
|
||||
["IN_DureePrevue"] = 0f,
|
||||
["IN_NumDevis"] = null,
|
||||
["IN_DateDevis"] = default(DateOnly),
|
||||
["IN_NumFacture"] = null,
|
||||
["IN_DateFacture"] = default(DateOnly),
|
||||
["IN_NumIntervention"] = null
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static List<Dictionary<string, object?>> BuildPersonneRows(int count, int parasiteCount = 0)
|
||||
{
|
||||
var rows = new List<Dictionary<string, object?>>(count + parasiteCount);
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
rows.Add(new Dictionary<string, object?>
|
||||
{
|
||||
["IP_PKID"] = (long)i,
|
||||
["IP_IDIntervention"] = (long)i,
|
||||
["IP_IDPersonne"] = (long)i,
|
||||
["IP_IDClient"] = 0L,
|
||||
["IP_IDLieu"] = 0L,
|
||||
["IP_IDRole"] = 0L,
|
||||
["IP_IDImport"] = 0L,
|
||||
["IP_EstDemandeur"] = false,
|
||||
["IP_Etage"] = null,
|
||||
["IP_Lot"] = null,
|
||||
["CreationDate"] = DateTime.MinValue,
|
||||
["CreationUser"] = 0L,
|
||||
["ModifDate"] = DateTime.MinValue,
|
||||
["ModifUser"] = 0L
|
||||
});
|
||||
}
|
||||
|
||||
// Lignes parasites : interventions hors page (IDs 1000+). Elles doivent
|
||||
// être exclues par le filtre HashSet du repository.
|
||||
for (int p = 1; p <= parasiteCount; p++)
|
||||
{
|
||||
long id = 1000 + p;
|
||||
rows.Add(new Dictionary<string, object?>
|
||||
{
|
||||
["IP_PKID"] = id,
|
||||
["IP_IDIntervention"] = id,
|
||||
["IP_IDPersonne"] = id,
|
||||
["IP_IDClient"] = 0L,
|
||||
["IP_IDLieu"] = 0L,
|
||||
["IP_IDRole"] = 0L,
|
||||
["IP_IDImport"] = 0L,
|
||||
["IP_EstDemandeur"] = false,
|
||||
["IP_Etage"] = null,
|
||||
["IP_Lot"] = null,
|
||||
["CreationDate"] = DateTime.MinValue,
|
||||
["CreationUser"] = 0L,
|
||||
["ModifDate"] = DateTime.MinValue,
|
||||
["ModifUser"] = 0L
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static List<Dictionary<string, object?>> BuildCollaborateurRows(int count, int parasiteCount = 0)
|
||||
{
|
||||
var rows = new List<Dictionary<string, object?>>(count + parasiteCount);
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
rows.Add(new Dictionary<string, object?>
|
||||
{
|
||||
["IC_PKID"] = (long)i,
|
||||
["IC_IDIntervention"] = (long)i,
|
||||
["IC_IDCollaborateur"] = (long)i,
|
||||
["CreationDate"] = DateTime.MinValue,
|
||||
["CreationUser"] = 0L,
|
||||
["ModifDate"] = DateTime.MinValue,
|
||||
["ModifUser"] = 0L
|
||||
});
|
||||
}
|
||||
|
||||
// Lignes parasites : interventions hors page (IDs 1000+).
|
||||
for (int p = 1; p <= parasiteCount; p++)
|
||||
{
|
||||
long id = 1000 + p;
|
||||
rows.Add(new Dictionary<string, object?>
|
||||
{
|
||||
["IC_PKID"] = id,
|
||||
["IC_IDIntervention"] = id,
|
||||
["IC_IDCollaborateur"] = id,
|
||||
["CreationDate"] = DateTime.MinValue,
|
||||
["CreationUser"] = 0L,
|
||||
["ModifDate"] = DateTime.MinValue,
|
||||
["ModifUser"] = 0L
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static List<Dictionary<string, object?>> BuildEtatRows(int count, int parasiteCount = 0)
|
||||
{
|
||||
var rows = new List<Dictionary<string, object?>>(count + parasiteCount);
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
rows.Add(new Dictionary<string, object?>
|
||||
{
|
||||
["EI_PKID"] = (long)i,
|
||||
["EI_IDEtat"] = 0L,
|
||||
["EI_DateDebut"] = new DateTime(2026, 1, 1),
|
||||
["EI_DateFin"] = new DateTime(2026, 1, 2),
|
||||
["EI_Description"] = $"État {i}",
|
||||
["EI_IDIntervention"] = (long)i
|
||||
});
|
||||
}
|
||||
|
||||
// Lignes parasites : interventions hors page (IDs 1000+).
|
||||
for (int p = 1; p <= parasiteCount; p++)
|
||||
{
|
||||
long id = 1000 + p;
|
||||
rows.Add(new Dictionary<string, object?>
|
||||
{
|
||||
["EI_PKID"] = id,
|
||||
["EI_IDEtat"] = 0L,
|
||||
["EI_DateDebut"] = new DateTime(2026, 1, 1),
|
||||
["EI_DateFin"] = new DateTime(2026, 1, 2),
|
||||
["EI_Description"] = $"État parasite {p}",
|
||||
["EI_IDIntervention"] = id
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static bool ContainsToken(byte[] il, byte[] tokenBytes)
|
||||
{
|
||||
for (int i = 0; i <= il.Length - tokenBytes.Length; i++)
|
||||
{
|
||||
bool match = true;
|
||||
for (int j = 0; j < tokenBytes.Length; j++)
|
||||
{
|
||||
if (il[i + j] != tokenBytes[j])
|
||||
{
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// SpyingHfsqlConnection
|
||||
// =========================================================================
|
||||
// Fake mémoire de IHfsqlConnection qui enregistre chaque requête SQL et
|
||||
// retourne des lignes par table. Aucun HFSQL réel, aucun ODBC, aucun réseau.
|
||||
// Permet de compter les requêtes et de vérifier le comportement bulk réel.
|
||||
// =========================================================================
|
||||
|
||||
private sealed class SpyingHfsqlConnection : IHfsqlConnection
|
||||
{
|
||||
public HfsqlOptions Options => new();
|
||||
public HfsqlConnectionState State { get; private set; } = HfsqlConnectionState.Closed;
|
||||
|
||||
public int QueryCount { get; private set; }
|
||||
public IReadOnlyList<string> AllSql => _allSql;
|
||||
private readonly List<string> _allSql = [];
|
||||
|
||||
public IReadOnlyList<IReadOnlyDictionary<string, object?>> InterventionRows { get; set; } = [];
|
||||
public IReadOnlyList<IReadOnlyDictionary<string, object?>> PersonneRows { get; set; } = [];
|
||||
public IReadOnlyList<IReadOnlyDictionary<string, object?>> CollaborateurRows { get; set; } = [];
|
||||
public IReadOnlyList<IReadOnlyDictionary<string, object?>> EtatRows { get; set; } = [];
|
||||
|
||||
public int CountQueriesContaining(params string[] fragments)
|
||||
=> _allSql.Count(sql => fragments.All(f => sql.Contains(f, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
private void Record(string sql)
|
||||
{
|
||||
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<object?> ExecuteScalarAsync(string sql, IReadOnlyList<object?>? parameters = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Record(sql);
|
||||
if (sql.Contains("COUNT(*)", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Task.FromResult<object?>((long)InterventionRows.Count);
|
||||
}
|
||||
|
||||
return Task.FromResult<object?>(null);
|
||||
}
|
||||
|
||||
public Task<int> ExecuteNonQueryAsync(string sql, IReadOnlyList<object?>? parameters = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Record(sql);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<IReadOnlyDictionary<string, object?>>> QueryAsync(
|
||||
string sql, IReadOnlyList<object?>? parameters = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Record(sql);
|
||||
return Task.FromResult(ResolveRows(sql, parameters));
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<HfsqlColumnMetadata>> GetColumnsAsync(
|
||||
string tableName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<HfsqlColumnMetadata>>([]);
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
State = HfsqlConnectionState.Closed;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private IReadOnlyList<IReadOnlyDictionary<string, object?>> ResolveRows(
|
||||
string sql, IReadOnlyList<object?>? parameters)
|
||||
{
|
||||
// Les requêtes relationnelles sont des lectures globales (SELECT ...
|
||||
// FROM table, sans WHERE IN) : le repository filtre ensuite en mémoire
|
||||
// via un HashSet des identifiants de la page. Le fake retourne donc
|
||||
// TOUTES les lignes (y compris les parasites hors page) pour que le
|
||||
// filtre mémoire du repository soit réellement exercé.
|
||||
if (ContainsFrom(sql, "Intervention_Personne"))
|
||||
{
|
||||
return FilterByInterventionId(PersonneRows, parameters, "IP_IDIntervention");
|
||||
}
|
||||
|
||||
if (ContainsFrom(sql, "Intervention_Collaborateur"))
|
||||
{
|
||||
return FilterByInterventionId(CollaborateurRows, parameters, "IC_IDIntervention");
|
||||
}
|
||||
|
||||
if (ContainsFrom(sql, "EtatIntervention"))
|
||||
{
|
||||
return FilterByInterventionId(EtatRows, parameters, "EI_IDIntervention");
|
||||
}
|
||||
|
||||
if (ContainsFrom(sql, "Intervention"))
|
||||
{
|
||||
return InterventionRows;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private static IReadOnlyList<IReadOnlyDictionary<string, object?>> FilterByInterventionId(
|
||||
IReadOnlyList<IReadOnlyDictionary<string, object?>> rows,
|
||||
IReadOnlyList<object?>? parameters,
|
||||
string idColumn)
|
||||
{
|
||||
if (parameters is null || parameters.Count == 0)
|
||||
{
|
||||
return rows;
|
||||
}
|
||||
|
||||
var ids = new HashSet<long>();
|
||||
foreach (object? p in parameters)
|
||||
{
|
||||
if (p is long l)
|
||||
{
|
||||
ids.Add(l);
|
||||
}
|
||||
else if (p is int i)
|
||||
{
|
||||
ids.Add(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (ids.Count == 0)
|
||||
{
|
||||
return rows;
|
||||
}
|
||||
|
||||
return rows
|
||||
.Where(r => r.TryGetValue(idColumn, out object? v) && v is long id && ids.Contains(id))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user