SqlManagerda View, Function ve Sp Deploy özelliği

This commit is contained in:
Sedat Öztürk 2026-05-04 00:17:11 +03:00
parent e01875b7c9
commit 953b4e7d98
2 changed files with 265 additions and 11 deletions

View file

@ -23,6 +23,24 @@ namespace Sozsoft.SqlQueryManager.Application;
[Authorize("App.SqlQueryManager")] [Authorize("App.SqlQueryManager")]
public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerAppService public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerAppService
{ {
private const string QueryExecutedSuccessfullyMessage = "Query executed successfully.";
private const string QueryExecutedAndDeployedMessage = "Query executed and deployed successfully";
private const string SqlIdentifierPattern = @"(?:\[[^\]]+\]|\""[^\""\r\n]+\""|[A-Za-z_][A-Za-z0-9_@$#]*)";
private const string MultiPartSqlIdentifierPattern = SqlIdentifierPattern + @"(?:\s*\.\s*" + SqlIdentifierPattern + @"){0,2}";
private static readonly Regex SqlObjectDefinitionRegex = new(
@"^\s*(?:(?:--[^\r\n]*|/\*[\s\S]*?\*/)\s*)*(?<verb>CREATE|ALTER)\s+(?:OR\s+ALTER\s+)?(?<type>VIEW|PROC(?:EDURE)?|FUNCTION)\s+(?<name>" + MultiPartSqlIdentifierPattern + @")",
RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
private static readonly Regex SqlObjectDropRegex = new(
@"^\s*(?:(?:--[^\r\n]*|/\*[\s\S]*?\*/)\s*)*DROP\s+(?<type>VIEW|PROC(?:EDURE)?|FUNCTION)\s+(?:IF\s+EXISTS\s+)?(?<names>" + MultiPartSqlIdentifierPattern + @"(?:\s*,\s*" + MultiPartSqlIdentifierPattern + @")*)",
RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
private static readonly Regex SqlObjectHeaderCanonicalizeRegex = new(
@"^(?<prefix>\s*(?:(?:--[^\r\n]*\r?\n|/\*[\s\S]*?\*/)\s*)*)(?<verb>CREATE|ALTER)\s+(?<after>(?:OR\s+ALTER\s+)?(?:VIEW|PROC(?:EDURE)?|FUNCTION)\b)",
RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
private readonly ISqlExecutorService _sqlExecutorService; private readonly ISqlExecutorService _sqlExecutorService;
private readonly ISqlTemplateProvider _templateProvider; private readonly ISqlTemplateProvider _templateProvider;
private readonly ICurrentTenant _currentTenant; private readonly ICurrentTenant _currentTenant;
@ -166,6 +184,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
public async Task<SqlQueryExecutionResultDto> ExecuteQueryAsync(ExecuteSqlQueryDto input) public async Task<SqlQueryExecutionResultDto> ExecuteQueryAsync(ExecuteSqlQueryDto input)
{ {
ValidateTenantAccess(); ValidateTenantAccess();
var isDeployed = false;
// Split on GO batch separators (SQL Server SSMS convention — not valid T-SQL) // Split on GO batch separators (SQL Server SSMS convention — not valid T-SQL)
var batches = Regex.Split(input.QueryText ?? string.Empty, @"^\s*GO\s*$", RegexOptions.Multiline | RegexOptions.IgnoreCase) var batches = Regex.Split(input.QueryText ?? string.Empty, @"^\s*GO\s*$", RegexOptions.Multiline | RegexOptions.IgnoreCase)
@ -180,7 +199,13 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
input.QueryText, input.QueryText,
input.DataSourceCode, input.DataSourceCode,
input.Parameters); input.Parameters);
return MapExecutionResult(result);
if (result.Success)
{
isDeployed = TrySyncSqlObjectScriptFile(input.QueryText);
}
return MapExecutionResult(result, isDeployed);
} }
// Multiple batches — execute sequentially, return last meaningful result // Multiple batches — execute sequentially, return last meaningful result
@ -194,9 +219,11 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
if (!lastResult.Success) if (!lastResult.Success)
return MapExecutionResult(lastResult); return MapExecutionResult(lastResult);
isDeployed |= TrySyncSqlObjectScriptFile(batch);
} }
return MapExecutionResult(lastResult!); return MapExecutionResult(lastResult!, isDeployed);
} }
public async Task<string> GetNativeObjectDefinitionAsync(string dataSourceCode, string schemaName, string objectName) public async Task<string> GetNativeObjectDefinitionAsync(string dataSourceCode, string schemaName, string objectName)
@ -219,12 +246,12 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
if (row != null && row.ContainsKey("Definition")) if (row != null && row.ContainsKey("Definition"))
{ {
var definition = row["Definition"]?.ToString() ?? string.Empty; var definition = row["Definition"]?.ToString() ?? string.Empty;
// Replace first CREATE keyword with ALTER so the user edits, not recreates // Always open object script as CREATE OR ALTER in editor.
definition = System.Text.RegularExpressions.Regex.Replace( definition = Regex.Replace(
definition, definition,
@"^\s*CREATE\s+", @"^\s*(?:CREATE|ALTER)\s+(?:OR\s+ALTER\s+)?",
"ALTER ", "CREATE OR ALTER ",
System.Text.RegularExpressions.RegexOptions.IgnoreCase); RegexOptions.IgnoreCase);
return definition; return definition;
} }
} }
@ -276,12 +303,14 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
return columns; return columns;
} }
private SqlQueryExecutionResultDto MapExecutionResult(SqlExecutionResult result) private SqlQueryExecutionResultDto MapExecutionResult(SqlExecutionResult result, bool isDeployed = false)
{ {
return new SqlQueryExecutionResultDto return new SqlQueryExecutionResultDto
{ {
Success = result.Success, Success = result.Success,
Message = result.Message, Message = result.Success
? (isDeployed ? QueryExecutedAndDeployedMessage : QueryExecutedSuccessfullyMessage)
: result.Message,
Data = result.Data, Data = result.Data,
RowsAffected = result.RowsAffected, RowsAffected = result.RowsAffected,
ExecutionTimeMs = result.ExecutionTimeMs, ExecutionTimeMs = result.ExecutionTimeMs,
@ -320,6 +349,231 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
return Task.CompletedTask; return Task.CompletedTask;
} }
private bool TrySyncSqlObjectScriptFile(string sqlScript)
{
if (string.IsNullOrWhiteSpace(sqlScript))
return false;
if (TrySaveSqlObjectScript(sqlScript))
return true;
return TryDeleteSqlObjectScripts(sqlScript);
}
private bool TrySaveSqlObjectScript(string sqlScript)
{
var match = SqlObjectDefinitionRegex.Match(sqlScript);
if (!match.Success)
return false;
var objectType = NormalizeObjectType(match.Groups["type"].Value);
var (schemaName, objectName) = ParseSchemaAndObjectName(match.Groups["name"].Value);
if (string.IsNullOrWhiteSpace(objectName))
return false;
var fileName = BuildSqlObjectScriptFileName(objectType, schemaName, objectName);
var canonicalScript = CanonicalizeSqlObjectScriptForSeed(sqlScript);
SaveSqlDataFile(fileName, canonicalScript);
return true;
}
private bool TryDeleteSqlObjectScripts(string sqlScript)
{
var match = SqlObjectDropRegex.Match(sqlScript);
if (!match.Success)
return false;
var objectType = NormalizeObjectType(match.Groups["type"].Value);
var rawNames = match.Groups["names"].Value
.Split(',')
.Select(x => x.Trim())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
foreach (var rawName in rawNames)
{
var (schemaName, objectName) = ParseSchemaAndObjectName(rawName);
if (string.IsNullOrWhiteSpace(objectName))
continue;
var fileName = BuildSqlObjectScriptFileName(objectType, schemaName, objectName);
DeleteSqlDataFile(fileName);
}
return rawNames.Count > 0;
}
private void SaveSqlDataFile(string fileName, string content)
{
try
{
var outputPath = ResolveSqlDataOutputPath();
Directory.CreateDirectory(outputPath);
var safeFileName = string.Concat(fileName.Split(Path.GetInvalidFileNameChars()));
if (string.IsNullOrWhiteSpace(safeFileName))
return;
var filePath = Path.Combine(outputPath, $"{safeFileName}.sql");
File.WriteAllText(filePath, content ?? string.Empty);
_logger.LogInformation("SQL object script saved: {FilePath}", filePath);
}
catch (Exception ex)
{
// File save failure does not block query execution
_logger.LogError(ex, "Failed to save SQL object script: {Message}", ex.Message);
}
}
private void DeleteSqlDataFile(string fileName)
{
try
{
var outputPath = ResolveSqlDataOutputPath();
if (!Directory.Exists(outputPath))
return;
var safeFileName = string.Concat(fileName.Split(Path.GetInvalidFileNameChars()));
if (string.IsNullOrWhiteSpace(safeFileName))
return;
var filePath = Path.Combine(outputPath, $"{safeFileName}.sql");
if (!File.Exists(filePath))
return;
File.Delete(filePath);
_logger.LogInformation("SQL object script deleted: {FilePath}", filePath);
}
catch (Exception ex)
{
// File delete failure does not block query execution
_logger.LogError(ex, "Failed to delete SQL object script: {Message}", ex.Message);
}
}
private static string BuildSqlObjectScriptFileName(string objectType, string schemaName, string objectName)
{
return objectName;
}
private static string CanonicalizeSqlObjectScriptForSeed(string sqlScript)
{
if (string.IsNullOrWhiteSpace(sqlScript))
return string.Empty;
var canonical = SqlObjectHeaderCanonicalizeRegex.Replace(sqlScript, match =>
{
var prefix = match.Groups["prefix"].Value;
var after = match.Groups["after"].Value;
after = Regex.Replace(after, @"^OR\s+ALTER\s+", string.Empty, RegexOptions.IgnoreCase);
return $"{prefix}CREATE OR ALTER {after}";
}, 1);
return canonical;
}
private static string NormalizeObjectType(string rawType)
{
if (string.Equals(rawType, "PROC", StringComparison.OrdinalIgnoreCase) ||
string.Equals(rawType, "PROCEDURE", StringComparison.OrdinalIgnoreCase))
{
return "procedure";
}
if (string.Equals(rawType, "FUNCTION", StringComparison.OrdinalIgnoreCase))
{
return "function";
}
return "view";
}
private static (string SchemaName, string ObjectName) ParseSchemaAndObjectName(string fullName)
{
var parts = SplitSqlMultipartIdentifier(fullName)
.Select(UnquoteSqlIdentifier)
.Where(p => !string.IsNullOrWhiteSpace(p))
.ToList();
if (parts.Count == 0)
return ("dbo", string.Empty);
if (parts.Count == 1)
return ("dbo", parts[0]);
return (parts[^2], parts[^1]);
}
private static List<string> SplitSqlMultipartIdentifier(string value)
{
var parts = new List<string>();
if (string.IsNullOrWhiteSpace(value))
return parts;
var buffer = new System.Text.StringBuilder();
var inBracket = false;
var inDoubleQuote = false;
foreach (var ch in value)
{
if (ch == '[' && !inDoubleQuote)
{
inBracket = true;
buffer.Append(ch);
continue;
}
if (ch == ']' && inBracket)
{
inBracket = false;
buffer.Append(ch);
continue;
}
if (ch == '"' && !inBracket)
{
inDoubleQuote = !inDoubleQuote;
buffer.Append(ch);
continue;
}
if (ch == '.' && !inBracket && !inDoubleQuote)
{
var token = buffer.ToString().Trim();
if (!string.IsNullOrWhiteSpace(token))
parts.Add(token);
buffer.Clear();
continue;
}
buffer.Append(ch);
}
var lastToken = buffer.ToString().Trim();
if (!string.IsNullOrWhiteSpace(lastToken))
parts.Add(lastToken);
return parts;
}
private static string UnquoteSqlIdentifier(string value)
{
if (string.IsNullOrWhiteSpace(value))
return string.Empty;
value = value.Trim();
if (value.Length >= 2 && value[0] == '[' && value[^1] == ']')
return value.Substring(1, value.Length - 2);
if (value.Length >= 2 && value[0] == '"' && value[^1] == '"')
return value.Substring(1, value.Length - 2);
return value;
}
[HttpPost("api/app/sql-object-manager/delete-sql-data-files")] [HttpPost("api/app/sql-object-manager/delete-sql-data-files")]
public Task DeleteSqlDataFilesAsync(DeleteSqlDataFilesDto input) public Task DeleteSqlDataFilesAsync(DeleteSqlDataFilesDto input)
{ {

View file

@ -240,7 +240,7 @@ SELECT
const normalizeNativeDefinitionToCreate = (definition: string) => { const normalizeNativeDefinitionToCreate = (definition: string) => {
if (!definition?.trim()) return '' if (!definition?.trim()) return ''
return definition.replace(/^\s*ALTER\s+/i, 'CREATE ') return definition.replace(/^\s*(?:CREATE|ALTER)\s+(?:OR\s+ALTER\s+)?/i, 'CREATE OR ALTER ')
} }
const buildDropIfExistsScript = (obj: SqlExplorerSelectedObject) => { const buildDropIfExistsScript = (obj: SqlExplorerSelectedObject) => {
@ -558,7 +558,7 @@ GO`,
objectName, objectName,
) )
if (result.data) { if (result.data) {
const definition = result.data.replace(/\bCREATE\b/i, 'ALTER') const definition = normalizeNativeDefinitionToCreate(result.data)
setState((prev) => ({ setState((prev) => ({
...prev, ...prev,
editorContent: definition, editorContent: definition,