From c61b2ead2e09674ff014aac2b523c0b8e7718cd6 Mon Sep 17 00:00:00 2001 From: Ansgar Becker Date: Wed, 30 Dec 2009 01:23:44 +0000 Subject: [PATCH] Implement SQL pretty formatter, fixes issue #428. --- res/icons/text_indent.png | Bin 0 -> 353 bytes source/helpers.pas | 132 +++++++++++++++++++++++++++++++++----- source/main.dfm | 38 +++++++++-- source/main.pas | 42 ++++++++++++ 4 files changed, 193 insertions(+), 19 deletions(-) create mode 100644 res/icons/text_indent.png diff --git a/res/icons/text_indent.png b/res/icons/text_indent.png new file mode 100644 index 0000000000000000000000000000000000000000..9364532344e4b182fc286c4ad78c738533335d73 GIT binary patch literal 353 zcmV-n0iOPeP)A?80v9Z^YZA8as&YaOiG2qAVzYLj+o?N|k`)kPFX7%epMT)pIgD?z$ zz0k7x#eWfQHil2%e>0rE^YzTbTVLW%P1UySD{rve8ZZphV4}^gtsg#oV8j^<1D2Tp2_^}JGDwbz00000NkvXXu0mjfU=gFa literal 0 HcmV?d00001 diff --git a/source/helpers.pas b/source/helpers.pas index 33e183d9..c9d6e1c1 100644 --- a/source/helpers.pas +++ b/source/helpers.pas @@ -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: // - 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 } diff --git a/source/main.dfm b/source/main.dfm index 8558d145..27808f1a 100644 --- a/source/main.dfm +++ b/source/main.dfm @@ -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 diff --git a/source/main.pas b/source/main.pas index 6e28ba65..45138b1f 100644 --- a/source/main.pas +++ b/source/main.pas @@ -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;