using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Erp.Platform.DynamicData; using Erp.Platform.Queries; using Microsoft.Extensions.DependencyInjection; using Volo.Abp; using Volo.Abp.Domain.Services; namespace Erp.SqlQueryManager.Domain.Services; public class SqlExecutorService : DomainService, ISqlExecutorService { private readonly IDataSourceManager _dataSourceManager; private readonly IServiceProvider _serviceProvider; public SqlExecutorService( IDataSourceManager dataSourceManager, IServiceProvider serviceProvider) { _dataSourceManager = dataSourceManager; _serviceProvider = serviceProvider; } private async Task GetRepositoryAsync(string dataSourceCode) { // Get DataSource to determine database type var dataSource = await _dataSourceManager.GetDataSourceAsync( CurrentTenant.IsAvailable, dataSourceCode); if (dataSource == null) { throw new BusinessException("SqlQueryManager:DataSourceNotFound") .WithData("DataSourceCode", dataSourceCode); } // Get appropriate repository based on database type // For now, using MS SQL Server repository var repository = _serviceProvider.GetKeyedService("Ms"); if (repository == null) { throw new BusinessException("SqlQueryManager:RepositoryNotFound") .WithData("DatabaseType", "Ms"); } return repository; } public async Task ExecuteQueryAsync( string sql, string dataSourceCode, Dictionary parameters = null) { var stopwatch = Stopwatch.StartNew(); var result = new SqlExecutionResult(); try { var connectionString = await _dataSourceManager.GetConnectionStringAsync( CurrentTenant.IsAvailable, dataSourceCode); if (string.IsNullOrWhiteSpace(connectionString)) { throw new BusinessException("SqlQueryManager:InvalidConnectionString") .WithData("DataSourceCode", dataSourceCode); } var repository = await GetRepositoryAsync(dataSourceCode); var data = await repository.QueryAsync(sql, connectionString, parameters); stopwatch.Stop(); result.Success = true; result.Data = data; result.RowsAffected = data?.Count() ?? 0; result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds; result.Message = $"Query executed successfully."; } catch (Exception ex) { stopwatch.Stop(); result.Success = false; result.Message = $"Query execution failed: {ex.Message}"; result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds; result.Metadata["ErrorDetail"] = ex.ToString(); } return result; } public async Task ExecuteNonQueryAsync( string sql, string dataSourceCode, Dictionary parameters = null) { var stopwatch = Stopwatch.StartNew(); var result = new SqlExecutionResult(); try { var connectionString = await _dataSourceManager.GetConnectionStringAsync( CurrentTenant.IsAvailable, dataSourceCode); if (string.IsNullOrWhiteSpace(connectionString)) { throw new BusinessException("SqlQueryManager:InvalidConnectionString") .WithData("DataSourceCode", dataSourceCode); } var repository = await GetRepositoryAsync(dataSourceCode); var rowsAffected = await repository.ExecuteAsync(sql, connectionString, parameters); stopwatch.Stop(); result.Success = true; result.RowsAffected = rowsAffected; result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds; result.Message = $"Command executed successfully. Rows affected: {rowsAffected}"; } catch (Exception ex) { stopwatch.Stop(); result.Success = false; result.Message = $"Command execution failed: {ex.Message}"; result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds; result.Metadata["ErrorDetail"] = ex.ToString(); } return result; } public async Task ExecuteScalarAsync( string sql, string dataSourceCode, Dictionary parameters = null) { var stopwatch = Stopwatch.StartNew(); var result = new SqlExecutionResult(); try { var connectionString = await _dataSourceManager.GetConnectionStringAsync( CurrentTenant.IsAvailable, dataSourceCode); if (string.IsNullOrWhiteSpace(connectionString)) { throw new BusinessException("SqlQueryManager:InvalidConnectionString") .WithData("DataSourceCode", dataSourceCode); } var repository = await GetRepositoryAsync(dataSourceCode); var scalarValue = await repository.ExecuteScalarAsync(sql, connectionString, parameters); stopwatch.Stop(); result.Success = true; result.Data = new[] { new { Value = scalarValue } }; result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds; result.Message = "Scalar query executed successfully"; result.Metadata["ScalarValue"] = scalarValue; } catch (Exception ex) { stopwatch.Stop(); result.Success = false; result.Message = $"Scalar query execution failed: {ex.Message}"; result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds; result.Metadata["ErrorDetail"] = ex.ToString(); } return result; } public async Task DeployStoredProcedureAsync( string procedureBody, string dataSourceCode) { return await ExecuteNonQueryAsync(procedureBody, dataSourceCode); } public async Task DeployViewAsync( string viewDefinition, string dataSourceCode) { return await ExecuteNonQueryAsync(viewDefinition, dataSourceCode); } public async Task DeployFunctionAsync( string functionBody, string dataSourceCode) { return await ExecuteNonQueryAsync(functionBody, dataSourceCode); } public async Task CheckObjectExistsAsync( string objectName, string objectType, string dataSourceCode, string schemaName = "dbo") { try { var sql = $@" SELECT COUNT(*) FROM sys.objects o INNER JOIN sys.schemas s ON o.schema_id = s.schema_id WHERE s.name = @SchemaName AND o.name = @ObjectName AND o.type_desc = @ObjectType"; var parameters = new Dictionary { { "SchemaName", schemaName }, { "ObjectName", objectName }, { "ObjectType", objectType.ToUpperInvariant() } }; var result = await ExecuteScalarAsync(sql, dataSourceCode, parameters); return result.Success && result.Metadata.ContainsKey("ScalarValue") && Convert.ToInt32(result.Metadata["ScalarValue"]) > 0; } catch { return false; } } public async Task DropObjectAsync( string objectName, string objectType, string dataSourceCode, string schemaName = "dbo") { var dropCommand = objectType.ToUpperInvariant() switch { "SQL_STORED_PROCEDURE" => $"DROP PROCEDURE IF EXISTS [{schemaName}].[{objectName}]", "VIEW" => $"DROP VIEW IF EXISTS [{schemaName}].[{objectName}]", "SQL_SCALAR_FUNCTION" => $"DROP FUNCTION IF EXISTS [{schemaName}].[{objectName}]", "SQL_TABLE_VALUED_FUNCTION" => $"DROP FUNCTION IF EXISTS [{schemaName}].[{objectName}]", "SQL_INLINE_TABLE_VALUED_FUNCTION" => $"DROP FUNCTION IF EXISTS [{schemaName}].[{objectName}]", _ => throw new BusinessException("SqlQueryManager:UnsupportedObjectType") .WithData("ObjectType", objectType) }; return await ExecuteNonQueryAsync(dropCommand, dataSourceCode); } public Task<(bool IsValid, string ErrorMessage)> ValidateSqlAsync(string sql) { try { if (string.IsNullOrWhiteSpace(sql)) { return Task.FromResult((false, "SQL query is empty")); } // Basic validation - check for dangerous keywords var dangerousPatterns = new[] { @"\bDROP\s+DATABASE\b", @"\bDROP\s+SCHEMA\b", @"\bTRUNCATE\s+TABLE\b", @"\bALTER\s+DATABASE\b", @"\bSHUTDOWN\b", @"\bxp_cmdshell\b" }; foreach (var pattern in dangerousPatterns) { if (Regex.IsMatch(sql, pattern, RegexOptions.IgnoreCase)) { return Task.FromResult((false, $"SQL contains potentially dangerous command: {pattern}")); } } // Check for balanced parentheses var openCount = sql.Count(c => c == '('); var closeCount = sql.Count(c => c == ')'); if (openCount != closeCount) { return Task.FromResult((false, "Unbalanced parentheses in SQL")); } return Task.FromResult((true, string.Empty)); } catch (Exception ex) { return Task.FromResult((false, $"Validation error: {ex.Message}")); } } }