{ This file is part of the Free Pascal run time library. Copyright (c) 2008 by the Free Pascal development team Abstract SQL scripting engine. See the file COPYING.FPC, included in this distribution, for details about the copyright. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. **********************************************************************} unit sqlscript; {$mode objfpc}{$H+} interface uses Classes, SysUtils; type TSQLScriptStatementEvent = procedure(Sender: TObject; Statement: TStrings; var StopExecution: Boolean) of object; TSQLScriptDirectiveEvent = procedure(Sender: TObject; Directive, Argument: AnsiString; var StopExecution: Boolean) of object; TSQLScriptExceptionEvent = procedure(Sender: TObject; Statement: TStrings; TheException: Exception; var Continue: boolean) of object; TSQLSkipMode = (smNone, smIfBranch, smElseBranch, smAll); { TCustomSQLScript } TCustomSQLScript = class(TComponent) private FAutoCommit: Boolean; FLine: Integer; FCol: Integer; FDefines: TStrings; FOnException: TSQLScriptExceptionEvent; FSkipMode: TSQLSkipMode; FIsSkipping: Boolean; FSkipStackIndex: Integer; FSkipModeStack: array[0..255] of TSQLSkipMode; FIsSkippingStack: array[0..255] of Boolean; FAborted: Boolean; FUseSetTerm, FUseDefines, FUseCommit, FCommentsInSQL: Boolean; FTerminator: AnsiString; FSQL: TStrings; FCurrentStatement: TStrings; FDirectives: TStrings; FEmitLine: Boolean; procedure SetDefines(const Value: TStrings); function FindNextSeparator(sep: array of string): AnsiString; procedure AddToStatement(value: AnsiString; ForceNewLine : boolean); procedure SetDirectives(value: TStrings); procedure SetSQL(value: TStrings); procedure SQLChange(Sender: TObject); function GetLine: Integer; Function ProcessConditional(Directive : String; Param : String) : Boolean; virtual; function NextStatement: AnsiString; procedure ProcessStatement; function Available: Boolean; procedure InternalStatement (Statement: TStrings; var StopExecution: Boolean); procedure InternalDirective (Directive, Argument: String; var StopExecution: Boolean); procedure InternalCommit; protected procedure DefaultDirectives; virtual; procedure ExecuteStatement (Statement: TStrings; var StopExecution: Boolean); virtual; abstract; procedure ExecuteDirective (Directive, Argument: String; var StopExecution: Boolean); virtual; abstract; procedure ExecuteCommit; virtual; abstract; public constructor Create (AnOwner: TComponent); override; destructor Destroy; override; procedure Execute; virtual; protected property Aborted: Boolean read FAborted; property Line: Integer read GetLine; Property AutoCommit : Boolean Read FAutoCommit Write FAutoCommit; property CommentsInSQL: Boolean read FCommentsInSQL write FCommentsInSQL; property UseSetTerm: Boolean read FUseSetTerm write FUseSetTerm; property UseCommit: Boolean read FUseCommit write FUseCommit; property UseDefines: Boolean read FUseDefines write FUseDefines; property Defines : TStrings Read FDefines Write SetDefines; property Directives: TStrings read FDirectives write SetDirectives; property Script: TStrings read FSQL write SetSQL; // script to execute property Terminator: AnsiString read FTerminator write FTerminator; property OnException : TSQLScriptExceptionEvent read FOnException write FOnException; end; { TEventSQLScript } TEventSQLScript = class (TCustomSQLScript) private FAfterExec: TNotifyEvent; FBeforeExec: TNotifyEvent; FOnCommit: TNotifyEvent; FOnSQLStatement: TSQLScriptStatementEvent; FOnDirective: TSQLScriptDirectiveEvent; protected procedure ExecuteStatement (SQLStatement: TStrings; var StopExecution: Boolean); override; procedure ExecuteDirective (Directive, Argument: String; var StopExecution: Boolean); override; procedure ExecuteCommit; override; public procedure Execute; override; property Aborted; property Line; published property Directives; property Defines; property Script; property Terminator; property CommentsinSQL; property UseSetTerm; property UseCommit; property UseDefines; property OnException; property OnSQLStatement: TSQLScriptStatementEvent read FOnSQLStatement write FOnSQLStatement; property OnDirective: TSQLScriptDirectiveEvent read FOnDirective write FOnDirective; property OnCommit: TNotifyEvent read FOnCommit write FOnCommit; property BeforeExecute : TNotifyEvent read FBeforeExec write FBeforeExec; property AfterExecute : TNotifyEvent read FAfterExec write FAfterExec; end; ESQLScript = Class(Exception); implementation Resourcestring SErrIfXXXNestingLimitReached = '#IFDEF nesting limit reached'; SErrInvalidEndif = '#ENDIF without #IFDEF'; SErrInvalidElse = '#ELSE without #IFDEF'; { --------------------------------------------------------------------- Auxiliary Functions ---------------------------------------------------------------------} function StartsWith(S1, S2: AnsiString): Boolean; var L1,L2 : Integer; begin Result:=False; L1:=Length(S1); L2:=Length(S2); if (L2=0) or (L10) and (CNil) then begin for i:=0 to value.Count - 1 do begin S:=UpperCase(ConvertWhiteSpace(value[i])); if Length(S)>0 then FDirectives.Add(S); end; end; DefaultDirectives; end; procedure TCustomSQLScript.SetSQL(value: TStrings); begin FSQL.Assign(value); FLine:=1; FCol:=1; end; function TCustomSQLScript.GetLine: Integer; begin Result:=FLine - 1; end; procedure TCustomSQLScript.AddToStatement(value: AnsiString; ForceNewLine : Boolean); begin With FCurrentStatement do if ForceNewLine or (Count=0) then Add(value) else Strings[Count-1]:=Strings[Count-1] + value; end; function TCustomSQLScript.FindNextSeparator(Sep: array of string): AnsiString; var S: AnsiString; begin Result:=''; while (FLine<=FSQL.Count) do begin S:=FSQL.Strings[FLine-1]; if (FCol>1) then begin S:=Copy(S,FCol,length(S)); end; Result:=GetFirstSeparator(S,Sep); if (Result='') then begin if FEmitLine then AddToStatement(S,(FCol=1)); FCol:=1; FLine:=FLine+1; end else begin if FEmitLine then AddToStatement(Copy(S,1,Pos(Result,S)-1),(FCol=1)); FCol:=(FCol-1)+Pos(Result,S); break; end; end; end; function TCustomSQLScript.Available: Boolean; var SCol, SLine: Integer; begin SCol:=FCol; SLine:=FLine; try Result:=Length(Trim(NextStatement()))>0; Finally FCol:=SCol; FLine:=SLine; end; end; procedure TCustomSQLScript.InternalStatement(Statement: TStrings; var StopExecution: Boolean); var cont : boolean; begin try ExecuteStatement(Statement, StopExecution); except on E : Exception do begin cont := false; if assigned (FOnException) then FOnException (self, Statement, E, cont); if not cont then Raise; end; end; end; procedure TCustomSQLScript.InternalDirective(Directive, Argument: String; var StopExecution: Boolean); var cont : boolean; l : TStrings; begin try ExecuteDirective(Directive, Argument, StopExecution); except on E : Exception do begin cont := false; if assigned (FOnException) then begin l := TStringlist.Create; try L.Add(Directive); if Argument <> '' then L.Add(Argument); FOnException (self, l, E, cont); finally L.Free; end; end; if not cont then Raise; end; end; end; procedure TCustomSQLScript.InternalCommit; var cont : boolean; l : TStrings; begin try ExecuteCommit; except on E : Exception do begin cont := false; if assigned (FOnException) then begin l := TStringlist.Create; try L.Add('COMMIT'); FOnException (self, l, E, cont); finally L.Free; end; end; if not cont then Raise; end; end; end; procedure TCustomSQLScript.ProcessStatement; Var S, Directive : String; I : longint; begin if (FCurrentStatement.Count=0) then Exit; S:=DeleteComments(FCurrentStatement.Text, Terminator); I:=0; Directive:=''; While (i'') then begin S:=Trim(Copy(S,Length(Directive)+1,length(S))); If (Directive[1]='#') then begin if not FUseDefines or not ProcessConditional(Directive,S) then if Not FIsSkipping then InternalDirective (Directive, S, FAborted); end else If Not FIsSkipping then begin // If AutoCommit, skip any explicit commits. if FUseCommit and (Directive = 'COMMIT') and not FAutoCommit then InternalCommit else if FUseSetTerm and (Directive = 'SET TERM') then FTerminator:=S else InternalDirective (Directive,S,FAborted) end end else if (not FIsSkipping) then begin InternalStatement(FCurrentStatement,FAborted); If FAutoCommit and not FAborted then InternalCommit; end; end; procedure TCustomSQLScript.Execute; begin FSkipMode:=smNone; FIsSkipping:=False; FSkipStackIndex:=0; Faborted:=False; DefaultDirectives; while not FAborted and Available() do begin NextStatement(); ProcessStatement; end; end; function TCustomSQLScript.NextStatement: AnsiString; var pnt: AnsiString; terminator_found: Boolean; begin terminator_found:=False; FCurrentStatement.Clear; while FLine <= FSQL.Count do begin pnt:=FindNextSeparator([FTerminator, '/*', '"', '''']); if (pnt=FTerminator) then begin FCol:=FCol + length(pnt); terminator_found:=True; break; end else if pnt = '/*' then begin if FCommentsInSQL then AddToStatement(pnt,false) else FEmitLine:=False; FCol:=FCol + length(pnt); pnt:=FindNextSeparator(['*/']); if FCommentsInSQL then AddToStatement(pnt,false) else FEmitLine:=True; FCol:=FCol + length(pnt); end else if pnt = '"' then begin AddToStatement(pnt,false); FCol:=FCol + length(pnt); pnt:=FindNextSeparator(['"']); AddToStatement(pnt,false); FCol:=FCol + length(pnt); end else if pnt = '''' then begin AddToStatement(pnt,False); FCol:=FCol + length(pnt); pnt:=FindNextSeparator(['''']); AddToStatement(pnt,false); FCol:=FCol + length(pnt); end; end; if not terminator_found then FCurrentStatement.Clear(); while (FCurrentStatement.Count > 0) and (trim(FCurrentStatement.Strings[0]) = '') do FCurrentStatement.Delete(0); Result:=FCurrentStatement.Text; end; Constructor TCustomSQLScript.Create (AnOwner: TComponent); Var L : TStringList; begin inherited; L:=TStringList.Create; With L do begin Sorted:=True; Duplicates:=dupIgnore; end; FDefines:=L; FCommentsInSQL:=True; FTerminator:=';'; L:=TStringList.Create(); L.OnChange:=@SQLChange; FSQL:=L; FDirectives:=TStringList.Create(); FCurrentStatement:=TStringList.Create(); FLine:=1; FCol:=1; FEmitLine:=True; FUseCommit := true; FUseDefines := True; FUseSetTerm := True; DefaultDirectives; end; destructor TCustomSQLScript.Destroy; begin FreeAndNil(FCurrentStatement); FreeAndNil(FSQL); FreeAndNil(FDirectives); FreeAndNil(FDefines); inherited Destroy; end; procedure TCustomSQLScript.SetDefines(const Value: TStrings); begin FDefines.Assign(Value); end; procedure TCustomSQLScript.DefaultDirectives; begin With FDirectives do begin if FUseSetTerm then Add('SET TERM'); if FUseCommit then Add('COMMIT'); if FUseDefines then begin Add('#IFDEF'); Add('#IFNDEF'); Add('#ELSE'); Add('#ENDIF'); Add('#DEFINE'); Add('#UNDEF'); Add('#UNDEFINE'); end; end; end; Function TCustomSQLScript.ProcessConditional(Directive: String; Param : String) : Boolean; Procedure PushSkipMode; begin if FSkipStackIndex=High(FSkipModeStack) then Raise ESQLScript.Create(SErrIfXXXNestingLimitReached); FSkipModeStack[FSkipStackIndex]:=FSkipMode; FIsSkippingStack[FSkipStackIndex]:=FIsSkipping; Inc(FSkipStackIndex); end; Procedure PopSkipMode; begin if FSkipStackIndex = 0 then Raise ESQLScript.Create(SErrInvalidEndif); Dec(FSkipStackIndex); FSkipMode := FSkipModeStack[FSkipStackIndex]; FIsSkipping := FIsSkippingStack[FSkipStackIndex]; end; Var Index : Integer; begin Result:=True; if (Directive='#DEFINE') then begin if not FIsSkipping then FDefines.Add(Param); end else if (Directive='#UNDEF') or (Directive='#UNDEFINE') then begin if not FIsSkipping then begin Index:=FDefines.IndexOf(Param); if (Index>=0) then FDefines.Delete(Index); end; end else if (Directive='#IFDEF') or (Directive='#IFNDEF') then begin PushSkipMode; if FIsSkipping then begin FSkipMode:=smAll; FIsSkipping:=true; end else begin Index:=FDefines.IndexOf(Param); if ((Directive='#IFDEF') and (Index<0)) or ((Directive='#IFNDEF') and (Index>=0)) then begin FSkipMode:=smIfBranch; FIsSkipping:=true; end else FSkipMode := smElseBranch; end; end else if (Directive='#ELSE') then begin if (FSkipStackIndex=0) then Raise ESQLScript.Create(SErrInvalidElse); if (FSkipMode=smIfBranch) then FIsSkipping:=false else if (FSkipMode=smElseBranch) then FIsSkipping:=true; end else if (Directive='#ENDIF') then PopSkipMode else Result:=False; end; { TEventSQLScript } procedure TEventSQLScript.ExecuteStatement(SQLStatement: TStrings; var StopExecution: Boolean); begin if assigned (FOnSQLStatement) then FOnSQLStatement (self, SQLStatement, StopExecution); end; procedure TEventSQLScript.ExecuteDirective(Directive, Argument: String; var StopExecution: Boolean); begin if assigned (FOnDirective) then FOnDirective (Self, Directive, Argument, StopExecution); end; procedure TEventSQLScript.ExecuteCommit; begin if assigned (FOnCommit) then FOnCommit (Self); end; procedure TEventSQLScript.Execute; begin if assigned (FBeforeExec) then FBeforeExec (Self); inherited Execute; if assigned (FAfterExec) then FAfterExec (Self); end; end.