using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Sozsoft.Platform.EntityFrameworkCore;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Uow;
using static Sozsoft.Settings.SettingsConsts;
namespace Sozsoft.Platform.Data.Seeds;
///
/// Seeds/{host|tenants/{tenantId}}/{sql|postgres}/execute klasöründeki .sql dosyaları
/// iki aşamada işlenir:
/// 1) Migration/seed sırasında tarafından, kapsam klasörüyle
/// (host/tenants/{tenantId}) aynı bağlamda çalıştırılır (nesne oluşturulur/güncellenir).
/// 2) Tüm migration ve seed işlemleri bittikten sonra bu sınıf, dosya adından türetilen stored
/// procedure'ü ilgili bağlamda (host ya da tenant) varlığı doğrulanarak çalıştırır.
/// Dosya adı = procedure adı. İsteğe bağlı olarak "schema.Procedure.sql" formatı da desteklenir.
///
public class AfterAllMigrationsSqlExecutor : IAfterAllMigrationsSqlExecutor, ITransientDependency
{
/// {sql|postgres} altındaki, migration sonrası da çalıştırılacak script klasörünün adı.
public const string DirectoryName = SeedPathResolver.ExecuteFolder;
private static readonly Regex SafeIdentifierRegex =
new(@"^[A-Za-z_][A-Za-z0-9_]{0,126}$", RegexOptions.Compiled);
private readonly IDbContextProvider _dbContextProvider;
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IConfiguration _configuration;
private readonly ILogger _logger;
public AfterAllMigrationsSqlExecutor(
IDbContextProvider dbContextProvider,
IUnitOfWorkManager unitOfWorkManager,
IConfiguration configuration,
ILogger logger)
{
_dbContextProvider = dbContextProvider;
_unitOfWorkManager = unitOfWorkManager;
_configuration = configuration;
_logger = logger;
}
public async Task ExecuteAsync(Guid? tenantId = null)
{
var directoryPath = GetScriptDirectoryPath(tenantId);
if (!Directory.Exists(directoryPath))
{
_logger.LogInformation("{Directory} directory not found, skipping AfterAllMigrationsSqlExecutor.", directoryPath);
return;
}
var sqlFiles = Directory.GetFiles(directoryPath, "*.sql")
.OrderBy(f => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase)
.ToArray();
if (sqlFiles.Length == 0)
{
_logger.LogInformation("No .sql files found in {Directory}, skipping AfterAllMigrationsSqlExecutor.", directoryPath);
return;
}
_logger.LogInformation(
"AfterAllMigrationsSqlExecutor started for {Scope}. {Count} procedure(s) to be executed.",
tenantId == null ? "Host" : $"Tenant {tenantId}",
sqlFiles.Length);
var failed = new List();
foreach (var filePath in sqlFiles)
{
var objectName = Path.GetFileNameWithoutExtension(filePath);
if (!TryResolveObjectName(objectName, out var schemaName, out var procedureName))
{
_logger.LogWarning(
"Skipped {FileName}: file name is not a valid procedure identifier ([schema.]ProcedureName).",
Path.GetFileName(filePath));
continue;
}
try
{
using var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
var dbContext = await _dbContextProvider.GetDbContextAsync();
_logger.LogInformation(
"Executing procedure {Schema}.{Procedure}",
schemaName,
procedureName);
await dbContext.Database.ExecuteSqlRawAsync(BuildExecuteScript(schemaName, procedureName));
await uow.CompleteAsync();
_logger.LogInformation(
"Executed procedure {Schema}.{Procedure}",
schemaName,
procedureName);
}
catch (Exception ex)
{
failed.Add($"{schemaName}.{procedureName}");
_logger.LogError(ex, "Procedure failed: {Schema}.{Procedure}", schemaName, procedureName);
}
}
if (failed.Count > 0)
{
_logger.LogWarning(
"AfterAllMigrationsSqlExecutor completed with {FailedCount} failed procedure(s): {Failed}",
failed.Count,
string.Join(", ", failed));
}
else
{
_logger.LogInformation("AfterAllMigrationsSqlExecutor completed. {Count} procedure(s) executed.", sqlFiles.Length);
}
}
private string GetScriptDirectoryPath(Guid? tenantId)
{
var providerDirectoryName = SeedPathResolver.GetProviderFolderName(
DefaultDatabaseProvider == DatabaseProvider.PostgreSql);
return SeedPathResolver.GetScopePath(_configuration, tenantId, providerDirectoryName, DirectoryName);
}
///
/// Dosya adını [schema.]ProcedureName olarak çözer ve her iki parçayı da beyaz liste ile doğrular.
/// Doğrulanmayan hiçbir ad SQL metnine yazılmaz.
///
private static bool TryResolveObjectName(string fileName, out string schemaName, out string procedureName)
{
var defaultSchema = DefaultDatabaseProvider == DatabaseProvider.PostgreSql ? "public" : "dbo";
schemaName = defaultSchema;
procedureName = fileName;
var parts = fileName.Split('.');
if (parts.Length == 2)
{
schemaName = parts[0];
procedureName = parts[1];
}
else if (parts.Length > 2)
{
return false;
}
return SafeIdentifierRegex.IsMatch(schemaName) && SafeIdentifierRegex.IsMatch(procedureName);
}
private static string BuildExecuteScript(string schemaName, string procedureName)
{
if (DefaultDatabaseProvider == DatabaseProvider.PostgreSql)
{
return $@"
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = '{schemaName}' AND p.proname = '{procedureName}' AND p.prokind = 'p'
) THEN
CALL ""{schemaName}"".""{procedureName}""();
ELSE
RAISE NOTICE 'Procedure {schemaName}.{procedureName} not found, skipped.';
END IF;
END
$$;";
}
return $@"
IF OBJECT_ID(N'[{schemaName}].[{procedureName}]', N'P') IS NOT NULL
EXEC [{schemaName}].[{procedureName}];
ELSE
PRINT N'Procedure [{schemaName}].[{procedureName}] not found, skipped.';";
}
}