using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Erp.SqlQueryManager.Application.Contracts; using Erp.SqlQueryManager.Domain.Entities; using Erp.SqlQueryManager.Domain.Services; using Erp.SqlQueryManager.Domain.Shared; using Microsoft.AspNetCore.Authorization; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; namespace Erp.SqlQueryManager.Application; /// /// Unified service for SQL Object Explorer /// Combines all SQL objects into a single endpoint /// [Authorize("App.SqlQueryManager")] public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerAppService { private readonly IRepository _queryRepository; private readonly IRepository _procedureRepository; private readonly IRepository _viewRepository; private readonly IRepository _functionRepository; private readonly ISqlExecutorService _sqlExecutorService; private readonly ISqlTemplateProvider _templateProvider; public SqlObjectManagerAppService( IRepository queryRepository, IRepository procedureRepository, IRepository viewRepository, IRepository functionRepository, ISqlExecutorService sqlExecutorService, ISqlTemplateProvider templateProvider) { _queryRepository = queryRepository; _procedureRepository = procedureRepository; _viewRepository = viewRepository; _functionRepository = functionRepository; _sqlExecutorService = sqlExecutorService; _templateProvider = templateProvider; } public async Task GetAllObjectsAsync(string dataSourceCode) { var result = new SqlObjectExplorerDto(); // Get all queries for this data source var queries = await _queryRepository.GetListAsync(); result.Queries = queries .Where(q => q.DataSourceCode == dataSourceCode) .Select(q => new SqlQueryDto { Id = q.Id, Code = q.Code, Name = q.Name, Description = q.Description, QueryText = q.QueryText, DataSourceCode = q.DataSourceCode, Status = q.Status, Category = q.Category, Tags = q.Tags, IsModifyingData = q.IsModifyingData, Parameters = q.Parameters, ExecutionCount = q.ExecutionCount, LastExecutedAt = q.LastExecutedAt }) .ToList(); // Get all stored procedures for this data source var procedures = await _procedureRepository.GetListAsync(); result.StoredProcedures = procedures .Where(p => p.DataSourceCode == dataSourceCode) .Select(p => new SqlStoredProcedureDto { Id = p.Id, ProcedureName = p.ProcedureName, SchemaName = p.SchemaName, DisplayName = p.DisplayName, Description = p.Description, ProcedureBody = p.ProcedureBody, DataSourceCode = p.DataSourceCode, Category = p.Category, Parameters = p.Parameters, IsDeployed = p.IsDeployed, LastDeployedAt = p.LastDeployedAt }) .ToList(); // Get all views for this data source var views = await _viewRepository.GetListAsync(); result.Views = views .Where(v => v.DataSourceCode == dataSourceCode) .Select(v => new SqlViewDto { Id = v.Id, ViewName = v.ViewName, SchemaName = v.SchemaName, DisplayName = v.DisplayName, Description = v.Description, ViewDefinition = v.ViewDefinition, DataSourceCode = v.DataSourceCode, Category = v.Category, WithSchemaBinding = v.WithSchemaBinding, IsDeployed = v.IsDeployed, LastDeployedAt = v.LastDeployedAt }) .ToList(); // Get all functions for this data source var functions = await _functionRepository.GetListAsync(); result.Functions = functions .Where(f => f.DataSourceCode == dataSourceCode) .Select(f => new SqlFunctionDto { Id = f.Id, FunctionName = f.FunctionName, SchemaName = f.SchemaName, DisplayName = f.DisplayName, Description = f.Description, FunctionType = f.FunctionType, FunctionBody = f.FunctionBody, ReturnType = f.ReturnType, DataSourceCode = f.DataSourceCode, Category = f.Category, Parameters = f.Parameters, IsDeployed = f.IsDeployed, LastDeployedAt = f.LastDeployedAt }) .ToList(); // Get all database tables result.Tables = await GetTablesAsync(dataSourceCode); // Get all templates result.Templates = _templateProvider.GetAvailableQueryTemplates() .Select(t => new SqlTemplateDto { Type = t.Type, Name = t.Name, Description = t.Description, Template = _templateProvider.GetQueryTemplate(t.Type) }) .ToList(); return result; } private async Task> GetTablesAsync(string dataSourceCode) { var query = @" SELECT SCHEMA_NAME(t.schema_id) AS SchemaName, t.name AS TableName FROM sys.tables t WHERE t.is_ms_shipped = 0 ORDER BY SCHEMA_NAME(t.schema_id), t.name"; var result = await _sqlExecutorService.ExecuteQueryAsync(query, dataSourceCode); var tables = new List(); if (result.Success && result.Data != null) { foreach (var row in result.Data) { var dict = row as System.Collections.Generic.IDictionary; if (dict != null) { tables.Add(new DatabaseTableDto { SchemaName = dict["SchemaName"]?.ToString() ?? "dbo", TableName = dict["TableName"]?.ToString() ?? "" }); } } } return tables; } #region Query Operations public async Task CreateQueryAsync(CreateSqlQueryDto input) { var query = ObjectMapper.Map(input); query.Status = SqlQueryStatus.Draft; var created = await _queryRepository.InsertAsync(query, autoSave: true); return ObjectMapper.Map(created); } public async Task UpdateQueryAsync(Guid id, UpdateSqlQueryDto input) { var query = await _queryRepository.GetAsync(id); query.Name = input.Name; query.Description = input.Description; query.QueryText = input.QueryText; query.Category = input.Category; query.Tags = input.Tags; var updated = await _queryRepository.UpdateAsync(query, autoSave: true); return ObjectMapper.Map(updated); } public async Task DeleteQueryAsync(Guid id) { await _queryRepository.DeleteAsync(id); } public async Task ExecuteQueryAsync(ExecuteSqlQueryDto input) { var sqlText = input.QueryText.Trim(); var sqlUpper = sqlText.ToUpperInvariant(); // Check if this is a DDL command (CREATE/ALTER/DROP for VIEW/PROCEDURE/FUNCTION) bool isDDLCommand = sqlUpper.Contains("CREATE VIEW") || sqlUpper.Contains("ALTER VIEW") || sqlUpper.Contains("CREATE PROCEDURE") || sqlUpper.Contains("CREATE PROC") || sqlUpper.Contains("ALTER PROCEDURE") || sqlUpper.Contains("ALTER PROC") || sqlUpper.Contains("CREATE FUNCTION") || sqlUpper.Contains("ALTER FUNCTION") || sqlUpper.Contains("DROP VIEW") || sqlUpper.Contains("DROP PROCEDURE") || sqlUpper.Contains("DROP PROC") || sqlUpper.Contains("DROP FUNCTION"); if (isDDLCommand) { // For DDL commands, only validate syntax without executing try { // Try to parse/validate the SQL using SET PARSEONLY var validationSql = $"SET PARSEONLY ON;\n{sqlText}\nSET PARSEONLY OFF;"; await _sqlExecutorService.ExecuteNonQueryAsync(validationSql, input.DataSourceCode); return new SqlQueryExecutionResultDto { Success = true, Message = "SQL syntax is valid. Use Save button to save and Deploy button to create in SQL Server.", Data = new List(), RowsAffected = 0, ExecutionTimeMs = 0 }; } catch (Exception ex) { return new SqlQueryExecutionResultDto { Success = false, Message = $"SQL syntax error: {ex.Message}", Data = new List(), RowsAffected = 0, ExecutionTimeMs = 0 }; } } // For DML commands (SELECT, INSERT, UPDATE, DELETE), execute normally var result = await _sqlExecutorService.ExecuteQueryAsync(input.QueryText, input.DataSourceCode); return MapExecutionResult(result); } public async Task ExecuteSavedQueryAsync(Guid id) { var query = await _queryRepository.GetAsync(id); var result = await _sqlExecutorService.ExecuteQueryAsync(query.QueryText, query.DataSourceCode); // Update execution statistics query.ExecutionCount++; query.LastExecutedAt = DateTime.UtcNow; await _queryRepository.UpdateAsync(query, autoSave: true); return MapExecutionResult(result); } #endregion #region Stored Procedure Operations public async Task UpdateStoredProcedureAsync(Guid id, UpdateSqlStoredProcedureDto input) { var procedure = await _procedureRepository.GetAsync(id); procedure.DisplayName = input.DisplayName; procedure.Description = input.Description; procedure.ProcedureBody = input.ProcedureBody; procedure.Category = input.Category; procedure.IsDeployed = false; procedure.LastDeployedAt = null; var updated = await _procedureRepository.UpdateAsync(procedure, autoSave: true); return ObjectMapper.Map(updated); } public async Task DeleteStoredProcedureAsync(Guid id) { var procedure = await _procedureRepository.GetAsync(id); // Drop stored procedure from SQL Server (always try, regardless of IsDeployed flag) try { var dropSql = $"IF OBJECT_ID('[{procedure.SchemaName}].[{procedure.ProcedureName}]', 'P') IS NOT NULL DROP PROCEDURE [{procedure.SchemaName}].[{procedure.ProcedureName}]"; await _sqlExecutorService.ExecuteNonQueryAsync(dropSql, procedure.DataSourceCode); } catch { // Ignore errors if object doesn't exist in database } await _procedureRepository.DeleteAsync(id); } public async Task DeployStoredProcedureAsync(DeployStoredProcedureDto input) { var procedure = await _procedureRepository.GetAsync(input.Id); try { // Önce DROP işlemi yap (varsa) var dropSql = $"IF OBJECT_ID('[{procedure.SchemaName}].[{procedure.ProcedureName}]', 'P') IS NOT NULL DROP PROCEDURE [{procedure.SchemaName}].[{procedure.ProcedureName}]"; await _sqlExecutorService.ExecuteNonQueryAsync(dropSql, procedure.DataSourceCode); // Sonra CREATE işlemi yap var result = await _sqlExecutorService.DeployStoredProcedureAsync( procedure.ProcedureBody, procedure.DataSourceCode ); if (result.Success) { procedure.IsDeployed = true; procedure.LastDeployedAt = DateTime.UtcNow; await _procedureRepository.UpdateAsync(procedure, autoSave: true); } return MapExecutionResult(result); } catch (Exception ex) { return new SqlQueryExecutionResultDto { Success = false, Message = $"Deploy failed: {ex.Message}", Data = new List(), RowsAffected = 0, ExecutionTimeMs = 0 }; } } #endregion #region View Operations public async Task UpdateViewAsync(Guid id, UpdateSqlViewDto input) { var view = await _viewRepository.GetAsync(id); view.DisplayName = input.DisplayName; view.Description = input.Description; view.ViewDefinition = input.ViewDefinition; view.Category = input.Category; view.IsDeployed = false; view.LastDeployedAt = null; var updated = await _viewRepository.UpdateAsync(view, autoSave: true); return ObjectMapper.Map(updated); } public async Task DeleteViewAsync(Guid id) { var view = await _viewRepository.GetAsync(id); // Drop view from SQL Server (always try, regardless of IsDeployed flag) try { var dropSql = $"IF OBJECT_ID('[{view.SchemaName}].[{view.ViewName}]', 'V') IS NOT NULL DROP VIEW [{view.SchemaName}].[{view.ViewName}]"; await _sqlExecutorService.ExecuteNonQueryAsync(dropSql, view.DataSourceCode); } catch { // Ignore errors if object doesn't exist in database } await _viewRepository.DeleteAsync(id); } public async Task DeployViewAsync(DeployViewDto input) { var view = await _viewRepository.GetAsync(input.Id); try { // Önce DROP işlemi yap (varsa) var dropSql = $"IF OBJECT_ID('[{view.SchemaName}].[{view.ViewName}]', 'V') IS NOT NULL DROP VIEW [{view.SchemaName}].[{view.ViewName}]"; await _sqlExecutorService.ExecuteNonQueryAsync(dropSql, view.DataSourceCode); // Sonra CREATE işlemi yap var result = await _sqlExecutorService.DeployViewAsync( view.ViewDefinition, view.DataSourceCode ); if (result.Success) { view.IsDeployed = true; view.LastDeployedAt = DateTime.UtcNow; await _viewRepository.UpdateAsync(view, autoSave: true); } return MapExecutionResult(result); } catch (Exception ex) { return new SqlQueryExecutionResultDto { Success = false, Message = $"Deploy failed: {ex.Message}", Data = new List(), RowsAffected = 0, ExecutionTimeMs = 0 }; } } #endregion #region Function Operations public async Task UpdateFunctionAsync(Guid id, UpdateSqlFunctionDto input) { var function = await _functionRepository.GetAsync(id); function.DisplayName = input.DisplayName; function.Description = input.Description; function.FunctionBody = input.FunctionBody; function.Category = input.Category; function.IsDeployed = false; function.LastDeployedAt = null; var updated = await _functionRepository.UpdateAsync(function, autoSave: true); return ObjectMapper.Map(updated); } public async Task DeleteFunctionAsync(Guid id) { var function = await _functionRepository.GetAsync(id); // Drop function from SQL Server (always try, regardless of IsDeployed flag) try { var dropSql = $"IF OBJECT_ID('[{function.SchemaName}].[{function.FunctionName}]', 'FN') IS NOT NULL DROP FUNCTION [{function.SchemaName}].[{function.FunctionName}]"; await _sqlExecutorService.ExecuteNonQueryAsync(dropSql, function.DataSourceCode); } catch { // Ignore errors if object doesn't exist in database } await _functionRepository.DeleteAsync(id); } public async Task DeployFunctionAsync(DeployFunctionDto input) { var function = await _functionRepository.GetAsync(input.Id); try { // Önce DROP işlemi yap (varsa) var dropSql = $"IF OBJECT_ID('[{function.SchemaName}].[{function.FunctionName}]', 'FN') IS NOT NULL DROP FUNCTION [{function.SchemaName}].[{function.FunctionName}]"; await _sqlExecutorService.ExecuteNonQueryAsync(dropSql, function.DataSourceCode); // Sonra CREATE işlemi yap var result = await _sqlExecutorService.DeployFunctionAsync( function.FunctionBody, function.DataSourceCode ); if (result.Success) { function.IsDeployed = true; function.LastDeployedAt = DateTime.UtcNow; await _functionRepository.UpdateAsync(function, autoSave: true); } return MapExecutionResult(result); } catch (Exception ex) { return new SqlQueryExecutionResultDto { Success = false, Message = $"Deploy failed: {ex.Message}", Data = new List(), RowsAffected = 0, ExecutionTimeMs = 0 }; } } #endregion #region Database Metadata Operations public async Task> GetTableColumnsAsync(string dataSourceCode, string schemaName, string tableName) { var query = $@" SELECT c.name AS ColumnName, TYPE_NAME(c.user_type_id) AS DataType, c.is_nullable AS IsNullable, c.max_length AS MaxLength FROM sys.columns c INNER JOIN sys.tables t ON c.object_id = t.object_id INNER JOIN sys.schemas s ON t.schema_id = s.schema_id WHERE s.name = '{schemaName}' AND t.name = '{tableName}' ORDER BY c.column_id"; var result = await _sqlExecutorService.ExecuteQueryAsync(query, dataSourceCode); var columns = new List(); if (result.Success && result.Data != null) { foreach (var row in result.Data) { var dict = row as System.Collections.Generic.IDictionary; if (dict != null) { columns.Add(new DatabaseColumnDto { ColumnName = dict["ColumnName"]?.ToString() ?? "", DataType = dict["DataType"]?.ToString() ?? "", IsNullable = dict["IsNullable"] is bool b && b, MaxLength = dict["MaxLength"] != null ? int.Parse(dict["MaxLength"].ToString()) : null }); } } } return columns; } #endregion public async Task SmartSaveAsync(SmartSaveInputDto input) { var result = new SmartSaveResultDto(); var sqlText = input.SqlText.Trim(); var sqlUpper = sqlText.ToUpperInvariant(); try { // Analyze SQL to determine object type if (sqlUpper.Contains("CREATE VIEW") || sqlUpper.Contains("ALTER VIEW")) { // Extract view name var viewName = input.Name; var schemaName = "dbo"; var displayName = input.Name; // Check if view already exists var existingView = (await _viewRepository.GetListAsync()) .FirstOrDefault(v => v.ViewName == viewName && v.DataSourceCode == input.DataSourceCode); SqlView view; if (existingView != null) { // Update existing view existingView.DisplayName = displayName; existingView.ViewDefinition = sqlText; existingView.IsDeployed = false; existingView.LastDeployedAt = null; if (!string.IsNullOrEmpty(input.Description)) { existingView.Description = input.Description; } view = await _viewRepository.UpdateAsync(existingView, autoSave: true); result.Message = $"View '{viewName}' updated successfully. Use Deploy button to deploy changes to SQL Server."; } else { // Create new view view = new SqlView( GuidGenerator.Create(), viewName, // ViewName from SQL schemaName, displayName, // DisplayName from user input sqlText, input.DataSourceCode ); if (!string.IsNullOrEmpty(input.Description)) { view.Description = input.Description; } await _viewRepository.InsertAsync(view, autoSave: true); result.Message = $"View '{viewName}' saved successfully. Use Deploy button to deploy to SQL Server."; } result.ObjectType = "View"; result.ObjectId = view.Id; result.Deployed = view.IsDeployed; } else if (sqlUpper.Contains("CREATE PROCEDURE") || sqlUpper.Contains("CREATE PROC") || sqlUpper.Contains("ALTER PROCEDURE") || sqlUpper.Contains("ALTER PROC")) { // Extract procedure name var procName = input.Name; var schemaName = "dbo"; var displayName = input.Name; // Check if procedure already exists var existingProcedure = (await _procedureRepository.GetListAsync()) .FirstOrDefault(p => p.ProcedureName == procName && p.DataSourceCode == input.DataSourceCode); SqlStoredProcedure procedure; if (existingProcedure != null) { // Update existing procedure existingProcedure.DisplayName = displayName; existingProcedure.ProcedureBody = sqlText; existingProcedure.IsDeployed = false; existingProcedure.LastDeployedAt = null; if (!string.IsNullOrEmpty(input.Description)) { existingProcedure.Description = input.Description; } procedure = await _procedureRepository.UpdateAsync(existingProcedure, autoSave: true); result.Message = $"Stored Procedure '{procName}' updated successfully. Use Deploy button to deploy changes to SQL Server."; } else { // Create new procedure procedure = new SqlStoredProcedure( GuidGenerator.Create(), procName, // ProcedureName from SQL schemaName, displayName, // DisplayName from user input sqlText, input.DataSourceCode ); if (!string.IsNullOrEmpty(input.Description)) { procedure.Description = input.Description; } await _procedureRepository.InsertAsync(procedure, autoSave: true); result.Message = $"Stored Procedure '{procName}' saved successfully. Use Deploy button to deploy to SQL Server."; } result.ObjectType = "StoredProcedure"; result.ObjectId = procedure.Id; result.Deployed = procedure.IsDeployed; } else if (sqlUpper.Contains("CREATE FUNCTION") || sqlUpper.Contains("ALTER FUNCTION")) { // Extract function name var funcName = input.Name; var schemaName = "dbo"; var returnType = "NVARCHAR(MAX)"; // Default, can be extracted from SQL var displayName = input.Name; // Check if function already exists var existingFunction = (await _functionRepository.GetListAsync()) .FirstOrDefault(f => f.FunctionName == funcName && f.DataSourceCode == input.DataSourceCode); SqlFunction function; if (existingFunction != null) { // Update existing function existingFunction.DisplayName = displayName; existingFunction.FunctionBody = sqlText; existingFunction.IsDeployed = false; existingFunction.LastDeployedAt = null; if (!string.IsNullOrEmpty(input.Description)) { existingFunction.Description = input.Description; } function = await _functionRepository.UpdateAsync(existingFunction, autoSave: true); result.Message = $"Function '{funcName}' updated successfully. Use Deploy button to deploy changes to SQL Server."; } else { // Create new function function = new SqlFunction( GuidGenerator.Create(), funcName, // FunctionName from SQL schemaName, displayName, // DisplayName from user input SqlFunctionType.ScalarFunction, sqlText, returnType, input.DataSourceCode ); if (!string.IsNullOrEmpty(input.Description)) { function.Description = input.Description; } await _functionRepository.InsertAsync(function, autoSave: true); result.Message = $"Function '{funcName}' saved successfully. Use Deploy button to deploy to SQL Server."; } result.ObjectType = "Function"; result.ObjectId = function.Id; result.Deployed = function.IsDeployed; } else { // Default to Query (SELECT, INSERT, UPDATE, DELETE, etc.) var queryName = input.Name ?? $"Query_{DateTime.Now:yyyyMMddHHmmss}"; var queryCode = queryName.Replace(" ", "_"); // Check if query already exists var existingQuery = (await _queryRepository.GetListAsync()) .FirstOrDefault(q => q.Code == queryCode && q.DataSourceCode == input.DataSourceCode); SqlQuery query; if (existingQuery != null) { // Update existing query existingQuery.Name = queryName; existingQuery.QueryText = sqlText; if (!string.IsNullOrEmpty(input.Description)) { existingQuery.Description = input.Description; } query = await _queryRepository.UpdateAsync(existingQuery, autoSave: true); result.Message = $"Query '{queryName}' updated successfully"; } else { // Create new query query = new SqlQuery( GuidGenerator.Create(), queryCode, queryName, sqlText, input.DataSourceCode ); if (!string.IsNullOrEmpty(input.Description)) { query.Description = input.Description; } await _queryRepository.InsertAsync(query, autoSave: true); result.Message = $"Query '{queryName}' saved successfully"; } result.ObjectType = "Query"; result.ObjectId = query.Id; result.Deployed = false; // Queries are not deployed; } } catch (Exception ex) { throw new Volo.Abp.UserFriendlyException($"Failed to save SQL object: {ex.Message}"); } return result; } private string ExtractObjectName(string sql, params string[] keywords) { var sqlUpper = sql.ToUpperInvariant(); foreach (var keyword in keywords) { var createIndex = sqlUpper.IndexOf($"CREATE {keyword}", StringComparison.OrdinalIgnoreCase); var alterIndex = sqlUpper.IndexOf($"ALTER {keyword}", StringComparison.OrdinalIgnoreCase); var startIndex = Math.Max(createIndex, alterIndex); if (startIndex >= 0) { startIndex = sqlUpper.IndexOf(keyword, startIndex, StringComparison.OrdinalIgnoreCase) + keyword.Length; var endIndex = sql.IndexOfAny(new[] { ' ', '\r', '\n', '\t', '(', '[' }, startIndex); if (endIndex > startIndex) { var name = sql.Substring(startIndex, endIndex - startIndex).Trim(); // Remove schema prefix if exists if (name.Contains(".")) { name = name.Substring(name.LastIndexOf('.') + 1); } // Remove square brackets name = name.Replace("[", "").Replace("]", ""); return name; } } } return "UnnamedObject"; } #region Helper Methods private SqlQueryExecutionResultDto MapExecutionResult(SqlExecutionResult result) { return new SqlQueryExecutionResultDto { Success = result.Success, Message = result.Message, Data = result.Data, RowsAffected = result.RowsAffected, ExecutionTimeMs = result.ExecutionTimeMs, Metadata = result.Metadata }; } #endregion }