From 5d13d0677b32ff51a59aae0e66ef07885b8b9cc1 Mon Sep 17 00:00:00 2001 From: Ansgar Becker Date: Fri, 8 Oct 2010 20:16:53 +0000 Subject: [PATCH] The 3 procedures Parse(Table|View|Routine)Structure() already do some connection specific stuff, and now even more, so they're moved to TMySQLConnection now. In order to display the right collation even if only the character set was found in a CREATE TABLE code, the default collation per charset is detected via CollationTable. See http://www.heidisql.com/forum.php?t=6348 . --- source/copytable.pas | 4 +- source/helpers.pas | 384 +--------------------------------- source/loaddata.pas | 4 +- source/main.pas | 14 +- source/mysql_connection.pas | 406 +++++++++++++++++++++++++++++++++++- source/routine_editor.pas | 2 +- source/table_editor.pas | 4 +- source/tabletools.pas | 6 +- source/view.pas | 2 +- 9 files changed, 422 insertions(+), 404 deletions(-) diff --git a/source/copytable.pas b/source/copytable.pas index 8f66749c..87c518c7 100644 --- a/source/copytable.pas +++ b/source/copytable.pas @@ -126,8 +126,8 @@ begin FKeys.Clear; FForeignKeys.Clear; case FDBObj.NodeType of - lntTable: ParseTableStructure(FDBObj.CreateCode, FColumns, FKeys, FForeignKeys); - lntView: ParseViewStructure(FDBObj.CreateCode, FDBObj.Name, FColumns, Algorithm, CheckOption, SelectCode); + lntTable: FDBObj.Connection.ParseTableStructure(FDBObj.CreateCode, FColumns, FKeys, FForeignKeys); + lntView: FDBObj.Connection.ParseViewStructure(FDBObj.CreateCode, FDBObj.Name, FColumns, Algorithm, CheckOption, SelectCode); else raise Exception.Create('Neither table nor view: '+FDBObj.Name); end; diff --git a/source/helpers.pas b/source/helpers.pas index a85090bc..526fffc9 100644 --- a/source/helpers.pas +++ b/source/helpers.pas @@ -10,7 +10,7 @@ interface uses Classes, SysUtils, Graphics, GraphUtil, ClipBrd, Dialogs, Forms, Controls, ComCtrls, ShellApi, CheckLst, - Windows, Contnrs, ShlObj, ActiveX, VirtualTrees, SynRegExpr, Messages, WideStrUtils, Math, + Windows, Contnrs, ShlObj, ActiveX, VirtualTrees, SynRegExpr, Messages, Math, Registry, SynEditHighlighter, DateUtils, Generics.Collections, StrUtils, AnsiStrings, TlHelp32, Types, mysql_connection, mysql_structures; @@ -141,10 +141,6 @@ type function DateBackFriendlyCaption(d: TDateTime): String; procedure InheritFont(AFont: TFont); function GetLightness(AColor: TColor): Byte; - procedure ParseTableStructure(CreateTable: String; Columns: TTableColumnList; Keys: TTableKeyList; ForeignKeys: TForeignKeyList); - procedure ParseViewStructure(CreateCode, ViewName: String; Columns: TTableColumnList; var Algorithm, CheckOption, SelectCode: String); - procedure ParseRoutineStructure(CreateCode: String; Parameters: TRoutineParamList; - var Deterministic: Boolean; var Returns, DataAccess, Security, Comment, Body: String); function ReformatSQL(SQL: String): String; function ParamBlobToStr(lpData: Pointer): TStringlist; function ParamStrToBlob(out cbData: DWORD): Pointer; @@ -2192,384 +2188,6 @@ begin end; -procedure ParseTableStructure(CreateTable: String; Columns: TTableColumnList; Keys: TTableKeyList; ForeignKeys: TForeignKeyList); -var - ColSpec: String; - rx, rxCol: TRegExpr; - i: Integer; - InLiteral: Boolean; - Col: TTableColumn; - Key: TTableKey; - ForeignKey: TForeignKey; -begin - if Assigned(Columns) then Columns.Clear; - if Assigned(Keys) then Keys.Clear; - if Assigned(ForeignKeys) then ForeignKeys.Clear; - if CreateTable = '' then - Exit; - rx := TRegExpr.Create; - rx.ModifierS := False; - rx.ModifierM := True; - rx.Expression := '^\s+[`"]([^`"]+)[`"]\s(\w+)'; - rxCol := TRegExpr.Create; - rxCol.ModifierI := True; - if rx.Exec(CreateTable) then while true do begin - if not Assigned(Columns) then - break; - ColSpec := ''; - for i:=rx.MatchPos[2]+rx.MatchLen[2] to Length(CreateTable) do begin - if CharInSet(CreateTable[i], [#13, #10]) then - break; - ColSpec := ColSpec + CreateTable[i]; - end; - - // Strip trailing comma - if (ColSpec <> '') and (ColSpec[Length(ColSpec)] = ',') then - Delete(ColSpec, Length(ColSpec), 1); - - Col := TTableColumn.Create; - Columns.Add(Col); - Col.Name := rx.Match[1]; - Col.OldName := Col.Name; - Col.Status := esUntouched; - Col.LengthCustomized := True; - - // Datatype - Col.DataType := GetDatatypeByName(UpperCase(rx.Match[2])); - - // Length / Set - // Various datatypes, e.g. BLOBs, don't have any length property - InLiteral := False; - if (ColSpec <> '') and (ColSpec[1] = '(') then begin - for i:=2 to Length(ColSpec) do begin - if (ColSpec[i] = ')') and (not InLiteral) then - break; - if ColSpec[i] = '''' then - InLiteral := not InLiteral; - end; - Col.LengthSet := Copy(ColSpec, 2, i-2); - Delete(ColSpec, 1, i); - end; - ColSpec := Trim(ColSpec); - - // Unsigned - if UpperCase(Copy(ColSpec, 1, 8)) = 'UNSIGNED' then begin - Col.Unsigned := True; - Delete(ColSpec, 1, 9); - end else - Col.Unsigned := False; - - // Zero fill - if UpperCase(Copy(ColSpec, 1, 8)) = 'ZEROFILL' then begin - Col.ZeroFill := True; - Delete(ColSpec, 1, 9); - end else - Col.ZeroFill := False; - - // Collation - rxCol.Expression := '^(CHARACTER SET \w+\s+)?COLLATE (\w+)\b'; - if rxCol.Exec(ColSpec) then begin - Col.Collation := rxCol.Match[2]; - Delete(ColSpec, 1, rxCol.MatchLen[0]+1); - end; - - // Allow NULL - if UpperCase(Copy(ColSpec, 1, 8)) = 'NOT NULL' then begin - Col.AllowNull := False; - Delete(ColSpec, 1, 9); - end else begin - Col.AllowNull := True; - // Sporadically there is a "NULL" found at this position. - if UpperCase(Copy(ColSpec, 1, 4)) = 'NULL' then - Delete(ColSpec, 1, 5); - end; - - // Default value - Col.DefaultType := cdtNothing; - Col.DefaultText := ''; - if UpperCase(Copy(ColSpec, 1, 14)) = 'AUTO_INCREMENT' then begin - Col.DefaultType := cdtAutoInc; - Col.DefaultText := 'AUTO_INCREMENT'; - Delete(ColSpec, 1, 15); - end else if UpperCase(Copy(ColSpec, 1, 8)) = 'DEFAULT ' then begin - Delete(ColSpec, 1, 8); - if UpperCase(Copy(ColSpec, 1, 4)) = 'NULL' then begin - Col.DefaultType := cdtNull; - Col.DefaultText := 'NULL'; - Delete(ColSpec, 1, 5); - end else if UpperCase(Copy(ColSpec, 1, 17)) = 'CURRENT_TIMESTAMP' then begin - Col.DefaultType := cdtCurTS; - Col.DefaultText := 'CURRENT_TIMESTAMP'; - Delete(ColSpec, 1, 18); - end else if ColSpec[1] = '''' then begin - InLiteral := True; - for i:=2 to Length(ColSpec) do begin - if ColSpec[i] = '''' then - InLiteral := not InLiteral - else if not InLiteral then - break; - end; - Col.DefaultType := cdtText; - Col.DefaultText := Copy(ColSpec, 2, i-3); - // A single quote gets escaped by single quote - remove the escape char - escaping is done in Save action afterwards - Col.DefaultText := StringReplace(Col.DefaultText, '''''', '''', [rfReplaceAll]); - Delete(ColSpec, 1, i); - end; - end; - if UpperCase(Copy(ColSpec, 1, 27)) = 'ON UPDATE CURRENT_TIMESTAMP' then begin - // Adjust default type - case Col.DefaultType of - cdtText: Col.DefaultType := cdtTextUpdateTS; - cdtNull: Col.DefaultType := cdtNullUpdateTS; - cdtCurTS: Col.DefaultType := cdtCurTSUpdateTS; - end; - Delete(ColSpec, 1, 28); - end; - - // Comment - if UpperCase(Copy(ColSpec, 1, 9)) = 'COMMENT ''' then begin - InLiteral := True; - for i:=10 to Length(ColSpec) do begin - if ColSpec[i] = '''' then - InLiteral := not InLiteral - else if not InLiteral then - break; - end; - Col.Comment := Copy(ColSpec, 10, i-11); - Col.Comment := StringReplace(Col.Comment, '''''', '''', [rfReplaceAll]); - Delete(ColSpec, 1, i); - end; - - if not rx.ExecNext then - break; - end; - - // Detect keys - // PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`), KEY `id_2` (`id`) USING BTREE, - // KEY `Text` (`Text`(100)), FULLTEXT KEY `Email` (`Email`,`Text`) - rx.Expression := '^\s+((\w+)\s+)?KEY\s+([`"]?([^`"]+)[`"]?\s+)?\((.+)\)(\s+USING\s+(\w+))?,?$'; - if rx.Exec(CreateTable) then while true do begin - if not Assigned(Keys) then - break; - Key := TTableKey.Create; - Keys.Add(Key); - Key.Name := rx.Match[4]; - if Key.Name = '' then Key.Name := rx.Match[2]; // PRIMARY - Key.OldName := Key.Name; - Key.IndexType := rx.Match[2]; - Key.OldIndexType := Key.IndexType; - Key.Algorithm := rx.Match[7]; - if Key.IndexType = '' then Key.IndexType := 'KEY'; // KEY - Key.Columns := Explode(',', rx.Match[5]); - for i:=0 to Key.Columns.Count-1 do begin - rxCol.Expression := '^[`"]?([^`"]+)[`"]?(\((\d+)\))?$'; - if rxCol.Exec(Key.Columns[i]) then begin - Key.Columns[i] := rxCol.Match[1]; - Key.SubParts.Add(rxCol.Match[3]); - end; - end; - if not rx.ExecNext then - break; - end; - - // Detect foreign keys - // CONSTRAINT `FK1` FOREIGN KEY (`which`) REFERENCES `fk1` (`id`) ON DELETE SET NULL ON UPDATE CASCADE - rx.Expression := '\s+CONSTRAINT\s+[`"]([^`"]+)[`"]\sFOREIGN KEY\s+\(([^\)]+)\)\s+REFERENCES\s+[`"]([^\(]+)[`"]\s\(([^\)]+)\)(\s+ON DELETE (RESTRICT|CASCADE|SET NULL|NO ACTION))?(\s+ON UPDATE (RESTRICT|CASCADE|SET NULL|NO ACTION))?'; - if rx.Exec(CreateTable) then while true do begin - if not Assigned(ForeignKeys) then - break; - ForeignKey := TForeignKey.Create; - ForeignKeys.Add(ForeignKey); - ForeignKey.KeyName := rx.Match[1]; - ForeignKey.OldKeyName := ForeignKey.KeyName; - ForeignKey.KeyNameWasCustomized := True; - ForeignKey.ReferenceTable := StringReplace(rx.Match[3], '`', '', [rfReplaceAll]); - ForeignKey.ReferenceTable := StringReplace(ForeignKey.ReferenceTable, '"', '', [rfReplaceAll]); - ExplodeQuotedList(rx.Match[2], ForeignKey.Columns); - ExplodeQuotedList(rx.Match[4], ForeignKey.ForeignColumns); - if rx.Match[6] <> '' then - ForeignKey.OnDelete := rx.Match[6]; - if rx.Match[8] <> '' then - ForeignKey.OnUpdate := rx.Match[8]; - if not rx.ExecNext then - break; - end; - - FreeAndNil(rxCol); - FreeAndNil(rx); -end; - - -procedure ParseViewStructure(CreateCode, ViewName: String; Columns: TTableColumnList; var Algorithm, CheckOption, SelectCode: String); -var - rx: TRegExpr; - Col: TTableColumn; - Results: TMySQLQuery; - DbName, DbAndViewName: String; -begin - if CreateCode <> '' then begin - // CREATE - // [OR REPLACE] - // [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] - // [DEFINER = { user | CURRENT_USER }] - // [SQL SECURITY { DEFINER | INVOKER }] - // VIEW view_name [(column_list)] - // AS select_statement - // [WITH [CASCADED | LOCAL] CHECK OPTION] - rx := TRegExpr.Create; - rx.ModifierG := False; - rx.ModifierI := True; - rx.Expression := '^CREATE\s+(OR\s+REPLACE\s+)?'+ - '(ALGORITHM\s*=\s*(\w+)\s+)?'+ - '(DEFINER\s*=\s*\S+\s+)?'+ - '(SQL\s+SECURITY\s+\w+\s+)?'+ - 'VIEW\s+(`?(\w[\w\s]*)`?\.)?(`?(\w[\w\s]*)`?)?\s+'+ - '(\([^\)]\)\s+)?'+ - 'AS\s+(.+)(\s+WITH\s+(\w+\s+)?CHECK\s+OPTION\s*)?$'; - if rx.Exec(CreateCode) then begin - Algorithm := rx.Match[3]; - // When exporting a view we need the db name for the below SHOW COLUMNS query, - // if the connection is on a different db currently - DbName := rx.Match[7]; - ViewName := rx.Match[9]; - CheckOption := Trim(rx.Match[13]); - SelectCode := rx.Match[11]; - end else - raise Exception.Create('Regular expression did not match the VIEW code in ParseViewStructure(): '+CRLF+CRLF+CreateCode); - rx.Free; - end; - - // Views reveal their columns only with a SHOW COLUMNS query. - // No keys available in views - SHOW KEYS always returns an empty result - if Assigned(Columns) then begin - Columns.Clear; - rx := TRegExpr.Create; - rx.Expression := '^(\w+)(\((.+)\))?'; - if DbName <> '' then - DbAndViewName := MainForm.mask(DbName)+'.'; - DbAndViewName := DbAndViewName + MainForm.mask(ViewName); - Results := MainForm.ActiveConnection.GetResults('SHOW /*!32332 FULL */ COLUMNS FROM '+DbAndViewName); - while not Results.Eof do begin - Col := TTableColumn.Create; - Columns.Add(Col); - Col.Name := Results.Col('Field'); - Col.AllowNull := Results.Col('Null') = 'YES'; - if rx.Exec(Results.Col('Type')) then begin - Col.DataType := GetDatatypeByName(rx.Match[1]); - Col.LengthSet := rx.Match[3]; - end; - Col.Unsigned := (Col.DataType.Category = dtcInteger) and (Pos('unsigned', Results.Col('Type')) > 0); - Col.AllowNull := UpperCase(Results.Col('Null')) = 'YES'; - Col.Collation := Results.Col('Collation', True); - Col.Comment := Results.Col('Comment', True); - if Col.DataType.Category <> dtcTemporal then - Col.DefaultText := Results.Col('Default'); - if Results.IsNull('Default') and Col.AllowNull then - Col.DefaultType := cdtNull - else if Col.DataType.Index = dtTimestamp then - Col.DefaultType := cdtCurTSUpdateTS - else if (Col.DefaultText = '') and Col.AllowNull then - Col.DefaultType := cdtNothing - else - Col.DefaultType := cdtText; - Results.Next; - end; - rx.Free; - end; -end; - - -procedure ParseRoutineStructure(CreateCode: String; Parameters: TRoutineParamList; - var Deterministic: Boolean; var Returns, DataAccess, Security, Comment, Body: String); -var - Params: String; - ParenthesesCount: Integer; - rx: TRegExpr; - i: Integer; - Param: TRoutineParam; -begin - // Parse CREATE code of stored function or procedure to detect parameters - 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; - Params := ''; - for i:=1 to Length(CreateCode) do begin - if CreateCode[i] = ')' then begin - Dec(ParenthesesCount); - if ParenthesesCount = 0 then - break; - end; - if ParenthesesCount >= 1 then - Params := Params + CreateCode[i]; - if CreateCode[i] = '(' then - Inc(ParenthesesCount); - end; - rx.Expression := '(^|,)\s*((IN|OUT|INOUT)\s+)?(\S+)\s+([^\s,\(]+(\([^\)]*\))?[^,]*)'; - if rx.Exec(Params) then while true do begin - Param := TRoutineParam.Create; - Param.Context := UpperCase(rx.Match[3]); - if Param.Context = '' then - Param.Context := 'IN'; - Param.Name := WideDequotedStr(rx.Match[4], '`'); - Param.Datatype := rx.Match[5]; - Parameters.Add(Param); - if not rx.ExecNext then - break; - end; - - // Cut left part including parameters, so it's easier to parse the rest - CreateCode := Copy(CreateCode, 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(CreateCode) then - Delete(CreateCode, rx.MatchPos[0], rx.MatchLen[0]); - rx.Expression := '\bRETURNS\s+(\w+(\([^\)]*\))?(\s+UNSIGNED)?)'; - if rx.Exec(CreateCode) then begin - Returns := rx.Match[1]; - Delete(CreateCode, rx.MatchPos[0], rx.MatchLen[0]); - end; - rx.Expression := '\b(NOT\s+)?DETERMINISTIC\b'; - if rx.Exec(CreateCode) then begin - Deterministic := rx.MatchLen[1] = -1; - Delete(CreateCode, rx.MatchPos[0], rx.MatchLen[0]); - end; - rx.Expression := '\b(CONTAINS SQL|NO SQL|READS SQL DATA|MODIFIES SQL DATA)\b'; - if rx.Exec(CreateCode) then begin - DataAccess := rx.Match[1]; - Delete(CreateCode, rx.MatchPos[0], rx.MatchLen[0]); - end; - rx.Expression := '\bSQL\s+SECURITY\s+(DEFINER|INVOKER)\b'; - if rx.Exec(CreateCode) then begin - Security := rx.Match[1]; - Delete(CreateCode, rx.MatchPos[0], rx.MatchLen[0]); - end; - rx.ModifierG := False; - rx.Expression := '\bCOMMENT\s+''((.+)[^''])''[^'']'; - if rx.Exec(CreateCode) then begin - Comment := StringReplace(rx.Match[1], '''''', '''', [rfReplaceAll]); - Delete(CreateCode, rx.MatchPos[0], rx.MatchLen[0]-1); - end; - rx.Expression := '^\s*CHARSET\s+[\w\d]+\s'; - if rx.Exec(CreateCode) then - Delete(CreateCode, rx.MatchPos[0], rx.MatchLen[0]-1); - // Tata, remaining code is the routine body - Body := TrimLeft(CreateCode); - - rx.Free; -end; - - function ReformatSQL(SQL: String): String; var AllKeywords, ImportantKeywords, PairKeywords: TStringList; diff --git a/source/loaddata.pas b/source/loaddata.pas index b096b043..1c99b88d 100644 --- a/source/loaddata.pas +++ b/source/loaddata.pas @@ -234,8 +234,8 @@ begin for Obj in DBObjects do begin if (Obj.Database=comboDatabase.Text) and (Obj.Name=comboTable.Text) then begin case Obj.NodeType of - lntTable: ParseTableStructure(Obj.CreateCode, Columns, nil, nil); - lntView: ParseViewStructure(Obj.CreateCode, Obj.Name, Columns, Algorithm, CheckOption, SelectCode); + lntTable: Obj.Connection.ParseTableStructure(Obj.CreateCode, Columns, nil, nil); + lntView: Obj.Connection.ParseViewStructure(Obj.CreateCode, Obj.Name, Columns, Algorithm, CheckOption, SelectCode); end; end; end; diff --git a/source/main.pas b/source/main.pas index a848d0f2..4cc5eea5 100644 --- a/source/main.pas +++ b/source/main.pas @@ -2968,7 +2968,7 @@ begin lntFunction: Query := 'SELECT '; end; Parameters := TRoutineParamList.Create; - ParseRoutineStructure(Obj.CreateCode, Parameters, Deterministic, Returns, DataAccess, Security, Comment, Body); + Obj.Connection.ParseRoutineStructure(Obj.CreateCode, Parameters, Deterministic, Returns, DataAccess, Security, Comment, Body); Query := Query + mask(Obj.Name); ParamInput := ''; for i:=0 to Parameters.Count-1 do begin @@ -4664,7 +4664,7 @@ begin for DbObj in DbObjects do begin if (CompareText(DbObj.Name, Identifier)=0) and (DbObj.NodeType in [lntFunction, lntProcedure]) then begin Params := TRoutineParamList.Create(True); - ParseRoutineStructure(DbObj.CreateCode, Params, DummyBool, DummyStr, DummyStr, DummyStr, DummyStr, DummyStr); + DbObj.Connection.ParseRoutineStructure(DbObj.CreateCode, Params, DummyBool, DummyStr, DummyStr, DummyStr, DummyStr, DummyStr); ItemText := ''; for i:=0 to Params.Count-1 do ItemText := ItemText + '"' + Params[i].Name + ': ' + Params[i].Datatype + '", '; @@ -6542,7 +6542,7 @@ begin lntTable, lntView: if Assigned(SelectDBObjectForm) and (Sender=SelectDBObjectForm.TreeDBO) then begin Columns := TTableColumnList.Create(True); - ParseTableStructure(DBObj.CreateCode, Columns, nil, nil); + DBObj.Connection.ParseTableStructure(DBObj.CreateCode, Columns, nil, nil); ChildCount := Columns.Count; end; end; @@ -6585,7 +6585,7 @@ begin Item.NodeType := lntColumn; ParentItem := Sender.GetNodeData(Node.Parent); Columns := TTableColumnList.Create(True); - ParseTableStructure(ParentItem.CreateCode, Columns, nil, nil); + ParentItem.Connection.ParseTableStructure(ParentItem.CreateCode, Columns, nil, nil); Item.Name := Columns[Node.Index].Name; end; end; @@ -6760,9 +6760,9 @@ begin try case ActiveDbObj.NodeType of lntTable: - ParseTableStructure(ActiveDbObj.CreateCode, SelectedTableColumns, SelectedTableKeys, SelectedTableForeignKeys); + ActiveConnection.ParseTableStructure(ActiveDbObj.CreateCode, SelectedTableColumns, SelectedTableKeys, SelectedTableForeignKeys); lntView: - ParseViewStructure(ActiveDbObj.CreateCode, ActiveDbObj.Name, SelectedTableColumns, Algorithm, CheckOption, SelectCode); + ActiveConnection.ParseViewStructure(ActiveDbObj.CreateCode, ActiveDbObj.Name, SelectedTableColumns, Algorithm, CheckOption, SelectCode); end; except on E:EDatabaseError do MessageDlg(E.Message, mtError, [mbOK], 0); @@ -7314,7 +7314,7 @@ begin Columns := TTableColumnList.Create; Keys := nil; ForeignKeys := nil; - ParseTableStructure(CreateTable, Columns, Keys, ForeignKeys); + ActiveConnection.ParseTableStructure(CreateTable, Columns, Keys, ForeignKeys); TextCol := ''; for TblColumn in Columns do begin if (TblColumn.DataType.Category = dtcText) and (TblColumn.Name <> ForeignKey.ForeignColumns[idx]) then begin diff --git a/source/mysql_connection.pas b/source/mysql_connection.pas index f9a8de83..985cda29 100644 --- a/source/mysql_connection.pas +++ b/source/mysql_connection.pas @@ -66,7 +66,7 @@ type Unsigned, AllowNull, ZeroFill, LengthCustomized: Boolean; DefaultType: TColumnDefaultType; DefaultText: String; - Comment, Collation: String; + Comment, Charset, Collation: String; FStatus: TEditingStatus; constructor Create; destructor Destroy; override; @@ -273,6 +273,10 @@ type function GetLastResults: TMySQLQueryList; procedure ClearDbObjects(db: String); procedure ClearAllDbObjects; + procedure ParseTableStructure(CreateTable: String; Columns: TTableColumnList; Keys: TTableKeyList; ForeignKeys: TForeignKeyList); + procedure ParseViewStructure(CreateCode, ViewName: String; Columns: TTableColumnList; var Algorithm, CheckOption, SelectCode: String); + procedure ParseRoutineStructure(CreateCode: String; Parameters: TRoutineParamList; + var Deterministic: Boolean; var Returns, DataAccess, Security, Comment, Body: String); property SessionName: String read FSessionName write FSessionName; property Parameters: TConnectionParameters read FParameters write FParameters; property ThreadId: Cardinal read GetThreadId; @@ -1579,6 +1583,402 @@ begin end; +procedure TMySQLConnection.ParseTableStructure(CreateTable: String; Columns: TTableColumnList; Keys: TTableKeyList; ForeignKeys: TForeignKeyList); +var + ColSpec: String; + rx, rxCol: TRegExpr; + i: Integer; + InLiteral: Boolean; + Col: TTableColumn; + Key: TTableKey; + ForeignKey: TForeignKey; + Collations: TMySQLQuery; +begin + if Assigned(Columns) then Columns.Clear; + if Assigned(Keys) then Keys.Clear; + if Assigned(ForeignKeys) then ForeignKeys.Clear; + if CreateTable = '' then + Exit; + rx := TRegExpr.Create; + rx.ModifierS := False; + rx.ModifierM := True; + rx.Expression := '^\s+[`"]([^`"]+)[`"]\s(\w+)'; + rxCol := TRegExpr.Create; + rxCol.ModifierI := True; + if rx.Exec(CreateTable) then while true do begin + if not Assigned(Columns) then + break; + ColSpec := ''; + for i:=rx.MatchPos[2]+rx.MatchLen[2] to Length(CreateTable) do begin + if CharInSet(CreateTable[i], [#13, #10]) then + break; + ColSpec := ColSpec + CreateTable[i]; + end; + + // Strip trailing comma + if (ColSpec <> '') and (ColSpec[Length(ColSpec)] = ',') then + Delete(ColSpec, Length(ColSpec), 1); + + Col := TTableColumn.Create; + Columns.Add(Col); + Col.Name := rx.Match[1]; + Col.OldName := Col.Name; + Col.Status := esUntouched; + Col.LengthCustomized := True; + + // Datatype + Col.DataType := GetDatatypeByName(UpperCase(rx.Match[2])); + + // Length / Set + // Various datatypes, e.g. BLOBs, don't have any length property + InLiteral := False; + if (ColSpec <> '') and (ColSpec[1] = '(') then begin + for i:=2 to Length(ColSpec) do begin + if (ColSpec[i] = ')') and (not InLiteral) then + break; + if ColSpec[i] = '''' then + InLiteral := not InLiteral; + end; + Col.LengthSet := Copy(ColSpec, 2, i-2); + Delete(ColSpec, 1, i); + end; + ColSpec := Trim(ColSpec); + + // Unsigned + if UpperCase(Copy(ColSpec, 1, 8)) = 'UNSIGNED' then begin + Col.Unsigned := True; + Delete(ColSpec, 1, 9); + end else + Col.Unsigned := False; + + // Zero fill + if UpperCase(Copy(ColSpec, 1, 8)) = 'ZEROFILL' then begin + Col.ZeroFill := True; + Delete(ColSpec, 1, 9); + end else + Col.ZeroFill := False; + + // Charset + rxCol.Expression := '^CHARACTER SET (\w+)\b\s*'; + if rxCol.Exec(ColSpec) then begin + Col.Charset := rxCol.Match[1]; + Delete(ColSpec, 1, rxCol.MatchLen[0]); + end; + + // Collation - probably not present when charset present + rxCol.Expression := '^COLLATE (\w+)\b\s*'; + if rxCol.Exec(ColSpec) then begin + Col.Collation := rxCol.Match[1]; + Delete(ColSpec, 1, rxCol.MatchLen[0]); + end; + if Col.Collation = '' then begin + Collations := CollationTable; + if Assigned(Collations) then while not Collations.Eof do begin + if (Collations.Col('Charset') = Col.Charset) and (Collations.Col('Default') = 'Yes') then begin + Col.Collation := Collations.Col('Collation'); + break; + end; + Collations.Next; + end; + end; + + // Allow NULL + if UpperCase(Copy(ColSpec, 1, 8)) = 'NOT NULL' then begin + Col.AllowNull := False; + Delete(ColSpec, 1, 9); + end else begin + Col.AllowNull := True; + // Sporadically there is a "NULL" found at this position. + if UpperCase(Copy(ColSpec, 1, 4)) = 'NULL' then + Delete(ColSpec, 1, 5); + end; + + // Default value + Col.DefaultType := cdtNothing; + Col.DefaultText := ''; + if UpperCase(Copy(ColSpec, 1, 14)) = 'AUTO_INCREMENT' then begin + Col.DefaultType := cdtAutoInc; + Col.DefaultText := 'AUTO_INCREMENT'; + Delete(ColSpec, 1, 15); + end else if UpperCase(Copy(ColSpec, 1, 8)) = 'DEFAULT ' then begin + Delete(ColSpec, 1, 8); + if UpperCase(Copy(ColSpec, 1, 4)) = 'NULL' then begin + Col.DefaultType := cdtNull; + Col.DefaultText := 'NULL'; + Delete(ColSpec, 1, 5); + end else if UpperCase(Copy(ColSpec, 1, 17)) = 'CURRENT_TIMESTAMP' then begin + Col.DefaultType := cdtCurTS; + Col.DefaultText := 'CURRENT_TIMESTAMP'; + Delete(ColSpec, 1, 18); + end else if ColSpec[1] = '''' then begin + InLiteral := True; + for i:=2 to Length(ColSpec) do begin + if ColSpec[i] = '''' then + InLiteral := not InLiteral + else if not InLiteral then + break; + end; + Col.DefaultType := cdtText; + Col.DefaultText := Copy(ColSpec, 2, i-3); + // A single quote gets escaped by single quote - remove the escape char - escaping is done in Save action afterwards + Col.DefaultText := StringReplace(Col.DefaultText, '''''', '''', [rfReplaceAll]); + Delete(ColSpec, 1, i); + end; + end; + if UpperCase(Copy(ColSpec, 1, 27)) = 'ON UPDATE CURRENT_TIMESTAMP' then begin + // Adjust default type + case Col.DefaultType of + cdtText: Col.DefaultType := cdtTextUpdateTS; + cdtNull: Col.DefaultType := cdtNullUpdateTS; + cdtCurTS: Col.DefaultType := cdtCurTSUpdateTS; + end; + Delete(ColSpec, 1, 28); + end; + + // Comment + if UpperCase(Copy(ColSpec, 1, 9)) = 'COMMENT ''' then begin + InLiteral := True; + for i:=10 to Length(ColSpec) do begin + if ColSpec[i] = '''' then + InLiteral := not InLiteral + else if not InLiteral then + break; + end; + Col.Comment := Copy(ColSpec, 10, i-11); + Col.Comment := StringReplace(Col.Comment, '''''', '''', [rfReplaceAll]); + Delete(ColSpec, 1, i); + end; + + if not rx.ExecNext then + break; + end; + + // Detect keys + // PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`), KEY `id_2` (`id`) USING BTREE, + // KEY `Text` (`Text`(100)), FULLTEXT KEY `Email` (`Email`,`Text`) + rx.Expression := '^\s+((\w+)\s+)?KEY\s+([`"]?([^`"]+)[`"]?\s+)?\((.+)\)(\s+USING\s+(\w+))?,?$'; + if rx.Exec(CreateTable) then while true do begin + if not Assigned(Keys) then + break; + Key := TTableKey.Create; + Keys.Add(Key); + Key.Name := rx.Match[4]; + if Key.Name = '' then Key.Name := rx.Match[2]; // PRIMARY + Key.OldName := Key.Name; + Key.IndexType := rx.Match[2]; + Key.OldIndexType := Key.IndexType; + Key.Algorithm := rx.Match[7]; + if Key.IndexType = '' then Key.IndexType := 'KEY'; // KEY + Key.Columns := Explode(',', rx.Match[5]); + for i:=0 to Key.Columns.Count-1 do begin + rxCol.Expression := '^[`"]?([^`"]+)[`"]?(\((\d+)\))?$'; + if rxCol.Exec(Key.Columns[i]) then begin + Key.Columns[i] := rxCol.Match[1]; + Key.SubParts.Add(rxCol.Match[3]); + end; + end; + if not rx.ExecNext then + break; + end; + + // Detect foreign keys + // CONSTRAINT `FK1` FOREIGN KEY (`which`) REFERENCES `fk1` (`id`) ON DELETE SET NULL ON UPDATE CASCADE + rx.Expression := '\s+CONSTRAINT\s+[`"]([^`"]+)[`"]\sFOREIGN KEY\s+\(([^\)]+)\)\s+REFERENCES\s+[`"]([^\(]+)[`"]\s\(([^\)]+)\)(\s+ON DELETE (RESTRICT|CASCADE|SET NULL|NO ACTION))?(\s+ON UPDATE (RESTRICT|CASCADE|SET NULL|NO ACTION))?'; + if rx.Exec(CreateTable) then while true do begin + if not Assigned(ForeignKeys) then + break; + ForeignKey := TForeignKey.Create; + ForeignKeys.Add(ForeignKey); + ForeignKey.KeyName := rx.Match[1]; + ForeignKey.OldKeyName := ForeignKey.KeyName; + ForeignKey.KeyNameWasCustomized := True; + ForeignKey.ReferenceTable := StringReplace(rx.Match[3], '`', '', [rfReplaceAll]); + ForeignKey.ReferenceTable := StringReplace(ForeignKey.ReferenceTable, '"', '', [rfReplaceAll]); + ExplodeQuotedList(rx.Match[2], ForeignKey.Columns); + ExplodeQuotedList(rx.Match[4], ForeignKey.ForeignColumns); + if rx.Match[6] <> '' then + ForeignKey.OnDelete := rx.Match[6]; + if rx.Match[8] <> '' then + ForeignKey.OnUpdate := rx.Match[8]; + if not rx.ExecNext then + break; + end; + + FreeAndNil(rxCol); + FreeAndNil(rx); +end; + + +procedure TMySQLConnection.ParseViewStructure(CreateCode, ViewName: String; Columns: TTableColumnList; var Algorithm, CheckOption, SelectCode: String); +var + rx: TRegExpr; + Col: TTableColumn; + Results: TMySQLQuery; + DbName, DbAndViewName: String; +begin + if CreateCode <> '' then begin + // CREATE + // [OR REPLACE] + // [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] + // [DEFINER = { user | CURRENT_USER }] + // [SQL SECURITY { DEFINER | INVOKER }] + // VIEW view_name [(column_list)] + // AS select_statement + // [WITH [CASCADED | LOCAL] CHECK OPTION] + rx := TRegExpr.Create; + rx.ModifierG := False; + rx.ModifierI := True; + rx.Expression := '^CREATE\s+(OR\s+REPLACE\s+)?'+ + '(ALGORITHM\s*=\s*(\w+)\s+)?'+ + '(DEFINER\s*=\s*\S+\s+)?'+ + '(SQL\s+SECURITY\s+\w+\s+)?'+ + 'VIEW\s+(`?(\w[\w\s]*)`?\.)?(`?(\w[\w\s]*)`?)?\s+'+ + '(\([^\)]\)\s+)?'+ + 'AS\s+(.+)(\s+WITH\s+(\w+\s+)?CHECK\s+OPTION\s*)?$'; + if rx.Exec(CreateCode) then begin + Algorithm := rx.Match[3]; + // When exporting a view we need the db name for the below SHOW COLUMNS query, + // if the connection is on a different db currently + DbName := rx.Match[7]; + ViewName := rx.Match[9]; + CheckOption := Trim(rx.Match[13]); + SelectCode := rx.Match[11]; + end else + raise Exception.Create('Regular expression did not match the VIEW code in ParseViewStructure(): '+CRLF+CRLF+CreateCode); + rx.Free; + end; + + // Views reveal their columns only with a SHOW COLUMNS query. + // No keys available in views - SHOW KEYS always returns an empty result + if Assigned(Columns) then begin + Columns.Clear; + rx := TRegExpr.Create; + rx.Expression := '^(\w+)(\((.+)\))?'; + if DbName <> '' then + DbAndViewName := EscapeString(DbName)+'.'; + DbAndViewName := DbAndViewName + EscapeString(ViewName); + Results := GetResults('SHOW /*!32332 FULL */ COLUMNS FROM '+DbAndViewName); + while not Results.Eof do begin + Col := TTableColumn.Create; + Columns.Add(Col); + Col.Name := Results.Col('Field'); + Col.AllowNull := Results.Col('Null') = 'YES'; + if rx.Exec(Results.Col('Type')) then begin + Col.DataType := GetDatatypeByName(rx.Match[1]); + Col.LengthSet := rx.Match[3]; + end; + Col.Unsigned := (Col.DataType.Category = dtcInteger) and (Pos('unsigned', Results.Col('Type')) > 0); + Col.AllowNull := UpperCase(Results.Col('Null')) = 'YES'; + Col.Collation := Results.Col('Collation', True); + Col.Comment := Results.Col('Comment', True); + if Col.DataType.Category <> dtcTemporal then + Col.DefaultText := Results.Col('Default'); + if Results.IsNull('Default') and Col.AllowNull then + Col.DefaultType := cdtNull + else if Col.DataType.Index = dtTimestamp then + Col.DefaultType := cdtCurTSUpdateTS + else if (Col.DefaultText = '') and Col.AllowNull then + Col.DefaultType := cdtNothing + else + Col.DefaultType := cdtText; + Results.Next; + end; + rx.Free; + end; +end; + + +procedure TMySQLConnection.ParseRoutineStructure(CreateCode: String; Parameters: TRoutineParamList; + var Deterministic: Boolean; var Returns, DataAccess, Security, Comment, Body: String); +var + Params: String; + ParenthesesCount: Integer; + rx: TRegExpr; + i: Integer; + Param: TRoutineParam; +begin + // Parse CREATE code of stored function or procedure to detect parameters + 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; + Params := ''; + for i:=1 to Length(CreateCode) do begin + if CreateCode[i] = ')' then begin + Dec(ParenthesesCount); + if ParenthesesCount = 0 then + break; + end; + if ParenthesesCount >= 1 then + Params := Params + CreateCode[i]; + if CreateCode[i] = '(' then + Inc(ParenthesesCount); + end; + rx.Expression := '(^|,)\s*((IN|OUT|INOUT)\s+)?(\S+)\s+([^\s,\(]+(\([^\)]*\))?[^,]*)'; + if rx.Exec(Params) then while true do begin + Param := TRoutineParam.Create; + Param.Context := UpperCase(rx.Match[3]); + if Param.Context = '' then + Param.Context := 'IN'; + Param.Name := DeQuoteIdent(rx.Match[4]); + Param.Datatype := rx.Match[5]; + Parameters.Add(Param); + if not rx.ExecNext then + break; + end; + + // Cut left part including parameters, so it's easier to parse the rest + CreateCode := Copy(CreateCode, 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(CreateCode) then + Delete(CreateCode, rx.MatchPos[0], rx.MatchLen[0]); + rx.Expression := '\bRETURNS\s+(\w+(\([^\)]*\))?(\s+UNSIGNED)?)'; + if rx.Exec(CreateCode) then begin + Returns := rx.Match[1]; + Delete(CreateCode, rx.MatchPos[0], rx.MatchLen[0]); + end; + rx.Expression := '\b(NOT\s+)?DETERMINISTIC\b'; + if rx.Exec(CreateCode) then begin + Deterministic := rx.MatchLen[1] = -1; + Delete(CreateCode, rx.MatchPos[0], rx.MatchLen[0]); + end; + rx.Expression := '\b(CONTAINS SQL|NO SQL|READS SQL DATA|MODIFIES SQL DATA)\b'; + if rx.Exec(CreateCode) then begin + DataAccess := rx.Match[1]; + Delete(CreateCode, rx.MatchPos[0], rx.MatchLen[0]); + end; + rx.Expression := '\bSQL\s+SECURITY\s+(DEFINER|INVOKER)\b'; + if rx.Exec(CreateCode) then begin + Security := rx.Match[1]; + Delete(CreateCode, rx.MatchPos[0], rx.MatchLen[0]); + end; + rx.ModifierG := False; + rx.Expression := '\bCOMMENT\s+''((.+)[^''])''[^'']'; + if rx.Exec(CreateCode) then begin + Comment := StringReplace(rx.Match[1], '''''', '''', [rfReplaceAll]); + Delete(CreateCode, rx.MatchPos[0], rx.MatchLen[0]-1); + end; + rx.Expression := '^\s*CHARSET\s+[\w\d]+\s'; + if rx.Exec(CreateCode) then + Delete(CreateCode, rx.MatchPos[0], rx.MatchLen[0]-1); + // Tata, remaining code is the routine body + Body := TrimLeft(CreateCode); + + rx.Free; +end; + + { TMySQLQuery } @@ -1957,9 +2357,9 @@ begin FForeignKeys := TForeignKeyList.Create; // This is probably a VIEW, so column names need to be fetched differently if UpperCase(Res.ColumnNames[0]) = 'TABLE' then - ParseTableStructure(CreateCode, FColumns, FKeys, FForeignKeys) + Connection.ParseTableStructure(CreateCode, FColumns, FKeys, FForeignKeys) else - ParseViewStructure(CreateCode, TableName, FColumns, Algorithm, CheckOption, SelectCode); + Connection.ParseViewStructure(CreateCode, TableName, FColumns, Algorithm, CheckOption, SelectCode); FreeAndNil(Res); FreeAndNil(FUpdateData); FUpdateData := TUpdateData.Create(True); diff --git a/source/routine_editor.pas b/source/routine_editor.pas index 62255819..6c79f80d 100644 --- a/source/routine_editor.pas +++ b/source/routine_editor.pas @@ -147,7 +147,7 @@ begin if DBObject.Name <> '' then begin // Editing existing routine comboType.ItemIndex := ListIndexByRegExpr(comboType.Items, '^'+FAlterRoutineType+'\b'); - ParseRoutineStructure(DBObject.CreateCode, Parameters, Deterministic, Returns, DataAccess, Security, Comment, Body); + DBObject.Connection.ParseRoutineStructure(DBObject.CreateCode, Parameters, Deterministic, Returns, DataAccess, Security, Comment, Body); comboReturns.Text := Returns; chkDeterministic.Checked := Deterministic; if DataAccess <> '' then diff --git a/source/table_editor.pas b/source/table_editor.pas index de1b3a70..e6374a67 100644 --- a/source/table_editor.pas +++ b/source/table_editor.pas @@ -326,7 +326,7 @@ begin memoComment.Lines.Text := MainForm.ActiveConnection.UnescapeString(rx.Match[1]) else memoComment.Lines.Clear; - ParseTableStructure(DBObject.CreateCode, FColumns, FKeys, FForeignKeys); + DBObject.Connection.ParseTableStructure(DBObject.CreateCode, FColumns, FKeys, FForeignKeys); end; listColumns.RootNodeCount := FColumns.Count; DeInitializeVTNodes(listColumns); @@ -2147,7 +2147,7 @@ var Col: TTableColumn; begin Columns := TTableColumnList.Create(False); - ParseTableStructure(Clipboard.AsText, Columns, nil, nil); + DBObject.Connection.ParseTableStructure(Clipboard.AsText, Columns, nil, nil); Node := listColumns.FocusedNode; if not Assigned(Node) then Node := listColumns.GetLast; diff --git a/source/tabletools.pas b/source/tabletools.pas index ba1f8b59..459d8fe1 100644 --- a/source/tabletools.pas +++ b/source/tabletools.pas @@ -556,8 +556,8 @@ begin ResultSQL := ''; Columns := TTableColumnList.Create(True); case DBObj.NodeType of - lntTable: ParseTableStructure(DBObj.CreateCode, Columns, nil, nil); - lntView: ParseViewStructure(DBObj.CreateCode, DBObj.Name, Columns, Dummy, Dummy, Dummy); + lntTable: DBObj.Connection.ParseTableStructure(DBObj.CreateCode, Columns, nil, nil); + lntView: DBObj.Connection.ParseViewStructure(DBObj.CreateCode, DBObj.Name, Columns, Dummy, Dummy, Dummy); else AddNotes(DBObj.Database, DBObj.Name, STRSKIPPED+'a '+LowerCase(DBObj.ObjType)+' does not contain rows.', ''); end; if Columns.Count > 0 then begin @@ -1109,7 +1109,7 @@ begin if not FSecondExportPass then begin // Create temporary VIEW replacement ColumnList := TTableColumnList.Create(True); - ParseViewStructure(DBObj.CreateCode, DBObj.Name, ColumnList, Dummy, Dummy, Dummy); + DBObj.Connection.ParseViewStructure(DBObj.CreateCode, DBObj.Name, ColumnList, Dummy, Dummy, Dummy); Struc := '# Creating temporary table to overcome VIEW dependency errors'+CRLF+ 'CREATE TABLE '; if ToDb then diff --git a/source/view.pas b/source/view.pas index 14be15ac..22bbfbe7 100644 --- a/source/view.pas +++ b/source/view.pas @@ -66,7 +66,7 @@ begin if Obj.Name <> '' then begin // Edit mode editName.Text := Obj.Name; - ParseViewStructure(Obj.CreateCode, Obj.Name, nil, Algorithm, CheckOption, SelectCode); + Obj.Connection.ParseViewStructure(Obj.CreateCode, Obj.Name, nil, Algorithm, CheckOption, SelectCode); rgAlgorithm.ItemIndex := rgAlgorithm.Items.IndexOf(Algorithm); rgCheck.ItemIndex := rgCheck.Items.IndexOf(CheckOption); if rgCheck.ItemIndex = -1 then