unit reformatter;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, System.Math, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, IdHTTP, IdSSLOpenSSL, System.JSON,
apphelpers, extra_controls, gnugettext, dbconnection, dbstructures, dbstructures.mysql;
type
TfrmReformatter = class(TExtForm)
grpReformatter: TRadioGroup;
btnCancel: TButton;
btnOk: TButton;
lblFormatProviderLink: TLinkLabel;
chkKeepAsking: TCheckBox;
procedure btnOkClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure grpReformatterClick(Sender: TObject);
procedure lblFormatProviderLinkLinkClick(Sender: TObject;
const Link: string; LinkType: TSysLinkType);
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 *.dfm}
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:EIdHTTPProtocolException do begin
ErrorDialog(E.Message + sLineBreak + sLineBreak + E.ErrorMessage);
ModalResult := mrNone;
end;
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;
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.Visible := True;
end;
2: begin
lblFormatProviderLink.Caption := 'SQLFormat.org (Andi Albrecht)';
lblFormatProviderLink.Visible := True;
end;
end;
end;
procedure TfrmReformatter.lblFormatProviderLinkLinkClick(Sender: TObject;
const Link: string; LinkType: TSysLinkType);
begin
apphelpers.ShellExec(Link);
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: TIdHTTP;
SSLio: TIdSSLIOHandlerSocketOpenSSL;
Parameters: TStringList;
begin
HttpReq := TIdHTTP.Create;
SSLio := TIdSSLIOHandlerSocketOpenSSL.Create;
HttpReq.IOHandler := SSLio;
SSLio.SSLOptions.SSLVersions := [sslvTLSv1_1, sslvTLSv1_2];
//HttpReq.Request.ContentType := 'application/json';
HttpReq.Request.CharSet := 'utf-8';
HttpReq.Request.UserAgent := apphelpers.UserAgent(Self);
Parameters := TStringList.Create;
Parameters.AddPair('indent', CodeIndent);
Parameters.AddPair('input', FInputCode);
Result := HttpReq.Post(APPDOMAIN + 'sql-formatter.php', Parameters);
if Result.IsEmpty then
raise Exception.Create(_('Empty result from online reformatter'));
HttpReq.Free;
end;
function TfrmReformatter.FormatSqlOnlineSqlformatOrg(SQL: String): String;
var
HttpReq: TIdHTTP;
SSLio: TIdSSLIOHandlerSocketOpenSSL;
Parameters: TStringList;
JsonResponseStr: String;
JsonTmp: TJSONValue;
begin
HttpReq := TIdHTTP.Create;
SSLio := TIdSSLIOHandlerSocketOpenSSL.Create;
HttpReq.IOHandler := SSLio;
SSLio.SSLOptions.SSLVersions := [sslvTLSv1_1, sslvTLSv1_2];
HttpReq.Request.CharSet := 'utf-8';
HttpReq.Request.UserAgent := apphelpers.UserAgent(Self);
// 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');
JsonResponseStr := HttpReq.Post('https://sqlformat.org/api/v1/format', Parameters);
if JsonResponseStr.IsEmpty then
raise Exception.Create(_('Empty result from online reformatter'));
JsonTmp := TJSONObject.ParseJSONValue(JsonResponseStr);
Result := JsonTmp.FindValue('result').Value;
JsonTmp.Free;
HttpReq.Free;
end;
end.