From 4de1b3012d1bd59861b63bbc2e49a59e2b0411c8 Mon Sep 17 00:00:00 2001 From: Ansgar Becker Date: Tue, 6 Mar 2012 21:52:57 +0000 Subject: [PATCH] Move code from GetSQLSplitMarkers and SplitSQL into TSQLBatch class. Enables me to access details like string size of batch. Preparation for query history. --- source/helpers.pas | 256 +++++++++++++++++++++++----------------- source/main.pas | 49 ++++---- source/runsqlfile.pas | 30 +++-- source/table_editor.pas | 56 ++++----- 4 files changed, 209 insertions(+), 182 deletions(-) diff --git a/source/helpers.pas b/source/helpers.pas index c54eca20..46a63491 100644 --- a/source/helpers.pas +++ b/source/helpers.pas @@ -50,11 +50,27 @@ type GripRect: TRect; end; + TSQLBatch = class; TSQLSentence = class(TObject) - LeftOffset, RightOffset: Integer; - SQL: String; + private + FOwner: TSQLBatch; + function GetSize: Integer; + function GetSQL: String; + public + LeftOffset, RightOffset: Integer; + constructor Create(Owner: TSQLBatch); + property SQL: String read GetSQL; + property Size: Integer read GetSize; + end; + TSQLBatch = class(TObjectList) + private + FSQL: String; + procedure SetSQL(Value: String); + function GetSize: Integer; + public + property Size: Integer read GetSize; + property SQL: String read FSQL write SetSQL; end; - TSQLBatch = TObjectList; // Threading stuff TQueryThread = class(TThread) @@ -100,8 +116,6 @@ type function Explode(Separator, Text: String) :TStringList; procedure ExplodeQuotedList(Text: String; var List: TStringList); function getEnumValues(str: String): String; - function GetSQLSplitMarkers(const SQL: String): TSQLBatch; - function SplitSQL(const SQL: String): TSQLBatch; function sstr(str: String; len: Integer) : String; function encrypt(str: String): String; function decrypt(str: String): String; @@ -289,109 +303,6 @@ begin end; -function GetSQLSplitMarkers(const SQL: String): TSQLBatch; -var - i, AllLen, DelimLen, DelimStart, LastLeftOffset, RightOffset, LastNewLineOffset: Integer; - c, n, LastStringEncloser: Char; - Delim, DelimTest, QueryTest: String; - InString, InComment, InBigComment, InEscape: Boolean; - Marker: TSQLSentence; - rx: TRegExpr; -const - StringEnclosers = ['"', '''', '`']; - NewLines = [#13, #10]; - WhiteSpaces = NewLines + [#9, ' ']; -begin - // Scan SQL batch for delimiters and return a list with start + end offsets - AllLen := Length(SQL); - i := 0; - LastLeftOffset := 1; - Delim := Mainform.Delimiter; - InString := False; // Loop in "enclosed string" or `identifier` - InComment := False; // Loop in one-line comment (# or --) - InBigComment := False; // Loop in /* multi-line */ or /*! condictional comment */ - InEscape := False; // Previous char was backslash - LastStringEncloser := #0; - DelimLen := Length(Delim); - Result := TSQLBatch.Create; - rx := TRegExpr.Create; - rx.Expression := '^\s*DELIMITER\s+(\S+)'; - rx.ModifierG := True; - rx.ModifierI := True; - rx.ModifierM := False; - while i < AllLen do begin - Inc(i); - // Current and next char - c := SQL[i]; - if i < AllLen then n := SQL[i+1] - else n := #0; - - // Check for comment syntax and for enclosed literals, so a query delimiter can be ignored - if (not InComment) and (not InBigComment) and (not InString) and ((c + n = '--') or (c = '#')) then - InComment := True; - if (not InComment) and (not InBigComment) and (not InString) and (c + n = '/*') then - InBigComment := True; - if InBigComment and (not InComment) and (not InString) and (c + n = '*/') then - InBigComment := False; - if (not InEscape) and (not InComment) and (not InBigComment) and CharInSet(c, StringEnclosers) then begin - if (not InString) or (InString and (c = LastStringEncloser)) then begin - InString := not InString; - LastStringEncloser := c; - end; - end; - if (CharInSet(c, NewLines) and (not CharInSet(n, NewLines))) or (i = 1) then begin - if i > 1 then - InComment := False; - if (not InString) and (not InBigComment) and rx.Exec(copy(SQL, i, 100)) then begin - Delim := rx.Match[1]; - DelimLen := rx.MatchLen[1]; - Inc(i, rx.MatchLen[0]); - LastLeftOffset := i; - continue; - end; - end; - if not InEscape then - InEscape := c = '\' - else - InEscape := False; - - // Prepare delimiter test string - if (not InComment) and (not InString) and (not InBigComment) then begin - DelimStart := Max(1, i+1-DelimLen); - DelimTest := Copy(SQL, DelimStart, i-Max(i-DelimLen, 0)); - end else - DelimTest := ''; - - // End of query or batch reached. Add query markers to result list if sentence is not empty. - if (DelimTest = Delim) or (i = AllLen) then begin - RightOffset := i+1; - if DelimTest = Delim then - Dec(RightOffset, DelimLen); - QueryTest := Trim(Copy(SQL, LastLeftOffset, RightOffset-LastLeftOffset)); - if (QueryTest <> '') and (QueryTest <> Delim) then begin - Marker := TSQLSentence.Create; - while CharInSet(SQL[LastLeftOffset], WhiteSpaces) do - Inc(LastLeftOffset); - Marker.LeftOffset := LastLeftOffset; - Marker.RightOffset := RightOffset; - Result.Add(Marker); - LastLeftOffset := i+1; - end; - end; - end; -end; - - -function SplitSQL(const SQL: String): TSQLBatch; -var - Query: TSQLSentence; -begin - // Return a list of queries tokenized from a big string. Replaces earlier parseSQL() method. - Result := GetSQLSplitMarkers(SQL); - for Query in Result do - Query.SQL := Copy(SQL, Query.LeftOffset, Query.RightOffset-Query.LeftOffset); -end; - {*** @@ -2813,6 +2724,135 @@ begin end; +{ TSQLSentence } + +constructor TSQLSentence.Create(Owner: TSQLBatch); +begin + // Use a back reference to the parent batch object, so we can extract SQL from it + FOwner := Owner; +end; + + +function TSQLSentence.GetSize: Integer; +begin + Result := RightOffset - LeftOffset; +end; + + +function TSQLSentence.GetSQL: String; +begin + Result := Copy(FOwner.SQL, LeftOffset, RightOffset-LeftOffset); +end; + + +{ TSQLBatch } + +function TSQLBatch.GetSize: Integer; +var + Query: TSQLSentence; +begin + // Return overall string length of batch + Result := 0; + for Query in Self do + Inc(Result, Query.Size); +end; + + +procedure TSQLBatch.SetSQL(Value: String); +var + i, AllLen, DelimLen, DelimStart, LastLeftOffset, RightOffset, LastNewLineOffset: Integer; + c, n, LastStringEncloser: Char; + Delim, DelimTest, QueryTest: String; + InString, InComment, InBigComment, InEscape: Boolean; + Marker: TSQLSentence; + rx: TRegExpr; +const + StringEnclosers = ['"', '''', '`']; + NewLines = [#13, #10]; + WhiteSpaces = NewLines + [#9, ' ']; +begin + // Scan SQL batch for delimiters and store a list with start + end offsets + FSQL := Value; + Clear; + AllLen := Length(FSQL); + i := 0; + LastLeftOffset := 1; + Delim := Mainform.Delimiter; + InString := False; // Loop in "enclosed string" or `identifier` + InComment := False; // Loop in one-line comment (# or --) + InBigComment := False; // Loop in /* multi-line */ or /*! condictional comment */ + InEscape := False; // Previous char was backslash + LastStringEncloser := #0; + DelimLen := Length(Delim); + rx := TRegExpr.Create; + rx.Expression := '^\s*DELIMITER\s+(\S+)'; + rx.ModifierG := True; + rx.ModifierI := True; + rx.ModifierM := False; + while i < AllLen do begin + Inc(i); + // Current and next char + c := FSQL[i]; + if i < AllLen then n := FSQL[i+1] + else n := #0; + + // Check for comment syntax and for enclosed literals, so a query delimiter can be ignored + if (not InComment) and (not InBigComment) and (not InString) and ((c + n = '--') or (c = '#')) then + InComment := True; + if (not InComment) and (not InBigComment) and (not InString) and (c + n = '/*') then + InBigComment := True; + if InBigComment and (not InComment) and (not InString) and (c + n = '*/') then + InBigComment := False; + if (not InEscape) and (not InComment) and (not InBigComment) and CharInSet(c, StringEnclosers) then begin + if (not InString) or (InString and (c = LastStringEncloser)) then begin + InString := not InString; + LastStringEncloser := c; + end; + end; + if (CharInSet(c, NewLines) and (not CharInSet(n, NewLines))) or (i = 1) then begin + if i > 1 then + InComment := False; + if (not InString) and (not InBigComment) and rx.Exec(copy(FSQL, i, 100)) then begin + Delim := rx.Match[1]; + DelimLen := rx.MatchLen[1]; + Inc(i, rx.MatchLen[0]); + LastLeftOffset := i; + continue; + end; + end; + if not InEscape then + InEscape := c = '\' + else + InEscape := False; + + // Prepare delimiter test string + if (not InComment) and (not InString) and (not InBigComment) then begin + DelimStart := Max(1, i+1-DelimLen); + DelimTest := Copy(FSQL, DelimStart, i-Max(i-DelimLen, 0)); + end else + DelimTest := ''; + + // End of query or batch reached. Add query markers to result list if sentence is not empty. + if (DelimTest = Delim) or (i = AllLen) then begin + RightOffset := i+1; + if DelimTest = Delim then + Dec(RightOffset, DelimLen); + QueryTest := Trim(Copy(FSQL, LastLeftOffset, RightOffset-LastLeftOffset)); + if (QueryTest <> '') and (QueryTest <> Delim) then begin + Marker := TSQLSentence.Create(Self); + while CharInSet(FSQL[LastLeftOffset], WhiteSpaces) do + Inc(LastLeftOffset); + Marker.LeftOffset := LastLeftOffset; + Marker.RightOffset := RightOffset; + Add(Marker); + LastLeftOffset := i+1; + end; + end; + end; +end; + + + end. diff --git a/source/main.pas b/source/main.pas index b76ce714..f578e70a 100644 --- a/source/main.pas +++ b/source/main.pas @@ -2199,9 +2199,8 @@ end; procedure TMainForm.actExecuteQueryExecute(Sender: TObject); var Query: TSQLSentence; - Text, LB: String; ProfileNode: PVirtualNode; - Batch: TSQLBatch; + BatchAll, Batch: TSQLBatch; Tab: TQueryTab; begin @@ -2210,32 +2209,25 @@ begin OperationRunning(True); ShowStatusMsg('Splitting SQL queries ...'); + Batch := TSQLBatch.Create; if Sender = actExecuteCurrentQuery then begin - Batch := GetSQLSplitMarkers(Tab.Memo.Text); - for Query in Batch do begin + BatchAll := TSQLBatch.Create; + BatchAll.SQL := Tab.Memo.Text; + for Query in BatchAll do begin if (Tab.Memo.SelStart >= Query.LeftOffset-1) and (Tab.Memo.SelStart < Query.RightOffset) then begin - Text := Copy(Tab.Memo.Text, Query.LeftOffset, Query.RightOffset-Query.LeftOffset); + Batch.SQL := Query.SQL; Tab.LeftOffsetInMemo := Query.LeftOffset; break; end; end; + BatchAll.Free; end else if Sender = actExecuteSelection then begin - Text := Tab.Memo.SelText; + Batch.SQL := Tab.Memo.SelText; Tab.LeftOffsetInMemo := Tab.Memo.SelStart; end else begin - Text := Tab.Memo.Text; + Batch.SQL := Tab.Memo.Text; Tab.LeftOffsetInMemo := 0; end; - // Give text back its original linebreaks if possible - case Tab.MemoLineBreaks of - lbsUnix: LB := LB_UNIX; - lbsMac: LB := LB_MAC; - lbsWide: LB := LB_WIDE; - end; - if LB <> '' then - Text := StringReplace(Text, CRLF, LB, [rfReplaceAll]); - Batch := SplitSQL(Text); - Text := ''; EnableProgress(Batch.Count); Tab.ResultTabs.Clear; @@ -2985,10 +2977,10 @@ end; } function TMainform.InitConnection(Params: TConnectionParameters; ActivateMe: Boolean; var Connection: TDBConnection): Boolean; var - i: Integer; SessionExists, RestoreLastActiveDatabase: Boolean; - StartupScript, StartupSQL, LastActiveDatabase: String; + StartupScript, LastActiveDatabase: String; StartupBatch: TSQLBatch; + Query: TSQLSentence; SessionNode, DBNode: PVirtualNode; begin Connection := Params.CreateConnection(Self); @@ -3051,10 +3043,10 @@ begin if not FileExists(StartupScript) then ErrorDialog('Startup script file not found: '+StartupScript) else begin - StartupSQL := ReadTextfile(StartupScript, nil); - StartupBatch := SplitSQL(StartupSQL); - for i:=0 to StartupBatch.Count-1 do try - Connection.Query(StartupBatch[i].SQL); + StartupBatch := TSQLBatch.Create; + StartupBatch.SQL := ReadTextfile(StartupScript, nil); + for Query in StartupBatch do try + Connection.Query(Query.SQL); except // Suppress popup, errors get logged into SQL log end; @@ -4820,7 +4812,7 @@ var Attri: TSynHighlighterAttributes; Proposal: TSynCompletionProposal; Editor: TCustomSynEdit; - QueryMarkers: TSQLBatch; + Queries: TSQLBatch; Query: TSQLSentence; Conn: TDBConnection; RoutineEditor: TfrmRoutineEditor; @@ -4921,14 +4913,15 @@ begin sql := 'SELECT * FROM '+ActiveDbObj.QuotedName+' WHERE ' + Editor.Text; end else begin // In a query tab - QueryMarkers := GetSQLSplitMarkers(Editor.Text); - for Query in QueryMarkers do begin + Queries := TSQLBatch.Create; + Queries.SQL := Editor.Text; + for Query in Queries do begin if (Query.LeftOffset <= Editor.SelStart) and (Editor.SelStart < Query.RightOffset) then begin - sql := Copy(Editor.Text, Query.LeftOffset, Query.RightOffset-Query.LeftOffset); + sql := Query.SQL; break; end; end; - FreeAndNil(QueryMarkers); + Queries.Free; end; // 2. Parse FROM clause, detect relevant table/view, probably aliased diff --git a/source/runsqlfile.pas b/source/runsqlfile.pas index 212fd27b..8347496e 100644 --- a/source/runsqlfile.pas +++ b/source/runsqlfile.pas @@ -48,15 +48,12 @@ uses } procedure TRunSQLFileForm.FormActivate(Sender: TObject); var - Stream : TFileStream; - lines : String; - filesize, - querycount, - rowsaffected : Int64; - starttime : Cardinal; - SQL : TSQLBatch; - i : Integer; - lines_remaining : String; + Stream: TFileStream; + lines, lines_remaining: String; + filesize, querycount, rowsaffected: Int64; + starttime: Cardinal; + Queries: TSQLBatch; + i: Integer; begin if Running then abort; @@ -71,6 +68,7 @@ begin rowsaffected := 0; lines_remaining := ''; starttime := GetTickCount; + Queries := TSQLBatch.Create; Screen.Cursor := crHourGlass; @@ -100,17 +98,17 @@ begin Repaint; // Split buffer into single queries - SQL := SplitSQL( lines_remaining + lines ); + Queries.SQL := lines_remaining + lines; lines := ''; lines_remaining := ''; // Execute detected queries - for i := 0 to SQL.Count - 1 do + for i:=0 to Queries.Count-1 do begin // Last line has to be processed in next loop if end of file is not reached - if (i = SQL.Count-1) and (Stream.Position < Stream.Size) then + if (i = Queries.Count-1) and (Stream.Position < Stream.Size) then begin - lines_remaining := SQL[i].SQL; + lines_remaining := Queries[i].SQL; break; end; @@ -119,19 +117,19 @@ begin lblQueryCountValue.Caption := FormatNumber( querycount ); // Display part of query - memoQueryValue.Text := sstr( SQL[i].SQL, 100 ); + memoQueryValue.Text := sstr(Queries[i].SQL, 100 ); // Time lblTimeValue.Caption := FormatTimeNumber((GetTickCount - starttime) DIV 1000, True); // Execute single query and display affected rows - Mainform.ActiveConnection.Query(SQL[i].SQL); + Mainform.ActiveConnection.Query(Queries[i].SQL); rowsaffected := rowsaffected + Mainform.ActiveConnection.RowsAffected; lblAffectedRowsValue.Caption := FormatNumber( rowsaffected ); Repaint; end; - SQL.Free; + Queries.Free; end; Stream.Free; diff --git a/source/table_editor.pas b/source/table_editor.pas index c6321cd1..c53efb0d 100644 --- a/source/table_editor.pas +++ b/source/table_editor.pas @@ -454,8 +454,7 @@ end; function TfrmTableEditor.ComposeAlterStatement: TSQLBatch; var Specs: TStringList; - ColSpec, IndexSQL: String; - Query: TSQLSentence; + ColSpec, IndexSQL, SQL: String; i: Integer; Results: TDBQuery; Col, PreviousCol: PTableColumn; @@ -465,19 +464,16 @@ var procedure AddQuery; begin if Specs.Count > 0 then begin - Query := TSQLSentence.Create; - Query.SQL := 'ALTER TABLE '+DBObject.Connection.QuoteIdent(DBObject.Name) + CRLF + #9 + ImplodeStr(',' + CRLF + #9, Specs); - Query.SQL := Trim(Query.SQL); - Result.Add(Query); + SQL := SQL + Trim('ALTER TABLE '+DBObject.Connection.QuoteIdent(DBObject.Name) + CRLF + #9 + ImplodeStr(',' + CRLF + #9, Specs)) + ';' + CRLF; Specs.Clear; end; end; begin // Compose ALTER query, called by buttons and for SQL code tab Mainform.ShowStatusMsg('Composing ALTER statement ...'); - Result := TSQLBatch.Create; Screen.Cursor := crHourglass; Specs := TStringList.Create; + SQL := ''; // Special case for altered foreign keys: These have to be dropped in a seperate query // otherwise the server would return error 121 "Duplicate key on write or update" @@ -631,6 +627,9 @@ begin AddQuery; + Result := TSQLBatch.Create; + Result.SQL := SQL; + FreeAndNil(Specs); Mainform.ShowStatusMsg; MainForm.DisableProgress; @@ -643,18 +642,14 @@ var i, IndexCount: Integer; Col: PTableColumn; Node: PVirtualNode; - tmp: String; - Query: TSQLSentence; + tmp, SQL: String; begin // Compose CREATE query, called by buttons and for SQL code tab - Result := TSQLBatch.Create; - Query := TSQLSentence.Create; - Result.Add(Query); - Query.SQL := 'CREATE TABLE '+DBObject.Connection.QuoteIdent(editName.Text)+' ('+CRLF; + SQL := 'CREATE TABLE '+DBObject.Connection.QuoteIdent(editName.Text)+' ('+CRLF; Node := listColumns.GetFirst; while Assigned(Node) do begin Col := listColumns.GetNodeData(Node); - Query.SQL := Query.SQL + #9 + Col.SQLCode + ','+CRLF; + SQL := SQL + #9 + Col.SQLCode + ','+CRLF; Node := listColumns.GetNextSibling(Node); end; @@ -662,43 +657,44 @@ begin for i:=0 to FKeys.Count-1 do begin tmp := FKeys[i].SQLCode; if tmp <> '' then begin - Query.SQL := Query.SQL + #9 + tmp + ','+CRLF; + SQL := SQL + #9 + tmp + ','+CRLF; Inc(IndexCount); end; end; for i:=0 to FForeignKeys.Count-1 do - Query.SQL := Query.SQL + #9 + FForeignKeys[i].SQLCode(True) + ','+CRLF; + SQL := SQL + #9 + FForeignKeys[i].SQLCode(True) + ','+CRLF; if Integer(listColumns.RootNodeCount) + IndexCount + FForeignKeys.Count > 0 then - Delete(Query.SQL, Length(Query.SQL)-2, 3); + Delete(SQL, Length(SQL)-2, 3); - Query.SQL := Query.SQL + CRLF + ')' + CRLF; + SQL := SQL + CRLF + ')' + CRLF; if memoComment.Text <> '' then - Query.SQL := Query.SQL + 'COMMENT='+esc(memoComment.Text) + CRLF; + SQL := SQL + 'COMMENT='+esc(memoComment.Text) + CRLF; if comboCollation.Text <> '' then - Query.SQL := Query.SQL + 'COLLATE='+esc(comboCollation.Text) + CRLF; + SQL := SQL + 'COLLATE='+esc(comboCollation.Text) + CRLF; if comboEngine.Text <> '' then begin if DBObject.Connection.ServerVersionInt < 40018 then - Query.SQL := Query.SQL + 'TYPE='+comboEngine.Text + CRLF + SQL := SQL + 'TYPE='+comboEngine.Text + CRLF else - Query.SQL := Query.SQL + 'ENGINE='+comboEngine.Text + CRLF; + SQL := SQL + 'ENGINE='+comboEngine.Text + CRLF; end; if comboRowFormat.Tag = ModifiedFlag then - Query.SQL := Query.SQL + 'ROW_FORMAT='+comboRowFormat.Text + CRLF; + SQL := SQL + 'ROW_FORMAT='+comboRowFormat.Text + CRLF; if chkChecksum.Checked then - Query.SQL := Query.SQL + 'CHECKSUM='+IntToStr(Integer(chkChecksum.Checked)) + CRLF; + SQL := SQL + 'CHECKSUM='+IntToStr(Integer(chkChecksum.Checked)) + CRLF; if editAutoInc.Text <> '' then - Query.SQL := Query.SQL + 'AUTO_INCREMENT='+editAutoInc.Text + CRLF; + SQL := SQL + 'AUTO_INCREMENT='+editAutoInc.Text + CRLF; if editAvgRowLen.Text <> '' then - Query.SQL := Query.SQL + 'AVG_ROW_LENGTH='+editAvgRowLen.Text + CRLF; + SQL := SQL + 'AVG_ROW_LENGTH='+editAvgRowLen.Text + CRLF; if editMaxRows.Text <> '' then - Query.SQL := Query.SQL + 'MAX_ROWS='+editMaxRows.Text + CRLF; + SQL := SQL + 'MAX_ROWS='+editMaxRows.Text + CRLF; if memoUnionTables.Enabled and (memoUnionTables.Text <> '') then - Query.SQL := Query.SQL + 'UNION=('+memoUnionTables.Text+')' + CRLF; + SQL := SQL + 'UNION=('+memoUnionTables.Text+')' + CRLF; if comboInsertMethod.Enabled and (comboInsertMethod.Text <> '') then - Query.SQL := Query.SQL + 'INSERT_METHOD='+comboInsertMethod.Text + CRLF; - Query.SQL := Trim(Query.SQL); + SQL := SQL + 'INSERT_METHOD='+comboInsertMethod.Text + CRLF; + Result := TSQLBatch.Create; + Result.SQL := Trim(SQL); end;