mirror of
https://github.com/HeidiSQL/HeidiSQL.git
synced 2025-08-06 18:24:26 +08:00
Implement SQL pretty formatter, fixes issue #428.
This commit is contained in:
BIN
res/icons/text_indent.png
Normal file
BIN
res/icons/text_indent.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 353 B |
@ -160,6 +160,9 @@ type
|
||||
procedure ExplodeQuotedList(Text: WideString; var List: TWideStringList);
|
||||
procedure ensureValidIdentifier(name: String);
|
||||
function getEnumValues(str: WideString): WideString;
|
||||
function IsWhitespace(const c: WideChar): Boolean;
|
||||
function IsLetter(const c: WideChar): Boolean;
|
||||
function IsNumber(const c: WideChar): Boolean;
|
||||
function parsesql(sql: WideString) : TWideStringList;
|
||||
function sstr(str: WideString; len: Integer) : WideString;
|
||||
function encrypt(str: String): String;
|
||||
@ -236,6 +239,7 @@ type
|
||||
function GetLightness(AColor: TColor): Byte;
|
||||
procedure ParseTableStructure(CreateTable: WideString; Columns: TObjectList=nil; Keys: TObjectList=nil; ForeignKeys: TObjectList=nil);
|
||||
procedure ParseViewStructure(ViewName: WideString; Columns: TObjectList);
|
||||
function ReformatSQL(SQL: WideString): WideString;
|
||||
|
||||
var
|
||||
MainReg : TRegistry;
|
||||
@ -474,32 +478,32 @@ end;
|
||||
Limitations: only recognizes ANSI whitespace.
|
||||
Eligible for inlining, hope the compiler does this automatically.
|
||||
}
|
||||
function isWhitespace(const c: WideChar): boolean;
|
||||
function IsWhitespace(const c: WideChar): Boolean;
|
||||
begin
|
||||
result :=
|
||||
(c = #9) or
|
||||
(c = #10) or
|
||||
(c = #13) or
|
||||
(c = #32)
|
||||
;
|
||||
Result := (c = #9) or (c = #10) or (c = #13) or (c = #32);
|
||||
end;
|
||||
|
||||
|
||||
function IsLetter(const c: WideChar): Boolean;
|
||||
var
|
||||
o: Integer;
|
||||
begin
|
||||
o := Ord(c);
|
||||
Result := ((o >= 65) and (o <= 90)) or ((o >= 97) and (o <= 122));
|
||||
end;
|
||||
|
||||
|
||||
{***
|
||||
Return true if given character represents a number.
|
||||
Limitations: only recognizes ANSI numerals.
|
||||
Eligible for inlining, hope the compiler does this automatically.
|
||||
}
|
||||
function isNumber(const c: WideChar): boolean;
|
||||
function IsNumber(const c: WideChar): Boolean;
|
||||
var
|
||||
b: word;
|
||||
begin
|
||||
b := ord(c);
|
||||
result :=
|
||||
(b >= 48) and
|
||||
(b <= 57)
|
||||
;
|
||||
Result := (b >= 48) and (b <= 57);
|
||||
end;
|
||||
|
||||
|
||||
@ -608,7 +612,7 @@ begin
|
||||
end;
|
||||
|
||||
// Skip whitespace immediately if at start of sentence.
|
||||
if (start = i) and isWhitespace(sql[i]) then begin
|
||||
if (start = i) and IsWhitespace(sql[i]) then begin
|
||||
start := start + 1;
|
||||
if i < len then continue;
|
||||
end;
|
||||
@ -635,7 +639,7 @@ begin
|
||||
if (not instring) and (not incomment) and (not inconditional) and (not indelimiter) and (start + 8 = i) and scanReverse(sql, i, 'delimiter', 9, true) then begin
|
||||
// The allowed DELIMITER format is:
|
||||
// <delimiter> <whitespace(s)> <character(s)> <whitespace(s)> <newline>
|
||||
if isWhitespace(secchar) then begin
|
||||
if IsWhitespace(secchar) then begin
|
||||
indelimiter := true;
|
||||
i := i + 1;
|
||||
if i < len then continue;
|
||||
@ -667,7 +671,7 @@ begin
|
||||
end;
|
||||
|
||||
if inconditional and (conditional = '') then begin
|
||||
if not isNumber(sql[i]) then begin
|
||||
if not IsNumber(sql[i]) then begin
|
||||
conditional := tmp;
|
||||
// note:
|
||||
// we do not trim the start of the SQL inside conditional
|
||||
@ -3047,6 +3051,104 @@ begin
|
||||
end;
|
||||
|
||||
|
||||
function ReformatSQL(SQL: WideString): WideString;
|
||||
var
|
||||
AllKeywords, ImportantKeywords: TWideStringlist;
|
||||
i, Run, KeywordMaxLen: Integer;
|
||||
IsEsc, IsQuote, InComment, InBigComment, InString, InKeyword, InIdent, LastWasComment: Boolean;
|
||||
c, p: WideChar;
|
||||
Keyword, PreviousKeyword, TestPair: WideString;
|
||||
begin
|
||||
// Known SQL keywords, get converted to UPPERCASE
|
||||
AllKeywords := TWideStringlist.Create;
|
||||
AllKeywords.Text := MySQLKeywords.Text;
|
||||
for i:=Low(MySQLFunctions) to High(MySQLFunctions) do begin
|
||||
if MySQLFunctions[i].Declaration <> '' then
|
||||
AllKeywords.Add(MySQLFunctions[i].Name);
|
||||
end;
|
||||
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');
|
||||
|
||||
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 SQL[i] = '`' 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 IsLetter(c);
|
||||
|
||||
// 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 IsWhitespace(Result[Run-1]) 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 (ImportantKeywords.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;
|
||||
|
||||
|
||||
|
||||
{ *** TTableColumn }
|
||||
|
||||
|
@ -252,7 +252,7 @@ object MainForm: TMainForm
|
||||
object ToolBarQuery: TToolBar
|
||||
Left = 494
|
||||
Top = 2
|
||||
Width = 222
|
||||
Width = 245
|
||||
Height = 22
|
||||
Align = alNone
|
||||
AutoSize = True
|
||||
@ -294,18 +294,23 @@ object MainForm: TMainForm
|
||||
Top = 0
|
||||
Action = actQueryReplace
|
||||
end
|
||||
object btnStopOnErrors: TToolButton
|
||||
object btnReformatSQL: TToolButton
|
||||
Left = 153
|
||||
Top = 0
|
||||
Action = actReformatSQL
|
||||
end
|
||||
object btnStopOnErrors: TToolButton
|
||||
Left = 176
|
||||
Top = 0
|
||||
Action = actQueryStopOnErrors
|
||||
end
|
||||
object btnQueryWordwrap: TToolButton
|
||||
Left = 176
|
||||
Left = 199
|
||||
Top = 0
|
||||
Action = actQueryWordWrap
|
||||
end
|
||||
object btnSetDelimiter: TToolButton
|
||||
Left = 199
|
||||
Left = 222
|
||||
Top = 0
|
||||
Action = actSetDelimiter
|
||||
end
|
||||
@ -2273,6 +2278,12 @@ object MainForm: TMainForm
|
||||
ShortCut = 32851
|
||||
OnExecute = actDataResetSortingExecute
|
||||
end
|
||||
object actReformatSQL: TAction
|
||||
Category = 'SQL'
|
||||
Caption = 'Reformat SQL'
|
||||
ImageIndex = 140
|
||||
OnExecute = actReformatSQLExecute
|
||||
end
|
||||
end
|
||||
object SaveDialog2: TSaveDialog
|
||||
DefaultExt = 'reg'
|
||||
@ -6150,6 +6161,22 @@ object MainForm: TMainForm
|
||||
F61B7C1AB23D0419EE3D0000000049454E44AE426082}
|
||||
Name = 'PngImage139'
|
||||
Background = clWindow
|
||||
end
|
||||
item
|
||||
PngImage.Data = {
|
||||
89504E470D0A1A0A0000000D49484452000000100000001008060000001FF3FF
|
||||
610000001974455874536F6674776172650041646F626520496D616765526561
|
||||
647971C9653C000000F04944415478DA63FCFFFF3F03258071F018505656F6FF
|
||||
DFBF7F0C7FFEFC81E3DFBF7FC3F1AF5FBFE03408EFDBB78F91BA2EE8D8FCEE4A
|
||||
85AF900E4C222626E63AB28DE8F8ECD9B39A280634AC7B7DE5C5E79F8CB76E7F
|
||||
0DD9D7A27E3D2C2CEC072ECD207CE5CA150EB801CD9B5EFC17E16663F8F4FD37
|
||||
C3E93B5FCF3CBCF525EEF434BDEB2479217BD18B2BAFDE7F67BA7BF553F0B959
|
||||
FAD73D3C3CAEC06CFBF9F327DC6698B71E3D7AA4836240E2EC6757E6A74AC1C3
|
||||
C0C1C101C30BC8B1F1E2C50B0E1403288E052F2FAFFFF8020D961E60E9E3F3E7
|
||||
CF544E07363636FFB10516B2CD1F3E7C60C46900C52E2017000067F426F037CF
|
||||
17CF0000000049454E44AE426082}
|
||||
Name = 'PngImage140'
|
||||
Background = clWindow
|
||||
end>
|
||||
PngOptions = [pngBlendOnDisabled, pngGrayscaleOnDisabled]
|
||||
Left = 104
|
||||
@ -6711,6 +6738,9 @@ object MainForm: TMainForm
|
||||
object MenuRunLine: TMenuItem
|
||||
Action = actExecuteLine
|
||||
end
|
||||
object ReformatSQL1: TMenuItem
|
||||
Action = actReformatSQL
|
||||
end
|
||||
object MenuItem1: TMenuItem
|
||||
Caption = '-'
|
||||
end
|
||||
|
@ -448,6 +448,9 @@ type
|
||||
Inverseselection1: TMenuItem;
|
||||
actDataResetSorting: TAction;
|
||||
Resetsorting1: TMenuItem;
|
||||
actReformatSQL: TAction;
|
||||
ReformatSQL1: TMenuItem;
|
||||
btnReformatSQL: TToolButton;
|
||||
procedure refreshMonitorConfig;
|
||||
procedure loadWindowConfig;
|
||||
procedure saveWindowConfig;
|
||||
@ -708,6 +711,7 @@ type
|
||||
procedure FormMouseWheel(Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint;
|
||||
var Handled: Boolean);
|
||||
procedure actDataResetSortingExecute(Sender: TObject);
|
||||
procedure actReformatSQLExecute(Sender: TObject);
|
||||
private
|
||||
ReachedEOT : Boolean;
|
||||
FDelimiter: String;
|
||||
@ -8747,6 +8751,44 @@ begin
|
||||
end;
|
||||
|
||||
|
||||
procedure TMainForm.actReformatSQLExecute(Sender: TObject);
|
||||
var
|
||||
m: TSynMemo;
|
||||
CursorPosStart, CursorPosEnd: Integer;
|
||||
NewSQL: WideString;
|
||||
Comp: TComponent;
|
||||
begin
|
||||
// Reformat SQL query
|
||||
Comp := (Sender as TAction).ActionComponent;
|
||||
if Comp is TMenuItem then
|
||||
m := TPopupMenu((Comp as TMenuItem).GetParentMenu).PopupComponent as TSynMemo
|
||||
else if QueryTabActive then
|
||||
m := ActiveQueryMemo
|
||||
else begin
|
||||
MessageDlg('Please select a query editor tab first.', mtError, [mbOK], 0);
|
||||
Exit;
|
||||
end;
|
||||
CursorPosStart := m.SelStart;
|
||||
CursorPosEnd := m.SelEnd;
|
||||
if not m.SelAvail then
|
||||
m.SelectAll;
|
||||
NewSQL := m.SelText;
|
||||
if Length(NewSQL) = 0 then
|
||||
MessageDlg('Cannot reformat anything - your editor is empty.', mtError, [mbOK], 0)
|
||||
else begin
|
||||
Screen.Cursor := crHourglass;
|
||||
m.UndoList.AddGroupBreak;
|
||||
NewSQL := ReformatSQL(NewSQL);
|
||||
m.SelText := NewSQL;
|
||||
m.SelStart := CursorPosStart;
|
||||
if CursorPosEnd > CursorPosStart then
|
||||
m.SelEnd := CursorPosStart + Length(NewSQL);
|
||||
m.UndoList.AddGroupBreak;
|
||||
Screen.Cursor := crDefault;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TMainForm.PageControlMainContextPopup(Sender: TObject; MousePos: TPoint; var Handled: Boolean);
|
||||
var
|
||||
ClickPoint: TPoint;
|
||||
|
Reference in New Issue
Block a user