unit table_editor; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, TntStdCtrls, ComCtrls, ToolWin, VirtualTrees, WideStrings, SynRegExpr, ActiveX, DB, ExtCtrls, ImgList, SynEdit, SynMemo, Menus, WideStrUtils; type TfrmTableEditor = class(TFrame) btnSave: TButton; btnDiscard: TButton; btnHelp: TButton; listColumns: TVirtualStringTree; PageControlMain: TPageControl; tabBasic: TTabSheet; tabIndexes: TTabSheet; tabOptions: TTabSheet; lblName: TLabel; editName: TTntEdit; memoComment: TTntMemo; 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: TTntMemo; comboInsertMethod: TComboBox; lblCollation: TLabel; comboCollation: TTNTComboBox; lblEngine: TLabel; comboEngine: TComboBox; treeIndexes: TVirtualStringTree; tlbIndexes: TToolBar; btnAddIndex: TToolButton; btnRemoveIndex: TToolButton; btnClearIndexes: TToolButton; btnMoveUpIndex: TToolButton; btnMoveDownIndex: TToolButton; StaticText1: TStaticText; pnlColumnsTop: TPanel; tlbColumns: TToolBar; btnAddColumn: TToolButton; btnRemoveColumn: TToolButton; btnClearColumns: 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; menuClearColumns: TMenuItem; menuMoveUpColumn: TMenuItem; menuMoveDownColumn: TMenuItem; chkCharsetConvert: TCheckBox; procedure editNameChange(Sender: TObject); procedure Modification(Sender: TObject); procedure btnAddColumnClick(Sender: TObject); procedure btnRemoveColumnClick(Sender: TObject); procedure btnClearColumnsClick(Sender: TObject); procedure listColumnsBeforePaint(Sender: TBaseVirtualTree; TargetCanvas: TCanvas); procedure listColumnsFocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex); procedure btnHelpClick(Sender: TObject); procedure listColumnsGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: WideString); procedure listColumnsEditing(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; var Allowed: Boolean); procedure listColumnsNewText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; NewText: WideString); 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: WideString); 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: WideString); 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); private { Private declarations } FModified: Boolean; FLoaded: Boolean; FAlterTableName: WideString; CreateCodeValid, AlterCodeValid: Boolean; Columns, ColumnNames, ColumnsChanges, Indexes, OldIndexes: TWideStringList; procedure ColumnsChange(Sender: TObject); procedure IndexesChange(Sender: TObject); procedure ValidateColumnControls; procedure ValidateIndexControls; procedure SelectNode(VT: TVirtualStringTree; idx: Cardinal; ParentNode: PVirtualNode=nil); procedure MoveFocusedColumn(NewIdx: Cardinal); procedure MoveFocusedIndexPart(NewIdx: Cardinal); procedure SetModified(Value: Boolean); procedure ResetModificationFlags; function ComposeCreateStatement: WideString; function ComposeAlterStatement: WideString; function GetIndexSQL(IndexList: TWideStringlist; idx: Integer): WideString; property Modified: Boolean read FModified write SetModified; procedure UpdateSQLcode; function CellEditingAllowed(Node: PVirtualNode; Column: TColumnIndex): Boolean; public { Public declarations } constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure Init(AlterTableName: WideString=''); end; implementation uses main, helpers, grideditlinks, mysql_structures; const PKEY = 'PRIMARY'; KEY = 'KEY'; UKEY = 'UNIQUE'; FKEY = 'FULLTEXT'; SKEY = 'SPATIAL'; NotModifiedFlag : Byte = 0; ModifiedFlag : Byte = 1; {$R *.dfm} constructor TfrmTableEditor.Create(AOwner: TComponent); begin inherited Create(AOwner); // Do not set alClient via DFM! In conjunction with ExplicitXXX properties that // repeatedly breaks the GUI layout when you reload the project Align := alClient; PageControlMain.Height := GetRegValue(REGNAME_TABLEEDITOR_TABSHEIGHT, PageControlMain.Height); InheritFont(Font); FixVT(listColumns); FixVT(treeIndexes); // 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[6].Width := Mainform.Canvas.TextWidth('AUTO_INCREMENT') + 4*listColumns.TextMargin; listColumns.Header.Columns[8].Width := Mainform.Canvas.TextWidth('macroman_general_ci') + 6*listColumns.TextMargin; // Overide column widths by custom values Mainform.RestoreListSetup(listColumns); Mainform.RestoreListSetup(treeIndexes); comboRowFormat.Items.CommaText := 'DEFAULT,DYNAMIC,FIXED,COMPRESSED,REDUNDANT,COMPACT'; comboInsertMethod.Items.CommaText := 'NO,FIRST,LAST'; SynMemoALTERcode.TabWidth := Mainform.SynMemoQuery.TabWidth; SynMemoALTERcode.Font.Name := Mainform.SynMemoQuery.Font.Name; SynMemoALTERcode.Font.Size := Mainform.SynMemoQuery.Font.Size; SynMemoCREATEcode.TabWidth := Mainform.SynMemoQuery.TabWidth; SynMemoCREATEcode.Font.Name := Mainform.SynMemoQuery.Font.Name; SynMemoCREATEcode.Font.Size := Mainform.SynMemoQuery.Font.Size; Columns := TWideStringList.Create; Columns.OnChange := ColumnsChange; ColumnNames := TWideStringList.Create; ColumnsChanges := TWideStringList.Create; Indexes := TWideStringList.Create; Indexes.OnChange := IndexesChange; OldIndexes := TWideStringList.Create; end; destructor TfrmTableEditor.Destroy; begin // Store GUI setup OpenRegistry; MainReg.WriteInteger(REGNAME_TABLEEDITOR_TABSHEIGHT, PageControlMain.Height); Mainform.SaveListSetup(listColumns); Mainform.SaveListSetup(treeIndexes); Inherited; end; procedure TfrmTableEditor.Init(AlterTableName: WideString=''); var ds: TDataset; Props: TWideStringlist; IndexType: String; LastKeyName, CreateTable, engine, ColSpec: WideString; rx, rxCol: TRegExpr; i: Integer; InLiteral: Boolean; ColDefaultType: TColumnDefaultType; ColDefaultText: WideString; begin Mainform.showstatus('Initializing editor ...'); Screen.Cursor := crHourglass; FLoaded := False; // Start with "basic" tab activated when just called if FAlterTableName <> AlterTableName then PageControlMain.ActivePage := tabBasic; Mainform.TableEnginesCombo(comboEngine); comboCollation.Items.Clear; Mainform.GetCollations(comboCollation.Items); FAlterTableName := AlterTableName; btnClearColumnsClick(Self); btnClearIndexesClick(Self); tabALTERcode.TabVisible := FAlterTableName <> ''; if FAlterTableName = '' then begin // Creating new table editName.Clear; Mainform.SetEditorTabCaption(Self, ''); memoComment.Text := ''; editAutoInc.Text := ''; editAvgRowLen.Text := ''; editMaxRows.Text := ''; chkChecksum.Checked := False; comboRowFormat.ItemIndex := 0; comboCollation.ItemIndex := comboCollation.Items.IndexOf(Mainform.GetVar('SHOW VARIABLES LIKE ''collation_database''', 1)); memoUnionTables.Clear; comboInsertMethod.ItemIndex := -1; PageControlMain.ActivePage := tabBasic; end else begin // Editing existing table editName.Text := FAlterTableName; Mainform.SetEditorTabCaption(Self, FAlterTableName); ds := Mainform.GetResults('SHOW TABLE STATUS LIKE '+esc(FAlterTableName)); memoComment.Text := ds.FieldByName(DBO_COMMENT).AsWideString; if ds.FindField(DBO_ENGINE) <> nil then engine := ds.FieldByName(DBO_ENGINE).AsString else engine := ds.FieldByName(DBO_TYPE).AsString; comboEngine.ItemIndex := comboEngine.Items.IndexOf(engine); if ds.FindField(DBO_COLLATION) <> nil then comboCollation.ItemIndex := comboCollation.Items.IndexOf(ds.FieldByName(DBO_COLLATION).AsWideString); editAutoInc.Text := ds.FieldByName(DBO_AUTOINC).AsString; editAvgRowLen.Text := ds.FieldByName(DBO_AVGROWLEN).AsString; comboRowFormat.ItemIndex := comboRowFormat.Items.IndexOf(ds.FieldByName(DBO_ROWFORMAT).AsString); FreeAndNil(ds); CreateTable := Mainform.GetVar('SHOW CREATE TABLE '+Mainform.mask(FAlterTableName), 1); rx := TRegExpr.Create; rx.ModifierI := True; rx.Expression := '\bUNION=\((.+)\)'; if rx.Exec(CreateTable) then memoUnionTables.Text := rx.Match[1] else memoUnionTables.Clear; listColumns.BeginUpdate; rx.ModifierS := False; rx.Expression := '\s+`([^`]+)`\s(\w+)(.+)'; rxCol := TRegExpr.Create; rxCol.ModifierI := True; if rx.Exec(CreateTable) then while true do begin Props := TWideStringlist.Create; Props.OnChange := ColumnsChange; // Create needed number of property columns Props.CommaText := ',,,,,,'; ColSpec := rx.Match[3]; // Strip trailing comma if ColSpec[Length(ColSpec)] = ',' then Delete(ColSpec, Length(ColSpec), 1); // Datatype Props[0] := UpperCase(rx.Match[2]); // Length / Set // Various datatypes, e.g. BLOBs, don't have any length property InLiteral := False; if (ColSpec <> '') and (ColSpec[1] = '(') then begin for i:=2 to Length(ColSpec) do begin if (ColSpec[i] = ')') and (not InLiteral) then break; if ColSpec[i] = '''' then InLiteral := not InLiteral; end; Props[1] := Copy(ColSpec, 2, i-2); Delete(ColSpec, 1, i); end; ColSpec := Trim(ColSpec); // Unsigned if UpperCase(Copy(ColSpec, 1, 8)) = 'UNSIGNED' then begin Props[2] := BoolToStr(True); Delete(ColSpec, 1, 9); end else Props[2] := BoolToStr(False); // Collation rxCol.Expression := '^(CHARACTER SET \w+)?\s+COLLATE (\w+)\b'; if rxCol.Exec(ColSpec) then begin Props[6] := rxCol.Match[2]; Delete(ColSpec, 1, rxCol.MatchLen[0]+1); end; // Allow NULL if UpperCase(Copy(ColSpec, 1, 8)) = 'NOT NULL' then begin Props[3] := BoolToStr(False); Delete(ColSpec, 1, 9); ColDefaultType := cdtText; ColDefaultText := ''; end else begin Props[3] := BoolToStr(True); ColDefaultType := cdtNull; ColDefaultText := 'NULL'; end; // Default value if UpperCase(Copy(ColSpec, 1, 14)) = 'AUTO_INCREMENT' then begin ColDefaultType := cdtAutoInc; ColDefaultText := 'AUTO_INCREMENT'; Delete(ColSpec, 1, 15); end else if UpperCase(Copy(ColSpec, 1, 8)) = 'DEFAULT ' then begin Delete(ColSpec, 1, 8); if UpperCase(Copy(ColSpec, 1, 4)) = 'NULL' then begin ColDefaultType := cdtNull; ColDefaultText := 'NULL'; Delete(ColSpec, 1, 5); end else if UpperCase(Copy(ColSpec, 1, 17)) = 'CURRENT_TIMESTAMP' then begin ColDefaultType := cdtCurTS; ColDefaultText := 'CURRENT_TIMESTAMP'; Delete(ColSpec, 1, 18); end else if ColSpec[1] = '''' then begin InLiteral := True; for i:=2 to Length(ColSpec) do begin if ColSpec[i] = '''' then InLiteral := not InLiteral else if not InLiteral then break; end; ColDefaultType := cdtText; ColDefaultText := Copy(ColSpec, 2, i-3); // A single quote gets escaped by single quote - remove the escape char - escaping is done in Save action afterwards ColDefaultText := WideStringReplace(ColDefaultText, '''''', '''', [rfReplaceAll]); Delete(ColSpec, 1, i); end; end; if Copy(ColSpec, 1, 27) = 'ON UPDATE CURRENT_TIMESTAMP' then begin // Adjust default type case ColDefaultType of cdtText: ColDefaultType := cdtTextUpdateTS; cdtNull: ColDefaultType := cdtNullUpdateTS; cdtCurTS: ColDefaultType := cdtCurTSUpdateTS; end; Delete(ColSpec, 1, 28); end; Props[4] := IntToStr(Integer(ColDefaultType)) + ColDefaultText; // Comment if UpperCase(Copy(ColSpec, 1, 9)) = 'COMMENT ''' then begin InLiteral := True; for i:=10 to Length(ColSpec) do begin if ColSpec[i] = '''' then InLiteral := not InLiteral else if not InLiteral then break; end; Props[5] := Copy(ColSpec, 10, i-11); Props[5] := WideStringReplace(Props[5], '''''', '''', [rfReplaceAll]); Delete(ColSpec, 1, i); end; Columns.AddObject(rx.Match[1], Props); if not rx.ExecNext then break; end; FreeAndNil(rx); FreeAndNil(rxCol); listColumns.EndUpdate; ds := Mainform.GetResults('SHOW KEYS FROM '+Mainform.mask(FAlterTableName)); LastKeyName := ''; Props := nil; while not ds.Eof do begin if LastKeyName <> ds.FieldByName('Key_name').AsWideString then begin Props := TWideStringlist.Create; Props.OnChange := IndexesChange; IndexType := ds.FieldByName('Key_name').AsString; if (ds.FindField('Index_type') <> nil) and ( (ds.FieldByName('Index_type').AsString = FKEY) or (ds.FieldByName('Index_type').AsString = SKEY)) then IndexType := ds.FieldByName('Index_type').AsString else if (IndexType <> PKEY) and (ds.FieldByName('Non_unique').AsInteger = 0) then IndexType := UKEY else if ds.FieldByName('Non_unique').AsInteger = 1 then IndexType := KEY; Indexes.AddObject(ds.FieldByName('Key_name').AsWideString+REGDELIM+IndexType, Props); end; Props.Add(ds.FieldByName('Column_name').AsWideString); if ds.FieldByName('Sub_part').AsString <> '' then Props[Props.Count-1] := Props[Props.Count-1] + '('+ds.FieldByName('Sub_part').AsString+')'; LastKeyName := ds.FieldByName('Key_name').AsWideString; ds.Next; end; FreeAndNil(ds); end; // Validate controls ColumnsChange(Self); comboEngineSelect(comboEngine); ValidateColumnControls; ValidateIndexControls; ResetModificationFlags; // Indicate change mechanisms can call their events now. See Modification(). FLoaded := True; // Empty status panel Mainform.showstatus; Screen.Cursor := crDefault; end; procedure TfrmTableEditor.btnDiscardClick(Sender: TObject); begin // Reinit GUI, discarding changes Init(FAlterTableName); end; procedure TfrmTableEditor.btnSaveClick(Sender: TObject); var sql: WideString; i: Integer; Props: TWideStringlist; begin // Create or alter table if FAlterTableName = '' then sql := ComposeCreateStatement else sql := ComposeAlterStatement; Mainform.ExecUpdateQuery(sql); // Set table name for altering if Apply was clicked FAlterTableName := editName.Text; tabALTERcode.TabVisible := FAlterTableName <> ''; Mainform.SetEditorTabCaption(Self, FAlterTableName); Mainform.tabData.TabVisible := True; if chkCharsetConvert.Checked then begin // Autoadjust column collations for i:=0 to Columns.Count-1 do begin Props := TWideStringlist(Columns.Objects[i]); if Props[6] <> '' then begin Props.OnChange := nil; Props[6] := comboCollation.Text; Props.OnChange := ColumnsChange; end; end; end; ResetModificationFlags; Mainform.ResetSelectedTableStuff; end; procedure TfrmTableEditor.ResetModificationFlags; var i: Integer; Parts: TWideStringlist; 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 := NotModifiedFlag; // Reset name value pairs for column changes ColumnNames.Clear; ColumnsChanges.Clear; for i:=0 to Columns.Count-1 do ColumnNames.Add(Columns[i] + ColumnNames.NameValueSeparator + Columns[i]); // Copy index list so we have one to work with and one which can later be used to detect changes OldIndexes.Clear; for i:=0 to Indexes.Count-1 do begin Parts := TWideStringlist.Create; Parts.Assign(TWideStringlist(Indexes.Objects[i])); OldIndexes.AddObject(Indexes[i], Parts); end; Modified := False; end; function TfrmTableEditor.ComposeAlterStatement: WideString; var Specs, Props, IndexesComposed, OldIndexesComposed: TWideStringlist; ColSpec, IndexSQL, DefaultText: WideString; i, j: Integer; AddIt, DropIt: Boolean; dt: TDatatype; DefaultType: TColumnDefaultType; ds: TDataset; begin // Compose ALTER query, called by buttons and for SQL code tab Mainform.showstatus('Composing ALTER statement ...'); Screen.Cursor := crHourglass; Specs := TWideStringlist.Create; if editName.Text <> FAlterTableName then Specs.Add('RENAME TO ' + Mainform.mask(editName.Text)); if memoComment.Tag = ModifiedFlag then Specs.Add('COMMENT=' + esc(memoComment.Text)); if (comboCollation.Tag = ModifiedFlag) or (chkCharsetConvert.Checked) then Specs.Add('COLLATE=' + comboCollation.Text); if comboEngine.Tag = ModifiedFlag then Specs.Add('ENGINE=' + comboEngine.Text); 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 ds := Mainform.GetCollations; while not ds.Eof do begin if ds.FieldByName('Collation').AsWideString = comboCollation.Text then begin Specs.Add('CONVERT TO CHARSET '+ds.FieldByName('Charset').AsWideString); break; end; ds.Next; end; end; // Update columns EnableProgressBar(Columns.Count + OldIndexes.Count + Indexes.Count); for i:=0 to Columns.Count - 1 do begin Mainform.ProgressBarStatus.StepIt; AddIt := True; ColSpec := ' ' + Mainform.mask(Columns[i]); Props := TWideStringlist(Columns.Objects[i]); ColSpec := ColSpec + ' ' +Props[0]; if Props[1] <> '' then ColSpec := ColSpec + '(' + Props[1] + ')'; dt := GetDatatypeByName(Props[0]); if dt.HasUnsigned and StrToBool(Props[2]) then ColSpec := ColSpec + ' UNSIGNED'; if not StrToBool(Props[3]) then ColSpec := ColSpec + ' NOT'; ColSpec := ColSpec + ' NULL'; if Props[4] <> '' then begin DefaultText := Props[4]; DefaultType := GetColumnDefaultType(DefaultText); ColSpec := ColSpec + ' ' + GetColumnDefaultClause(DefaultType, DefaultText); end; if Props[5] <> '' then ColSpec := ColSpec + ' COMMENT '+esc(Props[5]); if Props[6] <> '' then begin ColSpec := ColSpec + ' COLLATE '; if chkCharsetConvert.Checked then ColSpec := ColSpec + comboCollation.Text else ColSpec := ColSpec + Props[6]; end; // Server version requirement, see http://dev.mysql.com/doc/refman/4.1/en/alter-table.html if Mainform.mysql_version >= 40001 then begin if i = 0 then ColSpec := ColSpec + ' FIRST' else ColSpec := ColSpec + ' AFTER '+Mainform.mask(Columns[i-1]); end; for j:=0 to ColumnNames.Count - 1 do begin if ColumnNames.ValueFromIndex[j] = Columns[i] then begin AddIt := False; // Only touch column if it was changed if ColumnsChanges.IndexOf(Columns[i]) > -1 then Specs.Add('CHANGE COLUMN '+Mainform.mask(ColumnNames.Names[j]) + ColSpec); break; end; end; if AddIt then Specs.Add('ADD COLUMN '+ColSpec); end; // Delete columns for i:=0 to ColumnNames.Count-1 do begin if ColumnNames.ValueFromIndex[i] = '' then Specs.Add('DROP COLUMN '+Mainform.mask(ColumnNames.Names[i])); end; // Prepare ADD INDEX ... clauses once so we don't call GetIndexSQL() too often IndexesComposed := TWideStringlist.Create; for i:=0 to Indexes.Count-1 do IndexesComposed.Add(GetIndexSQL(Indexes, i)); OldIndexesComposed := TWideStringlist.Create; for i:=0 to OldIndexes.Count-1 do OldIndexesComposed.Add(GetIndexSQL(OldIndexes, i)); // Drop indexes for i:=0 to OldIndexesComposed.Count-1 do begin Mainform.ProgressBarStatus.StepIt; DropIt := IndexesComposed.IndexOf(OldIndexesComposed[i]) = -1; if not DropIt then Continue; IndexSQL := Copy(OldIndexes[i], LastPos(REGDELIM, OldIndexes[i])+1, Length(OldIndexes[i])); if IndexSQL = PKEY then IndexSQL := 'PRIMARY KEY' else IndexSQL := 'INDEX ' + Mainform.Mask(Copy(OldIndexes[i], 1, LastPos(REGDELIM, OldIndexes[i])-1)); Specs.Add('DROP '+IndexSQL); end; // Add changed or added indexes for i:=0 to IndexesComposed.Count-1 do begin Mainform.ProgressBarStatus.StepIt; if (OldIndexesComposed.IndexOf(IndexesComposed[i]) = -1) and (IndexesComposed[i] <> '') then Specs.Add('ADD '+IndexesComposed[i]); end; Result := 'ALTER TABLE '+Mainform.mask(FAlterTableName) + CRLF + #9 + ImplodeStr(',' + CRLF + #9, Specs); FreeAndNil(Specs); FreeAndNil(IndexesComposed); FreeAndNil(OldIndexesComposed); Mainform.showstatus; Mainform.ProgressBarStatus.Hide; Screen.Cursor := crDefault; end; function TfrmTableEditor.ComposeCreateStatement: WideString; var i, IndexCount: Integer; ColProps: TWideStringlist; dt: TDatatype; DefaultType: TColumnDefaultType; DefaultText, tmp: WideString; begin // Compose CREATE query, called by buttons and for SQL code tab Result := 'CREATE TABLE '+Mainform.mask(editName.Text)+' ('+CRLF; for i:=0 to Columns.Count - 1 do begin ColProps := TWideStringlist(Columns.Objects[i]); Result := Result + #9 + Mainform.mask(Columns[i]) + ' ' +ColProps[0]; if ColProps[1] <> '' then Result := Result + '(' + ColProps[1] + ')'; dt := GetDatatypeByName(ColProps[0]); if dt.HasUnsigned and StrToBool(ColProps[2]) then Result := Result + ' UNSIGNED'; if not StrToBool(ColProps[3]) then Result := Result + ' NOT'; Result := Result + ' NULL'; if ColProps[4] <> '' then begin DefaultText := ColProps[4]; DefaultType := GetColumnDefaultType(DefaultText); Result := Result + ' ' + GetColumnDefaultClause(DefaultType, DefaultText); end; if ColProps[5] <> '' then Result := Result + ' COMMENT '+esc(ColProps[5]); if ColProps[6] <> '' then Result := Result + ' COLLATE '+ColProps[6]; Result := Result + ','+CRLF; end; IndexCount := 0; for i:=0 to Indexes.Count - 1 do begin tmp := GetIndexSQL(Indexes, i); if tmp <> '' then begin Result := Result + #9 + GetIndexSQL(Indexes, i) + ','+CRLF; Inc(IndexCount); end; end; if Columns.Count + IndexCount > 0 then Delete(Result, Length(Result)-2, 3); Result := Result + CRLF + ')' + CRLF; if memoComment.Text <> '' then Result := Result + 'COMMENT='+esc(memoComment.Text) + CRLF; if comboCollation.Text <> '' then Result := Result + 'COLLATE='+comboCollation.Text + CRLF; if comboEngine.Text <> '' then Result := Result + 'ENGINE='+comboEngine.Text + CRLF; if comboRowFormat.Text <> '' then Result := Result + 'ROW_FORMAT='+comboRowFormat.Text + CRLF; if chkChecksum.Checked then Result := Result + 'CHECKSUM='+IntToStr(Integer(chkChecksum.Checked)) + CRLF; if editAutoInc.Text <> '' then Result := Result + 'AUTO_INCREMENT='+editAutoInc.Text + CRLF; if editAvgRowLen.Text <> '' then Result := Result + 'AVG_ROW_LENGTH='+editAvgRowLen.Text + CRLF; if editMaxRows.Text <> '' then Result := Result + 'MAX_ROWS='+editMaxRows.Text + CRLF; if memoUnionTables.Enabled and (memoUnionTables.Text <> '') then Result := Result + 'UNION=('+memoUnionTables.Text+')' + CRLF; if comboInsertMethod.Enabled and (comboInsertMethod.Text <> '') then Result := Result + 'INSERT_METHOD='+comboInsertMethod.Text + CRLF; end; function TfrmTableEditor.GetIndexSQL(IndexList: TWideStringlist; idx: Integer): WideString; var IndexName, IndexType, ColName: WideString; Parts: TWideStringlist; i, p: Integer; begin Result := ''; Parts := TWideStringlist(IndexList.Objects[idx]); // Supress SQL error trying index creation with 0 column if Parts.Count = 0 then Exit; IndexName := Copy(IndexList[idx], 1, LastPos(REGDELIM, IndexList[idx])-1); IndexType := Copy(IndexList[idx], LastPos(REGDELIM, IndexList[idx])+1, Length(IndexList[idx])); if IndexType = PKEY then Result := Result + 'PRIMARY KEY ' else begin if IndexType <> KEY then Result := Result + IndexType + ' '; Result := Result + 'INDEX ' + Mainform.Mask(IndexName) + ' '; end; Result := Result + '('; for i:=0 to Parts.Count - 1 do begin ColName := Parts[i]; p := LastPos('(', ColName); if p > 1 then Result := Result + Mainform.Mask(Copy(ColName, 1, p-1)) + Copy(ColName, p, Length(ColName)-p+1) else Result := Result + Mainform.Mask(ColName); Result := Result + ', '; end; if Parts.Count > 0 then Delete(Result, Length(Result)-1, 2); Result := Result + ')'; end; procedure TfrmTableEditor.editNameChange(Sender: TObject); begin // Name edited editName.Font.Color := clWindowText; editName.Color := clWindow; try ensureValidIdentifier( editName.Text ); except // Invalid name if editName.Text <> '' then begin editName.Font.Color := clRed; editName.Color := clYellow; end; end; Modification(Sender); 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; end; end; procedure TfrmTableEditor.btnAddColumnClick(Sender: TObject); var DefProperties, Properties: TWideStringList; fn: PVirtualNode; idx: Integer; begin // Add new column after selected one fn := listColumns.FocusedNode; Properties := TWideStringList.Create; Properties.OnChange := ColumnsChange; if Assigned(fn) then begin idx := fn.Index+1; // Copy properties from focused node DefProperties := TWideStringlist(Columns.Objects[fn.Index]); Properties.Assign(DefProperties); end else begin idx := Columns.Count; Properties.CommaText := 'INT,10,'+BoolToStr(False)+','+BoolToStr(True)+','+IntToStr(Integer(cdtNull))+'NULL,,'; end; Columns.InsertObject(idx, 'Column '+IntToStr(idx+1), Properties); SelectNode(listColumns, idx); listColumns.EditNode(listColumns.FocusedNode, 1); end; procedure TfrmTableEditor.btnRemoveColumnClick(Sender: TObject); var i: Integer; n: PVirtualNode; begin // Remove selected column n := listColumns.FocusedNode; for i:=0 to ColumnNames.Count-1 do begin if ColumnNames.ValueFromIndex[i] = Columns[n.Index] then begin ColumnNames[i] := ColumnNames.Names[i] + ColumnNames.NameValueSeparator; break; end; end; Columns.Delete(n.Index); if (not Assigned(n)) and (Columns.Count > 0) then SelectNode(listColumns, Columns.Count-1); end; procedure TfrmTableEditor.btnClearColumnsClick(Sender: TObject); var i: Integer; begin // Set empty values in changelist for i:=0 to ColumnNames.Count - 1 do ColumnNames[i] := ColumnNames.Names[i] + ColumnNames.NameValueSeparator; // Column data gets freed below - end any editor which could cause AV's if listColumns.IsEditing then listColumns.CancelEditNode; // I suspect Columns.Clear to be silly enough and leave its objects in memory, // so we'll free them explicitely for i := 0 to Columns.Count - 1 do Columns.Objects[i].Free; Columns.Clear; end; procedure TfrmTableEditor.btnMoveUpColumnClick(Sender: TObject); begin // Move column up MoveFocusedColumn(listColumns.FocusedNode.Index-1); end; procedure TfrmTableEditor.btnMoveDownColumnClick(Sender: TObject); begin // Move column down MoveFocusedColumn(listColumns.FocusedNode.Index+1); 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; begin Node := Sender.GetNodeAt(Pt.X, Pt.Y); if Assigned(Node) then MoveFocusedColumn(Node.Index); 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])) then begin TargetCanvas.Brush.Color := clBtnFace; TargetCanvas.FillRect(CellRect); end; end; procedure TfrmTableEditor.listColumnsAfterCellPaint(Sender: TBaseVirtualTree; TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex; CellRect: TRect); var Props: TWideStringlist; ImageIndex, X, Y: Integer; VT: TVirtualStringTree; begin // Paint checkbox image in certain columns // while restricting "Allow NULL" checkbox to numeric datatypes if (Column in [4, 5]) and CellEditingAllowed(Node, Column) then begin Props := TWideStringlist(Columns.Objects[Node.Index]); if StrToBool(Props[Column-2]) then ImageIndex := 128 else ImageIndex := 127; VT := TVirtualStringTree(Sender); X := CellRect.Left + (VT.Header.Columns[Column].Width div 2) - (VT.Images.Width div 2); Y := CellRect.Top + Integer(VT.NodeHeight[Node] div 2) - (VT.Images.Height div 2); VT.Images.Draw(TargetCanvas, X, Y, ImageIndex); end; end; procedure TfrmTableEditor.listColumnsBeforePaint(Sender: TBaseVirtualTree; TargetCanvas: TCanvas); var VT: TVirtualStringTree; OldNodeCount: Cardinal; begin // (Re)paint column list. Adjust number of nodes and autofit both counter and name column. VT := Sender as TVirtualStringTree; OldNodeCount := VT.RootNodeCount; VT.RootNodeCount := Columns.Count; if OldNodeCount <> VT.RootNodeCount then VT.Header.AutoFitColumns(False, smaUseColumnOption, 0, 1); 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 := Assigned(Node); btnClearColumns.Enabled := Columns.Count > 0; btnMoveUpColumn.Enabled := Assigned(Node) and (Node <> listColumns.GetFirst); btnMoveDownColumn.Enabled := Assigned(Node) and (Node <> listColumns.GetLast); menuRemoveColumn.Enabled := btnRemoveColumn.Enabled; menuClearColumns.Enabled := btnClearColumns.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])); end; function TfrmTableEditor.CellEditingAllowed(Node: PVirtualNode; Column: TColumnIndex): Boolean; var Props: TWideStringlist; dt: TDatatype; begin Props := TWideStringlist(Columns.Objects[Node.Index]); dt := GetDatatypeByName(Props[0]); case Column of // No editor for very first column and checkbox columns 0: Result := False; 3: Result := dt.HasLength; 4: Result := dt.HasUnsigned; // No editing of collation allowed if "Convert data" was checked 8: Result := not chkCharsetConvert.Checked; else Result := True; end; end; procedure TfrmTableEditor.listColumnsGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: WideString); var Properties: TWideStringList; begin // Display column text case Column of 0: CellText := IntToStr(Node.Index+1); 1: CellText := Columns[Node.Index]; 4, 5: CellText := ''; // Checkbox else begin Properties := TWideStringList(Columns.Objects[Node.Index]); CellText := Properties[Column-2]; if (Column = 8) and (CellText <> '') and (chkCharsetConvert.Checked) then CellText := comboCollation.Text; end; end; if Column = 6 then CellText := Copy(CellText, 2, Length(CellText)-1); end; procedure TfrmTableEditor.listColumnsPaintText(Sender: TBaseVirtualTree; const TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType); var Default: WideString; TextColor: TColor; dt: TDatatype; Props: TWideStringlist; begin // Give datatype column specific color, as set in preferences if vsSelected in Node.States then Exit; if not (Column in [2, 6]) then Exit; Props := TWideStringlist(Columns.Objects[Node.Index]); dt := GetDatatypeByName(Props[0]); Default := Props[4]; TextColor := TargetCanvas.Font.Color; case Column of 2: case dt.Category of dtcInteger, dtcReal: TextColor := Mainform.prefFieldColorNumeric; dtcTemporal: TextColor := Mainform.prefFieldColorDateTime; dtcText: TextColor := Mainform.prefFieldColorText; dtcBinary: TextColor := Mainform.prefFieldColorBinary; dtcIntegerNamed: TextColor := Mainform.prefFieldColorEnum; dtcSet, dtcSetNamed: TextColor := Mainform.prefFieldColorSet; // TODO: catSpatial else TextColor := TargetCanvas.Font.Color; end; 6: case GetColumnDefaultType(Default) of cdtNull, cdtNullUpdateTS: case dt.Category of dtcInteger, dtcReal: TextColor := Mainform.prefNullColorNumeric; dtcTemporal: TextColor := Mainform.prefNullColorDateTime; dtcText: TextColor := Mainform.prefNullColorText; dtcBinary: TextColor := Mainform.prefNullColorBinary; dtcIntegerNamed: TextColor := Mainform.prefNullColorEnum; dtcSet, dtcSetNamed: TextColor := Mainform.prefNullColorSet; end; cdtCurTS, cdtCurTSUpdateTS: TextColor := Mainform.prefFieldColorDateTime; cdtAutoInc: TextColor := clNavy; end; end; TargetCanvas.Font.Color := TextColor; end; procedure TfrmTableEditor.listColumnsNewText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; NewText: WideString); var Properties: TWideStringList; idx, i: Integer; dt: TDatatype; begin // Column property edited if Column = 1 then begin idx := Columns.IndexOf(NewText); if (idx > -1) and (idx <> Integer(Node.Index)) then begin MessageDlg('Column "'+NewText+'" already exists.', mtError, [mbOk], 0); Sender.EditNode(Node, Column); Exit; end; for i:=0 to ColumnNames.Count -1 do begin if ColumnNames.ValueFromIndex[i] = Columns[Node.Index] then begin ColumnNames.ValueFromIndex[i] := NewText; break; end; end; ColumnsChanges.Add(NewText); Columns[Node.Index] := NewText; end else if Column > 1 then begin ColumnsChanges.Add(Columns[Node.Index]); Properties := TWideStringList(Columns.Objects[Node.Index]); Properties[Column-2] := NewText; if Column = 2 then begin dt := GetDatatypeByName(NewText); // Reset length/set for column types which don't support that if not dt.HasLength then Properties[1] := ''; // Suggest length/set if required if dt.RequiresLength and (Properties[1] = '') then Properties[1] := dt.DefLengthSet; end; end; end; procedure TfrmTableEditor.listColumnsClick(Sender: TObject); var VT: TVirtualStringTree; Props: TWideStringlist; Click: THitInfo; Default: WideString; 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 not Assigned(Click.HitNode) then Exit; if Click.HitColumn in [4, 5] then begin // For checkboxes, cell editors are disabled, instead toggle their state Props := TWideStringList(Columns.Objects[Click.HitNode.Index]); if CellEditingAllowed(Click.HitNode, Click.HitColumn) then begin ColumnsChanges.Add(Columns[Click.HitNode.Index]); Props[Click.HitColumn-2] := BoolToStr(not StrToBool(Props[Click.HitColumn-2])); // Switch default value from NULL to Text if Allow Null is off if (Click.HitColumn = 5) and (not StrToBool(Props[Click.HitColumn-2])) then begin Default := Props[4]; if GetColumnDefaultType(Default) in [cdtNull, cdtNullUpdateTS] then Props[4] := IntToStr(Integer(cdtText)); end; VT.InvalidateNode(Click.HitNode); end; 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 Assigned(Click.HitNode) and (Click.HitColumn > NoColumn) then VT.EditNode(Click.HitNode, Click.HitColumn); end; end; procedure TfrmTableEditor.listColumnsCreateEditor(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; out EditLink: IVTEditLink); var EnumEditor: TEnumEditorLink; DefaultEditor: TColumnDefaultEditorLink; i: Integer; Props: TWideStringlist; begin // Start cell editor case Column of 2: begin // Datatype pulldown EnumEditor := TEnumEditorLink.Create; EnumEditor.ValueList := TWideStringList.Create; for i:=Low(Datatypes) to High(Datatypes) do EnumEditor.ValueList.Add(Datatypes[i].Name); EditLink := EnumEditor; end; 8: begin // Collation pulldown EnumEditor := TEnumEditorLink.Create; EnumEditor.ValueList := TWideStringList.Create; Mainform.GetCollations(EnumEditor.ValueList); EditLink := EnumEditor; end; 6: begin DefaultEditor := TColumnDefaultEditorLink.Create; Props := TWideStringlist(Columns.Objects[Node.Index]); DefaultEditor.DefaultText := Props[Column-2]; DefaultEditor.DefaultType := GetColumnDefaultType(DefaultEditor.DefaultText); EditLink := DefaultEditor; end else EditLink := TStringEditLink.Create end; end; procedure TfrmTableEditor.SetModified(Value: Boolean); begin // Some value has changed FModified := Value; btnSave.Enabled := FModified; btnDiscard.Enabled := FModified; CreateCodeValid := False; AlterCodeValid := False; UpdateSQLcode; end; procedure TfrmTableEditor.ColumnsChange(Sender: TObject); begin Modification(Sender); listColumns.Repaint; end; procedure TfrmTableEditor.SelectNode(VT: TVirtualStringTree; idx: Cardinal; ParentNode: PVirtualNode=nil); var Node: PVirtualNode; begin // Helper to focus and highlight a node by its index if Assigned(ParentNode) then Node := VT.GetFirstChild(ParentNode) else Node := VT.GetFirst; while Assigned(Node) do begin if Node.Index = idx then begin VT.FocusedNode := Node; VT.Selected[Node] := True; break; end; Node := VT.GetNextSibling(Node); end; end; procedure TfrmTableEditor.MoveFocusedColumn(NewIdx: Cardinal); begin if listColumns.IsEditing then listColumns.EndEditNode; listColumns.FocusedColumn := listColumns.Header.MainColumn; ColumnsChanges.Add(Columns[listColumns.FocusedNode.Index]); Columns.Move(listColumns.FocusedNode.Index, NewIdx); SelectNode(listColumns, NewIdx); 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 := IntToStr(Makeint(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.IndexesChange(Sender: TObject); begin Modification(Sender); treeIndexes.Invalidate; end; procedure TfrmTableEditor.btnAddIndexClick(Sender: TObject); var IndexParts: TWideStringlist; Name: WideString; begin // Add new index IndexParts := TWideStringlist.Create; IndexParts.OnChange := IndexesChange; Name := 'Index '+IntToStr(Indexes.Count+1); Indexes.AddObject(Name+REGDELIM+KEY, IndexParts); SelectNode(treeIndexes, Indexes.Count-1); end; procedure TfrmTableEditor.menuAddIndexColumnClick(Sender: TObject); var IndexParts: TWideStringlist; Node: PVirtualNode; i, j, p: Integer; Col, NewCol: WideString; ColExists: Boolean; begin // Add column to index Node := treeIndexes.FocusedNode; if not Assigned(Node) then Exit; if treeIndexes.GetNodeLevel(Node) = 1 then Node := Node.Parent; IndexParts := TWideStringlist(Indexes.Objects[Node.Index]); ColExists := False; NewCol := ''; for i:=0 to Columns.Count - 1 do begin for j:=0 to IndexParts.Count - 1 do begin Col := IndexParts[j]; p := LastPos('(', Col); if p > 0 then Col := Copy(Col, 0, p-1); ColExists := Col = Columns[i]; if ColExists then break; end; if not ColExists then begin NewCol := Columns[i]; break; end; end; treeIndexes.AddChild(Node); IndexParts.Add(NewCol); SelectNode(treeIndexes, IndexParts.Count-1, Node); end; procedure TfrmTableEditor.btnRemoveIndexClick(Sender: TObject); var IndexParts: TWideStringlist; idx: Integer; NewSelectNode: PVirtualNode; begin // Remove index or part case treeIndexes.GetNodeLevel(treeIndexes.FocusedNode) of 0: begin idx := treeIndexes.FocusedNode.Index; Indexes.Objects[idx].Free; Indexes.Delete(idx); end; 1: begin idx := treeIndexes.FocusedNode.Parent.Index; IndexParts := TWideStringlist(Indexes.Objects[idx]); IndexParts.Delete(treeIndexes.FocusedNode.Index); treeIndexes.DeleteNode(treeIndexes.FocusedNode); end; end; NewSelectNode := treeIndexes.GetPreviousVisible(treeIndexes.FocusedNode); if Assigned(NewSelectNode) then SelectNode(treeIndexes, NewSelectNode.Index, NewSelectNode.Parent); end; procedure TfrmTableEditor.btnClearIndexesClick(Sender: TObject); var i: Integer; begin // Clear all indexes // Column data gets freed below - end any editor which could cause AV's if treeIndexes.IsEditing then treeIndexes.CancelEditNode; for i := 0 to Indexes.Count - 1 do Indexes.Objects[i].Free; Indexes.Clear; treeIndexes.Clear; end; procedure TfrmTableEditor.treeIndexesGetImageIndex(Sender: TBaseVirtualTree; Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex; var Ghosted: Boolean; var ImageIndex: Integer); var IndexType: String; VT: TVirtualStringTree; begin // Icon image showing type of index VT := Sender as TVirtualStringTree; if Column <> 0 then Exit; case VT.GetNodeLevel(Node) of 0: begin IndexType := VT.Text[Node, 1]; if IndexType = PKEY then ImageIndex := ICONINDEX_PRIMARYKEY else if IndexType = KEY then ImageIndex := ICONINDEX_INDEXKEY else if IndexType = UKEY then ImageIndex := ICONINDEX_UNIQUEKEY else if IndexType = FKEY then ImageIndex := ICONINDEX_FULLTEXTKEY else if IndexType = SKEY then ImageIndex := ICONINDEX_SPATIALKEY; end; 1: ImageIndex := 42; end; end; procedure TfrmTableEditor.treeIndexesGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: WideString); var IndexProps: TWideStringlist; IndexType: WideString; begin // Index tree showing cell text case Sender.GetNodeLevel(Node) of 0: begin IndexType := Copy(Indexes[Node.Index], LastPos(REGDELIM, Indexes[Node.Index])+1, Length(Indexes[Node.Index])); case Column of 0: if IndexType = PKEY then CellText := IndexType + ' KEY' // Fixed name "PRIMARY KEY", cannot be changed else CellText := Copy(Indexes[Node.Index], 1, LastPos(REGDELIM, Indexes[Node.Index])-1); 1: CellText := IndexType; end; end; 1: case Column of 0: begin IndexProps := TWideStringlist(Indexes.Objects[Node.Parent.Index]); CellText := IndexProps[Node.Index]; end; else CellText := ''; end; end; end; procedure TfrmTableEditor.treeIndexesInitChildren(Sender: TBaseVirtualTree; Node: PVirtualNode; var ChildCount: Cardinal); var IndexParts: TWideStringlist; begin // Tell number of columns contained in index IndexParts := TWideStringlist(Indexes.Objects[Node.Index]); ChildCount := IndexParts.Count; 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) 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 := Indexes.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; begin // Disallow renaming primary key VT := Sender as TVirtualStringtree; if (VT.GetNodeLevel(Node) = 0) and (Column = VT.Header.MainColumn) and (VT.Text[Node, Column+1] = PKEY) then Allowed := False else if (VT.GetNodeLevel(Node) = 1) and (Column = 1) then Allowed := False else Allowed := True; end; procedure TfrmTableEditor.treeIndexesCreateEditor(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; out EditLink: IVTEditLink); var EnumEditor: TEnumEditorLink; Level: Cardinal; begin // Start cell editor Level := (Sender as TVirtualStringtree).GetNodeLevel(Node); if (Level = 0) and (Column = 1) then begin // Index type pulldown EnumEditor := TEnumEditorLink.Create; EnumEditor.ValueList := TWideStringList.Create; EnumEditor.ValueList.CommaText := PKEY +','+ KEY +','+ UKEY +','+ FKEY +','+ SKEY; EditLink := EnumEditor; end else if (Level = 1) and (Column = 0) then begin // Column names pulldown EnumEditor := TEnumEditorLink.Create; EnumEditor.ValueList := Columns; EnumEditor.AllowCustomText := True; // Allows adding a subpart in index parts: "TextCol(20)" EditLink := EnumEditor; end else EditLink := TStringEditLink.Create end; procedure TfrmTableEditor.treeIndexesNewText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; NewText: WideString); var VT: TVirtualStringtree; IndexParts: TWideStringlist; begin // Rename index of column VT := Sender as TVirtualStringtree; case VT.GetNodeLevel(Node) of 0: case Column of 0: Indexes[Node.Index] := NewText + REGDELIM + VT.Text[Node, Column+1]; 1: Indexes[Node.Index] := VT.Text[Node, Column-1] + REGDELIM + NewText; end; 1: begin IndexParts := TWideStringlist(Indexes.Objects[Node.Parent.Index]); IndexParts[Node.Index] := NewText; end; end; VT.RepaintNode(Node); end; procedure TfrmTableEditor.treeIndexesBeforePaint(Sender: TBaseVirtualTree; TargetCanvas: TCanvas); begin // (Re)paint index list (Sender as TVirtualStringTree).RootNodeCount := Indexes.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; IndexParts: TWideStringlist; ColName: WideString; ColPos: Integer; VT: TVirtualStringtree; begin // Column node dropped here VT := Sender 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 := MaxInt; if Source = Sender then MoveFocusedIndexPart(ColPos) else begin IndexParts := TWideStringlist(Indexes.Objects[Node.Index]); ColName := Columns[(Source as TVirtualStringTree).FocusedNode.Index]; if IndexParts.IndexOf(ColName) > -1 then begin if MessageDlg('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.'+CRLF+CRLF+'Add anyway?', mtConfirmation, [mbYes, mbNo], 0) = mrNo then Exit; end; if ColPos >= IndexParts.Count then IndexParts.Add(ColName) else IndexParts.Insert(ColPos, ColName); Node.States := Node.States + [vsHasChildren, vsExpanded]; end; // 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 IndexParts: TWideStringlist; begin // Move focused index or index part if treeIndexes.IsEditing then treeIndexes.EndEditNode; IndexParts := TWideStringlist(Indexes.Objects[treeIndexes.FocusedNode.Parent.Index]); IndexParts.Move(treeIndexes.FocusedNode.Index, NewIdx); SelectNode(treeIndexes, NewIdx, treeIndexes.FocusedNode.Parent); end; procedure TfrmTableEditor.PageControlMainChange(Sender: TObject); begin UpdateSQLcode; end; procedure TfrmTableEditor.UpdateSQLcode; var OldTopLine: Integer; begin if (PageControlMain.ActivePage = tabALTERCode) and (not AlterCodeValid) then begin SynMemoALTERcode.BeginUpdate; OldTopLine := SynMemoALTERcode.TopLine; SynMemoALTERcode.Text := ComposeAlterStatement; SynMemoALTERcode.TopLine := OldTopLine; SynMemoALTERcode.EndUpdate; AlterCodeValid := True; end else if (PageControlMain.ActivePage = tabCREATECode) and (not CreateCodeValid) then begin SynMemoCREATEcode.BeginUpdate; OldTopLine := SynMemoCREATEcode.TopLine; SynMemoCREATEcode.Text := ComposeCreateStatement; SynMemoCREATEcode.TopLine := OldTopLine; SynMemoCREATEcode.EndUpdate; CreateCodeValid := True; end; end; procedure TfrmTableEditor.chkCharsetConvertClick(Sender: TObject); begin chkCharsetConvert.Enabled := (FAlterTablename <> '') and (comboCollation.ItemIndex > -1); listColumns.Repaint; Modification(Sender); end; procedure TfrmTableEditor.btnHelpClick(Sender: TObject); begin // Help button Mainform.CallSQLHelpWithKeyword('CREATE TABLE'); end; end.