mirror of
https://github.com/HeidiSQL/HeidiSQL.git
synced 2025-08-06 18:24:26 +08:00
490 lines
17 KiB
ObjectPascal
490 lines
17 KiB
ObjectPascal
unit tabletools;
|
|
|
|
|
|
// -------------------------------------
|
|
// Table-diagnostics
|
|
// -------------------------------------
|
|
|
|
|
|
interface
|
|
|
|
uses
|
|
Windows, SysUtils, Classes, Controls, Forms, StdCtrls, ComCtrls, Buttons,
|
|
WideStrings, WideStrUtils, VirtualTrees, ExtCtrls, Db, Contnrs, Graphics, TntStdCtrls;
|
|
|
|
type
|
|
TfrmTableTools = class(TForm)
|
|
btnClose: TButton;
|
|
pnlTop: TPanel;
|
|
TreeObjects: TVirtualStringTree;
|
|
spltHorizontally: TSplitter;
|
|
pnlRight: TPanel;
|
|
ResultGrid: TVirtualStringTree;
|
|
lblResults: TLabel;
|
|
PageControlTools: TPageControl;
|
|
tabMaintenance: TTabSheet;
|
|
comboOperation: TComboBox;
|
|
lblOperation: TLabel;
|
|
chkQuick: TCheckBox;
|
|
chkFast: TCheckBox;
|
|
chkMedium: TCheckBox;
|
|
chkExtended: TCheckBox;
|
|
chkChanged: TCheckBox;
|
|
btnExecuteMaintenance: TButton;
|
|
chkUseFrm: TCheckBox;
|
|
lblOptions: TLabel;
|
|
btnHelp: TButton;
|
|
tabFind: TTabSheet;
|
|
lblFindText: TLabel;
|
|
memoFindText: TTntMemo;
|
|
btnFindText: TButton;
|
|
comboDataTypes: TComboBox;
|
|
lblDataTypes: TLabel;
|
|
pnlSkipLargeTables: TPanel;
|
|
lblSkipLargeTables: TLabel;
|
|
editSkipLargeTables: TEdit;
|
|
udSkipLargeTables: TUpDown;
|
|
lblSkipLargeTablesMB: TLabel;
|
|
procedure FormDestroy(Sender: TObject);
|
|
procedure FormCreate(Sender: TObject);
|
|
procedure FormShow(Sender: TObject);
|
|
procedure btnHelpClick(Sender: TObject);
|
|
procedure TreeObjectsGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex;
|
|
TextType: TVSTTextType; var CellText: WideString);
|
|
procedure TreeObjectsInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode;
|
|
var InitialStates: TVirtualNodeInitStates);
|
|
procedure TreeObjectsGetImageIndex(Sender: TBaseVirtualTree; Node: PVirtualNode; Kind: TVTImageKind;
|
|
Column: TColumnIndex; var Ghosted: Boolean; var ImageIndex: Integer);
|
|
procedure TreeObjectsInitChildren(Sender: TBaseVirtualTree; Node: PVirtualNode; var ChildCount: Cardinal);
|
|
procedure comboOperationChange(Sender: TObject);
|
|
procedure ExecuteOperation(Sender: TObject);
|
|
procedure ResultGridInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode;
|
|
var InitialStates: TVirtualNodeInitStates);
|
|
procedure ResultGridGetNodeDataSize(Sender: TBaseVirtualTree; var NodeDataSize: Integer);
|
|
procedure ResultGridGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex;
|
|
TextType: TVSTTextType; var CellText: WideString);
|
|
procedure TreeObjectsChecked(Sender: TBaseVirtualTree; Node: PVirtualNode);
|
|
procedure ResultGridHeaderClick(Sender: TVTHeader; HitInfo: TVTHeaderHitInfo);
|
|
procedure ResultGridCompareNodes(Sender: TBaseVirtualTree; Node1, Node2: PVirtualNode;
|
|
Column: TColumnIndex; var Result: Integer);
|
|
procedure ResultGridPaintText(Sender: TBaseVirtualTree; const TargetCanvas: TCanvas; Node: PVirtualNode;
|
|
Column: TColumnIndex; TextType: TVSTTextType);
|
|
procedure ValidateControls(Sender: TObject);
|
|
private
|
|
{ Private declarations }
|
|
FResults: TObjectList;
|
|
FRealResultCounter: Integer;
|
|
procedure ProcessTableNode(Sender: TObject; Node: PVirtualNode);
|
|
procedure AddResults(SQL: WideString);
|
|
procedure AddNotes(Col1, Col2, Col3, Col4: WideString);
|
|
procedure UpdateResultGrid;
|
|
public
|
|
{ Public declarations }
|
|
SelectedTables: TWideStringList;
|
|
end;
|
|
|
|
|
|
implementation
|
|
|
|
uses main, helpers, mysql_structures;
|
|
|
|
const
|
|
STRSKIPPED = 'Skipped - ';
|
|
|
|
{$R *.DFM}
|
|
|
|
|
|
procedure TfrmTableTools.FormCreate(Sender: TObject);
|
|
var
|
|
i: Integer;
|
|
begin
|
|
// Restore GUI setup
|
|
Width := GetRegValue(REGNAME_TOOLSWINWIDTH, Width);
|
|
Height := GetRegValue(REGNAME_TOOLSWINHEIGHT, Height);
|
|
TreeObjects.Width := GetRegValue(REGNAME_TOOLSTREEWIDTH, TreeObjects.Width);
|
|
memoFindText.Text := Utf8Decode(GetRegValue(REGNAME_TOOLSFINDTEXT, ''));
|
|
comboDatatypes.Items.Add('All data types');
|
|
for i:=Low(DatatypeCategories) to High(DatatypeCategories) do
|
|
comboDatatypes.Items.Add(DatatypeCategories[i].Name);
|
|
comboDatatypes.ItemIndex := GetRegValue(REGNAME_TOOLSDATATYPE, 0);
|
|
udSkipLargeTables.Position := GetRegValue(REGNAME_TOOLSSKIPMB, udSkipLargeTables.Position);
|
|
SetWindowSizeGrip( Self.Handle, True );
|
|
InheritFont(Font);
|
|
FixVT(TreeObjects);
|
|
FixVT(ResultGrid);
|
|
FResults := TObjectList.Create;
|
|
SelectedTables := TWideStringList.Create;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.FormDestroy(Sender: TObject);
|
|
begin
|
|
// Save GUI setup
|
|
OpenRegistry;
|
|
MainReg.WriteInteger( REGNAME_TOOLSWINWIDTH, Width );
|
|
MainReg.WriteInteger( REGNAME_TOOLSWINHEIGHT, Height );
|
|
MainReg.WriteInteger( REGNAME_TOOLSTREEWIDTH, TreeObjects.Width);
|
|
MainReg.WriteString( REGNAME_TOOLSFINDTEXT, Utf8Encode(memoFindText.Text));
|
|
MainReg.WriteInteger( REGNAME_TOOLSSKIPMB, udSkipLargeTables.Position);
|
|
MainReg.WriteInteger( REGNAME_TOOLSDATATYPE, comboDatatypes.ItemIndex);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.FormShow(Sender: TObject);
|
|
begin
|
|
// When this form is displayed the second time, databases may be deleted or filtered.
|
|
// Also, checked nodes must be unchecked and unchecked nodes may need to be checked.
|
|
TreeObjects.Clear;
|
|
TreeObjects.RootNodeCount := Mainform.DBtree.RootNodeCount;
|
|
// CHECKSUM available since MySQL 4.1.1
|
|
if Mainform.mysql_version < 40101 then
|
|
comboOperation.Items[comboOperation.Items.IndexOf('Checksum')] := 'Checksum ('+STR_NOTSUPPORTED+')';
|
|
comboOperation.OnChange(Sender);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.comboOperationChange(Sender: TObject);
|
|
var
|
|
op: String;
|
|
begin
|
|
// Only enable available options
|
|
op := LowerCase(comboOperation.Text);
|
|
chkQuick.Enabled := (op = 'check') or (op = 'checksum') or (op = 'repair');
|
|
chkFast.Enabled := op = 'check';
|
|
chkMedium.Enabled := op = 'check';
|
|
chkExtended.Enabled := (op = 'check') or (op = 'checksum') or (op = 'repair');
|
|
chkChanged.Enabled := op = 'check';
|
|
chkUseFrm.Enabled := op = 'repair';
|
|
ValidateControls(Sender);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.ValidateControls(Sender: TObject);
|
|
var
|
|
SomeChecked: Boolean;
|
|
begin
|
|
SomeChecked := TreeObjects.CheckedCount > 0;
|
|
btnExecuteMaintenance.Enabled := (Pos(STR_NOTSUPPORTED, comboOperation.Text) = 0) and SomeChecked;
|
|
btnFindText.Enabled := SomeChecked and (memoFindText.Text <> '');
|
|
// CHECKSUM's options are mutually exclusive
|
|
if comboOperation.Text = 'Checksum' then begin
|
|
if (Sender = chkExtended) and chkExtended.Checked then chkQuick.Checked := False
|
|
else if chkQuick.Checked then chkExtended.Checked := False;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.TreeObjectsChecked(Sender: TBaseVirtualTree; Node: PVirtualNode);
|
|
begin
|
|
ValidateControls(Sender);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.TreeObjectsGetImageIndex(Sender: TBaseVirtualTree; Node: PVirtualNode;
|
|
Kind: TVTImageKind; Column: TColumnIndex; var Ghosted: Boolean; var ImageIndex: Integer);
|
|
begin
|
|
Mainform.DBtreeGetImageIndex(Sender, Node, Kind, Column, Ghosted, ImageIndex);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.TreeObjectsGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
|
|
Column: TColumnIndex; TextType: TVSTTextType; var CellText: WideString);
|
|
begin
|
|
Mainform.DBtreeGetText(Sender, Node, Column, TextType, CellText);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.TreeObjectsInitChildren(Sender: TBaseVirtualTree; Node: PVirtualNode;
|
|
var ChildCount: Cardinal);
|
|
begin
|
|
Mainform.DBtreeInitChildren(Sender, Node, ChildCount);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.TreeObjectsInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode;
|
|
var InitialStates: TVirtualNodeInitStates);
|
|
var
|
|
ds: TDataset;
|
|
begin
|
|
// Attach a checkbox to all nodes
|
|
Mainform.DBtreeInitNode(Sender, ParentNode, Node, InitialStates);
|
|
Node.CheckType := ctTriStateCheckBox;
|
|
Node.CheckState := csUncheckedNormal;
|
|
case Sender.GetNodeLevel(Node) of
|
|
1: begin
|
|
if Mainform.Databases[Node.Index] = Mainform.ActiveDatabase then begin
|
|
if SelectedTables.Count = 0 then begin
|
|
// Preselect active database
|
|
Node.CheckState := csCheckedNormal;
|
|
TreeObjects.ReinitChildren(Node, False);
|
|
end else begin
|
|
// Expand db node so checked table nodes are visible
|
|
Include(InitialStates, ivsExpanded);
|
|
end;
|
|
end;
|
|
end;
|
|
2: begin
|
|
ds := Mainform.FetchDbTableList(Mainform.Databases[ParentNode.Index]);
|
|
ds.RecNo := Node.Index+1;
|
|
// No checkbox for stored routines
|
|
if not (GetDBObjectType(ds.Fields) in [lntTable, lntCrashedTable, lntView]) then
|
|
Node.CheckType := ctNone
|
|
else begin
|
|
if Node.Parent.CheckState in [csCheckedNormal, csCheckedPressed] then begin
|
|
// Check table node if either parent db is checked ...
|
|
Node.CheckState := csCheckedNormal
|
|
end else if (Mainform.Databases[Node.Parent.Index] = Mainform.ActiveDatabase)
|
|
// ... or table name is in SelectedTables
|
|
and (SelectedTables.Count > 0)
|
|
and (SelectedTables.IndexOf(ds.FieldByName(DBO_NAME).AsWideString) > -1) then begin
|
|
Node.CheckState := csCheckedNormal;
|
|
Node.Parent.CheckState := csMixedNormal;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
ValidateControls(Sender);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.btnHelpClick(Sender: TObject);
|
|
begin
|
|
Mainform.CallSQLHelpWithKeyword(UpperCase(comboOperation.Text) + ' TABLE');
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.ExecuteOperation(Sender: TObject);
|
|
var
|
|
DBNode, TableNode: PVirtualNode;
|
|
begin
|
|
Screen.Cursor := crHourGlass;
|
|
ResultGrid.Clear;
|
|
FResults.Clear;
|
|
FRealResultCounter := 0;
|
|
TreeObjects.SetFocus;
|
|
DBNode := TreeObjects.GetFirstChild(TreeObjects.GetFirst);
|
|
while Assigned(DBNode) do begin
|
|
if not (DBNode.CheckState in [csUncheckedNormal, csUncheckedPressed]) then begin
|
|
TableNode := TreeObjects.GetFirstChild(DBNode);
|
|
while Assigned(TableNode) do begin
|
|
ProcessTableNode(Sender, TableNode);
|
|
TableNode := TreeObjects.GetNextSibling(TableNode);
|
|
end;
|
|
end;
|
|
DBNode := TreeObjects.GetNextSibling(DBNode);
|
|
end;
|
|
Screen.Cursor := crDefault;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.ProcessTableNode(Sender: TObject; Node: PVirtualNode);
|
|
var
|
|
SQL, db, table, QuotedTable: WideString;
|
|
TableSize, RowsInTable: Int64;
|
|
ds: TDataset;
|
|
i: Integer;
|
|
HasSelectedDatatype: Boolean;
|
|
begin
|
|
// Prepare SQL for one table node
|
|
if (csCheckedNormal in [Node.CheckState, Node.Parent.CheckState]) and (Node.CheckType <> ctNone) then begin
|
|
db := TreeObjects.Text[Node.Parent, 0];
|
|
table := TreeObjects.Text[Node, 0];
|
|
QuotedTable := Mainform.mask(db)+'.'+Mainform.mask(table);
|
|
// Find table in cashed dataset and check its size - perhaps it has to be skipped
|
|
TableSize := 0;
|
|
RowsInTable := 0;
|
|
ds := Mainform.FetchDbTableList(db);
|
|
while not ds.Eof do begin
|
|
if (ds.FieldByName(DBO_NAME).AsWideString = table)
|
|
and (GetDBObjectType(ds.Fields) in [lntTable, lntCrashedTable]) then begin
|
|
TableSize := GetTableSize(ds);
|
|
RowsInTable := MakeInt(ds.FieldByName(DBO_ROWS).AsString);
|
|
// Avoid division by zero in below SQL
|
|
if RowsInTable = 0 then
|
|
RowsInTable := 1;
|
|
break;
|
|
end;
|
|
ds.Next;
|
|
end;
|
|
if (udSkipLargeTables.Position = 0) or ((TableSize div SIZE_MB) < udSkipLargeTables.Position) then try
|
|
if Sender = btnExecuteMaintenance then begin
|
|
SQL := UpperCase(comboOperation.Text) + ' TABLE ' + QuotedTable;
|
|
if chkQuick.Enabled and chkQuick.Checked then SQL := SQL + ' QUICK';
|
|
if chkFast.Enabled and chkFast.Checked then SQL := SQL + ' FAST';
|
|
if chkMedium.Enabled and chkMedium.Checked then SQL := SQL + ' MEDIUM';
|
|
if chkExtended.Enabled and chkExtended.Checked then SQL := SQL + ' EXTENDED';
|
|
if chkChanged.Enabled and chkChanged.Checked then SQL := SQL + ' CHANGED';
|
|
if chkUseFrm.Enabled and chkUseFrm.Checked then SQL := SQL + ' USE_FRM';
|
|
end else if Sender = btnFindText then begin
|
|
ds := Mainform.GetResults('SHOW COLUMNS FROM '+QuotedTable);
|
|
SQL := '';
|
|
while not ds.Eof do begin
|
|
HasSelectedDatatype := comboDatatypes.ItemIndex = 0;
|
|
if not HasSelectedDatatype then for i:=Low(Datatypes) to High(Datatypes) do begin
|
|
HasSelectedDatatype := (LowerCase(getFirstWord(ds.FieldByName('Type').AsString)) = LowerCase(Datatypes[i].Name))
|
|
and (Integer(Datatypes[i].Category)+1 = comboDatatypes.ItemIndex);
|
|
if HasSelectedDatatype then
|
|
break;
|
|
end;
|
|
if HasSelectedDatatype then
|
|
SQL := SQL + Mainform.mask(ds.FieldByName('Field').AsWideString) + ' LIKE ' + esc('%'+memoFindText.Text+'%') + ' OR ';
|
|
ds.Next;
|
|
end;
|
|
if SQL <> '' then begin
|
|
Delete(SQL, Length(SQL)-3, 3);
|
|
SQL := 'SELECT '''+db+''' AS `Database`, '''+table+''' AS `Table`, COUNT(*) AS `Found rows`, '
|
|
+ 'CONCAT(ROUND(100 / '+IntToStr(RowsInTable)+' * COUNT(*), 1), ''%'') AS `Relevance` FROM '+QuotedTable+' WHERE '
|
|
+ SQL;
|
|
end;
|
|
end;
|
|
if SQL <> '' then
|
|
AddResults(SQL)
|
|
else
|
|
AddNotes(db, table, STRSKIPPED+'table doesn''t have columns of selected type ('+comboDatatypes.Text+').', '');
|
|
except
|
|
// The above SQL can easily throw an exception, e.g. if a table is corrupted.
|
|
// In such cases we create a dummy row, including the error message
|
|
on E:Exception do
|
|
AddNotes(db, table, 'error', E.Message);
|
|
end else begin
|
|
AddNotes(db, table, STRSKIPPED+FormatByteNumber(TableSize), '');
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.AddResults(SQL: WideString);
|
|
var
|
|
i: Integer;
|
|
Col: TVirtualTreeColumn;
|
|
Row: TWideStringlist;
|
|
ds: TDataset;
|
|
begin
|
|
// Execute query and append results into grid
|
|
ds := Mainform.GetResults(SQL);
|
|
if ds = nil then
|
|
Exit;
|
|
|
|
// Add missing columns
|
|
for i:=ResultGrid.Header.Columns.Count to ds.FieldCount-1 do begin
|
|
Col := ResultGrid.Header.Columns.Add;
|
|
Col.Width := 130;
|
|
end;
|
|
// Remove superfluous columns
|
|
for i:=ResultGrid.Header.Columns.Count-1 downto ds.FieldCount do
|
|
ResultGrid.Header.Columns[i].Free;
|
|
// Set column header names
|
|
for i:=0 to ds.FieldCount-1 do begin
|
|
Col := ResultGrid.Header.Columns[i];
|
|
Col.Text := ds.Fields[i].FieldName;
|
|
if ds.Fields[i].DataType in [ftSmallint, ftInteger, ftWord, ftLargeint, ftFloat] then
|
|
Col.Alignment := taRightJustify
|
|
else
|
|
Col.Alignment := taLeftJustify;
|
|
end;
|
|
ds.First;
|
|
while not ds.Eof do begin
|
|
Row := TWideStringlist.Create;
|
|
for i:=0 to ds.FieldCount-1 do begin
|
|
Row.Add(ds.Fields[i].AsString);
|
|
end;
|
|
FResults.Add(Row);
|
|
ds.Next;
|
|
end;
|
|
|
|
Inc(FRealResultCounter);
|
|
lblResults.Caption := IntToStr(FRealResultCounter)+' results:';
|
|
lblResults.Repaint;
|
|
UpdateResultGrid;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.AddNotes(Col1, Col2, Col3, Col4: WideString);
|
|
var
|
|
Row: TWideStringlist;
|
|
begin
|
|
// Adds a row with non SQL results
|
|
Row := TWideStringlist.Create;
|
|
Row.Add(Col1);
|
|
Row.Add(Col2);
|
|
Row.Add(Col3);
|
|
Row.Add(Col4);
|
|
FResults.Add(Row);
|
|
UpdateResultGrid;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.UpdateResultGrid;
|
|
begin
|
|
// Refresh resultgrid
|
|
ResultGrid.RootNodeCount := FResults.Count;
|
|
ResultGrid.FocusedNode := ResultGrid.GetLast;
|
|
ResultGrid.Selected[ResultGrid.FocusedNode] := True;
|
|
ResultGrid.Repaint;
|
|
end;
|
|
|
|
procedure TfrmTableTools.ResultGridCompareNodes(Sender: TBaseVirtualTree; Node1, Node2: PVirtualNode;
|
|
Column: TColumnIndex; var Result: Integer);
|
|
begin
|
|
Mainform.vstCompareNodes(Sender, Node1, Node2, Column, Result);
|
|
end;
|
|
|
|
procedure TfrmTableTools.ResultGridGetNodeDataSize(Sender: TBaseVirtualTree; var NodeDataSize: Integer);
|
|
begin
|
|
NodeDataSize := SizeOf(TWideStringList);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.ResultGridInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode;
|
|
var InitialStates: TVirtualNodeInitStates);
|
|
var
|
|
Data: ^TWideStringList;
|
|
begin
|
|
// Bind string list to node
|
|
Data := Sender.GetNodeData(Node);
|
|
Data^ := FResults[Node.Index] as TWideStringList;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.ResultGridPaintText(Sender: TBaseVirtualTree; const TargetCanvas: TCanvas;
|
|
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType);
|
|
var
|
|
VT: TVirtualStringTree;
|
|
Msg: WideString;
|
|
begin
|
|
// Red text color for errors, purple for notes, grey for skipped tables
|
|
if not (vsSelected in Node.States) then begin
|
|
VT := Sender as TVirtualStringTree;
|
|
Msg := VT.Text[Node, 2];
|
|
if LowerCase(Msg) = 'note' then
|
|
TargetCanvas.Font.Color := clPurple
|
|
else if LowerCase(Msg) = 'error' then
|
|
TargetCanvas.Font.Color := clRed
|
|
else if Pos(STRSKIPPED, Msg) > 0 then
|
|
TargetCanvas.Font.Color := clGray;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmTableTools.ResultGridGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex;
|
|
TextType: TVSTTextType; var CellText: WideString);
|
|
var
|
|
Data: ^TWideStringList;
|
|
begin
|
|
if Column > NoColumn then begin
|
|
Data := Sender.GetNodeData(Node);
|
|
if Data^.Count > Column then
|
|
CellText := Data^[Column]
|
|
else
|
|
CellText := '';
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.ResultGridHeaderClick(Sender: TVTHeader; HitInfo: TVTHeaderHitInfo);
|
|
begin
|
|
// Header column clicked to sort
|
|
Mainform.vstHeaderClick(Sender, HitInfo);
|
|
end;
|
|
|
|
end.
|