sqlscript.pp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. {
  2. This file is part of the Free Pascal run time library.
  3. Copyright (c) 2008 by the Free Pascal development team
  4. Abstract SQL scripting engine.
  5. See the file COPYING.FPC, included in this distribution,
  6. for details about the copyright.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  10. **********************************************************************}
  11. unit sqlscript;
  12. {$mode objfpc}{$H+}
  13. interface
  14. uses
  15. Classes, SysUtils;
  16. type
  17. TSQLScriptStatementEvent = procedure(Sender: TObject; Statement: TStrings; var StopExecution: Boolean) of object;
  18. TSQLScriptDirectiveEvent = procedure(Sender: TObject; Directive, Argument: AnsiString; var StopExecution: Boolean) of object;
  19. TSQLScriptExceptionEvent = procedure(Sender: TObject; Statement: TStrings; TheException: Exception; var Continue: boolean) of object;
  20. TSQLSkipMode = (smNone, smIfBranch, smElseBranch, smAll);
  21. { TCustomSQLScript }
  22. TCustomSQLScript = class(TComponent)
  23. private
  24. FAutoCommit: Boolean;
  25. FLine: Integer;
  26. FCol: Integer;
  27. FDefines: TStrings;
  28. FOnException: TSQLScriptExceptionEvent;
  29. FSkipMode: TSQLSkipMode;
  30. FIsSkipping: Boolean;
  31. FSkipStackIndex: Integer;
  32. FSkipModeStack: array[0..255] of TSQLSkipMode;
  33. FIsSkippingStack: array[0..255] of Boolean;
  34. FAborted: Boolean;
  35. FUseSetTerm, FUseDefines, FUseCommit,
  36. FCommentsInSQL: Boolean;
  37. FTerminator: AnsiString;
  38. FSQL: TStrings;
  39. FCurrentStripped,
  40. FCurrentStatement: TStrings;
  41. FDirectives: TStrings;
  42. FComment,
  43. FEmitLine: Boolean;
  44. procedure SetDefines(const Value: TStrings);
  45. function FindNextSeparator(sep: array of string): AnsiString;
  46. procedure AddToStatement(value: AnsiString; ForceNewLine : boolean);
  47. procedure SetDirectives(value: TStrings);
  48. procedure SetSQL(value: TStrings);
  49. procedure SQLChange(Sender: TObject);
  50. function GetLine: Integer;
  51. protected
  52. procedure ClearStatement; virtual;
  53. procedure InternalStatement (Statement: TStrings; var StopExecution: Boolean); virtual;
  54. procedure InternalDirective (Directive, Argument: String; var StopExecution: Boolean); virtual;
  55. // Runs commit. If ComitRetaining, use CommitRetraining if possible, else stop/starttransaction
  56. procedure InternalCommit(CommitRetaining: boolean=true); virtual;
  57. Function ProcessConditional(Directive : String; Param : String) : Boolean; virtual;
  58. function NextStatement: AnsiString; virtual;
  59. procedure ProcessStatement; virtual;
  60. function Available: Boolean; virtual;
  61. procedure DefaultDirectives; virtual;
  62. procedure ExecuteStatement (Statement: TStrings; var StopExecution: Boolean); virtual; abstract;
  63. procedure ExecuteDirective (Directive, Argument: String; var StopExecution: Boolean); virtual; abstract;
  64. // Executes commit. If possible and CommitRetaining, use CommitRetaining, else
  65. procedure ExecuteCommit(CommitRetaining: boolean=true); virtual; abstract;
  66. public
  67. constructor Create (AnOwner: TComponent); override;
  68. destructor Destroy; override;
  69. procedure Execute; virtual;
  70. protected
  71. property Aborted: Boolean read FAborted;
  72. property Line: Integer read GetLine;
  73. Property AutoCommit : Boolean Read FAutoCommit Write FAutoCommit;
  74. property CommentsInSQL: Boolean read FCommentsInSQL write FCommentsInSQL;
  75. property UseSetTerm: Boolean read FUseSetTerm write FUseSetTerm;
  76. property UseCommit: Boolean read FUseCommit write FUseCommit;
  77. property UseDefines: Boolean read FUseDefines write FUseDefines;
  78. property Defines : TStrings Read FDefines Write SetDefines;
  79. property Directives: TStrings read FDirectives write SetDirectives;
  80. property Script: TStrings read FSQL write SetSQL; // script to execute
  81. property Terminator: AnsiString read FTerminator write FTerminator;
  82. property OnException : TSQLScriptExceptionEvent read FOnException write FOnException;
  83. end;
  84. { TEventSQLScript }
  85. TEventSQLScript = class (TCustomSQLScript)
  86. private
  87. FAfterExec: TNotifyEvent;
  88. FBeforeExec: TNotifyEvent;
  89. FOnCommit: TNotifyEvent;
  90. FOnSQLStatement: TSQLScriptStatementEvent;
  91. FOnDirective: TSQLScriptDirectiveEvent;
  92. protected
  93. procedure ExecuteStatement (SQLStatement: TStrings; var StopExecution: Boolean); override;
  94. procedure ExecuteDirective (Directive, Argument: String; var StopExecution: Boolean); override;
  95. procedure ExecuteCommit(CommitRetaining: boolean=true); override;
  96. public
  97. procedure Execute; override;
  98. property Aborted;
  99. property Line;
  100. published
  101. property Directives;
  102. property Defines;
  103. property Script;
  104. property Terminator;
  105. property CommentsinSQL;
  106. property UseSetTerm;
  107. property UseCommit;
  108. property UseDefines;
  109. property OnException;
  110. property OnSQLStatement: TSQLScriptStatementEvent read FOnSQLStatement write FOnSQLStatement;
  111. property OnDirective: TSQLScriptDirectiveEvent read FOnDirective write FOnDirective;
  112. property OnCommit: TNotifyEvent read FOnCommit write FOnCommit;
  113. property BeforeExecute : TNotifyEvent read FBeforeExec write FBeforeExec;
  114. property AfterExecute : TNotifyEvent read FAfterExec write FAfterExec;
  115. end;
  116. ESQLScript = Class(Exception);
  117. implementation
  118. Resourcestring
  119. SErrIfXXXNestingLimitReached = '#IFDEF nesting limit reached';
  120. SErrInvalidEndif = '#ENDIF without #IFDEF';
  121. SErrInvalidElse = '#ELSE without #IFDEF';
  122. { ---------------------------------------------------------------------
  123. Auxiliary Functions
  124. ---------------------------------------------------------------------}
  125. function StartsWith(S1, S2: AnsiString): Boolean;
  126. var
  127. L1,L2 : Integer;
  128. begin
  129. Result:=False;
  130. L1:=Length(S1);
  131. L2:=Length(S2);
  132. if (L2=0) or (L1<L2) then
  133. Exit;
  134. Result:=(AnsiCompareStr(Copy(s1,1,L2),S2)=0);
  135. Result := Result and ((L2 = L1) or (s1[L2+1] = ' '));
  136. end;
  137. function GetFirstSeparator(S: AnsiString; Sep: array of string): AnsiString;
  138. var
  139. i, C, M: Integer;
  140. begin
  141. M:=length(S) + 1;
  142. Result:='';
  143. for i:=0 to high(Sep) do
  144. begin
  145. C:=Pos(Sep[i],S);
  146. if (C<>0) and (C<M) then
  147. begin
  148. M:=C;
  149. Result:=Sep[i];
  150. end;
  151. end;
  152. end;
  153. Function ConvertWhiteSpace(S : String) : String;
  154. begin
  155. Result:=StringReplace(S,#13,' ',[rfReplaceAll]);
  156. Result:=StringReplace(Result,#10,' ',[rfReplaceAll]);
  157. Result:=Trim(Result);
  158. end;
  159. { ---------------------------------------------------------------------
  160. TSQLScript
  161. ---------------------------------------------------------------------}
  162. procedure TCustomSQLScript.SQLChange(Sender: TObject);
  163. begin
  164. FLine:=1;
  165. FCol:=1;
  166. end;
  167. procedure TCustomSQLScript.SetDirectives(value: TStrings);
  168. var
  169. i : Integer;
  170. S : AnsiString;
  171. begin
  172. FDirectives.Clear();
  173. if (Value<>Nil) then
  174. begin
  175. for i:=0 to value.Count - 1 do
  176. begin
  177. S:=UpperCase(ConvertWhiteSpace(value[i]));
  178. if Length(S)>0 then
  179. FDirectives.Add(S);
  180. end;
  181. end;
  182. DefaultDirectives;
  183. end;
  184. procedure TCustomSQLScript.SetSQL(value: TStrings);
  185. begin
  186. FSQL.Assign(value);
  187. FLine:=1;
  188. FCol:=1;
  189. end;
  190. function TCustomSQLScript.GetLine: Integer;
  191. begin
  192. Result:=FLine - 1;
  193. end;
  194. procedure TCustomSQLScript.AddToStatement(value: AnsiString; ForceNewLine : Boolean);
  195. Procedure DA(L : TStrings);
  196. begin
  197. With L do
  198. if ForceNewLine or (Count=0) then
  199. Add(value)
  200. else
  201. Strings[Count-1]:=Strings[Count-1] + value;
  202. end;
  203. begin
  204. DA(FCurrentStatement);
  205. if Not FComment then
  206. DA(FCurrentStripped);
  207. end;
  208. function TCustomSQLScript.FindNextSeparator(Sep: array of string): AnsiString;
  209. var
  210. S: AnsiString;
  211. begin
  212. Result:='';
  213. while (FLine<=FSQL.Count) do
  214. begin
  215. S:=FSQL.Strings[FLine-1];
  216. if (FCol>1) then
  217. begin
  218. S:=Copy(S,FCol,length(S));
  219. end;
  220. Result:=GetFirstSeparator(S,Sep);
  221. if (Result='') then
  222. begin
  223. if FEmitLine then
  224. AddToStatement(S,(FCol<=1));
  225. FCol:=1;
  226. FLine:=FLine+1;
  227. end
  228. else
  229. begin
  230. if FEmitLine then
  231. AddToStatement(Copy(S,1,Pos(Result,S)-1),(FCol=1));
  232. FCol:=(FCol-1)+Pos(Result,S);
  233. break;
  234. end;
  235. end;
  236. end;
  237. function TCustomSQLScript.Available: Boolean;
  238. begin
  239. With FSQL do
  240. Result:=(FLine<Count) or
  241. (
  242. ( FLine = Count ) and
  243. ( FCol < Length(Strings[Count-1] ) )
  244. );
  245. end;
  246. procedure TCustomSQLScript.InternalStatement(Statement: TStrings; var StopExecution: Boolean);
  247. var
  248. cont : boolean;
  249. begin
  250. try
  251. ExecuteStatement(Statement, StopExecution);
  252. except
  253. on E : Exception do
  254. begin
  255. cont := false;
  256. if assigned (FOnException) then
  257. FOnException (self, Statement, E, cont);
  258. if not cont then
  259. Raise;
  260. end;
  261. end;
  262. end;
  263. procedure TCustomSQLScript.InternalDirective(Directive, Argument: String; var StopExecution: Boolean);
  264. var
  265. cont : boolean;
  266. l : TStrings;
  267. begin
  268. try
  269. ExecuteDirective(Directive, Argument, StopExecution);
  270. except
  271. on E : Exception do
  272. begin
  273. cont := false;
  274. if assigned (FOnException) then
  275. begin
  276. l := TStringlist.Create;
  277. try
  278. L.Add(Directive);
  279. if Argument <> '' then
  280. L.Add(Argument);
  281. FOnException (self, l, E, cont);
  282. finally
  283. L.Free;
  284. end;
  285. end;
  286. if not cont then
  287. Raise;
  288. end;
  289. end;
  290. end;
  291. procedure TCustomSQLScript.InternalCommit(CommitRetaining: boolean=true);
  292. var
  293. cont : boolean;
  294. l : TStrings;
  295. begin
  296. try
  297. ExecuteCommit(CommitRetaining);
  298. except
  299. on E : Exception do
  300. begin
  301. cont := false;
  302. if assigned (FOnException) then
  303. begin
  304. l := TStringlist.Create;
  305. try
  306. L.Add('COMMIT');
  307. FOnException (self, l, E, cont);
  308. finally
  309. L.Free;
  310. end;
  311. end;
  312. if not cont then
  313. Raise;
  314. end;
  315. end;
  316. end;
  317. procedure TCustomSQLScript.ClearStatement;
  318. begin
  319. FCurrentStatement.Clear;
  320. FCurrentStripped.Clear;
  321. end;
  322. procedure TCustomSQLScript.ProcessStatement;
  323. Var
  324. S,
  325. Directive : String;
  326. I : longint;
  327. begin
  328. if (FCurrentStatement.Count=0) then
  329. Exit;
  330. S:=Trim(FCurrentStripped.Text);
  331. I:=0;
  332. Directive:='';
  333. While (i<FDirectives.Count) and (Directive='') do
  334. begin
  335. If StartsWith(AnsiUpperCase(S), FDirectives[i]) Then
  336. Directive:=FDirectives[i];
  337. Inc(I);
  338. end;
  339. If (Directive<>'') then
  340. begin
  341. S:=Trim(Copy(S,Length(Directive)+1,length(S)));
  342. If (Directive[1]='#') then
  343. begin
  344. if not FUseDefines or not ProcessConditional(Directive,S) then
  345. if Not FIsSkipping then
  346. InternalDirective (Directive, S, FAborted);
  347. end
  348. else If Not FIsSkipping then
  349. begin
  350. // If AutoCommit, skip any explicit commits.
  351. if FUseCommit
  352. and ((Directive = 'COMMIT') or (Directive = 'COMMIT WORK' {SQL standard}))
  353. and not FAutoCommit then
  354. InternalCommit(false) //explicit commit, no commit retaining
  355. else if FUseCommit
  356. and (Directive = 'COMMIT RETAIN') {at least Firebird syntax}
  357. and not FAutoCommit then
  358. InternalCommit(true)
  359. else if FUseSetTerm
  360. and (Directive = 'SET TERM' {Firebird/Interbase only}) then
  361. FTerminator:=S
  362. else
  363. InternalDirective (Directive,S,FAborted)
  364. end
  365. end
  366. else
  367. if (not FIsSkipping) then
  368. begin
  369. InternalStatement(FCurrentStatement,FAborted);
  370. If FAutoCommit and not FAborted then
  371. InternalCommit;
  372. end;
  373. end;
  374. procedure TCustomSQLScript.Execute;
  375. begin
  376. FSkipMode:=smNone;
  377. FIsSkipping:=False;
  378. FSkipStackIndex:=0;
  379. Faborted:=False;
  380. DefaultDirectives;
  381. Repeat
  382. NextStatement();
  383. if Length(Trim(FCurrentStripped.Text))>0 then
  384. ProcessStatement;
  385. Until FAborted or Not Available;
  386. end;
  387. function TCustomSQLScript.NextStatement: AnsiString;
  388. var
  389. pnt: AnsiString;
  390. terminator_found: Boolean;
  391. begin
  392. terminator_found:=False;
  393. ClearStatement;
  394. while FLine <= FSQL.Count do
  395. begin
  396. pnt:=FindNextSeparator([FTerminator, '/*', '"', '''', '--']);
  397. if (pnt=FTerminator) then
  398. begin
  399. FCol:=FCol + length(pnt);
  400. terminator_found:=True;
  401. break;
  402. end
  403. else if pnt = '/*' then
  404. begin
  405. FComment:=True;
  406. if FCommentsInSQL then
  407. AddToStatement(pnt,false)
  408. else
  409. FEmitLine:=False;
  410. FCol:=FCol + length(pnt);
  411. pnt:=FindNextSeparator(['*/']);
  412. if FCommentsInSQL then
  413. AddToStatement(pnt,false)
  414. else
  415. FEmitLine:=True;
  416. FCol:=FCol + length(pnt);
  417. FComment:=False;
  418. end
  419. else if pnt = '--' then
  420. begin
  421. FComment:=True;
  422. if FCommentsInSQL then
  423. AddToStatement(Copy(FSQL[FLine-1],FCol,Length(FSQL[FLine-1])-FCol+1),False);
  424. Inc(Fline);
  425. FCol:=1;
  426. FComment:=False;
  427. end
  428. else if pnt = '"' then
  429. begin
  430. AddToStatement(pnt,false);
  431. FCol:=FCol + length(pnt);
  432. pnt:=FindNextSeparator(['"']);
  433. AddToStatement(pnt,false);
  434. FCol:=FCol + length(pnt);
  435. end
  436. else if pnt = '''' then
  437. begin
  438. AddToStatement(pnt,False);
  439. FCol:=FCol + length(pnt);
  440. pnt:=FindNextSeparator(['''']);
  441. AddToStatement(pnt,false);
  442. FCol:=FCol + length(pnt);
  443. end;
  444. end;
  445. if not terminator_found then
  446. ClearStatement;
  447. while (FCurrentStatement.Count > 0) and (trim(FCurrentStatement.Strings[0]) = '') do
  448. FCurrentStatement.Delete(0);
  449. while (FCurrentStripped.Count > 0) and (trim(FCurrentStripped.Strings[0]) = '') do
  450. FCurrentStripped.Delete(0);
  451. Result:=FCurrentStatement.Text;
  452. end;
  453. Constructor TCustomSQLScript.Create (AnOwner: TComponent);
  454. Var
  455. L : TStringList;
  456. begin
  457. inherited;
  458. L:=TStringList.Create;
  459. With L do
  460. begin
  461. Sorted:=True;
  462. Duplicates:=dupIgnore;
  463. end;
  464. FDefines:=L;
  465. FCommentsInSQL:=True;
  466. FTerminator:=';';
  467. L:=TStringList.Create();
  468. L.OnChange:=@SQLChange;
  469. FSQL:=L;
  470. FDirectives:=TStringList.Create();
  471. FCurrentStripped:=TStringList.Create();
  472. FCurrentStatement:=TStringList.Create();
  473. FLine:=1;
  474. FCol:=1;
  475. FEmitLine:=True;
  476. FUseCommit := true;
  477. FUseDefines := True;
  478. FUseSetTerm := True;
  479. DefaultDirectives;
  480. end;
  481. destructor TCustomSQLScript.Destroy;
  482. begin
  483. FreeAndNil(FCurrentStripped);
  484. FreeAndNil(FCurrentStatement);
  485. FreeAndNil(FSQL);
  486. FreeAndNil(FDirectives);
  487. FreeAndNil(FDefines);
  488. inherited Destroy;
  489. end;
  490. procedure TCustomSQLScript.SetDefines(const Value: TStrings);
  491. begin
  492. FDefines.Assign(Value);
  493. end;
  494. procedure TCustomSQLScript.DefaultDirectives;
  495. begin
  496. With FDirectives do
  497. begin
  498. // Insertion order matters as testing for directives will be done with StartsWith
  499. if FUseSetTerm then
  500. Add('SET TERM');
  501. if FUseCommit then
  502. begin
  503. Add('COMMIT WORK'); {SQL Standard, equivalent to commit}
  504. Add('COMMIT RETAIN'); {Firebird/Interbase; probably won't hurt on other dbs}
  505. Add('COMMIT'); {Shorthand used in many dbs, e.g. Firebird}
  506. end;
  507. if FUseDefines then
  508. begin
  509. Add('#IFDEF');
  510. Add('#IFNDEF');
  511. Add('#ELSE');
  512. Add('#ENDIF');
  513. Add('#DEFINE');
  514. Add('#UNDEF');
  515. Add('#UNDEFINE');
  516. end;
  517. end;
  518. end;
  519. Function TCustomSQLScript.ProcessConditional(Directive: String; Param : String) : Boolean;
  520. Procedure PushSkipMode;
  521. begin
  522. if FSkipStackIndex=High(FSkipModeStack) then
  523. Raise ESQLScript.Create(SErrIfXXXNestingLimitReached);
  524. FSkipModeStack[FSkipStackIndex]:=FSkipMode;
  525. FIsSkippingStack[FSkipStackIndex]:=FIsSkipping;
  526. Inc(FSkipStackIndex);
  527. end;
  528. Procedure PopSkipMode;
  529. begin
  530. if FSkipStackIndex = 0 then
  531. Raise ESQLScript.Create(SErrInvalidEndif);
  532. Dec(FSkipStackIndex);
  533. FSkipMode := FSkipModeStack[FSkipStackIndex];
  534. FIsSkipping := FIsSkippingStack[FSkipStackIndex];
  535. end;
  536. Var
  537. Index : Integer;
  538. begin
  539. Result:=True;
  540. if (Directive='#DEFINE') then
  541. begin
  542. if not FIsSkipping then
  543. FDefines.Add(Param);
  544. end
  545. else if (Directive='#UNDEF') or (Directive='#UNDEFINE') then
  546. begin
  547. if not FIsSkipping then
  548. begin
  549. Index:=FDefines.IndexOf(Param);
  550. if (Index>=0) then
  551. FDefines.Delete(Index);
  552. end;
  553. end
  554. else if (Directive='#IFDEF') or (Directive='#IFNDEF') then
  555. begin
  556. PushSkipMode;
  557. if FIsSkipping then
  558. begin
  559. FSkipMode:=smAll;
  560. FIsSkipping:=true;
  561. end
  562. else
  563. begin
  564. Index:=FDefines.IndexOf(Param);
  565. if ((Directive='#IFDEF') and (Index<0)) or
  566. ((Directive='#IFNDEF') and (Index>=0)) then
  567. begin
  568. FSkipMode:=smIfBranch;
  569. FIsSkipping:=true;
  570. end
  571. else
  572. FSkipMode := smElseBranch;
  573. end;
  574. end
  575. else if (Directive='#ELSE') then
  576. begin
  577. if (FSkipStackIndex=0) then
  578. Raise ESQLScript.Create(SErrInvalidElse);
  579. if (FSkipMode=smIfBranch) then
  580. FIsSkipping:=false
  581. else if (FSkipMode=smElseBranch) then
  582. FIsSkipping:=true;
  583. end
  584. else if (Directive='#ENDIF') then
  585. PopSkipMode
  586. else
  587. Result:=False;
  588. end;
  589. { TEventSQLScript }
  590. procedure TEventSQLScript.ExecuteStatement(SQLStatement: TStrings;
  591. var StopExecution: Boolean);
  592. begin
  593. if assigned (FOnSQLStatement) then
  594. FOnSQLStatement (self, SQLStatement, StopExecution);
  595. end;
  596. procedure TEventSQLScript.ExecuteDirective(Directive, Argument: String;
  597. var StopExecution: Boolean);
  598. begin
  599. if assigned (FOnDirective) then
  600. FOnDirective (Self, Directive, Argument, StopExecution);
  601. end;
  602. procedure TEventSQLScript.ExecuteCommit(CommitRetaining: boolean=true);
  603. begin
  604. if assigned (FOnCommit) then
  605. FOnCommit (Self);
  606. end;
  607. procedure TEventSQLScript.Execute;
  608. begin
  609. if assigned (FBeforeExec) then
  610. FBeforeExec (Self);
  611. inherited Execute;
  612. if assigned (FAfterExec) then
  613. FAfterExec (Self);
  614. end;
  615. end.