using OpenDBDiff.Abstractions.Schema.Model; using System; using System.Text.RegularExpressions; using TSQL; using TSQL.Tokens; namespace OpenDBDiff.SqlServer.Schema.Model.Util { internal static class FormatCode { private static readonly Regex RegCreateAlter = new Regex("CREATE", RegexOptions.Compiled); private static readonly Regex SchemaBindingRegex = new Regex("WITH SCHEMABINDING", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly char[] TrimCharacters = { ' ', '\r', '\n', '\t' }; /// /// Find the first entry with the full name within a function, store, view, trigger or rule. /// Ignore comments. /// public static SearchItem FindAndNormalizeCreate(ISchemaBase item, string prevText) { var result = FindCreate(prevText) ?? throw new InvalidOperationException($"Could not find the CREATE statement for object '{item.Name}'"); // normalize the object name for better comparison var body = prevText.Substring(0, result.TypeEndPosition + 1) + " " + item.FullName + prevText.Substring(result.NameEndPosition + 1); return new SearchItem { Body = body, FindPosition = result.CreateBeginPosition }; } public static FindCreateStatementResult FindCreate(string body) { var tokenizer = new TSQLTokenizer(body); tokenizer.MoveNext(); while (tokenizer.Current != null && tokenizer.Current.AsKeyword?.Keyword != TSQLKeywords.CREATE) { tokenizer.MoveNext(); } if (tokenizer.Current == null) // Oops, we reached the end of the file and did not find the CREATE! return null; var createToken = tokenizer.Current.AsKeyword; tokenizer.MoveNext(); var typeKeyword = tokenizer.Current.AsKeyword; tokenizer.MoveNext(); // the object owner is optional var token0 = tokenizer.Current; tokenizer.MoveNext(); var token1 = tokenizer.Current; tokenizer.MoveNext(); var token2 = tokenizer.Current; tokenizer.MoveNext(); TSQLIdentifier entityNameToken; if (token1.AsCharacter?.Text == ".") { entityNameToken = token2.AsIdentifier; } else { entityNameToken = token0.AsIdentifier; } return new FindCreateStatementResult { CreateBeginPosition = createToken.BeginPosition, TypeEndPosition = typeKeyword.EndPosition, NameEndPosition = entityNameToken.EndPosition }; } public static string FormatAlter(string ObjectType, string body, ISchemaBase item, Boolean quitSchemaBinding) { string prevText = null; try { prevText = (string)body.Clone(); SearchItem sitem = FindAndNormalizeCreate(item, prevText); if (!quitSchemaBinding) return RegCreateAlter.Replace(sitem.Body, "ALTER", 1, sitem.FindPosition); //return prevText.Substring(0, iFind) + "ALTER " + sitem.ObjectType + " " + prevText.Substring(iFind + sitem.ObjectType.Length + 7, prevText.Length - (iFind + sitem.ObjectType.Length + 7)).TrimStart(); else { string text = RegCreateAlter.Replace(sitem.Body, "ALTER", 1, sitem.FindPosition); return SchemaBindingRegex.Replace(text, ""); } //return ""; } catch { return prevText; } } public static string FormatCreate(string ObjectType, string body, ISchemaBase item) { try { string prevText = (string)body.Clone(); prevText = FindAndNormalizeCreate(item, prevText).Body; if (String.IsNullOrEmpty(prevText)) prevText = body; prevText = CleanLast(prevText); return SmartGO(prevText); } catch { return SmartGO(CleanLast(body)); } } /// /// Clears all unnecessary characters that are after the END statement of the body, and whitespaces at the beginning. /// private static string CleanLast(string body) { if (string.IsNullOrEmpty(body)) { return string.Empty; } return body.TrimStart().TrimEnd(TrimCharacters); } /// /// Ensure statement ends with a GO statement /// private static string SmartGO(string code) { string prevText = code; try { if (!prevText.EndsWith("\r\n")) prevText += "\r\n"; return prevText + "GO\r\n"; } catch { return prevText; } } internal struct SearchItem { public string Body; public int FindPosition; } internal class FindCreateStatementResult { public int CreateBeginPosition; public int NameEndPosition; public int TypeEndPosition; } } }