mirror of
https://github.com/HeidiSQL/HeidiSQL.git
synced 2025-08-06 18:24:26 +08:00
2236 lines
77 KiB
ObjectPascal
2236 lines
77 KiB
ObjectPascal
unit table_editor;
|
|
|
|
interface
|
|
|
|
uses
|
|
Windows, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls,
|
|
ComCtrls, ToolWin, VirtualTrees, SynRegExpr, ActiveX, ExtCtrls, SynEdit,
|
|
SynMemo, Menus, Clipbrd, Math,
|
|
grideditlinks, mysql_structures, dbconnection, helpers;
|
|
|
|
type
|
|
TFrame = TDBObjectEditor;
|
|
TfrmTableEditor = class(TFrame)
|
|
btnSave: TButton;
|
|
btnDiscard: TButton;
|
|
btnHelp: TButton;
|
|
listColumns: TVirtualStringTree;
|
|
PageControlMain: TPageControl;
|
|
tabBasic: TTabSheet;
|
|
tabIndexes: TTabSheet;
|
|
tabOptions: TTabSheet;
|
|
lblName: TLabel;
|
|
editName: TEdit;
|
|
memoComment: TMemo;
|
|
lblComment: TLabel;
|
|
lblAutoinc: TLabel;
|
|
lblAvgRowLen: TLabel;
|
|
lblInsertMethod: TLabel;
|
|
lblUnion: TLabel;
|
|
lblMaxRows: TLabel;
|
|
lblRowFormat: TLabel;
|
|
editAutoInc: TEdit;
|
|
editAvgRowLen: TEdit;
|
|
editMaxRows: TEdit;
|
|
chkChecksum: TCheckBox;
|
|
comboRowFormat: TComboBox;
|
|
memoUnionTables: TMemo;
|
|
comboInsertMethod: TComboBox;
|
|
lblCollation: TLabel;
|
|
comboCollation: TComboBox;
|
|
lblEngine: TLabel;
|
|
comboEngine: TComboBox;
|
|
treeIndexes: TVirtualStringTree;
|
|
tlbIndexes: TToolBar;
|
|
btnAddIndex: TToolButton;
|
|
btnRemoveIndex: TToolButton;
|
|
btnClearIndexes: TToolButton;
|
|
btnMoveUpIndex: TToolButton;
|
|
btnMoveDownIndex: TToolButton;
|
|
pnlColumnsTop: TPanel;
|
|
tlbColumns: TToolBar;
|
|
btnAddColumn: TToolButton;
|
|
btnRemoveColumn: TToolButton;
|
|
btnMoveUpColumn: TToolButton;
|
|
btnMoveDownColumn: TToolButton;
|
|
SplitterTopBottom: TSplitter;
|
|
tabCREATEcode: TTabSheet;
|
|
tabALTERCode: TTabSheet;
|
|
SynMemoCREATEcode: TSynMemo;
|
|
SynMemoALTERcode: TSynMemo;
|
|
popupIndexes: TPopupMenu;
|
|
menuAddIndex: TMenuItem;
|
|
menuAddIndexColumn: TMenuItem;
|
|
menuRemoveIndex: TMenuItem;
|
|
menuMoveUpIndex: TMenuItem;
|
|
menuMoveDownIndex: TMenuItem;
|
|
menuClearIndexes: TMenuItem;
|
|
popupColumns: TPopupMenu;
|
|
menuAddColumn: TMenuItem;
|
|
menuRemoveColumn: TMenuItem;
|
|
menuMoveUpColumn: TMenuItem;
|
|
menuMoveDownColumn: TMenuItem;
|
|
chkCharsetConvert: TCheckBox;
|
|
N1: TMenuItem;
|
|
menuCreateIndex: TMenuItem;
|
|
menuAddToIndex: TMenuItem;
|
|
tabForeignKeys: TTabSheet;
|
|
tlbForeignKeys: TToolBar;
|
|
btnAddForeignKey: TToolButton;
|
|
btnRemoveForeignKey: TToolButton;
|
|
btnClearForeignKeys: TToolButton;
|
|
menuCopyColumnCell: TMenuItem;
|
|
N2: TMenuItem;
|
|
popupSQLmemo: TPopupMenu;
|
|
menuSQLCopy: TMenuItem;
|
|
menuSQLSelectAll: TMenuItem;
|
|
pnlNoForeignKeys: TPanel;
|
|
listForeignKeys: TVirtualStringTree;
|
|
menuCopyColumns: TMenuItem;
|
|
menuPasteColumns: TMenuItem;
|
|
procedure Modification(Sender: TObject);
|
|
procedure btnAddColumnClick(Sender: TObject);
|
|
procedure btnRemoveColumnClick(Sender: TObject);
|
|
procedure listColumnsFocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex);
|
|
procedure btnHelpClick(Sender: TObject);
|
|
procedure listColumnsGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
|
|
Column: TColumnIndex; TextType: TVSTTextType; var CellText: String);
|
|
procedure listColumnsEditing(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; var Allowed: Boolean);
|
|
procedure listColumnsNewText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; NewText: String);
|
|
procedure btnMoveUpColumnClick(Sender: TObject);
|
|
procedure btnMoveDownColumnClick(Sender: TObject);
|
|
procedure listColumnsDragOver(Sender: TBaseVirtualTree; Source: TObject; Shift: TShiftState; State: TDragState;
|
|
Pt: TPoint; Mode: TDropMode; var Effect: Integer; var Accept: Boolean);
|
|
procedure listColumnsDragDrop(Sender: TBaseVirtualTree; Source: TObject; DataObject: IDataObject; Formats: TFormatArray;
|
|
Shift: TShiftState; Pt: TPoint; var Effect: Integer; Mode: TDropMode);
|
|
procedure listColumnsPaintText(Sender: TBaseVirtualTree; const TargetCanvas: TCanvas; Node: PVirtualNode;
|
|
Column: TColumnIndex; TextType: TVSTTextType);
|
|
procedure listColumnsCreateEditor(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; out EditLink: IVTEditLink);
|
|
procedure treeIndexesInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
|
|
procedure treeIndexesGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
|
|
var CellText: String);
|
|
procedure treeIndexesBeforePaint(Sender: TBaseVirtualTree; TargetCanvas: TCanvas);
|
|
procedure treeIndexesInitChildren(Sender: TBaseVirtualTree; Node: PVirtualNode; var ChildCount: Cardinal);
|
|
procedure treeIndexesGetImageIndex(Sender: TBaseVirtualTree; Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex;
|
|
var Ghosted: Boolean; var ImageIndex: Integer);
|
|
procedure btnClearIndexesClick(Sender: TObject);
|
|
procedure btnSaveClick(Sender: TObject);
|
|
procedure editNumEditChange(Sender: TObject);
|
|
procedure comboEngineSelect(Sender: TObject);
|
|
procedure listColumnsClick(Sender: TObject);
|
|
procedure listColumnsBeforeCellPaint(Sender: TBaseVirtualTree;
|
|
TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
|
|
CellPaintMode: TVTCellPaintMode; CellRect: TRect; var ContentRect: TRect);
|
|
procedure listColumnsAfterCellPaint(Sender: TBaseVirtualTree;
|
|
TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
|
|
CellRect: TRect);
|
|
procedure btnAddIndexClick(Sender: TObject);
|
|
procedure treeIndexesDragOver(Sender: TBaseVirtualTree; Source: TObject;
|
|
Shift: TShiftState; State: TDragState; Pt: TPoint; Mode: TDropMode;
|
|
var Effect: Integer; var Accept: Boolean);
|
|
procedure treeIndexesDragDrop(Sender: TBaseVirtualTree; Source: TObject;
|
|
DataObject: IDataObject; Formats: TFormatArray; Shift: TShiftState;
|
|
Pt: TPoint; var Effect: Integer; Mode: TDropMode);
|
|
procedure treeIndexesNewText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; NewText: String);
|
|
procedure treeIndexesEditing(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; var Allowed: Boolean);
|
|
procedure treeIndexesFocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex);
|
|
procedure treeIndexesCreateEditor(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; out EditLink: IVTEditLink);
|
|
procedure btnMoveUpIndexClick(Sender: TObject);
|
|
procedure btnMoveDownIndexClick(Sender: TObject);
|
|
procedure btnRemoveIndexClick(Sender: TObject);
|
|
procedure menuAddIndexColumnClick(Sender: TObject);
|
|
procedure PageControlMainChange(Sender: TObject);
|
|
procedure chkCharsetConvertClick(Sender: TObject);
|
|
procedure treeIndexesClick(Sender: TObject);
|
|
procedure btnDiscardClick(Sender: TObject);
|
|
procedure popupColumnsPopup(Sender: TObject);
|
|
procedure AddIndexByColumn(Sender: TObject);
|
|
procedure listForeignKeysBeforePaint(Sender: TBaseVirtualTree; TargetCanvas: TCanvas);
|
|
procedure listForeignKeysCreateEditor(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex; out EditLink: IVTEditLink);
|
|
procedure listForeignKeysFocusChanged(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex);
|
|
procedure listForeignKeysGetImageIndex(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex;
|
|
var Ghosted: Boolean; var ImageIndex: Integer);
|
|
procedure listForeignKeysGetText(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
|
|
var CellText: String);
|
|
procedure listForeignKeysNewText(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex; NewText: String);
|
|
procedure btnClearForeignKeysClick(Sender: TObject);
|
|
procedure btnAddForeignKeyClick(Sender: TObject);
|
|
procedure btnRemoveForeignKeyClick(Sender: TObject);
|
|
procedure listForeignKeysEditing(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex;
|
|
var Allowed: Boolean);
|
|
procedure listColumnsInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode;
|
|
var InitialStates: TVirtualNodeInitStates);
|
|
procedure listColumnsGetNodeDataSize(Sender: TBaseVirtualTree; var NodeDataSize: Integer);
|
|
procedure listColumnsNodeMoved(Sender: TBaseVirtualTree; Node: PVirtualNode);
|
|
procedure popupSQLmemoPopup(Sender: TObject);
|
|
procedure listColumnsKeyPress(Sender: TObject; var Key: Char);
|
|
procedure vtHandleClickOrKeyPress(Sender: TVirtualStringTree;
|
|
Node: PVirtualNode; Column: TColumnIndex; HitPositions: THitPositions);
|
|
procedure menuCopyColumnsClick(Sender: TObject);
|
|
procedure menuPasteColumnsClick(Sender: TObject);
|
|
private
|
|
{ Private declarations }
|
|
FLoaded: Boolean;
|
|
CreateCodeValid, AlterCodeValid: Boolean;
|
|
FColumns: TTableColumnList;
|
|
FKeys: TTableKeyList;
|
|
FForeignKeys: TForeignKeyList;
|
|
DeletedKeys, DeletedForeignKeys: TStringList;
|
|
procedure ValidateColumnControls;
|
|
procedure ValidateIndexControls;
|
|
procedure MoveFocusedIndexPart(NewIdx: Cardinal);
|
|
procedure ResetModificationFlags;
|
|
function ComposeCreateStatement: TSQLBatch;
|
|
function ComposeAlterStatement: TSQLBatch;
|
|
procedure UpdateSQLcode;
|
|
function CellEditingAllowed(Node: PVirtualNode; Column: TColumnIndex): Boolean;
|
|
procedure CalcMinColWidth;
|
|
public
|
|
{ Public declarations }
|
|
constructor Create(AOwner: TComponent); override;
|
|
destructor Destroy; override;
|
|
procedure Init(Obj: TDBObject); override;
|
|
function ApplyModifications: TModalResult; override;
|
|
end;
|
|
|
|
|
|
implementation
|
|
|
|
uses main;
|
|
|
|
|
|
{$R *.dfm}
|
|
|
|
|
|
constructor TfrmTableEditor.Create(AOwner: TComponent);
|
|
begin
|
|
inherited;
|
|
PageControlMain.Height := GetRegValue(REGNAME_TABLEEDITOR_TABSHEIGHT, DEFAULT_TABLEEDITOR_TABSHEIGHT);
|
|
FixVT(listColumns);
|
|
FixVT(treeIndexes);
|
|
FixVT(listForeignKeys);
|
|
// Try the best to auto fit various column widths, respecting a custom DPI setting and a pulldown arrow
|
|
listColumns.Header.Columns[2].Width := Mainform.Canvas.TextWidth('GEOMETRYCOLLECTION') + 6*listColumns.TextMargin;
|
|
listColumns.Header.Columns[7].Width := Mainform.Canvas.TextWidth('AUTO_INCREMENT') + 4*listColumns.TextMargin;
|
|
listColumns.Header.Columns[9].Width := Mainform.Canvas.TextWidth('macroman_general_ci') + 6*listColumns.TextMargin;
|
|
// Overide column widths by custom values
|
|
Mainform.RestoreListSetup(listColumns);
|
|
Mainform.RestoreListSetup(treeIndexes);
|
|
Mainform.RestoreListSetup(listForeignKeys);
|
|
comboRowFormat.Items.CommaText := 'DEFAULT,DYNAMIC,FIXED,COMPRESSED,REDUNDANT,COMPACT';
|
|
comboInsertMethod.Items.CommaText := 'NO,FIRST,LAST';
|
|
FColumns := TTableColumnList.Create;
|
|
FKeys := TTableKeyList.Create;
|
|
FForeignKeys := TForeignKeyList.Create;
|
|
DeletedKeys := TStringList.Create;
|
|
DeletedForeignKeys := TStringList.Create;
|
|
editName.MaxLength := NAME_LEN;
|
|
end;
|
|
|
|
|
|
destructor TfrmTableEditor.Destroy;
|
|
begin
|
|
// Store GUI setup
|
|
OpenRegistry;
|
|
MainReg.WriteInteger(REGNAME_TABLEEDITOR_TABSHEIGHT, PageControlMain.Height);
|
|
Mainform.SaveListSetup(listColumns);
|
|
Mainform.SaveListSetup(treeIndexes);
|
|
Mainform.SaveListSetup(listForeignKeys);
|
|
inherited;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.Init(Obj: TDBObject);
|
|
var
|
|
AttrName, AttrValue: String;
|
|
rx: TRegExpr;
|
|
begin
|
|
inherited;
|
|
FLoaded := False;
|
|
comboEngine.Items := DBObject.Connection.TableEngines;
|
|
comboEngine.ItemIndex := comboEngine.Items.IndexOf(DBObject.Connection.TableEngineDefault);
|
|
comboCollation.Items := DBObject.Connection.CollationList;
|
|
if DBObject.Connection.Parameters.IsMariaDB then begin
|
|
with listColumns.Header do begin
|
|
Columns[10].Options := Columns[10].Options + [coVisible];
|
|
Columns[11].Options := Columns[11].Options + [coVisible];
|
|
end;
|
|
end;
|
|
listColumns.BeginUpdate;
|
|
FColumns.Clear;
|
|
btnClearIndexesClick(Self);
|
|
btnClearForeignKeysClick(Self);
|
|
tabALTERcode.TabVisible := DBObject.Name <> '';
|
|
// Clear value editors
|
|
memoComment.Text := '';
|
|
if Obj.Connection.ServerVersionInt < 50503 then
|
|
memoComment.MaxLength := 60
|
|
else
|
|
memoComment.MaxLength := 2048;
|
|
editAutoInc.Text := '';
|
|
editAvgRowLen.Text := '';
|
|
editMaxRows.Text := '';
|
|
chkChecksum.Checked := False;
|
|
comboRowFormat.ItemIndex := 0;
|
|
comboCollation.ItemIndex := -1;
|
|
memoUnionTables.Clear;
|
|
comboInsertMethod.ItemIndex := -1;
|
|
|
|
if DBObject.Name = '' then begin
|
|
// Creating new table
|
|
editName.Text := '';
|
|
if DBObject.Connection.Parameters.NetTypeGroup = ngMySQL then
|
|
comboCollation.ItemIndex := comboCollation.Items.IndexOf(DBObject.Connection.GetVar('SHOW VARIABLES LIKE ''collation_database''', 1));
|
|
PageControlMain.ActivePage := tabBasic;
|
|
end else begin
|
|
// Editing existing table
|
|
editName.Text := DBObject.Name;
|
|
// Try collation from SHOW TABLE STATUS, sometimes missing in SHOW CREATE TABLE result
|
|
comboCollation.ItemIndex := comboCollation.Items.IndexOf(DBObject.Collation);
|
|
rx := TRegExpr.Create;
|
|
rx.ModifierI := True;
|
|
rx.Expression := '\s(\S+)\s*=\s*(\S+)';
|
|
if rx.Exec(DBObject.CreateCode) then while true do begin
|
|
AttrName := UpperCase(rx.Match[1]);
|
|
AttrValue := rx.Match[2];
|
|
if (AttrName='ENGINE') or (AttrName='TYPE') then
|
|
comboEngine.ItemIndex := comboEngine.Items.IndexOf(AttrValue)
|
|
else if AttrName='COLLATE' then
|
|
comboCollation.ItemIndex := comboCollation.Items.IndexOf(AttrValue)
|
|
else if AttrName='AVG_ROW_LENGTH' then
|
|
editAvgRowLen.Text := AttrValue
|
|
else if AttrName='AUTO_INCREMENT' then
|
|
editAutoInc.Text := AttrValue
|
|
else if AttrName='ROW_FORMAT' then
|
|
comboRowFormat.ItemIndex := comboRowFormat.Items.IndexOf(AttrValue)
|
|
else if AttrName='CHECKSUM' then
|
|
chkChecksum.Checked := AttrValue='1'
|
|
else if AttrName='MAX_ROWS' then
|
|
editMaxRows.Text := AttrValue
|
|
else if AttrName='INSERT_METHOD' then
|
|
comboInsertMethod.ItemIndex := comboInsertMethod.Items.IndexOf(AttrValue);
|
|
if not rx.ExecNext then
|
|
break;
|
|
end;
|
|
|
|
rx.Expression := '\bUNION=\((.+)\)';
|
|
if rx.Exec(DBObject.CreateCode) then
|
|
memoUnionTables.Lines.Text := rx.Match[1]
|
|
else
|
|
memoUnionTables.Lines.Clear;
|
|
rx.Expression := '\bCOMMENT=''((.+)[^''])''';
|
|
if rx.Exec(DBObject.CreateCode) then
|
|
memoComment.Lines.Text := DBObject.Connection.UnescapeString(rx.Match[1])
|
|
else
|
|
memoComment.Lines.Clear;
|
|
DBObject.Connection.ParseTableStructure(DBObject.CreateCode, FColumns, FKeys, FForeignKeys);
|
|
end;
|
|
listColumns.RootNodeCount := FColumns.Count;
|
|
DeInitializeVTNodes(listColumns);
|
|
listColumns.EndUpdate;
|
|
// Validate controls
|
|
comboEngineSelect(comboEngine);
|
|
ValidateColumnControls;
|
|
ValidateIndexControls;
|
|
ResetModificationFlags;
|
|
CreateCodeValid := False;
|
|
AlterCodeValid := False;
|
|
PageControlMainChange(Self); // Foreign key editor needs a hit
|
|
UpdateSQLCode;
|
|
CalcMinColWidth;
|
|
// Indicate change mechanisms can call their events now. See Modification().
|
|
FLoaded := True;
|
|
// Empty status panel
|
|
Mainform.ShowStatusMsg;
|
|
Screen.Cursor := crDefault;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.btnDiscardClick(Sender: TObject);
|
|
begin
|
|
// Reinit GUI, discarding changes
|
|
Modified := False;
|
|
Init(DBObject);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.btnSaveClick(Sender: TObject);
|
|
begin
|
|
ApplyModifications;
|
|
UpdateSQLcode;
|
|
end;
|
|
|
|
|
|
function TfrmTableEditor.ApplyModifications: TModalResult;
|
|
var
|
|
Batch: TSQLBatch;
|
|
Query: TSQLSentence;
|
|
i: Integer;
|
|
begin
|
|
// Create or alter table
|
|
Result := mrOk;
|
|
if DBObject.Name = '' then
|
|
Batch := ComposeCreateStatement
|
|
else
|
|
Batch := ComposeAlterStatement;
|
|
try
|
|
for Query in Batch do
|
|
DBObject.Connection.Query(Query.SQL);
|
|
tabALTERcode.TabVisible := DBObject.Name <> '';
|
|
if chkCharsetConvert.Checked then begin
|
|
// Autoadjust column collations
|
|
for i:=0 to FColumns.Count-1 do begin
|
|
if FColumns[i].Collation <> '' then
|
|
FColumns[i].Collation := comboCollation.Text;
|
|
end;
|
|
end;
|
|
// Set table name for altering if Apply was clicked
|
|
DBObject.Name := editName.Text;
|
|
DBObject.CreateCode := '';
|
|
tabALTERcode.TabVisible := DBObject.Name <> '';
|
|
Mainform.UpdateEditorTab;
|
|
Mainform.RefreshTree(DBObject);
|
|
Mainform.RefreshHelperNode(HELPERNODE_COLUMNS);
|
|
ResetModificationFlags;
|
|
AlterCodeValid := False;
|
|
CreateCodeValid := False;
|
|
except
|
|
on E:EDatabaseError do begin
|
|
ErrorDialog(E.Message);
|
|
Result := mrAbort;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.ResetModificationFlags;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
// Enable converting data for an existing table
|
|
chkCharsetConvertClick(comboCollation);
|
|
// Assist the user in auto unchecking this checkbox so data doesn't get converted more than once accidently
|
|
chkCharsetConvert.Checked := False;
|
|
// Reset modification flags of TEdits and TMemos
|
|
for i:=0 to ComponentCount-1 do
|
|
Components[i].Tag := 0;
|
|
// Reset column changes
|
|
for i:=FColumns.Count-1 downto 0 do begin
|
|
if FColumns[i].Status = esDeleted then
|
|
FColumns.Delete(i)
|
|
else begin
|
|
FColumns[i].OldName := FColumns[i].Name;
|
|
FColumns[i].Status := esUntouched;
|
|
end;
|
|
end;
|
|
DeletedKeys.Clear;
|
|
for i:=0 to FKeys.Count-1 do begin
|
|
FKeys[i].OldName := FKeys[i].Name;
|
|
FKeys[i].OldIndexType := FKeys[i].IndexType;
|
|
FKeys[i].Added := False;
|
|
FKeys[i].Modified := False;
|
|
end;
|
|
DeletedForeignKeys.Clear;
|
|
for i:=0 to FForeignKeys.Count-1 do begin
|
|
FForeignKeys[i].OldKeyName := FForeignKeys[i].KeyName;
|
|
FForeignKeys[i].Added := False;
|
|
FForeignKeys[i].Modified := False;
|
|
end;
|
|
Modified := False;
|
|
btnSave.Enabled := Modified;
|
|
btnDiscard.Enabled := Modified;
|
|
end;
|
|
|
|
|
|
function TfrmTableEditor.ComposeAlterStatement: TSQLBatch;
|
|
var
|
|
Specs: TStringList;
|
|
ColSpec, IndexSQL, SQL: String;
|
|
i: Integer;
|
|
Results: TDBQuery;
|
|
Col, PreviousCol: PTableColumn;
|
|
Node: PVirtualNode;
|
|
IsVirtual: Boolean;
|
|
|
|
procedure AddQuery;
|
|
begin
|
|
if Specs.Count > 0 then begin
|
|
SQL := SQL + Trim('ALTER TABLE '+DBObject.Connection.QuoteIdent(DBObject.Name) + CRLF + #9 + ImplodeStr(',' + CRLF + #9, Specs)) + ';' + CRLF;
|
|
Specs.Clear;
|
|
end;
|
|
end;
|
|
begin
|
|
// Compose ALTER query, called by buttons and for SQL code tab
|
|
Mainform.ShowStatusMsg('Composing ALTER statement ...');
|
|
Screen.Cursor := crHourglass;
|
|
Specs := TStringList.Create;
|
|
SQL := '';
|
|
|
|
// Special case for altered foreign keys: These have to be dropped in a seperate query
|
|
// otherwise the server would return error 121 "Duplicate key on write or update"
|
|
// See also http://dev.mysql.com/doc/refman/5.1/en/innodb-foreign-key-constraints.html :
|
|
// "You cannot add a foreign key and drop a foreign key in separate clauses of a single
|
|
// ALTER TABLE statement. Separate statements are required."
|
|
for i:=0 to FForeignKeys.Count-1 do begin
|
|
if FForeignKeys[i].Modified and (not FForeignKeys[i].Added) then
|
|
Specs.Add('DROP FOREIGN KEY '+DBObject.Connection.QuoteIdent(FForeignKeys[i].OldKeyName));
|
|
end;
|
|
AddQuery;
|
|
|
|
// Special case for removed default values on columns, which can neither be done in
|
|
// ALTER TABLE ... CHANGE COLUMN query, as there is no "no default" clause, nor by
|
|
// appending an ALTER COLUMN ... DROP DEFAULT, without getting an "unknown column" error.
|
|
// Also, do this after the data type was altered, if from TEXT > VARCHAR e.g.
|
|
for i:=0 to FColumns.Count-1 do begin
|
|
if (FColumns[i].FStatus = esModified)
|
|
and (FColumns[i].DefaultType = cdtNothing)
|
|
and (FColumns[i].OldDataType.HasDefault)
|
|
then
|
|
Specs.Add('ALTER '+DBObject.Connection.QuoteIdent(FColumns[i].OldName)+' DROP DEFAULT');
|
|
end;
|
|
AddQuery;
|
|
|
|
if editName.Text <> DBObject.Name then
|
|
Specs.Add('RENAME TO ' + DBObject.Connection.QuoteIdent(editName.Text));
|
|
if memoComment.Tag = MODIFIEDFLAG then
|
|
Specs.Add('COMMENT=' + esc(memoComment.Text));
|
|
if (comboCollation.Tag = MODIFIEDFLAG) or (chkCharsetConvert.Checked) then
|
|
Specs.Add('COLLATE=' + esc(comboCollation.Text));
|
|
if comboEngine.Tag = MODIFIEDFLAG then begin
|
|
if DBObject.Connection.ServerVersionInt < 40018 then
|
|
Specs.Add('TYPE=' + comboEngine.Text)
|
|
else
|
|
Specs.Add('ENGINE=' + comboEngine.Text);
|
|
end;
|
|
if comboRowFormat.Tag = MODIFIEDFLAG then
|
|
Specs.Add('ROW_FORMAT=' + comboRowFormat.Text);
|
|
if chkChecksum.Tag = MODIFIEDFLAG then
|
|
Specs.Add('CHECKSUM=' + IntToStr(Integer(chkChecksum.Checked)));
|
|
if editAutoInc.Tag = MODIFIEDFLAG then
|
|
Specs.Add('AUTO_INCREMENT=' + IntToStr(MakeInt(editAutoInc.Text)));
|
|
if editAvgRowLen.Tag = MODIFIEDFLAG then
|
|
Specs.Add('AVG_ROW_LENGTH=' + IntToStr(MakeInt(editAvgRowLen.Text)));
|
|
if editMaxRows.Tag = MODIFIEDFLAG then
|
|
Specs.Add('MAX_ROWS=' + IntToStr(MakeInt(editMaxRows.Text)));
|
|
if memoUnionTables.Enabled and (memoUnionTables.Tag = MODIFIEDFLAG) and (memoUnionTables.Text <> '') then
|
|
Specs.Add('UNION=('+memoUnionTables.Text+')');
|
|
if comboInsertMethod.Enabled and (comboInsertMethod.Tag = MODIFIEDFLAG) and (comboInsertMethod.Text <> '') then
|
|
Specs.Add('INSERT_METHOD='+comboInsertMethod.Text);
|
|
if chkCharsetConvert.Checked then begin
|
|
Results := DBObject.Connection.CollationTable;
|
|
if Assigned(Results) then while not Results.Eof do begin
|
|
if Results.Col('Collation') = comboCollation.Text then begin
|
|
Specs.Add('CONVERT TO CHARSET '+Results.Col('Charset'));
|
|
break;
|
|
end;
|
|
Results.Next;
|
|
end;
|
|
end;
|
|
|
|
// Update columns
|
|
MainForm.EnableProgress(FColumns.Count + DeletedKeys.Count + FKeys.Count);
|
|
Node := listColumns.GetFirst;
|
|
PreviousCol := nil;
|
|
while Assigned(Node) do begin
|
|
Mainform.ProgressStep;
|
|
Col := listColumns.GetNodeData(Node);
|
|
if Col.Status <> esUntouched then begin
|
|
ColSpec := DBObject.Connection.QuoteIdent(Col.Name);
|
|
ColSpec := ColSpec + ' ' + Col.DataType.Name;
|
|
IsVirtual := (Col.Expression <> '') and (Col.Virtuality <> '');
|
|
if Col.LengthSet <> '' then
|
|
ColSpec := ColSpec + '(' + Col.LengthSet + ')';
|
|
if (Col.DataType.Category in [dtcInteger, dtcReal]) and Col.Unsigned then
|
|
ColSpec := ColSpec + ' UNSIGNED';
|
|
if (Col.DataType.Category in [dtcInteger, dtcReal]) and Col.ZeroFill then
|
|
ColSpec := ColSpec + ' ZEROFILL';
|
|
if not IsVirtual then begin
|
|
if not Col.AllowNull then
|
|
ColSpec := ColSpec + ' NOT';
|
|
ColSpec := ColSpec + ' NULL';
|
|
end;
|
|
if Col.DefaultType <> cdtNothing then begin
|
|
ColSpec := ColSpec + ' ' + GetColumnDefaultClause(Col.DefaultType, Col.DefaultText);
|
|
ColSpec := TrimRight(ColSpec); // Remove whitespace for columns without default value
|
|
end;
|
|
if IsVirtual then
|
|
ColSpec := ColSpec + ' AS ('+Col.Expression+') '+Col.Virtuality;
|
|
if Col.Comment <> '' then
|
|
ColSpec := ColSpec + ' COMMENT '+esc(Col.Comment);
|
|
if Col.Collation <> '' then begin
|
|
ColSpec := ColSpec + ' COLLATE ';
|
|
if chkCharsetConvert.Checked then
|
|
ColSpec := ColSpec + esc(comboCollation.Text)
|
|
else
|
|
ColSpec := ColSpec + esc(Col.Collation);
|
|
end;
|
|
// Server version requirement, see http://dev.mysql.com/doc/refman/4.1/en/alter-table.html
|
|
if DBObject.Connection.ServerVersionInt >= 40001 then begin
|
|
if PreviousCol = nil then
|
|
ColSpec := ColSpec + ' FIRST'
|
|
else
|
|
ColSpec := ColSpec + ' AFTER '+DBObject.Connection.QuoteIdent(PreviousCol.Name);
|
|
end;
|
|
if Col.Status = esModified then
|
|
Specs.Add('CHANGE COLUMN '+DBObject.Connection.QuoteIdent(Col.OldName) + ' ' + ColSpec)
|
|
else if Col.Status in [esAddedUntouched, esAddedModified] then
|
|
Specs.Add('ADD COLUMN ' + ColSpec);
|
|
end;
|
|
PreviousCol := Col;
|
|
Node := listColumns.GetNextSibling(Node);
|
|
end;
|
|
|
|
// Deleted columns, not available as Node in above loop
|
|
for i:=0 to FColumns.Count-1 do begin
|
|
if FColumns[i].Status = esDeleted then
|
|
Specs.Add('DROP COLUMN '+DBObject.Connection.QuoteIdent(FColumns[i].OldName));
|
|
end;
|
|
|
|
// Drop indexes, also changed indexes, which will be readded below
|
|
for i:=0 to DeletedKeys.Count-1 do begin
|
|
Mainform.ProgressStep;
|
|
if DeletedKeys[i] = PKEY then
|
|
IndexSQL := 'PRIMARY KEY'
|
|
else
|
|
IndexSQL := 'INDEX ' + DBObject.Connection.QuoteIdent(DeletedKeys[i]);
|
|
Specs.Add('DROP '+IndexSQL);
|
|
end;
|
|
// Add changed or added indexes
|
|
for i:=0 to FKeys.Count-1 do begin
|
|
Mainform.ProgressStep;
|
|
if FKeys[i].Modified and (not FKeys[i].Added) then begin
|
|
if FKeys[i].OldIndexType = PKEY then
|
|
IndexSQL := 'PRIMARY KEY'
|
|
else
|
|
IndexSQL := 'INDEX ' + DBObject.Connection.QuoteIdent(FKeys[i].OldName);
|
|
Specs.Add('DROP '+IndexSQL);
|
|
end;
|
|
if FKeys[i].Added or FKeys[i].Modified then
|
|
Specs.Add('ADD '+FKeys[i].SQLCode);
|
|
end;
|
|
|
|
for i:=0 to DeletedForeignKeys.Count-1 do
|
|
Specs.Add('DROP FOREIGN KEY '+DBObject.Connection.QuoteIdent(DeletedForeignKeys[i]));
|
|
for i:=0 to FForeignKeys.Count-1 do begin
|
|
if FForeignKeys[i].Added or FForeignKeys[i].Modified then
|
|
Specs.Add('ADD '+FForeignKeys[i].SQLCode(True));
|
|
end;
|
|
|
|
AddQuery;
|
|
|
|
Result := TSQLBatch.Create;
|
|
Result.SQL := SQL;
|
|
|
|
FreeAndNil(Specs);
|
|
Mainform.ShowStatusMsg;
|
|
MainForm.DisableProgress;
|
|
Screen.Cursor := crDefault;
|
|
end;
|
|
|
|
|
|
function TfrmTableEditor.ComposeCreateStatement: TSQLBatch;
|
|
var
|
|
i, IndexCount: Integer;
|
|
Col: PTableColumn;
|
|
Node: PVirtualNode;
|
|
tmp, SQL: String;
|
|
begin
|
|
// Compose CREATE query, called by buttons and for SQL code tab
|
|
SQL := 'CREATE TABLE '+DBObject.Connection.QuoteIdent(editName.Text)+' ('+CRLF;
|
|
Node := listColumns.GetFirst;
|
|
while Assigned(Node) do begin
|
|
Col := listColumns.GetNodeData(Node);
|
|
SQL := SQL + #9 + Col.SQLCode + ','+CRLF;
|
|
Node := listColumns.GetNextSibling(Node);
|
|
end;
|
|
|
|
IndexCount := 0;
|
|
for i:=0 to FKeys.Count-1 do begin
|
|
tmp := FKeys[i].SQLCode;
|
|
if tmp <> '' then begin
|
|
SQL := SQL + #9 + tmp + ','+CRLF;
|
|
Inc(IndexCount);
|
|
end;
|
|
end;
|
|
|
|
for i:=0 to FForeignKeys.Count-1 do
|
|
SQL := SQL + #9 + FForeignKeys[i].SQLCode(True) + ','+CRLF;
|
|
|
|
if Integer(listColumns.RootNodeCount) + IndexCount + FForeignKeys.Count > 0 then
|
|
Delete(SQL, Length(SQL)-2, 3);
|
|
|
|
SQL := SQL + CRLF + ')' + CRLF;
|
|
if memoComment.Text <> '' then
|
|
SQL := SQL + 'COMMENT='+esc(memoComment.Text) + CRLF;
|
|
if comboCollation.Text <> '' then
|
|
SQL := SQL + 'COLLATE='+esc(comboCollation.Text) + CRLF;
|
|
if comboEngine.Text <> '' then begin
|
|
if DBObject.Connection.ServerVersionInt < 40018 then
|
|
SQL := SQL + 'TYPE='+comboEngine.Text + CRLF
|
|
else
|
|
SQL := SQL + 'ENGINE='+comboEngine.Text + CRLF;
|
|
end;
|
|
if comboRowFormat.Tag = MODIFIEDFLAG then
|
|
SQL := SQL + 'ROW_FORMAT='+comboRowFormat.Text + CRLF;
|
|
if chkChecksum.Checked then
|
|
SQL := SQL + 'CHECKSUM='+IntToStr(Integer(chkChecksum.Checked)) + CRLF;
|
|
if editAutoInc.Text <> '' then
|
|
SQL := SQL + 'AUTO_INCREMENT='+editAutoInc.Text + CRLF;
|
|
if editAvgRowLen.Text <> '' then
|
|
SQL := SQL + 'AVG_ROW_LENGTH='+editAvgRowLen.Text + CRLF;
|
|
if editMaxRows.Text <> '' then
|
|
SQL := SQL + 'MAX_ROWS='+editMaxRows.Text + CRLF;
|
|
if memoUnionTables.Enabled and (memoUnionTables.Text <> '') then
|
|
SQL := SQL + 'UNION=('+memoUnionTables.Text+')' + CRLF;
|
|
if comboInsertMethod.Enabled and (comboInsertMethod.Text <> '') then
|
|
SQL := SQL + 'INSERT_METHOD='+comboInsertMethod.Text + CRLF;
|
|
Result := TSQLBatch.Create;
|
|
Result.SQL := Trim(SQL);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.Modification(Sender: TObject);
|
|
begin
|
|
// Memorize modified status
|
|
if FLoaded then begin
|
|
if Sender is TComponent then
|
|
TComponent(Sender).Tag := MODIFIEDFLAG;
|
|
Modified := True;
|
|
btnSave.Enabled := Modified and (editName.Text <> '') and (listColumns.RootNodeCount > 0);
|
|
btnDiscard.Enabled := Modified;
|
|
CreateCodeValid := False;
|
|
AlterCodeValid := False;
|
|
UpdateSQLcode;
|
|
CalcMinColWidth;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.btnAddColumnClick(Sender: TObject);
|
|
var
|
|
NewCol: TTableColumn;
|
|
FocusedCol: PTableColumn;
|
|
fn, NewNode: PVirtualNode;
|
|
idx: Integer;
|
|
begin
|
|
// Add new column after selected one
|
|
if listColumns.IsEditing then
|
|
listColumns.EndEditNode;
|
|
fn := listColumns.FocusedNode;
|
|
NewCol := TTableColumn.Create(DBObject.Connection);
|
|
if Assigned(fn) then begin
|
|
idx := fn.Index+1;
|
|
// Copy properties from focused node
|
|
FocusedCol := listColumns.GetNodeData(fn);
|
|
NewCol.DataType := FocusedCol.DataType;
|
|
NewCol.LengthSet := FocusedCol.LengthSet;
|
|
NewCol.Unsigned := FocusedCol.Unsigned;
|
|
NewCol.AllowNull := FocusedCol.AllowNull;
|
|
// There can be only one
|
|
if FocusedCol.DefaultType = cdtAutoInc then begin
|
|
NewCol.DefaultType := cdtText;
|
|
NewCol.DefaultText := '0';
|
|
end else begin
|
|
NewCol.DefaultType := FocusedCol.DefaultType;
|
|
NewCol.DefaultText := FocusedCol.DefaultText;
|
|
end;
|
|
NewCol.Comment := FocusedCol.Comment;
|
|
NewCol.Collation := '';
|
|
end else begin
|
|
idx := listColumns.RootNodeCount;
|
|
NewCol.DataType := DBObject.Connection.GetDatatypeByName('INT');
|
|
NewCol.LengthSet := '10';
|
|
NewCol.Unsigned := False;
|
|
NewCol.AllowNull := True;
|
|
NewCol.DefaultType := cdtNothing;
|
|
NewCol.DefaultText := '';
|
|
NewCol.Comment := '';
|
|
NewCol.Collation := '';
|
|
end;
|
|
NewCol.Name := 'Column '+IntToStr(idx+1);
|
|
FColumns.Insert(idx, NewCol);
|
|
NewNode := listColumns.InsertNode(fn, amInsertAfter, @NewCol);
|
|
NewCol.Status := esAddedUntouched;
|
|
SelectNode(listColumns, NewNode);
|
|
Modification(Sender);
|
|
ValidateColumnControls;
|
|
listColumns.EditNode(NewNode, 1);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.btnRemoveColumnClick(Sender: TObject);
|
|
var
|
|
Node, NodeDelete, NodeFocus: PVirtualNode;
|
|
Col: PTableColumn;
|
|
begin
|
|
// Remove selected column(s)
|
|
Node := GetNextNode(listColumns, nil, True);
|
|
while Assigned(Node) do begin
|
|
Col := listColumns.GetNodeData(Node);
|
|
Col.Name := Col.OldName;
|
|
Col.Status := esDeleted;
|
|
Node := GetNextNode(listColumns, Node, True);
|
|
end;
|
|
// Find next unselected node for new focus
|
|
Node := listColumns.FocusedNode;
|
|
NodeFocus := nil;
|
|
while Assigned(Node) do begin
|
|
if not (vsSelected in Node.States) then begin
|
|
NodeFocus := Node;
|
|
break;
|
|
end;
|
|
Node := listColumns.GetNextSibling(Node);
|
|
end;
|
|
// Delete selected + visible
|
|
Node := GetNextNode(listColumns, nil, True);
|
|
while Assigned(Node) do begin
|
|
NodeDelete := Node;
|
|
Node := GetNextNode(listColumns, Node, True);
|
|
listColumns.DeleteNode(NodeDelete);
|
|
end;
|
|
|
|
if not Assigned(NodeFocus) then
|
|
NodeFocus := listColumns.GetLast;
|
|
if Assigned(NodeFocus) then
|
|
SelectNode(listColumns, NodeFocus.Index);
|
|
Modification(Sender);
|
|
ValidateColumnControls;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.btnMoveUpColumnClick(Sender: TObject);
|
|
begin
|
|
// Move column up
|
|
listColumns.EndEditNode;
|
|
listColumns.MoveTo(listColumns.FocusedNode, listColumns.GetPreviousSibling(listColumns.FocusedNode), amInsertBefore, False);
|
|
ValidateColumnControls;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.btnMoveDownColumnClick(Sender: TObject);
|
|
begin
|
|
// Move column down
|
|
listColumns.EndEditNode;
|
|
listColumns.MoveTo(listColumns.FocusedNode, listColumns.GetNextSibling(listColumns.FocusedNode), amInsertAfter, False);
|
|
ValidateColumnControls;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listColumnsDragOver(Sender: TBaseVirtualTree;
|
|
Source: TObject; Shift: TShiftState; State: TDragState; Pt: TPoint;
|
|
Mode: TDropMode; var Effect: Integer; var Accept: Boolean);
|
|
begin
|
|
Accept := (Source = Sender) and (Mode <> dmNowhere);
|
|
// Not sure what this effect does, probably show a specific mouse cursor?
|
|
Effect := DROPEFFECT_MOVE;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listColumnsDragDrop(Sender: TBaseVirtualTree;
|
|
Source: TObject; DataObject: IDataObject; Formats: TFormatArray;
|
|
Shift: TShiftState; Pt: TPoint; var Effect: Integer; Mode: TDropMode);
|
|
var
|
|
Node: PVirtualNode;
|
|
AttachMode: TVTNodeAttachMode;
|
|
begin
|
|
Node := Sender.GetNodeAt(Pt.X, Pt.Y);
|
|
if Assigned(Node) then begin
|
|
case Mode of
|
|
dmAbove, dmOnNode: AttachMode := amInsertBefore;
|
|
else AttachMode := amInsertAfter;
|
|
end;
|
|
listColumns.MoveTo(listColumns.FocusedNode, Node, AttachMode, False);
|
|
ValidateColumnControls;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listColumnsBeforeCellPaint(Sender: TBaseVirtualTree;
|
|
TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
|
|
CellPaintMode: TVTCellPaintMode; CellRect: TRect; var ContentRect: TRect);
|
|
begin
|
|
// Darken cell background to signalize it doesn't allow length/set
|
|
// Exclude non editable checkbox columns - grey looks ugly there.
|
|
if (not CellEditingAllowed(Node, Column)) and (not (Column in [4, 5, 6])) then begin
|
|
TargetCanvas.Brush.Color := clBtnFace;
|
|
TargetCanvas.FillRect(CellRect);
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.CalcMinColWidth;
|
|
var
|
|
i, j, MinWidthThisCol, MinWidthAllCols: Integer;
|
|
begin
|
|
// Find maximum column widths so the index icons have enough room after auto-fitting
|
|
MinWidthAllCols := 0;
|
|
for i:=0 to FColumns.Count-1 do begin
|
|
MinWidthThisCol := 0;
|
|
for j:=0 to FKeys.Count-1 do begin
|
|
if FKeys[j].Columns.IndexOf(FColumns[i].Name) > -1 then
|
|
Inc(MinWidthThisCol, listColumns.Images.Width);
|
|
end;
|
|
for j:=0 to FForeignKeys.Count-1 do begin
|
|
if FForeignKeys[j].Columns.IndexOf(FColumns[i].Name) > -1 then
|
|
Inc(MinWidthThisCol, listColumns.Images.Width);
|
|
end;
|
|
MinWidthAllCols := Max(MinWidthAllCols, MinWidthThisCol);
|
|
end;
|
|
// Add space for number
|
|
Inc(MinWidthAllCols, listColumns.Canvas.TextWidth(IntToStr(FColumns.Count+1)) + listColumns.TextMargin*4);
|
|
listColumns.Header.Columns[0].Width := MinWidthAllCols;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listColumnsAfterCellPaint(Sender: TBaseVirtualTree;
|
|
TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
|
|
CellRect: TRect);
|
|
var
|
|
Col: PTableColumn;
|
|
ImageIndex, X, Y, i: Integer;
|
|
VT: TVirtualStringTree;
|
|
begin
|
|
VT := TVirtualStringTree(Sender);
|
|
Col := Sender.GetNodeData(Node);
|
|
Y := CellRect.Top + Integer(VT.NodeHeight[Node] div 2) - (VT.Images.Height div 2);
|
|
|
|
// Paint one icon per index of which this column is part of
|
|
if Column = 0 then begin
|
|
X := 0;
|
|
for i:=0 to FKeys.Count-1 do begin
|
|
if FKeys[i].Columns.IndexOf(Col.Name) > -1 then begin
|
|
ImageIndex := GetIndexIcon(FKeys[i].IndexType);
|
|
VT.Images.Draw(TargetCanvas, X, Y, ImageIndex);
|
|
Inc(X, VT.Images.Width);
|
|
end;
|
|
end;
|
|
for i:=0 to FForeignKeys.Count-1 do begin
|
|
if FForeignKeys[i].Columns.IndexOf(Col.Name) > -1 then begin
|
|
ImageIndex := ICONINDEX_FOREIGNKEY;
|
|
VT.Images.Draw(TargetCanvas, X, Y, ImageIndex);
|
|
Inc(X, VT.Images.Width);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
// Paint checkbox image in certain columns
|
|
// while restricting "Allow NULL" checkbox to numeric datatypes
|
|
if (Column in [4, 5, 6]) and CellEditingAllowed(Node, Column) then begin
|
|
if (Col.Unsigned and (Column=4)) or (Col.AllowNull and (Column=5)) or (Col.ZeroFill and (Column = 6)) then
|
|
ImageIndex := 128
|
|
else ImageIndex := 127;
|
|
X := CellRect.Left + (VT.Header.Columns[Column].Width div 2) - (VT.Images.Width div 2);
|
|
VT.Images.Draw(TargetCanvas, X, Y, ImageIndex);
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listColumnsFocusChanged(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex);
|
|
begin
|
|
// Column focus changed
|
|
ValidateColumnControls;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.ValidateColumnControls;
|
|
var Node: PVirtualNode;
|
|
begin
|
|
Node := listColumns.FocusedNode;
|
|
btnRemoveColumn.Enabled := listColumns.SelectedCount > 0;
|
|
btnMoveUpColumn.Enabled := Assigned(Node) and (Node <> listColumns.GetFirst);
|
|
btnMoveDownColumn.Enabled := Assigned(Node) and (Node <> listColumns.GetLast);
|
|
|
|
menuRemoveColumn.Enabled := btnRemoveColumn.Enabled;
|
|
menuMoveUpColumn.Enabled := btnMoveUpColumn.Enabled;
|
|
menuMoveDownColumn.Enabled := btnMoveDownColumn.Enabled;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listColumnsEditing(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex; var Allowed: Boolean);
|
|
begin
|
|
// Allow text editing? Explicitely block that in checkbox columns
|
|
Allowed := CellEditingAllowed(Node, Column) and (not (Column in [4,5,6]));
|
|
end;
|
|
|
|
|
|
function TfrmTableEditor.CellEditingAllowed(Node: PVirtualNode; Column: TColumnIndex): Boolean;
|
|
var
|
|
Col: PTableColumn;
|
|
begin
|
|
Col := listColumns.GetNodeData(Node);
|
|
case Column of
|
|
// No editor for very first column and checkbox columns
|
|
0: Result := False;
|
|
3: Result := Col.DataType.HasLength;
|
|
4,6: Result := (Col.DataType.Category in [dtcInteger, dtcReal]) and (DBObject.Connection.Parameters.NetTypeGroup = ngMySQL);
|
|
// No editing of collation allowed if "Convert data" was checked
|
|
9: Result := not chkCharsetConvert.Checked;
|
|
else Result := True;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listColumnsGetText(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
|
|
var CellText: String);
|
|
var
|
|
Col: PTableColumn;
|
|
begin
|
|
// Display column text
|
|
Col := Sender.GetNodeData(Node);
|
|
case Column of
|
|
0: CellText := IntToStr(Node.Index+1);
|
|
1: CellText := Col.Name;
|
|
2: CellText := Col.DataType.Name;
|
|
3: CellText := Col.LengthSet;
|
|
4, 5, 6: CellText := ''; // Checkbox
|
|
7: begin
|
|
case Col.DefaultType of
|
|
cdtNothing: CellText := 'No default';
|
|
cdtText, cdtTextUpdateTS: begin
|
|
if Col.DataType.Category in [dtcInteger, dtcReal] then
|
|
CellText := Col.DefaultText
|
|
else
|
|
CellText := esc(Col.DefaultText);
|
|
end;
|
|
cdtNull, cdtNullUpdateTS: CellText := 'NULL';
|
|
cdtCurTS, cdtCurTSUpdateTS: CellText := 'CURRENT_TIMESTAMP';
|
|
cdtAutoInc: CellText := 'AUTO_INCREMENT';
|
|
end;
|
|
if Col.DefaultType in [cdtTextUpdateTS, cdtNullUpdateTS, cdtCurTSUpdateTS] then
|
|
CellText := CellText + ' ON UPDATE CURRENT_TIMESTAMP';
|
|
end;
|
|
8: CellText := Col.Comment;
|
|
9: begin
|
|
CellText := Col.Collation;
|
|
if (CellText <> '') and (chkCharsetConvert.Checked) then
|
|
CellText := comboCollation.Text;
|
|
end;
|
|
10: CellText := Col.Expression;
|
|
11: CellText := Col.Virtuality;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listColumnsInitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode;
|
|
var InitialStates: TVirtualNodeInitStates);
|
|
var
|
|
Col: PTableColumn;
|
|
begin
|
|
// Bind data to node
|
|
Col := Sender.GetNodeData(Node);
|
|
Col^ := FColumns[Node.Index];
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listColumnsGetNodeDataSize(Sender: TBaseVirtualTree; var NodeDataSize: Integer);
|
|
begin
|
|
NodeDataSize := SizeOf(TTableColumn);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listColumnsPaintText(Sender: TBaseVirtualTree;
|
|
const TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
|
|
TextType: TVSTTextType);
|
|
var
|
|
TextColor: TColor;
|
|
i: Integer;
|
|
Col: PTableColumn;
|
|
begin
|
|
Col := Sender.GetNodeData(Node);
|
|
// Bold font for primary key columns
|
|
for i:=0 to FKeys.Count-1 do begin
|
|
if (FKeys[i].IndexType = PKEY) and (FKeys[i].Columns.IndexOf(Col.Name) > -1) then begin
|
|
TargetCanvas.Font.Style := TargetCanvas.Font.Style + [fsBold];
|
|
break;
|
|
end;
|
|
end;
|
|
|
|
// No specific colors for selected nodes, would interfere with blue selection background
|
|
if vsSelected in Node.States then Exit;
|
|
// Break early if nothing to do
|
|
if not (Column in [2, 7]) then Exit;
|
|
|
|
// Give datatype column specific color, as set in preferences
|
|
TextColor := TargetCanvas.Font.Color;
|
|
case Column of
|
|
2: TextColor := DatatypeCategories[Col.DataType.Category].Color;
|
|
|
|
7: case Col.DefaultType of
|
|
cdtNothing, cdtNull, cdtNullUpdateTS:
|
|
TextColor := clGray;
|
|
cdtCurTS, cdtCurTSUpdateTS:
|
|
TextColor := DatatypeCategories[dtcTemporal].Color;
|
|
cdtAutoInc:
|
|
TextColor := clNavy;
|
|
end;
|
|
end;
|
|
TargetCanvas.Font.Color := TextColor;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listColumnsNewText(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex; NewText: String);
|
|
var
|
|
i: Integer;
|
|
Col: PTableColumn;
|
|
Key: TTableKey;
|
|
begin
|
|
// Column property edited
|
|
Col := Sender.GetNodeData(Node);
|
|
case Column of
|
|
1: begin // Name of column
|
|
for i:=0 to FColumns.Count-1 do begin
|
|
if FColumns[i].Name = NewText then begin
|
|
ErrorDialog('Column "'+NewText+'" already exists.');
|
|
Exit;
|
|
end;
|
|
end;
|
|
for Key in FKeys do begin
|
|
for i:=0 to Key.Columns.Count-1 do begin
|
|
if Key.Columns[i] = Col.Name then
|
|
Key.Columns[i] := NewText;
|
|
end;
|
|
end;
|
|
treeIndexes.Invalidate;
|
|
Col.Name := NewText;
|
|
end;
|
|
2: begin // Data type
|
|
Col.DataType := DBObject.Connection.GetDatatypeByName(NewText);
|
|
// Reset length/set for column types which don't support that
|
|
if not Col.DataType.HasLength then
|
|
Col.LengthSet := '';
|
|
// Suggest length/set if required
|
|
if (not Col.LengthCustomized) or (Col.DataType.RequiresLength and (Col.LengthSet = '')) then
|
|
Col.LengthSet := Col.DataType.DefLengthSet;
|
|
// Auto-fix user selected default type which can be invalid now
|
|
case Col.DataType.Category of
|
|
dtcInteger, dtcReal: begin
|
|
if Col.DefaultType in [cdtCurTS, cdtCurTSUpdateTS] then
|
|
Col.DefaultType := cdtNothing;
|
|
if Col.DefaultType = cdtTextUpdateTS then
|
|
Col.DefaultType := cdtText;
|
|
if Col.DefaultType = cdtNullUpdateTS then
|
|
Col.DefaultType := cdtNull;
|
|
end;
|
|
dtcText, dtcBinary, dtcSpatial, dtcOther: begin
|
|
if Col.DefaultType in [cdtCurTS, cdtCurTSUpdateTS, cdtAutoInc] then
|
|
Col.DefaultType := cdtNothing;
|
|
if Col.DefaultType = cdtNullUpdateTS then
|
|
Col.DefaultType := cdtNull;
|
|
end;
|
|
dtcTemporal: begin
|
|
if Col.DefaultType = cdtAutoinc then
|
|
Col.DefaultType := cdtNothing;
|
|
end;
|
|
end;
|
|
|
|
end; // Length / Set
|
|
3: begin
|
|
Col.LengthSet := NewText;
|
|
Col.LengthCustomized := True;
|
|
end;
|
|
// 4 + 5 are checkboxes - handled in OnClick
|
|
7: begin // Default value
|
|
Col.DefaultText := NewText;
|
|
Col.DefaultType := GetColumnDefaultType(Col.DefaultText);
|
|
if Col.DefaultType in [cdtNull, cdtNullUpdateTS] then
|
|
Col.AllowNull := True;
|
|
end;
|
|
8: Col.Comment := NewText;
|
|
9: Col.Collation := NewText;
|
|
10: Col.Expression := NewText;
|
|
11: Col.Virtuality := NewText;
|
|
end;
|
|
Col.Status := esModified;
|
|
Modification(Sender);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listColumnsNodeMoved(Sender: TBaseVirtualTree; Node: PVirtualNode);
|
|
var
|
|
Col: PTableColumn;
|
|
begin
|
|
Col := Sender.GetNodeData(Node);
|
|
Col.Status := esModified;
|
|
Modification(Sender);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listColumnsClick(Sender: TObject);
|
|
var
|
|
VT: TVirtualStringTree;
|
|
Click: THitInfo;
|
|
begin
|
|
// Handle click event
|
|
VT := Sender as TVirtualStringTree;
|
|
VT.GetHitTestInfoAt(Mouse.CursorPos.X-VT.ClientOrigin.X, Mouse.CursorPos.Y-VT.ClientOrigin.Y, True, Click);
|
|
vtHandleClickOrKeyPress(VT, Click.HitNode, Click.HitColumn, Click.HitPositions);
|
|
end;
|
|
|
|
|
|
|
|
procedure TfrmTableEditor.listColumnsKeyPress(Sender: TObject; var Key: Char);
|
|
var
|
|
VT: TVirtualStringTree;
|
|
begin
|
|
// Space/click on checkbox column
|
|
VT := Sender as TVirtualStringTree;
|
|
if (Ord(Key) = VK_SPACE) and (VT.FocusedColumn in [4, 5, 6]) then
|
|
vtHandleClickOrKeyPress(VT, VT.FocusedNode, VT.FocusedColumn, []);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.vtHandleClickOrKeyPress(Sender: TVirtualStringTree;
|
|
Node: PVirtualNode; Column: TColumnIndex; HitPositions: THitPositions);
|
|
var
|
|
Col: PTableColumn;
|
|
VT: TVirtualStringTree;
|
|
begin
|
|
if (not Assigned(Node)) or (Column = NoColumn) then
|
|
Exit;
|
|
VT := Sender as TVirtualStringTree;
|
|
// For checkboxes, cell editors are disabled, instead toggle their state
|
|
if CellEditingAllowed(Node, Column) then begin
|
|
Col := VT.GetNodeData(Node);
|
|
case Column of
|
|
4: begin
|
|
Col.Unsigned := not Col.Unsigned;
|
|
Col.Status := esModified;
|
|
Modification(Sender);
|
|
VT.InvalidateNode(Node);
|
|
end;
|
|
5: begin
|
|
Col.AllowNull := not Col.AllowNull;
|
|
// Switch default value from NULL to Text if Allow Null is off
|
|
if (not Col.AllowNull) and (Col.DefaultType in [cdtNull, cdtNullUpdateTS]) then begin
|
|
Col.DefaultType := cdtNothing;
|
|
Col.DefaultText := '';
|
|
end;
|
|
Col.Status := esModified;
|
|
Modification(Sender);
|
|
VT.InvalidateNode(Node);
|
|
end;
|
|
6: begin
|
|
Col.ZeroFill := not Col.ZeroFill;
|
|
Col.Status := esModified;
|
|
Modification(Sender);
|
|
VT.InvalidateNode(Node);
|
|
end;
|
|
else begin
|
|
// All other cells go into edit mode please
|
|
// Explicitely done on OnClick, not in OnFocusChanged which seemed annoying for keyboard users
|
|
if hiOnItemLabel in HitPositions then
|
|
VT.EditNode(Node, Column);
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TfrmTableEditor.listColumnsCreateEditor(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex; out EditLink: IVTEditLink);
|
|
var
|
|
VT: TVirtualStringTree;
|
|
EnumEditor: TEnumEditorLink;
|
|
DefaultEditor: TColumnDefaultEditorLink;
|
|
DatatypeEditor: TDatatypeEditorLink;
|
|
Col: PTableColumn;
|
|
begin
|
|
// Start cell editor
|
|
VT := Sender as TVirtualStringTree;
|
|
Col := Sender.GetNodeData(Node);
|
|
case Column of
|
|
2: begin // Datatype pulldown
|
|
DatatypeEditor := TDatatypeEditorLink.Create(VT);
|
|
DatatypeEditor.Datatype := Col.DataType.Index;
|
|
EditLink := DataTypeEditor;
|
|
end;
|
|
9: begin // Collation pulldown
|
|
EnumEditor := TEnumEditorLink.Create(VT);
|
|
EnumEditor.ValueList := TStringList.Create;
|
|
EnumEditor.ValueList.Text := DBObject.Connection.CollationList.Text;
|
|
EnumEditor.ValueList.Insert(0, '');
|
|
EditLink := EnumEditor;
|
|
end;
|
|
7: begin
|
|
DefaultEditor := TColumnDefaultEditorLink.Create(VT);
|
|
DefaultEditor.DefaultText := Col.DefaultText;
|
|
DefaultEditor.DefaultType := Col.DefaultType;
|
|
DefaultEditor.DataType := Col.DataType.Index;
|
|
EditLink := DefaultEditor;
|
|
end;
|
|
11: begin // Virtuality pulldown
|
|
EnumEditor := TEnumEditorLink.Create(VT);
|
|
EnumEditor.ValueList := TStringList.Create;
|
|
EnumEditor.ValueList.Add('');
|
|
EnumEditor.ValueList.Add('VIRTUAL');
|
|
EnumEditor.ValueList.Add('PERSISTENT');
|
|
EditLink := EnumEditor;
|
|
end
|
|
else
|
|
EditLink := TInplaceEditorLink.Create(VT);
|
|
end;
|
|
TBaseGridEditorLink(EditLink).Connection := DBObject.Connection;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.editNumEditChange(Sender: TObject);
|
|
var
|
|
ed: TEdit;
|
|
ShouldBe: String;
|
|
CursorPos: Integer;
|
|
begin
|
|
// Only numbers allowed in this TEdit
|
|
Modification(Sender);
|
|
ed := Sender as TEdit;
|
|
ShouldBe := CleanupNumber(ed.Text);
|
|
if (ed.Text = ShouldBe) or (ed.Text = '') then Exit;
|
|
MessageBeep(MB_ICONEXCLAMATION);
|
|
CursorPos := ed.SelStart;
|
|
ed.OnChange := nil;
|
|
ed.Text := ShouldBe;
|
|
ed.SelStart := CursorPos;
|
|
ed.OnChange := editNumEditChange;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.comboEngineSelect(Sender: TObject);
|
|
var
|
|
IsMerge: Boolean;
|
|
begin
|
|
// Enable/disable engine specific option controls
|
|
IsMerge := (Sender as TComboBox).Text = 'MRG_MYISAM';
|
|
memoUnionTables.Enabled := IsMerge;
|
|
comboInsertMethod.Enabled := IsMerge;
|
|
Modification(Sender);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.btnAddIndexClick(Sender: TObject);
|
|
var
|
|
TblKey: TTableKey;
|
|
begin
|
|
// Add new index
|
|
TblKey := TTableKey.Create(DBObject.Connection);
|
|
TblKey.Name := 'Index '+IntToStr(FKeys.Count+1);
|
|
TblKey.OldName := TblKey.Name;
|
|
TblKey.IndexType := KEY;
|
|
TblKey.OldIndexType := TblKey.IndexType;
|
|
TblKey.Added := True;
|
|
FKeys.Add(TblKey);
|
|
Modification(Sender);
|
|
treeIndexes.Invalidate;
|
|
SelectNode(treeIndexes, FKeys.Count-1);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.menuAddIndexColumnClick(Sender: TObject);
|
|
var
|
|
Node: PVirtualNode;
|
|
i, j: Integer;
|
|
NewCol, PartLength: String;
|
|
ColExists: Boolean;
|
|
Column: TTableColumn;
|
|
TblKey: TTableKey;
|
|
begin
|
|
// Add column to index
|
|
Node := treeIndexes.FocusedNode;
|
|
if not Assigned(Node) then
|
|
Exit;
|
|
if treeIndexes.GetNodeLevel(Node) = 1 then
|
|
Node := Node.Parent;
|
|
TblKey := FKeys[Node.Index];
|
|
// Find the first unused column for that index as default
|
|
ColExists := False;
|
|
NewCol := '';
|
|
PartLength := '';
|
|
for i:=0 to FColumns.Count-1 do begin
|
|
Column := FColumns[i];
|
|
if Column.Status = esDeleted then
|
|
Continue;
|
|
for j:=0 to TblKey.Columns.Count - 1 do begin
|
|
ColExists := TblKey.Columns[j] = Column.Name;
|
|
if ColExists then
|
|
break;
|
|
end;
|
|
if not ColExists then begin
|
|
NewCol := Column.Name;
|
|
if (TblKey.IndexType <> FKEY) and (Column.DataType.Index in [dtTinyText, dtText, dtMediumText, dtLongText, dtTinyBlob, dtBlob, dtMediumBlob, dtLongBlob]) then
|
|
PartLength := '100';
|
|
break;
|
|
end;
|
|
end;
|
|
treeIndexes.AddChild(Node);
|
|
TblKey.Columns.Add(NewCol);
|
|
TblKey.SubParts.Add(PartLength);
|
|
Modification(Sender);
|
|
treeIndexes.Invalidate;
|
|
SelectNode(treeIndexes, FKeys.Count-1, Node);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.btnRemoveIndexClick(Sender: TObject);
|
|
var
|
|
idx: Integer;
|
|
NewSelectNode: PVirtualNode;
|
|
begin
|
|
// Remove index or part
|
|
if treeIndexes.IsEditing then
|
|
treeIndexes.CancelEditNode;
|
|
case treeIndexes.GetNodeLevel(treeIndexes.FocusedNode) of
|
|
0: begin
|
|
idx := treeIndexes.FocusedNode.Index;
|
|
if not FKeys[idx].Added then
|
|
DeletedKeys.Add(FKeys[idx].OldName);
|
|
FKeys.Delete(idx);
|
|
// Delete node although ReinitChildren would do the same, but the Repaint before
|
|
// creates AVs in certain cases. See issue #2557
|
|
treeIndexes.DeleteNode(treeIndexes.FocusedNode);
|
|
end;
|
|
1: begin
|
|
idx := treeIndexes.FocusedNode.Parent.Index;
|
|
FKeys[idx].Columns.Delete(treeIndexes.FocusedNode.Index);
|
|
FKeys[idx].SubParts.Delete(treeIndexes.FocusedNode.Index);
|
|
treeIndexes.DeleteNode(treeIndexes.FocusedNode);
|
|
end;
|
|
end;
|
|
Modification(Sender);
|
|
treeIndexes.Repaint;
|
|
treeIndexes.ReinitChildren(nil, True);
|
|
NewSelectNode := treeIndexes.GetPreviousVisible(treeIndexes.FocusedNode);
|
|
if Assigned(NewSelectNode) then
|
|
SelectNode(treeIndexes, NewSelectNode.Index, NewSelectNode.Parent);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.btnClearIndexesClick(Sender: TObject);
|
|
var
|
|
i: Integer;
|
|
TblKey: TTableKey;
|
|
begin
|
|
// Clear all indexes
|
|
// Column data gets freed below - end any editor which could cause AV's
|
|
if treeIndexes.IsEditing then
|
|
treeIndexes.CancelEditNode;
|
|
// Trigger ValidateIndexControls
|
|
SelectNode(treeIndexes, nil);
|
|
for i:=0 to FKeys.Count-1 do begin
|
|
TblKey := TTableKey(FKeys[i]);
|
|
if not TblKey.Added then
|
|
DeletedKeys.Add(TblKey.OldName);
|
|
end;
|
|
FKeys.Clear;
|
|
Modification(Sender);
|
|
treeIndexes.Clear;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.treeIndexesGetImageIndex(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex;
|
|
var Ghosted: Boolean; var ImageIndex: Integer);
|
|
var
|
|
VT: TVirtualStringTree;
|
|
begin
|
|
// Icon image showing type of index
|
|
VT := Sender as TVirtualStringTree;
|
|
if Column <> 0 then Exit;
|
|
if not (Kind in [ikNormal, ikSelected]) then Exit;
|
|
case VT.GetNodeLevel(Node) of
|
|
0: ImageIndex := GetIndexIcon(VT.Text[Node, 1]);
|
|
1: ImageIndex := 42;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.treeIndexesGetText(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
|
|
var CellText: String);
|
|
var
|
|
TblKey: TTableKey;
|
|
begin
|
|
// Index tree showing cell text
|
|
case Sender.GetNodeLevel(Node) of
|
|
0: begin
|
|
TblKey := FKeys[Node.Index];
|
|
case Column of
|
|
0: if TblKey.IndexType = PKEY then
|
|
CellText := TblKey.IndexType + ' KEY' // Fixed name "PRIMARY KEY", cannot be changed
|
|
else
|
|
CellText := TblKey.Name;
|
|
1: CellText := TblKey.IndexType;
|
|
2: CellText := TblKey.Algorithm;
|
|
end;
|
|
end;
|
|
1: begin
|
|
TblKey := FKeys[Node.Parent.Index];
|
|
case Column of
|
|
0: CellText := TblKey.Columns[Node.Index];
|
|
1: CellText := TblKey.SubParts[Node.Index];
|
|
2: CellText := '';
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.treeIndexesInitChildren(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; var ChildCount: Cardinal);
|
|
begin
|
|
// Tell number of columns contained in index
|
|
ChildCount := FKeys[Node.Index].Columns.Count;
|
|
ListColumns.Invalidate;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.treeIndexesInitNode(Sender: TBaseVirtualTree;
|
|
ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
|
|
begin
|
|
// Show plus sign on first level nodes
|
|
if Sender.GetNodeLevel(Node) = 0 then
|
|
Include( InitialStates, ivsHasChildren);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.treeIndexesFocusChanged(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex);
|
|
begin
|
|
ValidateIndexControls;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.treeIndexesClick(Sender: TObject);
|
|
var
|
|
VT: TVirtualStringTree;
|
|
Click: THitInfo;
|
|
begin
|
|
// Handle click event
|
|
VT := Sender as TVirtualStringTree;
|
|
VT.GetHitTestInfoAt(Mouse.CursorPos.X-VT.ClientOrigin.X, Mouse.CursorPos.Y-VT.ClientOrigin.Y, True, Click);
|
|
if Assigned(Click.HitNode) and (Click.HitColumn > NoColumn) and (hiOnItemLabel in Click.HitPositions) then
|
|
VT.EditNode(Click.HitNode, Click.HitColumn);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.ValidateIndexControls;
|
|
var
|
|
Node: PVirtualNode;
|
|
HasNode: Boolean;
|
|
Level: Integer;
|
|
begin
|
|
// Enable/disable buttons
|
|
Node := treeIndexes.FocusedNode;
|
|
HasNode := Assigned(Node);
|
|
if HasNode then Level := treeIndexes.GetNodeLevel(Node)
|
|
else Level := -1;
|
|
|
|
btnRemoveIndex.Enabled := HasNode;
|
|
btnClearIndexes.Enabled := FKeys.Count > 0;
|
|
btnMoveUpIndex.Enabled := HasNode and (Level = 1) and (Node <> treeIndexes.GetFirstChild(Node.Parent));
|
|
btnMoveDownIndex.Enabled := HasNode and (Level = 1) and (Node <> treeIndexes.GetLastChild(Node.Parent));
|
|
|
|
menuAddIndexColumn.Enabled := HasNode;
|
|
menuRemoveIndex.Enabled := btnRemoveIndex.Enabled;
|
|
menuClearIndexes.Enabled := btnClearIndexes.Enabled;
|
|
menuMoveUpIndex.Enabled := btnMoveUpIndex.Enabled;
|
|
menuMoveDownIndex.Enabled := btnMoveDownIndex.Enabled;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.treeIndexesEditing(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex; var Allowed: Boolean);
|
|
var
|
|
VT: TVirtualStringtree;
|
|
IndexedColName: String;
|
|
i: Integer;
|
|
begin
|
|
VT := Sender as TVirtualStringtree;
|
|
Allowed := False;
|
|
if VT.GetNodeLevel(Node) = 0 then begin
|
|
// Disallow renaming primary key
|
|
if (Column <> 0) or (VT.Text[Node, 1] <> PKEY) then
|
|
Allowed := True
|
|
end else begin
|
|
// Column length is allowed for (var)char/text types only, even mandantory for text and blobs
|
|
IndexedColName := VT.Text[Node, 0];
|
|
for i:=0 to FColumns.Count-1 do begin
|
|
if FColumns[i].Name = IndexedColName then begin
|
|
Allowed := FColumns[i].DataType.Category = dtcText;
|
|
break;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.treeIndexesCreateEditor(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex; out EditLink: IVTEditLink);
|
|
var
|
|
VT: TVirtualStringTree;
|
|
EnumEditor: TEnumEditorLink;
|
|
Level: Cardinal;
|
|
ColNode: PVirtualNode;
|
|
Col: PTableColumn;
|
|
begin
|
|
// Start cell editor
|
|
VT := Sender as TVirtualStringTree;
|
|
Level := (Sender as TVirtualStringtree).GetNodeLevel(Node);
|
|
if (Level = 0) and (Column = 1) then begin
|
|
// Index type pulldown
|
|
EnumEditor := TEnumEditorLink.Create(VT);
|
|
EnumEditor.ValueList := TStringList.Create;
|
|
EnumEditor.ValueList.CommaText := PKEY +','+ KEY +','+ UKEY +','+ FKEY +','+ SKEY;
|
|
EditLink := EnumEditor;
|
|
end else if (Level = 0) and (Column = 2) then begin
|
|
// Algorithm pulldown
|
|
EnumEditor := TEnumEditorLink.Create(VT);
|
|
EnumEditor.ValueList := Explode(',', ',BTREE,HASH,RTREE');
|
|
EditLink := EnumEditor;
|
|
end else if (Level = 1) and (Column = 0) then begin
|
|
// Column names pulldown
|
|
EnumEditor := TEnumEditorLink.Create(VT);
|
|
ColNode := listColumns.GetFirst;
|
|
while Assigned(ColNode) do begin
|
|
Col := listColumns.GetNodeData(ColNode);
|
|
EnumEditor.ValueList.Add(Col.Name);
|
|
ColNode := listColumns.GetNext(ColNode);
|
|
end;
|
|
EnumEditor.AllowCustomText := True; // Allows adding a subpart in index parts: "TextCol(20)"
|
|
EditLink := EnumEditor;
|
|
end else
|
|
EditLink := TInplaceEditorLink.Create(VT);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.treeIndexesNewText(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex; NewText: String);
|
|
var
|
|
VT: TVirtualStringtree;
|
|
TblKey: TTableKey;
|
|
rx: TRegExpr;
|
|
begin
|
|
// Rename index of column
|
|
VT := Sender as TVirtualStringtree;
|
|
case VT.GetNodeLevel(Node) of
|
|
0: begin
|
|
TblKey := FKeys[Node.Index];
|
|
case Column of
|
|
0: TblKey.Name := NewText;
|
|
1: TblKey.IndexType := NewText;
|
|
2: TblKey.Algorithm := NewText;
|
|
end;
|
|
// Needs to be called manually for Name and IndexType properties:
|
|
TblKey.Modification(Sender);
|
|
end;
|
|
1: begin
|
|
TblKey := FKeys[Node.Parent.Index];
|
|
case Column of
|
|
0: begin
|
|
// Detect input of "col(123)" and move "123" into subpart
|
|
rx := TRegExpr.Create;
|
|
rx.Expression := '.+\((\d+)\)';
|
|
if rx.Exec(NewText) then begin
|
|
TblKey.Columns[Node.Index] := Copy(NewText, 1, Length(NewText)-rx.MatchLen[1]-2);
|
|
TblKey.Subparts[Node.Index] := rx.Match[1];
|
|
end else
|
|
TblKey.Columns[Node.Index] := NewText;
|
|
end;
|
|
1: TblKey.SubParts[Node.Index] := NewText;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
Modification(Sender);
|
|
VT.RepaintNode(Node);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.treeIndexesBeforePaint(Sender: TBaseVirtualTree;
|
|
TargetCanvas: TCanvas);
|
|
begin
|
|
// (Re)paint index list
|
|
(Sender as TVirtualStringTree).RootNodeCount := FKeys.Count;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.treeIndexesDragOver(Sender: TBaseVirtualTree;
|
|
Source: TObject; Shift: TShiftState; State: TDragState; Pt: TPoint;
|
|
Mode: TDropMode; var Effect: Integer; var Accept: Boolean);
|
|
var
|
|
Node: PVirtualNode;
|
|
begin
|
|
// Accept nodes from the column list and allow column moving
|
|
if Source = listColumns then begin
|
|
Accept := True;
|
|
Exit;
|
|
end else if Source = Sender then begin
|
|
Node := Sender.GetNodeAt(Pt.X, Pt.Y);
|
|
Accept := Assigned(Node) and (Sender.GetNodeLevel(Node) = 1) and
|
|
(Node <> Sender.FocusedNode) and (Node.Parent = Sender.FocusedNode.Parent);
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.treeIndexesDragDrop(Sender: TBaseVirtualTree;
|
|
Source: TObject; DataObject: IDataObject; Formats: TFormatArray;
|
|
Shift: TShiftState; Pt: TPoint; var Effect: Integer; Mode: TDropMode);
|
|
var
|
|
Node: PVirtualNode;
|
|
ColName, PartLength: String;
|
|
ColPos: Integer;
|
|
VT, SourceVT: TVirtualStringtree;
|
|
Col: PTableColumn;
|
|
TblKey: TTableKey;
|
|
begin
|
|
// Column node dropped here
|
|
VT := Sender as TVirtualStringtree;
|
|
SourceVT := Source as TVirtualStringtree;
|
|
Node := VT.GetNodeAt(Pt.X, Pt.Y);
|
|
if not Assigned(Node) then begin
|
|
MessageBeep(MB_ICONEXCLAMATION);
|
|
Exit;
|
|
end;
|
|
if VT.GetNodeLevel(Node) = 1 then begin
|
|
ColPos := Node.Index;
|
|
if Mode = dmBelow then Inc(ColPos);
|
|
Node := Node.Parent;
|
|
end else
|
|
ColPos := Node.ChildCount;
|
|
|
|
if Source = Sender then
|
|
MoveFocusedIndexPart(ColPos)
|
|
else begin
|
|
TblKey := FKeys[Node.Index];
|
|
Col := SourceVT.GetNodeData(SourceVT.FocusedNode);
|
|
ColName := Col.Name;
|
|
if TblKey.Columns.IndexOf(ColName) > -1 then begin
|
|
if MessageDialog('Add duplicated column to index?', 'Index "'+VT.Text[Node, 0]+'" already contains the column "'+ColName+'". It is possible to add a column twice into a index, but total nonsense in practice.',
|
|
mtConfirmation, [mbYes, mbNo]) = mrNo then
|
|
Exit;
|
|
end;
|
|
|
|
TblKey.Columns.Insert(ColPos, ColName);
|
|
PartLength := '';
|
|
if (TblKey.IndexType <> FKEY) and (Col.DataType.Index in [dtTinyText, dtText, dtMediumText, dtLongText, dtTinyBlob, dtBlob, dtMediumBlob, dtLongBlob]) then
|
|
PartLength := '100';
|
|
TblKey.Subparts.Insert(ColPos, PartLength);
|
|
Node.States := Node.States + [vsHasChildren, vsExpanded];
|
|
end;
|
|
Modification(Sender);
|
|
// Finally tell parent node to update its children
|
|
VT.ReinitChildren(Node, False);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.btnMoveUpIndexClick(Sender: TObject);
|
|
begin
|
|
// Move index part up
|
|
MoveFocusedIndexPart(treeIndexes.FocusedNode.Index-1);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.btnMoveDownIndexClick(Sender: TObject);
|
|
begin
|
|
// Move index part down
|
|
MoveFocusedIndexPart(treeIndexes.FocusedNode.Index+1);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.MoveFocusedIndexPart(NewIdx: Cardinal);
|
|
var
|
|
TblKey: TTableKey;
|
|
begin
|
|
// Move focused index or index part
|
|
if treeIndexes.IsEditing then
|
|
treeIndexes.EndEditNode;
|
|
TblKey := FKeys[treeIndexes.FocusedNode.Parent.Index];
|
|
TblKey.Columns.Move(treeIndexes.FocusedNode.Index, NewIdx);
|
|
TblKey.SubParts.Move(treeIndexes.FocusedNode.Index, NewIdx);
|
|
Modification(treeIndexes);
|
|
SelectNode(treeIndexes, NewIdx, treeIndexes.FocusedNode.Parent);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.PageControlMainChange(Sender: TObject);
|
|
var
|
|
SupportsForeignKeys: Boolean;
|
|
begin
|
|
treeIndexes.EndEditNode;
|
|
listForeignKeys.EndEditNode;
|
|
if PageControlMain.ActivePage = tabForeignKeys then begin
|
|
SupportsForeignKeys := LowerCase(comboEngine.Text) = 'innodb';
|
|
ListForeignKeys.Enabled := SupportsForeignKeys;
|
|
tlbForeignKeys.Enabled := SupportsForeignKeys;
|
|
pnlNoForeignKeys.Caption := 'The selected table engine ('+comboEngine.Text+') does not support foreign keys.';
|
|
if SupportsForeignKeys then
|
|
ListForeignKeys.Margins.Bottom := 0
|
|
else
|
|
ListForeignKeys.Margins.Bottom := GetTextHeight(pnlNoForeignKeys.Font)+4;
|
|
ListForeignKeys.Repaint;
|
|
end;
|
|
UpdateSQLcode;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.UpdateSQLcode;
|
|
var
|
|
OldTopLine: Integer;
|
|
Query: TSQLSentence;
|
|
begin
|
|
if (PageControlMain.ActivePage = tabALTERCode) and (not AlterCodeValid) then begin
|
|
SynMemoALTERcode.BeginUpdate;
|
|
OldTopLine := SynMemoALTERcode.TopLine;
|
|
SynMemoALTERcode.Clear;
|
|
for Query in ComposeAlterStatement do
|
|
SynMemoALTERcode.Text := SynMemoALTERcode.Text + Query.SQL + ';' + CRLF;
|
|
SynMemoALTERcode.TopLine := OldTopLine;
|
|
SynMemoALTERcode.EndUpdate;
|
|
AlterCodeValid := True;
|
|
end else if (PageControlMain.ActivePage = tabCREATECode) and (not CreateCodeValid) then begin
|
|
SynMemoCREATEcode.BeginUpdate;
|
|
OldTopLine := SynMemoCREATEcode.TopLine;
|
|
SynMemoCREATEcode.Clear;
|
|
for Query in ComposeCreateStatement do
|
|
SynMemoCREATEcode.Text := SynMemoCREATEcode.Text + Query.SQL + ';' + CRLF;
|
|
SynMemoCREATEcode.TopLine := OldTopLine;
|
|
SynMemoCREATEcode.EndUpdate;
|
|
CreateCodeValid := True;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.chkCharsetConvertClick(Sender: TObject);
|
|
begin
|
|
chkCharsetConvert.Enabled := (DBObject.Name <> '') and (comboCollation.ItemIndex > -1);
|
|
listColumns.Repaint;
|
|
Modification(Sender);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.popupColumnsPopup(Sender: TObject);
|
|
function AddItem(Parent: TMenuItem; Caption: String; ImageIndex: Integer): TMenuItem;
|
|
begin
|
|
Result := TMenuItem.Create(Parent.GetParentMenu);
|
|
Result.Caption := Caption;
|
|
Result.ImageIndex := ImageIndex;
|
|
Result.OnClick := AddIndexByColumn;
|
|
Parent.Add(Result);
|
|
end;
|
|
var
|
|
i: Integer;
|
|
Item: TMenuItem;
|
|
PrimaryKeyExists,
|
|
ColumnsSelected: Boolean;
|
|
IndexName: String;
|
|
Node: PVirtualNode;
|
|
Col: PTableColumn;
|
|
begin
|
|
ColumnsSelected := ListColumns.SelectedCount > 0;
|
|
menuCopyColumns.Enabled := ColumnsSelected;
|
|
menuPasteColumns.Enabled := Clipboard.HasFormat(CF_TEXT);
|
|
menuAddToIndex.Clear;
|
|
menuCreateIndex.Clear;
|
|
menuAddToIndex.Enabled := ColumnsSelected;
|
|
menuCreateIndex.Enabled := ColumnsSelected;
|
|
if not ColumnsSelected then
|
|
Exit;
|
|
|
|
// Auto create submenu items for "Add to index" ...
|
|
PrimaryKeyExists := False;
|
|
for i:=0 to FKeys.Count-1 do begin
|
|
if FKeys[i].IndexType = PKEY then begin
|
|
PrimaryKeyExists := True;
|
|
IndexName := PKEY;
|
|
end else
|
|
IndexName := FKeys[i].Name + ' ('+FKeys[i].IndexType+')';
|
|
Item := AddItem(menuAddToIndex, IndexName, GetIndexIcon(FKeys[i].IndexType));
|
|
// Disable menuitem if all selected columns are already part of this index,
|
|
// enable it if one or more selected columns are not.
|
|
Item.Enabled := False;
|
|
Node := GetNextNode(listColumns, nil, True);
|
|
while Assigned(Node) do begin
|
|
Col := listColumns.GetNodeData(Node);
|
|
if FKeys[i].Columns.IndexOf(Col.Name) = -1 then begin
|
|
Item.Enabled := True;
|
|
Break;
|
|
end;
|
|
Node := GetNextNode(listColumns, Node, True);
|
|
end;
|
|
end;
|
|
menuAddToIndex.Enabled := menuAddToIndex.Count > 0;
|
|
|
|
// ... and for item "Create index"
|
|
Item := AddItem(menuCreateIndex, PKEY, ICONINDEX_PRIMARYKEY);
|
|
Item.Enabled := not PrimaryKeyExists;
|
|
AddItem(menuCreateIndex, KEY, ICONINDEX_INDEXKEY);
|
|
AddItem(menuCreateIndex, UKEY, ICONINDEX_UNIQUEKEY);
|
|
AddItem(menuCreateIndex, FKEY, ICONINDEX_FULLTEXTKEY);
|
|
AddItem(menuCreateIndex, SKEY, ICONINDEX_SPATIALKEY);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.AddIndexByColumn(Sender: TObject);
|
|
var
|
|
Item: TMenuItem;
|
|
i: Integer;
|
|
NewType: String;
|
|
NewParts: TStringList;
|
|
TblKey: TTableKey;
|
|
Node: PVirtualNode;
|
|
begin
|
|
// Auto create index or add columns to existing one by rightclicking a column
|
|
Item := (Sender as TMenuItem);
|
|
NewParts := TStringList.Create;
|
|
Node := GetNextNode(listColumns, nil, True);
|
|
while Assigned(Node) do begin
|
|
NewParts.Add(listColumns.Text[Node, 1]);
|
|
Node := GetNextNode(listColumns, Node, True);
|
|
end;
|
|
if Item.Parent = menuCreateIndex then begin
|
|
NewType := StripHotkey(Item.Caption);
|
|
// Avoid creating a second key with the same columns
|
|
for i:=0 to FKeys.Count-1 do begin
|
|
TblKey := FKeys[i];
|
|
if (TblKey.IndexType = NewType) and (TblKey.Columns.Text = NewParts.Text) then begin
|
|
if MessageDialog('Key already exists. Really create another identical one?',
|
|
'This will increase disk usage and probably slow down queries on this table.',
|
|
mtConfirmation, [mbYes, mbNo]) = mrNo then
|
|
Exit;
|
|
break;
|
|
end;
|
|
end;
|
|
TblKey := TTableKey.Create(DBObject.Connection);
|
|
TblKey.Name := ImplodeStr('_', NewParts);
|
|
TblKey.IndexType := NewType;
|
|
TblKey.Added := True;
|
|
TblKey.Columns := NewParts;
|
|
for i:=0 to TblKey.Columns.Count do
|
|
TblKey.SubParts.Add('');
|
|
FKeys.Add(TblKey);
|
|
PageControlMain.ActivePage := tabIndexes;
|
|
treeIndexes.Repaint;
|
|
SelectNode(treeIndexes, FKeys.Count-1);
|
|
SelectNode(treeIndexes, 0, treeIndexes.FocusedNode);
|
|
end else begin
|
|
PageControlMain.ActivePage := tabIndexes;
|
|
TblKey := FKeys[Item.MenuIndex];
|
|
for i:=0 to NewParts.Count-1 do begin
|
|
if TblKey.Columns.IndexOf(NewParts[i]) = -1 then begin
|
|
TblKey.Columns.Add(NewParts[i]);
|
|
TblKey.Subparts.Add('');
|
|
end;
|
|
end;
|
|
SelectNode(treeIndexes, Item.MenuIndex);
|
|
treeIndexes.ReinitNode(treeIndexes.FocusedNode, False);
|
|
SelectNode(treeIndexes, TblKey.Columns.Count-1, treeIndexes.FocusedNode);
|
|
end;
|
|
Modification(Sender);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.btnAddForeignKeyClick(Sender: TObject);
|
|
var
|
|
Key: TForeignKey;
|
|
idx: Integer;
|
|
begin
|
|
// Add new foreign key
|
|
Key := TForeignKey.Create(DBObject.Connection);
|
|
idx := FForeignKeys.Add(Key);
|
|
Key.KeyName := 'FK'+IntToStr(idx+1);
|
|
Key.OnUpdate := '';
|
|
Key.OnDelete := '';
|
|
Key.Added := True;
|
|
Modification(Sender);
|
|
listForeignKeys.Repaint;
|
|
SelectNode(listForeignKeys, idx);
|
|
listForeignKeys.EditNode(listForeignKeys.FocusedNode, listForeignKeys.Header.MainColumn);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.btnRemoveForeignKeyClick(Sender: TObject);
|
|
var
|
|
Key: TForeignKey;
|
|
begin
|
|
// Remove a foreign key
|
|
if listForeignKeys.IsEditing then
|
|
listForeignKeys.CancelEditNode;
|
|
Key := FForeignKeys[listForeignKeys.FocusedNode.Index];
|
|
if not Key.Added then
|
|
DeletedForeignKeys.Add(Key.OldKeyName);
|
|
FForeignKeys.Delete(listForeignKeys.FocusedNode.Index);
|
|
Modification(Sender);
|
|
listForeignKeys.Repaint;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.btnClearForeignKeysClick(Sender: TObject);
|
|
var
|
|
i: Integer;
|
|
begin
|
|
// Clear all foreign keys
|
|
if listForeignKeys.IsEditing then
|
|
listForeignKeys.CancelEditNode;
|
|
for i:=FForeignKeys.Count-1 downto 0 do begin
|
|
if not FForeignKeys[i].Added then
|
|
DeletedForeignKeys.Add(FForeignKeys[i].OldKeyName);
|
|
FForeignKeys.Delete(i);
|
|
end;
|
|
Modification(Sender);
|
|
listForeignKeys.Repaint;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listForeignKeysBeforePaint(Sender: TBaseVirtualTree; TargetCanvas: TCanvas);
|
|
var
|
|
VT: TVirtualStringTree;
|
|
begin
|
|
// Set RootNodeCount
|
|
VT := Sender as TVirtualStringTree;
|
|
VT.RootNodeCount := FForeignKeys.Count;
|
|
btnClearForeignKeys.Enabled := VT.RootNodeCount > 0;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listForeignKeysEditing(Sender: TBaseVirtualTree; Node: PVirtualNode;
|
|
Column: TColumnIndex; var Allowed: Boolean);
|
|
var
|
|
Key: TForeignKey;
|
|
begin
|
|
// Disallow editing foreign columns when no reference table was selected.
|
|
// Also, check for existance of reference table and warn if it's missing.
|
|
if Column = 3 then begin
|
|
Key := FForeignKeys[Node.Index];
|
|
Allowed := False;
|
|
if Key.ReferenceTable = '' then
|
|
ErrorDialog('Please select a reference table before selecting foreign columns.')
|
|
else begin
|
|
try
|
|
DBObject.Connection.GetVar('SELECT 1 FROM '+DBObject.Connection.QuoteIdent(Key.ReferenceTable, True, '.'));
|
|
Allowed := True;
|
|
except
|
|
// Leave Allowed = False
|
|
ErrorDialog('Reference table "'+Key.ReferenceTable+'" seems to be missing, broken or non-accessible.')
|
|
end;
|
|
end;
|
|
end else
|
|
Allowed := True;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listForeignKeysCreateEditor(
|
|
Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex;
|
|
out EditLink: IVTEditLink);
|
|
var
|
|
VT: TVirtualStringTree;
|
|
EnumEditor: TEnumEditorLink;
|
|
SetEditor: TSetEditorLink;
|
|
DBObjects: TDBObjectList;
|
|
Key: TForeignKey;
|
|
ColNode: PVirtualNode;
|
|
Col: PTableColumn;
|
|
i: Integer;
|
|
begin
|
|
// Init grid editor in foreign key list
|
|
VT := Sender as TVirtualStringTree;
|
|
case Column of
|
|
0: EditLink := TInplaceEditorLink.Create(VT);
|
|
1: begin
|
|
SetEditor := TSetEditorLink.Create(VT);
|
|
ColNode := listColumns.GetFirst;
|
|
while Assigned(ColNode) do begin
|
|
Col := listColumns.GetNodeData(ColNode);
|
|
SetEditor.ValueList.Add(Col.Name);
|
|
ColNode := listColumns.GetNextSibling(ColNode);
|
|
end;
|
|
EditLink := SetEditor;
|
|
end;
|
|
2: begin
|
|
EnumEditor := TEnumEditorLink.Create(VT);
|
|
EnumEditor.AllowCustomText := True;
|
|
DBObjects := DBObject.Connection.GetDBObjects(DBObject.Connection.Database);
|
|
for i:=0 to DBObjects.Count-1 do begin
|
|
if DBObjects[i].NodeType = lntTable then
|
|
EnumEditor.ValueList.Add(DBObjects[i].Name);
|
|
end;
|
|
EditLink := EnumEditor;
|
|
end;
|
|
3: begin
|
|
Key := FForeignKeys[Node.Index];
|
|
SetEditor := TSetEditorLink.Create(VT);
|
|
SetEditor.ValueList := DBObject.Connection.GetCol('SHOW COLUMNS FROM '+DBObject.Connection.QuoteIdent(Key.ReferenceTable, True, '.'));
|
|
EditLink := SetEditor;
|
|
end;
|
|
4, 5: begin
|
|
EnumEditor := TEnumEditorLink.Create(VT);
|
|
EnumEditor.ValueList.Text := 'RESTRICT'+CRLF+'CASCADE'+CRLF+'SET NULL'+CRLF+'NO ACTION';
|
|
EditLink := EnumEditor;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listForeignKeysFocusChanged(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex);
|
|
begin
|
|
// Focus on foreign key list changed
|
|
btnRemoveForeignKey.Enabled := Assigned(Sender.FocusedNode);
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listForeignKeysGetImageIndex(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex;
|
|
var Ghosted: Boolean; var ImageIndex: Integer);
|
|
begin
|
|
// Return image index for node cell in foreign key list
|
|
if not (Kind in [ikNormal, ikSelected]) then Exit;
|
|
case Column of
|
|
0: ImageIndex := 136;
|
|
else ImageIndex := -1;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listForeignKeysGetText(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
|
|
var CellText: String);
|
|
var
|
|
Key: TForeignKey;
|
|
begin
|
|
// Return cell text in foreign key list
|
|
Key := FForeignKeys[Node.Index];
|
|
case Column of
|
|
0: CellText := Key.KeyName;
|
|
1: CellText := ImplodeStr(',', Key.Columns);
|
|
2: CellText := Key.ReferenceTable;
|
|
3: CellText := ImplodeStr(',', Key.ForeignColumns);
|
|
4: begin
|
|
CellText := Key.OnUpdate;
|
|
// Both ON UPDATE + DELETE default to "RESTRICT", see http://dev.mysql.com/doc/refman/5.1/en/innodb-foreign-key-constraints.html
|
|
if CellText = '' then
|
|
CellText := 'RESTRICT';
|
|
end;
|
|
5: begin
|
|
CellText := Key.OnDelete;
|
|
if CellText = '' then
|
|
CellText := 'RESTRICT';
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.listForeignKeysNewText(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex; NewText: String);
|
|
var
|
|
Key, OtherKey: TForeignKey;
|
|
i: Integer;
|
|
NameInUse: Boolean;
|
|
begin
|
|
// Cell text in foreign key list edited
|
|
Key := FForeignKeys[Node.Index];
|
|
Key.Modified := True;
|
|
Modification(Sender);
|
|
case Column of
|
|
0: begin
|
|
Key.KeyName := NewText;
|
|
Key.KeyNameWasCustomized := True;
|
|
end;
|
|
1: Key.Columns := Explode(',', NewText);
|
|
2: begin
|
|
Key.ReferenceTable := NewText;
|
|
if not Key.KeyNameWasCustomized then begin
|
|
Key.KeyName := 'FK_'+DBObject.Name+'_'+Key.ReferenceTable;
|
|
i := 1;
|
|
NameInUse := True;
|
|
while NameInUse do begin
|
|
for OtherKey in FForeignKeys do begin
|
|
NameInUse := (Key <> OtherKey) and (Key.KeyName = OtherKey.KeyName);
|
|
if NameInUse then break;
|
|
end;
|
|
if NameInUse then begin
|
|
Inc(i);
|
|
Key.KeyName := 'FK_'+DBObject.Name+'_'+Key.ReferenceTable+'_'+IntToStr(i);
|
|
end;
|
|
end;
|
|
|
|
end;
|
|
end;
|
|
3: Key.ForeignColumns := Explode(',', NewText);
|
|
4: Key.OnUpdate := NewText;
|
|
5: Key.OnDelete := NewText;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.btnHelpClick(Sender: TObject);
|
|
begin
|
|
// Help button
|
|
Mainform.CallSQLHelpWithKeyword('CREATE TABLE');
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.popupSQLmemoPopup(Sender: TObject);
|
|
begin
|
|
// Ensure SynMemo's have focus, otherwise Select-All and Copy actions may fail
|
|
if PageControlMain.ActivePage = tabCREATEcode then
|
|
SynMemoCreateCode.SetFocus
|
|
else if PageControlMain.ActivePage = tabALTERcode then
|
|
SynMemoAlterCode.SetFocus;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.menuCopyColumnsClick(Sender: TObject);
|
|
var
|
|
Node: PVirtualNode;
|
|
Col: PTableColumn;
|
|
SQL: String;
|
|
begin
|
|
// Copy selected columns as a CREATE TABLE query to clipboard
|
|
Node := GetNextNode(listColumns, nil, True);
|
|
SQL := 'CREATE TABLE dummy ('+CRLF;
|
|
while Assigned(Node) do begin
|
|
Col := listColumns.GetNodeData(Node);
|
|
SQL := SQL + #9 + Col.SQLCode + ','+CRLF;
|
|
Node := GetNextNode(listColumns, Node, True);
|
|
end;
|
|
Delete(SQL, Length(SQL)-2, 3);
|
|
SQL := SQL + CRLF + ')';
|
|
Clipboard.AsText := SQL;
|
|
end;
|
|
|
|
|
|
procedure TfrmTableEditor.menuPasteColumnsClick(Sender: TObject);
|
|
var
|
|
Columns: TTableColumnList;
|
|
Node: PVirtualNode;
|
|
Col: TTableColumn;
|
|
begin
|
|
Columns := TTableColumnList.Create(False);
|
|
DBObject.Connection.ParseTableStructure(Clipboard.AsText, Columns, nil, nil);
|
|
Node := listColumns.FocusedNode;
|
|
if not Assigned(Node) then
|
|
Node := listColumns.GetLast;
|
|
listcolumns.BeginUpdate;
|
|
try
|
|
for Col in Columns do begin
|
|
Col.Status := esAddedUntouched;
|
|
// Create new node, insert column structure into list, and let OnInitNode bind its pointer
|
|
Node := listColumns.InsertNode(Node, amInsertAfter, nil);
|
|
FColumns.Insert(Node.Index, Col);
|
|
end;
|
|
finally
|
|
listcolumns.EndUpdate;
|
|
end;
|
|
listColumns.Invalidate;
|
|
Modification(Sender);
|
|
Columns.Free;
|
|
end;
|
|
|
|
|
|
end.
|