using System.Data; using Dapper; using Microsoft.Data.SqlClient; using PlotLine.Models; namespace PlotLine.Data; public interface INameIntelligenceRepository { Task GetLatestImportAsync(string packageId, string version, CancellationToken cancellationToken = default); Task StartImportAsync(string packageId, string version, string checksum, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default); Task CompleteImportAsync(int importId, bool succeeded, int localeRows, int givenNameRows, int relationshipRows, int usageRows, string? errorSummary, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default); Task UpsertLocalesAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default); Task UpsertGivenNamesAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default); Task UpsertRelationshipsAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default); Task UpsertUsagesAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default); Task> ListLocalesAsync(CancellationToken cancellationToken = default); Task RecogniseAsync(string normalisedName, string? localeCode, int? birthYear, CancellationToken cancellationToken = default); Task> ListRelationshipsAsync(int givenNameId, CancellationToken cancellationToken = default); Task> SearchAsync(int userId, NameSearchFilter filter, CancellationToken cancellationToken = default); Task> ListUsedDecorationsAsync(int userId, int projectId, int? bookId, string normalisedName, string usedScope, CancellationToken cancellationToken = default); } public sealed class NameIntelligenceRepository(ISqlConnectionFactory connectionFactory) : INameIntelligenceRepository { public async Task GetLatestImportAsync(string packageId, string version, CancellationToken cancellationToken = default) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync(Command( "dbo.NameReferenceImport_GetLatest", new { PackageID = packageId, SemanticVersion = version }, cancellationToken: cancellationToken)); } public async Task StartImportAsync(string packageId, string version, string checksum, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default) => await connection.QuerySingleAsync(Command( "dbo.NameReferenceImport_Start", new { PackageID = packageId, SemanticVersion = version, PackageChecksum = checksum }, transaction, cancellationToken)); public Task CompleteImportAsync(int importId, bool succeeded, int localeRows, int givenNameRows, int relationshipRows, int usageRows, string? errorSummary, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default) => connection.ExecuteAsync(Command( "dbo.NameReferenceImport_Complete", new { NameReferenceImportID = importId, Succeeded = succeeded, LocaleRows = localeRows, GivenNameRows = givenNameRows, RelationshipRows = relationshipRows, UsageRows = usageRows, ErrorSummary = errorSummary }, transaction, cancellationToken)); public Task UpsertLocalesAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default) => ExecuteTableAsync("dbo.NameLocale_BulkUpsert", "Rows", "dbo.NameLocaleImportTable", rows, connection, transaction, cancellationToken); public Task UpsertGivenNamesAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default) => ExecuteTableAsync("dbo.GivenName_BulkUpsert", "Rows", "dbo.GivenNameImportTable", rows, connection, transaction, cancellationToken); public Task UpsertRelationshipsAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default) => ExecuteTableAsync("dbo.NameRelationship_BulkUpsert", "Rows", "dbo.NameRelationshipImportTable", rows, connection, transaction, cancellationToken); public Task UpsertUsagesAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default) => ExecuteTableAsync("dbo.NameUsage_BulkUpsert", "Rows", "dbo.NameUsageImportTable", rows, connection, transaction, cancellationToken); public async Task> ListLocalesAsync(CancellationToken cancellationToken = default) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync(Command("dbo.NameLocale_List", cancellationToken: cancellationToken)); return rows.ToList(); } public async Task RecogniseAsync(string normalisedName, string? localeCode, int? birthYear, CancellationToken cancellationToken = default) { using var connection = connectionFactory.CreateConnection(); using var grid = await connection.QueryMultipleAsync(Command( "dbo.GivenName_Recognise", new { NormalisedName = normalisedName, LocaleCode = localeCode, BirthYear = birthYear }, cancellationToken: cancellationToken)); var name = await grid.ReadSingleOrDefaultAsync(); if (name is null) { return new NameRecognitionResult(); } var usage = await grid.ReadSingleOrDefaultAsync(); var related = (await grid.ReadAsync()).ToList(); return new NameRecognitionResult { Recognised = true, Name = name, Usage = usage, RelatedForms = related, Message = "Recognised given-name form." }; } public async Task> ListRelationshipsAsync(int givenNameId, CancellationToken cancellationToken = default) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync(Command( "dbo.GivenName_Relationships", new { GivenNameID = givenNameId }, cancellationToken: cancellationToken)); return rows.ToList(); } public async Task> SearchAsync(int userId, NameSearchFilter filter, CancellationToken cancellationToken = default) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync(Command( "dbo.GivenName_Search", new { UserID = userId, filter.ProjectID, filter.BookID, Search = NullIfWhiteSpace(filter.Search), LocaleCode = NullIfWhiteSpace(filter.LocaleCode), filter.BirthYear, GenderPreference = NullIfWhiteSpace(filter.GenderPreference), filter.GenderThreshold, NameType = NullIfWhiteSpace(filter.NameType), filter.MinimumUsage, filter.UsedScope, filter.UsedOnly, filter.Page, filter.PageSize }, cancellationToken: cancellationToken)); return rows.ToList(); } public async Task> ListUsedDecorationsAsync(int userId, int projectId, int? bookId, string normalisedName, string usedScope, CancellationToken cancellationToken = default) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync(Command( "dbo.GivenName_UsedDecorations", new { UserID = userId, ProjectID = projectId, BookID = bookId, NormalisedName = normalisedName, UsedScope = usedScope }, cancellationToken: cancellationToken)); return rows.ToList(); } public IDbConnection CreateConnection() => connectionFactory.CreateConnection(); private static async Task ExecuteTableAsync(string procedure, string parameterName, string typeName, DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken) { var parameters = new DynamicParameters(); parameters.Add(parameterName, rows.AsTableValuedParameter(typeName)); await connection.ExecuteAsync(Command(procedure, parameters, transaction, cancellationToken)); } private static CommandDefinition Command(string commandText, object? parameters = null, IDbTransaction? transaction = null, CancellationToken cancellationToken = default) => new(commandText, parameters, transaction, commandType: CommandType.StoredProcedure, cancellationToken: cancellationToken); private static string? NullIfWhiteSpace(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); }