unit table_editor; interface uses Windows, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls, ToolWin, VirtualTrees, SynRegExpr, ActiveX, ExtCtrls, SynEdit, SynMemo, Menus, Clipbrd, Math, System.UITypes, grideditlinks, mysql_structures, dbconnection, helpers, gnugettext; 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; tabPartitions: TTabSheet; SynMemoPartitions: TSynMemo; Saveastextfile1: 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; TranslateComponent(Self); PageControlMain.Height := AppSettings.ReadInt(asTableEditorTabsHeight); 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 AppSettings.WriteInt(asTableEditorTabsHeight, 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; SynMemoPartitions.Clear; if DBObject.Name = '' then begin // Creating new table editName.Text := ''; if DBObject.Connection.Parameters.IsMySQL 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; rx.Expression := '\b(PARTITION\s+.+)(\*/)'; if rx.Exec(DBObject.CreateCode) then SynMemoPartitions.Text := rx.Match[1] else SynMemoPartitions.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 if ApplyModifications = mrOK then Init(DBObject); end; function TfrmTableEditor.ApplyModifications: TModalResult; var Batch: TSQLBatch; Query: TSQLSentence; i: Integer; Rename: String; begin // Check if all indexes have at least one column // If not, exit early for i := 0 to FKeys.Count-1 do begin if FKeys.Items[i].Columns.Count = 0 then begin ErrorDialog( f_('%s Index "%s" does not contain any column. You can add columns using drag''n drop from the columns list.', [FKeys.Items[i].IndexType, FKeys.Items[i].Name])); Result := mrAbort; Exit; end; end; // 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); // Rename table if (DBObject.Name <> '') and (editName.Text <> DBObject.Name) then begin Rename := DBObject.Connection.GetSQLSpecifity(spRenameTable); Rename := Format(Rename, [DBObject.QuotedName, DBObject.Connection.QuoteIdent(editName.Text)]); DBObject.Connection.Query(Rename); end; 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.tabData.TabVisible := True; 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, OldColName, OverrideCollation: 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.QuotedName + 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 DBObject.Connection.Parameters.IsMySQL and (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 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 OverrideCollation := ''; if chkCharsetConvert.Checked then OverrideCollation := comboCollation.Text; ColSpec := Col.SQLCode(OverrideCollation); // Server version requirement, see http://dev.mysql.com/doc/refman/4.1/en/alter-table.html if (DBObject.Connection.Parameters.NetTypeGroup = ngMySQL) and (DBObject.Connection.ServerVersionInt >= 40001) then begin if PreviousCol = nil then ColSpec := ColSpec + ' FIRST' else ColSpec := ColSpec + ' AFTER '+DBObject.Connection.QuoteIdent(PreviousCol.Name); end; case DBObject.Connection.Parameters.NetTypeGroup of ngMySQL: OldColName := DBObject.Connection.QuoteIdent(Col.OldName); ngMSSQL: OldColName := ''; end; if Col.Status = esModified then Specs.Add(Format(DBObject.Connection.GetSQLSpecifity(spChangeColumn), [OldColName, ColSpec])) else if Col.Status in [esAddedUntouched, esAddedModified] then Specs.Add(Format(DBObject.Connection.GetSQLSpecifity(spAddColumn), [ColSpec])); // MSSQL wants one ALTER TABLE query per ADD/CHANGE COLUMN if DBObject.Connection.Parameters.IsMSSQL then AddQuery; 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 begin Specs.Add('DROP COLUMN '+DBObject.Connection.QuoteIdent(FColumns[i].OldName)); // MSSQL wants one ALTER TABLE query per DROP COLUMN if DBObject.Connection.Parameters.IsMSSQL then AddQuery; end; 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.Text <> 'DEFAULT' 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; if SynMemoPartitions.GetTextLen > 0 then SQL := SQL + '/*!50100 ' + SynMemoPartitions.Text + ' */'; 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; DefaultType: String; 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.Collation := ''; end else begin idx := listColumns.RootNodeCount; DefaultType := 'INT'; NewCol.DataType := DBObject.Connection.GetDatatypeByName(DefaultType, False); 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; Checked: Boolean; 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 := FKeys[i].ImageIndex; 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]) then begin Checked := (Col.Unsigned and (Column=4)) or (Col.AllowNull and (Column=5)) or (Col.ZeroFill and (Column = 6)); if CellEditingAllowed(Node, Column) then begin if Checked then ImageIndex := 128 else ImageIndex := 127; end else begin if Checked then ImageIndex := 176 else ImageIndex := 175; end; 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) and (DBObject.Connection.Parameters.NetTypeGroup = ngMySQL); btnMoveDownColumn.Enabled := Assigned(Node) and (Node <> listColumns.GetLast) and (DBObject.Connection.Parameters.NetTypeGroup = ngMySQL); 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; i: Integer; 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: begin Result := (Col.DataType.Category in [dtcInteger, dtcReal]) and (Col.DataType.Index <> dtBit) and (DBObject.Connection.Parameters.IsMySQL); if (not Result) and Col.Unsigned then begin Col.Unsigned := False; Col.Status := esModified; end; end; 5: begin // Do not allow NULL, and force NOT NULL, on primary key columns Result := True; for i:=0 to FKeys.Count-1 do begin if (FKeys[i].IndexType = PKEY) and (FKeys[i].Columns.IndexOf(Col.Name) > -1) then begin if Col.AllowNull then begin Col.AllowNull := False; Col.Status := esModified; end; Result := False; break; end; end; end; 6: begin Result := (Col.DataType.Category in [dtcInteger, dtcReal]) and (Col.DataType.Index <> dtBit) and (DBObject.Connection.Parameters.IsMySQL); if (not Result) and Col.ZeroFill then begin Col.ZeroFill := False; Col.Status := esModified; end; end; // 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: CellText := Col.DefaultText; 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) and (not (FColumns[i].Status in [esDeleted, esAddedDeleted])) then begin ErrorDialog(f_('Column "%s" already exists.', [NewText])); 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, False); // 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); 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; 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).TableColumn := Col^; 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 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 TblKey in FKeys do begin 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 := FKeys[Node.Index].ImageIndex; 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 case Column of 0: Allowed := True; 1: 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; 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: begin TblKey.IndexType := NewText; if NewText = PKEY then TblKey.Name := PKEY; end; 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; TblKey.Modified := True; 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?'), f_('Index "%s" already contains the column "%s". It is possible to add a column twice into a index, but total nonsense in practice.', [VT.Text[Node, 0], ColName]), 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 // Foreign keys supported by InnoDB engine and NDB cluster. See http://www.heidisql.com/forum.php?t=16059 SupportsForeignKeys := (LowerCase(comboEngine.Text) = 'innodb') or (DBObject.Connection.NdbClusterVersionInt >= 70300); ListForeignKeys.Enabled := SupportsForeignKeys; tlbForeignKeys.Enabled := SupportsForeignKeys; pnlNoForeignKeys.Caption := f_('The selected table engine (%s) does not support foreign keys.', [comboEngine.Text]); 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, FKeys[i].ImageIndex); // 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, '.')+' LIMIT 1'); Allowed := True; except // Leave Allowed = False ErrorDialog(f_('Reference table "%s" seems to be missing, broken or non-accessible.', [Key.ReferenceTable])) 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; Obj: TDBObject; 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 Obj in DBObjects do begin if (Obj.NodeType = lntTable) and (LowerCase(Obj.Engine) = 'innodb') then EnumEditor.ValueList.Add(Obj.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, j, k: Integer; NameInUse: Boolean; RefCreateCode, RefDatabase, RefTable: String; KeyColumnsSQLCode, RefColumnsSQLCode: String; Err: String; RefColumns: TTableColumnList; TypesMatch: 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: begin Key.ForeignColumns := Explode(',', NewText); // Compare data types and unsigned flags of source and reference columns. See issue #400 Err := ''; if Key.Columns.Count <> Key.ForeignColumns.Count then begin Err := _('Foreign column count does not match source column count.'); end else begin RefDatabase := DBObject.Database; RefTable := Key.ReferenceTable; i := Pos('.', RefTable); if i > 0 then begin RefDatabase := Copy(RefTable, 1, i-1); RefTable := Copy(RefTable, i+1, MaxInt); end; RefCreateCode := DBObject.Connection.GetCreateCode(RefDatabase, '', RefTable, lntTable); RefColumns := TTableColumnList.Create(True); DBObject.Connection.ParseTableStructure(RefCreateCode, RefColumns, nil, nil); TypesMatch := True; KeyColumnsSQLCode := ''; RefColumnsSQLCode := ''; for i:=0 to Key.Columns.Count-1 do begin for j:=0 to FColumns.Count-1 do begin if FColumns[j].Name = Key.Columns[i] then begin KeyColumnsSQLCode := KeyColumnsSQLCode + FColumns[j].SQLCode + CRLF; for k:=0 to RefColumns.Count-1 do begin if RefColumns[k].Name = Key.ForeignColumns[i] then begin RefColumnsSQLCode := RefColumnsSQLCode + RefColumns[k].SQLCode + CRLF; TypesMatch := TypesMatch and (RefColumns[k].DataType.Index = FColumns[j].DataType.Index) and (RefColumns[k].Unsigned = FColumns[j].Unsigned); end; end; end; end; end; if not TypesMatch then begin Err := _('The selected foreign column do not match the source columns data type and unsigned flag. This will give you an error message when trying to save this change. Please compare yourself:'); Err := Err + CRLF + CRLF + KeyColumnsSQLCode + CRLF + Trim(RefColumnsSQLCode); end; end; if Err <> '' then ErrorDialog(_('Foreign key mismatch'), Err); end; 4: Key.OnUpdate := NewText; 5: Key.OnDelete := NewText; end; end; procedure TfrmTableEditor.btnHelpClick(Sender: TObject); begin // Help button Help(Self, 'createtable'); 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.