mirror of
https://github.com/HeidiSQL/HeidiSQL.git
synced 2025-08-06 18:24:26 +08:00

* enhance db object lists for storing additional details like last modification and data size * remove home brown Min() and Max() functions and use those from Math unit which are not restricted to Integers.
1289 lines
52 KiB
ObjectPascal
1289 lines
52 KiB
ObjectPascal
unit tabletools;
|
|
|
|
|
|
// -------------------------------------
|
|
// Table-diagnostics
|
|
// -------------------------------------
|
|
|
|
|
|
interface
|
|
|
|
uses
|
|
Windows, SysUtils, Classes, Controls, Forms, StdCtrls, ComCtrls, Buttons, Dialogs, StdActns,
|
|
VirtualTrees, ExtCtrls, Contnrs, Graphics, SynRegExpr, Math,
|
|
mysql_connection, helpers;
|
|
|
|
type
|
|
TToolMode = (tmMaintenance, tmFind, tmSQLExport, tmBulkTableEdit);
|
|
TfrmTableTools = class(TForm)
|
|
btnClose: TButton;
|
|
pnlTop: TPanel;
|
|
TreeObjects: TVirtualStringTree;
|
|
spltHorizontally: TSplitter;
|
|
pnlRight: TPanel;
|
|
ResultGrid: TVirtualStringTree;
|
|
tabsTools: TPageControl;
|
|
tabMaintenance: TTabSheet;
|
|
comboOperation: TComboBox;
|
|
lblOperation: TLabel;
|
|
chkQuick: TCheckBox;
|
|
chkFast: TCheckBox;
|
|
chkMedium: TCheckBox;
|
|
chkExtended: TCheckBox;
|
|
chkChanged: TCheckBox;
|
|
chkUseFrm: TCheckBox;
|
|
lblOptions: TLabel;
|
|
btnHelp: TButton;
|
|
tabFind: TTabSheet;
|
|
lblFindText: TLabel;
|
|
memoFindText: TMemo;
|
|
comboDataTypes: TComboBox;
|
|
lblDataTypes: TLabel;
|
|
tabSQLexport: TTabSheet;
|
|
chkExportDatabasesCreate: TCheckBox;
|
|
chkExportDatabasesDrop: TCheckBox;
|
|
chkExportTablesDrop: TCheckBox;
|
|
chkExportTablesCreate: TCheckBox;
|
|
lblExportData: TLabel;
|
|
comboExportData: TComboBox;
|
|
lblExportOutputType: TLabel;
|
|
comboExportOutputType: TComboBox;
|
|
comboExportOutputTarget: TComboBox;
|
|
lblExportDatabases: TLabel;
|
|
lblExportTables: TLabel;
|
|
lblExportOutputTarget: TLabel;
|
|
btnExecute: TButton;
|
|
editSkipLargeTables: TEdit;
|
|
udSkipLargeTables: TUpDown;
|
|
lblSkipLargeTablesMB: TLabel;
|
|
lblSkipLargeTables: TLabel;
|
|
btnExportOutputTargetSelect: TButton;
|
|
tabBulkTableEdit: TTabSheet;
|
|
chkBulkTableEditDatabase: TCheckBox;
|
|
comboBulkTableEditDatabase: TComboBox;
|
|
chkBulkTableEditResetAutoinc: TCheckBox;
|
|
chkBulkTableEditCollation: TCheckBox;
|
|
comboBulkTableEditCollation: TComboBox;
|
|
chkBulkTableEditEngine: TCheckBox;
|
|
comboBulkTableEditEngine: TComboBox;
|
|
chkBulkTableEditCharset: TCheckBox;
|
|
comboBulkTableEditCharset: TComboBox;
|
|
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: String);
|
|
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 Execute(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: String);
|
|
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);
|
|
procedure chkExportOptionClick(Sender: TObject);
|
|
procedure btnExportOutputTargetSelectClick(Sender: TObject);
|
|
procedure comboExportOutputTargetChange(Sender: TObject);
|
|
procedure comboExportOutputTargetExit(Sender: TObject);
|
|
procedure comboExportOutputTypeChange(Sender: TObject);
|
|
procedure FormClose(Sender: TObject; var Action: TCloseAction);
|
|
procedure TreeObjectsPaintText(Sender: TBaseVirtualTree; const TargetCanvas: TCanvas; Node: PVirtualNode;
|
|
Column: TColumnIndex; TextType: TVSTTextType);
|
|
procedure chkBulkTableEditCheckComboClick(Sender: TObject);
|
|
procedure TreeObjectsChange(Sender: TBaseVirtualTree; Node: PVirtualNode);
|
|
procedure TreeObjectsChecking(Sender: TBaseVirtualTree; Node: PVirtualNode; var NewState: TCheckState;
|
|
var Allowed: Boolean);
|
|
private
|
|
{ Private declarations }
|
|
FResults: TObjectList;
|
|
FToolMode: TToolMode;
|
|
OutputFiles, OutputDirs: TStringList;
|
|
ExportStream: TStream;
|
|
ExportLastDatabase: String;
|
|
FTargetConnection: TMySQLConnection;
|
|
FLastOutputSelectedIndex: Integer;
|
|
FModifiedDbs: TStringList;
|
|
procedure SetToolMode(Value: TToolMode);
|
|
procedure AddResults(SQL: String);
|
|
procedure AddNotes(Col1, Col2, Col3, Col4: String);
|
|
procedure SetupResultGrid(Results: TMySQLQuery=nil);
|
|
procedure UpdateResultGrid;
|
|
procedure DoMaintenance(DBObj: TDBObject);
|
|
procedure DoFind(DBObj: TDBObject);
|
|
procedure DoExport(DBObj: TDBObject);
|
|
procedure DoBulkTableEdit(DBObj: TDBObject);
|
|
public
|
|
{ Public declarations }
|
|
SelectedTables: TStringList;
|
|
property ToolMode: TToolMode read FToolMode write SetToolMode;
|
|
end;
|
|
|
|
|
|
implementation
|
|
|
|
uses main, mysql_structures;
|
|
|
|
const
|
|
STRSKIPPED: String = 'Skipped - ';
|
|
OUTPUT_FILE = 'One big file';
|
|
OUTPUT_CLIPBOARD = 'Clipboard';
|
|
OUTPUT_DIR = 'Directory - one file per object in database subdirectories';
|
|
OUTPUT_DB = 'Database';
|
|
OUTPUT_SERVER = 'Server: ';
|
|
DATA_NO = 'No data';
|
|
DATA_REPLACE = 'DELETE + INSERT (truncate existing data)';
|
|
DATA_INSERT = 'INSERT';
|
|
DATA_INSERTNEW = 'INSERT IGNORE (do not update existing)';
|
|
DATA_UPDATE = 'REPLACE existing data';
|
|
EXPORT_FILE_FOOTER = '/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;'+CRLF+
|
|
'/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;'+CRLF+
|
|
'/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;'+CRLF;
|
|
|
|
{$R *.DFM}
|
|
|
|
|
|
procedure TfrmTableTools.FormCreate(Sender: TObject);
|
|
var
|
|
i: Integer;
|
|
begin
|
|
// Restore GUI setup
|
|
InheritFont(Font);
|
|
Width := GetRegValue(REGNAME_TOOLSWINWIDTH, Width);
|
|
Height := GetRegValue(REGNAME_TOOLSWINHEIGHT, Height);
|
|
TreeObjects.Width := GetRegValue(REGNAME_TOOLSTREEWIDTH, TreeObjects.Width);
|
|
|
|
// Find text tab
|
|
memoFindText.Text := 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);
|
|
|
|
// SQL export tab
|
|
chkExportDatabasesCreate.Checked := GetRegValue(REGNAME_EXP_CREATEDB, chkExportDatabasesCreate.Checked);
|
|
chkExportDatabasesDrop.Checked := GetRegValue(REGNAME_EXP_DROPDB, chkExportDatabasesDrop.Checked);
|
|
chkExportTablesCreate.Checked := GetRegValue(REGNAME_EXP_CREATETABLE, chkExportTablesCreate.Checked);
|
|
chkExportTablesDrop.Checked := GetRegValue(REGNAME_EXP_DROPTABLE, chkExportTablesDrop.Checked);
|
|
comboExportData.Items.Text := DATA_NO+CRLF +DATA_REPLACE+CRLF +DATA_INSERT+CRLF +DATA_INSERTNEW+CRLF +DATA_UPDATE;
|
|
comboExportData.ItemIndex := GetRegValue(REGNAME_EXP_DATAHOW, 0);
|
|
OutputFiles := TStringList.Create;
|
|
OutputDirs := TStringList.Create;
|
|
OutputFiles.Text := GetRegValue(REGNAME_EXP_OUTFILES, '');
|
|
OutputDirs.Text := GetRegValue(REGNAME_EXP_OUTDIRS, '');
|
|
comboExportOutputType.Items.Text := OUTPUT_FILE+CRLF +OUTPUT_DIR+CRLF +OUTPUT_CLIPBOARD+CRLF +OUTPUT_DB;
|
|
comboExportOutputType.ItemIndex := GetRegValue(REGNAME_EXP_OUTPUT, 0);
|
|
comboExportOutputTarget.Text := '';
|
|
|
|
// Various
|
|
udSkipLargeTables.Position := GetRegValue(REGNAME_TOOLSSKIPMB, udSkipLargeTables.Position);
|
|
SetWindowSizeGrip( Self.Handle, True );
|
|
FixVT(TreeObjects);
|
|
FixVT(ResultGrid);
|
|
FResults := TObjectList.Create;
|
|
SelectedTables := TStringList.Create;
|
|
FModifiedDbs := TStringList.Create;
|
|
FModifiedDbs.Duplicates := dupIgnore;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.FormDestroy(Sender: TObject);
|
|
var
|
|
OutputItem: Integer;
|
|
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, memoFindText.Text);
|
|
MainReg.WriteInteger( REGNAME_TOOLSDATATYPE, comboDatatypes.ItemIndex);
|
|
|
|
MainReg.WriteBool(REGNAME_EXP_CREATEDB, chkExportDatabasesCreate.Checked);
|
|
MainReg.WriteBool(REGNAME_EXP_DROPDB, chkExportDatabasesDrop.Checked);
|
|
MainReg.WriteBool(REGNAME_EXP_CREATETABLE, chkExportTablesCreate.Checked);
|
|
MainReg.WriteBool(REGNAME_EXP_DROPTABLE, chkExportTablesDrop.Checked);
|
|
MainReg.WriteInteger(REGNAME_EXP_DATAHOW, comboExportData.ItemIndex);
|
|
// Do not remember a selected session name for the next time
|
|
OutputItem := comboExportOutputType.ItemIndex;
|
|
if OutputItem > 3 then
|
|
OutputItem := 0;
|
|
MainReg.WriteInteger(REGNAME_EXP_OUTPUT, OutputItem);
|
|
MainReg.WriteString(REGNAME_EXP_OUTFILES, OutputFiles.Text);
|
|
MainReg.WriteString(REGNAME_EXP_OUTDIRS, OutputDirs.Text);
|
|
|
|
MainReg.WriteInteger( REGNAME_TOOLSSKIPMB, udSkipLargeTables.Position);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.FormShow(Sender: TObject);
|
|
var
|
|
DBNode, TableNode, FirstChecked: PVirtualNode;
|
|
idx, i: Integer;
|
|
SessionNames: TStringList;
|
|
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;
|
|
|
|
DBNode := TreeObjects.GetFirstChild(TreeObjects.GetFirst);
|
|
while Assigned(DBNode) do begin
|
|
if TreeObjects.Text[DBNode, 0] = Mainform.ActiveDatabase then begin
|
|
if SelectedTables.Count = 0 then begin
|
|
// Preselect active database
|
|
DBNode.CheckState := csCheckedNormal;
|
|
end else begin
|
|
DBNode.CheckState := csMixedNormal;
|
|
// Expand db node so checked table nodes are visible
|
|
TreeObjects.Expanded[DBNode] := true;
|
|
TableNode := TreeObjects.GetFirstChild(DBNode);
|
|
while Assigned(TableNode) do begin
|
|
if SelectedTables.IndexOf(TreeObjects.Text[TableNode, 0]) > -1 then
|
|
TableNode.CheckState := csCheckedNormal;
|
|
TableNode := TreeObjects.GetNextSibling(TableNode);
|
|
end;
|
|
end;
|
|
end;
|
|
DBNode := TreeObjects.GetNextSibling(DBNode);
|
|
end;
|
|
|
|
FirstChecked := TreeObjects.GetFirstChecked;
|
|
if Assigned(FirstChecked) then
|
|
TreeObjects.ScrollIntoView(FirstChecked, True);
|
|
// CHECKSUM available since MySQL 4.1.1
|
|
idx := comboOperation.ItemIndex;
|
|
if idx = -1 then idx := 0;
|
|
comboOperation.Items.CommaText := 'Check,Analyze,Checksum,Optimize,Repair';
|
|
if Mainform.Connection.ServerVersionInt < 40101 then
|
|
comboOperation.Items.Text := StringReplace(comboOperation.Items.Text, 'Checksum', 'Checksum ('+STR_NOTSUPPORTED+')', [rfReplaceAll]);
|
|
comboOperation.ItemIndex := idx;
|
|
comboOperation.OnChange(Sender);
|
|
|
|
// Add session names from registry
|
|
for i:=comboExportOutputType.Items.Count-1 downto 0 do begin
|
|
if Pos(OUTPUT_SERVER, comboExportOutputType.Items[i]) = 1 then
|
|
comboExportOutputType.Items.Delete(i);
|
|
end;
|
|
SessionNames := TStringList.Create;
|
|
MainReg.OpenKey(RegPath + REGKEY_SESSIONS, True);
|
|
MainReg.GetKeyNames(SessionNames);
|
|
for i:=0 to SessionNames.Count-1 do begin
|
|
if SessionNames[i] <> Mainform.SessionName then
|
|
comboExportOutputType.Items.Add(OUTPUT_SERVER+SessionNames[i]);
|
|
end;
|
|
comboExportOutputType.OnChange(Sender);
|
|
|
|
comboBulkTableEditDatabase.Items.Text := Mainform.Databases.Text;
|
|
if comboBulkTableEditDatabase.Items.Count > 0 then
|
|
comboBulkTableEditDatabase.ItemIndex := 0;
|
|
|
|
comboBulkTableEditEngine.Items := Mainform.Connection.TableEngines;
|
|
if comboBulkTableEditEngine.Items.Count > 0 then
|
|
comboBulkTableEditEngine.ItemIndex := comboBulkTableEditEngine.Items.IndexOf(Mainform.Connection.TableEngineDefault);
|
|
|
|
comboBulkTableEditCollation.Items := Mainform.Connection.CollationList;
|
|
if comboBulkTableEditCollation.Items.Count > 0 then
|
|
comboBulkTableEditCollation.ItemIndex := 0;
|
|
|
|
comboBulkTableEditCharset.Items := Mainform.Connection.CharsetList;
|
|
if comboBulkTableEditCharset.Items.Count > 0 then
|
|
comboBulkTableEditCharset.ItemIndex := 0;
|
|
|
|
ValidateControls(Sender);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.FormClose(Sender: TObject; var Action: TCloseAction);
|
|
begin
|
|
// Auto close temorary connection
|
|
if Assigned(FTargetConnection) then
|
|
FreeAndNil(FTargetConnection);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.ValidateControls(Sender: TObject);
|
|
var
|
|
SomeChecked, OptionChecked: Boolean;
|
|
op: String;
|
|
begin
|
|
SomeChecked := TreeObjects.CheckedCount > 0;
|
|
if tabsTools.ActivePage = tabMaintenance then begin
|
|
btnExecute.Caption := 'Execute';
|
|
btnExecute.Enabled := (Pos(STR_NOTSUPPORTED, comboOperation.Text) = 0) and SomeChecked;
|
|
// 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';
|
|
// 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 else if tabsTools.ActivePage = tabFind then begin
|
|
btnExecute.Caption := 'Find';
|
|
btnExecute.Enabled := SomeChecked and (memoFindText.Text <> '');
|
|
end else if tabsTools.ActivePage = tabSQLExport then begin
|
|
btnExecute.Caption := 'Export';
|
|
btnExecute.Enabled := SomeChecked and ((comboExportOutputTarget.Text <> '') or (not comboExportOutputTarget.Enabled));
|
|
end else if tabsTools.ActivePage = tabBulkTableEdit then begin
|
|
btnExecute.Caption := 'Update';
|
|
chkBulkTableEditCollation.Enabled := Mainform.Connection.IsUnicode;
|
|
chkBulkTableEditCharset.Enabled := Mainform.Connection.IsUnicode;
|
|
OptionChecked := chkBulkTableEditDatabase.Checked or chkBulkTableEditEngine.Checked or chkBulkTableEditCollation.Checked
|
|
or chkBulkTableEditCharset.Checked or chkBulkTableEditResetAutoinc.Checked;
|
|
btnExecute.Enabled := SomeChecked and OptionChecked;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.TreeObjectsChange(Sender: TBaseVirtualTree; Node: PVirtualNode);
|
|
begin
|
|
Mainform.DBtreeChange(Sender, Node);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.TreeObjectsChecked(Sender: TBaseVirtualTree; Node: PVirtualNode);
|
|
begin
|
|
ValidateControls(Sender);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.TreeObjectsChecking(Sender: TBaseVirtualTree; Node: PVirtualNode;
|
|
var NewState: TCheckState; var Allowed: Boolean);
|
|
var
|
|
n: PVirtualNode;
|
|
begin
|
|
// Ensure to also toggle check state of not yet initialized nodes
|
|
Allowed := True;
|
|
// Weird fix: Just iterate through all sub nodes for implicit initialization. Without this
|
|
// loop a checkbox click on a parent node would only auto-check its visible children.
|
|
n := Sender.GetFirstChild(Node);
|
|
while Assigned(n) do
|
|
n := Sender.GetNextSibling(n);
|
|
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: String);
|
|
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);
|
|
begin
|
|
// Attach a checkbox to all nodes
|
|
Mainform.DBtreeInitNode(Sender, ParentNode, Node, InitialStates);
|
|
Node.CheckType := ctTriStateCheckBox;
|
|
Node.CheckState := csUncheckedNormal;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.TreeObjectsPaintText(Sender: TBaseVirtualTree; const TargetCanvas: TCanvas;
|
|
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType);
|
|
begin
|
|
Mainform.DBtreePaintText(Sender, TargetCanvas, Node, Column, TextType);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.btnHelpClick(Sender: TObject);
|
|
begin
|
|
Mainform.CallSQLHelpWithKeyword(UpperCase(comboOperation.Text) + ' TABLE');
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.Execute(Sender: TObject);
|
|
var
|
|
DBNode, TableNode: PVirtualNode;
|
|
DBObjects, Views: TDBObjectList;
|
|
DBObj: TDBObject;
|
|
i: Integer;
|
|
ProcessNodeFunc: procedure(DBObj: TDBObject) of Object;
|
|
begin
|
|
Screen.Cursor := crHourGlass;
|
|
if tabsTools.ActivePage = tabMaintenance then
|
|
FToolMode := tmMaintenance
|
|
else if tabsTools.ActivePage = tabFind then
|
|
FToolMode := tmFind
|
|
else if tabsTools.ActivePage = tabSQLExport then
|
|
FToolMode := tmSQLExport
|
|
else if tabsTools.ActivePage = tabBulkTableEdit then
|
|
FToolMode := tmBulkTableEdit;
|
|
ResultGrid.Clear;
|
|
FResults.Clear;
|
|
Views := TDBObjectList.Create;
|
|
Views.OwnsObjects := False; // So we can .Free that object afterwards without loosing the contained objects
|
|
TreeObjects.SetFocus;
|
|
ProcessNodeFunc := nil;
|
|
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
|
|
if (csCheckedNormal in [TableNode.CheckState, DBNode.CheckState]) and (TableNode.CheckType <> ctNone) then begin
|
|
DBObjects := Mainform.Connection.GetDBObjects(TreeObjects.Text[DBNode, 0]);
|
|
DBObj := DBObjects[TableNode.Index];
|
|
// Find table in cashed dataset and check its size - perhaps it has to be skipped
|
|
if (udSkipLargeTables.Position = 0) or ((DBObj.Size div SIZE_MB) < udSkipLargeTables.Position) then try
|
|
case FToolMode of
|
|
tmMaintenance: ProcessNodeFunc := DoMaintenance;
|
|
tmFind: ProcessNodeFunc := DoFind;
|
|
tmSQLExport: ProcessNodeFunc := DoExport;
|
|
tmBulkTableEdit: ProcessNodeFunc := DoBulkTableEdit;
|
|
end;
|
|
// Views have to be exported at the very end so at least all needed tables are ready when a view gets imported
|
|
if DBObj.NodeType = lntView then
|
|
Views.Add(DBObj)
|
|
else
|
|
ProcessNodeFunc(DBObj);
|
|
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(DBObj.Database, DBObj.Name, 'error', E.Message);
|
|
end else begin
|
|
AddNotes(DBObj.Database, DBObj.Name, STRSKIPPED+FormatByteNumber(DBObj.Size), '');
|
|
end;
|
|
end;
|
|
TableNode := TreeObjects.GetNextSibling(TableNode);
|
|
end;
|
|
end;
|
|
DBNode := TreeObjects.GetNextSibling(DBNode);
|
|
end;
|
|
|
|
// Special block for late created views in export mode
|
|
for i:=0 to Views.Count-1 do begin
|
|
try
|
|
ProcessNodeFunc(Views[i]);
|
|
except on E:Exception do
|
|
AddNotes(Views[i].Database, Views[i].Name, 'error', E.Message);
|
|
end;
|
|
end;
|
|
|
|
if Assigned(ExportStream) then begin
|
|
if (comboExportOutputType.Text = OUTPUT_FILE) or (comboExportOutputType.Text = OUTPUT_CLIPBOARD) then
|
|
StreamWrite(ExportStream, EXPORT_FILE_FOOTER);
|
|
if comboExportOutputType.Text = OUTPUT_CLIPBOARD then
|
|
StreamToClipboard(ExportStream, nil, false);
|
|
FreeAndNil(ExportStream);
|
|
end;
|
|
ExportLastDatabase := '';
|
|
|
|
if FModifiedDbs.Count > 0 then begin
|
|
for i:=0 to FModifiedDbs.Count-1 do
|
|
Mainform.Connection.ClearDbObjects(FModifiedDbs[i]);
|
|
TreeObjects.ResetNode(TreeObjects.GetFirst);
|
|
Mainform.DBtree.ResetNode(Mainform.DBtree.GetFirst);
|
|
FModifiedDbs.Clear;
|
|
end;
|
|
Screen.Cursor := crDefault;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.DoMaintenance(DBObj: TDBObject);
|
|
var
|
|
SQL: String;
|
|
begin
|
|
if not (DBObj.NodeType in [lntTable, lntView]) then begin
|
|
AddNotes(DBObj.Database, DBObj.Name, STRSKIPPED+'a '+LowerCase(DBObj.ObjType)+' cannot be maintained.', '');
|
|
Exit;
|
|
end;
|
|
SQL := UpperCase(comboOperation.Text) + ' TABLE ' + Mainform.mask(DBObj.Database) + '.' + Mainform.mask(DBObj.Name);
|
|
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';
|
|
AddResults(SQL);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.DoFind(DBObj: TDBObject);
|
|
var
|
|
Results: TMySQLQuery;
|
|
SQL: String;
|
|
HasSelectedDatatype: Boolean;
|
|
i: Integer;
|
|
begin
|
|
if not (DBObj.NodeType in [lntTable, lntView]) then begin
|
|
AddNotes(DBObj.Database, DBObj.Name, STRSKIPPED+'a '+LowerCase(DBObj.ObjType)+' does not contain rows.', '');
|
|
Exit;
|
|
end;
|
|
Results := Mainform.Connection.GetResults('SHOW COLUMNS FROM '+Mainform.mask(DBObj.Database)+'.'+Mainform.mask(DBObj.Name));
|
|
SQL := '';
|
|
while not Results.Eof do begin
|
|
HasSelectedDatatype := comboDatatypes.ItemIndex = 0;
|
|
if not HasSelectedDatatype then for i:=Low(Datatypes) to High(Datatypes) do begin
|
|
HasSelectedDatatype := (LowerCase(getFirstWord(Results.Col('Type'))) = LowerCase(Datatypes[i].Name))
|
|
and (Integer(Datatypes[i].Category)+1 = comboDatatypes.ItemIndex);
|
|
if HasSelectedDatatype then
|
|
break;
|
|
end;
|
|
if HasSelectedDatatype then
|
|
SQL := SQL + Mainform.mask(Results.Col('Field')) + ' LIKE ' + esc('%'+memoFindText.Text+'%') + ' OR ';
|
|
Results.Next;
|
|
end;
|
|
if SQL <> '' then begin
|
|
Delete(SQL, Length(SQL)-3, 3);
|
|
SQL := 'SELECT '''+DBObj.Database+''' AS `Database`, '''+DBObj.Name+''' AS `Table`, COUNT(*) AS `Found rows`, '
|
|
+ 'CONCAT(ROUND(100 / '+IntToStr(Max(DBObj.Rows,1))+' * COUNT(*), 1), ''%'') AS `Relevance` FROM '+Mainform.mask(DBObj.Database)+'.'+Mainform.mask(DBObj.Name)+' WHERE '
|
|
+ SQL;
|
|
AddResults(SQL);
|
|
end else
|
|
AddNotes(DBObj.Database, DBObj.Name, STRSKIPPED+'table doesn''t have columns of selected type ('+comboDatatypes.Text+').', '');
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.AddResults(SQL: String);
|
|
var
|
|
i: Integer;
|
|
Row: TStringList;
|
|
Results: TMySQLQuery;
|
|
begin
|
|
// Execute query and append results into grid
|
|
Results := Mainform.Connection.GetResults(SQL);
|
|
if Results = nil then
|
|
Exit;
|
|
|
|
SetupResultGrid(Results);
|
|
Results.First;
|
|
while not Results.Eof do begin
|
|
Row := TStringList.Create;
|
|
for i:=0 to Results.ColumnCount-1 do begin
|
|
if Results.DataType(i).Category = dtcInteger then
|
|
Row.Add(FormatNumber(Results.Col(i)))
|
|
else
|
|
Row.Add(Results.Col(i));
|
|
end;
|
|
FResults.Add(Row);
|
|
Results.Next;
|
|
end;
|
|
Results.Free;
|
|
|
|
UpdateResultGrid;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.AddNotes(Col1, Col2, Col3, Col4: String);
|
|
var
|
|
Row: TStringList;
|
|
begin
|
|
// Adds a row with non SQL results
|
|
SetupResultGrid;
|
|
Row := TStringList.Create;
|
|
Row.Add(Col1);
|
|
Row.Add(Col2);
|
|
Row.Add(Col3);
|
|
Row.Add(Col4);
|
|
FResults.Add(Row);
|
|
UpdateResultGrid;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.SetupResultGrid(Results: TMySQLQuery=nil);
|
|
var
|
|
ColCount, i: Integer;
|
|
Col: TVirtualTreeColumn;
|
|
begin
|
|
if Assigned(Results) then begin
|
|
ColCount := Results.ColumnCount;
|
|
ResultGrid.Header.Options := ResultGrid.Header.Options + [hoVisible];
|
|
end else begin
|
|
ColCount := 4;
|
|
// Remove column headers if this is the first row
|
|
if FResults.Count = 0 then
|
|
ResultGrid.Header.Options := ResultGrid.Header.Options - [hoVisible];
|
|
end;
|
|
|
|
// Add missing columns
|
|
for i:=ResultGrid.Header.Columns.Count to ColCount-1 do begin
|
|
Col := ResultGrid.Header.Columns.Add;
|
|
Col.Width := 130;
|
|
end;
|
|
// Remove superfluous columns
|
|
for i:=ResultGrid.Header.Columns.Count-1 downto ColCount do
|
|
ResultGrid.Header.Columns[i].Free;
|
|
|
|
// Set column header names
|
|
for i:=0 to ResultGrid.Header.Columns.Count-1 do begin
|
|
Col := ResultGrid.Header.Columns[i];
|
|
if Assigned(Results) then begin
|
|
Col.Text := Results.ColumnNames[i];
|
|
if Results.DataType(i).Category in [dtcInteger, dtcIntegerNamed, dtcReal] then
|
|
Col.Alignment := taRightJustify
|
|
else
|
|
Col.Alignment := taLeftJustify;
|
|
end;
|
|
end;
|
|
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(TStringList);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.ResultGridInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode;
|
|
var InitialStates: TVirtualNodeInitStates);
|
|
var
|
|
Data: ^TStringList;
|
|
begin
|
|
// Bind string list to node
|
|
Data := Sender.GetNodeData(Node);
|
|
Data^ := FResults[Node.Index] as TStringList;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.ResultGridPaintText(Sender: TBaseVirtualTree; const TargetCanvas: TCanvas;
|
|
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType);
|
|
var
|
|
VT: TVirtualStringTree;
|
|
Msg: String;
|
|
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 := clGrayText;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmTableTools.ResultGridGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex;
|
|
TextType: TVSTTextType; var CellText: String);
|
|
var
|
|
Data: ^TStringList;
|
|
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;
|
|
|
|
|
|
procedure TfrmTableTools.comboExportOutputTypeChange(Sender: TObject);
|
|
var
|
|
OldItem: String;
|
|
NewIdx: Integer;
|
|
DBNode: PVirtualNode;
|
|
SessionName: String;
|
|
begin
|
|
// Target type (file, directory, ...) selected
|
|
OldItem := comboExportOutputTarget.Text;
|
|
comboExportOutputTarget.Enabled := True;
|
|
if Assigned(FTargetConnection) then
|
|
FreeAndNil(FTargetConnection);
|
|
if comboExportOutputType.Text = OUTPUT_FILE then begin
|
|
comboExportOutputTarget.Style := csDropDown;
|
|
comboExportOutputTarget.Items.Text := OutputFiles.Text;
|
|
lblExportOutputTarget.Caption := 'Filename:';
|
|
btnExportOutputTargetSelect.Enabled := True;
|
|
btnExportOutputTargetSelect.ImageIndex := 10;
|
|
end else if comboExportOutputType.Text = OUTPUT_DIR then begin
|
|
comboExportOutputTarget.Style := csDropDown;
|
|
comboExportOutputTarget.Items.Text := OutputDirs.Text;
|
|
lblExportOutputTarget.Caption := 'Directory:';
|
|
btnExportOutputTargetSelect.Enabled := True;
|
|
btnExportOutputTargetSelect.ImageIndex := 51;
|
|
end else if comboExportOutputType.Text = OUTPUT_CLIPBOARD then begin
|
|
comboExportOutputTarget.Enabled := False;
|
|
comboExportOutputTarget.Items.Clear;
|
|
lblExportOutputTarget.Caption := '';
|
|
btnExportOutputTargetSelect.Enabled := False;
|
|
btnExportOutputTargetSelect.ImageIndex := 4;
|
|
end else if comboExportOutputType.Text = OUTPUT_DB then begin
|
|
comboExportOutputTarget.Style := csDropDownList;
|
|
lblExportOutputTarget.Caption := 'Database:';
|
|
btnExportOutputTargetSelect.Enabled := False;
|
|
btnExportOutputTargetSelect.ImageIndex := 27;
|
|
// Add unchecked databases
|
|
comboExportOutputTarget.Items.Clear;
|
|
DBNode := TreeObjects.GetFirstChild(TreeObjects.GetFirst);
|
|
while Assigned(DBNode) do begin
|
|
if DBNode.CheckState in [csUncheckedNormal, csUncheckedPressed] then
|
|
comboExportOutputTarget.Items.Add(TreeObjects.Text[DBNode, 0]);
|
|
DBNode := TreeObjects.GetNextSibling(DBNode);
|
|
end;
|
|
end else begin
|
|
// Server selected. Display databases in below dropdown
|
|
comboExportOutputTarget.Style := csDropDownList;
|
|
lblExportOutputTarget.Caption := 'Database:';
|
|
btnExportOutputTargetSelect.Enabled := False;
|
|
btnExportOutputTargetSelect.ImageIndex := 27;
|
|
SessionName := Copy(comboExportOutputType.Text, Length(OUTPUT_SERVER)+1, Length(comboExportOutputType.Text));
|
|
FreeAndNil(FTargetConnection);
|
|
FTargetConnection := TMySQLConnection.Create(Self);
|
|
FTargetConnection.Parameters := LoadConnectionParams(SessionName);
|
|
FTargetConnection.LogPrefix := '['+SessionName+'] ';
|
|
FTargetConnection.OnLog := Mainform.LogSQL;
|
|
Screen.Cursor := crHourglass;
|
|
try
|
|
FTargetConnection.Active := True;
|
|
comboExportOutputTarget.Items := FTargetConnection.GetCol('SHOW DATABASES');
|
|
comboExportOutputTarget.Items.Insert(0, '[Same as on source server]');
|
|
comboExportOutputTarget.ItemIndex := 0;
|
|
Screen.Cursor := crDefault;
|
|
except
|
|
on E:Exception do begin
|
|
Screen.Cursor := crDefault;
|
|
MessageDlg(E.Message, mtError, [mbOK], 0);
|
|
comboExportOutputType.ItemIndex := FLastOutputSelectedIndex;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
FLastOutputSelectedIndex := comboExportOutputType.ItemIndex;
|
|
chkExportDatabasesCreate.Enabled := (comboExportOutputType.Text = OUTPUT_FILE)
|
|
or (comboExportOutputType.Text = OUTPUT_CLIPBOARD)
|
|
or (Copy(comboExportOutputType.Text, 1, Length(OUTPUT_SERVER)) = OUTPUT_SERVER);
|
|
chkExportDatabasesDrop.Enabled := chkExportDatabasesCreate.Enabled;
|
|
NewIdx := comboExportOutputTarget.Items.IndexOf(OldItem);
|
|
if (NewIdx = -1) and (comboExportOutputTarget.Items.Count > 0) then
|
|
NewIdx := 0;
|
|
comboExportOutputTarget.ItemIndex := NewIdx;
|
|
ValidateControls(Sender);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.comboExportOutputTargetChange(Sender: TObject);
|
|
begin
|
|
ValidateControls(Sender);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.comboExportOutputTargetExit(Sender: TObject);
|
|
var
|
|
ItemList: TStringList;
|
|
idx: Integer;
|
|
begin
|
|
// Add typed text to recent items
|
|
if comboExportOutputTarget.Text = '' then
|
|
Exit;
|
|
ItemList := nil;
|
|
if comboExportOutputType.Text = OUTPUT_FILE then
|
|
ItemList := OutputFiles
|
|
else if comboExportOutputType.Text = OUTPUT_DIR then
|
|
ItemList := OutputDirs;
|
|
if not Assigned(ItemList) then
|
|
Exit;
|
|
idx := ItemList.IndexOf(comboExportOutputTarget.Text);
|
|
if idx > -1 then
|
|
ItemList.Delete(idx);
|
|
ItemList.Insert(0, comboExportOutputTarget.Text);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.chkExportOptionClick(Sender: TObject);
|
|
procedure WarnIfChecked(chk: TCheckBox; LabelText: String);
|
|
begin
|
|
if chk.Checked then begin
|
|
chk.Caption := LabelText + '!!';
|
|
chk.Font.Style := chk.Font.Style + [fsBold];
|
|
end else begin
|
|
chk.Caption := LabelText;
|
|
chk.Font.Style := Font.Style;
|
|
end;
|
|
end;
|
|
begin
|
|
if (Sender = chkExportDatabasesDrop) and chkExportDatabasesDrop.Checked then
|
|
chkExportDatabasesCreate.Checked := True
|
|
else if (Sender = chkExportDatabasesCreate) and (not chkExportDatabasesCreate.Checked) then
|
|
chkExportDatabasesDrop.Checked := False
|
|
else if (Sender = chkExportTablesDrop) and chkExportTablesDrop.Checked then
|
|
chkExportTablesCreate.Checked := True
|
|
else if (Sender = chkExportTablesCreate) and (not chkExportTablesCreate.Checked) then
|
|
chkExportTablesDrop.Checked := False;
|
|
WarnIfChecked(chkExportDatabasesDrop, 'Drop');
|
|
WarnIfChecked(chkExportTablesDrop, 'Drop');
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.btnExportOutputTargetSelectClick(Sender: TObject);
|
|
var
|
|
SaveDialog: TSaveDialog;
|
|
Browse: TBrowseForFolder;
|
|
begin
|
|
case comboExportOutputType.ItemIndex of
|
|
0: begin
|
|
// Select filename
|
|
SaveDialog := TSaveDialog.Create(Self);
|
|
SaveDialog.DefaultExt := 'sql';
|
|
SaveDialog.Filter := 'SQL-Scripts (*.sql)|*.sql|All Files (*.*)|*.*';
|
|
SaveDialog.Options := SaveDialog.Options + [ofOverwritePrompt];
|
|
if SaveDialog.Execute then
|
|
comboExportOutputTarget.Text := SaveDialog.FileName;
|
|
SaveDialog.Free;
|
|
end;
|
|
1: begin
|
|
Browse := TBrowseForFolder.Create(Self);
|
|
Browse.Folder := comboExportOutputTarget.Text;
|
|
Browse.DialogCaption := 'Select output directory';
|
|
// Enable "Create new folder" button
|
|
Browse.BrowseOptions := Browse.BrowseOptions - [bifNoNewFolderButton] + [bifNewDialogStyle];
|
|
if Browse.Execute then
|
|
comboExportOutputTarget.Text := Browse.Folder;
|
|
Browse.Free;
|
|
end;
|
|
end;
|
|
ValidateControls(Sender);
|
|
end;
|
|
|
|
procedure TfrmTableTools.SetToolMode(Value: TToolMode);
|
|
begin
|
|
FToolMode := Value;
|
|
case FToolMode of
|
|
tmMaintenance: tabsTools.ActivePage := tabMaintenance;
|
|
tmFind: tabsTools.ActivePage := tabFind;
|
|
tmSQLExport: tabsTools.ActivePage := tabSQLExport;
|
|
tmBulkTableEdit: tabsTools.ActivePage := tabBulkTableEdit;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.DoExport(DBObj: TDBObject);
|
|
var
|
|
ToFile, ToDir, ToClipboard, ToDb, ToServer, IsLastRowInChunk, NeedsDBStructure: Boolean;
|
|
Struc, Header, DbDir, FinalDbName, BaseInsert, Row, TargetDbAndObject, BinContent: String;
|
|
MultiSQL: TStringList;
|
|
i: Integer;
|
|
RowCount, MaxRowsInChunk, RowsInChunk, Limit, Offset, ResultCount: Int64;
|
|
StartTime: Cardinal;
|
|
StrucResult, Data: TMySQLQuery;
|
|
rx: TRegExpr;
|
|
const
|
|
TempDelim = '//';
|
|
|
|
// Short version of Mainform.Mask()
|
|
function m(s: String): String;
|
|
begin
|
|
Result := Mainform.mask(s);
|
|
end;
|
|
|
|
// Pass output to file or query, and append semicolon if needed
|
|
procedure Output(SQL: String; IsEndOfQuery, ForFile, ForDir, ForDb, ForServer: Boolean);
|
|
var
|
|
SA: AnsiString;
|
|
ChunkSize: Integer;
|
|
begin
|
|
if (ToFile and ForFile) or (ToDir and ForDir) or (ToClipboard and ForFile) then begin
|
|
if IsEndOfQuery then
|
|
SQL := SQL + ';'+CRLF;
|
|
StreamWrite(ExportStream, SQL);
|
|
end;
|
|
if (ToDb and ForDb) or (ToServer and ForServer) then begin
|
|
StreamWrite(ExportStream, SQL);
|
|
if IsEndOfQuery then begin
|
|
ExportStream.Position := 0;
|
|
ChunkSize := ExportStream.Size;
|
|
SetLength(SA, ChunkSize div SizeOf(AnsiChar));
|
|
ExportStream.Read(PAnsiChar(SA)^, ChunkSize);
|
|
ExportStream.Size := 0;
|
|
SQL := UTF8ToString(SA);
|
|
if ToDB then Mainform.Connection.Query(SQL)
|
|
else if ToServer then FTargetConnection.Query(SQL);
|
|
SQL := '';
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure LogStatistic(RowsDone: Int64);
|
|
var
|
|
LogRow: TStringlist;
|
|
Percent: Double;
|
|
begin
|
|
LogRow := TStringList(FResults.Last);
|
|
Percent := 100 / Max(DBObj.Rows,1) * RowsDone;
|
|
LogRow[2] := FormatNumber(RowsDone) + ' / ' + FormatNumber(Percent, 0)+'%';
|
|
LogRow[3] := FormatTimeNumber((GetTickCount-StartTime) DIV 1000);
|
|
UpdateResultGrid;
|
|
end;
|
|
|
|
begin
|
|
// Handle one table, view or routine in SQL export mode
|
|
AddResults('SELECT '+esc(DBObj.Database)+' AS '+Mainform.mask('Database')+', ' +
|
|
esc(DBObj.Name)+' AS '+Mainform.mask('Table')+', ' +
|
|
IntToStr(DBObj.Rows)+' AS '+Mainform.mask('Rows')+', '+
|
|
'0 AS '+Mainform.mask('Duration')
|
|
);
|
|
ToFile := comboExportOutputType.Text = OUTPUT_FILE;
|
|
ToDir := comboExportOutputType.Text = OUTPUT_DIR;
|
|
ToClipboard := comboExportOutputType.Text = OUTPUT_CLIPBOARD;
|
|
ToDb := comboExportOutputType.Text = OUTPUT_DB;
|
|
ToServer := Copy(comboExportOutputType.Text, 1, Length(OUTPUT_SERVER)) = OUTPUT_SERVER;
|
|
StartTime := GetTickCount;
|
|
if ToDir then begin
|
|
FreeAndNil(ExportStream);
|
|
DbDir := comboExportOutputTarget.Text;
|
|
if DbDir[Length(DbDir)] <> '\' then
|
|
DbDir := DbDir + '\';
|
|
DbDir := DbDir + DBObj.Database + '\';
|
|
if not DirectoryExists(DbDir) then
|
|
ForceDirectories(DbDir);
|
|
ExportStream := TFileStream.Create(DbDir + DBObj.Name+'.sql', fmCreate or fmOpenWrite);
|
|
end;
|
|
if ToFile and (not Assigned(ExportStream)) then
|
|
ExportStream := TFileStream.Create(comboExportOutputTarget.Text, fmCreate or fmOpenWrite);
|
|
if ToClipboard and (not Assigned(ExportStream)) then
|
|
ExportStream := TMemoryStream.Create;
|
|
if ToDb or ToServer then
|
|
ExportStream := TMemoryStream.Create;
|
|
if (DBObj.Database<>ExportLastDatabase) or ToDir then begin
|
|
Header := '# --------------------------------------------------------' + CRLF +
|
|
Format('# %-30s%s', ['Host:', Mainform.Connection.Parameters.HostName]) + CRLF +
|
|
Format('# %-30s%s', ['Database:', DBObj.Database]) + CRLF +
|
|
Format('# %-30s%s', ['Server version:', Mainform.Connection.ServerVersionUntouched]) + CRLF +
|
|
Format('# %-30s%s', ['Server OS:', Mainform.Connection.GetVar('SHOW VARIABLES LIKE ' + esc('version_compile_os'), 1)]) + CRLF +
|
|
Format('# %-30s%s', [APPNAME + ' version:', Mainform.AppVersion]) + CRLF +
|
|
Format('# %-30s%s', ['Date/time:', DateTimeToStr(Now)]) + CRLF +
|
|
'# --------------------------------------------------------' + CRLF + CRLF +
|
|
'/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;' + CRLF +
|
|
'/*!40101 SET NAMES '+Mainform.Connection.CharacterSet+' */;' + CRLF +
|
|
'/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;' + CRLF +
|
|
'/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE=''NO_AUTO_VALUE_ON_ZERO'' */;';
|
|
Output(Header, False, DBObj.Database<>ExportLastDatabase, True, False, False);
|
|
end;
|
|
|
|
// Database structure. Do that only in single-file and server mode. drop/create/use in directory or database mode makes no sense
|
|
FinalDbName := DBObj.Database;
|
|
if ToDb or (ToServer and (comboExportOutputTarget.ItemIndex > 0)) then
|
|
FinalDbName := comboExportOutputTarget.Text;
|
|
NeedsDBStructure := FinalDbName <> ExportLastDatabase;
|
|
if chkExportDatabasesDrop.Checked or chkExportDatabasesCreate.Checked then begin
|
|
Output(CRLF+'# Dumping database structure for '+DBObj.Database+CRLF, False, NeedsDBStructure, False, False, False);
|
|
if chkExportDatabasesDrop.Checked and chkExportDatabasesDrop.Enabled then
|
|
Output('DROP DATABASE IF EXISTS '+m(FinalDbName), True, NeedsDBStructure, False, False, NeedsDBStructure);
|
|
if chkExportDatabasesCreate.Checked and chkExportDatabasesCreate.Enabled then begin
|
|
if Mainform.Connection.ServerVersionInt >= 40100 then begin
|
|
Struc := Mainform.Connection.GetVar('SHOW CREATE DATABASE '+m(DBObj.Database), 1);
|
|
// Gracefully ignore it when target database exists, important in server mode
|
|
Insert('IF NOT EXISTS ', Struc, Pos('DATABASE', Struc) + 9);
|
|
// Create the right dbname
|
|
Struc := StringReplace(Struc, DBObj.Database, FinalDbName, []);
|
|
end else
|
|
Struc := 'CREATE DATABASE IF NOT EXISTS '+m(FinalDbName);
|
|
Output(Struc, True, NeedsDBStructure, False, False, NeedsDBStructure);
|
|
Output('USE '+m(FinalDbName), True, NeedsDBStructure, False, False, NeedsDBStructure);
|
|
end;
|
|
end;
|
|
if ToServer and (not chkExportDatabasesCreate.Checked) then begin
|
|
// Export to server without "CREATE/USE dbname" and "Same dbs as on source server" - needs a "USE dbname"
|
|
Output('USE '+m(FinalDbName), True, False, False, False, NeedsDBStructure);
|
|
end;
|
|
|
|
// Table structure
|
|
if chkExportTablesDrop.Checked or chkExportTablesCreate.Checked then begin
|
|
Output(CRLF+CRLF+'# Dumping structure for '+LowerCase(DBObj.ObjType)+' '+DBObj.Database+'.'+DBObj.Name+CRLF, False, True, True, False, False);
|
|
if chkExportTablesDrop.Checked then begin
|
|
Struc := 'DROP '+UpperCase(DBObj.ObjType)+' IF EXISTS ';
|
|
if ToDb then
|
|
Struc := Struc + m(FinalDbName)+'.';
|
|
Struc := Struc + m(DBObj.Name);
|
|
Output(Struc, True, True, True, True, True);
|
|
end;
|
|
if chkExportTablesCreate.Checked then begin
|
|
try
|
|
case DBObj.NodeType of
|
|
lntTable, lntView: begin
|
|
Struc := Mainform.Connection.GetVar('SHOW CREATE TABLE '+m(DBObj.Database)+'.'+m(DBObj.Name), 1);
|
|
// Remove AUTO_INCREMENT clause if no data gets exported
|
|
if comboExportData.Text = DATA_NO then begin
|
|
rx := TRegExpr.Create;
|
|
rx.ModifierI := True;
|
|
rx.Expression := '\sAUTO_INCREMENT\s*\=\s*\d+\s';
|
|
Struc := rx.Replace(Struc, ' ', false);
|
|
rx.Free;
|
|
end;
|
|
if DBObj.NodeType = lntTable then
|
|
Insert('IF NOT EXISTS ', Struc, Pos('TABLE', Struc) + 6);
|
|
if ToDb then begin
|
|
if DBObj.NodeType = lntTable then
|
|
Insert(m(FinalDbName)+'.', Struc, Pos('EXISTS', Struc) + 7 )
|
|
else if DBObj.NodeType = lntView then
|
|
Insert(m(FinalDbName)+'.', Struc, Pos('VIEW', Struc) + 5 );
|
|
end;
|
|
end;
|
|
|
|
lntTrigger: begin
|
|
StrucResult := Mainform.Connection.GetResults('SHOW TRIGGERS WHERE `Trigger`='+esc(DBObj.Name));
|
|
Struc := 'CREATE '+UpperCase(DBObj.ObjType)+' '+m(DBObj.Name)+' '+StrucResult.Col('Timing')+' '+StrucResult.Col('Event')+
|
|
' ON '+m(StrucResult.Col('Table'))+' FOR EACH ROW '+StrucResult.Col('Statement');
|
|
if ToDb then
|
|
Insert(m(FinalDbName)+'.', Struc, Pos('TRIGGER', Struc) + 8 );
|
|
if ToFile or ToClipboard or ToDir then begin
|
|
Struc := 'SET SESSION SQL_MODE=' + esc(StrucResult.Col('sql_mode')) + ';' + CRLF +
|
|
'DELIMITER ' + TempDelim + CRLF +
|
|
Struc + TempDelim + CRLF +
|
|
'DELIMITER ;' + CRLF +
|
|
'SET SESSION SQL_MODE=@OLD_SQL_MODE';
|
|
end;
|
|
end;
|
|
|
|
lntFunction, lntProcedure: begin
|
|
Struc := Mainform.Connection.GetVar('SHOW CREATE '+UpperCase(DBObj.ObjType)+' '+m(DBObj.Database)+'.'+m(DBObj.Name), 2);
|
|
// Todo: Why exploding?
|
|
MultiSQL := Explode(';', Struc);
|
|
Struc := ImplodeStr(';'+CRLF, MultiSQL);
|
|
if ToDb then begin
|
|
if DBObj.NodeType = lntProcedure then
|
|
Insert(m(FinalDbName)+'.', Struc, Pos('PROCEDURE', Struc) + 10 )
|
|
else if DBObj.NodeType = lntFunction then
|
|
Insert(m(FinalDbName)+'.', Struc, Pos('FUNCTION', Struc) + 9 );
|
|
end;
|
|
// Change delimiter for file output, so readers split queries at the right string position
|
|
if ToFile or ToDir or ToClipboard then
|
|
Struc := 'DELIMITER ' + TempDelim + CRLF + Struc + TempDelim + CRLF + 'DELIMITER ';
|
|
end;
|
|
end;
|
|
Struc := fixNewlines(Struc);
|
|
Output(Struc, True, True, True, True, True);
|
|
except
|
|
On E:Exception do begin
|
|
// Catch the exception message and dump it into the export file for debugging reasons
|
|
Output('/* '+E.Message+' */', False, True, True, False, False);
|
|
Raise;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
if DBObj.NodeType = lntTable then begin
|
|
// Table data
|
|
if comboExportData.Text = DATA_NO then begin
|
|
Output(CRLF+'# Data exporting was unselected.'+CRLF, False, True, True, False, False);
|
|
end else begin
|
|
Output(CRLF+'# Dumping data for table '+DBObj.Database+'.'+DBObj.Name+': '+FormatNumber(DBObj.Rows)+' rows'+CRLF, False, True, True, False, False);
|
|
TargetDbAndObject := m(DBObj.Name);
|
|
if ToDb then
|
|
TargetDbAndObject := m(FinalDbName) + '.' + TargetDbAndObject;
|
|
Offset := 0;
|
|
RowCount := 0;
|
|
// Calculate limit so we select ~100MB per loop
|
|
Limit := Round(100 * SIZE_MB / Max(DBObj.AvgRowLen,1));
|
|
// Calculate max rows per INSERT, so we should never exceed 1MB per INSERT
|
|
MaxRowsInChunk := Round(SIZE_MB * 0.4 / Max(DBObj.AvgRowLen,1));
|
|
if comboExportData.Text = DATA_REPLACE then
|
|
Output('DELETE FROM '+TargetDbAndObject, True, True, True, True, True);
|
|
Output('/*!40000 ALTER TABLE '+TargetDbAndObject+' DISABLE KEYS */', True, True, True, True, True);
|
|
while true do begin
|
|
Data := Mainform.Connection.GetResults('SELECT * FROM '+m(DBObj.Database)+'.'+m(DBObj.Name)+' LIMIT '+IntToStr(Offset)+', '+IntToStr(Limit));
|
|
Inc(Offset, Limit);
|
|
if Data.RecordCount = 0 then
|
|
break;
|
|
BaseInsert := 'INSERT INTO ';
|
|
if comboExportData.Text = DATA_INSERTNEW then
|
|
BaseInsert := 'INSERT IGNORE INTO '
|
|
else if comboExportData.Text = DATA_UPDATE then
|
|
BaseInsert := 'REPLACE INTO ';
|
|
BaseInsert := BaseInsert + TargetDbAndObject + ' (';
|
|
for i:=0 to Data.ColumnCount-1 do
|
|
BaseInsert := BaseInsert + m(Data.ColumnNames[i]) + ', ';
|
|
Delete(BaseInsert, Length(BaseInsert)-1, 2);
|
|
BaseInsert := BaseInsert + ') VALUES (';
|
|
while true do begin
|
|
RowsInChunk := 0;
|
|
Output(BaseInsert, False, True, True, True, True);
|
|
|
|
while not Data.Eof do begin
|
|
Inc(RowCount);
|
|
Inc(RowsInChunk);
|
|
Row := '';
|
|
for i:=0 to Data.ColumnCount-1 do begin
|
|
if Data.IsNull(i) then
|
|
Row := Row + 'NULL'
|
|
else case Data.DataType(i).Category of
|
|
dtcInteger, dtcReal: Row := Row + Data.Col(i);
|
|
dtcBinary: begin
|
|
BinContent := Data.BinColAsHex(i);
|
|
if Length(BinContent) > 0 then
|
|
Row := Row + '_binary 0x' + BinContent
|
|
else
|
|
Row := Row + esc('');
|
|
end;
|
|
else Row := Row + esc(Data.Col(i));
|
|
end;
|
|
if i<Data.ColumnCount-1 then
|
|
Row := Row + ', ';
|
|
end;
|
|
Row := Row + ')';
|
|
Data.Next;
|
|
IsLastRowInChunk := (RowsInChunk = MaxRowsInChunk) or Data.Eof;
|
|
if not IsLastRowInChunk then
|
|
Row := Row + ', (';
|
|
Output(Row, False, True, True, True, True);
|
|
if IsLastRowInChunk then
|
|
break;
|
|
end;
|
|
Output('', True, True, True, True, True);
|
|
LogStatistic(RowCount);
|
|
if Data.Eof then
|
|
break;
|
|
|
|
end;
|
|
ResultCount := Data.RecordCount;
|
|
FreeAndNil(Data);
|
|
// Break if end of table data, avoid one last empty/useless SELECT in next loop
|
|
if ResultCount < Limit then
|
|
break;
|
|
|
|
end;
|
|
Output('/*!40000 ALTER TABLE '+TargetDbAndObject+' ENABLE KEYS */', True, True, True, True, True);
|
|
Output(EXPORT_FILE_FOOTER, False, False, True, False, False);
|
|
LogStatistic(DbObj.Rows); // Cosmetic fix for estimated InnoDB row count
|
|
end;
|
|
end;
|
|
|
|
ExportLastDatabase := FinalDbName;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.chkBulkTableEditCheckComboClick(Sender: TObject);
|
|
var
|
|
chk: TCheckBox;
|
|
combo: TWinControl;
|
|
begin
|
|
chk := TCheckBox(Sender);
|
|
if chk = chkBulkTableEditDatabase then combo := comboBulkTableEditDatabase
|
|
else if chk = chkBulkTableEditEngine then combo := comboBulkTableEditEngine
|
|
else if chk = chkBulkTableEditCollation then combo := comboBulkTableEditCollation
|
|
else combo := comboBulkTableEditCharset;
|
|
combo.Enabled := chk.Checked;
|
|
ValidateControls(Sender);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableTools.DoBulkTableEdit(DBObj: TDBObject);
|
|
var
|
|
Specs, LogRow: TStringList;
|
|
CreateView: String;
|
|
rx: TRegExpr;
|
|
HasCharsetClause: Boolean;
|
|
begin
|
|
AddResults('SELECT '+esc(DBObj.Database)+' AS '+Mainform.mask('Database')+', ' +
|
|
esc(DBObj.Name)+' AS '+Mainform.mask('Table')+', ' +
|
|
esc('Updating...')+' AS '+Mainform.mask('Operation')+', '+
|
|
''''' AS '+Mainform.mask('Result')
|
|
);
|
|
Specs := TStringList.Create;
|
|
if chkBulkTableEditDatabase.Checked and (comboBulkTableEditDatabase.Text <> DBObj.Database) then begin
|
|
case DBObj.NodeType of
|
|
lntTable: Specs.Add('RENAME ' + Mainform.mask(comboBulkTableEditDatabase.Text)+'.'+Mainform.mask(DBObj.Name));
|
|
lntView: begin
|
|
// Although RENAME works for views, that does not work for moving to another database without getting
|
|
// a "Changing schema from x to y is not allowed". Instead, recreate them manually
|
|
CreateView := Mainform.Connection.GetVar('SHOW CREATE VIEW '+Mainform.mask(DBObj.Database) + '.' + Mainform.mask(DBObj.Name), 1);
|
|
rx := TRegExpr.Create;
|
|
rx.ModifierI := True;
|
|
// Replace old database references in VIEW body
|
|
rx.Expression := '(["`])'+QuoteRegExprMetaChars(DBObj.Database)+'(["`])';
|
|
CreateView := rx.Replace(CreateView, Mainform.mask(comboBulkTableEditDatabase.Text), false);
|
|
rx.Free;
|
|
// Temporarily switch to new database for VIEW creation, so the database references are correct
|
|
Mainform.Connection.Database := comboBulkTableEditDatabase.Text;
|
|
Mainform.Connection.Query(CreateView);
|
|
Mainform.Connection.Database := DBObj.Database;
|
|
Mainform.Connection.Query('DROP VIEW '+Mainform.mask(DBObj.Name));
|
|
end;
|
|
end;
|
|
FModifiedDbs.Add(DBObj.Database);
|
|
FModifiedDbs.Add(comboBulkTableEditDatabase.Text);
|
|
end;
|
|
if (DBObj.NodeType = lntTable) and chkBulkTableEditEngine.Checked then begin
|
|
if Mainform.Connection.ServerVersionInt < 40018 then
|
|
Specs.Add('TYPE '+comboBulkTableEditEngine.Text)
|
|
else
|
|
Specs.Add('ENGINE '+comboBulkTableEditEngine.Text);
|
|
end;
|
|
if DBObj.NodeType = lntTable then begin
|
|
HasCharsetClause := False;
|
|
if chkBulkTableEditCharset.Checked and (comboBulkTableEditCharset.ItemIndex > -1) then begin
|
|
Mainform.Connection.CharsetTable.RecNo := comboBulkTableEditCharset.ItemIndex;
|
|
Specs.Add('CONVERT TO CHARSET '+Mainform.Connection.CharsetTable.Col('Charset'));
|
|
HasCharsetClause := True;
|
|
end;
|
|
if chkBulkTableEditCollation.Checked and (comboBulkTableEditCollation.ItemIndex > -1) then begin
|
|
if HasCharsetClause then // No comma between charset + collation clause
|
|
Specs[Specs.Count-1] := Specs[Specs.Count-1] + ' COLLATE '+comboBulkTableEditCollation.Text
|
|
else
|
|
Specs.Add('COLLATE '+comboBulkTableEditCollation.Text);
|
|
end;
|
|
if chkBulkTableEditResetAutoinc.Checked then
|
|
Specs.Add('AUTO_INCREMENT=0');
|
|
end;
|
|
|
|
LogRow := TStringList(FResults.Last);
|
|
if Specs.Count > 0 then begin
|
|
Mainform.Connection.Query('ALTER TABLE ' + Mainform.mask(DBObj.Database) + '.' + Mainform.mask(DBObj.Name) + ' ' + ImplodeStr(', ', Specs));
|
|
LogRow[2] := 'Done';
|
|
LogRow[3] := 'Success';
|
|
end else begin
|
|
LogRow[2] := 'Nothing to do';
|
|
LogRow[3] := 'Selected operations cannot be applied to a '+LowerCase(DBObj.ObjType);
|
|
end;
|
|
UpdateResultGrid;
|
|
end;
|
|
|
|
|
|
end.
|