mirror of
https://github.com/HeidiSQL/HeidiSQL.git
synced 2025-08-06 18:24:26 +08:00
Implement stored routine editor, see issue #420
Todo: make routines visible in the database tab so they can be edited.
This commit is contained in:
477
source/routine_editor.pas
Normal file
477
source/routine_editor.pas
Normal file
@ -0,0 +1,477 @@
|
||||
unit routine_editor;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
|
||||
Dialogs, SynEdit, SynMemo, StdCtrls, TntStdCtrls, ComCtrls, ToolWin,
|
||||
VirtualTrees, WideStrings, db, SynRegExpr;
|
||||
|
||||
type
|
||||
TfrmRoutineEditor = class(TForm)
|
||||
btnApply: TButton;
|
||||
btnCancel: TButton;
|
||||
btnOK: TButton;
|
||||
btnHelp: TButton;
|
||||
lblName: TLabel;
|
||||
lblType: TLabel;
|
||||
lblReturns: TLabel;
|
||||
comboReturns: TComboBox;
|
||||
comboType: TTNTComboBox;
|
||||
editName: TTntEdit;
|
||||
lblParameters: TLabel;
|
||||
tlbParameters: TToolBar;
|
||||
btnAddParam: TToolButton;
|
||||
btnRemoveParam: TToolButton;
|
||||
btnClearParams: TToolButton;
|
||||
listParameters: TVirtualStringTree;
|
||||
lblSQL: TLabel;
|
||||
comboDataAccess: TComboBox;
|
||||
lblSecurity: TLabel;
|
||||
comboSecurity: TComboBox;
|
||||
lblComment: TLabel;
|
||||
editComment: TTntEdit;
|
||||
chkDeterministic: TCheckBox;
|
||||
lblSQLcode: TLabel;
|
||||
SynMemoBody: TSynMemo;
|
||||
procedure FormCreate(Sender: TObject);
|
||||
procedure FormDestroy(Sender: TObject);
|
||||
procedure FormShow(Sender: TObject);
|
||||
procedure FormClose(Sender: TObject; var Action: TCloseAction);
|
||||
procedure comboTypeSelect(Sender: TObject);
|
||||
procedure PostChanges(Sender: TObject);
|
||||
procedure btnHelpClick(Sender: TObject);
|
||||
procedure editNameChange(Sender: TObject);
|
||||
procedure btnAddParamClick(Sender: TObject);
|
||||
procedure listParametersGetText(Sender: TBaseVirtualTree;
|
||||
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
|
||||
var CellText: WideString);
|
||||
procedure listParametersGetImageIndex(Sender: TBaseVirtualTree;
|
||||
Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex;
|
||||
var Ghosted: Boolean; var ImageIndex: Integer);
|
||||
procedure btnClearParamsClick(Sender: TObject);
|
||||
procedure btnRemoveParamClick(Sender: TObject);
|
||||
procedure listParametersBeforePaint(Sender: TBaseVirtualTree;
|
||||
TargetCanvas: TCanvas);
|
||||
procedure listParametersFocusChanged(Sender: TBaseVirtualTree;
|
||||
Node: PVirtualNode; Column: TColumnIndex);
|
||||
procedure listParametersCreateEditor(Sender: TBaseVirtualTree;
|
||||
Node: PVirtualNode; Column: TColumnIndex; out EditLink: IVTEditLink);
|
||||
procedure listParametersNewText(Sender: TBaseVirtualTree;
|
||||
Node: PVirtualNode; Column: TColumnIndex; NewText: WideString);
|
||||
procedure listParametersEditing(Sender: TBaseVirtualTree;
|
||||
Node: PVirtualNode; Column: TColumnIndex; var Allowed: Boolean);
|
||||
procedure Modification(Sender: TObject);
|
||||
procedure SynMemoBodyDragOver(Sender, Source: TObject; X, Y: Integer;
|
||||
State: TDragState; var Accept: Boolean);
|
||||
procedure SynMemoBodyDragDrop(Sender, Source: TObject; X, Y: Integer);
|
||||
procedure listParametersPaintText(Sender: TBaseVirtualTree;
|
||||
const TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
|
||||
TextType: TVSTTextType);
|
||||
private
|
||||
{ Private declarations }
|
||||
Parameters: TWideStringList;
|
||||
AlterRoutineType: String;
|
||||
FModified: Boolean;
|
||||
procedure SetModified(Value: Boolean);
|
||||
property Modified: Boolean read FModified write SetModified;
|
||||
public
|
||||
{ Public declarations }
|
||||
AlterRoutineName: WideString;
|
||||
end;
|
||||
|
||||
|
||||
implementation
|
||||
|
||||
uses main, helpers, mysql_structures, grideditlinks;
|
||||
|
||||
{$R *.dfm}
|
||||
|
||||
const
|
||||
DELIM = '|';
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.FormCreate(Sender: TObject);
|
||||
var
|
||||
i: Integer;
|
||||
begin
|
||||
// Restore form dimensions
|
||||
Width := GetRegValue(REGNAME_PROCEDITOR_WIDTH, Width);
|
||||
Height := GetRegValue(REGNAME_PROCEDITOR_HEIGHT, Height);
|
||||
// Combo items in a .dfm are sporadically lost after an IDE restart,
|
||||
// so we set them here to avoid developer annoyance
|
||||
comboType.Items.Add('Procedure (doesn''t return a result)');
|
||||
comboType.Items.Add('Function (returns a result)');
|
||||
comboDataAccess.Items.Add('Contains SQL');
|
||||
comboDataAccess.Items.Add('No SQL');
|
||||
comboDataAccess.Items.Add('Reads SQL data');
|
||||
comboDataAccess.Items.Add('Modifies SQL data');
|
||||
comboSecurity.Items.Add('Definer');
|
||||
comboSecurity.Items.Add('Invoker');
|
||||
for i := Low(MySqlDataTypeArray) to High(MySqlDataTypeArray) do
|
||||
comboReturns.Items.Add(MySqlDataTypeArray[i].Name);
|
||||
SetWindowSizeGrip(Handle, True);
|
||||
InheritFont(Font);
|
||||
SynMemoBody.Font.Name := Mainform.SynMemoQuery.Font.Name;
|
||||
SynMemoBody.Font.Size := Mainform.SynMemoQuery.Font.Size;
|
||||
SynMemoBody.Options := Mainform.SynMemoQuery.Options;
|
||||
Parameters := TWideStringList.Create;
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.FormDestroy(Sender: TObject);
|
||||
begin
|
||||
// Store form dimensions
|
||||
OpenRegistry;
|
||||
MainReg.WriteInteger(REGNAME_PROCEDITOR_WIDTH, Width);
|
||||
MainReg.WriteInteger(REGNAME_PROCEDITOR_HEIGHT, Height);
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.FormShow(Sender: TObject);
|
||||
var
|
||||
ds: TDataSet;
|
||||
Create: WideString;
|
||||
Params: TWideStringlist;
|
||||
Context: String;
|
||||
rx: TRegExpr;
|
||||
i: Integer;
|
||||
begin
|
||||
editName.Text := AlterRoutineName;
|
||||
comboType.ItemIndex := 0;
|
||||
comboReturns.Text := '';
|
||||
listParameters.Clear;
|
||||
comboDataAccess.ItemIndex := 0;
|
||||
comboSecurity.ItemIndex := 0;
|
||||
editComment.Clear;
|
||||
SynMemoBody.Text := 'BEGIN'+CRLF+CRLF+'END';
|
||||
if editName.Text <> '' then begin
|
||||
// Editing existing routine
|
||||
ds := Mainform.GetResults('SELECT * FROM '+DBNAME_INFORMATION_SCHEMA+'.ROUTINES'+
|
||||
' WHERE ROUTINE_SCHEMA='+esc(Mainform.ActiveDatabase)+' AND ROUTINE_NAME='+esc(AlterRoutineName));
|
||||
if ds.RecordCount <> 1 then begin
|
||||
MessageDlg('Cannot find properties of stored routine '+AlterRoutineName, mtError, [mbOK], 0);
|
||||
Close;
|
||||
end;
|
||||
ds.First;
|
||||
AlterRoutineType := ds.FieldByName('ROUTINE_TYPE').AsString;
|
||||
comboType.ItemIndex := ListIndexByRegExpr(comboType.Items, '^'+AlterRoutineType+'\b');
|
||||
chkDeterministic.Checked := ds.FieldByName('IS_DETERMINISTIC').AsString = 'YES';
|
||||
comboReturns.Text := ds.FieldByName('DTD_IDENTIFIER').AsWideString;
|
||||
comboDataAccess.ItemIndex := comboDataAccess.Items.IndexOf(ds.FieldByName('SQL_DATA_ACCESS').AsString);
|
||||
comboSecurity.ItemIndex := comboSecurity.Items.IndexOf(ds.FieldByName('SECURITY_TYPE').AsString);
|
||||
editComment.Text := ds.FieldByName('ROUTINE_COMMENT').AsWideString;
|
||||
SynMemoBody.Text := ds.FieldByName('ROUTINE_DEFINITION').AsWideString;
|
||||
Create := Mainform.GetVar('SHOW CREATE '+AlterRoutineType+' '+Mainform.mask(editName.Text), 2);
|
||||
rx := TRegExpr.Create;
|
||||
rx.ModifierI := True;
|
||||
rx.ModifierG := False;
|
||||
// CREATE DEFINER=`root`@`localhost` PROCEDURE `bla2`(IN p1 INT, p2 VARCHAR(20))
|
||||
rx.Expression := '^CREATE\s.+\s(PROCEDURE|FUNCTION)\s.+`\((.+)\)\s';
|
||||
if rx.Exec(Create) then begin
|
||||
Params := explode(',', rx.Match[2]);
|
||||
rx.Expression := '^((IN|OUT|INOUT)\s+)?(\S+)\s+(.+)$';
|
||||
for i := 0 to Params.Count - 1 do begin
|
||||
if rx.Exec(Trim(Params[i])) then begin
|
||||
Context := rx.Match[2];
|
||||
if Context = '' then
|
||||
Context := 'IN';
|
||||
Parameters.Add(rx.Match[3] + DELIM + rx.Match[4] + DELIM + Context);
|
||||
end;
|
||||
end;
|
||||
FreeAndNil(Params);
|
||||
end;
|
||||
FreeAndNil(ds);
|
||||
end;
|
||||
editName.SetFocus;
|
||||
editNameChange(Sender);
|
||||
comboTypeSelect(comboType);
|
||||
btnRemoveParam.Enabled := Assigned(listParameters.FocusedNode);
|
||||
Modified := False;
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.FormClose(Sender: TObject; var Action: TCloseAction);
|
||||
begin
|
||||
// Reset edited proc name for the next call
|
||||
AlterRoutineName := '';
|
||||
Parameters.Clear;
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.editNameChange(Sender: TObject);
|
||||
begin
|
||||
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;
|
||||
Modified := True;
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.Modification(Sender: TObject);
|
||||
begin
|
||||
Modified := True;
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.comboTypeSelect(Sender: TObject);
|
||||
var
|
||||
isfunc: Boolean;
|
||||
begin
|
||||
isfunc := (Sender as TTNTComboBox).ItemIndex = 1;
|
||||
lblReturns.Enabled := isfunc;
|
||||
comboReturns.Enabled := isfunc;
|
||||
Modified := True;
|
||||
listParameters.Repaint;
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.btnAddParamClick(Sender: TObject);
|
||||
begin
|
||||
Parameters.Add('Param'+IntToStr(Parameters.Count+1)+DELIM+'INT'+DELIM+'IN');
|
||||
// See List.OnPaint:
|
||||
listParameters.Repaint;
|
||||
Modified := True;
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.btnRemoveParamClick(Sender: TObject);
|
||||
begin
|
||||
Parameters.Delete(ListParameters.FocusedNode.Index);
|
||||
listParameters.Repaint;
|
||||
Modified := True;
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.btnClearParamsClick(Sender: TObject);
|
||||
begin
|
||||
Parameters.Clear;
|
||||
listParameters.Repaint;
|
||||
Modified := True;
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.listParametersBeforePaint(Sender: TBaseVirtualTree;
|
||||
TargetCanvas: TCanvas);
|
||||
begin
|
||||
(Sender as TVirtualStringTree).RootNodeCount := Parameters.Count;
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.listParametersGetImageIndex(Sender: TBaseVirtualTree;
|
||||
Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex;
|
||||
var Ghosted: Boolean; var ImageIndex: Integer);
|
||||
var
|
||||
List: TVirtualStringTree;
|
||||
Context: String;
|
||||
begin
|
||||
// Draw arrow icon to indicate in/out context
|
||||
List := Sender as TVirtualStringTree;
|
||||
if Column <> 3 then
|
||||
ImageIndex := -1
|
||||
else begin
|
||||
Context := List.Text[Node, 3];
|
||||
if Context = 'IN' then ImageIndex := 120
|
||||
else if Context = 'OUT' then ImageIndex := 121
|
||||
else if Context = 'INOUT' then ImageIndex := 122;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.listParametersGetText(Sender: TBaseVirtualTree;
|
||||
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
|
||||
var CellText: WideString);
|
||||
var
|
||||
Values: TWideStringList;
|
||||
begin
|
||||
if Column = 0 then
|
||||
CellText := IntToStr(Node.Index+1)
|
||||
else if (Column = 3) and (comboType.ItemIndex = 1) then
|
||||
CellText := 'IN' // A function can only have IN parameters
|
||||
else begin
|
||||
Values := explode(DELIM, Parameters[Node.Index]);
|
||||
CellText := Values[Column-1];
|
||||
FreeAndNil(Values);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.listParametersPaintText(Sender: TBaseVirtualTree;
|
||||
const TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
|
||||
TextType: TVSTTextType);
|
||||
begin
|
||||
if (Column = 3) and (comboType.ItemIndex = 1) then
|
||||
TargetCanvas.Font.Color := clBtnShadow;
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.listParametersFocusChanged(Sender: TBaseVirtualTree;
|
||||
Node: PVirtualNode; Column: TColumnIndex);
|
||||
begin
|
||||
btnRemoveParam.Enabled := Assigned(Node);
|
||||
if Assigned(Node) and (not ((comboType.ItemIndex = 1) and (Column=3))) then
|
||||
Sender.EditNode(Node, Column);
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.listParametersNewText(Sender: TBaseVirtualTree;
|
||||
Node: PVirtualNode; Column: TColumnIndex; NewText: WideString);
|
||||
var
|
||||
OldValues: TWideStringList;
|
||||
new: WideString;
|
||||
begin
|
||||
OldValues := explode(DELIM, Parameters[Node.Index]);
|
||||
case Column of
|
||||
1: new := NewText + DELIM + OldValues[1] + DELIM + OldValues[2];
|
||||
2: new := OldValues[0] + DELIM + NewText + DELIM + OldValues[2];
|
||||
3: new := OldValues[0] + DELIM + OldValues[1] + DELIM + NewText;
|
||||
end;
|
||||
Parameters[Node.Index] := new;
|
||||
Modified := True;
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.listParametersCreateEditor(Sender: TBaseVirtualTree;
|
||||
Node: PVirtualNode; Column: TColumnIndex; out EditLink: IVTEditLink);
|
||||
var
|
||||
EnumEditor: TEnumEditorLink;
|
||||
i: Integer;
|
||||
begin
|
||||
if Column = 1 then
|
||||
EditLink := TStringEditLink.Create
|
||||
else if Column = 2 then begin
|
||||
EnumEditor := TEnumEditorLink.Create;
|
||||
EnumEditor.AllowCustomText := True;
|
||||
EnumEditor.ValueList := TWideStringList.Create;
|
||||
for i:=Low(MySqlDataTypeArray) to High(MySqlDataTypeArray) do
|
||||
EnumEditor.ValueList.Add(MySqlDataTypeArray[i].Name);
|
||||
EditLink := EnumEditor;
|
||||
end else if Column = 3 then begin
|
||||
EnumEditor := TEnumEditorLink.Create;
|
||||
EnumEditor.ValueList := TWideStringList.Create;
|
||||
EnumEditor.ValueList.Add('IN');
|
||||
EnumEditor.ValueList.Add('OUT');
|
||||
EnumEditor.ValueList.Add('INOUT');
|
||||
EditLink := EnumEditor;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.listParametersEditing(Sender: TBaseVirtualTree;
|
||||
Node: PVirtualNode; Column: TColumnIndex; var Allowed: Boolean);
|
||||
begin
|
||||
// Do not allow the number cells to be edited
|
||||
Allowed := Column > 0;
|
||||
if (Column = 3) and (comboType.ItemIndex = 1) then begin
|
||||
Allowed := False;
|
||||
MessageDlg('A stored function can only have IN parameters so context editing is blocked.', mtInformation, [mbOK], 0);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.SynMemoBodyDragOver(Sender, Source: TObject; X,
|
||||
Y: Integer; State: TDragState; var Accept: Boolean);
|
||||
begin
|
||||
// Allow dragging parameters here
|
||||
Accept := Source = listParameters;
|
||||
// set cursor position
|
||||
SynMemoBody.CaretX := (x - SynMemoBody.Gutter.Width) div SynMemoBody.CharWidth - 1 + SynMemoBody.LeftChar;
|
||||
SynMemoBody.CaretY := y div SynMemoBody.LineHeight + SynMemoBody.TopLine;
|
||||
if not SynMemoBody.Focused then
|
||||
SynMemoBody.SetFocus;
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.SynMemoBodyDragDrop(Sender, Source: TObject; X,
|
||||
Y: Integer);
|
||||
var
|
||||
list: TVirtualStringTree;
|
||||
memo: TSynMemo;
|
||||
begin
|
||||
list := Source as TVirtualStringTree;
|
||||
memo := Sender as TSynMemo;
|
||||
memo.SelText := list.Text[list.FocusedNode, 1];
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.PostChanges(Sender: TObject);
|
||||
var
|
||||
sql: WideString;
|
||||
i: Integer;
|
||||
par: TWideStringList;
|
||||
ProcOrFunc: String;
|
||||
begin
|
||||
// Apply or OK button clicked
|
||||
ProcOrFunc := UpperCase(GetFirstWord(comboType.Text));
|
||||
if editName.Text = '' then begin
|
||||
MessageDlg('Please specify the routine''s name.', mtError, [mbOK], 0);
|
||||
editName.SetFocus;
|
||||
Exit;
|
||||
end else if (ProcOrFunc = 'FUNCTION') and (comboReturns.Text = '') then begin
|
||||
MessageDlg('Please specify the function''s returning datatype.', mtError, [mbOK], 0);
|
||||
comboReturns.SetFocus;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
sql := 'CREATE '+ProcOrFunc+' '+Mainform.mask(editName.Text)+'(';
|
||||
for i := 0 to Parameters.Count - 1 do begin
|
||||
par := explode(DELIM, Parameters[i]);
|
||||
if ProcOrFunc = 'PROCEDURE' then
|
||||
sql := sql + par[2] + ' ';
|
||||
sql := sql + par[0] + ' ' + par[1];
|
||||
if i < Parameters.Count-1 then
|
||||
sql := sql + ', ';
|
||||
end;
|
||||
sql := sql + ') ';
|
||||
if comboReturns.Enabled then
|
||||
sql := sql + 'RETURNS '+comboReturns.Text+' ';
|
||||
sql := sql + 'LANGUAGE SQL ';
|
||||
if not chkDeterministic.Checked then
|
||||
sql := sql + 'NOT ';
|
||||
sql := sql + 'DETERMINISTIC ';
|
||||
sql := sql + UpperCase(comboDataAccess.Text)+' ';
|
||||
sql := sql + 'SQL SECURITY ' + UpperCase(comboSecurity.Text)+' ';
|
||||
sql := sql + 'COMMENT ' + esc(editComment.Text)+' ';
|
||||
sql := sql + SynMemoBody.Text;
|
||||
try
|
||||
// Drop the old routine first. There is no way to ALTER parameters or the name of it.
|
||||
// See also: http://dev.mysql.com/doc/refman/5.0/en/alter-procedure.html
|
||||
if AlterRoutineName <> '' then
|
||||
Mainform.ExecUpdateQuery('DROP '+AlterRoutineType+' '+Mainform.mask(AlterRoutineName), True);
|
||||
Mainform.ExecUpdateQuery(sql, False, True);
|
||||
// Set editing name if create/alter query was successful
|
||||
AlterRoutineName := editName.Text;
|
||||
AlterRoutineType := ProcOrFunc;
|
||||
Modified := False;
|
||||
except
|
||||
On E:Exception do begin
|
||||
ModalResult := mrNone;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.SetModified(Value: Boolean);
|
||||
begin
|
||||
FModified := Value;
|
||||
btnOK.Enabled := FModified;
|
||||
btnApply.Enabled := FModified;
|
||||
end;
|
||||
|
||||
|
||||
procedure TfrmRoutineEditor.btnHelpClick(Sender: TObject);
|
||||
begin
|
||||
// Help button
|
||||
Mainform.CallSQLHelpWithKeyword('CREATE PROCEDURE');
|
||||
end;
|
||||
|
||||
|
||||
end.
|
Reference in New Issue
Block a user