Files
HeidiSQL/source/routine_editor.pas
2010-03-08 23:21:34 +00:00

579 lines
19 KiB
ObjectPascal

unit routine_editor;
interface
uses
Windows, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, SynEdit, SynMemo, StdCtrls,
ComCtrls, ToolWin, VirtualTrees, SynRegExpr, WideStrUtils,
mysql_connection, helpers, mysql_api;
type
TFrame = TDBObjectEditor;
TfrmRoutineEditor = class(TFrame)
btnSave: TButton;
btnDiscard: TButton;
btnHelp: TButton;
lblSQLcode: TLabel;
SynMemoBody: TSynMemo;
PageControlMain: TPageControl;
tabOptions: TTabSheet;
tabParameters: TTabSheet;
tabCreateCode: TTabSheet;
chkDeterministic: TCheckBox;
editComment: TEdit;
comboSecurity: TComboBox;
comboDataAccess: TComboBox;
comboReturns: TComboBox;
comboType: TComboBox;
editName: TEdit;
lblName: TLabel;
lblType: TLabel;
lblReturns: TLabel;
lblSQL: TLabel;
lblSecurity: TLabel;
lblComment: TLabel;
listParameters: TVirtualStringTree;
tlbParameters: TToolBar;
btnAddParam: TToolButton;
btnRemoveParam: TToolButton;
btnClearParams: TToolButton;
SynMemoCREATEcode: TSynMemo;
procedure comboTypeSelect(Sender: TObject);
procedure btnSaveClick(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: String);
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: String);
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);
procedure btnDiscardClick(Sender: TObject);
private
{ Private declarations }
FAlterRoutineType: String;
function ComposeCreateStatement(NameOfObject: String): String;
public
{ Public declarations }
Parameters: TStringList;
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure Init(ObjectName: String=''; ObjectType: TListNodeType=lntNone); override;
function ApplyModifications: TModalResult; override;
end;
implementation
uses main, mysql_structures, grideditlinks;
{$R *.dfm}
constructor TfrmRoutineEditor.Create(AOwner: TComponent);
var
i: Integer;
begin
inherited;
ScaleControls(Screen.PixelsPerInch, FORMS_DPI);
// 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(Datatypes) to High(Datatypes) do
comboReturns.Items.Add(Datatypes[i].Name);
Mainform.SynCompletionProposal.AddEditor(SynMemoBody);
FixVT(listParameters);
Mainform.RestoreListSetup(listParameters);
Parameters := TStringList.Create;
editName.MaxLength := NAME_LEN;
end;
destructor TfrmRoutineEditor.Destroy;
begin
// Store GUI setup
OpenRegistry;
Mainform.SaveListSetup(listParameters);
inherited;
end;
procedure TfrmRoutineEditor.Init(ObjectName: String=''; ObjectType: TListNodeType=lntNone);
var
Create, Params: String;
ParenthesesCount: Integer;
Context: String;
rx: TRegExpr;
i: Integer;
begin
inherited;
if ObjectType = lntProcedure then FAlterRoutineType := 'PROCEDURE'
else FAlterRoutineType := 'FUNCTION';
editName.Text := FEditObjectName;
comboType.ItemIndex := 0;
comboReturns.Text := '';
chkDeterministic.Checked := False;
listParameters.Clear;
Parameters.Clear;
comboDataAccess.ItemIndex := 0;
comboSecurity.ItemIndex := 0;
editComment.Clear;
SynMemoBody.Text := 'BEGIN'+CRLF+CRLF+'END';
if FEditObjectName <> '' then begin
// Editing existing routine
comboType.ItemIndex := ListIndexByRegExpr(comboType.Items, '^'+FAlterRoutineType+'\b');
Create := Mainform.Connection.GetVar('SHOW CREATE '+FAlterRoutineType+' '+Mainform.mask(editName.Text), 2);
rx := TRegExpr.Create;
rx.ModifierI := True;
rx.ModifierG := True;
// CREATE DEFINER=`root`@`localhost` PROCEDURE `bla2`(IN p1 INT, p2 VARCHAR(20))
// CREATE DEFINER=`root`@`localhost` FUNCTION `test3`(`?b` varchar(20)) RETURNS tinyint(4)
// CREATE DEFINER=`root`@`localhost` PROCEDURE `test3`(IN `Param1` int(1) unsigned)
ParenthesesCount := 0;
for i:=1 to Length(Create) do begin
if Create[i] = ')' then begin
Dec(ParenthesesCount);
if ParenthesesCount = 0 then
break;
end;
if ParenthesesCount >= 1 then
Params := Params + Create[i];
if Create[i] = '(' then
Inc(ParenthesesCount);
end;
rx.Expression := '(^|,)\s*((IN|OUT|INOUT)\s+)?(\S+)\s+([^\s,\(]+(\([^\)]*\))?[^,]*)';
if rx.Exec(Params) then while true do begin
Context := UpperCase(rx.Match[3]);
if Context = '' then
Context := 'IN';
Parameters.Add(WideDequotedStr(rx.Match[4], '`') + DELIM + rx.Match[5] + DELIM + Context);
if not rx.ExecNext then
break;
end;
// Cut left part including parameters, so it's easier to parse the rest
Create := Copy(Create, i+1, MaxInt);
// CREATE PROCEDURE sp_name ([proc_parameter[,...]]) [characteristic ...] routine_body
// CREATE FUNCTION sp_name ([func_parameter[,...]]) RETURNS type [characteristic ...] routine_body
// LANGUAGE SQL
// | [NOT] DETERMINISTIC // IS_DETERMINISTIC
// | { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA } // DATA_ACCESS
// | SQL SECURITY { DEFINER | INVOKER } // SECURITY_TYPE
// | COMMENT 'string' // COMMENT
rx.Expression := '\bLANGUAGE SQL\b';
if rx.Exec(Create) then
Delete(Create, rx.MatchPos[0], rx.MatchLen[0]);
rx.Expression := '\bRETURNS\s+(\w+(\([^\)]*\))?)';
if rx.Exec(Create) then begin
comboReturns.Text := rx.Match[1];
Delete(Create, rx.MatchPos[0], rx.MatchLen[0]);
end;
rx.Expression := '\b(NOT\s+)?DETERMINISTIC\b';
if rx.Exec(Create) then begin
chkDeterministic.Checked := rx.MatchLen[1] = -1;
Delete(Create, rx.MatchPos[0], rx.MatchLen[0]);
end;
rx.Expression := '\b('+UpperCase(ImplodeStr('|', comboDataAccess.Items))+')\b';
if rx.Exec(Create) then begin
comboDataAccess.ItemIndex := comboDataAccess.Items.IndexOf(rx.Match[1]);
Delete(Create, rx.MatchPos[0], rx.MatchLen[0]);
end;
rx.Expression := '\bSQL\s+SECURITY\s+(DEFINER|INVOKER)\b';
if rx.Exec(Create) then begin
comboSecurity.ItemIndex := comboSecurity.Items.IndexOf(rx.Match[1]);
Delete(Create, rx.MatchPos[0], rx.MatchLen[0]);
end;
rx.ModifierG := False;
rx.Expression := '\bCOMMENT\s+''((.+)[^''])''[^'']';
if rx.Exec(Create) then begin
editComment.Text := StringReplace(rx.Match[1], '''''', '''', [rfReplaceAll]);
Delete(Create, rx.MatchPos[0], rx.MatchLen[0]-1);
end;
rx.Expression := '^\s*CHARSET\s+[\w\d]+\s';
if rx.Exec(Create) then
Delete(Create, rx.MatchPos[0], rx.MatchLen[0]-1);
// Tata, remaining code is the routine body
Create := TrimLeft(Create);
SynMemoBody.Text := Create;
rx.Free;
end else begin
editName.Text := '';
end;
editNameChange(Self);
comboTypeSelect(comboType);
btnRemoveParam.Enabled := Assigned(listParameters.FocusedNode);
Modified := False;
btnSave.Enabled := Modified;
btnDiscard.Enabled := Modified;
Mainform.ShowStatusMsg;
Screen.Cursor := crDefault;
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;
Modification(Sender);
end;
procedure TfrmRoutineEditor.Modification(Sender: TObject);
begin
Modified := True;
btnSave.Enabled := Modified;
btnDiscard.Enabled := Modified;
SynMemoCreateCode.Text := ComposeCreateStatement(editName.Text);
end;
procedure TfrmRoutineEditor.comboTypeSelect(Sender: TObject);
var
isfunc: Boolean;
begin
isfunc := (Sender as TComboBox).ItemIndex = 1;
lblReturns.Enabled := isfunc;
comboReturns.Enabled := isfunc;
Modification(Sender);
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;
Modification(Sender);
end;
procedure TfrmRoutineEditor.btnRemoveParamClick(Sender: TObject);
begin
Parameters.Delete(ListParameters.FocusedNode.Index);
listParameters.Repaint;
Modification(Sender);
end;
procedure TfrmRoutineEditor.btnClearParamsClick(Sender: TObject);
begin
Parameters.Clear;
listParameters.Repaint;
Modification(Sender);
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
if not (Kind in [ikNormal, ikSelected]) then Exit;
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: String);
var
Values: TStringList;
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: String);
var
OldValues: TStringList;
new: String;
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;
Modification(Sender);
end;
procedure TfrmRoutineEditor.listParametersCreateEditor(Sender: TBaseVirtualTree;
Node: PVirtualNode; Column: TColumnIndex; out EditLink: IVTEditLink);
var
VT: TVirtualStringTree;
EnumEditor: TEnumEditorLink;
i: Integer;
Datatype: String;
begin
VT := Sender as TVirtualStringTree;
if Column = 1 then
EditLink := TStringEditLink.Create
else if Column = 2 then begin
EnumEditor := TEnumEditorLink.Create(VT);
EnumEditor.AllowCustomText := True;
EnumEditor.ValueList := TStringList.Create;
for i:=Low(Datatypes) to High(Datatypes) do begin
Datatype := Datatypes[i].Name;
if Datatypes[i].RequiresLength then
Datatype := Datatype + '(' + Datatypes[i].DefLengthSet + ')';
EnumEditor.ValueList.Add(Datatype);
end;
EditLink := EnumEditor;
end else if Column = 3 then begin
EnumEditor := TEnumEditorLink.Create(VT);
EnumEditor.ValueList := TStringList.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.btnSaveClick(Sender: TObject);
begin
// Apply or OK button clicked
ApplyModifications;
end;
function TfrmRoutineEditor.ApplyModifications: TModalResult;
var
TempName: String;
i: Integer;
allRoutineNames: TStringList;
ProcOrFunc: String;
TargetExists: Boolean;
begin
// Save changes
Result := mrOk;
ProcOrFunc := UpperCase(GetFirstWord(comboType.Text));
// There is no way to ALTER parameters or the name of it.
// Create a temp routine, check for syntax errors, then drop the old routine and create it.
// See also: http://dev.mysql.com/doc/refman/5.0/en/alter-procedure.html
try
if FEditObjectName <> '' then begin
// Create temp name
i := 0;
allRoutineNames := Mainform.Connection.GetCol('SELECT ROUTINE_NAME FROM '+Mainform.mask(DBNAME_INFORMATION_SCHEMA)+'.'+Mainform.mask('ROUTINES')+
' WHERE ROUTINE_SCHEMA = '+esc(Mainform.ActiveDatabase)+
' AND ROUTINE_TYPE = '+esc(ProcOrFunc)
);
TargetExists := ((editName.Text <> FEditObjectName) or (ProcOrFunc <> FAlterRoutineType)) and
(allRoutineNames.IndexOf(editName.Text) > -1);
if TargetExists then begin
Result := MessageDlg('Routine "'+editName.Text+'" already exists. Overwrite it?',
mtConfirmation, [mbYes, mbNo, mbCancel], 0);
if Result = mrNo then
Exit;
end;
while True do begin
inc(i);
TempName := APPNAME + '_temproutine_' + IntToStr(i);
if allRoutineNames.IndexOf(TempName) = -1 then
break;
end;
Mainform.Connection.Query(ComposeCreateStatement(tempName));
// Drop temporary routine, used for syntax checking
Mainform.Connection.Query('DROP '+ProcOrFunc+' IF EXISTS '+Mainform.mask(TempName));
// Drop edited routine
Mainform.Connection.Query('DROP '+FAlterRoutineType+' IF EXISTS '+Mainform.mask(FEditObjectName));
if TargetExists then begin
// Drop target routine - overwriting has been confirmed, see above
Mainform.Connection.Query('DROP '+ProcOrFunc+' IF EXISTS '+Mainform.mask(editName.Text));
end;
end;
Mainform.Connection.Query(ComposeCreateStatement(editName.Text));
// Set editing name if create/alter query was successful
FEditObjectName := editName.Text;
FAlterRoutineType := UpperCase(GetFirstWord(comboType.Text));
Mainform.SetEditorTabCaption(Self, FEditObjectName);
Mainform.RefreshTreeDB(Mainform.ActiveDatabase);
Modified := False;
btnSave.Enabled := Modified;
btnDiscard.Enabled := Modified;
except
on E:Exception do begin
MessageDlg(E.Message, mtError, [mbOk], 0);
Result := mrAbort;
end;
end;
end;
function TfrmRoutineEditor.ComposeCreateStatement(NameOfObject: String): String;
var
ProcOrFunc: String;
par: TStringList;
i: Integer;
begin
ProcOrFunc := UpperCase(GetFirstWord(comboType.Text));
Result := 'CREATE '+ProcOrFunc+' '+Mainform.mask(NameOfObject)+'(';
for i:=0 to Parameters.Count-1 do begin
par := explode(DELIM, Parameters[i]);
if ProcOrFunc = 'PROCEDURE' then
Result := Result + par[2] + ' ';
Result := Result + Mainform.Mask(par[0]) + ' ' + par[1];
if i < Parameters.Count-1 then
Result := Result + ', ';
end;
Result := Result + ')'+CRLF;
if comboReturns.Enabled then
Result := Result + #9 + 'RETURNS '+comboReturns.Text+CRLF;
Result := Result + #9 + 'LANGUAGE SQL'+CRLF + #9;
if not chkDeterministic.Checked then
Result := Result + 'NOT ';
Result := Result + 'DETERMINISTIC'+CRLF
+ #9 + UpperCase(comboDataAccess.Text)+CRLF
+ #9 + 'SQL SECURITY ' + UpperCase(comboSecurity.Text)+CRLF
+ #9 + 'COMMENT ' + esc(editComment.Text)+CRLF
+ SynMemoBody.Text;
end;
procedure TfrmRoutineEditor.btnDiscardClick(Sender: TObject);
var
t: TListNodeType;
begin
Modified := False;
if FAlterRoutineType = 'PROCEDURE' then t := lntProcedure
else t := lntFunction;
Init(FEditObjectName, t);
end;
procedure TfrmRoutineEditor.btnHelpClick(Sender: TObject);
begin
// Help button
Mainform.CallSQLHelpWithKeyword('CREATE PROCEDURE');
end;
end.