Implement SQL pretty formatter, fixes issue #428.

This commit is contained in:
Ansgar Becker
2009-12-30 01:23:44 +00:00
parent 51cdaa9995
commit c61b2ead2e
4 changed files with 193 additions and 19 deletions

BIN
res/icons/text_indent.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

View File

@ -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 }

View File

@ -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

View File

@ -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;