unit reformatter; {$mode delphi}{$H+} interface uses SysUtils, Variants, Classes, Math, Graphics, fpjson, jsonparser, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, fphttpclient, apphelpers, extra_controls, dbconnection, dbstructures, dbstructures.mysql; type TfrmReformatter = class(TExtForm) grpReformatter: TRadioGroup; btnCancel: TButton; btnOk: TButton; lblFormatProviderLink: TLabel; chkKeepAsking: TCheckBox; procedure btnOkClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure grpReformatterClick(Sender: TObject); procedure lblFormatProviderLinkClick(Sender: TObject); private { Private declarations } FInputCode, FOutputCode: String; public { Public declarations } function FormatSqlInternal(SQL: String): String; function FormatSqlOnlineHeidisql(SQL: String): String; function FormatSqlOnlineSqlformatOrg(SQL: String): String; property InputCode: String read FInputCode write FInputCode; property OutputCode: String read FOutputCode; end; var frmReformatter: TfrmReformatter; implementation uses main; {$R *.lfm} procedure TfrmReformatter.btnOkClick(Sender: TObject); var StartTime: UInt64; TimeElapsed: Double; begin Screen.Cursor := crHourGlass; try StartTime := GetTickCount64; case grpReformatter.ItemIndex of 0: begin // Internal FOutputCode := FormatSqlInternal(FInputCode); end; 1: begin // Online FOutputCode := FormatSqlOnlineHeidisql(FInputCode); end; 2: begin // sqlformat.org FOutputCode := FormatSqlOnlineSqlformatOrg(FInputCode); end; end; // Unify line breaks, so selection end will be correct: FOutputCode := fixNewlines(FOutputCode); TimeElapsed := GetTickCount64 - StartTime; MainForm.LogSQL(f_('Code reformatted in %s, using formatter %s', [FormatTimeNumber(TimeElapsed/1000, True, 3), '#'+grpReformatter.ItemIndex.ToString])); except on E:Exception do begin ErrorDialog(E.ClassName + ': ' + E.Message); ModalResult := mrNone; end; end; if not chkKeepAsking.Checked then begin // No dialog next time please AppSettings.WriteInt(asReformatterNoDialog, grpReformatter.ItemIndex+1); end; Screen.Cursor := crDefault; end; procedure TfrmReformatter.FormCreate(Sender: TObject); begin grpReformatter.Items.Clear; grpReformatter.Items.Add(_('Internal')); grpReformatter.Items.Add(f_('Online on %s', [APPDOMAIN])); grpReformatter.Items.Add(f_('Online on %s', ['sqlformat.org'])); if AppSettings.ReadInt(asReformatterNoDialog) = 0 then begin grpReformatter.ItemIndex := AppSettings.ReadInt(asReformatter); end else begin // asReformatterNoDialog has the same items with an additional "always ask" item at index 0 grpReformatter.ItemIndex := AppSettings.ReadInt(asReformatterNoDialog) - 1; end; lblFormatProviderLink.Font.Style := lblFormatProviderLink.Font.Style + [fsUnderline]; end; procedure TfrmReformatter.FormDestroy(Sender: TObject); begin AppSettings.WriteInt(asReformatter, grpReformatter.ItemIndex); end; procedure TfrmReformatter.grpReformatterClick(Sender: TObject); begin case grpReformatter.ItemIndex of 0: lblFormatProviderLink.Visible := False; 1: begin lblFormatProviderLink.Caption := 'github.com/doctrine/sql-formatter (Jeremy Dorn)'; lblFormatProviderLink.Hint := 'https://github.com/doctrine/sql-formatter'; lblFormatProviderLink.Visible := True; end; 2: begin lblFormatProviderLink.Caption := 'SQLFormat.org (Andi Albrecht)'; lblFormatProviderLink.Hint := 'https://sqlformat.org/'; lblFormatProviderLink.Visible := True; end; end; end; procedure TfrmReformatter.lblFormatProviderLinkClick(Sender: TObject); begin apphelpers.ShellExec(TLabel(Sender).Hint); end; function TfrmReformatter.FormatSqlInternal(SQL: String): String; var Conn: TDBConnection; SQLFunc: TSQLFunction; AllKeywords, ImportantKeywords, PairKeywords: TStringList; i, Run, KeywordMaxLen: Integer; IsEsc, IsQuote, InComment, InBigComment, InString, InKeyword, InIdent, LastWasComment: Boolean; c, p: Char; Keyword, PreviousKeyword, TestPair: String; Datatypes: TDBDataTypeArray; const WordChars = ['a'..'z', 'A'..'Z', '0'..'9', '_', '.']; WhiteSpaces = [#9, #10, #13, #32]; begin Conn := MainForm.ActiveConnection; // Known SQL keywords, get converted to UPPERCASE AllKeywords := TStringList.Create; AllKeywords.Text := MySQLKeywords.Text; for SQLFunc in Conn.SQLFunctions do begin // Leave out operator functions like ">>", and the "X()" function so hex values don't get touched if (SQLFunc.Declaration <> '') and (SQLFunc.Name <> 'X') then AllKeywords.Add(SQLFunc.Name); end; Datatypes := Conn.Datatypes; for i:=Low(Datatypes) to High(Datatypes) do AllKeywords.Add(Datatypes[i].Name); KeywordMaxLen := 0; for i:=0 to AllKeywords.Count-1 do KeywordMaxLen := Max(KeywordMaxLen, Length(AllKeywords[i])); // A subset of the above list, each of them will get a linebreak left to it ImportantKeywords := Explode(',', 'SELECT,FROM,LEFT,RIGHT,STRAIGHT,NATURAL,INNER,JOIN,WHERE,GROUP,ORDER,HAVING,LIMIT,CREATE,DROP,UPDATE,INSERT,REPLACE,TRUNCATE,DELETE'); // Keywords which followers should not get separated into a new line PairKeywords := Explode(',', 'LEFT,RIGHT,STRAIGHT,NATURAL,INNER,ORDER,GROUP'); IsEsc := False; InComment := False; InBigComment := False; LastWasComment := False; InString := False; InIdent := False; Run := 1; Result := ''; SQL := SQL + ' '; SetLength(Result, Length(SQL)*2); Keyword := ''; PreviousKeyword := ''; for i:=1 to Length(SQL) do begin c := SQL[i]; // Current char if i > 1 then p := SQL[i-1] else p := #0; // Previous char // Detection logic - where are we? if c = '\' then IsEsc := not IsEsc else IsEsc := False; IsQuote := (c = '''') or (c = '"'); if c = '`' then InIdent := not InIdent; if (not IsEsc) and IsQuote then InString := not InString; if (c = '#') or ((c = '-') and (p = '-')) then InComment := True; if ((c = #10) or (c = #13)) and InComment then begin LastWasComment := True; InComment := False; end; if (c = '*') and (p = '/') and (not InComment) and (not InString) then InBigComment := True; if (c = '/') and (p = '*') and (not InComment) and (not InString) then InBigComment := False; InKeyword := (not InComment) and (not InBigComment) and (not InString) and (not InIdent) and CharInSet(c, WordChars); // Creation of returning text if InKeyword then begin Keyword := Keyword + c; end else begin if Keyword <> '' then begin if AllKeywords.IndexOf(KeyWord) > -1 then begin while (Run > 1) and CharInSet(Result[Run-1], WhiteSpaces) do Dec(Run); Keyword := UpperCase(Keyword); if Run > 1 then begin // SELECT, WHERE, JOIN etc. get a new line, but don't separate LEFT JOIN with linebreaks if LastWasComment or ((ImportantKeywords.IndexOf(Keyword) > -1) and (PairKeywords.IndexOf(PreviousKeyword) = -1)) then Keyword := CRLF + Keyword else if (Result[Run-1] <> '(') then Keyword := ' ' + Keyword; end; LastWasComment := False; end; PreviousKeyword := Trim(Keyword); Insert(Keyword, Result, Run); Inc(Run, Length(Keyword)); Keyword := ''; end; if (not InComment) and (not InBigComment) and (not InString) and (not InIdent) then begin TestPair := Result[Run-1] + c; if (TestPair = ' ') or (TestPair = '( ') then begin c := Result[Run-1]; Dec(Run); end; if (TestPair = ' )') or (TestPair = ' ,') then Dec(Run); end; Result[Run] := c; Inc(Run); end; end; // Cut overlength SetLength(Result, Run-2); end; function TfrmReformatter.FormatSqlOnlineHeidisql(SQL: String): String; var HttpReq: THttpDownload; Parameters, Response: TStringList; begin HttpReq := THttpDownload.Create(Self); HttpReq.RequestHeaders.Add('Content-Type: text/plain; charset=utf-8'); Parameters := TStringList.Create; Parameters.AddPair('indent', CodeIndent); Parameters.AddPair('input', FInputCode); Response := TStringList.Create; HttpReq.FormPost(APPDOMAIN + 'sql-formatter.php', Parameters, Response); if Response.Count = 0 then raise Exception.Create(_('Empty result from online reformatter')); Result := Response.Text; HttpReq.Free; end; function TfrmReformatter.FormatSqlOnlineSqlformatOrg(SQL: String): String; var HttpReq: THttpDownload; Parameters, Response: TStringList; JsonResponseStr: String; JsonParser: TJSONParser; JsonTmp: TJSONData; begin HttpReq := THttpDownload.Create(Self); HttpReq.RequestHeaders.Add('Content-Type: application/json; charset=utf-8'); // Parameter documentation: https://sqlformat.org/api/ Parameters := TStringList.Create; Parameters.AddPair('sql', FInputCode); Parameters.AddPair('reindent', '1'); if AppSettings.ReadBool(asTabsToSpaces) then Parameters.AddPair('indent_width', AppSettings.ReadInt(asTabWidth).ToString) else Parameters.AddPair('indent_width', '2'); Parameters.AddPair('keyword_case', 'upper'); Response := TStringList.Create; HttpReq.FormPost('https://sqlformat.org/api/v1/format', Parameters, Response); JsonResponseStr := Response.Text; if JsonResponseStr.IsEmpty then raise Exception.Create(_('Empty result from online reformatter')); JsonParser := TJSONParser.Create(JsonResponseStr); JsonTmp := JsonParser.Parse; Result := JsonTmp.GetPath('result').AsString; JsonTmp.Free; JsonParser.Free; HttpReq.Free; end; end.