2025-12-05 08:56:53 +00:00
|
|
|
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<IDynamicDataRepository> 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<IDynamicDataRepository>("Ms");
|
|
|
|
|
|
|
|
|
|
if (repository == null)
|
|
|
|
|
{
|
|
|
|
|
throw new BusinessException("SqlQueryManager:RepositoryNotFound")
|
|
|
|
|
.WithData("DatabaseType", "Ms");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return repository;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<SqlExecutionResult> ExecuteQueryAsync(
|
|
|
|
|
string sql,
|
|
|
|
|
string dataSourceCode,
|
|
|
|
|
Dictionary<string, object> 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;
|
2025-12-05 22:38:21 +00:00
|
|
|
result.Message = $"Query executed successfully.";
|
2025-12-05 08:56:53 +00:00
|
|
|
}
|
|
|
|
|
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<SqlExecutionResult> ExecuteNonQueryAsync(
|
|
|
|
|
string sql,
|
|
|
|
|
string dataSourceCode,
|
|
|
|
|
Dictionary<string, object> 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<SqlExecutionResult> ExecuteScalarAsync<T>(
|
|
|
|
|
string sql,
|
|
|
|
|
string dataSourceCode,
|
|
|
|
|
Dictionary<string, object> 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<T>(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<SqlExecutionResult> DeployStoredProcedureAsync(
|
|
|
|
|
string procedureBody,
|
|
|
|
|
string dataSourceCode)
|
|
|
|
|
{
|
|
|
|
|
return await ExecuteNonQueryAsync(procedureBody, dataSourceCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<SqlExecutionResult> DeployViewAsync(
|
|
|
|
|
string viewDefinition,
|
|
|
|
|
string dataSourceCode)
|
|
|
|
|
{
|
|
|
|
|
return await ExecuteNonQueryAsync(viewDefinition, dataSourceCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<SqlExecutionResult> DeployFunctionAsync(
|
|
|
|
|
string functionBody,
|
|
|
|
|
string dataSourceCode)
|
|
|
|
|
{
|
|
|
|
|
return await ExecuteNonQueryAsync(functionBody, dataSourceCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<bool> 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<string, object>
|
|
|
|
|
{
|
|
|
|
|
{ "SchemaName", schemaName },
|
|
|
|
|
{ "ObjectName", objectName },
|
|
|
|
|
{ "ObjectType", objectType.ToUpperInvariant() }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var result = await ExecuteScalarAsync<int>(sql, dataSourceCode, parameters);
|
|
|
|
|
|
|
|
|
|
return result.Success && result.Metadata.ContainsKey("ScalarValue")
|
|
|
|
|
&& Convert.ToInt32(result.Metadata["ScalarValue"]) > 0;
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<SqlExecutionResult> 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}"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|