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 ExplodeQuotedList(Text: WideString; var List: TWideStringList);
|
||||||
procedure ensureValidIdentifier(name: String);
|
procedure ensureValidIdentifier(name: String);
|
||||||
function getEnumValues(str: WideString): WideString;
|
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 parsesql(sql: WideString) : TWideStringList;
|
||||||
function sstr(str: WideString; len: Integer) : WideString;
|
function sstr(str: WideString; len: Integer) : WideString;
|
||||||
function encrypt(str: String): String;
|
function encrypt(str: String): String;
|
||||||
@ -236,6 +239,7 @@ type
|
|||||||
function GetLightness(AColor: TColor): Byte;
|
function GetLightness(AColor: TColor): Byte;
|
||||||
procedure ParseTableStructure(CreateTable: WideString; Columns: TObjectList=nil; Keys: TObjectList=nil; ForeignKeys: TObjectList=nil);
|
procedure ParseTableStructure(CreateTable: WideString; Columns: TObjectList=nil; Keys: TObjectList=nil; ForeignKeys: TObjectList=nil);
|
||||||
procedure ParseViewStructure(ViewName: WideString; Columns: TObjectList);
|
procedure ParseViewStructure(ViewName: WideString; Columns: TObjectList);
|
||||||
|
function ReformatSQL(SQL: WideString): WideString;
|
||||||
|
|
||||||
var
|
var
|
||||||
MainReg : TRegistry;
|
MainReg : TRegistry;
|
||||||
@ -474,32 +478,32 @@ end;
|
|||||||
Limitations: only recognizes ANSI whitespace.
|
Limitations: only recognizes ANSI whitespace.
|
||||||
Eligible for inlining, hope the compiler does this automatically.
|
Eligible for inlining, hope the compiler does this automatically.
|
||||||
}
|
}
|
||||||
function isWhitespace(const c: WideChar): boolean;
|
function IsWhitespace(const c: WideChar): Boolean;
|
||||||
begin
|
begin
|
||||||
result :=
|
Result := (c = #9) or (c = #10) or (c = #13) or (c = #32);
|
||||||
(c = #9) or
|
|
||||||
(c = #10) or
|
|
||||||
(c = #13) or
|
|
||||||
(c = #32)
|
|
||||||
;
|
|
||||||
end;
|
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.
|
Return true if given character represents a number.
|
||||||
Limitations: only recognizes ANSI numerals.
|
Limitations: only recognizes ANSI numerals.
|
||||||
Eligible for inlining, hope the compiler does this automatically.
|
Eligible for inlining, hope the compiler does this automatically.
|
||||||
}
|
}
|
||||||
function isNumber(const c: WideChar): boolean;
|
function IsNumber(const c: WideChar): Boolean;
|
||||||
var
|
var
|
||||||
b: word;
|
b: word;
|
||||||
begin
|
begin
|
||||||
b := ord(c);
|
b := ord(c);
|
||||||
result :=
|
Result := (b >= 48) and (b <= 57);
|
||||||
(b >= 48) and
|
|
||||||
(b <= 57)
|
|
||||||
;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|
||||||
@ -608,7 +612,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
// Skip whitespace immediately if at start of sentence.
|
// 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;
|
start := start + 1;
|
||||||
if i < len then continue;
|
if i < len then continue;
|
||||||
end;
|
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
|
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:
|
// The allowed DELIMITER format is:
|
||||||
// <delimiter> <whitespace(s)> <character(s)> <whitespace(s)> <newline>
|
// <delimiter> <whitespace(s)> <character(s)> <whitespace(s)> <newline>
|
||||||
if isWhitespace(secchar) then begin
|
if IsWhitespace(secchar) then begin
|
||||||
indelimiter := true;
|
indelimiter := true;
|
||||||
i := i + 1;
|
i := i + 1;
|
||||||
if i < len then continue;
|
if i < len then continue;
|
||||||
@ -667,7 +671,7 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
if inconditional and (conditional = '') then begin
|
if inconditional and (conditional = '') then begin
|
||||||
if not isNumber(sql[i]) then begin
|
if not IsNumber(sql[i]) then begin
|
||||||
conditional := tmp;
|
conditional := tmp;
|
||||||
// note:
|
// note:
|
||||||
// we do not trim the start of the SQL inside conditional
|
// we do not trim the start of the SQL inside conditional
|
||||||
@ -3047,6 +3051,104 @@ begin
|
|||||||
end;
|
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 }
|
{ *** TTableColumn }
|
||||||
|
|
||||||
|
@ -252,7 +252,7 @@ object MainForm: TMainForm
|
|||||||
object ToolBarQuery: TToolBar
|
object ToolBarQuery: TToolBar
|
||||||
Left = 494
|
Left = 494
|
||||||
Top = 2
|
Top = 2
|
||||||
Width = 222
|
Width = 245
|
||||||
Height = 22
|
Height = 22
|
||||||
Align = alNone
|
Align = alNone
|
||||||
AutoSize = True
|
AutoSize = True
|
||||||
@ -294,18 +294,23 @@ object MainForm: TMainForm
|
|||||||
Top = 0
|
Top = 0
|
||||||
Action = actQueryReplace
|
Action = actQueryReplace
|
||||||
end
|
end
|
||||||
object btnStopOnErrors: TToolButton
|
object btnReformatSQL: TToolButton
|
||||||
Left = 153
|
Left = 153
|
||||||
Top = 0
|
Top = 0
|
||||||
|
Action = actReformatSQL
|
||||||
|
end
|
||||||
|
object btnStopOnErrors: TToolButton
|
||||||
|
Left = 176
|
||||||
|
Top = 0
|
||||||
Action = actQueryStopOnErrors
|
Action = actQueryStopOnErrors
|
||||||
end
|
end
|
||||||
object btnQueryWordwrap: TToolButton
|
object btnQueryWordwrap: TToolButton
|
||||||
Left = 176
|
Left = 199
|
||||||
Top = 0
|
Top = 0
|
||||||
Action = actQueryWordWrap
|
Action = actQueryWordWrap
|
||||||
end
|
end
|
||||||
object btnSetDelimiter: TToolButton
|
object btnSetDelimiter: TToolButton
|
||||||
Left = 199
|
Left = 222
|
||||||
Top = 0
|
Top = 0
|
||||||
Action = actSetDelimiter
|
Action = actSetDelimiter
|
||||||
end
|
end
|
||||||
@ -2273,6 +2278,12 @@ object MainForm: TMainForm
|
|||||||
ShortCut = 32851
|
ShortCut = 32851
|
||||||
OnExecute = actDataResetSortingExecute
|
OnExecute = actDataResetSortingExecute
|
||||||
end
|
end
|
||||||
|
object actReformatSQL: TAction
|
||||||
|
Category = 'SQL'
|
||||||
|
Caption = 'Reformat SQL'
|
||||||
|
ImageIndex = 140
|
||||||
|
OnExecute = actReformatSQLExecute
|
||||||
|
end
|
||||||
end
|
end
|
||||||
object SaveDialog2: TSaveDialog
|
object SaveDialog2: TSaveDialog
|
||||||
DefaultExt = 'reg'
|
DefaultExt = 'reg'
|
||||||
@ -6150,6 +6161,22 @@ object MainForm: TMainForm
|
|||||||
F61B7C1AB23D0419EE3D0000000049454E44AE426082}
|
F61B7C1AB23D0419EE3D0000000049454E44AE426082}
|
||||||
Name = 'PngImage139'
|
Name = 'PngImage139'
|
||||||
Background = clWindow
|
Background = clWindow
|
||||||
|
end
|
||||||
|
item
|
||||||
|
PngImage.Data = {
|
||||||
|
89504E470D0A1A0A0000000D49484452000000100000001008060000001FF3FF
|
||||||
|
610000001974455874536F6674776172650041646F626520496D616765526561
|
||||||
|
647971C9653C000000F04944415478DA63FCFFFF3F03258071F018505656F6FF
|
||||||
|
DFBF7F0C7FFEFC81E3DFBF7FC3F1AF5FBFE03408EFDBB78F91BA2EE8D8FCEE4A
|
||||||
|
85AF900E4C222626E63AB28DE8F8ECD9B39A280634AC7B7DE5C5E79F8CB76E7F
|
||||||
|
0DD9D7A27E3D2C2CEC072ECD207CE5CA150EB801CD9B5EFC17E16663F8F4FD37
|
||||||
|
C3E93B5FCF3CBCF525EEF434BDEB2479217BD18B2BAFDE7F67BA7BF553F0B959
|
||||||
|
FAD73D3C3CAEC06CFBF9F327DC6698B71E3D7AA4836240E2EC6757E6A74AC1C3
|
||||||
|
C0C1C101C30BC8B1F1E2C50B0E1403288E052F2FAFFFF8020D961E60E9E3F3E7
|
||||||
|
CF544E07363636FFB10516B2CD1F3E7C60C46900C52E2017000067F426F037CF
|
||||||
|
17CF0000000049454E44AE426082}
|
||||||
|
Name = 'PngImage140'
|
||||||
|
Background = clWindow
|
||||||
end>
|
end>
|
||||||
PngOptions = [pngBlendOnDisabled, pngGrayscaleOnDisabled]
|
PngOptions = [pngBlendOnDisabled, pngGrayscaleOnDisabled]
|
||||||
Left = 104
|
Left = 104
|
||||||
@ -6711,6 +6738,9 @@ object MainForm: TMainForm
|
|||||||
object MenuRunLine: TMenuItem
|
object MenuRunLine: TMenuItem
|
||||||
Action = actExecuteLine
|
Action = actExecuteLine
|
||||||
end
|
end
|
||||||
|
object ReformatSQL1: TMenuItem
|
||||||
|
Action = actReformatSQL
|
||||||
|
end
|
||||||
object MenuItem1: TMenuItem
|
object MenuItem1: TMenuItem
|
||||||
Caption = '-'
|
Caption = '-'
|
||||||
end
|
end
|
||||||
|
@ -448,6 +448,9 @@ type
|
|||||||
Inverseselection1: TMenuItem;
|
Inverseselection1: TMenuItem;
|
||||||
actDataResetSorting: TAction;
|
actDataResetSorting: TAction;
|
||||||
Resetsorting1: TMenuItem;
|
Resetsorting1: TMenuItem;
|
||||||
|
actReformatSQL: TAction;
|
||||||
|
ReformatSQL1: TMenuItem;
|
||||||
|
btnReformatSQL: TToolButton;
|
||||||
procedure refreshMonitorConfig;
|
procedure refreshMonitorConfig;
|
||||||
procedure loadWindowConfig;
|
procedure loadWindowConfig;
|
||||||
procedure saveWindowConfig;
|
procedure saveWindowConfig;
|
||||||
@ -708,6 +711,7 @@ type
|
|||||||
procedure FormMouseWheel(Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint;
|
procedure FormMouseWheel(Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint;
|
||||||
var Handled: Boolean);
|
var Handled: Boolean);
|
||||||
procedure actDataResetSortingExecute(Sender: TObject);
|
procedure actDataResetSortingExecute(Sender: TObject);
|
||||||
|
procedure actReformatSQLExecute(Sender: TObject);
|
||||||
private
|
private
|
||||||
ReachedEOT : Boolean;
|
ReachedEOT : Boolean;
|
||||||
FDelimiter: String;
|
FDelimiter: String;
|
||||||
@ -8747,6 +8751,44 @@ begin
|
|||||||
end;
|
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);
|
procedure TMainForm.PageControlMainContextPopup(Sender: TObject; MousePos: TPoint; var Handled: Boolean);
|
||||||
var
|
var
|
||||||
ClickPoint: TPoint;
|
ClickPoint: TPoint;
|
||||||
|
Reference in New Issue
Block a user